http://blog.youkuaiyun.com/muy/article/details/46742083
起因
前几天有朋友遇到一个问题来问我。他有一个Windows 7 x64下的minidump文件(http://pan.baidu.com/s/1dD524hz),经过初步分析,知道系统崩溃最初是因用户态调用了NtDeviceIoControlFile造成的,KeBugCheckEx调用已经位于多重函数调用的深处,问如何找到最初NtDeviceIoControlFile的参数,尤其是头4个参数。经过一番努力,我总算是把问题解决了,其中得到一些领悟,想趁热记录,也想趁这个机会将内核服务调用的基本技术点梳理一下。
在开始针对问题开始讨论之前,我们先要补习一下Windows x64下函数调用的基本知识。
x64函数调用堆栈
x64下,不再使用x86下以“pushebp/mov ebp, esp”为特征的堆栈帧格式。取而代之的是如下图所示格局:

函数调用的头4个参数虽然在堆栈中预留了位置,但参数值并不真的存储在那里。实际上从第1个参数到第4个参数是依次是使用rcx、rdx、r8、r9(浮点数情况下依次对应:xmm0、xmm1、xmm2、xmm3)。几乎(注意是:几乎)所有函数在头几条指令范围内就通过sub rsp, xxh的形式准备好了本函数用到的堆栈空间,之后直到函数返回,此函数不再进行改变rsp的任何操作。也就是说函数的堆栈是“一步到位”的。这样一步到位的好处是避免堆栈指针的反复调整,从而有效提高代码执行效率。其副作用是:头4个参数不在堆栈上,不利于追踪;没有特殊的ebp堆栈基址(rbp不再使用),堆栈回溯较难。在函数调用时,rdi、rsi、rbx、rbx、rbp、r12、r13、r14、r15是所谓的非容失性(nonvolatile)寄存器。也就是说,在函数调用过程中,被调用函数必须保证这些寄存器在函数返回时和进入函数时是一样的。
某处代码在调用某个函数时,会将头4个参数分别存入相应寄存器,如果超过4个参数,会在位于rsp+20h的地方存入第5个参数。随后call指令会将返回地址压入堆栈,造成rsp减8。此时第1个参数对应位置是rsp+8,第4个参数对应位置是rsp+20h。随后,一般的函数会一次性sub rsp, xxh,其中xxh不同函数有所不同。通过IDA Pro进行观察会发现,函数内部代码对局部变量和参数的访问,都翻译成[rsp+xxh+yyh]和形式,其中yyh是数据相对于函数第1条指令将要执行而尚未执行时rsp的指针位置(当然返回地址位置是rsp+0)。
补习完基础知识,我们就可以开始研究问题了。当然在开始之前,要启动我们的Windbg,装入dump文件,下载并装入相应符号。
第5个及以后的参数
首先NtDeviceIoControlFile的原型是:
- NTSTATUS WINAPI NtDeviceIoControlFile(
- _In_ HANDLE FileHandle,
- _In_ HANDLE Event,
- _In_ PIO_APC_ROUTINE ApcRoutine,
- _In_ PVOID ApcContext,
- _Out_ PIO_STATUS_BLOCK IoStatusBlock,
- _In_ ULONG IoControlCode,
- _In_ PVOID InputBuffer,
- _In_ ULONG InputBufferLength,
- _Out_ PVOID OutputBuffer,
- _In_ ULONG OutputBufferLength
- );
毫无疑问,第5个及以后的参数存储在堆栈中。我们使用kv来观察堆栈:
- 1:kd> kv
- Child-SP RetAddr : Args to Child : Call Site
- fffff880`0253ff58 fffff800`03f577ab : 00000000`0000001e ffffffff`c0000005 fffff880`0254001000000000`00000000 : nt!KeBugCheckEx
- fffff880`0253ff60 fffff800`03f16118 : 00000000`00000001 00000000`00000000 fffffa80`03306b30fffff880`02540920 : nt!KipFatalFilter+0x1b
- fffff880`0253ffa0 fffff800`03eee89c : 00000000`00000000 fffffa80`03ee0da0 fffff8a0`00262450fffff8a0`00262140 : nt! ?? ::FNODOBFM::`string'+0x83d
- fffff880`0253ffe0 fffff800`03eee31d : fffff800`0400f30c fffff880`02541610 00000000`00000000fffff800`03e50000 : nt!_C_specific_handler+0x8c
- fffff880`02540050 fffff800`03eed0f5 : fffff800`0400f30c fffff880`025400c8 fffff880`02540f38fffff800`03e50000 : nt!RtlpExecuteHandlerForException+0xd
- fffff880`02540080 fffff800`03efe081 : fffff880`02540f38 fffff880`02540790 fffff880`0000000000000000`00000000 : nt!RtlDispatchException+0x415
- fffff880`02540760 fffff800`03ec20c2 : fffff880`02540f38 fffffa80`02884010 fffff880`02540fe000000000`00000010 : nt!KiDispatchException+0x135
- fffff880`02540e00 fffff800`03ec0c3a : 00000000`00000001 00000000`00000018 00000000`00000000fffffa80`02884010 : nt!KiExceptionDispatch+0xc2
- fffff880`02540fe0 fffff880`01374092 : fffff880`0512bf79 fffffa80`03f99a80 fffff880`0254127000000000`00000000 : nt!KiPageFault+0x23a (TrapFrame @ fffff880`02540fe0)
- fffff880`02541178 00000000`00000087 : fffff800`04057580 fffffa80`03fb9ba0 fffff880`0512bee9fffffa80`03fb9ba0 : Ntfs!NtfsIterateMft+0x192
- fffff880`02541218 fffff800`04057580 : fffffa80`03fb9ba0 fffff880`0512bee9 fffffa80`03fb9ba000000000`00000000 : 0x87
- fffff880`02541220 fffffa80`03fb9ba0 : fffff880`0512bee9 fffffa80`03fb9ba0 00000000`00000000fffffa80`02884010 : nt!NonPagedPoolDescriptor
- fffff880`02541228 fffff880`0512bee9 : fffffa80`03fb9ba0 00000000`00000000 fffffa80`02884010fffff800`03ffb44e : 0xfffffa80`03fb9ba0
- fffff880`02541230 fffff880`0512bc1b : fffffa80`03fb9ba0 00000000`00000000 fffffa80`03fc0080fffff800`0406da28 : afd!AfdTLBindComplete2+0xb9
- fffff880`025412d0 fffff880`016777b9 : 00000000`00000000 fffffa80`00000000 fffffa80`03f99a8000000000`00000000 : afd!AfdTLBindComplete+0x3b
- fffff880`02541370 fffff880`016774aa : 00000000`00000002 fffffa80`03f99a80 00000000`0000000200000000`00000000 : tcpip!TcpBindEndpointInspectComplete+0x249
- fffff880`02541410 fffff880`016782ff : fffffa80`025c9100 fffff880`00000000 fffffa80`03f99a80fffffa80`025c9100 : tcpip!TcpBindEndpointRequestInspectComplete+0x27a
- fffff880`025414f0 fffff880`016780c7 : fffffa80`03e17240 fffff880`0512bbe0 fffffa80`02884010fffffa80`02884010 : tcpip!TcpBindEndpointWorkQueueRoutine+0x8f
- fffff880`02541540 fffff880`01678168 : fffff880`025417f0 fffff880`0174e160 fffffa80`03f99a80fffff880`02541810 : tcpip!TcpBindEndpoint+0x87
- fffff880`02541570 fffff880`0167706c : 00000000`00000000 fffff880`02541720 00000000`0000000100000000`00004800 : tcpip!TcpIoControlEndpoint+0x68
- fffff880`025415b0 fffff800`03ecf3d8 : fffff880`02541ca0 fffff800`04102540 00000000`00000000fffff880`02541870 : tcpip!TcpTlEndpointIoControlEndpointCalloutRoutine+0x1c
- fffff880`02541610 fffff880`016771e0 : fffff880`01677050 fffff800`03ecf3d8 00000000`00000000fffff800`03ecfd01 : nt!KeExpandKernelStackAndCalloutEx+0xd8
- fffff880`025416f0 fffff880`051404a4 : fffffa80`03f99a80 fffff880`01677170 fffffa80`03fb9ba0fffff880`025417f0 : tcpip!TcpTlEndpointIoControlEndpoint+0x70
- fffff880`02541760 fffff880`0512efc5 : 00000000`00000000 fffff880`01677170 fffffa80`03fb9ba000000000`00000018 : afd! ?? ::GFJBLGFE::`string'+0xa1c0
- fffff880`025417d0 fffff880`051404cb : fffffa80`03f99a80 fffff880`01677170 fffffa80`03fb9ba0fffff880`025418e0 : afd!AfdTLBind+0x75
- fffff880`02541850 fffff880`0512bdf9 : 00000000`00000010 fffff880`02541ca0 fffffa80`0288401000000000`00000000 : afd! ?? ::GFJBLGFE::`string'+0xa1e7
- fffff880`025418c0 fffff880`05114639 : fffffa80`03f7f560 fffffa80`00000000 00000000`00000001fffff880`02541ca0 : afd!AfdTLBindSecurity+0x139
- fffff880`02541940 fffff800`041de8d7 : fffffa80`02884010 fffff880`00000003 fffffa80`0000001000000000`00000000 : afd!AfdBind+0x399
- fffff880`02541a10 fffff800`041df136 : fffff683`ff7e6eb8 00000000`00000188 00000000`0000000000000000`00000000 : nt!IopXxxControlFile+0x607
- fffff880`02541b40 fffff800`03ec1cd3 : fffffa80`03fbf530 0000007f`ffffffff fffff880`02541bc800000980`00000000 : nt!NtDeviceIoControlFile+0x56
- fffff880`02541bb0 00000000`776adc4a : 00000000`00000000 00000000`00000000 00000000`0000000000000000`00000000 : nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @fffff880`02541c20)
- 00000000`0054f228 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`0000000000000000`00000000 : 0x776adc4a
通过Call Site对应列的显示内容可以看出,异常确实起源于NtDeviceIoControlFile。对NtDeviceControlFile的调用源自符号KiSystemServiceCopyEnd附近(那里是系统从用户态转换到核心态,并查找调用系统服务例程的地方)。Call Site列基本显示出函数的调用关系,因此我们也引用它来表示“当前”某个函数。
nt!KiSystemServiceCopyEnd+0x13的Child-SP为fffff880`02541bb0我们使用下面的命令观察:
- 1:kd> dq fffff880`02541bb0 L4
- fffff880`02541bb0 fffffa80`03fbf530 0000007f`ffffffff
- fffff880`02541bc0 fffff880`02541bc8 00000980`00000000
- 1:kd> dqs fffff880`02541bb0-8 L1
- fffff880`02541ba8 fffff800`03ec1cd3 nt!KiSystemServiceCopyEnd+0x13
地址fffff880`02541bb0是KiSystemServiceCopyEnd所处函数所使用的堆栈顶端。也就是说,这个位置是调用NtDeviceControlFile第1个参数的位置,此处之后4个QWORD值与kv命令显示的nt!NtDeviceIoControlFile+0x56这一行的Args to Child相同。这个位置减去8,是NtDeviceControlFile的返回地址RetAddr,也就是nt!KiSystemServiceCopyEnd+0x13。那么第5个参数应当位于fffff880`02541bb0+0x20处:
- 1:kd> dq fffff880`02541bb0+0x20 L6
- fffff880`02541bd0 00000000`0054f2c0 000007fe`00012003
- fffff880`02541be0 00000000`0054f2e8 00000000`00000014
- fffff880`02541bf0 00000000`0054f2e8 00000000`00000010
这样我们就得出第5至第10个参数的值依次为:
- IoStatusBlock = 00000000`0054f2c0
- IoControlCode = 0x12003
- InputBuffer = 00000000`0054f2e8
- InputBufferLength = 0x14
- OutputBuffer = 00000000`0054f2e8
- OutputBufferLength = 0x10
第2、3、4个参数
用kv命令看到的NtDeviceIoControlFile前4个参数明显不对,它只是堆栈上的随机值罢了。实际上前4个参数存储在当时的rcx、rdx、r8、r9寄存器中。我开始猜想,既然nt!NeDeviceIoControlFile是从ntdll!NtDeviceIoControlFile调用过来的,系统应该在切换处理器状态时,将这4个寄存器存入创建的_KTRAP_FRAME结构中。于是在kv命令中,我们看到这个:
- nt!KiSystemServiceCopyEnd+0x13(TrapFrame @ fffff880`02541c20)
这里有个疑问——我无法访问当前线程。查看gs:188h处内存,会得到:
- 1:kd> dq gs:188 L1
- 002b:00000000`00000188 ????????`????????
查看gs寄存器基址(x64下GDT已废弃不用,命令rM100已派不上用场):
- 1:kd> rdmsr c0000101
- nosuch msr
使用命令!pcr:
- 1:kd> !pcr
- Cannot get PRCB address
但是使用命令.thread、!thread或r $thread,却能看到当前线程对象为fffffa80`03fbf530。用dt _KTHREAD TrapFrame fffffa80`03fbf530命令查看到的TrapFrame指针与kv命令显示的最底层TrapFrame相同,都是fffff880`02541c20。于是:
- 1:kd> dt _KTRAP_FRAME fffff880`02541c20
- nt!_KTRAP_FRAME
- ……
- +0x038 Rcx : 0x14
- +0x040 Rdx : 0x10
- +0x048 R8 : 1
- +0x050 R9 : 0
- ……
但这里第3个参数ApcRoutine为1,使我深深怀疑它的准确性。
既然TrapFrame数据不正确,那我们只能顺着代码追一追,看看以后的代码是否会将前4个参数存到我们可以访问到的地方。逆向nt!NtDeviceIoControlFile:
- DeviceIoControlFile proc near
- sub rsp,68h
- mov eax, dword ptr[rsp+68h+arg_48]
- mov byte ptr [rsp+68h+var_18], 1
- mov [rsp+68h+var_20], eax
- mov rax, [rsp+68h+arg_40]
- mov [rsp+68h+Src], rax
- mov eax, [rsp+68h+arg_38]
- mov [rsp+68h+var_30], eax
- mov rax, qword ptr[rsp+68h+arg_30]
- mov qword ptr [rsp+68h+var_38], rax
- mov eax, dword ptr[rsp+68h+arg_28]
- mov dword ptr [rsp+68h+var_40], eax
- mov rax, [rsp+68h+arg_20]
- mov [rsp+68h+var_48], rax
- call IopXxxControlFile
- add rsp,68h
- retn
- NtDeviceIoControlFile endp
可以看出,这个函数没有改变前4个参数所在的寄存器值,而只是复制其余参数,然后调用了IopXxxControlFile。逆向IopXxxControlFile看到头4条指令是:
- IopXxxControlFile proc near
- mov r11, rsp
- mov [r11+20h], r9
- mov [r11+18h], r8
- mov [r11+10h], rdx
原来这个函数一上来就将第2至第4个参数保存到相应预留的位置。那么在异常发生时,这3个值依然在那里。kv命令显示,nt!IopXxxControlFile+0x607行的这3个参数是:
- Event = 0x188
- ApcRoutine = 0
- ApcContext = 0
那个难缠的第1个参数
顺着这条路向下,发现很难追到rcx的踪迹了。于是转变思路,看一看NtDeviceIoControlFile是如何从用户态切换到核心态的,也许能发现什么蛛丝马迹。
在ntdll中NtDeviceIoControlFile是这样子的:
- NtDeviceIoControlFile proc near
- mov r10, rcx
- mov eax,4
- syscall
- retn
- NtDeviceIoControlFile endp
ntdll!NtDeviceIoControlFile被调用时,无疑参数传递也遵循x64的函数调用约定。第1句用r10临时存储rcx,因为随后的syscall调用会用到rcx。eax中是NtDeviceIoControlFile所对应的系统服务号。在syscall调用之前,rsp是返回地址,rsp+8h是第1个参数位置,以此类推rsp+28h是第5个参数位置。根据AMD64手册,syscall指令所做操作大致为:
1) 下一条指令——即retn——的地址存入rcx;
2) 将rflags内容存入r11;
3) 将MSR_LSTAR内容存入rip;
4) 设置cs;
5) 设置ss;
6) 改变当前CPL(当前权限等级)为0;
7) 设置rflags。
我们可以在一个正常的被调试的Windowsx64系统中(不是在当前加载dump文件时)看到:
- 1:kd> rdmsr c0000082
- Msr[c0000082]= fffff800`03cd3640
- 1:kd> ln fffff800`03cd3640
- (fffff800`03cd3640)nt!KiSystemCall64
- ……
MSR_LSTAR的内容是内核地址KiSystemCall64:
- KiSystemCall64 proc near
- (1) swapgs
- (2) mov gs:10h, rsp
- (3) mov rsp, gs:1A8h
- push 2Bh
- push qword ptr gs:10h
- push r11
- push 33h
- push rcx
- mov rcx, r10
- sub rsp,8
- push rbp
- sub rsp,158h
- lea rbp, [rsp+190h+var_110]
- (4) mov [rbp+0C0h],rbx
- mov [rbp+0C8h], rdi
- mov [rbp+0D0h], rsi
- mov byte ptr [rbp-55h], 2
- (5) mov rbx, gs:188h
- prefetchw byte ptr [rbx+1D8h]
- stmxcsr dword ptr [rbp-54h]
- ldmxcsr dword ptr gs:180h
- (6) cmp byte ptr [rbx+3],0
- mov word ptr [rbp+80h],0
- jz loc_140071B50
- mov [rbp-50h], rax
- (7) mov [rbp-48h], rcx
- mov [rbp-40h], rdx
- test byte ptr [rbx+3],3
- mov [rbp-38h], r8
- mov [rbp-30h], r9
- ……
指令(1)将gs的基址与MSR[c0000102]内容互换,设置过后gs:0指向内核处理器控制域_KPCR。指令(2)将用户态堆栈指针保存到_KPCR+0x010的成员UserRsp里。指令(3)使用_KPCR+0x1A8的成员RspBase设置当前rsp,这个成员存储了当前线程核心态的堆栈指针。一路执行下来,到达指令(4), rsp与设置伊始相比共减少了0x190字节。期间恢复了rcx,在堆栈上保存了一些值。使用下面的命令,我们可以看出,0x190正是_KTRAP_FRAME的大小:
- 1:kd> ??sizeof(_KTRAP_FRAME)
- unsigned int64 0x190
现在rsp指向_KTRAP_FRAME的起始地址,rbp指向_KTRAP_FRAME+0x80的位置。我们惊奇地发现指令(7)处及以下若干条指令将rcx、rdx、r8、r9存入了_KTRAP_FRAME当中,其中rbp-48h相当于rsp+80h-48h即rsp+38h。但为什么我们之前分析_KTRAP_FRAME时得到的是错误的数据呢?再仔细看看!指令(6)处将rbx+3处的字节与0进行了比较,如果是0则不会保存那4个寄存器。而通过指令(5),我们知道rbx存储了当前线程对象的起始地址:
- 1:kd> dt _KTHREAD -b
- nt!_KTHREAD
- +0x000 Header : _DISPATCHER_HEADER
- +0x000 Type : UChar
- ……
- +0x003 DebugActive : UChar
- ……
可见只有在当前线程的DebugActive为非0时,系统才会在_KTRAP_FRAME中存储系统服务调用的头4个参数。绝望了吗?继续向下看,代码跳转至loc_140071B50后:
- loc_140071B50:
- sti
- mov [rbx+1E0h], rcx
- mov [rbx+1F8h], eax
上帝终于看不下去出手拯救了我们:
- 1:kd> dt _KTHREAD fffffa80`03fbf530
- nt!_KTHREAD
- ……
- +0x1e0 FirstArgument : 0x00000000`0000018c Void
- ……
于是我们得到了第1个参数的值:
到这里似乎问题已得到了解答。但我们还想看一看,系统服务是如何被调用的,参数又是如何从用户态堆栈转移到核心态堆栈的。
系统服务的调用
沿着KiSystemCall64的执行路径继续向下,到达了KiSystemServiceStart:
- KiSystemServiceStart proc near
- mov [rbx+1D8h], rsp
- mov edi, eax
- shr edi,7
- and edi,20h
- and eax,0FFFh
这里_KTHREAD+0x1D8h处就是TrapFrame。之后,系统取得了存于eax中的服务号。这个服务号的最高位,即第13个比特位(Bit12,最低位为Bit0),表示服务的类型是属于执行体的还是图形界面的。如果这一位置位,则结果是rdi为0x20,否则就为0。服务号的其余低位是真正的服务索引。在往后,到达了KiSystemServiceRepeat:
- KiSystemServiceRepeat proc near
- lea r10,KeServiceDescriptorTable
- lea r11,KeServiceDescriptorTableShadow
- (1) test dword ptr [rbx+100h],80h
- (2) cmovnz r10, r11
- (3) cmp eax, [rdi+r10+10h]
- jnb loc_140071E82
- (4) mov r10, [rdi+r10]
- (5) movsxd r11, dword ptr [r10+rax*4]
- mov rax, r11
- sar r11,4
- add r10, r11
- cmp edi,20h
- (6) jnz short loc_140071C00
指令(1)比较了_KTHREAD+0x100处的GuiThread比特位是否置位,置位则表示当前线程有图形界面。r10作为服务描述符表的基址,语句(2)决定了在无图形界面时这个基址是KeServiceDescriptorTable,有图形界面时这个基址是KeServiceDescriptorTableShadow。语句(3)比较当前服务号是否超出了服务分派表内服务数量的上限。为什么会把rdi设置为0或0x20的作用这里体现出来了,这个服务描述符表可能包含2个元素,每个元素描述了一个服务分派表,而每个元素的大小恰好是0x20。语句(4)根据服务号的最高位选择使用哪一个服务分派表。系统维护的两个服务分派表,一个指向ntoskrnl.exe内的服务例程nt!Nt*,一个指向wind32k.sys内的win32k!Nt*。指令(5)取得服务分派表内与服务号相对应的元素并存入r11并复制了一份在rax当中。之后使用下面的公式计算服务例程地址,并把它放入r10:
- 服务例程地址 = 服务分派表基址 + ( 服务分派表元素>> 4 )
这里我们先不去管GUI线程,由指令(6)跳转至loc_140071C00:
- loc_140071C00:
- (1) and eax, 0Fh
- jz KiSystemServiceCopyEnd
- (2) shl eax, 3
- (3) lea rsp, [rsp-70h]
- (4) lea rdi, [rsp+18h]
- (5) mov rsi, [rbp+100h]
- (6) lea rsi, [rsi+20h]
- test byte ptr [rbp+0F0h],1
- jz short loc_140071C40
- cmp rsi, cs:MmUserProbeAddress
- cmovnb rsi, cs:MmUserProbeAddress
- nop dword ptr [rax+00000000h]
- loc_140071C40:
- (7) lea r11, KiSystemServiceCopyEnd
- sub r11, rax
- jmp r11
指令(1)比较服务分派表元素低4位是否为0。之后指令(2)将这低4位乘以8。指令(3)预留了足够多的堆栈空间以便复制参数。如果我们假定rdi当前指向第1个参数的位置,那么指令(4)将rdi定位在了第4个参数的位置上。注意,这是精心设计的。指令(5)将rsi设置为_KTRAP_FRAME+0x180位置的值,即用户态的rsp。还记得syscall之前它的位置吗?它指向ntdll!NtDeviceIoControlFile的返回地址,于是rsi+8就是ntdll!NtDeviceIoControlFile第1个参数的位置,那么指令(6)的rsi+20h就是第4个参数的位置。这样,rsi与rdi分别指向了用户态和核心态的第4个参数的位置。看来是为复制参数做好了准备。从指令(7)开始的3条指令,可以看出服务分派表元素低4位原来是需要复制参数的个数,它的数据为m-4,其中m为服务调用的参数个数。如果这个服务有5个参数,则这个服务对应元素的低4位就应当是1。如果要复制的参数个数为n(n=m-4),那么代码会跳转至KiSystemServiceCopyEnd之前的n*8个字节的位置执行。KiSystemServiceCopyEnd前后的代码揭示了这个精巧设计的秘密:
- ……
- 48 8B 46 10 mov rax, [rsi+10h]
- 48 89 47 10 mov [rdi+10h], rax
- 48 8B 46 08 mov rax, [rsi+8]
- 48 89 47 08 mov [rdi+8], rax
- KiSystemServiceCopyEnd proc near
- test cs:dword_140207348,40h
- jnz loc_140071F20
- call r10
原来KiSystemServiceCopyEnd之前是复制参数的指令,每复制一个参数需要8个字节的指令。还记得rsi和rdi的位置吗?它们指向第4个参数位置,于是加上8就是第5个参数。复制是从第5个参数开始的,头4个参数在寄存器中。之后的代码就无需做过多解释了。
结论
我们为了找到系统服务调用NtDeviceIoControlFile的参数做出了很大的努力,比较自然地找到了第5个及以后的参数,费力地找到了第1个参数,比较幸运地找到了第2、3、4个参数——因为系统比较偶然的将它们存入了堆栈。对于第2、3、4个参数来说,无法保证系统一直会这样处理它们,实际上有许多的系统服务例程没有这样做,比如说NtCreateThread,所以也就没有通用可靠的方法在系统出现崩溃时去找到它们的值。当然系统服务例程应该有责任避免崩溃的发生,但却难以避免正在开发的驱动程序在其深处崩溃。