深入理解Windows X64调试

深入理解Windows X64调试

随着64位操作系统的普及,都开始大力进军x64,X64下的调试机制也发生了改变,与x86相比,添加了许多自己的新特性,之前学习了Windows x64的调试机制,这里本着“拿来主义”的原则与大家分享。

本文属于译文,英文原文链接:http://www.codemachine.com/article_x64deepdive.html

翻译原文地址:深入Windows X64 调试

在正式开始这篇译文之前,译者先定义下面两个关于栈帧的翻译:

  • frame pointer:栈帧寄存器、栈帧指针,在X86平台上,是EBP所指的位置
  • stack pointer:栈顶寄存器、栈顶指针,在X86平台上,是ESP所指的位置

这个教程讨论一些在 X64 CPU 上代码执行的要点,如:编译器优化、异常处理、参数传递和参数恢复,并且介绍这几个topic之间的关联。我们会涉及与上述topic相关的一些重要的调试命令,并且提供必要的背景知识去理解这些命令的输出。同时,也会重点介绍X64平台的调试与X86平台的不同,以及这些不同对调试的影响。最后,我们会活学活用,利用上面介绍的知识来展示如何将这些知识应用于X64平台的基于寄存器存储的参数恢复上,当然,这也是X64平台上调试的难点。

0x00                 编译器优化

这一节主要讨论影响X64 code生成的编译器优化,首先从X64寄存器开始,然后,介绍优化细节,如:函数内联处理(function in-lining),消除尾部调用(tail call elimination), 栈帧指针优化(frame pointer optimization)和基于栈顶指针的局部变量访问(stack pointer based local variable access)。

  •  寄存器的变化

 X64平台上的所有寄存器,除了段寄存器和EFlags寄存器,都是64位的,这就意味着在x64平台上所有内存的操作都是按64位宽度进行的。同样,X64指令有能力一次性处理64位的地址和数据。增加了8个新的寄存器,如: r8~r15,与其他的使用字母命名的寄存器不同,这些寄存器都是使用数字命名。下面的调试命令输出了 X64 平台上寄存器的信息:

复制代码
1: kd> r 
rax=fffffa60005f1b70 rbx=fffffa60017161b0 rcx=000000000000007f rdx=0000000000000008 rsi=fffffa60017161d0 rdi=0000000000000000 rip=fffff80001ab7350 rsp=fffffa60005f1a68 rbp=fffffa60005f1c30  r8=0000000080050033  r9=00000000000006f8 r10=fffff80001b1876c r11=0000000000000000 r12=000000000000007b r13=0000000000000002 r14=0000000000000006 r15=0000000000000004 iopl=0         nv up ei ng nz na pe nc cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00000282 nt!KeBugCheckEx: 
fffff800`01ab7350 48894c2408      mov     qword ptr [rsp+8],rcx ss:0018:fffffa60`005f1a70=000000000000007f 
复制代码

相比较X86平台,一些寄存器的用法已经发生变化,这些变化可以按如下分组:

  1. 不可变寄存器是那些在函数调用过程中,值被保存起来的寄存器。X64平台拥有一个扩展的不可变寄存器集合,在这个集合中,以前x86平台下原有的不可变寄存器也包含在内,新增的寄存器是从R12到R15,这些寄存器对于函数参数的恢复很重要。
  2. Fastcall寄存器用于传递函数参数。Fastcall是X64平台上默认的调用约定,前4个参数通过RCX, RDX, R8, R9传递。
  3. RBP不再用作栈帧寄存器。现在RBP和RBX,RCX一样都是通用寄存器,调试前不再使用RBP来回溯调用栈。
  4. 在X86 CPU中,FS段寄存器用于指向线程环境块(TEB)和处理器控制区(Processor Control Region, KPCR),但是,在X64上,GS段寄存器在用户态是指向TEB,在内核态是指向KPCR。然而,当运行WOW64程序中,FS 寄存器仍然指向32位的TEB。

在X64平台上,trap frame的数据结构(nt!_KTRAP_FRAME)中不包含不可变寄存器的合法内容。如果X64函数会使用到这些不可变寄存器,那么,指令的序言部分会保存不可变寄存器的值。这样,调试器能够一直从栈中取到这些不可变寄存器原先的值,而不是从trap frame中去取。在X64内核模式调试状态下,`.trap`命令的输出会打印一个NOTE,用于告诉用户所有从trap frame中取出的寄存器信息可能不准确,如下所示:

