|
作者:sunwear[E.S.T] shellcoder@163.com
! L& J6 ^/ @* U# ]& {来源:邪恶八进制 中国
Y& M+ x$ ~7 J3 j& R. [5 g此文只能说是一篇笔记,是关于本机API的.本机API是除了Win32 API,NT平台开放了另一个基本接口。本
, a- y# X s& V9 v% H6 _; S机API也被很多人所熟悉,因为内核模式模块位于更低的系统级别,在那个级别上环境子系统是不可见的
/ V8 E$ K: j6 P; `# H。尽管如此,并不需要驱动级别去访问这个接口,普通的Win32程序可以在任何时候向下调用本机API。并 2 _! s% M1 B9 A g z
没有任何技术上的限制,只不过微软不支持这种应用开发方法。 / C3 R7 d: o; k/ I5 l* C
" ]7 L* v- w( O( |+ O4 _+ S# I
User32.dll,kernel32.dll,shell32.dll,gdi32.dll,rpcrt4.dll,comctl32.dll,advapi32.dll,version.d 8 E$ e* ]) O5 K2 n K7 h
ll等dll代表了Win32 API的基本提供者。Win32 API中的所有调用最终都转向了ntdll.dll,再由它转发至 + H# m+ |% D* ?. N/ `$ G
ntoskrnl.exe。ntdll.dll是本机 API用户模式的终端。真正的接口在ntoskrnl.exe里完成。事实上,内
2 d4 l! }9 e0 L% J1 G核模式的驱动大部分时间调用这个模块,如果它们请求系统服务。Ntdll.dll的主要作用就是让内核函数 8 j& z. ~5 ?) q2 ~' W
的特定子集可以被用户模式下运行的程序调用。Ntdll.dll通过软件中断int 2Eh进入ntoskrnl.exe,就是 " B$ p9 k3 }6 S$ e
通过中断门切换CPU特权级。比如kernel32.dll导出的函数DeviceIoControl()实际上调用ntdll.dll中导 8 U# K$ |- j2 l' t H
出的NtDeviceIoControlFile(),反汇编一下这个函数可以看到,EAX载入magic数0x38,实际上是系统调 0 g1 w! C" T0 M( z0 q" _
用号,然后EDX指向堆栈。目标地址是当前堆栈指针ESP+4,所以EDX指向返回地址后面一个,也就是指向
. t5 b* f {, z: E! c$ Y, m在进入NtDeviceIoControlFile()之前存入堆栈的东西。事实上就是函数的参数。下一个指令是int 2Eh,
0 t Y C+ z" F5 g8 ]转到中断描述符表IDT位置0x2E处的中断处理程序。 ' K& j2 Z2 L( A! _' ]
反编汇这个函数得到: ; s0 M! K, `+ o) l8 ?
mov eax, 38h
* a) D" K- @3 ulea edx, [esp+4]
4 J' x2 m0 h( r$ Q1 J9 S& b0 z# nint 2Eh
- Y5 W/ s0 Z7 v- T% C" z8 Gret 28h 9 E# G0 j/ C! J; l2 D; I) y5 H
当然int 2E接口不仅仅是简单的API调用调度员,他是从用户模式进入内核模式的main gate。 & w8 U) m% Z' P3 s+ M$ |) _) r: V
W2k Native API由248个这么处理的函数组成,比NT 4.0多了37个。可以从ntdll.dll的导出列表中很容易 ' J1 Q3 A8 G$ h8 O8 z
认出来:前缀Nt。Ntdll.dll中导出了249个,原因在于NtCurrentTeb()为一个纯用户模式函数,所以不需 4 k% @5 y( C# W7 F+ z
要传给内核。令人惊奇的是,仅仅Native API的一个子集能够从内核模式调用。而另一方面,
3 w' }( G7 q+ P* qntoskrnl.exe导出了两个Nt*符号,它们不存在于ntdll.dll中: NtBuildNumber, NtGlobalFlag。它们不 / P/ B' }0 G: @/ u% }) a- A4 W" `
指向函数,事实上,是指向ntoskrnl.exe的变量,可以被使用C编译器extern关键字的驱动模块导入。 - U+ E) ^! o2 v8 M4 Q$ ~- I7 G
Ntdll.dll和ntoskrnl.exe中都有两种前缀Nt*,Zw*。事实上ntdll.dll中反汇编结果两者是一样的。而在
3 A* F% g v9 mntoskrnl.exe中,nt前缀指向真正的代码,而zw还是一个int 2Eh的stub。也就是说zw*函数集通过用户模 ! e' [( N8 P6 N9 X
式到内核模式门传递的,而Nt*符号直接指向模式切换以后的代码。Ntdll.dll中的NtCurrentTeb()没有相 % F4 q, G; j) [' T5 v
对应的zw函数。Ntoskrnl并不导出配对的Nt/zw函数。有些函数只以一种方式出现。
2 X; D7 _. i0 ~! v$ E O: d2Eh中断处理程序把EAX里的值作为查找表中的索引,去找到最终的目标函数。这个表就是系统服务表SST - n( Y6 ?" J% Q, o8 U& c4 y( K- q7 D
,C的结构SYSTEM_SERVICE_TABLE的定义如下:清单也包含了结构SERVICE_DESCRIPTOR_TABLE中的定义,为 1 N. J: b8 Q8 _
SST数组第四个成员,前两个有着特别的用途。
$ z0 {+ _& B- |8 i: I; s% v7 Otypedef NTSTATUS (NTAPI *NTPROC) ( ) ; " T# L0 O4 v4 Y8 \# o8 V
typedef NTPROC *PNTPROC;
# N. C, Q1 a3 ]* g6 b8 A#define NTPROC_ sizeof (NTPROC) % @/ K, _! O W2 n# U
typedef struct _SYSTEM_SERVICE_TABLE 9 r0 w& k2 z, q0 F/ y
{ PNTPROC ServiceTable; // 这里是入口指针数组
% q, b" E$ M! V0 A9 A! oPDWORD CounterTable; // 此处是调用次数计数数组
; ^# D7 _5 Q$ a5 ^2 NDWORD ServiceLimit ; // 服务入口的个数
0 d. O( o" S" ZPBYTE ArgumentTable; // 服务参数字节数的数组
; R6 Y2 M3 C+ a2 Z) SYSTEM_SERVICE_TABLE ,
9 J' O6 E' f/ F* PSYSTEM_SERVICE_TABLE ,
+ R5 O5 }" k( ]" h8 I* * PPSYSTEM_SERVICE_TABLE ;
9 ]9 J4 v w( W5 E' u2 d/ v/ / _ _ _ _ _ _ _ _ _ _ _ _
; M+ U# x1 g7 H! ]typedef struct _SERVICE_DESCRIPTOR_TABLE
9 i* L* Y1 Q2 M{ SYSTEM_SERVICE_TABLE ntoskrnl ; // ntoskrnl所实现的系统服务,本机的API}
+ U; \# q0 V2 s1 V5 uSYSTEM_SERVICE_TABLE win32k; // win32k所实现的系统服务 & x N0 H2 Z* U, \% m; x
SYSTEM_SERVICE_TABLE Table3; // 未使用
4 w3 H) q+ t+ H& m YSYSTEM_SERVICE_TABLE Table4; // 未使用 6 k4 }2 ]1 U" M$ Y; E
} SERVICE_DESCRIPTOR_TABLE ,
" c. i- d7 M( Z! u* PSERVICE_DESCRIPTOR_TABLE,
0 H t, Y4 Z: C* PPSERVICE_DESCRIPTOR_TABLE ;
+ q* `7 A5 \. R u7 P" ^9 gntoskrnl通过KeServiceDescriptorTable符号,导出了主要SDT的一个指针。内核维护另外的一个SDT,就 3 r" \- s2 Q+ w
是KeServiceDescriptorTableShadow。但这个符号没有导出。要想在内核模式组件中存取主要SDT很简单
0 j7 `9 Z- \6 s) A,只需两行C语言的代码:
( R8 v! |, t% @, I, p5 g1 ~9 H0 j9 rextern PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable;
+ P& C4 h2 N z- F3 t6 s, N5 tPSERVICE_DESCRIPTOR_TABLE psdt= KeServiceDescriptorTable; 0 o* e+ y, w4 z' X! E8 N
NTPROC为本机 API的方便的占位符,他类似于Win32编程中的PROC。Native API正常的返回应该是一个 y7 L e; b( h" u% ?1 E! F& R
NTSTATUS代码,他使用NTAPI调用约定,它和_stdcall一样。ServiceLimit成员有在ServiceTable数组里
^$ L, G+ Q- N3 v* ^- s/ R找到的入口数目。在2000下,默认值是248。ArgumentTable为BYTEs的数组,每一个对应于ServiceTable ' n0 f6 t6 G+ ~9 h
的位置并显示了在调用者堆栈里的参数比特数。这个信息与EDX结合,这是内核从调用者堆栈copy参数到 9 [) r3 ^% E- p& K* ~
自己的堆栈所需的。CounterTable成员在free buid的2000中并没有使用到,在debug build中,这个成员 ; m+ I0 |* ~) b- N
指向代表所有函数使用计数的DWORDS数组,这个信息能用于性能分析。+ L8 c5 ^7 Z# l1 U
可以使用这个命令来显示:dd KeServiceDescriptorTable,调试器把此符号解析为0x8046e0c0。只有 & N- I0 O) A6 I7 M! m
前四行是最重要的,对应那四个SDT成员。* o) n7 h% S; _/ f
运行这个命令:ln 8046e100,显示符号是KeServiceDescriptorTableShadow,说明第五个开始确实为
# \! I5 i% X0 c内核维护的第二个SDT。主要的区别在于后一个包含了win32k.sys的入口,前一个却没有。在这两个表中 # J+ _. L- ^& Q j3 t) _
,Table3与Table4都是空的。Ntoskrnl.exe提供了一个方便的API函数。这个函数的名字为: 9 S, N& {- n' F5 o# E: k
KeAddSystemServiceTable0 W0 c1 o# N3 x/ R
此函数去填充这些位置。
, f! g L% Z0 H. c; J2 Z8 b" G2Eh的中断处理标记是KisystemService()。这也是ntoskrnl.exe没有导出的内部的符号,但包含在2k符号 ) t0 @5 ~. X! G4 j# s4 w
文件中。关于KisystemService的操作如下: ' x# b4 l6 B* K7 w: S* A0 p& k
1 从当前的线程控制块检索SDT指针 ; {+ W" U' h X7 ^4 {7 @+ J
2 决定使用SDT中4个SST的其中一个。通过测试EAX中递送ID的第12和13位来决定。ID在0x0000-0x0fff的
! ^* j( H1 f0 B |3 A9 H映射至ntoskrnl表格,ID在
! X6 z. ^2 r8 x! _+ q% w0x1000与0x1ffff的分配给win32k表格。剩下的0x2000-0x2ffff与
* J5 V7 i5 H" c9 y0x3000-0x3ffff则是Table3和Table4保留。
1 C4 m; H6 Z6 X. D) D& W v; @3 通过选定SST中的ServiceLimit成员检查EAX的0-11位。如果ID超过了范围,返回错误代码为
: ^3 N1 h( o7 s& p) d' ASTATUS_INVALID_SYSTEM_SERVICE。
5 X9 \- I7 E" `, t3 [4 检查EAX中的参数堆栈指针与MmUserProbeAddress。这是一个ntoskrnl导出的全局变量。通常等于
$ }1 S" }! k0 f- r& P0x7FFF0000,如果参数指针不在这个地址之下,返回STATUS_ACCESS_VIOLATION。
- S( |% Y% u8 ?% E5 查找ArgumentTable中的参数堆栈的字节数,从调用者的堆栈copy所有的参数至当前内核模式堆栈。 % _+ i' l0 t6 `" ?: w% @4 B2 m$ F
6 搜索serviceTable中的服务函数指针,并调用这个函数。
9 |. j% H& E0 c7 控制转到内部的函数KiserviceExit,在此次服务调用返回之后。 / [9 ?) C- ~$ D" E8 Z- `: v
从对SDT的讨论可以看到与本机API一起还有第二个内核模式接口。这个接口把Win32子系统的图形设备接 ! k; Z) b% Y- E* C7 c
口和窗口管理器和内核模式组件Win32k连接起来。Win32k接口一样是基于int 2eh。本机API的服务号是从
4 W/ l3 k5 g/ P% @) y* }& N3 s9 b0x0000到0x0fff,win32k的服务号是从0x1000到0x1fff。(ddW32pServiceTable认定win32k.sys的符号可
9 n4 R& Z5 h: t+ g) J( l用。)win32k总共包含639个系统服务。
O# U6 f9 ]( A" ~0 _
) b" K* ]6 @8 n. K! N2 D2Eh的处理过程没有使用全局SDT KeServiceDescriptorTable。
$ e' Y. a4 \. T! S/ V% s而是一个与线程相关的指针。显然,线程可以有不同得SDT相关到自身。线程初试化的时 8 r# Z3 b+ R' @% H
候,KeInitializeThread()把KeServiceDescriptorTable写到线程的控制块。尽管这样,这个默认设置之 0 a r( j5 H5 [! ?, ?6 S
后可能被改变为其它值,例如KeServiceDescriptorTableShadow。 " g3 I2 ?7 H$ |$ }' \; A2 w) y
4 d6 C* u7 o" Q }1 [& c: g
Windows 2000运行时库
7 x! z% ~ b# B+ j* y' M5 oNtdll.dll至少导出了不少于1179个符号。其中的249/248是属于Nt*/zw*集合。所以还有682个函数不是通
% ~* r% W3 x+ o过int 2eh门中转。很显然,这么多的函数不依靠2k的内核。 0 N, ?2 N! I* [1 l
其中一些是和c运行时库几乎一样的函数。其实ntoskrnl也实现了一些类似C运行时库的一些函数。可以
9 Y4 D4 G# u- U. f0 R* j通过ddk里的ntdll.lib来链接和使用这些函数。反汇编ntdll.dll与ntoskrnl.exe的C运行时函数能发现
" z/ L+ ?0 |7 }! t8 v,ntdll.dll并不是依赖ntoskrnl.exe。这两个模块各自实现了这些函数。
X1 O0 q9 Q- ~除了C运行时库外,2000还提供了一个扩展的运行时函数集合。再一次,ntdll.dll与ntoskrnl.exe各自
5 Y2 g7 ^5 x" W/ R3 l% G实现了它们。同样,实现集合有重复,但是并不完全匹配。这个集合的函数都是以Rtl开头的。2000运行 4 U8 @) v, Q. N: k$ M6 `* @9 S
时库包括一些辅助函数用于C运行时候无法完成的任务。例如有些处理安全事务,另外的操纵2000专用的
1 S( L/ V8 j0 `: |9 ~0 F数据结构,还有些支持内存管理。微软仅仅在DDK中记录了很有用的406个函数中的115个函数。
3 g% D8 f$ r% ~+ A+ j* D( xNtdll.dll还提供了另外一个函数集合,以__e前缀开头。实际上它们用于浮点数模拟器。
. K l& V* m: C; d7 y, c) S还有很多的函数集合,所有这些函数的前缀如下:
& @9 ~( D$ k6 W/ ?8 L__e(浮点模拟),Cc(Cache管理),Csr(c/s运行时库),Dbg(调试支持),Ex(执行支持),FsRtl(文件系统运行 : X" P7 a) Q4 F) M1 K
时),Hal(硬件抽象层),Inbv(系统初试化/vga启动驱动程序bootvid.dll),Init(系统初试 . |1 _3 _3 G4 ^* L2 }
: l( ^5 d3 N; N化),Interlocked(线程安全变量操作),Io(IO管理器),Kd(内核调试器支持),Ke(内核例程),Ki(内核中断处
& G5 S# w3 k6 w3 o3 I# r理),Ldr(映象装载器),Lpc(本地过程调用),Lsa(本地安全授权),Mm(内存管理),Nls(国际化语言支持),Nt
: x8 |9 g3 B+ N& h(NT本机API),Ob(对象管理器),Pfx(前缀处理),Po(电源管理),Ps(进程支持),READ_REGISTER_(从寄存器
0 |* G. x! `4 y- D地址读),Rtl(2k运行时库),Se(安全处理),WRITE_REGISTER_(写寄存器地址),Zw(本机API的替换叫法)
3 _2 ] l9 }+ C6 ^ j,<其它>(辅助函数和C运行时库)。
' s) n1 R# c `: H! h* W当编写从用户模式通过ntdll.dll或内核模式通过ntoskrnl.exe和2000内核交互的软件的时候,需要处理
, D5 B U9 M4 ~很多基本的数据结构,这些结构在Win32世界中很少见到。
7 l+ @( R% E0 L' `0 m9 k# _常用数据结构
% ?; H: p7 }3 `9 Al 整数
; {- E/ k- S4 O8 sANSI字符是有符号的,而Unicode WCHAR是无符号的
" G" m- A- ^) S: c* kMASM的TBYTE是80位的浮点数,用于高精度浮点运算单元操作,注意它与Win32的TBYTE(text byte)完全
% l( O y2 K% ~9 F# C8 Z# L8 @% }不同。
, ^; _0 C0 r2 vTABLE 2-3. Equivalent Integral Data Types : Y) @" s/ H' K4 m# P
BITS MASM FUNDAMENTAL ALIAS #1 ALIAS #2 SIGNED ' }/ D* q+ d: f( n' ~8 g- n3 T; V
8 BYTE unsigned char UCHAR CHAR
0 r* G% ~: L% c# l3 I" |0 B7 P16 WORD unsigned short USHORT WCHAR SHORT
7 C3 v ], W* z32 DWORD unsigned long ULONG LONG
) I% m: p9 F6 e: J! d+ }32 DWORD unsigned int UINT INT 9 f. ?9 v; `. E
64 QWORD unsigned _int64 ULONGLONG DWORDLONG LONGLONG 7 ?* b2 D3 Y9 q/ V
80 TBYTE N/A
) z6 a; O/ d9 ]7 L, V+ e5 u+ _$ mtypedef union _LARGE_INTEGER . h4 p! J* I- k9 m e" d( I
{ struct{ # k, a& x `5 i0 M
ULONG LowPart; $ s! l( O* B6 r" i
LONG HighPart;}; + |9 Q0 s& g3 L9 x* h- c& f! y7 y$ k
LONGLONG QuadPart;
2 F- j v6 O7 }; r2 c} 8 t7 F. w. K( Z6 N. E5 ]
LARGE_INTEGER , * PULARGE_INTEGER ;
5 a) B; K/ B; _- ctypedef union _ULARGE_INTEGER{
" B r& v$ {' z& _+ w$ vstruct{
! L6 n2 L4 ^- a( \8 t& N& |: K1 ZULONG LowPart; ( t+ u5 S) A4 h& D5 ]
ULONG HighPart;}
, p6 L' g4 N F* ?7 k; t) }ULONGLONG QuadPart;
B( B) N: }+ h I}ULARGE_INTEGER, *PULARGE_INTEGER; " h8 H; h( h# y- ?+ L: f
l 字符
2 N6 f1 f. K- D* ~# I7 [* t Win32编程中PSTR用户CHAR*,PWSTR用于WCHAR*。取决于是否定义了UNICODE,PTSTR解释为PSTR或者 % {) v/ w' h7 }" C
PWSTR。在2k内核模式下,常用的数据类型是UNICODE_STRING,而STRING用来表示ANSI字符串: 0 c+ ` D9 v: W( A5 A
typedef struct _UNICODE_STRING{ * }, B' N* G x9 g2 e; w; S
USHORT Length; //当前字节长度,不是字符!!!
4 Y: o0 ]/ M( H7 D3 [USHORT MaximumLength; //Buffer的最大字节长度 3 G& _+ ]/ I1 r3 B- C5 f3 j8 d: F! p
PWSTR Buffer;}UNICODE_STRING , * PUNICODE_STRING ;
( e0 v T( E: Y! G5 ltypedef struct _STRING{ p! I9 {# `: w) `5 w& N
USHORT Length; 8 U$ |* `' O$ J* T: U) a0 e% t3 r: _8 G1 R
USHORT MaximumLength; 7 E5 a1 @+ e6 r7 T- ~
PCHAR Buffer;}STRING, *PSTRING;
& X6 }* m4 X' f6 Y) n" b- G( ?7 L) k' ^typedef STRING ANSI_STRING, *PANSI_STRING;
6 p3 Y& v2 _! s3 J; Stypedef STRING OEM_STRING, *POEM_STRING;
1 h; C* ^& c% P& V操纵函数:RtlCreatUnicodeString(),RtlInitUnicodeString(), $ x4 ?5 U, I0 G
RtlCopyUnicodeString()等等
- [* \1 F T3 p9 h. H$ h' Bl 结构
$ F7 K! [# p1 ~( I( B4 N! \许多内核API函数需要一个固定大小的OBJECT_ATTRIBUTES结构,比如NtOpenFile()。对象的属性是OBJ_*
% S& |+ A+ p" j) u# D9 l; P值的组合,可以从ntdef.h中查到。
6 ] D% m& d A6 _' M' `9 \3 |8 jIO_STATUS_BLOCK结构提供了所请求操作结果的信息,很简单,status成员包含一个NTSTATUS代码, 如果
) P* O0 @8 A5 v; J2 b! N操作成功 information成员提供特定请求的信息。
. e% d& f- Z; F; r, y8 ^# M还有一个结构是LIST_ENTRY,这是一个双向环链表。 1 E/ w( c) y) X6 H( w8 W7 f& }
typedef struct _OBJECT_ATTRIBUTES
+ R6 R$ C- g4 R! z3 I6 Z- G{ : r; M; \* z% E
ULONG Length;
4 {& F$ i3 Y8 y2 {6 sHANDLE RootDirectory;
# ^. @/ K9 t; t8 q3 HPUNICODE_STRING ObjectName;
1 N: D3 o+ ~& K* eULONG Attributes; 9 E i, m4 @" K& x
PVOID SecurityDescriptor;
" m; e) |9 b' L) c7 VPVOID SecurityQualityOfService;
7 w$ ?* R3 s' t: f0 i} OBJECT_ATTRIBDTES, *POBJECT_ ATTRIBUTES;
7 _4 M$ b. j/ h; Y$ ~( f# ^typedef struct _IO_STATUS_BLOCK
, _, h. h) F& N5 Y B{
5 m* m) A: z# H5 `, }9 K+ MNTSTATDS Status;
5 W: Z) D0 n4 W0 GULONG Information;
3 U" c9 E. h7 [& M; a- [* h& I}IO_STATUS_BLOCK , * PIO_STATUS_BLOCK ; 8 r. l6 M1 e8 v0 J
typedef struct _LIST_ENTRY 8 H8 A% G3 l# k( O6 Z6 L
{ 1 p' r6 M: p; P& y
Struct _LIST_ENTRY *Flink; & S e: o+ z% H- @
Struct _LIST_ENTRY *Blink; " K& R' R, a9 ?1 @0 R
}LIST_ENTRY, *PLIST_ENTRY;
$ \9 z" f1 X( m- [双向链表的典型例子就是进程和线程链。内部变量PsActiveProcessHead是一个LIST_ENTRY结构,在
! g- r) @* E" f( C' E# xntoskrnl.exe的数据段中,指定了系统进程列表的第一个成员。 - R3 E" X, M/ i( e
CLIENT_ID结构由进程和线程ID组成。 - P4 q$ A- M& n
typedef struct _CLIENT_ID 7 ^. m; ~! u. ~1 O$ J
{ HANDLE UniqueProcess;
$ ^" M, O: x4 vHANDLE UniqueThread; $ Q& ]) J$ z B3 d& ~
)CLIENT_ID, *PCLIENT_ID;
2 P5 w$ i" O! ?* V& R- E8 l2 j想要从用户模式调用ntdll.dll中的API函数,必须考虑到以下四点:
* J4 S4 p; L# d9 C9 ^1 SDK头文件没有包括这些函数的原型
4 C; v: A; U$ R9 o; p2 这些函数使用的若干基本数据类型没有包括在SDK文件中
" F8 o& f- j- q! {5 ~$ X+ B7 x* P6 K3 SDK和DDK头文件不兼容,不能在win32的c源文件包含ntddk.h中 ) V7 t8 P- m) x6 m4 P2 r% G6 ^! w) }
4 ntdll.lib没有包括在VC的默认导入库列表中。 q8 d; l) c5 u* `
第4个很容易解决:#progma comment(linker,“/defaultlib:ntdll.lib”) 9 V/ z' ~ Q# @5 r+ _# P9 H9 u
缺失的定义比较难解决,最简单的方法是写一个自定义的头文件,刚刚包含需要调用ntdll.dll中函数的 8 S. s' c6 K7 a' }' e/ G
定义。幸运的是,已经在光盘的w2k_def.h文件中做了这个工作。因为这个头文件将用于用户模式和内核 0 c$ v$ D: D ~8 Z/ I8 I% ]! O1 o
模式程序,所以必须在用户模式代码中,#include<w2k_def.h>之前#define _USER_MODE_,使得DDK中出
7 s) Q. b) @' H- w现而SDK中没有的定义可用。
: ^8 @* l; a% ~2 a% ]& U
& I4 h! R( O; @' z本文部分翻译于一篇电子书<win api about>.也感谢朋友GameHunter这位英语极好的朋友帮忙.与Free的
8 z, W# f) s0 F- w. Y$ E/ w指导2 T6 N0 E& g9 x- q# Y" ^/ `# ~
+ j/ F' F q I- m5 j
, i! F) Q1 g" Z/ W& Z; E
|