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

来源:互联网 发布:日本交友聊天软件 编辑:程序博客网 时间:2024/06/11 17:45

第二章  The Windows 2000 Native API

翻译:Kendiv(fcczj@263.net)

更新:Friday, February 04, 2005

 

声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。

Windows 2000运行时库

Nt*()Zw*()函数构成了Native API的基本部分,但并不是主要部分,还有一部分代码位于ntdll.dll中。该DLL至少导出了1179个符号。其中的249248个分别属于Nt*()Zw*()函数集,剩余的682个函数并不通过INT 2eh中断进行调用。显然,这一大组函数并不依赖Windows 2000内核。那提供它们的目的何在呢?让我们继续往下看。

 

C运行时库

如果你研究过位于ntdll.dll导出节(export section)的符号,你会发现很多在C程序员看来很熟悉的小写的函数名称。这些都是众所周知的名子,如memcpy()sprintf()qsort(),这些C运行时库中的函数都合并到了ntdll.dll中。对于ntoskrnl.exe也是如此,它同样提供了一组与C运行时函数十分相像的函数,虽然这两组函数并不相同。附录BB-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.dllntoskrnl.exe),会发现ntdll.dll并不依赖于ntoskrnl.exe,这和ndll.dll中的Native API不一样。事实上,这两个模块分别实现了这些函数。本节出现的其他函数也是如此。注意,表B-3中的一些函数并不使用其导出的名称。例如,如果在内核模式的驱动程序中针对一个64位的LARGE_INTEGER使用移位操作符<<>>,编译器和链接器会自动导入ntoskrnl.exe_allshr()_allshl()

 

扩展的运行时函数

随同标准的C运行时函数,Windows 2000还提供了一组扩展的运行时函数。在次强调,ntdll.dllntoskrnl.exe分别实现了它们。并且其中有些函数是重叠的。这些扩展函数的名字都有一个共同的前缀Rtlfor Runtime Library)。附录BB-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中的FPUFloating-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 2Instruction Set ReferenceIntel 1999b)。

 

其它的API函数

附录B2-1列出的函数外,ntdll.dllntoskrnl.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 SDKC/C++的基本数据类型定义了对应的BYTEWORDDWORD别名。“别名1”和“别名2”两列包含其经常使用的别名。例如,WCHAR代表基础的Unicode字符类型。最后一列“有符号的”,列出了对应的有符号类型的常见别名。一定要记住ANSI字符类型CHAR是有符号的,而Unicode类型WCHAR是无符号的。当编译器将表达式或计算中的这些类型转换为整数类型时,这种不一致性将导致意外的错误。

 

2-3最后一行的MASMTBYTE类型(读做“10-byte”)是一个80位的浮点数,用于高精度的浮点运算操作。Microsoft Visual C/C++没有为Win32程序员提供对应的数据类型。需要注意的是,MASMTBYTEWin32TBYTE(读做“text byte”)没有任何关系,后者只是一个用于转换的宏,根据源文件中是否有#define UNICODE而分别对应CHARWCHAR

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_INTEGERULARGE_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_INTEGERULARGE_INTEGER

字符串

Win32程序设计中,常使用PSTRPWSTR来分别代替ANSIUnicode字符串。PSTR被定义为CHAR*PWSTR则定义为WCHAR*(参见表2-3)。通过源文件中是否出现#define UNICODE指示符,附加的PTSTR类型分别对应PSTRPWSTR,这样就可通过单一的源文件来维护应用程序的ANSIUnicode版本。基本上,这些字符串都是简单的指向以零结尾的CHARWCHAR类型的数组。如果你常和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 2000ANSI字符串叫做STRING,如列表2-4中所示。为了方便,nedef.h分别定义了ANSI_STRINGOEM_STRING来代表使用不同代码页的8位字符串(ANSI默认代码页为1252OEM默认代码页为437)。不过,Windows 2000内核使用的主要字符串类型还是UNICODE_STRING。你可能偶尔会碰到8位字符串。

 