复制代码
1: kd> kv 
Child-SP          RetAddr           : Args to Child .  
.  
. 
nt!KiDoubleFaultAbort+0xb8 (TrapFrame @ fffffa60`005f1bb0) .  
.  
. 
 
1: kd> .trap  fffffa60`005f1bb0 
NOTE: The trap frame does not contain all registers. 
Some register values may be zeroed or incorrect 
复制代码
  •  函数内联处理(Function in-lining)

如果满足一定的规则以后,X64编译器会执行内联函数的扩展,这样会将所有内联函数的调用部分用函数体来替换。内联函数的优点是避免函数调用过程中的栈帧创建以及函数退出时的栈平衡,缺点是由于指令的重复对导致可执行程序的大小增大不少,同时,也会导致cache未命中和page fault的增加。内联函数同样也会影响调试,因为当用户尝试在内联函数上设置断点时,调试器是不能找到对应的符号的。源码级别的内联可以通过编译器的/Ob flag 进行控制,并且可以通过__declspec(noinline)禁止一个函数的内联过程。图1显示函数2和函数3被内联到函数1的过程。

 

                           Figure 1 : Function In-lining 

  •  消除尾部调用(Tail Call Elimination)

 X64编译器可以使用jump指令替换函数体内最后的call指令,通过这种方式来优化函数的调用过程。这种方法可以避免被调函数的栈帧创建,调用函数与被调函数共享相同的栈帧,并且,被调函数可以直接返回到自己爷爷级别的调用函数,这种优化在调用函数与被调函数拥有相同参数的情况下格外有用,因为如果相应的参数已经被放在指定的寄存器中,并且没有改变,那么,它们就不用被重新加载。图2显示了TCE,我们在函数1的最后调用函数4:

 

                                                     Figure 2 : Tail Call Elimination

 

  • 栈帧指针省略(Frame Pointer Omission, FPO) 

 在X86平台下,EBP寄存器用于访问栈上的参数与局部变量,而在X64平台下,RBP寄存器不再使用充当同样的作用。取而代之的是,在X64环境下,使用RSP作为栈帧寄存器和栈顶寄存器,具体是如何使用的,我们会在后续的章节中做详细的叙述。(译者注:请区分X86中的FPO与X64中的FPO,有很多相似的地方,也有不同之处。关于 X86上的FPO,请参考《软件调试》中关于栈的描述)所以,在X64环境下,RBP寄存器已经不再担当栈帧寄存器,而是作为一般的通用寄存器使用。但是,有一个例外情况,当使用alloca()动态地在栈上分配空间的时候,这时,会和X86环境一样,使用RBP作为栈帧寄存器。 下面的汇编代码片段展示了X86环境下的KERNELBASE!Sleep函数,可以看到EBP寄存器被用作栈帧寄存器。当调用SleepEx()函数的时候,参数被压到栈上,然后,使用call指令调用SleepEx()。

复制代码
0:009> uf KERNELBASE!Sleep KERNELBASE!Sleep: 
75ed3511 8bff            mov     edi,edi 
75ed3513 55              push    ebp 
75ed3514 8bec            mov     ebp,esp 
75ed3516 6a00            push    0 
75ed3518 ff7508          push    dword ptr [ebp+8] 
75ed351b e8cbf6ffff      call    KERNELBASE!SleepEx (75ed2beb) 
75ed3520 5d              pop     ebp 75ed3521 c20400          ret     4. 
复制代码

 

 下面的代码片段展示的是X64环境下相同的函数,与X86的code比起来有明显的不同。X64版本的看起来非常紧凑,主要是由于不需要保存、恢复RBP寄存器。

0:000> uf KERNELBASE!Sleep KERNELBASE!Sleep: 
000007fe`fdd21140 xor     edx,edx 
000007fe`fdd21142 jmp     KERNELBASE!SleepEx (000007fe`fdd21150) 

 

  •  基于栈顶指针的局部变量访问(Stack Pointer based local variable access)

在X86平台上,EBP的最重要作用就是可以通过EBP访问实参和局部变量,而在X64平台上,如我们前面所述, RBP寄存器不再充当栈帧寄存器的作用,所以,在X64平台上,RSP即充当栈帧寄存器(frame pointer),又充当栈顶寄存器(stack pointer)。所以,X64上所有的引用都是基于RSP的。由于这个原因,依赖于RSP的函数,其栈帧在函数体执行过程中是固定不变的,从而可以方便访问局部变量和参数。因为PUSH和POP指令会改变栈顶指针,所以,X64函数会限制这些指令只能在函数的首尾使用。如图3所示,X64函数的结构:

 

                                 Figure 3 : Static Stack Pointer

下面的代码片段展示了函数 user32!DrawTestExW 的完整信息,这个函数的首部以指令“sub rsp,48h”结束,尾部以“add rsp,48h”开始。因为首尾之间的指令通过RSP访问栈上的内容,所以,没有PUSH或者POP之类的指令在函数体内。

复制代码
0:000> uf user32!DrawTextExW user32!DrawTextExW: 
00000000`779c9c64 sub     rsp,48h 
00000000`779c9c68 mov     rax,qword ptr [rsp+78h] 
00000000`779c9c6d or      dword ptr [rsp+30h],0FFFFFFFFh 
00000000`779c9c72 mov     qword ptr [rsp+28h],rax 
00000000`779c9c77 mov     eax,dword ptr [rsp+70h] 
00000000`779c9c7b mov     dword ptr [rsp+20h],eax 
00000000`779c9c7f call    user32!DrawTextExWorker (00000000`779ca944) 
00000000`779c9c84 add     rsp,48h 
00000000`779c9c88 ret 
复制代码

 

 

 

0x01    异常处理(Exception Handling)

这一节讨论X64函数用于异常处理的底层机制和数据结构,以及调试器如何使用这些数据结构回溯调用栈的,同时,也介绍一些X64调用栈上特有的内容。

  •  RUNTIME_FUNCTION 

 X64 可执行文件使用了一种 PE 文件格式的变种,叫做 PE32+,这种文件有一个额外的段,叫做“.pdata”或者Exception Directory,用于存放处理异常的信息。这个“Exception Directory”包含一系列RUNTIME_FUNCTION 结构,每一个non-leaf函数都会有一个RUNTIME_FUNCTION,这里所谓的non-leaf函数是指那些不再调用其他函数的函数。每一个RUNTIME_FUNCTION结构包含函数第一条指令和最后一条指令的偏移,以及一个指向unwind information结构的指针。Unwind information结构用于描述在异常发生的时候,函数调用栈该如何展开。 图4展示了一个模块的RUNTIME_FUNCTION结构。

 

                        Figure 4 : RUNTIME_FUNCTION 

 下面的汇编代码片段展示了X86平台与X64平台上异常处理的不同。在X86平台上,当高级语言使用了结构化异常处理,编译器会在函数的首尾生成特定的代码片段,用于在运行时构建异常栈帧。这些可以在下面的代码片段中看到,如:调用了ntdll!_SEH_prolog4和 ntdll!_SEH_epilog4. 

复制代码
0:009> uf ntdll!__RtlUserThreadStart ntdll!__RtlUserThreadStart: 
77009d4b push    14h 
77009d4d push    offset ntdll! ?? ::FNODOBFM::`string'+0xb5e (76ffc3d0) 
77009d52 call    ntdll!_SEH_prolog4 (76ffdd64) 
77009d57 and     dword ptr [ebp-4],0 
77009d5b mov     eax,dword ptr [ntdll!Kernel32ThreadInitThunkFunction (770d4224)] 
77009d60 push    dword ptr [ebp+0Ch] 
77009d63 test    eax,eax 
77009d65 je      ntdll!__RtlUserThreadStart+0x25 (77057075) 
 ntdll!__RtlUserThreadStart+0x1c: 
77009d6b mov     edx,dword ptr [ebp+8] 
77009d6e xor     ecx,ecx 
77009d70 call    eax 
77009d72 mov     dword ptr [ebp-4],0FFFFFFFEh 
77009d79 call    ntdll!_SEH_epilog4 (76ffdda9) 
77009d7e ret     8 
复制代码

 

 然而,在X64环境上的相同函数中,没有任何迹象表明当前函数使用了结构化异常处理,因为没有运行时的异常栈帧。通过从可执行文件中提取相应的信息,可以使用RUNTIME_FUNCTION结构和RIP一起确定相应的异常处理信息。

复制代码
0:000> uf ntdll!RtlUserThreadStart 
Flow analysis was incomplete, some code may be missing ntdll!RtlUserThreadStart: 
00000000`77c03260 sub     rsp,48h 
00000000`77c03264 mov     r9,rcx 
00000000`77c03267 mov     rax,qword ptr [ntdll!Kernel32ThreadInitThunkFunction (00000000`77d08e20)] 
00000000`77c0326e test    rax,rax 
00000000`77c03271 je      ntdll!RtlUserThreadStart+0x1f (00000000`77c339c5) 
 ntdll!RtlUserThreadStart+0x13: 00000000`77c03277 mov     r8,rdx 
00000000`77c0327a mov     rdx,rcx 
00000000`77c0327d xor     ecx,ecx 
00000000`77c0327f call    rax 
00000000`77c03281 jmp     ntdll!RtlUserThreadStart+0x39 (00000000`77c03283) 
 ntdll!RtlUserThreadStart+0x39: 
00000000`77c03283 add     rsp,48h 
00000000`77c03287 ret 
 ntdll!RtlUserThreadStart+0x1f: 
00000000`77c339c5 mov     rcx,rdx 00000000`77c339c8 call    r9 
00000000`77c339cb mov     ecx,eax 
00000000`77c339cd call    ntdll!RtlExitUserThread (00000000`77bf7130) 
00000000`77c339d2 nop 
00000000`77c339d3 jmp     ntdll!RtlUserThreadStart+0x2c (00000000`77c53923) 
复制代码

 

 

  •  UNWIND_INFO和UNWIND_CODE 

 RUNTIME_FUNCTION结构的BeginAddress和EndAddress存放着虚拟地址空间上的函数首地址和尾地址所对应的偏移,这些偏移是相对于模块基址的。当函数产生异常时,OS 会扫描内存中 PE,寻找当前指令地址所在的RUNTIME_FUNCTION结构。UnwindData域指向另外一个结构,用于告诉OS如何去展开栈。这个UNWIND_INFO结构包含各种UNWIND_CODE结构,每一个UNWIND_CODE都代表函数首部对应的操作。对 于 动 态 生 成 的 代 码 , OS 支 持 下 面 两 个 函 数 RtlAddFunctionTable() andRtlInstallFunctionTableCallback(),可以用于在运行过程中创建RUNTIME_FUNCTION 。

图5展示RUNTIME_FUNCTION和UNWIND_INFO的关系

                                                         Figure 5 : Unwind Information 

调试器命令“.fnent”可以显示指定函数的 RUNTIME_FUNCTIOIN 结构,下面的例子,使用”.fnent”显示 ntdll!RtlUserThreadStart 

复制代码
0:000> .fnent ntdll!RtlUserThreadStart 
Debugger function entry 00000000`03be6580 for: 
(00000000`77c03260)   ntdll!RtlUserThreadStart   |  (00000000`77c03290)   ntdll!RtlRunOnceExecuteOnce Exact matches:     ntdll!RtlUserThreadStart =  
 
BeginAddress      = 00000000`00033260 
EndAddress        = 00000000`00033290 
UnwindInfoAddress = 00000000`00128654 
 
Unwind info at 00000000`77cf8654, 10 bytes   version 1, flags 1, prolog 4, codes 1   frame reg 0, frame offs 0   handler routine: ntdll!_C_specific_handler (00000000`77be50ac), data 3 
  00: offs 4, unwind op 2, op info 8 UWOP_ALLOC_SMALL 
复制代码

 

 如果上面的 BeginAddress 加上 NTDLL 的基址,结果是 0x0000000077c03260,也就是函数RtlUserThreadStart 的首地址,如下面所示:

复制代码
0:000> ?ntdll+00000000`00033260 
Evaluate expression: 2009084512 = 00000000`77c03260 
 
0:000> u ntdll+00000000`00033260 ntdll!RtlUserThreadStart: 
00000000`77c03260 sub     rsp,48h 
00000000`77c03264 mov     r9,rcx 
00000000`77c03267 mov     rax,qword ptr [ntdll!Kernel32ThreadInitThunkFunction (00000000`77d08e20)] 
00000000`77c0326e test    rax,rax 
00000000`77c03271 je      ntdll!RtlUserThreadStart+0x1f (00000000`77c339c5) 
00000000`77c03277 mov     r8,rdx 
00000000`77c0327a mov     rdx,rcx 
00000000`77c0327d xor     ecx,ecx 
复制代码

如果EndAddress也用同样的方法计算,其结果指向上面函数的末尾 

复制代码
0:000> ?ntdll+00000000`00033290 
Evaluate expression: 2009084560 = 00000000`77c03290 
 
0:000> ub 00000000`77c03290 L10 ntdll!RtlUserThreadStart+0x11: 
00000000`77c03271 je      ntdll!RtlUserThreadStart+0x1f (00000000`77c339c5) 
00000000`77c03277 mov     r8,rdx 
00000000`77c0327a mov     rdx,rcx 
00000000`77c0327d xor     ecx,ecx 
00000000`77c0327f call    rax 
00000000`77c03281 jmp     ntdll!RtlUserThreadStart+0x39 (00000000`77c03283) 
00000000`77c03283 add     rsp,48h 
00000000`77c03287 ret 
00000000`77c03288 nop 
00000000`77c03289 nop 
00000000`77c0328a nop 
00000000`77c0328b nop 
00000000`77c0328c nop 
00000000`77c0328d nop 
00000000`77c0328e nop 
00000000`77c0328f nop 
复制代码

 

 所以,RUNTIME_FUNCTION结构中的BeginAddress和EndAddress描述了相应的函数在memory中的位置。然而,在链接过程中的优化可能会改变上述的内容,我们会在后续的章节中介绍。

 虽然UNWIND_INFO和UNWIND_CODE的主要目的是用于描述异常发生时,如何展开栈的。但是,调试器也可以利用这些信息,在没有symbol的时候,回溯函数调用栈。每一个UNWIND_CODE结构可以描述下面的一种操作,这些操作都会在函数首部中执行。

  • SAVE_NONVOL       将不可变寄存器的值保存在栈上
  • PUSH_NONVOL          将不可变寄存器的值压入栈
  • ALLOC_SMALL           在栈上分配空间,最多128 bytes Ø ALLOC_LARGE – 在栈上分配空间,最多4GB

所以,本质上,UNWIND_CODE是函数首部指令所对应的元指令,或者说是伪代码。 图6展示了函数首部操作栈的指令与UNWIND_CODE之间的关系。UNWIND_CODE结构与它们所对应的指令呈相反的顺序,这样,在异常发生的时候,栈可以按照创建时相反的方向进行展开。

 

                                                         Figure 6 : Unwind Code

 下面的例子展示了X64下的notepad.exe的`.pdata`段的HEADER信息,`virtual address`域指示了.pdata 段的位置是在可执行文件的0x13000的偏移处。

复制代码
T:\link -dump -headers c:\windows\system32\notepad.exe . 
. 
. 
SECTION HEADER #4 
  .pdata name 
     6B4 virtual size 
   13000 virtual address (0000000100013000 to 00000001000136B3) 
     800 size of raw data 
    F800 file pointer to raw data (0000F800 to 0000FFFF) 
       0 file pointer to relocation table 
       0 file pointer to line numbers 
       0 number of relocations 
       0 number of line numbers 
40000040 flags 
         Initialized Data 
         Read Only . 
复制代码

 

 下面一个例子是显示相同可执行文件的UNWIND_INFO和UNWIND_CODE,每一个UNWIND_CODE描述了一个操作,像PUSH_NONVOL或ALLOC_SMALL,这些指令是在函数首部执行的,并在栈展开时撤销的。”.fnent”命令可以显示这两个结构的内容,但是,不够详细,而"link -dump -unwindinfo"命令可以显示完整的内容。

复制代码
T:\link -dump -unwindinfo c:\windows\system32\notepad.exe . 
 
. 
. 
  00000018 00001234 0000129F 0000EF68 
    Unwind version: 1 
    Unwind flags: None 
    Size of prologue: 0x12 
    Count of codes: 5     Unwind codes: 
      12: ALLOC_SMALL, size=0x28 
      0E: PUSH_NONVOL, register=rdi 
      0D: PUSH_NONVOL, register=rsi 
      0C: PUSH_NONVOL, register=rbp       0B: PUSH_NONVOL, register=rbx. 
. 
. 
. 
复制代码

上述的ALLOC_SMALL代表函数首部的sub指令,这会在栈空间上分配0x28字节的空间,每一个PUSH_NONVOL对应一个push指令,用于将不可变寄存器压入栈,并使用pop指令进行还原。这些指令可以在函数的汇编代码中看到

复制代码
0:000> ln notepad+1234 
(00000000`ff971234)   notepad!StringCchPrintfW   |  (00000000`ff971364)   notepad!CheckSave Exact matches: 
    notepad!StringCchPrintfW =      notepad!StringCchPrintfW =  
 
0:000> uf notepad!StringCchPrintfW notepad!StringCchPrintfW: 
00000001`00001234 mov     qword ptr [rsp+18h],r8 
00000001`00001239 mov     qword ptr [rsp+20h],r9 
00000001`0000123e push    rbx 
00000001`0000123f push    rbp 
00000001`00001240 push    rsi 
00000001`00001241 push    rdi 
00000001`00001242 sub     rsp,28h 
00000001`00001246 xor     ebp,ebp 
00000001`00001248 mov     rsi,rcx 
00000001`0000124b mov     ebx,ebp 
00000001`0000124d cmp     rdx,rbp 
00000001`00001250 je      notepad!StringCchPrintfW+0x27 (00000001`000077b5) ... notepad!StringCchPrintfW+0x5c: 00000001`00001294 mov     eax,ebx 
00000001`00001296 add     rsp,28h 
00000001`0000129a pop     rdi 
00000001`0000129b pop     rsi 
00000001`0000129c pop     rbp 
00000001`0000129d pop     rbx 
复制代码

 

  •  性能优化(Performance Optimization)

 Windows 操作系统中的可执行文件采用了一种叫做 Basic Block Tools(BBT)的优化,这种优化会提升代码的局部性。频繁执行的函数块被放在一起,这样会更可能放在相同的页上,而对于那些不频繁使用的部分被移到其他位置。这种方法减少了需要同时保留在内存中的页数,从而导致整个working set的减少。为了使用这种优化方案,可执行文件会被链接、执行、评测,最后,使用评测结果重新组合那些频繁执行的函数部分。 在重组过的函数中,一些函数块被移出函数主体,这些原本是定义在RUNTIME_FUNCTION结构中的。由于函数块的移动,导致函数体被分割成多个不同的部分。因此,链接过程中生成的UNTIME_FUNCTION结构已经不能再准确地描述这个函数。为了解决这个问题,BBT过程新增了多个 RUNTIME_FUNCTION 结构,每一个 RUNTIME_FUNCTION 对应一个优化过的函数块。这些RUNTIME_FUNCTION被链在一起,以最初的RUNTIME_FUNTION结尾,这样,最后的这个RUNTIME_FUNTION的BeginAddress会一直指向函数的首地址。 图7展示了由3个基础块组成的函数。在BBT优化以后,#2块被移除函数体,从而导致原先的RUNTIME_FUNCTION 的信息失效。所以,BBT优化过程创建了第二个RUNTIME_FUNCTION结构,并将它串联到第一个,下图描述了整个过程。

 

                                     Figure 7 : Performance Optimization : Basic Block Tools 

当前公开版本的调试器不能回溯RUNTIME_FUNCTION的完整链,所以,调试器不能正确地显示优化过的函数名,相应的返回地址映射到那些被移出函数体的函数块。

下面的例子展示了函数的调用栈,其中,函数名不能正常显示,取而代之的是ntdll! ?? ::FNODOBFM::`string'。调 试 器 错 误 地 将 返 回 地 址 0x00000000`77c17623 转 成 #0x0c 号 栈 帧 的 函 数 名 ntdll! ?? ::FNODOBFM::`string'+0x2bea0 

复制代码
0:000> kn 
 # Child-SP          RetAddr           Call Site 
00 00000000`0029e4b8 000007fe`fdd21726 ntdll! ?? ::FNODOBFM::`string'+0x6474 
01 00000000`0029e4c0 000007fe`fdd2dab6 KERNELBASE!BaseSetLastNTError+0x16 
02 00000000`0029e4f0 00000000`77ad108f KERNELBASE!AccessCheck+0x64 
03 00000000`0029e550 00000000`77ad0d46 kernel32!BasepIsServiceSidBlocked+0x24f 
04 00000000`0029e670 00000000`779cd161 kernel32!LoadAppInitDlls+0x36 
05 00000000`0029e6e0 00000000`779cd42d user32!ClientThreadSetup+0x22e 
06 00000000`0029e950 00000000`77c1fdf5 user32!_ClientThreadSetup+0x9 
07 00000000`0029e980 000007fe`ffe7527a ntdll!KiUserCallbackDispatcherContinue 
08 00000000`0029e9d8 000007fe`ffe75139 gdi32!ZwGdiInit+0xa 
09 00000000`0029e9e0 00000000`779ccd1f gdi32!GdiDllInitialize+0x11b 
0a 00000000`0029eb40 00000000`77c0c3b8 user32!UserClientDllInitialize+0x465 
0b 00000000`0029f270 00000000`77c18368 ntdll!LdrpRunInitializeRoutines+0x1fe 
0c 00000000`0029f440 00000000`77c17623 ntdll!LdrpInitializeProcess+0x1c9b 
0d 00000000`0029f940 00000000`77c0308e ntdll! ?? ::FNODOBFM::`string'+0x2bea0 
0e 00000000`0029f9b0 00000000`00000000 ntdll!LdrInitializeThunk+0xe 
复制代码

 

下面的例子将使用上面用到的返回地址 0x00000000`77c17623 来显示错误函数名的 RUNTIME_FUNCTION, UNWIND_INFO和UNWIND_CODEs。显示的信息包含一个名为”Chained Info”的段,用于指示函数代码块被移出函数体。

复制代码
0:000> .fnent 00000000`77c17623 
Debugger function entry 00000000`03b35da0 for: 
(00000000`77c55420)   ntdll! ?? ::FNODOBFM::`string'+0x2bea0   |  (00000000`77c55440)   ntdll! ?? ::FNODOBFM::`string' 
 
BeginAddress      = 00000000`000475d3 
EndAddress        = 00000000`00047650 
UnwindInfoAddress = 00000000`0012eac0 
 
Unwind info at 00000000`77cfeac0, 10 bytes   version 1, flags 4, prolog 0, codes 0   frame reg 0, frame offs 0 
 
Chained info: 
BeginAddress      = 00000000`000330f0 
EndAddress        = 00000000`000331c0 
UnwindInfoAddress = 00000000`0011d08c 
 
Unwind info at 00000000`77ced08c, 20 bytes   version 1, flags 1, prolog 17, codes a   frame reg 0, frame offs 0   handler routine: 00000000`79a2e560, data 0 
  00: offs f0, unwind op 0, op info 3 UWOP_PUSH_NONVOL 
  01: offs 3, unwind op 0, op info 0 UWOP_PUSH_NONVOL 
  02: offs c0, unwind op 1, op info 3 UWOP_ALLOC_LARGE FrameOffset: d08c0003 
  04: offs 8c, unwind op 0, op info d UWOP_PUSH_NONVOL 
  05: offs 11, unwind op 0, op info 0 UWOP_PUSH_NONVOL 
  06: offs 28, unwind op 0, op info 0 UWOP_PUSH_NONVOL 
  07: offs 0, unwind op 0, op info 0 UWOP_PUSH_NONVOL 
  08: offs 0, unwind op 0, op info 0 UWOP_PUSH_NONVOL 
  09: offs 0, unwind op 0, op info 0 UWOP_PUSH_NONVOL 
复制代码

上面说到的Chained Info中BeginAddress指向原先函数的首地址,可以使用`ln`命令看看这个函数的实际函数名。

0:000> ln ntdll+000330f0 
(00000000`77c030f0)   ntdll!LdrpInitialize   |  (00000000`77c031c0)   ntdll!LdrpAllocateTls 

Exact matches:

 ntdll!LdrpInitialize =  

调试器的`uf`命令可以显示完整的函数汇编代码,这个命令之所以可以做到这点,是通过每个代码块最后的 jmp/jCC指令来访问所有的代码块。下面的输出展示了函数ntdll!LdrpInitialize的汇编代码,函数主体是从00000000`77c030f0到00000000`77c031b3,然而,有一个代码块是在00000000`77bfd1a4。这样的代码移动是由于 BBT 优化的结果,调试器尝试将这个地址与最近的符号对应起来,也就是上面说到

的 "ntdll! ?? ::FNODOBFM::`string'+0x2c01c" 

复制代码
0:000> uf 00000000`77c030f0 ntdll! ?? ::FNODOBFM::`string'+0x2c01c: 
00000000`77bfd1a4 48c7842488000000206cfbff mov qword ptr [rsp+88h],0FFFFFFFFFFFB6C20h 
00000000`77bfd1b0 443935655e1000  cmp     dword ptr [ntdll!LdrpProcessInitialized (00000000`77d0301c)],r14d 
00000000`77bfd1b7 0f856c5f0000    jne     ntdll!LdrpInitialize+0x39 (00000000`77c03129) . 
. . 
ntdll!LdrpInitialize: 
00000000`77c030f0 48895c2408      mov     qword ptr [rsp+8],rbx 
00000000`77c030f5 4889742410      mov     qword ptr [rsp+10h],rsi 
00000000`77c030fa 57              push    rdi 
00000000`77c030fb 4154            push    r12 
00000000`77c030fd 4155            push    r13 
00000000`77c030ff 4156            push    r14 
00000000`77c03101 4157            push    r15 
00000000`77c03103 4883ec40        sub     rsp,40h 
00000000`77c03107 4c8bea          mov     r13,rdx 00000000`77c0310a 4c8be1          mov     r12,rcx . 
. . ntdll!LdrpInitialize+0xac: 
00000000`77c0319c 488b5c2470      mov     rbx,qword ptr [rsp+70h] 
00000000`77c031a1 488b742478      mov     rsi,qword ptr [rsp+78h] 
00000000`77c031a6 4883c440        add     rsp,40h 
00000000`77c031aa 415f            pop     r15 
00000000`77c031ac 415e            pop     r14 
00000000`77c031ae 415d            pop     r13 
00000000`77c031b0 415c            pop     r12 
00000000`77c031b2 5f              pop     rdi 
00000000`77c031b3 c3              ret 
复制代码

经过BBT优化过的模块可以被`!lmi`命令识别出来,在命令的输出中,”Characteristics”域会标示为”perf”。

复制代码
0:000> !lmi notepad 
Loaded Module Info: [notepad]  
         Module: notepad 
   Base Address: 00000000ff4f0000 
     Image Name: notepad.exe 
   Machine Type: 34404 (X64) 
     Time Stamp: 4a5bc9b3 Mon Jul 13 16:56:35 2009 
           Size: 35000 
       CheckSum: 3e749 
Characteristics: 22  perf 
Debug Data Dirs: Type  Size     VA  Pointer 
             CODEVIEW    24,  b74c,    ad4c RSDS - GUID: 
{36CFD5F9-888C-4483-B522-B9DB242D8478} 
               Age: 2, Pdb: notepad.pdb 
                CLSID     4,  b748,    ad48 [Data not mapped] 
     Image Type: MEMORY   - Image read successfully from loaded memory.     Symbol Type: PDB      - Symbols loaded successfully from symbol server. 
                 c:\symsrv\notepad.pdb\36CFD5F9888C4483B522B9DB242D84782\notepad.pdb     Load Report: public symbols , not source indexed                   c:\symsrv\notepad.pdb\36CFD5F9888C4483B522B9DB242D84782\notepad.pdb 
复制代码

 

 

0x02      参数传递(Parameter Passing)

本节讨论X64平台上参数是如何传递的,函数栈帧是如何构建的,以及调试器如何使用这些信息回溯调用栈。

  • 基于寄存器的参数传递(Register based parameter passing)

X64平台上,函数的前4个参数是通过寄存器传递,剩余的参数是通过栈传递。这是调试过程中最主要的痛苦之一,因为寄存器的值在函数执行过程中会被修改,从而导致很难确定传入函数的参数值是什么。另外一个问题是参数恢复问题,X64平台上的调试与X86平台上的调试有很大的差异。 图8展示了X64汇编代码如何在调用函数与被调函数之间传递参数的:

                                                  Figure 8 : Parameter Passing on X64 

下面的调用栈展示函数kernel32!CreateFileWImplementation调用 KERNELBASE!CreateFileW。

复制代码
0:000> kn 
 # Child-SP          RetAddr           Call Site 
00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 
01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 
02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d . 
. 
. 
复制代码

MSDN的文档上来看,函数CreateFileW()有7个参数,函数原型如下:

复制代码
HANDLE WINAPI  
CreateFile( 
  __in      LPCTSTR lpFileName, 
  __in      DWORD dwDesiredAccess, 
  __in      DWORD dwShareMode, 
  __in_opt  LPSECURITY_ATTRIBUTES lpSecurityAttributes, 
  __in      DWORD dwCreationDisposition, 
  __in      DWORD dwFlagsAndAttributes, 
  __in_opt  HANDLE hTemplateFile ); 
复制代码

 

从上面的调用栈可以看出,函数KERNELBASE!CreateFileW的返回地址是00000000`77ac2aad。可以反向显示这个地址的汇编代码,那样,就可以看到调用 KERNELBASE!CreateFileW 之前的代码。下面这 4 条指令:"mov rcx,rdi", "mov edx,ebx", "mov r8d,ebp", "mov r9,rsi" 是在做调用kernel32!CreateFileW函数的准备工作,将前4个参数放在寄存器上。同样,下面这几条指令:"mov dword ptr [rsp+20h],eax", "mov dword ptr [rsp+28h],eax" and "mov qword ptr [rsp+30h],rax" 是将参数放在栈帧上。

复制代码
0:000> ub  00000000`77ac2aad L10 kernel32!CreateFileWImplementation+0x35: 
00000000`77ac2a65 lea     rcx,[rsp+40h] 
00000000`77ac2a6a mov     edx,ebx 
00000000`77ac2a6c call    kernel32!BaseIsThisAConsoleName (00000000`77ad2ca0) 
00000000`77ac2a71 test    rax,rax 
00000000`77ac2a74 jne     kernel32!zzz_AsmCodeRange_End+0x54fc (00000000`77ae7bd0) 
00000000`77ac2a7a mov     rax,qword ptr [rsp+90h] 
00000000`77ac2a82 mov     r9,rsi 
00000000`77ac2a85 mov     r8d,ebp 
00000000`77ac2a88 mov     qword ptr [rsp+30h],rax 
00000000`77ac2a8d mov     eax,dword ptr [rsp+88h] 
00000000`77ac2a94 mov     edx,ebx 
00000000`77ac2a96 mov     dword ptr [rsp+28h],eax 
00000000`77ac2a9a mov     eax,dword ptr [rsp+80h] 
00000000`77ac2aa1 mov     rcx,rdi 
00000000`77ac2aa4 mov     dword ptr [rsp+20h],eax 
00000000`77ac2aa8 call    kernel32!CreateFileW (00000000`77ad2c88) 
复制代码

 

  • Homing Space 

虽然前4个参数被放在寄存器上,但是,在栈帧空间上依然会分配相应的空间。这个叫做参数的Homing Space, 用于存放参数的值,如果参数是传址而不是传值,或者函数编译过程中打开/homeparams标志。这个Homing Space 的最小空间尺寸是0x20个字节,即便函数的参数小于4个。如果Homing Space没有用于存放参数的值,编译器会用它们存放不可变寄存器的值。 图9展示了栈空间上的Homing Space,以及在函数初始阶段是如何将不可变寄存器的值存放在Homing Space中。

                                            Figure 9 : Parameter Homing Space 

在下面的例子中,指令"sub rsp, 20h"表明函数初始阶段在栈空间上分配了0x20个字节的空间,这已足以存放4 个64位的值。下面一部分显示msvcrt!malloc()是一个no-leaf函数,它会调用其他的函数。

复制代码
0:000> uf msvcrt!malloc msvcrt!malloc: 
000007fe`fe6612dc mov     qword ptr [rsp+8],rbx 
000007fe`fe6612e1 mov     qword ptr [rsp+10h],rsi 
000007fe`fe6612e6 push    rdi 
000007fe`fe6612e7 sub     rsp,20h 
000007fe`fe6612eb cmp     qword ptr [msvcrt!crtheap (000007fe`fe6f1100)],0 
000007fe`fe6612f3 mov     rbx,rcx 
000007fe`fe6612f6 je      msvcrt!malloc+0x1c (000007fe`fe677f74) . 
. 
. 
 
0:000> uf /c msvcrt!malloc msvcrt!malloc (000007fe`fe6612dc)   msvcrt!malloc+0x6a (000007fe`fe66132c): 
    call to ntdll!RtlAllocateHeap (00000000`77c21b70)   msvcrt!malloc+0x1c (000007fe`fe677f74): 
    call to msvcrt!core_crt_dll_init (000007fe`fe66a0ec)   msvcrt!malloc+0x45 (000007fe`fe677f83): 
    call to msvcrt!FF_MSGBANNER (000007fe`fe6ace0c)   msvcrt!malloc+0x4f (000007fe`fe677f8d): 
    call to msvcrt!NMSG_WRITE (000007fe`fe6acc10)   msvcrt!malloc+0x59 (000007fe`fe677f97): 
    call to msvcrt!_crtExitProcess (000007fe`fe6ac030)   msvcrt!malloc+0x83 (000007fe`fe677fad): 
    call to msvcrt!callnewh (000007fe`fe696ad0)   msvcrt!malloc+0x8e (000007fe`fe677fbb):     call to msvcrt!errno (000007fe`fe661918) . 
. 
. 
复制代码

下面的汇编代码片段是WinMain函数的初始阶段,4个不可变寄存器将被保存在栈空间上的Homing Space。

复制代码
0:000> u notepad!WinMain notepad!WinMain: 
00000000`ff4f34b8 mov     rax,rsp 
00000000`ff4f34bb mov     qword ptr [rax+8],rbx 
00000000`ff4f34bf mov     qword ptr [rax+10h],rbp 
00000000`ff4f34c3 mov     qword ptr [rax+18h],rsi 
00000000`ff4f34c7 mov     qword ptr [rax+20h],rdi 
00000000`ff4f34cb push    r12 
00000000`ff4f34cd sub     rsp,70h 
00000000`ff4f34d1 xor     r12d,r12d 
复制代码
  • Parameter Homing 

如上一节所描述,所有的X64 non-leaf函数都会在他们的栈空间中分配相应的Homing Space。如X64的调用约定,调用函数使用 4 个寄存器传递参数给被调函数。当使用/homeparams 标志开启参数空间时,只有被调函数的代码会受到影响。使用Windows Driver Kit(WDK)编译环境,在checked/debug build中,这个标志一直是打开的。被调函数的初始化阶段从寄存器中读取参数的值,并将这些值存放在参数的homing space中。 图10展示了调用函数的汇编代码,它将参数传到相应的寄存器中。同时,也展示了被调函数的初始化阶段,这个函数使用了/homeparams 标志,从而,会将参数放在 homing space 上。被调函数的初始化阶段从寄存器中读取参数,并将这些值存放在栈上的参数homing space中。

                                               Figure 10 : Parameter Homing 

下面的代码片段展示了寄存器的值被存放在homing area上

复制代码
0:000> uf msvcrt!printf msvcrt!printf: 
000007fe`fe667e28 mov     rax,rsp 
000007fe`fe667e2b mov     qword ptr [rax+8],rcx 
000007fe`fe667e2f mov     qword ptr [rax+10h],rdx 
000007fe`fe667e33 mov     qword ptr [rax+18h],r8 
000007fe`fe667e37 mov     qword ptr [rax+20h],r9 
000007fe`fe667e3b push    rbx 
000007fe`fe667e3c push    rsi 
000007fe`fe667e3d sub     rsp,38h 
000007fe`fe667e41 xor     eax,eax 
000007fe`fe667e43 test    rcx,rcx 000007fe`fe667e46 setne   al 
000007fe`fe667e49 test    eax,eax 
000007fe`fe667e4b je      msvcrt!printf+0x25 (000007fe`fe67d74b) . 
. 
. 
复制代码

 

 

0x03        堆栈使用(Stack Usage)

X64函数的栈帧包括下面内容:

  • 返回地址
  • 不可变寄存器的值
  • 局部变量
  • 基于栈的参数 Ø 基于寄存器的参数 除了返回地址之前,其他都是在函数初始阶段存放的。栈空间由局部变量、基于栈的参数和参数Homing Space组成,并且都是由这样的一条指令完成空间分配的:"sub rsp, xxx"。为基于栈的参数所预留的空间可以为调用者提供空间存放绝大多数的参数,基于寄存器的参数homing space只在non-leaf函数中保留。 图11展示X64 CPU上函数栈帧的布局。

                    Figure 11 : Stack Usage

调试器的”knf”命令可以显示调用栈上每一个栈帧所需的空间,这个值被放在”Memory”一栏。

复制代码
0:000> knf 
 #   Memory  Child-SP          RetAddr           Call Site 
00           00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 
01         8 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 
02       160 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d 
03        60 00000000`0029bdc0 000007fe`fe55dc08 usp10!UniStorInit+0xdd 
04        a0 00000000`0029be60 000007fe`fe5534af usp10!InitUnistor+0x1d8 
复制代码

下面的汇编代码片段展示CreateFileW函数的初始阶段,将不可变寄存器R8D和EDX的值保存在参数空间中,将RBX,RBP,RSI,RDI压入栈上,然后,分配0x138字节的空间,用于存放局部变量和将要传给被调函数的参数。

复制代码
0:000> uf KERNELBASE!CreateFileW KERNELBASE!CreateFileW: 
000007fe`fdd24ac0 mov     dword ptr [rsp+18h],r8d 
000007fe`fdd24ac5 mov     dword ptr [rsp+10h],edx 
000007fe`fdd24ac9 push    rbx 
000007fe`fdd24aca push    rbp 
000007fe`fdd24acb push    rsi 
000007fe`fdd24acc push    rdi 
000007fe`fdd24acd sub     rsp,138h 
000007fe`fdd24ad4 mov     edi,dword ptr [rsp+180h] 
000007fe`fdd24adb mov     rsi,r9 
000007fe`fdd24ade mov     rbx,rcx 
000007fe`fdd24ae1 mov     ebp,2 
000007fe`fdd24ae6 cmp     edi,3 
000007fe`fdd24ae9 jne     KERNELBASE!CreateFileW+0x449 (000007fe`fdd255ff) 
复制代码

 

 

  • Child-SP

调试命令`k`显示的Child-SP寄存器的值代表着RSP寄存器所指向的地址,也就是所显示的函数在完成函数初始阶段之后,栈顶指针的位置。随后被压入栈的是函数的返回地址,由于X64函数在函数初始化以后不会修改RSP,任何涉及栈访问的操作都是通过这个栈指针(RSP)完成的,包括访问参数和局部变量。图12展示函数f2的栈帧以及它与命令`k`所显示的调用栈之间的关系。返回地址RA1指向函数f2在调用`call f1` 这条指令之后的位置,这个地址出现在调用栈上紧邻RSP2所指向的位置。

           Figure 12 : Relationship between Child-SP and function frames 

在下面的调用栈中,栈帧#1的Child-SP是00000000`0029bc00,这是函数CreateFileW()的初始化阶段结束以后,RSP的值。

复制代码
0:000> knf 
 #   Memory  Child-SP          RetAddr           Call Site 
00           00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 
01         8 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 
02       160 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d 
03        60 00000000`0029bdc0 000007fe`fe55dc08 usp10!UniStorInit+0xdd 04        a0 00000000`0029be60 000007fe`fe5534af usp10!InitUnistor+0x1d8 . 
. 
. 
复制代码

 

如上所述,函数#01的RSP(value is 00000000`0029bc00)所指位置之前的8个字节应该是函数#00的返回地址。

0:000> dps 00000000`0029bc00-8 L1 
00000000`0029bbf8  000007fe`fdd24d76 KERNELBASE!CreateFileW+0x2cd 
  • 回溯调用栈(Walking the call stack)

在X86 CPU上,调试器使用EBP chain来回溯调用栈,从最近的函数栈帧到最远的函数栈帧。通常情况下,调试器可以回溯栈帧,而不依赖于调试符号。然而,EBP chain 可能会在某些情况下被破坏,如 frame pointer omitted(FPO)。这种情况下,调试器需要使用相应的调试符号才能正确地回溯栈帧。在X64函数中,并没有使用RBP作为栈帧指针,从而,调试器没有EBP chain来做栈回溯。在这种情况下,调试器通过定位RUNTIME_FUNCTION, UNWIND_INFO和UNWIND_CODE这些结构,去计算每一个函数所需的栈帧空间,然后,加上相应的RSP,便可以计算出下面Child-SP的值。图13展示函数栈帧的布局,栈帧的大小=返回地址(8个字节)+不可变寄存器+局部变量+基于栈的参数+基于寄存器的参数(0x20个字节)。UNWIND_CODE中的信息包含了不可变寄存器的数量,以及栈上的局部变量和参数信息。

                Figure 13 : Walking the x64 call stack 

下面的调用栈中,栈帧#1(CreateFileW)所对应的栈帧空间时 0x160 个字节,下一节会告诉你,这个数值是如何计算出来的,以及调试器是如何计算栈帧#2的Child-SP的。注意:函数#1栈帧空间的值是在函数#2的Memory 栏。

复制代码
0:000> knf 
 #   Memory  Child-SP          RetAddr           Call Site 
00           00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 
01         8 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 
02       160 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d
03        60 00000000`0029bdc0 000007fe`fe55dc08 usp10!UniStorInit+0xdd 04        a0 00000000`0029be60 000007fe`fe5534af usp10!InitUnistor+0x1d8 . 
. 
. 
复制代码

下面是UNWIND_CODE的输出信息。共有4个不可变寄存器被压入栈中,分配了0x138字节的空间给局部变量和参数使用。

复制代码
0:000> .fnent kernelbase!CreateFileW 
Debugger function entry 00000000`03be6580 for: 
(000007fe`fdd24ac0)   KERNELBASE!CreateFileW   |  (000007fe`fdd24e2c)   
KERNELBASE!SbSelectProcedure Exact matches: 
    KERNELBASE!CreateFileW =  
 
BeginAddress      = 00000000`00004ac0 
EndAddress        = 00000000`00004b18 
UnwindInfoAddress = 00000000`00059a48 
 
Unwind info at 000007fe`fdd79a48, 10 bytes   version 1, flags 0, prolog 14, codes 6   frame reg 0, frame offs 0 
  00: offs 14, unwind op 1, op info 0 UWOP_ALLOC_LARGE FrameOffset: 138 
  02: offs d, unwind op 0, op info 7 UWOP_PUSH_NONVOL 
  03: offs c, unwind op 0, op info 6 UWOP_PUSH_NONVOL 
  04: offs b, unwind op 0, op info 5 UWOP_PUSH_NONVOL 
  05: offs a, unwind op 0, op info 3 UWOP_PUSH_NONVOL 
复制代码

根据上面的分析,栈帧空间应该是0x138+(8*4)=0x158字节

0:000> ?138+(8*4) 
Evaluate expression: 344 = 00000000`00000158 

再加上8个字节的返回地址,正好是0x160字节。这与调试命令`knf`所显示的一致。

0:000> ?158+8 
Evaluate expression: 352 = 00000000`00000160 

根据`knf`命令的输出,调试器在栈帧#01的RSP(00000000`0029bc00)基础上加上0x160,正好可以得到栈帧#02的RSP,即:00000000`0029bd60 

0:000> ?00000000`0029bc00+160 
Evaluate expression: 2735456 = 00000000`0029bd60 

所以,每一个栈帧所需的空间可以通过PE文件中的RUNTIME_FUNCTION,UNWIND_INFO以及UNWIND_CODE计算出。由于这个原因,调试器可以无需调试符号的情况下回溯栈帧。下面的调用栈是vmswitch模块的状态,虽然没有调试符号,但是,这并不影响调试器正常地显示和回溯栈帧。这里告诉了一个事实:X64调用栈可以在没有调试符号的情况下回溯。

复制代码
1: kd> kn 
 # Child-SP          RetAddr           Call Site 
00 fffffa60`005f1a68 fffff800`01ab70ee nt!KeBugCheckEx 
01 fffffa60`005f1a70 fffff800`01ab5938 nt!KiBugCheckDispatch+0x6e . 
. . 
21    fffffa60`01718840 fffffa60`0340b69e vmswitch+0x5fba 
22    fffffa60`017188f0 fffffa60`0340d5cc vmswitch+0x769e 
23    fffffa60`01718ae0 fffffa60`0340e615 vmswitch+0x95cc 24 fffffa60`01718d10 fffffa60`009ae31a vmswitch+0xa615 . 
. . 
44    fffffa60`0171aed0 fffffa60`0340b69e vmswitch+0x1d286 
45    fffffa60`0171af60 fffffa60`0340d4af vmswitch+0x769e 
46    fffffa60`0171b150 fffffa60`034255a0 vmswitch+0x94af 
47    fffffa60`0171b380 fffffa60`009ac33c vmswitch+0x215a0 . 
. 
. 
复制代码

 

 

0x04              参数找回(Parameter Retrieval)

在之前的章节中,我们通过调试器输出的调用栈的信息剖析了X64的内部工作机理。在本节中,这些理论知识将被用于找回基于寄存器的参数。很不幸,并没有什么特别有效的方法去找回这些参数,这里所介绍的技巧依赖于X64 汇编指令。如果参数不能在memory中找到,那么,并没有什么简单的方法去获取这种参数。即便有调试符号,也没有什么帮助,因为,调试符号会告诉相应函数的参数类型以及数量,但是,并不会告诉我们这些参数是什么。

 

0x05                  技术总结(Summary of Techniques) 

本节讨论是假设X64函数并没有使用/homeparams编译,当使用了/homeparams,找回基于寄存器的参数并没有意义,因为它们已经被放在栈上的homing parameters区域。同样,无论是否使用/homeparams,第五个以及更高的参数也被放在栈上,所以,找回这些参数也不是什么问题。 在live debugging中,在函数上设置断点是最简单的方法去获取传入的参数,因为在函数的初始化阶段,前四个参数肯定是放在RCX,RDX,R8和R9上的。 然而,在函数体内,参数寄存器的内容可能已经改变了,所以,在函数执行的任何时刻,确定寄存器参数的值,我们需要知道,这些参数是从哪里读取的,以及将被写入到什么地方?可以按照下面这些过程来回答这些问题:

  • 参数是否是从内存中加载到寄存器中的,如果是的话,相应的内存位置存放参数值
  • 参数是否是从不可变寄存器中加载的,并且,这些不可变寄存器被被调函数保存,如果是的话,不可变寄存器存放参数
  • 参数是否是从寄存器中保存到内存中,如果是的话,相应的内存位置存放参数值
  • 参数是否是保存到不可变寄存器中,并且,这些不可变寄存器被被调函数保存,如果是的话,不可变寄存器存放参数

在下面章节中,会用例子详细描述上面介绍的技巧,每一个技巧都需要反汇编相应调用函数与被调函数。在图 14 中,为了找出函数f2的参数,frame 02用于从源头找出参数,frame 00用于从目标找出参数。

                   Figure 14 : Finding Register Based Parameters 

 

  • 识别参数的读取目标(Identifying Parameter Sources)

这个技巧是用于识别被加载到参数寄存器的值所对应的源是什么,对常量、全局数据、栈地址和存放在栈上的数据有效。如图15所示,反汇编X64Caller可以看到加载到RCX,RDX,R8和R9的值,被作为参数传入X64Callee。

                         Figure 15 : Identifying parameter sources 

下面的例子用这个技巧来找出函数NtCreateFile()的第三个参数的值

复制代码
0:000> kn 
 # Child-SP          RetAddr           Call Site 
00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 
01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 
02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d 
. 
. 
. 
复制代码

从函数NtCreateFile()的原型可以知道,第三个参数的类型是POBJECT_ATTRIBUTES 

复制代码
NTSTATUS NtCreateFile( 
  __out     PHANDLE FileHandle, 
  __in      ACCESS_MASK DesiredAccess, 
  __in      POBJECT_ATTRIBUTES ObjectAttributes, 
  __out     PIO_STATUS_BLOCK IoStatusBlock, . 
. 
. ); 
复制代码

用返回地址反汇编调用者,显示下面的指令。加载到R8寄存器的值是RSP+0xC8。根据上面`kn`命令的输出,此时的RSP是函数KERNELBASE!CreateFileW的RSP,即:00000000`0029bc00

复制代码
0:000> ub 000007fe`fdd24d76 
KERNELBASE!CreateFileW+0x29d: 
000007fe`fdd24d46 and     ebx,7FA7h 
000007fe`fdd24d4c lea     r9,[rsp+88h] 
000007fe`fdd24d54 lea     r8,[rsp+0C8h] 000007fe`fdd24d5c lea     rcx,[rsp+78h] 
000007fe`fdd24d61 mov     edx,ebp 
000007fe`fdd24d63 mov     dword ptr [rsp+28h],ebx 
000007fe`fdd24d67 mov     qword ptr [rsp+20h],0 
000007fe`fdd24d70 call    qword ptr [KERNELBASE!_imp_NtCreateFile] 
复制代码

手工重构被加载到R8的值

复制代码
0:000> dt ntdll!_OBJECT_ATTRIBUTES 00000000`0029bc00+c8 
   +0x000 Length           : 0x30 
   +0x008 RootDirectory    : (null)  
   +0x010 ObjectName       : 0x00000000`0029bcb0 _UNICODE_STRING 
"\??\C:\Windows\Fonts\staticcache.dat" 
   +0x018 Attributes       : 0x40 
   +0x020 SecurityDescriptor : (null)  
   +0x028 SecurityQualityOfService : 0x00000000`0029bc68 
复制代码
  • 不可变寄存器做参数读取目标(Non-Volatile Registers as parameter sources)

图 16 显示调用函数(X64Caller)和被调函数(X64Callee)的汇编代码。从下面的汇编代码可以看出,被加载到参数寄存器中的值是从不可变寄存器中读取的,并且,这些不可变寄存器又被保存在被调函数的栈上。这些保存的值可以被找回,也就间接地说明之前传入的参数也可以被找回。

                       Figure 16 : Non-Volatile Registers as parameter sources 

下面的例子使用这个技巧,用于找回函数CreateFileW()的第一个参数

复制代码
0:000> kn 
 # Child-SP          RetAddr           Call Site 
00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 
01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 
02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d . 
. 
. 
复制代码

函数CreateFile()的原型如下,第一个参数的类型是LPCTSTR 

复制代码
HANDLE WINAPI  
CreateFile( 
  __in      LPCTSTR lpFileName, 
  __in      DWORD dwDesiredAccess, 
  __in      DWORD dwShareMode, 
  __in_opt  LPSECURITY_ATTRIBUTES lpSecurityAttributes, . 
. 
. ); 
复制代码

 

使用frame 1的返回地址,反汇编调用函数。加载到RCX的值是RDI,一个不可变寄存器。下一步是看看被调函数如何保存RDI

复制代码
0:000> ub 00000000`77ac2aad L B kernel32!CreateFileWImplementation+0x4a: 
00000000`77ac2a7a mov     rax,qword ptr [rsp+90h] 
00000000`77ac2a82 mov     r9,rsi 
00000000`77ac2a85 mov     r8d,ebp 
00000000`77ac2a88 mov     qword ptr [rsp+30h],rax 
00000000`77ac2a8d mov     eax,dword ptr [rsp+88h] 
00000000`77ac2a94 mov     edx,ebx 
00000000`77ac2a96 mov     dword ptr [rsp+28h],eax 
00000000`77ac2a9a mov     eax,dword ptr [rsp+80h] 
00000000`77ac2aa1 mov     rcx,rdi 
00000000`77ac2aa4 mov     dword ptr [rsp+20h],eax 
00000000`77ac2aa8 call    kernel32!CreateFileW (00000000`77ad2c88) 
复制代码

反汇编被调函数,看看函数的初始阶段指令。RDI是被指令`push rdi`压入栈中,这个值与RCX的值一致。下一步是找回RDI的值

复制代码
0:000> u KERNELBASE!CreateFileW 
KERNELBASE!CreateFileW: 
000007fe`fdd24ac0 mov     dword ptr [rsp+18h],r8d 
000007fe`fdd24ac5 mov     dword ptr [rsp+10h],edx 
000007fe`fdd24ac9 push    rbx 
000007fe`fdd24aca push    rbp 
000007fe`fdd24acb push    rsi 000007fe`fdd24acc push    rdi 
000007fe`fdd24acd sub     rsp,138h 
000007fe`fdd24ad4 mov     edi,dword ptr [rsp+180h] 
复制代码

 

调试器的`.frame /r`命令显示不可变寄存器的值,所以,可以用于找回上述的不可变寄存器RDI。下面的命令显示RDI为000000000029beb0,这个值可以用于显示CreateFile()函数的第一个参数file name.

复制代码
0:000> .frame /r 2 
02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d rax=0000000000000005 rbx=0000000080000000 rcx=000000000029bc78 rdx=0000000080100080 rsi=0000000000000000 rdi=000000000029beb0 rip=0000000077ac2aad rsp=000000000029bd60 rbp=0000000000000005  r8=000000000029bcc8  r9=000000000029bc88 r10=0057005c003a0043 r11=00000000003ab0d8 r12=0000000000000000 r13=ffffffffb6011c12 r14=0000000000000000 r15=0000000000000000 
 
0:000> du /c 100 000000000029beb0 
00000000`0029beb0  "C:\Windows\Fonts\staticcache.dat" 
复制代码
  • 识别参数存储目标(Identifying parameter destinations)

这个技巧是找出参数寄存器中的值是否被写入内存。当函数使用/homeparams编译时,函数的初始阶段将保存寄存器参数到栈上的参数homing区域。然而,对于那些没有使用/homeparams编译的函数,参数寄存器的内容可能被写入到任意的内存区域。图17展示函数的汇编代码,这里寄存器RCX,RDX,R8和R9的值被写入栈上。所以,可以使用当前栈帧的RSP 来确定相应参数的内容。

                 Figure 17 : Identifying parameter destinations 

下面的例子使用这个技巧找出函数DispatchClientMessage()的第三个和第四个参数的值。

复制代码
0:000> kn 
 # Child-SP          RetAddr           Call Site .  
.  
. 
26    00000000`0029dc70 00000000`779ca01b user32!UserCallWinProcCheckWow+0x1ad 
27    00000000`0029dd30 00000000`779c2b0c user32!DispatchClientMessage+0xc3 
28    00000000`0029dd90 00000000`77c1fdf5 user32!_fnINOUTNCCALCSIZE+0x3c 
29    00000000`0029ddf0 00000000`779c255a ntdll!KiUserCallbackDispatcherContinue .  
.  
. 
复制代码

函数的第三个和第四个参数分别被放置在R8和R9寄存器上。反汇编函数DispatchClientMessage(),查看R8 和R9被写入到什么位置。可以看到这两个寄存器分别被这两条指令写入栈上,’mov qword ptr [rsp+20h],r8’ and ’mov qword ptr [rsp+28h],r9’。由于这两条指令并非在函数的初始阶段,而只是函数体首部的一部分。值得注意的是,在保存r8,r9之前,很有可能这两个寄存器的值已经被修改,所以,我们在使用这个技巧的时候,需要注意这个细节。当然,我们可以看到,这个例子中并没有这样的问题。

复制代码
0:000> uf user32!DispatchClientMessage user32!DispatchClientMessage: 
00000000`779c9fbc sub     rsp,58h 
00000000`779c9fc0 mov     rax,qword ptr gs:[30h] 
00000000`779c9fc9 mov     r10,qword ptr [rax+840h] 
00000000`779c9fd0 mov     r11,qword ptr [rax+850h] 
00000000`779c9fd7 xor     eax,eax 
00000000`779c9fd9 mov     qword ptr [rsp+40h],rax 
00000000`779c9fde cmp     edx,113h 
00000000`779c9fe4 je      user32!DispatchClientMessage+0x2a (00000000`779d7fe3) 
 user32!DispatchClientMessage+0x92: 
00000000`779c9fea lea     rax,[rcx+28h] 
00000000`779c9fee mov     dword ptr [rsp+38h],1 
00000000`779c9ff6 mov     qword ptr [rsp+30h],rax 
00000000`779c9ffb mov     qword ptr [rsp+28h],r9 
00000000`779ca000 mov     qword ptr [rsp+20h],r8 
00000000`779ca005 mov     r9d,edx 
00000000`779ca008 mov     r8,r10 
00000000`779ca00b mov     rdx,qword ptr [rsp+80h] 
00000000`779ca013 mov     rcx,r11 
00000000`779ca016 call    user32!UserCallWinProcCheckWow (00000000`779cc2a4) . 
. 
. 
复制代码

使用函数#27的RSP,可以分别找出r8和r9中的值

0:000> dp 00000000`0029dd30+20 L1 
00000000`0029dd50  00000000`00000000 
0:000> dp 00000000`0029dd30+28 L1 
00000000`0029dd58  00000000`0029de70 

 

  • 参数的存储目标是不可变寄存器(Non-Volatile Registers as Parameter  Destinations)

图18展示x64caller与x64callee的汇编代码。左边的代码说明寄存器参数被存放在不可变寄存器(RDI,RSI, RBX,RBP)上,右边的代码说明这些不可变寄存器的值被保存在栈上,所以,我们可以间接地找出传入的参数。

             Figure 18 : Non-Volatile Registers as Parameter Destinations

下面的例子将找出函数CreateFileW ()的前4个参数(译者注:原文是找出函数CreateFileWImplementation() 的参数,可能是作者的笔误)

0:000> kn 
 # Child-SP          RetAddr           Call Site 
00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 
01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 
02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d 
03 00000000`0029bdc0 000007fe`fe55dc08 usp10!UniStorInit+0xdd 

函数CreateFileWImplementation()完整的汇编代码如下,从函数初始阶段的指令来看,参数寄存器被保存在不可变寄存器中。注意:检查在调用 CreateFileW之前,这些不可变寄存器没有被修改过,这很重要!下一步是反汇编CreateFileW函数,找出这些保存参数的不可变寄存器是否被保存在栈上。

复制代码
0:000> uf kernel32!CreateFileWImplementation kernel32!CreateFileWImplementation: 
00000000`77ac2a30 mov     qword ptr [rsp+8],rbx 
00000000`77ac2a35 mov     qword ptr [rsp+10h],rbp 
00000000`77ac2a3a mov     qword ptr [rsp+18h],rsi 
00000000`77ac2a3f push    rdi 
00000000`77ac2a40 sub     rsp,50h 
00000000`77ac2a44 mov     ebx,edx 
00000000`77ac2a46 mov     rdi,rcx 
00000000`77ac2a49 mov     rdx,rcx 
00000000`77ac2a4c lea     rcx,[rsp+40h] 
00000000`77ac2a51 mov     rsi,r9 
00000000`77ac2a54 mov     ebp,r8d 
00000000`77ac2a57 call    qword ptr [kernel32!_imp_RtlInitUnicodeStringEx 
(00000000`77b4cb90)] 
00000000`77ac2a5d test    eax,eax 
00000000`77ac2a5f js      kernel32!zzz_AsmCodeRange_End+0x54ec (00000000`77ae7bc0) . 
. 
复制代码

下面函数 CreateFileW()的汇编代码可以看出,这些不可变寄存器都被保存在栈上,从而使用命令’.frame /r’ 来显示这些值。

复制代码
0:000> u KERNELBASE!CreateFileW KERNELBASE!CreateFileW: 
000007fe`fdd24ac0 mov     dword ptr [rsp+18h],r8d 
000007fe`fdd24ac5 mov     dword ptr [rsp+10h],edx 
000007fe`fdd24ac9 push    rbx 
000007fe`fdd24aca push    rbp 
000007fe`fdd24acb push    rsi 
000007fe`fdd24acc push    rdi 
000007fe`fdd24acd sub     rsp,138h 
000007fe`fdd24ad4 mov     edi,dword ptr [rsp+180h] 
复制代码

在栈帧#2上运行命令’.frame /r’,可以发现函数CreateFileWImplementation()栈帧上的不可变寄存器的值。

复制代码
0:000> .frame /r 02 
02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d rax=0000000000000005 rbx=0000000080000000 rcx=000000000029bc78 rdx=0000000080100080 rsi=0000000000000000 rdi=000000000029beb0 rip=0000000077ac2aad rsp=000000000029bd60 rbp=0000000000000005  r8=000000000029bcc8  r9=000000000029bc88 r10=0057005c003a0043 r11=00000000003ab0d8 r12=0000000000000000 r13=ffffffffb6011c12 r14=0000000000000000 r15=0000000000000000 iopl=0         nv up ei pl zr na po nc cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000244 kernel32!CreateFileWImplementation+0x7d: 
00000000`77ac2aad mov     rbx,qword ptr [rsp+60h] 
ss:00000000`0029bdc0={usp10!UspFreeForUniStore (000007fe`fe55d8a0)} 
复制代码

观察相应的mov指令可以看到不可变寄存器与参数之间的关系,如下:

  • P1 = RCX = RDI = 000000000029beb0
  • P2 = EDX = EBX = 0000000080000000
  • P3 = R8D = EBP = 0000000000000005
  • P4 = R9 = RSI = 0000000000000000

使用上面的技能找回 x64 调用栈上的参数的时候,可能会比较耗时和麻烦。CodeMachine 提供了一个 windbg extension,可以自动完成上述的过程,找回参数,有兴趣可以继续阅读相关的文章。

http://www.codemachine.com/tool_cmkd.html#stack

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
深入解析WINDOWS操作系统(第4版) ISBN:9787121039690 本书是著名的操作系统内核专家Mark Russinovich和David Solomon撰写的Windows操作系统原理的最新版著作,全面和深入地阐述了Windows操作系统的整体结构以及内部工作细节。本书针对Windows Server 2003、Windows XP和Windows 2000做了全面更新,通过许多练习实验让你直接感受到Windows的内部行为。另外,本书还介绍了一些高级诊断技术,以便使你的系统运行得更加平稳和高效。无论你是开发人员还是系统管理员,你都可以在本书中找到一些关键的、有关体系结构方面的知识,通过这些知识你可以更好地做系统设计、调试,以及性能优化。 全书内容丰富、信息全面,主要包括的Windows操作系统深度知识有:理解Windows的关键机制,包括系统服务分发和调度机制、启动和停机,以及注册表;挖掘Windows的安全模型,包括访问控制、特权和审计;利用内核调试器和其他的工具来检查内部系统结构;检查与进程、线程和作业相关的数据结构和算法;观察Windows如何管理虚拟内存和物理内存;理解NTFS的操作和格式,诊断文件系统访问问题;从上往下查看Windows的网络,包括映射、API、名称解析和协议驱动程序;诊断引导问题,执行崩溃分析。 本书适合广大Windows平台开发人员、IT专业从业人员等参考使用。 编辑推荐 ■ 国内知名译者潘爱民先生译作 ■ Windows系统之父Jim Allchin亲自撰文推荐! ■ Windows NT首席设计师David N. Cutler亲自撰文推荐! ■ 深入解析Windows操作系统!彻底揭开Windows技术内幕! ■ Csdn、博客堂、博客园、《程序员》杂志鼎力推荐! 目录第1章 概念和工具 1 1.1 Windows操作系统的版本 1 1.2 基础概念和术语 3 Windows API 3 服务、函数和例程 5 进程、线程和作业 6 虚拟内存 14 内核模式和用户模式 16 终端服务及多个会话 21 对象和句柄 22 安全性 23 注册表 24 Unicode 25 1.3 挖掘Windows内部机理 25 性能工具 27 Windows支持工具箱 27 Windows资源工具箱 27 内核调试 28 Platform SDK 33 DDK(设备驱动程序开发工具) 34 Sysinternals工具 34 1.4 本章总结 34 第2章 系统结构 35 2.1 需求和设计目标 35 2.2 操作系统模型 36 2.3 总体结构 37 可移植性 40 对称多处理 41 可伸缩性 46 客户和服务器版本之间的差异 47 检查版本 49 2.4 关键的系统组件 51 环境子系统和子系统DLL 53 硬件抽象层(HAL) 67 设备驱动程序 69 系统进程 75 2.5 本章总结 84 第3章 系统机制 85 3.1 陷阱分发 85 中断分发 87 异常分发 109 系统服务分发 119 3.2 对象管理器 124 执行体对象 126 对象结构 128 3.3 同步 149 高IRQL的同步 151 低IRQL的同步 155 3.4 系统辅助线程 166 3.5 Windows全局标志 168 3.6 本地过程调用(LPC) 171 3.7 内核事件追踪 175 3.8 Wow64 178 Wow64进程地址空间布局结构 179 系统调用 179 异常分发 179 用户回调 179 文件系统重定向 180 注册表的重定向和反射 180 I/O控制请求 181 16位安装器应用程序 182 打印 182 一些限制 182 3.9 本章总结 182 第4章 管理机制 183 4.1 注册表 183 查看和修改注册表 183 注册表用法 184 注册表数据类型 185 注册表逻辑结构 186 注册表问题的诊断 192 注册表的内部机理 197 4.2 服务 211 服务应用 212 服务账户 217 服务控制管理器 223 服务启动 225 启动错误 229 接受当前引导和“最后已知的好控制集” 230 服务失败 231 服务停机 232 共享的服务进程 233 服务控制程序 236 4.3 Windows管理规范 237 WMI体系结构 237 提供者 239 公共信息模型(CIM)和可管理对象的格式语言 240 WMI名字空间 243 类关联 244 WMI实现 247 WMI安全性 248 4.4 本章总结 249 第5章 启动和停机 251 5.1 引导过程 251 x86和x64引导准备 251 x86/x64引导扇区和Ntldr 255 IA64引导过程 264 初始化内核和执行体子系统 266 Smss、Csrss和Winlogon 269 自动启动的映像文件 273 5.2 引导和启动问题的故障检查 274 最后已知的好配置 274 安全模式 274 安全模式下的驱动程序加载 275 恢复控制台(Recovery Console) 279 解决常见的引导问题 281 5.3 停机 286 5.4 本章总结 288 第6章 进程、线程和作业 289 6.1 进程的内部机理 289 数据结构 289 内核变量 297 性能计数器 297 有关的函数 298 6.2 CreateProcess的流程 300 阶段1:打开将要被执行的映像 302 阶段2:创建Windows执行体进程对象 304 阶段3:创建初始线程,以及它的和执行环境 308 阶段4:将新进程通知Windows子系统 309 阶段5:启动初始线程的执行 310 阶段6:在新进程环境下执行进程初始化 310 6.3 线程的内部机理 313 数据结构 313 内核变量 320 性能计数器 321 有关的函数 322 一个线程的产生 322 6.4 检查线程活动 323 6.5 线程调度 325 Windows调度的概述 326 优先级别 327 Windows调度API 330 有关的工具 331 实时优先级 333 线程状态 334 分发器数据库 338 时限 340 调度情形 345 环境切换 347 空闲(Idle)线程 348 优先级提升 348 多处理器系统 357 多处理器的线程调度算法 366 6.6 作业对象 369 6.7 本章总结 374 第7章 内存管理 375 7.1 内存管理器简介 375 内存管理器组件 376 内部同步 377 配置内存管理器 378 检查内存的使用情况 378 7.2 内存管理器提供的服务 382 大页面和小页面 382 保留的和提交的页面 384 锁住内存 385 分配粒度 385 共享内存和映射文件 386 保护内存 388 “不可执行”页面保护 390 写时复制 392 堆管理器 394 地址窗口扩展 399 7.3 系统内存池 401 配置内存池的大小 401 监视内存池的使用 404 预读列表(Look-Aside List) 408 驱动程序检验器(Driver Verifier) 409 7.4 虚拟地址空间的布局结构 413 x86用户地址空间的布局结构 415 x86系统地址空间的布局结构 417 x86会话空间 418 系统页表项(PTE,Page Table Entry) 421 64位地址空间布局结构 422 7.5 地址转译 425 x86虚拟地址转译 425 地址转译快查缓冲区 434 物理地址扩展(PAE) 435 IA-64虚拟地址转译 437 x64虚拟地址转译 438 7.6 页面错误处理 439 无效PTE 440 原型PTE 441 页面换入I/O 443 冲突的页面错误 444 页面文件 444 7.7 虚拟地址描述符 448 7.8 内存区对象 450 7.9 工作集 457 按需换页 458 7.10 逻辑预取器 458 放置策略 462 工作集管理 463 平衡集管理器和交换器 466 系统工作集 467 7.11 页面帧编号数据库 469 页面列表的动态变化 472 已修改页面写出器 475 PFN数据结构 476 低内存通知和高内存通知 479 7.12 本章总结 483 第8章 安全性 485 8.1 安全系统组件 488 8.2 保护对象 492 访问检查 493 安全描述符和访问控制 506 8.3 账户权限和特权 516 账户权限 517 特权 518 超级特权 523 8.4 安全审计 524 8.5 登录(Logon) 526 Winlogon初始化 528 用户登录步骤 529 8.6 软件限制策略 533 8.7 本章总结 535 第9章 I/O系统 537 9.1 I/O系统组件 537 I/O管理器 539 典型的I/O处理过程 540 9.2 设备驱动程序 541 设备驱动程序的类型 541 驱动程序的结构 548 驱动程序对象和设备对象 550 打开设备 555 9.3 I/O处理 561 I/O类型 561 映射文件I/O和文件缓存 564 I/O请求包 564 针对单层驱动程序的I/O请求 569 针对分层的驱动程序的I/O请求 577 I/O完成端口 585 驱动程序检验器(Driver Verifier) 589 9.4 即插即用(PnP)管理器 590 即插即用支持的级别 591 驱动程序对于即插即用的支持 592 驱动程序加载、初始化和安装 594 驱动程序安装 603 9.5 电源管理器 607 电源管理器的操作 609 驱动程序的电源操作 610 驱动程序对于设备电源的控制 613 9.6 本章总结 613 第10章 存储管理 615 10.1 有关存储的术语 615 10.2 磁盘驱动程序 616 Ntldr 616 磁盘类、端口和小端口驱动程序 617 磁盘设备对象 620 分区管理器 622 10.3 卷的管理 622 基本磁盘 624 动态磁盘 626 多分区卷的管理 632 卷名字空间 638 卷的I/O操作 646 虚拟磁盘服务 648 卷影像(shadow)拷贝服务 649 10.4 本章总结 654 第11章 缓存管理器 655 11.1 缓存管理器的关键特性 655 单个中心化的系统缓存 656 内存管理器 656 缓存一致性 656 虚拟块缓存 658 流式缓存机制 658 对可恢复文件系统的支持 658 11.2 缓存的虚拟内存管理 660 11.3 缓存的大小 662 LargeSystemCache 662 缓存的虚拟大小 663 缓存的工作集大小 665 缓存的物理大小 667 11.4 缓存的数据结构 668 系统范围的缓存数据结构 669 针对每个文件的缓存数据结构 670 11.5 文件系统接口 674 从缓存中来回拷贝数据 676 通过映射和锁定接口进行缓存 677 通过直接内存访问接口进行缓存 678 11.6 快速I/O 679 11.7 预读(Read Ahead)和滞后写(Write Behind) 682 智能预读 682 回写缓存(Write-Back Caching)和延迟写(Lazy Writing) 683 写节流(Write Throttling) 686 系统线程 687 11.8 本章总结 688 第12章 文件系统 689 12.1 Windows文件系统格式 690 CDFS 690 UDF 691 FAT12、FAT16和FAT32 691 NTFS 694 12.2 文件系统驱动程序总体结构 694 本地FSD 695 远程FSD 696 文件系统操作 700 文件系统过滤型驱动程序 705 12.3 诊断文件系统的问题 711 Filemon的基本和高级模式 711 Filemon诊断技巧 712 12.4 NTFS设计目标和特性 717 高端(High-End)文件系统的需求 717 NTFS的高级特性 719 12.5 NTFS文件系统驱动程序 729 12.6 NTFS在磁盘上的结构 732 卷(volume) 732 簇(cluster) 732 主文件表(MFT) 733 文件引用号 739 文件纪录 740 文件名 742 驻留的和非驻留的属性 744 数据压缩和稀疏文件 747 变化日志文件 752 索引 753 对象ID 754 配额跟踪 755 统一的安全性 756 重解析点 758 12.7 NTFS的恢复支持 758 文件系统设计的演变 759 日志记录 761 恢复 767 NTFS的坏簇恢复 771 12.8 加密文件系统(EFS)安全性 775 第一次加密一个文件 778 解密过程 783 加密文件的备份 784 12.9 本章总结 785 第13章 网络 787 13.1 Windows的网络总体结构 787 OSI参考模型 787 Windows网络组件 789 13.2 网络API 791 Windows套接字(Windows Sockets) 791 远过程调用 798 Web访问API 803 命名管道和邮件槽 804 NetBIOS 811 NetBIOS的操作 812 其他的网络API 813 13.3 多重定向器支持 815 多提供者转发器 816 多UNC提供者 818 13.4 名称解析 820 域名系统 820 Windows Internet名称服务 820 13.5 协议驱动程序 821 TCP/IP的扩展 824 13.6 NDIS驱动程序 828 NDIS小端口的变化形式 832 面向连接的NDIS 832 外接NDIS(Remote NDIS) 835 QOS 836 13.7 绑定 838 13.8 分层的网络服务 839 远程访问(Remote Access) 839 活动目录 840 网络负载平衡 841 文件复制服务 843 分布式文件系统 843 13.9 本章总结 844 第14章 崩溃转储分析 845 14.1 Windows为什么会崩溃 845 14.2 蓝屏 846 14.3 崩溃转储文件 849 崩溃转储的生成 852 14.4 Windows错误报告 853 14.5 在线崩溃分析 854 14.6 基本的崩溃转储分析 855 Notmyfault 855 基本的崩溃转储分析 856 详细的分析 858 14.7 使用崩溃诊断工具 860 缓冲区溢出和特殊内存池 861 代码改写和系统代码写保护 863 14.8 高级的崩溃转储分析 864 破坏 865 挂起的系统或无响应的系统 866 当没有崩溃转储时 869 术语表 871 术语对照表 895 索引 901
在深入了解一个系统的原理、实现细节之前,应当先准备好它的源码编译环境、运行环境。如果能在实际环境安装和运行Spark,显然能够提升读者对于Spark的一些感受,对系统能有个大体的印象,有经验的技术人员甚至能够猜出一些Spark采用的编程模型、部署模式等。当你通过一些途径知道了系统的原理之后,难道不会问问自己?这是怎么做到的。如果只是游走于系统使用、原理了解的层面,是永远不可能真正理解整个系统的。很多IDE本身带有调试的功能,每当你阅读源码,陷入重围时,调试能让我们更加理解运行期的系统。如果没有调试功能,不敢想象阅读源码的困难。本章的主要目的是帮助读者构建源码学习环境,主要包括以下内容:在windows环境下搭建源码阅读环境;在Linux搭建基本的执行环境;Spark的基本使用,如spark-shell。 《深入理解SPARK:核心思想与源码分析》结合大量图和示例,对Spark的架构、部署模式和工作模块的设计理念、实现源码与使用技巧进行了深入的剖析与解读。, 《深入理解SPARK:核心思想与源码分析》一书对Spark1.2.0版本的源代码进行了全面而深入的分析,旨在为Spark的优化、定制和扩展提供原理性的指导。阿里巴巴集团专家鼎力推荐 、阿里巴巴资深Java开发和大数据专家撰写。, 本书分为三篇:, 准备篇(第1~2章),介绍了Spark的环境搭建、设计理念与基本架构,帮助读者了解一些背景知识。, 核心设计篇(第3~7章),着重讲解SparkContext的初始化、存储体系、任务提交与执行、计算引擎及部署模式的原理和源码分析。通过这部分的内容,读者可以通过源码剖析更加深入理解Spark的核心设计与实现,以便在实际使用中能够快速解决线上问题并对性能进行调优。, 扩展篇(第8~11章),主要讲解基于Spark核心的各种扩展及应用,包括SQL处理引擎、Hive处理、流式计算框架Spark Streaming、图计算框架GraphX、机器学习库MLlib等内容。通过阅读这部分内容,读者可以扩展实际项目中对Spark的应用场景,让Spark焕发活力。
### 回答1: x64dbg是一款功能强大的开源反汇编器和调试器,它可以帮助程序员调试、反汇编和逆向工程Windows x64平台上的程序。但是,这款工具的英文界面也许会给不熟悉英文的用户带来困惑,因此需要中文使用文档来帮助他们更好地使用。 x64dbg中文使用文档需要包含以下内容: 1. 界面说明: x64dbg的界面相对较为复杂,包括菜单栏、工具栏、快捷键、调试窗口等。需要对每个部分进行解释和说明,让用户了解其作用和功能。 2. 基本功能: x64dbg的基本功能包括反汇编、调试、断点、跟踪、变量监视等。需要详细介绍每个功能的使用方法和注意事项。 3. 插件使用:x64dbg支持插件,插件可以扩展功能,如反混淆、加壳解壳、自动化脚本等。需要介绍常用的插件和如何安装和使用。 4. 常用场景: 需要详细介绍在具体场景下如何使用x64dbg,例如调试崩溃的程序、反汇编目标文件等等。 5. 错误处理:最后需要介绍x64dbg使用过程中可能出现的一些错误和如何处理。 总之,一份具有实际应用场景的、详细全面的x64dbg中文使用文档将使用户更快捷地掌握这款工具,更好地应对实际应用中的需求。 ### 回答2: x64dbg是一款十分优秀的反汇编工具,通过它的使用可以方便地对程序进行调试和分析。由于x64dbg的界面和操作上的设置较复杂,对于初学者而言,了解如何使用x64dbg也许是一件颇具挑战性的事情。然而,很幸运的是,我们可以很方便地找到x64dbg中文使用文档,从而帮助我们更好地掌握这一工具。 在x64dbg中文使用文档中,我们可以轻松地找到该工具的安装、启动、调试等操作的详细说明。此外,文档中还有各种截图和实例,帮助我们更加深入理解和掌握x64dbg的使用方法。在文档中,一些常见的问题也有解答,这对于我们解决一些潜在的x64dbg使用难题也十分有帮助。 总之,x64dbg中文使用文档的出现对于那些打算使用这一工具的初学者而言,可以极大地减少其入门的难度。通过文档的认真学习和实践,我们可以更加熟练地使用x64dbg,并且不断探索一些更深层次的应用。同时,也希望这些文档的贡献者们能够一直坚持下去,不断更新、完善,以帮助更多的人更加深入地理解x64dbg的理论和应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值