CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
  • Windows Native API编程 专栏说明
  • 第1章 原生API(Native API)开发入门
  • 第2章 原生API(Native API)基础
  • 第3章 原生应用程序(Native Applications)
  • 第4章:系统信息
  • 第5章:进程
  • 第6章:线程
  • 第7章:对象与句柄
  • 第 8 章:内存(第一部分)
  • 第9章:I/O
    • 9.1 文件与设备(Files and Devices)
    • 9.2 文件与设备API(File and Device API)
    • 9.3 文件信息(File Information)
      • 9.3.1 文件基本信息(FileBasicInformation)
      • 9.3.2 文件标准信息(FileStandardInformation)
      • 9.3.3 文件名称信息(FileNameInformation)
      • 9.3.4 文件卷名信息(FileVolumeNameInformation)
      • 9.3.5 文件重命名信息(FileRenameInformation)
      • 9.3.6 文件处置信息(FileDispositionInformation)
      • 9.3.7 文件位置信息(FilePositionInformation)
      • 9.3.8 文件模式信息(FileModeInformation)
      • 9.3.9 文件全部信息(FileAllInformation)
    • 9.4 仅适用于目录的信息(Directory-Only Information)
      • 9.4.1 文件目录信息(FileDirectoryInformation)、文件完整目录信息(FileFullDirectoryInformation)、文件双重目录信息(FileBothDirectoryInformation)
    • 9.5 NTFS流(NTFS Streams)
    • 9.6 扩展属性(Extended Attributes)
    • 9.7 访问设备(Accessing Devices)
    • 9.8 I/O 完成端口
    • 9.9:其他函数
      • 9.9.1 驱动程序(Drivers)
      • 9.9.2 文件锁定(Locking Files)
      • 9.9.3 更改通知(Change Notifications)
    • 9.10 总结
  • 第10章:ALPC
  • 第11章 安全性(Security)
  • 第12章 内存(第二部分)
  • 第13章 注册表
目录

第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));
1
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函数。

Object Manager界面截图

图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);
1
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;
1
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);
1
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);
1
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);
1
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);
1
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);
1
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);
1
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);
1
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);
1
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;
1
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;
}
1
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);
1
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;
1
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;
1
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;
1
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;
1
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
1
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;
1
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;
}
1
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");
1

我们可以扩展该示例,通过指定根目录句柄来支持将文件移动到不同目录。可按如下方式打开目录句柄以实现此目的:

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;
}
1
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;
1
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;
}
1
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;
1
2
3

# 9.3.8 文件模式信息(FileModeInformation)

该信息类返回文件的打开模式(mode)。返回的结构是文件模式信息结构(FILE_MODE_INFORMATION),本质上是一组标志:

typedef  struct  _FILE_MODE_INFORMATION  {
    ULONG  Mode;
}  FILE_MODE_INFORMATION,  *PFILE_MODE_INFORMATION;
1
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;
1
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);
1
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;
1
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;
}
1
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;
1
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;
1
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);
1
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
1
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;
1
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;
}
1
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
1
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;
1
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);
1
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);
    }
}
1
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);
1
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));
}
1
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);
1
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);
1
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
1
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, &params, sizeof(params), nullptr, 0);
1
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);
}
1
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;
1
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));
1
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);
1
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);
1
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;
1
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);
1
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;
1
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);
1
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);
1

其中,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);
1

# 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);
1
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);
1
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);
1
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);
1
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)。

第 8 章:内存(第一部分)
第10章:ALPC

← 第 8 章:内存(第一部分) 第10章:ALPC→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2026 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式