1 《Undocumented Windows 2000 Secrets》翻译 --- 第二章( 二 )


Ntdll.dll 是一个操作系统组件,它为 Native API 准确地提供服务,ntdll.dll 是 Native API 在用户模式下的前端 。Native API 真正的接口在 ntoskrnl.exe 中实现 。从其文件名可以猜出它就是 NT 操作系统内核 。事实上,内核模式驱动程序对系统服务的请求多数时候都会进入该模块 。Ntdll.dll 的主要任务就是为运行于用户模式的程序提供一个确定的内核函数的子集,这其中就包括 Win32 子系统 DLLs。在 图 2-1 中,从 ntdll.dll 指向 ntoskrnl.exe 的箭头旁标注的 INT 2eh 表示 Windows 2000 使用此中断将 CPU 特权级从用户模式切换到内核模式 。开发内核( kernel-mode )模式程序的人员认为用户模式的代码是具有攻击性的、充满错误的和危险的 。因此,必须让这些代码远离内核函数 。而通过在调用 API 的过程中将特权级别从用户模式切换到内核模式是一种可控制这些问题的方式 。调用程序从来不可能触及内核,它只能察看它们 。
例如,由 kernel32.dll 导出的 Win32 API 函数 DeviceIoControl() 最终会调用由 ntdll.dll 导出的 NtDeviceIoControlFile()。通过反编译该函数会发现此函数令人惊讶的实现方式 — 它是如此的简单! 示列 2-1 展示了这些 。首先,CPU 寄存器 EAX 被装入了一个“魔术”数字 0x38,这是一个分派 ID。接下来,寄存器 EDX 被设置指向堆栈中的某处,其地址为堆栈指针 ESP 加上 4,因此,EDX 将指向堆栈中返回地址的后面,该返回地址在进入 NtDeviceIoControlFile() 时将被立即保存下来 。显而易见,EDX 指向的位置是用来临时存放传递进来的参数的 。接下来的指令是一个简单的 INT 2eh,该指令将跳转到中断描述符表( Interrupt Descriptor Table,IDT )的 0x2e 位置上存放的中断处理例程( interrupt handler )中 。这看上去是不是很熟悉?事实上,这有些像 Dos 下的 INT 21h 调用 。然而,Windows 2000 的 INT 2eh 接口要远比一个简单的 API 调用有用,分配器( dispatcher )利用它从用户模式进入内核模式 。请注意,这种模式切换方式是 x86 处理器特有的 。在 Alpha 平台上,有不同的方式来实现此种功能 。
NtDeviceIoControlFile:
mov eax, 38h
lea edx, [esp 4]
int 2Eh
ret 28h
示列 2-1. ntdll.NtDeviceIoControlFile() 的实现方式
Windows 2000 Native API 由 248 个函数组成,这些函数都采用上述方式进入内核 。与 Windows NT 4.0 相比多出了 37 个 。你很容易在 ntdll.dll 的导出列表中通过 Nt 前缀来认出它们 。Ntdll.dll 总共导出了 249 个这样的符号 。多出的那个函数是 NtCurrentTeb(),该函数是一个纯粹的用户模式函数,它无需进入内核 。附录 B 中的 表 B-1 列出了所有可用的 Native API。该表同时还指出那个函数是由 ntoskrnl.exe 导出的 。令人奇怪的是,在处于内核模式的模块中,只能调用 Native API 的一个子集 。另一方面,ntoskrnl.exe 导出了两个 ntdll.dll 没有提供的 Nt* 符号(指以 Nt 开头的符号): NtBuildNumber 和 NtGlobalFlag。这两个符号都没有指向函数的入口地址,而是指向 ntoskrnl.exe 中的变量 。驱动模块( driver module )可以使用 C 编译器的 extern 关键字来导入这些变量 。Window 2000 采用此种方式导出了很多变量,稍后我将给出一个示例代码来使用其中的几个 。
你可能会奇怪为什么 表 B-1 (位于附录 B 中)分别为 ntdll.dll 和 ntoskrnl.exe 提供了两列,其名称分别为: ntdll.Nt* 、 ntdll.Zw* 和 ntoskrnl.Nt* 、 ntoskrnl.Zw*。原因是,这两个模块导出了两组相互关联的 Native API 符号 。在 表 B-1 (位于附录 B 中)的最左列给出了所有名字中包含 Nt 前缀的符号 。另一个集合包含相似的名字,不过由 Zw 前缀代替了 Nt。反编译 ndll.dll 可看出每对符号都指向相同的代码 。这看起来似乎是浪费内存 。然而,如果你反编译 ntoskrnl.exe,你就会发现 Nt* 符号指向实际的代码而 Zw* 指向 INT 2eh stubs (如 示列 2-1 列出的) 。这意味着 Zw* 函数集合将从用户模式转入内核模式,而 Nt* 符号直接指向的代码会在模式切换后被执行 。

推荐阅读