2-3中,我给出了两个典型的UNICODE_STRING示例。左面的那个包含两个独立的内存块:一个UNICODE_STRING结构和一个16PWCHAR类型的Unicode字符数组。这或许是在Windows 2000数据类型中最常见的字符串类型。右边的是一种频繁出现的特殊类型,在此种类型中,UNICODE_STRINGPWCHAR数组位于同一个内存块中。有些内核函数,包括Native API内部使用的一些函数,都在连续的内存块中保存其返回的结构化的系统信息。如果数据中包含字符串,它们通常都存储在嵌入式的UNICODE_STRING中,如2-3右面所示。例如,NtQuerySystemInformation()函数就频繁使用了这种特殊的字符串类型。

 

这些字符串结构不许要手工维护,ntdll.dllntoskrnl.exe导出了一组丰富的运行时API函数,如RtlCreateUnicodeString()RtlInitUnicodeString()RtlCopyUnicodeString()等。通常,STRINGANSI_STRING也有对应的等价函数。这些函数中的大多数在DDK中都有文档记录,但其中有些没有。不过,很容易猜出这些未文档化的字符串函数的功能及其需要的参数。使用UNICODE_STRINGSTRING的好处是,可以隐示的指定Buffer可容纳的字符串的大小。如果你给一个函数传递了一个UNICODE_STRING类型的字符串,而该函数需要适当改变该字符串的值,而这可能会增加该字符串的长度,那这个函数只需要简单的检查MaximumLength成员就可确定是否有足够的空间来存放结果。

 

结构体

个别的几个内核API函数期望其处理的对象有一个合适的OBJECT_ATTRIBUTES结构,列表2-5给出了该结构的定义。例如,NtOpenFile()函数没有PWSTRPUNICODE_STRING参数用来指定要打开的文件的路径。替代的,OBJECT_ATTRIBUTES结构中的ObjectName成员给出了该路径。通常,设置该结构很容易。除ObjectName外,还需要设置LengthAttributes成员。Length必须设置为:sizeof(OBJECT_ATTRIBUTES)Attributes是一组来自ntdef.hOBJ_*常量。例如,如果你对象名称不区分大小写的话,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结构必须引用其自身,也就是说,FlinkBlink都指向其自己。

 

 

typedef struct  _LIST_ENTRY

{

       struct  _LIST_ENTRY  *Flink;

       struct  _LIST_ENTRY  *Blink;

} LIST_ENTRY, *PLIST_ENTRY;

列表2-7.   LIST_ENTRY结构

 

2-4展示了对象链表各成员间的关系。对象A1A2A3属于同一链表。注意,A3Flink指向A1A1Blink指向A3。最右边的对象B1仅有一个成员,因此,其FlinkBlink都指向相同的地址---即对象B1的地址。典型的双向链表的例子是进程和线程链表。内部变量PsActiveProcessHead就是一个LIST_ENTRY结构,位于ntoskrnl.exe.data节中。该变量指向系统进程列表的首部(通过其Blink指针)。你可以在内核调试器中使用dd PsActiveProcessHead来获取该链表的首部,然后通过其FlinkBlink指针遍历整个链表(仍使用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,实际上,从严格的意义上来讲UniqueProcessUniqueThread成员并不是句柄(Handle),它们都是整数型的进程ID和线程ID。即标准Win32函数GetCurrentProcessId()GetCurrentThreadId()返回的DWORD类型的数值。

 

Windows 2000执行体(Executive)还使用CLIENT_ID结构在全局范围内标识唯一的线程。例如,如果你使用内核调试器的!thread命令来显示当前线程参数,就会在输出的第一行看到类似“Cid ppp.ttt”的显示,其中“ppp”就是CLIENT_IDUniqueProcess成员,而“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.         SDKDDK头文件并不兼容,你不能将#include <ntddk.h>加入你的Win32 C源代码文件中。

4.         ntdll.lib并没有加入Visual C/C++默认的导入库列表中

 

最后一个问题很容易解决,只需要编辑工程的设置属性,或者将如下内容加入你的源代码中,#pragma comment(linker,”defaultlib:ntdll.lib”),像在前面的Windows 2000运行时库一节解释的那样,这会在编译时,将ntdll.dll加入链接器的/defaultlib设置中。解决缺失的定义比较困难。因为不可能将SDKDDK头文件整合到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符号文件相匹配都可以使用此方法。如你所见,在本书剩余章节中还有很多有趣的信息。但是,在我们到达那儿之前,我们会继续讨论一些基本的概念和技术。

 

< 本章完 >