-
Notifications
You must be signed in to change notification settings - Fork 1
【Zig 日报】绕过 Kernel32.dll 的乐趣与非盈利目的 #302
Description
作者:Andrew Kelley
微软 Windows 为在内核中执行操作提供了大量的 ABI(应用程序二进制接口)表面区域。然而,并非所有 ABI 都是平等的。正如 Casey Muratori 在他的讲座《唯一不可打破的定律》(The Only Unbreakable Law)中所指出的,软件开发团队的组织结构直接影响了他们所生产的软件的结构。
Windows 上的 DLL 被组织成一个层次结构,其中一些 API 是对更底层 API 的高级封装。例如,无论何时你调用 kernel32.dll 的函数,最终的实际工作都是由 ntdll.dll 完成的。你可以通过使用 ProcMon.exe 并检查堆栈跟踪来直接观察这一点。
我们凭经验得知,ntdll API 通常设计精良、合理且强大,而 kernel32 封装则引入了不必要的堆分配、额外的故障模式、意外的 CPU 使用和冗余。使用 ntdll 函数感觉就像在使用资深工程师制作的软件,而使用 kernel32 函数感觉就像在使用微软员工制作的软件。
这就是为什么 Zig 标准库的策略是避免使用除 ntdll 之外的所有 DLL。我们还没有完全达到这个目标——我们仍然有很多对 kernel32 的调用——但我们最近取得了巨大的进展。我将给你两个例子。
示例 1:熵(Entropy)
根据官方文档,Windows 没有一个直接的方法来获取随机字节。
许多项目,包括 Chromium、boringssl、Firefox 和 Rust,都调用了 advapi32.dll 中的 SystemFunction036,因为它在 Windows 8 之前的版本中是可用的。
不幸的是,从 Windows 8 开始,当你第一次调用这个函数时,它会动态加载 bcryptprimitives.dll 并调用 ProcessPrng。如果加载 DLL 失败(例如由于系统过载,我们在 Zig CI 上观察到过几次),它会返回错误 38(来自一个返回类型为 void 且文档声称永不失败的函数)。
ProcessPrng 所做的第一件事是堆分配一小段常量字节。如果这失败了,它会在一个 BOOL 中返回 NO_MEMORY(文档规定的行为是永不失败,并且总是返回 TRUE)。
bcryptprimitives.dll 显然每次加载时还会运行一个测试套件。
ProcessPrng 真正所做的只是对 "\\Device\\CNG" 执行 NtOpenFile,并使用 NtDeviceIoControlFile 读取 48 字节以获取一个种子,然后初始化一个基于 AES 的每 CPU CSPRNG(加密安全伪随机数生成器)。
因此,对 bcryptprimitives.dll 和 advapi32.dll 的依赖都可以避免,而且首次读取 RNG 时的非确定性故障和延迟也可以避免。
示例 2:NtReadFile 和 NtWriteFile
ReadFile 看起来像这样:
pub extern "kernel32" fn ReadFile(
hFile: HANDLE,
lpBuffer: LPVOID,
nNumberOfBytesToRead: DWORD,
lpNumberOfBytesRead: ?*DWORD,
lpOverlapped: ?*OVERLAPPED,
) callconv(.winapi) BOOL;NtReadFile 看起来像这样:
pub extern "ntdll" fn NtReadFile(
FileHandle: HANDLE,
Event: ?HANDLE,
ApcRoutine: ?*const IO_APC_ROUTINE,
ApcContext: ?*anyopaque,
IoStatusBlock: *IO_STATUS_BLOCK,
Buffer: *anyopaque,
Length: ULONG,
ByteOffset: ?*const LARGE_INTEGER,
Key: ?*const ULONG,
) callconv(.winapi) NTSTATUS;提醒一下,上面的函数是通过调用下面的函数实现的。
我们已经可以看到使用较低级别 API 的一些好处。例如,真实的 API 只是将错误代码作为返回值提供给我们,而 kernel32 封装将状态码隐藏在某个地方,返回一个 BOOL,然后要求你调用 GetLastError 来找出哪里出了问题。想象一下!函数返回一个值 🌈
此外,OVERLAPPED 是一个假类型。Windows 内核根本不知道或不关心它!这里的实际原语是事件(events)、APC(异步过程调用)和 IO_STATUS_BLOCK。
如果你有一个同步文件句柄,那么 Event 和 ApcRoutine 必须为空。你会立即在 IO_STATUS_BLOCK 中得到结果。如果你在这里传递一个 APC 例程,那么一些古老且腐烂的 32 位代码就会运行,你会得到垃圾结果。
另一方面,如果你有一个异步文件句柄,那么你需要使用 Event 或 ApcRoutine。kernel32.dll 使用事件,这意味着它正在进行额外的、不必要的资源分配和管理,仅仅是为了从文件中读取数据。相反,Zig 现在传递一个 APC 例程,然后调用 NtDelayExecution。这与取消操作无缝集成,使得在任务执行文件 I/O 时能够取消任务,无论文件是以同步模式还是异步模式打开的。
加入我们
Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来:
- 供稿,分享自己使用 Zig 的心得
- 改进 ZigCC 组织下的开源项目
- 加入微信群、Telegram 群组