第二章 The Windows 2000 Native API
翻译:Kendiv(fcczj@263.net)
更新: Friday, February 04, 2005
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
Windows 2000运行时库
Nt*()和Zw*()函数构成了Native API的基本部分,但并不是主要部分,还有一部分代码位于ntdll.dll中。该DLL至少导出了1179个符号。其中的249和248个分别属于Nt*()和Zw*()函数集,剩余的682个函数并不通过INT 2eh中断进行调用。显然,这一大组函数并不依赖Windows 2000内核。那提供它们的目的何在呢?让我们继续往下看。
C运行时库
如果你研究过位于ntdll.dll导出节(export section)的符号,你会发现很多在C程序员看来很熟悉的小写的函数名称。这些都是众所周知的名子,如memcpy()、sprintf()和qsort(),这些C运行时库中的函数都合并到了ntdll.dll中。对于ntoskrnl.exe也是如此,它同样提供了一组与C运行时函数十分相像的函数,虽然这两组函数并不相同。附录B的表B-3列出了这两组函数,并指出了每个函数分别属于哪个模块。
你可以简单的将ntdll.lib(来自Windows 2000 DDK)添加到导入库列表(链接器在解析符号期间将扫描该列表)中,就可以链接到这些函数。如果你更喜欢对话框,你可以选择Visual C/C++的工程菜单中的Settings子菜单,然后单击Linke页,选择Category General,然后将ntdll.dll添加到Object/Library模块列表中。还有一种方法:在源文件中,添加如下的内容:
#pragma comment(linker,”/defaultlib:ntdll.lib”)
这同样有效,好处是,其他开发人员可以使用Visual C/C++的默认设置来rebuild你的工程。
反编译这些与C运行时函数类似的函数(来自ntdll.dll和ntoskrnl.exe),会发现ntdll.dll并不依赖于ntoskrnl.exe,这和ndll.dll中的Native API不一样。事实上,这两个模块分别实现了这些函数。本节出现的其他函数也是如此。注意,表B-3中的一些函数并不使用其导出的名称。例如,如果在内核模式的驱动程序中针对一个64位的LARGE_INTEGER使用移位操作符<<和>>,编译器和链接器会自动导入ntoskrnl.exe的_allshr()和_allshl()。
扩展的运行时函数
随同标准的C运行时函数,Windows 2000还提供了一组扩展的运行时函数。在次强调,ntdll.dll和ntoskrnl.exe分别实现了它们。并且其中有些函数是重叠的。这些扩展函数的名字都有一个共同的前缀Rtl(for Runtime Library)。附录B的表B-4列出了所有这些扩展函数。Windows 2000提供的这些运行时函数还包含用于普通任务的助手函数(helper function),这些任务都超过了C运行时函数的能力范围。例如,其中的某些用于管理安全性,另一些用于操作Windows 2000特有的数据结构,还有一些对内存管理提供支持。很难理解为什么微软仅在Windows 2000 DDK中提供了其中115个函数的文档,而扔掉了其余406个非常有用的函数。
浮点模拟器(The Floating-Point Emulator)
让我用ntdll.dll提供的另一组函数集合来结束这次API函数汇展。表2-1列出了这些函数的名称,这些名称可能对于汇编程序员有些眼熟。去了名称前的__e前缀,你就会得到i386系列CPU中的FPU(Floating-Point Unit)汇编助记符。事实上,从表2-1中列出的函数来看,ntdll.dll包含了一个完整的浮点模拟器。这再次证明了这个DLL是一个庞大的代码仓库,这吸引了众多的System Spelunker去反编译它。
表2-1. ntdll.dll的浮点模拟器接口
函数名称 | |||
_eCommonExceptions | _eFIST32 | _eFLD64 | _eFSTP32 |
_eEnulatorInit | _eFISTP16 | _eFLD80 | _eFSTp64 |
_eF2XM1 | _eFISTP32 | _eFLDCW | _eFSTP80 |
_eFABS | _eFISTP64 | _eFLDENV | _eFSTSW |
_eFADD32 | _eFISUB16 | _eFLDL2E | _eFSUB32 |
_eFADD64 | _eFISUB32 | _eFLDLN2 | _eFSUB64 |
_eFADDPreg | _eFISUBR16 | _eFLDPI | _eFSUBPreg |
_eFADDreg | _eFISUBR32 | _eFLDZ | _eFSUBR32 |
_eFADDtop | _eFLDI | _eFMUL32 | _eFSUBR64 |
_eFCHS | _eFIDIVR16 | _eFMUL64 | _eFSUBreg |
_eFCOM | _eFIDIVR32 | _eFMULPreg | _eFSUBRPreg |
_eFCOM32 | _eFILD16 | _eFMULreg | _eFSUBRreg |
_eCOM64 | _eFILD32 | _eFMULtop | _eFSUBRtop |
_eFCOMP | _eFILD64 | _eFPATAN | _eFSUBtop |
_eFCOMP32 | _eFIMUL16 | _eFPREm | _eFTST |
_eFCOMP64 | _eFIMUL32 | _eFPREM1 | _eFUCOM |
_eFCOMPP | _eFINCSTP | _eFPTAN | _eFUCOMP |
_eFCOS | _eFINIT | _eFRNDINT | _eFUCOMPP |
_eFDECSTP | _eFIST16 | _eFRSTOR | _eFXAM |
_eFIDIVR16 | _eFIST32 | _eFSAVE | _eFXCH |
_eFIDIVR32 | _eFISTP16 | _eFSCALE | _eFXTRACT |
_eFILD16 | _eFISTP32 | _eFSIN | _eFYL2X |
_eFILD32 | _eFISTP64 | _eFSQRT | _eFYL2XP1 |
_eFILD64 | _eFISUB16 | _eFST | _eGetStatusWord |
_eFIMUL16 | _eFISUB32 | _eFST32 | NPXEMULATORTABLE |
_eFIMUL32 | _eFISUBR16 | _eFST64 | RestoreEm87Context |
_eFINCSTP | _eFISUBR32 | _eFSTCW | SaveEm87Context |
_eFINIT | _eFLD16 | _eFSTENV |
|
_eFIST16 | _eFLD32 | _eFSTP |
|
有关浮点指令集的更多信息,请参考Intel 80386 CPU的原始文档。可以从Intel官方网站:http://developer.intel.com/design/pentium/manuals/来下载PDF格式的Pentium手册。讲解这些机器码指令集的手册是:Intel Architecture SoftWare Developer’s Manual. Volume 2:Instruction Set Reference(Intel 1999b)。
其它的API函数
除附录B和表2-1列出的函数外,ntdll.dll和ntoskrnl.exe还为多个内核组件导出了为数众多的函数。为了避免更长的表格,我这里仅列出可用函数的名称前缀及其所属类别(表2-2)。
表2-2 函数名前缀及其所属分类
前缀 | ntdll.dll | ntoskrnl.exe | 分类 |
_e |
| N/A | 浮点模拟器 |
Cc |
| N/A | Cache管理器 |
Csr |
|
| Client-Server运行时库 |
Dbg | N/A |
| 调试支持 |
Ex | N/A |
| 执行支持(Executive Support) |
FsRtl | N/A |
| 文件系统运行时库 |
Hal | N/A |
| 硬件抽象层调度器 |
Inbv | N/A |
| 系统初始化/VGA启动驱动(bootvid.dll) |
Init | N/A |
| 系统初始化 |
Interlocked | N/A |
| 处理线程安全的变量 |
Io | N/A |
| I/O管理器 |
Kd | N/A |
| 内核调试支持 |
Ke | N/A |
| 内核例程 |
Ki |
|
| 内核中断例程 |
Ldr |
|
| 映像加载器 |
Lpc | N/A |
| 本地过程调用(LPC)设备 |
Lsa | N/A |
| 本地安全授权 |
Mm | N/A |
| 内存管理器 |
Nls |
|
| National Language Support (NLS) |
Nt |
|
| NT Native API |
Ob | N/A |
| 对象管理器 |
Pfx |
|
| 前缀处理 |
Po | N/A |
| 电源管理器 |
Ps | N/A |
| 进程支持 |
READ_REGISTER_ | N/A |
| 从寄存器地址中读取 |
Rtl |
|
| Windows 2000运行时库 |
Se | N/A |
| 安全处理 |
WRITE_REGISTER_ | N/A |
| 向寄存器地址中写入 |
Zw |
|
| 另一组Native API |
<other> |
|
| 帮助函数和C运行时库 |
很多内核函数都使用统一的命名规则----PrefixOperationObject()。例如,NtQueryInformationFile()函数属于Native API,这是因为其Nt前缀,而且该函数显然针对一个文件对象执行了QueryInformation操作。但并不是所有函数都遵循这一规则,不过绝大多数都是如此。因此,可以很容易的通过函数的名称猜测其功能。
经常使用的数据类型
当编写与Windows 2000内核有关的软件时---不管是和用户模式的ntdll.dll还是和内核模式的ntoskrnl.exe,你都必须处理几个基本的数据类型,而这些数据类型在Win32世界里非常少见。它们中的多数都会在本书中反复出现。下面的章节将介绍使用频率最高的数据类型。
整型
一般说来,整数类型有多个不同的变体。Win32 SDK的头文件和SDK文档使用了其专有的术语,这些术语很容易和C/C++的基本类型以及一些派生类型相混淆。表2-3列出了这些整数类型,以及它们之间的等价关系。在“MASM”列中,给出了微软宏汇编语言(MASM)使用的类型名称。Win32 SDK为C/C++的基本数据类型定义了对应的BYTE、WORD、DWORD别名。“别名1”和“别名2”两列包含其经常使用的别名。例如,WCHAR代表基础的Unicode字符类型。最后一列“有符号的”,列出了对应的有符号类型的常见别名。一定要记住ANSI字符类型CHAR是有符号的,而Unicode类型WCHAR是无符号的。当编译器将表达式或计算中的这些类型转换为整数类型时,这种不一致性将导致意外的错误。
表2-3最后一行的MASM的TBYTE类型(读做“10-byte”)是一个80位的浮点数,用于高精度的浮点运算操作。Microsoft Visual C/C++没有为Win32程序员提供对应的数据类型。需要注意的是,MASM的TBYTE和Win32的TBYTE(读做“text byte”)没有任何关系,后者只是一个用于转换的宏,根据源文件中是否有#define UNICODE而分别对应CHAR或WCHAR。
表2-3. 等价的整数类型
位数 | MASM | 基本类型 | 别名1 | 别名2 | 有符号的 |
8 | BTYE | unsigned char | UCHAR |
| CHAR |
16 | WORD | unsigned short | USHORT | WCHAR | SHORT |
32 | DWORD | unsigned long | ULONG |
| LONG |
32 | DWORD | unsigned int | UINT |
| INT |
64 | QWORD | unsigned __int64 | ULONGLONG | DWORDLONG | LONGLONG |
80 | TBYTE | N/A |
|
|
|
由于在32位编程环境中较难处理64位整数,Windows 2000通常不提供64位的基本类型,如__int64或其派生类型。替代的,DDK头文件ntdef.h中定义了一个精巧的union结构,可以将一个64位数解释为一对32位数或一个完整的64位数,参见列表2-3给出了LARGE_INTEGER和ULARGE_INTEGER类型定义。该类型可分别表示有符号和无符号的整数。通过使用LONGLONG/ULONGLONG(针对64位的QuadPart成员)或者LONG/ULONG(针对32位的HighPart成员)来控制有无符号。
typedef union _LARGE_INTEGER
{
struct
{
ULONG LowPart;
LONG HighPart;
}
LONGLONG QuadPart;
} LARGE_INTEGER,*PLARGE_INTEGER;
typedef union _ULARGE_INTEGER
{
struct
{
ULONG LowPart;
ULONG HighPat;
}
ULONGLONG QuadPat;
} ULARGE_INTEGER,*PULARGE_INTEGER;
列表2-3. LARGE_INTEGER和ULARGE_INTEGER
字符串
在Win32程序设计中,常使用PSTR和PWSTR来分别代替ANSI和Unicode字符串。PSTR被定义为CHAR*,PWSTR则定义为WCHAR*(参见表2-3)。通过源文件中是否出现#define UNICODE指示符,附加的PTSTR类型分别对应PSTR或PWSTR,这样就可通过单一的源文件来维护应用程序的ANSI和Unicode版本。基本上,这些字符串都是简单的指向以零结尾的CHAR或WCHAR类型的数组。如果你常和Windows 2000内核打交道,你将必须处理一种很不同的字符串表示法。最常见的类型是UNICODE_STRING,这是一个第三方类型,列表2-4给出了它的定义。
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING,*PUNICODE_STRING;
typedef struct _STRING
{
USHORT Length;
USHORT MaximumLength;
PCHAR Buffer;
} STRING, *PSTRING;
typedef STRING ANSI_STRING, *PANSI_STRING;
typedef STRING OEM_STRING, *POEM_STRING;
列表2-4. 字符串类型
Length成员给出了当前字符串的字节数(注意,不是字符个数),MaximumLength成员指出Buffer所指向内存块的大小,实际的字符串数据将保存在该内存块中。注意,MaximumLength也是字节数。由于Unicode字符宽度为16位,所有其长度总是字符个数的两倍。通常,Buffer指向的字符串都是以零结尾的。然而,有些内核模块可能仅依赖字符串的长度值,而不考虑结尾的0字符,这种情况下要小心处理。
Windows 2000的ANSI字符串叫做STRING,如列表2-4中所示。为了方便,nedef.h分别定义了ANSI_STRING和OEM_STRING来代表使用不同代码页的8位字符串(ANSI默认代码页为1252;OEM默认代码页为437)。不过,Windows 2000内核使用的主要字符串类型还是UNICODE_STRING。你可能偶尔会碰到8位字符串。
在图2-3中,我给出了两个典型的UNICODE_STRING示例。左面的那个包含两个独立的内存块:一个UNICODE_STRING结构和一个16位PWCHAR类型的Unicode字符数组。这或许是在Windows 2000数据类型中最常见的字符串类型。右边的是一种频繁出现的特殊类型,在此种类型中,UNICODE_STRING和PWCHAR数组位于同一个内存块中。有些内核函数,包括Native API内部使用的一些函数,都在连续的内存块中保存其返回的结构化的系统信息。如果数据中包含字符串,它们通常都存储在嵌入式的UNICODE_STRING中,如图2-3右面所示。例如,NtQuerySystemInformation()函数就频繁使用了这种特殊的字符串类型。
这些字符串结构不许要手工维护,ntdll.dll和ntoskrnl.exe导出了一组丰富的运行时API函数,如RtlCreateUnicodeString()、RtlInitUnicodeString()、RtlCopyUnicodeString()等。通常,STRING和ANSI_STRING也有对应的等价函数。这些函数中的大多数在DDK中都有文档记录,但其中有些没有。不过,很容易猜出这些未文档化的字符串函数的功能及其需要的参数。使用UNICODE_STRING、STRING的好处是,可以隐示的指定Buffer可容纳的字符串的大小。如果你给一个函数传递了一个UNICODE_STRING类型的字符串,而该函数需要适当改变该字符串的值,而这可能会增加该字符串的长度,那这个函数只需要简单的检查MaximumLength成员就可确定是否有足够的空间来存放结果。
结构体
个别的几个内核API函数期望其处理的对象有一个合适的OBJECT_ATTRIBUTES结构,列表2-5给出了该结构的定义。例如,NtOpenFile()函数没有PWSTR或PUNICODE_STRING参数用来指定要打开的文件的路径。替代的,OBJECT_ATTRIBUTES结构中的ObjectName成员给出了该路径。通常,设置该结构很容易。除ObjectName外,还需要设置Length和Attributes成员。Length必须设置为:sizeof(OBJECT_ATTRIBUTES),Attributes是一组来自ntdef.h的OBJ_*常量。例如,如果你对象名称不区分大小写的话,Attributes应设置为OBJ_CASE_INSENSITIVE。当然,ObjectName成员是一个UNICODE_STRING指针,并不是通常的PWSTR。剩余的成员只要不使用,都可设置为NULL。
typedef struct _OBJECT_ATTRIBUTES
{
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;
列表2-5. OBJECT_ATTRIBUTES结构
OBJECT_ATTRIBUTES结构仅描述函数使用的数据的细节,列表2-6给出的IO_STATUS_BLOCK结构则用于记录对用户所提交的操作的处理结果。该结构很简单---Staus成员存放一个NTSTATUS类型的代码,其值可能是STATUS_SUCCESS或定义于ntstatus.h中的所有可能的错误代码。Information成员在操作成功的情况下,提供与操作相关的附加数据。比如,如果函数返回一个数据块,该成员将被设置为该数据块的大小。
typedef struct _IO_STRATUS_BLOCK
{
NTSTATUS Status;
ULONG Information;
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;
列表2-6. IO_STATUS_BLOCK结构
另一个常见的Windows 2000数据类型是LIST_ENTRY结构,列表2-7给出了该结构的定义。内核使用该结构将所有对象维护在一个双向链表中。一个对象分属多个链表是很常见的,Flink成员是一个向前链接,指向下一个LIST_ENTRY结构,Blink成员则是一个向后链接,指向前一个LIST_ENTRY结构。通常情况下,这些链表都成环形,也就是说,最后一个Flink指向链表中的第一个LIST_ENTRY结构,而第一个Blink指向最后一个。这样就很容易双向遍历该链表。如果一个程序要遍历整个链表,它需要保存第一个LIST_ENTRY结构的地址,以判断是否已遍历了整个链表。如果链表仅包含一个LIST_ENTRY结构,那么该LIST_ENTRY结构必须引用其自身,也就是说,Flink和Blink都指向其自己。
typedef struct _LIST_ENTRY
{
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;
列表2-7. LIST_ENTRY结构
图2-4展示了对象链表各成员间的关系。对象A1、A2、A3属于同一链表。注意,A3的Flink指向A1,A1的Blink指向A3。最右边的对象B1仅有一个成员,因此,其Flink和Blink都指向相同的地址---即对象B1的地址。典型的双向链表的例子是进程和线程链表。内部变量PsActiveProcessHead就是一个LIST_ENTRY结构,位于ntoskrnl.exe的.data节中。该变量指向系统进程列表的首部(通过其Blink指针)。你可以在内核调试器中使用dd PsActiveProcessHead来获取该链表的首部,然后通过其Flink和Blink指针遍历整个链表(仍使用dd命令)。当然,这种探测Windows进程的方法非常繁琐,但这可使你深入的观察基本的系统结构。Windows 2000 Native API提供了更便利的方法来枚举进程,如NtQuerySystemInformation()函数。
typedef struct _CLIENT_ID
{
HANDLE UniqueProcess;
HANDLE UniqueThread;
} CLIENT_ID, *PCLIENT_ID;
列表2-8. CLIENT_ID结构
处理进程和线程的API函数,如:NtOpenProcess()和NtOpenThread(),使用列表2-8给出的CLIENT_ID结构来和特定的进程、线程相关联。尽管其类型为HANDLE,实际上,从严格的意义上来讲UniqueProcess和UniqueThread成员并不是句柄(Handle),它们都是整数型的进程ID和线程ID。即标准Win32函数GetCurrentProcessId()和GetCurrentThreadId()返回的DWORD类型的数值。
Windows 2000执行体(Executive)还使用CLIENT_ID结构在全局范围内标识唯一的线程。例如,如果你使用内核调试器的!thread命令来显示当前线程参数,就会在输出的第一行看到类似“Cid ppp.ttt”的显示,其中“ppp”就是CLIENT_ID的UniqueProcess成员,而“ttt”则代表UniqueThread,如下所示。注意,我用黑体标出的地方。
kd> !thread
THREAD 83a51ba8 Cid 0a5c.0e64 Teb: 7ffdd000 Win32Thread: e14f4eb0 RUNNING on processor 0
Not impersonating
DeviceMap e20fb208
Owning Process 83a14708
Wait Start TickCount 906512 Elapsed Ticks: 68570
Context Switch Count 266 LargeStack
UserTime 00:00:00.0312
KernelTime 00:00:00.0015
。。。。。。。。。。。。。。。。。。。
Native API的接口
对于内核模式的驱动程序,使用Native API的接口非常平常,就像在用户模式下的程序中调用Win32 API一样。Windows 2000 DDK提供的头文件和库包含了所有在调用ntoskrnl.exe导出的Native API时所需的信息。而另一方面,Win32 SDK几乎不支持在程序中调用ntdll.dll导出的Native API。我说“几乎不”是因为Win32 SDK实际上提供了一个重要的东西:导入库ntdll.lib,该文件位于/Program Files/Microsoft Platfrom SDK/Lib目录中。如果没有这个库,将很难调用ntdll.dll导出的函数。
译注:
你需要安装Windows 2000 DDK才能获得ntdll.lib
可以到 http://www.microsoft.com/msdownload/platformsdk/sdkupdate/ 下载最新的SDK
将NTDLL.DLL导入库添加到工程中
在你能成功的编译和链接在用户模式下使用ntdll.dll导出函数的代码之前,你必须考虑如下的四个重点:
1. SDK的头文件中,没有包含这些函数的原型。
2. SDK文件中缺少这些函数使用的几个基本的数据类型。
3. SDK和DDK头文件并不兼容,你不能将#include <ntddk.h>加入你的Win32 C源代码文件中。
4. ntdll.lib并没有加入Visual C/C++默认的导入库列表中
最后一个问题很容易解决,只需要编辑工程的设置属性,或者将如下内容加入你的源代码中,#pragma comment(linker,”defaultlib:ntdll.lib”),像在前面的Windows 2000运行时库一节解释的那样,这会在编译时,将ntdll.dll加入链接器的/defaultlib设置中。解决缺失的定义比较困难。因为不可能将SDK和DDK头文件整合到C程序中,最简易的解决方法是写一格自定义的头文件,在该头文件中包含所有调用ntdll.dll导出函数必须的定义。幸运的是,你不需要开始这项工作了,在本书光盘的/src/common/include目录下的w2k_def.h文件包含了你所需要的所有基本信息。该头文件将在第六、七两章中扮演重要角色。因为它被设计为可同时兼容用户模式和内核模式的工程,在用户模式代码中,你必须在#include <w2k_def.h>之前插入#define _USER_MODE_,以加入仅出现在DDK中的一些定义。
有关Native API编程的很多详细信息都已经出版,目前看来,针对Windows 2000平台的好书是Gary Nebbett’s的《Windows NT/2000 Native API Reference》。该书提供的示例程序较少,但它覆盖了Windows NT/2000平台上的所有Native API,还包括这些函数需要的数据结构定义以及其他必须的一些结构定义。
将在第六章介绍的w2k_call.dll示例库,演示了w2k_def.h的典型用法。第六章还将讨论另一种在用户模式进入Windows 2000内核的方法,此种方法不受限于Native API。事实上,这种技巧也可用于ntoskrnl.exe,对于所有加载到内核空间的模块,只要它们导出了函数或者可以和.dbg或.pdb符号文件相匹配都可以使用此方法。如你所见,在本书剩余章节中还有很多有趣的信息。但是,在我们到达那儿之前,我们会继续讨论一些基本的概念和技术。
< 本章完 >