第9章:I/O
# 第9章:I/O
本章包含以下内容:
- 文件与设备(Files and Devices)
- 文件与设备API(File and Device API)
- 文件信息(File Information)
- NTFS流(NTFS Streams)
- 扩展属性(Extended Attributes)
- 设备访问(Accessing Devices)
- I/O完成端口(I/O Completion Ports)
- 其他函数(Miscellaneous Functions)
# 9.1 文件与设备(Files and Devices)
文件对象(file object)是对与设备对象(device object)之间连接的抽象。设备对象可表示物理设备(如硬盘、键盘)或虚拟设备(如管道,pipe)。设备对象负责数据的实际读写操作,文件对象则为用户提供了与设备对象交互的便捷接口。
创建文件对象的本质是建立与设备的连接。用户模式(user-mode)无法直接打开设备对象,Windows API函数CreateFile是创建文件对象的主要接口。CreateFile中的“创建(create)”并非特指在文件系统中创建文件——文件系统中的文件源自某个磁盘设备;其核心是创建文件对象本身,这是一个表示与设备对象之间连接的内核结构(kernel structure)。对设备对象的所有访问都必须通过文件对象进行。图9-1展示了文件对象与设备对象的关系,可见多个文件对象可与同一个设备对象通信。

图9-1:文件对象与设备对象
用户熟悉的驱动器名称(如“C:”“D:”等)本质上都是符号链接(symbolic link)——这是我们在第7章介绍过的一种对象类型。可通过Windows API函数QueryDosDevice,根据符号链接获取目标设备名称。示例如下:
WCHAR buffer[256];
QueryDosDevice(L"c:", buffer, _countof(buffer));
2
在我的系统中,运行结果为\Device\HarddiskVolume3,这就是表示C盘的设备对象。可通过Sysinternals的WinObj工具或我自己开发的Object Explorer工具,查看CreateFile Windows API可直接访问的符号链接。图9-2展示了Object Explorer中的“Global??”对象管理器目录(Object Manager directory),其中包含了可用于CreateFile的符号链接。
除驱动器号和某些旧DOS名称(如CON)外,传递给
CreateFile的符号链接必须以\\.\为前缀,这样它们才会被解释为符号链接,而非文件名。
QueryDosDevice在底层调用了NtQuerySymbolicLinkObject函数。
图9-2:Global??对象管理器目录
与CreateFile不同,原生API(native API)允许直接访问对象管理器命名空间(Object Manager namespace)中的任何对象。
# 9.2 文件与设备API(File and Device API)
用于访问设备的原生API是NtCreateFile和NtOpenFile:
NTSTATUS NtCreateFile(
_Out_ PHANDLE FileHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_opt_ PLARGE_INTEGER AllocationSize,
_In_ ULONG FileAttributes,
_In_ ULONG ShareAccess,
_In_ ULONG CreateDisposition,
_In_ ULONG CreateOptions,
_In_reads_bytes_opt_(EaLength) PVOID EaBuffer,
_In_ ULONG EaLength);
NTSTATUS NtOpenFile(
_Out_ PHANDLE FileHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ ULONG ShareAccess,
_In_ ULONG OpenOptions);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
幸运的是,这些API在WDK(Windows驱动开发工具包)中已正式文档化(以ZwCreateFile和ZwOpenFile为名),因为设备驱动开发者在I/O操作中需要用到这些API。NtOpenFile相对简单,仅提供NtCreateFile的部分功能。在内部,两者都会调用内核中的一个公共函数(IopCreateFile),NtOpenFile会传递一些可通过NtCreateFile配置的默认值。
CreateFile Windows API在底层调用了NtCreateFile,这一点可通过用户模式调试器(user-mode debugger)轻松观察到。
NtOpenFile的大多数参数如今应该已为人熟知。ObjectAttributes用于指定设备名称,IoStatusBlock是输出参数,包含以下详情:
typedef struct _IO_STATUS_BLOCK {
union {
NTSTATUS Status;
PVOID Pointer;
};
ULONG_PTR Information;
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;
2
3
4
5
6
7
Information包含以下值之一,指示所执行的操作类型。这对文件系统中的真实文件/目录有效,但对设备无效(设备必须在尝试打开之前已存在,打开操作才能成功):
FILE_CREATED(2)——新文件已创建。FILE_OPENED(1)——现有文件已打开。FILE_OVERWRITTEN(3)——现有文件已覆盖。FILE_SUPERSEDED(0)——现有文件已替代(即文件被替换)。FILE_EXISTS(4)——找到现有文件。FILE_DOES_NOT_EXIST(5)——未找到现有文件。
DesiredAccess是请求的访问掩码(access mask),可包含文件对象的特定访问掩码(如FILE_READ_DATA)、文件通用访问掩码(FILE_GENERIC_READ),甚至对象通用访问掩码(如GENERIC_READ);这与其他对象类型在原则上并无不同。
ShareAccess是打开/创建文件的共享模式(share mode),仅在实际创建新文件时才有意义。其值包括FILE_SHARE_READ、FILE_SHARE_WRITE和FILE_SHARE_DELETE。最后,OpenOptions允许指定更多标志以自定义操作,此类标志数量众多,可在WDK的ZwCreateFile文档中查看。
以下示例从System32目录打开Kernel32.dll文件:
UNICODE_STRING path;
RtlInitUnicodeString(&path, L"\\SystemRoot\\System32\\kernel32.dll");
OBJECT_ATTRIBUTES attr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&path, 0);
HANDLE hFile;
IO_STATUS_BLOCK ioStatus;
status = NtOpenFile(&hFile, FILE_READ_DATA, &attr, &ioStatus,
FILE_SHARE_READ, 0);
2
3
4
5
6
7
为什么这样能生效?SystemRoot是一个符号链接,指向Windows安装目录的根目录——从用户视角来看通常是C:\Windows,但在底层其路径形式有所不同。通过Object Explorer或第7章开发的ObjDir工具可以看到,SystemRoot是一个指向Device\BootDevice\Window的符号链接。那么Device\BootDevice是什么?进入Device目录并找到BootDevice,会发现它也是一个符号链接,指向Device\HarddiskVolume3。查看\Global??\C:可知,它指向同一个设备,即该机器上的C盘。
注:请以管理员权限运行WinObj或ObjExp工具,以查看上述符号链接的目标。
若要使用常见的用户模式路径(如c:\Something),需在路径前添加\??\。例如,c:\Something应写为\??\c:\Something;\??\与\Global??\等价。 |
|---|
当然,我们也可以通过Device\HarddiskVolume3\Windows直接访问C:\Windows目录,但该设备在不同Windows系统上可能不同。因此,使用符号链接更为有利——它们在所有Windows系统上的形式都是一致的。
上述文件打开示例看似简单,但实际并非如此。假设我们要读取文件的前1024字节,可使用NtReadFile API:
typedef void (NTAPI *PIO_APC_ROUTINE)(
_In_ PVOID ApcContext,
_In_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ ULONG Reserved);
NTSTATUS NtReadFile(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_(Length) PVOID Buffer,
_In_ ULONG Length,
_In_opt_ PLARGE_INTEGER ByteOffset,
_In_opt_ PULONG Key);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FileHandle参数是NtOpenFile或NtCreateFile返回的句柄。Event是可选的事件句柄(event handle),在操作完成时发出信号。ApcRoutine是可选的异步过程调用回调函数(APC callback),在I/O操作完成时,会在调用线程上构造一个异步过程调用(APC)。若要最终执行APC,线程必须进入可警报状态(alertable state)。ApcContext是传递给APC例程的可选上下文值。Event、ApcRoutine和ApcContext仅在操作是异步(asynchronous)时才有意义,后续将详细说明。
Buffer是用于存储读取数据的缓冲区,Length是要读取的字节数。ByteOffset是可选的读取偏移量,通常设为NULL,表示从当前文件位置开始读取。
Key是用于操作的可选键值,在用户模式中无实际用途。以下是从之前打开的文件中读取前1KB数据的简单代码:
BYTE buffer[1024];
status = NtReadFile(hFile, nullptr, nullptr, nullptr, &ioStatus,
buffer, sizeof(buffer), nullptr, nullptr);
2
3
该调用会莫名失败,返回STATUS_INVALID_PARAMETER。问题出在哪里?原因是文件对象是以异步访问(asynchronous access)方式打开的。我们在调用NtCreateFile时未指定任何特殊参数,但默认情况下是异步访问模式。这与CreateFile函数不同,CreateFile默认创建用于同步访问(synchronous access)的文件对象。
要创建用于同步访问的文件对象,需在NtOpenFile的OpenOptions参数中指定FILE_SYNCHRONOUS_IO_NONALERT或FILE_SYNCHRONOUS_IO_ALERT标志。以下是正确的NtOpenFile调用:
status = NtOpenFile(&hFile, FILE_READ_DATA | SYNCHRONIZE, &attr, &ioStatus,
FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT);
2
注意,访问掩码中包含了SYNCHRONIZE标志。这是必需的,因为同步操作在内部需要等待文件对象,而该标志是等待操作的前提。此时,NtReadFile调用可成功执行,且调用返回时数据已可用。
如何让异步调用生效?在这种情况下,NtReadFile调用必须在ByteOffset参数中指定文件位置。这是因为以异步访问方式打开的文件对象不会跟踪文件位置——考虑到可能有多个操作并发启动,单一的文件位置已无实际意义。此外,应提供一个事件句柄(或APC回调),以便线程判断操作何时完成、数据何时可用。异步操作成功启动时,返回的状态为STATUS_PENDING(0x103)。
以下是异步调用后立即等待的简单示例:
HANDLE hEvent;
NtCreateEvent(&hEvent, EVENT_ALL_ACCESS, nullptr, NotificationEvent, FALSE);
LARGE_INTEGER pos{}; // pos设为0(文件起始位置)
status = NtReadFile(hFile, hEvent, nullptr, nullptr, &ioStatus,
buffer, sizeof(buffer), &pos, nullptr);
if (status != STATUS_PENDING)
{
// 发生实际错误
}
else
{
NtWaitForSingleObject(hEvent, FALSE, nullptr);
// 缓冲区中已包含数据
}
NtClose(hEvent);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在实际场景中,不会在异步调用启动后立即等待——这会违背异步I/O的初衷。相反,线程会执行其他工作,并定期检查操作状态;或由其他线程等待事件;或使用线程池(thread pool)等待事件,并在操作完成时执行回调函数。
注意:对于异步操作,提供的
IO_STATUS_BLOCK和缓冲区必须在操作完成前保持有效。如果它们是栈分配的(stack-allocated),则线程在操作完成前不得退出当前函数。
向文件对象写入数据与读取类似,使用NtWriteFile函数:
NTSTATUS NtWriteFile(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_reads_bytes_(Length) PVOID Buffer,
_In_ ULONG Length,
_In_opt_ PLARGE_INTEGER ByteOffset,
_In_opt_ PULONG Key);
2
3
4
5
6
7
8
9
10
该函数与NtReadFile几乎完全相同,仅在SAL标注(SAL annotations)和Buffer参数可声明为const这两点上存在差异。
有关这些API(包括NtCreateFile)的更多信息,请参考WDK文档。
# 9.3 文件信息(File Information)
文件和目录信息首先可通过NtQueryInformationFile和NtSetInformationFile函数获取,这两个函数遵循我们之前见过的经典模型:
NTSTATUS NtQueryInformationFile(
_In_ HANDLE FileHandle,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_ (Length) PVOID FileInformation,
_In_ ULONG Length,
_In_ FILE_INFORMATION_CLASS FileInformationClass);
NTSTATUS NtSetInformationFile(
_In_ HANDLE FileHandle,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_reads_bytes_(Length) PVOID FileInformation,
_In_ ULONG Length,
_In_ FILE_INFORMATION_CLASS FileInformationClass);
2
3
4
5
6
7
8
9
10
11
12
13
一个相关的查询函数通过文件名而非句柄(handle)工作:
NTSTATUS NtQueryInformationByName(
_In_ POBJECT_ATTRIBUTES ObjectAttributes,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_ (Length) PVOID FileInformation,
_In_ ULONG Length,
_In_ FILE_INFORMATION_CLASS FileInformationClass);
2
3
4
5
6
正如你可能预期的,文件信息类(FILE_INFORMATION_CLASS)有一长串列表。其中一些仅适用于文件,一些仅适用于目录,还有一些两者都适用。在以下小节中,我们将介绍其中的一部分。
部分信息类及相关结构在Windows驱动开发工具包(WDK)和/或软件开发工具包(SDK)中有文档说明。可在WDK中查找ZwQueryInformationFile,在SDK中查找GetFileInformationByHandleEx。
# 9.3.1 文件基本信息(FileBasicInformation)
该信息类返回文件或目录的基本详情:
typedef struct _FILE_BASIC_INFORMATION {
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
ULONG FileAttributes;
} FILE_BASIC_INFORMATION, *PFILE_BASIC_INFORMATION;
2
3
4
5
6
7
这些成员大多不言自明,其中时间以1601年1月1日以来的100纳秒(100nsec)为单位表示。文件属性(FileAttributes)是一组文件属性标志,例如目录属性(FILE_ATTRIBUTE_DIRECTORY)、隐藏属性(FILE_ATTRIBUTE_HIDDEN)、只读属性(FILE_ATTRIBUTE_READONLY)等。修改时间(ChangeTime)和最后写入时间(LastWriteTime)的区别在于,修改时间(ChangeTime)可能表示文件名或文件属性的更改,而最后写入时间(LastWriteTime)仅指文件内容最后一次被修改的时间。
同一个信息类可用于设置这些属性。文件句柄必须包含文件写入属性访问掩码(FILE_WRITE_ATTRIBUTES)(文件通用写入权限(FILE_GENERIC_WRITE)和通用写入权限(GENERIC_WRITE)已包含该掩码)。
以下示例将文件的创建时间设置为指定值:
NTSTATUS SetCreateTime(PCWSTR filename, LARGE_INTEGER const& createTime)
{
FILE_BASIC_INFORMATION fbi;
UNICODE_STRING name;
RtlInitUnicodeString(&name, filename);
OBJECT_ATTRIBUTES attr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
HANDLE hFile;
IO_STATUS_BLOCK ioStatus;
auto status = NtOpenFile(&hFile, FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES, &attr, &ioStatus, 0 , 0);
if (!NT_SUCCESS(status))
return status;
status = NtQueryInformationFile(hFile, &ioStatus, &fbi,
sizeof(fbi), FileBasicInformation);
if (!NT_SUCCESS(status))
return status;
fbi .CreationTime = createTime;
status = NtSetInformationFile(hFile, &ioStatus, &fbi,
sizeof(fbi), FileBasicInformation);
NtClose(hFile);
return status;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
以下示例将文件的创建时间设置为未来24小时:
LARGE_INTEGER time;
NtQuerySystemTime(&time);
time.QuadPart += 10000000ULL * 60 * 60 * 24;
SetCreateTime(L"\\??\\C:\\Temp\\myfile .txt" , time);
2
3
4
# 9.3.2 文件标准信息(FileStandardInformation)
该信息类返回以下结构:
typedef struct _FILE_STANDARD_INFORMATION {
LARGE_INTEGER AllocationSize;
LARGE_INTEGER EndOfFile;
ULONG NumberOfLinks;
BOOLEAN DeletePending;
BOOLEAN Directory;
} FILE_STANDARD_INFORMATION, *PFILE_STANDARD_INFORMATION;
2
3
4
5
6
7
分配大小(AllocationSize)是文件的分配大小,通常是物理设备底层扇区大小的整数倍。文件末尾(EndOfFile)是文件大小(目录的该值为0)。链接数(NumberOfLinks)是指向该文件的硬链接(hard links)数量。删除挂起(DeletePending)指示文件是否处于待删除状态。例如,如果文件是以关闭时删除标志(FILE_DELETE_ON_CLOSE)打开的。目录标志(Directory)指示该文件是否为目录。
# 9.3.3 文件名称信息(FileNameInformation)
该信息类在文件名称信息结构(FILE_NAME_INFORMATION)中返回文件或目录的完整名称:
typedef struct _FILE_NAME_INFORMATION {
ULONG FileNameLength; // in bytes
WCHAR FileName[1];
} FILE_NAME_INFORMATION, *PFILE_NAME_INFORMATION;
2
3
4
文件名称长度(FileNameLength)是以字节为单位的文件名长度。文件名称(FileName)是文件名本身,不一定以空字符(null)结尾。如果提供的缓冲区不足以容纳完整名称,函数将返回缓冲区溢出状态(STATUS_BUFFER_OVERFLOW),且文件名称长度(FileNameLength)会被设置为所需的大小(至少文件系统驱动程序(file system drivers)和筛选器(filters)应如此处理)。
返回的文件名始终以反斜杠(backslash)开头,不包含驱动器号(drive letter)或卷名(volume name)。例如,对于文件路径C:\Windows\System32\kernel32.dll,返回的文件名为\Windows\System32\kernel32.dll。如果文件是以路径\SystemRoot\System32\kernel32.dll打开的,返回的文件名也相同。
如何获取卷名?请参见下一个信息类。
# 9.3.4 文件卷名信息(FileVolumeNameInformation)
该信息类在文件卷名信息结构(FILE_VOLUME_NAME_INFORMATION)中返回卷设备名(volume device name):
typedef struct _FILE_VOLUME_NAME_INFORMATION {
ULONG DeviceNameLength;
WCHAR DeviceName[1];
} FILE_VOLUME_NAME_INFORMATION, *PFILE_VOLUME_NAME_INFORMATION;
2
3
4
其格式与文件名称信息结构(FILE_NAME_INFORMATION)完全相同。以下是使用该结构的示例:
BYTE buffer[1 << 11];
status = NtQueryInformationFile(hFile, &ioStatus, buffer,
sizeof(buffer), FileVolumeNameInformation);
auto info = (FILE_VOLUME_NAME_INFORMATION*)buffer;
2
3
4
返回值类似\Device\HarddiskVolume3。将其与文件名称信息类(FileNameInformation)结合,可得到设备格式的完整路径名。如果需要驱动器号,需将驱动器号作为符号链接名(symbolic link names)查找,看哪个指向该设备名。这部分内容留给感兴趣的读者自行探索。请注意,此操作可能失败——并非所有卷都映射到驱动器号,不存在这样的强制要求。快速查看映射情况的方法是使用命令行工具fltmc.exe,该工具必须在管理员权限(elevated)的命令提示符下运行。例如(为清晰起见进行了格式调整):
C:\Users\Pavel> fltmc volumes
Dos Name Volume Name FileSystem
----------------------------------------------------------------------
\Device\Mup Remote
D: \Device\HarddiskVolume7 NTFS
C: \Device\HarddiskVolume3 NTFS
\Device\NamedPipe NamedPipe
\Device\Mailslot Mailslot
\Device\HarddiskVolume1 FAT
\Device\HarddiskVolume5 NTFS
F: \Device\HarddiskVolume8 NTFS
\Device\HarddiskVolume4 NTFS
\Device\HarddiskVolumeShadowCopy4 NTFS
\Device\HarddiskVolumeShadowCopy6 NTFS
\Device\HarddiskVolumeShadowCopy7 NTFS
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fltmc.exe工具位于系统32(System32)目录中。
# 9.3.5 文件重命名信息(FileRenameInformation)
该信息类可与NtSetInformationFile结合使用,以重命名或移动文件或目录。所需的结构是文件重命名信息结构(FILE_RENAME_INFORMATION):
typedef struct _FILE_RENAME_INFORMATION {
BOOLEAN ReplaceIfExists;
HANDLE RootDirectory;
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_RENAME_INFORMATION, *PFILE_RENAME_INFORMATION;
2
3
4
5
6
如果存在标志(ReplaceIfExists)指示当新文件名已存在时,是否替换现有文件。根目录(RootDirectory)是新文件应所在目录的句柄。如果根目录(RootDirectory)为NULL,则文件在同一目录中重命名。文件名称长度(FileNameLength)是以字节为单位的新名称长度,文件名称(FileName)是新名称,不一定以空字符结尾。
以下是在同一目录中重命名文件的示例:
NTSTATUS RenameFile(PCWSTR path, PCWSTR newName)
{
UNICODE_STRING name;
RtlInitUnicodeString(&name, path);
OBJECT_ATTRIBUTES attr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
HANDLE hFile;
IO_STATUS_BLOCK ioStatus;
auto status = NtOpenFile(&hFile, DELETE, &attr, &ioStatus, 0 , 0);
if (NT_SUCCESS(status))
{
BYTE buffer[1 << 10];
auto info = (FILE_RENAME_INFORMATION*)buffer;
info->ReplaceIfExists = FALSE;
info->RootDirectory = nullptr;
info->FileNameLength = (ULONG)wcslen(newName) * sizeof(WCHAR);
memcpy(info->FileName, newName, info->FileNameLength);
status = NtSetInformationFile(hFile, &ioStatus, info,
sizeof(buffer), FileRenameInformation);
NtClose(hFile);
}
return status;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
注意:
- 文件必须以删除访问掩码(DELETE access mask)打开。创建选项(CreateOptions)应设为0(而非类似同步非警报I/O标志(FILE_SYNCHRONOUS_IO_NONALERT)的值)。
- 上述代码中提供给
NtSetInformationFile的缓冲区大小大于所需,但这不会有问题。不过,缓冲区应根据新名称的长度动态分配。
以下是使用示例:
RenameFile(L"\\??\\C:\\Temp\\myfile .txt" , L"somefile.txt");
我们可以扩展该示例,通过指定根目录句柄来支持将文件移动到不同目录。可按如下方式打开目录句柄以实现此目的:
HANDLE OpenDirectoryForMoving(PCWSTR path)
{
IO_STATUS_BLOCK ioStatus;
UNICODE_STRING name;
RtlInitUnicodeString(&name, path);
HANDLE hDir = nullptr;
OBJECT_ATTRIBUTES dirAtt = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
NtOpenFile(&hDir, FILE_GENERIC_WRITE, &dirAtt, &ioStatus,
FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_DIRECTORY_ FILE);
return hDir;
}
2
3
4
5
6
7
8
9
10
11
目标目录必须与被移动文件位于同一卷中。
# 9.3.6 文件处置信息(FileDispositionInformation)
该信息类与NtSetInformationFile结合使用,以删除文件或目录。所需的结构是一个简单的布尔值(BOOLEAN):
typedef struct _FILE_DISPOSITION_INFORMATION {
BOOLEAN DeleteFile;
} FILE_DISPOSITION_INFORMATION, *PFILE_DISPOSITION_INFORMATION;
2
3
将删除文件标志(DeleteFile)设为TRUE即可删除文件。文件必须以删除访问掩码(DELETE access mask)打开。以下是删除文件的示例:
NTSTATUS NtDeleteFile(PCWSTR path)
{
UNICODE_STRING name;
RtlInitUnicodeString(&name, path);
OBJECT_ATTRIBUTES attr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
HANDLE hFile;
IO_STATUS_BLOCK ioStatus;
auto status = NtOpenFile(&hFile, DELETE, &attr, &ioStatus, 0 , 0);
if (NT_SUCCESS(status))
{
FILE_DISPOSITION_INFORMATION info{ TRUE };
status = NtSetInformationFile(hFile, &ioStatus, &info,
sizeof(info), FileDispositionInformation);
NtClose(hFile);
}
return status;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
文件实际上会在句柄关闭时被删除。删除文件的一种简单方法是,在打开文件时(作为创建选项(CreateOptions)的一部分)指定关闭时删除标志(FILE_DELETE_ON_CLOSE),然后关闭句柄。
# 9.3.7 文件位置信息(FilePositionInformation)
该信息类可用于查询或设置文件指针(file pointer),以64位有符号整数(LARGE_INTEGER)表示:
typedef struct _FILE_POSITION_INFORMATION {
LARGE_INTEGER CurrentByteOffset;
} FILE_POSITION_INFORMATION, *PFILE_POSITION_INFORMATION;
2
3
# 9.3.8 文件模式信息(FileModeInformation)
该信息类返回文件的打开模式(mode)。返回的结构是文件模式信息结构(FILE_MODE_INFORMATION),本质上是一组标志:
typedef struct _FILE_MODE_INFORMATION {
ULONG Mode;
} FILE_MODE_INFORMATION, *PFILE_MODE_INFORMATION;
2
3
模式(Mode)包含诸如写穿标志(FILE_WRITE_THROUGH)、仅顺序访问标志(FILE_SEQUENTIAL_ONLY)、无中间缓冲标志(FILE_NO_INTERMEDIATE_BUFFERING)、同步警报I/O标志(FILE_SYNCHRONOUS_IO_ALERT)等标志。这些标志可作为创建选项(CreateOptions)的一部分,用于NtCreateFile和NtOpenFile函数。有关更多信息,请参阅Windows驱动开发工具包(WDK)文档。
该信息类也部分支持NtSetInformationFile,尤其适用于修改文件打开时的同步/异步模式(synchronous/asynchronous open)相关设置。
# 9.3.9 文件全部信息(FileAllInformation)
该信息类将我们之前见过的多个结构整合为一个,一次性返回:
typedef struct _FILE_ALL_INFORMATION {
FILE_BASIC_INFORMATION BasicInformation;
FILE_STANDARD_INFORMATION StandardInformation;
FILE_INTERNAL_INFORMATION InternalInformation;
FILE_EA_INFORMATION EaInformation;
FILE_ACCESS_INFORMATION AccessInformation;
FILE_POSITION_INFORMATION PositionInformation;
FILE_MODE_INFORMATION ModeInformation;
FILE_ALIGNMENT_INFORMATION AlignmentInformation;
FILE_NAME_INFORMATION NameInformation;
} FILE_ALL_INFORMATION, *PFILE_ALL_INFORMATION;
2
3
4
5
6
7
8
9
10
11
如果需要获取大部分文件详情,这种方式比多次调用单个信息类函数更高效。请注意,这是一个变长结构(variable length structure),因为最后一个成员是变长字符串(variable-length string)。
# 9.4 仅适用于目录的信息(Directory-Only Information)
有些信息类仅适用于目录,用于获取目录内容相关信息。NtQueryDirectoryFile函数用于获取此类信息:
NTSTATUS NtQueryDirectoryFile(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_ (Length) PVOID FileInformation,
_In_ ULONG Length,
_In_ FILE_INFORMATION_CLASS FileInformationClass,
_In_ BOOLEAN ReturnSingleEntry,
_In_opt_ PUNICODE_STRING FileName,
_In_ BOOLEAN RestartScan);
2
3
4
5
6
7
8
9
10
11
12
文件句柄(FileHandle)必须是目录句柄(在调用NtOpenFile或NtCreateFile时指定了目录文件标志(FILE_DIRECTORY_FILE))。
最后三个参数是新增的。返回单个条目标志(ReturnSingleEntry)指示返回单个条目还是所有条目(请注意这是针对目录的操作)。文件名(FileName)可用于筛选返回的条目。如果指定为NULL,则不进行筛选;如果非NULL,则指示感兴趣的条目,支持通配符(wildcards)。例如,指定Hello.*将返回所有名为“hello”且带有任意扩展名的文件/目录。重启扫描标志(RestartScan)指示是从开头重新扫描还是从最后一个条目继续扫描。当提供的缓冲区不足以容纳所有条目时,此参数非常有用。首次使用特定句柄调用NtQueryDirectoryFile时,重启扫描标志(RestartScan)将被忽略,后续调用时会生效。
以下小节将介绍与目录内容相关的信息类。
# 9.4.1 文件目录信息(FileDirectoryInformation)、文件完整目录信息(FileFullDirectoryInformation)、文件双重目录信息(FileBothDirectoryInformation)
这些信息类较为相似,区别在于返回的详细程度不同。提供的句柄(handle)必须指向目录(而非文件)。文件目录信息(FileDirectoryInformation)类返回以下结构:
typedef struct _FILE_DIRECTORY_INFORMATION {
ULONG NextEntryOffset;
ULONG FileIndex;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_DIRECTORY_INFORMATION, *PFILE_DIRECTORY_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
这是一个可变大小的结构,因为文件名(filename)作为最后一个成员存在。通过下一个条目偏移量(NextEntryOffset)可定位到下一个结构,该字段指示需向前移动的字节数以找到下一个条目。当下一个条目偏移量(NextEntryOffset)为0时,表示没有更多条目。此结构在Windows驱动开发工具包(WDK)文档中有详细说明。以下是各成员的简要描述:
- 文件索引(FileIndex):文件相对于父目录的字节偏移量。通常应忽略该字段,例如NTFS文件系统(NTFS)并不使用它。
- 创建时间(CreationTime)、最后访问时间(LastAccessTime)、最后写入时间(LastWriteTime)和修改时间(ChangeTime):这些字段已为大家所熟知。
- 文件末尾(EndOfFile):文件的大小(以字节为单位)。
- 分配大小(AllocationSize):文件的分配大小,通常是物理设备基础扇区大小的整数倍。
- 文件属性(FileAttributes):文件的属性集合,例如目录属性(FILE_ATTRIBUTE_DIRECTORY)、隐藏属性(FILE_ATTRIBUTE_HIDDEN)、只读属性(FILE_ATTRIBUTE_READONLY)等。
- 文件名长度(FileNameLength):文件名的长度(以字节为单位)。
- 文件名(FileName):文件名本身,不一定以空字符结尾。
以下示例列出指定目录中的项目:
NTSTATUS ListDirectory(PCWSTR path, PCWSTR filter) {
//
// init directory name
//
UNICODE_STRING name;
RtlInitUnicodeString(&name, path);
//
// init filter (if any)
//
UNICODE_STRING filterName;
if (filter)
RtlInitUnicodeString(&filterName, filter);
OBJECT_ATTRIBUTES attr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
HANDLE hDir;
IO_STATUS_BLOCK ioStatus;
//
// open directory
//
auto status = NtOpenFile(&hDir, FILE_LIST_DIRECTORY | SYNCHRONIZE, &attr, &ioStatus,
FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT);
if (!NT_SUCCESS(status))
return status;
BYTE buffer[1 << 10];
for (;;) {
//
// retriueve as many items as would fit in the buffer
//
status = NtQueryDirectoryFile(hDir, nullptr, nullptr, nullptr,
&ioStatus, buffer, sizeof(buffer), FileDirectoryInformation,
FALSE, filter ? &filterName : nullptr, FALSE);
if (!NT_SUCCESS(status))
break;
auto info = (FILE_DIRECTORY_INFORMATION*)buffer;
for (;;) {
printf("%.*ws", (int)(info->FileNameLength / sizeof(WCHAR)), info->FileName);
//
// add <DIR> in case of a directory
// otherwise, add file size in KB
//
if (info->FileAttributes & FILE_ATTRIBUTE_DIRECTORY)
printf(" <DIR>");
else
printf(" [%llu KB]", info->EndOfFile.QuadPart >> 10);
printf("\n ");
if (info->NextEntryOffset == 0)
break;
info = (FILE_DIRECTORY_INFORMATION*)((BYTE*)info +
info->NextEntryOffset);
}
}
NtClose(hDir);
return status;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
文件完整目录信息(FileFullDirectoryInformation)返回一个扩展结构:
typedef struct _FILE_FULL_DIR_INFORMATION {
ULONG NextEntryOffset;
ULONG FileIndex;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
ULONG EaSize;
WCHAR FileName[1];
} FILE_FULL_DIR_INFORMATION, *PFILE_FULL_DIR_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
14
扩展属性大小(EaSize):扩展属性(extended attributes)的大小(以字节为单位)。有关更多信息,请参阅本章后面的“扩展属性”小节。
最后,文件双重目录信息(FileBothDirectoryInformation)返回一个更全面的扩展结构,其中包含“短文件名”(采用旧的DOS 8.3格式):
typedef struct _FILE_BOTH_DIR_INFORMATION {
ULONG NextEntryOffset;
ULONG FileIndex;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
ULONG EaSize;
CCHAR ShortNameLength;
WCHAR ShortName[12];
WCHAR FileName[1];
} FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATION;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 9.5 NTFS流(NTFS Streams)
NTFS文件系统(NTFS file system)支持备用流(alternate streams),即与文件一起存储的附加数据项。一个文件可以有多个备用流,每个流都有自己的名称。主流(main stream)是未命名的,默认作为文件内容使用。备用流的创建/打开方式与普通文件相同,流名称紧跟在文件名之后,中间用冒号分隔。以下示例创建一个备用流并向其中写入一些数据(省略错误处理):
HANDLE hFile;
UNICODE_STRING name;
IO_STATUS_BLOCK ioStatus;
OBJECT_ATTRIBUTES attr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
RtlInitUnicodeString(&name, L"\\??\\C:\\Temp\\myfile.txt:mystream");
NtCreateFile(&hFile, GENERIC_WRITE | SYNCHRONIZE, &attr, &ioStatus, nullptr, 0,
FILE_SHARE_WRITE, FILE_OPEN_IF, FILE_SYNCHRONOUS_IO_NONALERT, nullptr, 0);
char data[] = "Hello, stream!";
NtWriteFile(hFile, nullptr, nullptr, nullptr, &ioStatus,
data, (ULONG)strlen(data), nullptr, nullptr);
NtClose(hFile);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
请注意,必须使用创建文件函数(NtCreateFile)来创建备用流。如果流已存在,打开文件函数(NtOpenFile)也可正常使用。Windows资源管理器(Windows Explorer)等标准工具显示的文件大小仅为主流的大小。这意味着你可以创建一个大小为0的文件,但让备用流包含任意大小的数据,且这些数据在标准工具中几乎不可见。Sysinternals的Streams工具可以列出文件中的流:
C:\temp>streams -nobanner myfile.txt
C:\temp\myfile.txt:
:mystream:$DATA 14
2
3
我自己开发的NTFS流工具(NTFS Streams tool)不仅可以显示流的名称和大小,还能展示流的内容(图9-3)。

图9-3:NTFS流(NTFS Streams)
如你所见,对备用流的读写操作与“普通”文件并无不同。然而,枚举文件中的流则是另一回事。可以使用文件信息查询函数(NtQueryInformationFile)并指定文件流信息(FileStreamInformation,22)信息类来枚举文件中的流。预期的结构是文件流信息(FILE_STREAM_INFORMATION):
typedef struct _FILE_STREAM_INFORMATION {
ULONG NextEntryOffset;
ULONG StreamNameLength;
LARGE_INTEGER StreamSize;
LARGE_INTEGER StreamAllocationSize;
WCHAR StreamName[1];
} FILE_STREAM_INFORMATION, *PFILE_STREAM_INFORMATION;
2
3
4
5
6
7
以下示例展示如何枚举文件中的流(完整示例请参见Streams样本代码):
NTSTATUS EnumStreams(PCWSTR filename) {
std::wstring path(filename);
// deal with a drive letter
if (filename[1] == L':')
path = L"\\??\\ " + path;
UNICODE_STRING name;
RtlInitUnicodeString(&name, path.c_str());
HANDLE hFile;
IO_STATUS_BLOCK ioStatus;
OBJECT_ATTRIBUTES attr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
auto status = NtOpenFile(&hFile, FILE_READ_ACCESS | SYNCHRONIZE,
&attr, &ioStatus, FILE_SHARE_READ | FILE_SHARE_WRITE,
FILE_SYNCHRONOUS_IO_NONALERT);
if (!NT_SUCCESS(status))
return status;
BYTE buffer[1 << 10];
status = NtQueryInformationFile(hFile, &ioStatus,
buffer, sizeof(buffer), FileStreamInformation);
if (!NT_SUCCESS(status))
return status;
auto info = (FILE_STREAM_INFORMATION*)buffer;
for (;;) {
printf("Name: %.*ws Size: %llu bytes\n ",
(int)(info->StreamNameLength / sizeof(WCHAR)), info->StreamName, info->StreamSize.QuadPart);
if (info->NextEntryOffset == 0)
break;
info = (FILE_STREAM_INFORMATION*)((PBYTE)info + info->NextEntryOffset);
}
NtClose(hFile);
return status;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
对于前面示例中“空”的myfile.txt文件,输出如下:
C:\winnativeapibooksamples\Chapter09\>Streams.exe c:\temp\myfile.txt
Name: ::$DATA Size: 0 bytes
Name: :mystream:$DATA Size: 14 bytes
2
3
# 9.6 扩展属性(Extended Attributes)
扩展属性(Extended attributes,EA)是一种存储文件或目录附加信息的方式。并非所有文件系统都支持扩展属性,但NTFS文件系统(NTFS)支持。可以将扩展属性视为另一种与文件关联自定义信息的方式。
有几个信息类与扩展属性相关。第一个是文件扩展属性信息(FileEaInformation,7),它返回一个无符号长整数(ULONG)类型的扩展属性大小(以字节为单位)。第二个(也是更重要的一个)是文件完整扩展属性信息(FileFullEaInformation,15),它以(可能是链式的)文件完整扩展属性信息(FILE_FULL_EA_INFORMATION)结构返回扩展属性:
typedef struct _FILE_FULL_EA_INFORMATION {
ULONG NextEntryOffset;
UCHAR Flags;
UCHAR EaNameLength;
USHORT EaValueLength;
CHAR EaName[1];
} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;
2
3
4
5
6
7
下一个条目偏移量(NextEntryOffset)指示移动到下一个属性所需的字节数,其中0表示没有更多属性。标志(Flags)的值要么是0,要么是文件需要扩展属性(FILE_NEED_EA,0x80),用于指示该扩展属性是文件正常运行所必需的。扩展属性名称长度(EaNameLength)是扩展属性名称的长度(以字节为单位),名称本身(EaName)由内部存储为大写的ASCII字符组成。扩展属性值长度(EaValueLength)是扩展属性值的长度(以字节为单位)。扩展属性值紧跟在该结构之后。其布局如图9-4所示。

图9-4:布局(Layout)
查询文件的扩展属性有两种方法。第一种是调用文件信息查询函数(NtQueryInformationFile)并指定文件完整扩展属性信息(FileFullEaInformation),然后将返回的缓冲区解释为链式的文件完整扩展属性信息(FILE_FULL_EA_INFORMATION)结构。这种方法可行,但灵活性有限。第二种是调用扩展属性查询函数(NtQueryEaFile),它也返回扩展属性,但以增加复杂性为代价提供了更高的灵活性:
NTSTATUS NtQueryEaFile(
_In_ HANDLE FileHandle,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_(Length) PVOID Buffer,
_In_ ULONG Length,
_In_ BOOLEAN ReturnSingleEntry,
_In_reads_bytes_opt_(EaListLength) PVOID EaList,
_In_ ULONG EaListLength,
_In_opt_ PULONG EaIndex,
_In_ BOOLEAN RestartScan);
2
3
4
5
6
7
8
9
10
前四个参数大家应该已经熟悉。返回单个条目(ReturnSingleEntry)指示是返回单个扩展属性还是所有扩展属性。扩展属性列表(EaList)是要返回的扩展属性名称列表。如果指定为NULL,则返回所有扩展属性。扩展属性列表长度(EaListLength)是该列表的长度(以字节为单位)。扩展属性索引(EaIndex)指示要返回的扩展属性的索引。如果指定为NULL,则返回第一个扩展属性或所有扩展属性(基于返回单个条目(ReturnSingleEntry)的值)。
重新开始扫描(RestartScan)指示是从开头重新开始扫描还是从上一个条目继续扫描。当提供的缓冲区不足以容纳所有条目时,这一点非常有用。首次使用特定句柄调用扩展属性查询函数(NtQueryEaFile)时,重新开始扫描(RestartScan)参数将被忽略。后续调用时该参数将生效。
以下示例展示如何检索文件的所有扩展属性:
BYTE buffer[1 << 10];
// hFile is an open handle to a file
for (;;) {
status = NtQueryEaFile(hFile, &ioStatus, buffer, sizeof(buffer), FALSE,
nullptr, 0, nullptr, FALSE);
if (!NT_SUCCESS(status))
break;
auto info = (FILE_FULL_EA_INFORMATION*)buffer;
for (;;) {
//
// assume for demonstration purposes that the
// EA value is a Unicode string
//
printf("Name: %.*s Value: %.*ws\n ",
info->EaNameLength, info->EaName,
(int)(info->EaValueLength / sizeof(WCHAR)),
(PCWSTR)(info->EaName + info->EaNameLength + 1));
if (info->NextEntryOffset == 0)
break;
info = (PFILE_FULL_EA_INFORMATION)((PBYTE)info + info->NextEntryOffset);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
要写入扩展属性,请使用扩展属性设置函数(NtSetEaFile):
NTSTATUS NtSetEaFile(
_In_ HANDLE FileHandle,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_reads_bytes_(Length) PVOID Buffer,
_In_ ULONG Length);
2
3
4
5
预期的缓冲区是链式的文件完整扩展属性信息(FILE_FULL_EA_INFORMATION)结构,如前所述。每个结构都必须按照图9-4所示的格式进行设置。以下示例展示如何设置单个扩展属性(假设属性值是Unicode字符串):
NTSTATUS WriteEaToFile(HANDLE hFile, PCSTR name, PCWSTR value) {
//
// use a trick to allow FILE_FULL_EA_INFORMATION to extend
// beyond its static bounds without generating memory access violation
//
union {
FILE_FULL_EA_INFORMATION info{};
BYTE buffer[1 << 10];
} u;
//
// copy name
//
u.info.EaNameLength = (UCHAR)strlen(name); // 补充缺失的名称长度赋值
strcpy_s(u.info.EaName, u.info.EaNameLength + 1, name);
u.info.Flags = 0; // 补充必要的Flags字段初始化
u.info.NextEntryOffset = 0;
u.info.EaValueLength = (USHORT)((1 + u.info.EaNameLength) * sizeof(WCHAR));
wcscpy_s((PWSTR)(u.info.EaName + u.info.EaNameLength + 1),
u.info.EaValueLength / sizeof(WCHAR), value);
IO_STATUS_BLOCK ioStatus; // 补充缺失的ioStatus定义
return NtSetEaFile(hFile, &ioStatus, &u.info, sizeof(u.buffer));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
允许指示的缓冲区大小大于其实际大小,因为文件系统驱动程序(file system driver)将根据下一个条目偏移量(NextEntryOffset)来遍历链式结构。
完整的样本代码位于ea项目中。
# 9.7 访问设备(Accessing Devices)
NtCreateFile 和 NtOpenFile 函数可用于打开不一定是文件系统文件或卷的设备对象(Device object)。任何命名的设备对象都有可能被打开。打开操作可能会因访问权限(access rights)问题而失败,但对象管理器(Object Manager)的所有命名空间(namespace)都是可访问的。
例如,Beep Windows API 会根据频率(frequency)和持续时间(duration)发出声音。在内部,它是通过一个名为 Beep 的设备来实现的。Beep 函数是同步的——也就是说,线程会等待声音播放完毕。如果我们想异步播放声音该怎么办?我们可以通过打开 Beep 设备的句柄(handle)来直接访问它:
HANDLE hFile;
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"\\Device\\Beep");
OBJECT_ATTRIBUTES attr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&name, 0);
IO_STATUS_BLOCK ioStatus;
status = NtOpenFile(&hFile, FILE_WRITE_DATA | SYNCHRONIZE, &attr,
&ioStatus, FILE_SHARE_WRITE | FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT);
2
3
4
5
6
7
8
9
路径 \Device\Beep 可以通过 WinObj 或 Object Explorer 查看(图 9-5)。

图 9-5:WinObj 中的 Beep 设备
我们已经见过多次这类代码。一旦获得了有效的句柄,我们该如何“告知”Beep 设备播放声音呢?对于文件系统中的文件,我们会调用 NtReadFile 或 NtWriteFile。而对于设备,则取决于该设备的驱动程序(driver)。驱动程序可能期望通过 NtReadFile 或 NtWriteFile 调用,并传入一些它能理解的格式化数据,但更多情况下会使用第三个 API——NtDeviceIoControlFile:
NTSTATUS NtDeviceIoControlFile(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ ULONG IoControlCode,
_In_reads_bytes_opt_(InputBufferLength) PVOID InputBuffer,
_In_ ULONG InputBufferLength,
_Out_writes_bytes_opt_(OutputBufferLength) PVOID OutputBuffer,
_In_ ULONG OutputBufferLength);
2
3
4
5
6
7
8
9
10
11
NtDeviceIoControlFile 是 DeviceIoControl Windows API 对应的原生(native,底层)函数,就像 NtReadFile 和 NtWriteFile 是 ReadFile 和 WriteFile 对应的原生函数一样。
NtDeviceIoControlFile 在与设备通信时提供了更高的灵活性。IoControlCode 是一个 32 位值,驱动程序会将其解释为要执行的操作。当然,如果驱动程序无法识别该值,调用将会失败。
输入缓冲区(InputBuffer,可选)可用于向驱动程序发送输入数据,而输出缓冲区(OutputBuffer)可用于接收结果。这一切都取决于驱动程序以及它期望的通信方式。
我们如何知道与 Beep 设备通信的正确方式呢?除了对其操作进行逆向工程(reverse engineering)外,我们还需要相关文档。幸运的是,对于 Beep 设备,微软提供了 Ntddbeep.h 头文件,其中包含了所需的信息。
在该文件中,我们可以找到以下定义(为清晰起见稍作编辑):
#define DD_BEEP_DEVICE_NAME "\\Device\\Beep"
#define DD_BEEP_DEVICE_NAME_U L"\\Device\\Beep"
#define IOCTL_BEEP_SET \
CTL_CODE(FILE_DEVICE_BEEP, 0, METHOD_BUFFERED, FILE_ANY_ACCESS)
typedef struct _BEEP_SET_PARAMETERS
{
ULONG Frequency;
ULONG Duration;
} BEEP_SET_PARAMETERS, *PBEEP_SET_PARAMETERS;
#define BEEP_FREQUENCY_MINIMUM 0x25
#define BEEP_FREQUENCY_MAXIMUM 0x7FFF
2
3
4
5
6
7
8
9
10
11
12
13
14
该头文件包含了 ASCII 和 Unicode 格式的设备名称。BEEP_SET_PARAMETERS 是驱动程序期望的输入缓冲区。IOCTL_BEEP_SET 是期望的控制代码(control code),无需进行任何猜测。以下示例将播放一个频率为 600Hz、持续时间为 2 秒的声音:
BEEP_SET_PARAMETERS params;
params.Duration = 2000; // msec
params.Frequency = 600; // Hz
NTSTATUS status; // 补充缺失的status声明
IO_STATUS_BLOCK ioStatus; // 补充缺失的ioStatus声明
status = NtDeviceIoControlFile(hFile, nullptr, nullptr, nullptr, &ioStatus,
IOCTL_BEEP_SET, ¶ms, sizeof(params), nullptr, 0);
2
3
4
5
6
7
8
尽管设备是以同步访问(synchronous access)方式打开的,但实际上 Beep 驱动程序是异步工作的;NtDeviceIoControlFile 调用会立即返回,而声音则在后台播放。我们可以通过等待声音播放完毕,同时不时输出一些内容来“证明”这一点:
LARGE_INTEGER ticks, delay;
NtQuerySystemTime(&ticks);
delay.QuadPart = -250000; // 25msec
// calculate future time based on the duration
auto target = ticks.QuadPart + params.Duration * 10000;
while (ticks.QuadPart < target)
{
printf(" . ");
// wait a bit
NtDelayExecution(FALSE, &delay);
NtQuerySystemTime(&ticks);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
完整的示例位于 Beep 项目中。
除了 NtDeviceIoControlFile 之外,还有 NtFsControlFile,它用于与文件系统驱动程序(file system drivers)通信。它与 NtDeviceIoControlFile 类似,具有完全相同的参数,但期望的控制代码不同,这些控制代码以 FSCTL_ 开头。例如,NTFS 文件系统驱动程序支持 FSCTL_GET_NTFS_VOLUME_DATA 控制代码,该代码会返回与文件句柄所打开的卷相关的信息。它会返回一个 NTFS_VOLUME_DATA_BUFFER 结构:
typedef struct
{
LARGE_INTEGER VolumeSerialNumber;
LARGE_INTEGER NumberSectors;
LARGE_INTEGER TotalClusters;
LARGE_INTEGER FreeClusters;
LARGE_INTEGER TotalReserved;
DWORD BytesPerSector;
DWORD BytesPerCluster;
DWORD BytesPerFileRecordSegment;
DWORD ClustersPerFileRecordSegment;
LARGE_INTEGER MftValidDataLength;
LARGE_INTEGER MftStartLcn;
LARGE_INTEGER Mft2StartLcn;
LARGE_INTEGER MftZoneStart;
LARGE_INTEGER MftZoneEnd;
} NTFS_VOLUME_DATA_BUFFER, *PNTFS_VOLUME_DATA_BUFFER;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
以下是一个示例:
NTFS_VOLUME_DATA_BUFFER data;
status = NtFsControlFile(hFile, nullptr, nullptr, nullptr, &ioStatus,
FSCTL_GET_NTFS_VOLUME_DATA, nullptr, 0, &data, sizeof(data));
2
3
4
上述文件句柄可以是所请求卷中的任何文件或目录的句柄。奇怪的是,使用相同的控制代码调用 NtDeviceIoControlFile 会失败,并返回 STATUS_INVALID_PARAMETER。然而,由于 NtFsControlFile 不属于文档化的 Windows API,使用相同的控制代码调用 DeviceIoControl(文档化的 API)却能正常工作。
文件系统可能支持的所有标准控制代码和结构都定义在 <WinIoCtl.h> 中,其中许多都有官方文档说明。
# 9.8 I/O 完成端口
I/O 完成(端口)对象(I/O completion (port) objects)是一种在异步 I/O 操作(asynchronous I/O operations)完成时调用处理程序(handlers)的方式。有多种方法可以知道异步 I/O 操作何时完成,例如使用许多 I/O 函数提供的(可选)事件句柄(event handle)。I/O 完成对象提供了最高效的方式,它允许多个线程响应 I/O 完成事件,并且可以选择性地利用线程池(thread pool),线程可以从线程池中获取已完成的操作并运行处理程序。
一个 I/O 完成对象可以与多个文件对象(file objects)相关联。它也可以独立于任何 I/O 操作使用,作为一种提交(post)可由多个线程处理的操作的机制。图 9-6 展示了 I/O 完成对象的主要组成部分。

图 9-6:I/O 完成(端口)对象
使用 NtCreateIoCompletion 可以创建 I/O 完成对象:
NTSTATUS NtCreateIoCompletion(
_Out_ PHANDLE IoCompletionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ ULONG Count);
2
3
4
5
DesiredAccess 是对新对象的期望访问权限,通常为 IO_COMPLETION_ALL_ACCESS(定义在 <WinNt.h> 中)。可以提供 ObjectAttributes,其中可以包含名称。最后,Count 表示最多可以有多少个线程处理 I/O 完成事件。零表示系统上的逻辑处理器(logical processors)数量。
奇怪的是,Windows API CreateIoCompletionPort 不允许指定名称。这在某种程度上是合理的,因为在进程之间共享 I/O 完成对象并不实用,因此拥有名称的价值就降低了;尽管如此,仍然支持指定名称。
在会话命名空间(session namespace)之外(例如全局命名空间(global namespace))创建命名对象需要 SeCreateGlobalPrivilege 权限(privilege),该权限默认授予管理员组(Administrators group)。
由于允许指定名称,因此可以使用 NtOpenIoCompletion 打开现有 I/O 完成对象的句柄:
NTSTATUS NtOpenIoCompletion(
_Out_ PHANDLE IoCompletionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes);
2
3
4
在这种情况下,ObjectAttributes 是必填的。
要将文件句柄与 I/O 完成对象相关联,可以使用 NtSetInformationFile,并指定 FileCompletionInformation(30),此时期望的缓冲区是 FILE_COMPLETION_INFORMATION:
typedef struct _FILE_COMPLETION_INFORMATION
{
HANDLE Port;
PVOID Key;
} FILE_COMPLETION_INFORMATION, *PFILE_COMPLETION_INFORMATION;
2
3
4
5
可以多次调用该函数,使用不同的文件句柄但相同的端口。Port 是从 NtCreateIoCompletion 或 NtOpenIoCompletion 返回的端口句柄。Key 是一个任意值,可用于标识特定的文件。
执行 I/O 操作时,任意数量的线程都可以调用 NtRemoveIoCompletion 来等待操作完成。操作完成后,线程将被解除阻塞(unblocked),并可以执行所需的任何操作。从 NtRemoveIoCompletion 中释放的最大线程数基于创建 I/O 完成对象时指定的 Count 值。以下是 NtRemoveIoCompletion 及其扩展版本 NtRemoveIoCompletionEx:
NTSTATUS NtRemoveIoCompletion(
_In_ HANDLE IoCompletionHandle,
_Out_ PVOID* KeyContext,
_Out_ PVOID* ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_opt_ PLARGE_INTEGER Timeout);
NTSTATUS NtRemoveIoCompletionEx(
_In_ HANDLE IoCompletionHandle,
_Out_writes_to_(Count, *Removed) PFILE_IO_COMPLETION_INFORMATION Information,
_In_ ULONG Count,
_Out_ PULONG Removed,
_In_opt_ PLARGE_INTEGER Timeout,
_In_ BOOLEAN Alertable);
2
3
4
5
6
7
8
9
10
11
12
13
14
较简单的函数会等待异步 I/O 操作完成(或超时)。如果操作完成,它可以在此时运行所需的任何代码。已完成操作返回的信息包括 KeyContext(提供给 NtSetInformationFile 的键)、*ApcContext(传递给异步 I/O 操作的值)*以及 IoStatusBlock(指示操作结果)。
扩展函数可以等待多个操作完成,最多可等待 Count 个操作。*Removed 参数指示已完成的操作数量。每个已完成操作的信息都返回到 Information 数组中,该数组的类型如下:
typedef struct _FILE_IO_COMPLETION_INFORMATION
{
PVOID KeyContext;
PVOID ApcContext;
IO_STATUS_BLOCK IoStatusBlock;
} FILE_IO_COMPLETION_INFORMATION, *PFILE_IO_COMPLETION_INFORMATION;
2
3
4
5
6
这基本上将 NtRemoveIoCompletion 单独返回的相同信息进行了打包。最后,Alertable 指示等待是否应是可警报的(alertable)。
这些函数的返回值可以是 STATUS_SUCCESS(如果操作完成)、STATUS_TIMEOUT(如果超时)或 STATUS_USER_APC(如果等待是可警报的且执行了 APC)。
如前所述,无论是否存在 I/O 操作(或除了 I/O 操作之外),I/O 完成对象都可用于提交可由多个线程等待的操作。这可以通过 NtSetIoCompletion 或其扩展版本 NtSetIoCompletionEx 来实现:
NTSTATUS NtSetIoCompletion(
_In_ HANDLE IoCompletionHandle,
_In_opt_ PVOID KeyContext,
_In_opt_ PVOID ApcContext,
_In_ NTSTATUS IoStatus,
_In_ ULONG_PTR IoStatusInformation);
NTSTATUS NtSetIoCompletionEx(
_In_ HANDLE IoCompletionHandle,
_In_ HANDLE IoCompletionPacketHandle,
_In_opt_ PVOID KeyContext,
_In_opt_ PVOID ApcContext,
_In_ NTSTATUS IoStatus,
_In_ ULONG_PTR IoStatusInformation);
2
3
4
5
6
7
8
9
10
11
12
13
14
NtSetIoCompletion 会将一个项排入 I/O 完成对象的队列,调用 NtRemoveIoCompletion(Ex) 的线程之一可以获取该队列项。除了完成对象句柄之外,其他参数的含义由调用者决定。扩展函数使用 I/O 预留对象句柄(I/O Reserve object handle),这超出了本章的范围。
最后,可以使用线程池(thread pool)来处理 I/O 完成对象的“完成事件”。这需要使用 TpAllocIoCompletion 创建线程池 I/O,感兴趣的读者可以自行练习。
# 9.9:其他函数
以下小节简要讨论其他与 I/O 相关的 API。
# 9.9.1 驱动程序(Drivers)
可以通过调用 NtLoadDriver 将内核驱动程序(kernel driver)加载到系统中:
NTSTATUS NtLoadDriver(_In_ PUNICODE_STRING DriverServiceName);
其中,DriverServiceName 是存储在 \HKLM\System\CurrentControlSet\Services 下的驱动程序注册表项(Registry key)的名称。
图 9-7 展示了注册表编辑器(RegEdit.exe)中的该注册表项及其一些子项。

图 9-7:服务注册表项(Services Registry Key)
驱动程序通常通过 INF 文件或 Sc.Exe 等命令行工具进行安装。加载驱动程序是一项特权操作(privilege operation),默认授予管理员组。
驱动程序加载后,其注册表项可以被删除,且不会产生任何不利影响。
对应的函数 NtUnloadDriver 可用于卸载(停止)驱动程序:
NTSTATUS NtUnloadDriver(_In_ PUNICODE_STRING DriverServiceName);
# 9.9.2 文件锁定(Locking Files)
可以使用 NtLockFile 锁定文件中的一个字节范围:
NTSTATUS NtLockFile(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ PLARGE_INTEGER ByteOffset,
_In_ PLARGE_INTEGER Length,
_In_ ULONG Key,
_In_ BOOLEAN FailImmediately,
_In_ BOOLEAN ExclusiveLock);
2
3
4
5
6
7
8
9
10
11
前五个参数现在应该已经很熟悉了。*ByteOffset 是开始锁定的偏移量(offset)。注意,NULL 不是有效值,否则会导致访问冲突(access violation)。*Length 是要锁定的字节数(NULL 是非法的)。Key 是用于标识锁定的任意值。FailImmediately 指示如果无法立即获取锁定(即使锁定操作被指定为异步),是否要失败。
ExclusiveLock 指示获取独占锁(exclusive lock,TRUE)还是共享锁(shared lock,FALSE)。不再需要锁定时,可以使用 NtUnlockFile 释放它:
NTSTATUS NtUnlockFile(
_In_ HANDLE FileHandle,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_In_ PLARGE_INTEGER ByteOffset,
_In_ PLARGE_INTEGER Length,
_In_ ULONG Key);
2
3
4
5
6
# 9.9.3 更改通知(Change Notifications)
可以通过调用 NtNotifyChangeDirectoryFile 或 NtNotifyChangeDirectoryFileEx 来检测文件系统中的更改:
NTSTATUS NtNotifyChangeDirectoryFile(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_(Length) PVOID Buffer, // FILE_NOTIFY_INFORMATION
_In_ ULONG Length,
_In_ ULONG CompletionFilter,
_In_ BOOLEAN WatchTree);
NTSTATUS NtNotifyChangeDirectoryFileEx(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_(Length) PVOID Buffer,
_In_ ULONG Length,
_In_ ULONG CompletionFilter,
_In_ BOOLEAN WatchTree,
_In_opt_ DIRECTORY_NOTIFY_INFORMATION_CLASS DirectoryNotifyInformationClass);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NtNotifyChangeDirectoryFile 是一个快捷方式,它调用 NtNotifyChangeDirectoryFileEx 并将 DirectoryNotifyInformationClass 设置为 DirectoryNotifyInformation。DirectoryNotifyInformationClass 指示要返回的数据缓冲区:
NTSTATUS NtNotifyChangeDirectoryFile(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_(Length) PVOID Buffer, // FILE_NOTIFY_INFORMATION
_In_ ULONG Length,
_In_ ULONG CompletionFilter,
_In_ BOOLEAN WatchTree);
NTSTATUS NtNotifyChangeDirectoryFileEx(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE Event,
_In_opt_ PIO_APC_ROUTINE ApcRoutine,
_In_opt_ PVOID ApcContext,
_Out_ PIO_STATUS_BLOCK IoStatusBlock,
_Out_writes_bytes_(Length) PVOID Buffer,
_In_ ULONG Length,
_In_ ULONG CompletionFilter,
_In_ BOOLEAN WatchTree,
_In_opt_ DIRECTORY_NOTIFY_INFORMATION_CLASS DirectoryNotifyInformationClass);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
相关结构定义在 <WinNt.h> 中。FileHandle 必须是目录句柄(而非文件句柄)。当检测到更改时,操作完成。操作完成时,信息会返回到 Buffer 中,Length 是缓冲区的大小(以字节为单位)。CompletionFilter 是一组标志,指示要检测的更改类型(也定义在 <WinNt.h> 中)。例如:
FILE_NOTIFY_CHANGE_FILE_NAME 指示检测文件名更改,FILE_NOTIFY_CHANGE_DIR_NAME 指示检测目录名更改,FILE_NOTIFY_CHANGE_LAST_WRITE 指示检测最后写入时间(last write time)的更改。WatchTree 指示是否递归监视目录树(directory tree)。
如果同步调用该函数,调用会阻塞(block),直到检测到更改。使用该函数的一种简单方式是在循环中同步调用它,逐个处理检测到的更改。
# 9.10 总结
I/O API 用于操作文件和设备。本章介绍了最常见的原生 I/O API,其中一些在 Windows 驱动程序工具包(WDK)中有官方文档说明。I/O 系统支持同步和异步访问,所需的模式在 NtOpenFile 或 NtCreateFile 调用中指定。
在下一章中,我们将探讨 Windows 的一个未文档化特性——异步本地过程调用(Asynchronous Local Procedure Calls,ALPC)。