第68章 Windows NT

68.1 CRT (Win32)

所有程序都是从main()函数开始执行的吗?事实并非如此。如果用IDA或者HIEW打开可执行文件,我们可以看到原始入口OEP(Original Entry Point)总是指向其他的一段代码。这些代码会在启动程序之前进行一些维护和准备工作。这就是所谓的启动代码/startup-code即CRT代码(C RunTime)。

在通过命令行指令启动程序的时候,main()函数通过外来数组获取启动参数及系统的环境变量。然而,实际传递给程序的不是数组而是参数字符串。CRT代码会根据空格对字符串进行切割。另外,CRT代码还会通过envp数组向main()函数传递系统的环境变量。在Win32的GUI程序里,主函数变为了WinMain(),并且拥有自己的参数传递规格:

int CALLBACK WinMain(
 _In_ HINSTANCE hInstance,
 _In_ HINSTANCE hPrevInstance,
 _In_ LPSTR lpCmdLine,
 _In_ int nCmdShow
);

上述参数同样是由CRT代码准备的。

在程序结束以后,主函数main()会返回其退出代码。这个退出代码会被传递给CRT的 ExitProcess()函数,作为后者的一个参数。

通常来说,不同的编辑器会有不同的CRT代码。

以下列出的是MSVC 2008特有的CRT代码:

  1  ___tmainCRTStartup proc near
  2
  3  var_24 = dword ptr -24h
  4  var_20 = dword ptr -20h
  5  var_1C = dword ptr -1Ch
  6  ms_exc = CPPEH_RECORD ptr -18h
  7
  8        push     14h
  9        push     offset stru_4092D0
 10        call     __SEH_prolog4
 11        mov      eax, 5A4Dh
 12        cmp      ds:400000h, ax
 13        jnz      short loc_401096
 14        mov      eax, ds:40003Ch
 15        cmp      dword ptr [eax+400000h], 4550h
 16        jnz      short loc_401096
 17        mov      ecx, 10Bh
 18        cmp      [eax+400018h], cx
 19        jnz      short loc_401096
 20        cmp      dword ptr [eax+400074h], 0Eh
 21        jbe      short loc_401096
 22        xor      ecx, ecx
 23        cmp      [eax+4000E8h], ecx
 24        setnz    cl
 25        mov      [ebp+var_1C], ecx
 26        jmp      short loc_40109A
 27
 28
 29  loc_401096: ; CODE XREF: ___tmainCRTStartup+18
 30              ; ___tmainCRTStartup+29 ...
 31        and      [ebp+var_1C], 0
 32
 33  loc_40109A: ; CODE XREF: ___tmainCRTStartup+50
 34        push     1
 35        call     __heap_init
 36        pop      ecx
 37        test     eax, eax
 38        jnz      short loc_4010AE
 39        push     1Ch
 40        call     _fast_error_exit
 41        pop      ecx
 42
 43  loc_4010AE: ; CODE XREF: ___tmainCRTStartup+60
 44        call     __mtinit
 45        test     eax, eax
 46        jnz      short loc_4010BF
 47        push     10h
 48        call     _fast_error_exit
 49        pop      ecx
 50
 51  loc_4010BF: ; CODE XREF: ___tmainCRTStartup+71
 52        call     sub_401F2B
 53        and      [ebp+ms_exc.disabled], 0
 54        call     __ioinit
 55        test     eax, eax
 56        jge      short loc_4010D9
 57        push     1Bh
 58        call     __amsg_exit
 59        pop      ecx
 60
 61  loc_4010D9: ; CODE XREF: ___tmainCRTStartup+8B
 62        call     ds:GetCommandLineA
 63        mov      dword_40B7F8, eax
 64        call     ___crtGetEnvironmentStringsA
 65        mov      dword_40AC60, eax
 66        call     __setargv
 67        test     eax, eax
 68        jge      short loc_4010FF
 69        push     8
 70        call     __amsg_exit
 71        pop      ecx
 72
 73  loc_4010FF: ; CODE XREF: ___tmainCRTStartup+B1
 74        call     __setenvp
 75        test     eax, eax
 76        jge      short loc_401110
 77        push     9
 78        call     __amsg_exit
 79        pop      ecx
 80
 81  loc_401110: ; CODE XREF: ___tmainCRTStartup+C2
 82        push     1
 83        call     __cinit
 84        pop      ecx
 85        test     eax, eax
 86        jz       short loc_401123
 87        push     eax
 88        call     __amsg_exit
 89        pop      ecx
 90
 91  loc_401123: ; CODE XREF: ___tmainCRTStartup+D6
 92        mov      eax, envp
 93        mov      dword_40AC80, eax
 94        push     eax             ; envp
 95        push     argv            ; argv
 96        push     argc            ; argc
 97        call     _main
 98        add      esp, 0Ch
 99        mov      [ebp+var_20], eax
100        cmp      [ebp+var_1C], 0
101        jnz      short $LN28
102        push     eax             ; uExitCode
103        call     $LN32
104
105  $LN28:      ; CODE XREF: ___tmainCRTStartup+105
106        call     __cexit
107        jmp      short loc_401186
108
109
110  $LN27:      ; DATA XREF: .rdata:stru_4092D0
111        mov      eax, [ebp+ms_exc.exc_ptr] ; Exception filter 0 for function 401044
112        mov      ecx, [eax]
113        mov      ecx, [ecx]
114        mov      [ebp+var_24], ecx
115        push     eax
116        push     ecx
117        call     __XcptFilter
118        pop      ecx
119        pop      ecx
120
121  $LN24:
122        retn
123
124
125  $LN14:      ; DATA XREF: .rdata:stru_4092D0
126        mov      esp, [ebp+ms_exc.old_esp] ; Exception handler 0 for function 401044
127        mov      eax, [ebp+var_24]
128        mov      [ebp+var_20], eax
129        cmp      [ebp+var_1C], 0
130        jnz      short $LN29
131        push     eax             ; int
132        call     __exit
133
134
135  $LN29:      ; CODE XREF: ___tmainCRTStartup+135
136         call    __c_exit
137
138  loc_401186: ; CODE XREF: ___tmainCRTStartup+112
139        mov      [ebp+ms_exc.disabled], 0FFFFFFFEh
140        mov      eax, [ebp+var_20]
141        call     __SEH_epilog4
142        retn

在程序的第62行、第66行和第74行我们分别可以看到的是GetCommandLineA、setargv()和setenvp()这三个函数,从这三个函数的名称可以看出它们处理的分别是argc、argv和envp这三个全局变量。

最后,第97行的主函数main()会获取这些外部参数。

CRT中的函数名称通常都可以自然解释。例如第35行和第54行的heap_init()和ioinit()这两个函数。

堆的初始化操作是由CRT代码完成的。若在没有CRT代码的情况下调用内存分配函数malloc(),就会引发异常退出,并将看到下述错误代码:

runtime error R6030
- CRT not initialized

在C++程序中,CRT代码还要在启动主函数main()之前初始化全部全局对象。我们可以参考本书的51.4.1节。

主函数main()返回的数值会传递给cexit(),或者是在$LN32,随后会调用函数doexit()。

下面一个问题是:我们有没有可能不采用CRT呢?这是有可能的,前提是您清楚地知道自己在做什么。

我们可以通过MSVC的Linker程序的/ENTRY选项设置程序的入口点。

比如下面的这个程序代码:

#include <windows.h>

int main()
{
        MessageBox (NULL, "hello, world", "caption", MB_OK);
};

选用以下的命令行来编译:

cl no_crt.c user32.lib /link /entry:main

上述指令最终生成一个大小为2560字节的可执行文件。该文件中具备标准的PE文件头,调用MessageBox的指令,其数据段声明了两个字符串,并从库文件user32.dll导入MessageBox函数。整个可执行文件没有其他的内容了。

虽然这个程序确实可以正常运行,但是这种程序无法获取WinMain()函数所需的4个参数。确切地说,程序确实可以启动起来,但是在程序启动的时候外部参数没有被准备或传递过来。

不能直接采用包括4个参数在内的主函数WinMain()的方式,而且不采用main()函数。更加精确一点来说,虽然能传递参数,但是参数不是在程序一执行时就被传递的。

另外,如果通过编译指令限定PE段向更小地址对齐(默认值是4096字节),那么.编译器将会生成尺寸更小的exe文件:

cl no_crt.c user32.lib /link /entry:main /align:16

链接器Linker将会提示:

LINK : warning LNK4108: /ALIGN specified without /DRIVER; image may not run

上述指令将生成一个长度为720字节的exe可执行文件。它可以运行于x86构架的Windows 7系统,但是却不能运行于64位的Windows 7系统(执行的时候,系统会给出错误提示)。从这里我们可以看到,虽然我们可以想办法让可执行文件变得更小一些,但是同时兼容性问题也会越来越突出。

68.2 Win32 PE文件

PE(Portable Executable)格式,是微软Windows环境可移植可执行文件(如exe、dll、vxd、sys和vdm等)的标准文件格式。

与其他格式的PE文件不同的是,exe和sys文件通常只有导入表而没有导出表。

和其他的PE文件一样,DLL文件也有一个原始代码入口点OEP(就是DllMain()函数的地址)。但是DLL的这个函数通常来讲什么也不会做。

sys文件通常来说是一个系统驱动程序。说到驱动程序,Windows操作系统需要在PE文件里保存其校验和,以验证该文件的正确性(Hiew就可以验证这个校验和)。

从Vista开始,所有的Windows驱动程序必须具备数字签名,否则系统会拒绝加载它们。

每个PE文件都由一段打印“This program cannot be run in DOS mode.”的DOS程序块开始。如果在DOS或者Windows 3.1环境下运行这个程序,那么只会看到上述字符串。因为DOS及Windows 3.1 系统不能识别PE格式的文件。

68.2.1 术语
68.2.2 基地址

在开发各自的DLL动态链接库文件时,多数开发团队都有意让其他人直接调用自己的动态链接库。然而,具体到“谁的DLL到底应该加载到哪个地址”这种问题,却没有一种公开的协议或标准。

因此,当同一个进程的两个DLL库具有相同的基地址时,只会有一个DLL被真正加载到基地址上。而另外一个DLL则会分配到进程内存的某段空闲空间里。在调用后者时,每个虚拟地址都会被重新校对。

通常来说,MSVC编译成的可执行程序的基地址都是0x400000,而代码段则从0x401000开始。由此可知,这种程序代码段的相对虚拟地址RVA的首地址都是0x1000。而MSVC通常把DLL的基地址设定为0x10000000。

操纵系统可能会把模块加载到不同的基地址中,还可能是因为程序自身的要求。当程序“点名”启用地址空间分布的随机化(Address Space Layout Randomization,ASLR)技术的时候,操作系统会把其各个模块加载到随机的基地址上。

ASLR是shellcode的应对策略。shellcode都会调用系统函数。

在Windows Vista之前早期系统里,系统的DLL(如kernel32.dll,user32.dll的加载地址是已知的固定地址。在同一个版本的操作系统里,系统DLL里的系统函数地址也几乎一尘不变。也就是说,shellcode可以根据版本信息直接调用系统函数。

为了避免这个问题,地址空间分布的随机化ASLR技术应运而生。它能够将程序以及程序所需模块加载到无法事先确定的随机地址。

在PE文件中,我们通过设置一个标识来实现ASLR。这个标识的名称是:IMAGE_DLL_ CHARACTERISTICS_ DYNAMIC_BASE。

68.2.3 子系统

PE文件有一个子系统字段。这个字段的值通常是下列之一:

68.2.4 操作系统版本

PE文件还指定了可加载它的Windows操作系统最低版本号。如需查阅版本号码和Windows发行名称的完整列表,请参阅:https://en.wikipedia.org/wiki/Windows_NT#Releases

举个例子,MSVC 2005编译的.exe文件只能运行在Windows NT4(版本号为4.00)及以后的操作系统上。但是MSVC 2008编译调应用程序(版本号是5.00)不兼容NT4系统,只能运行于Windows 2000及以后的操作系统。

在MSVC 2012生成的.exe文件里,操作系统版本号的默认值是6.00。这种程序仅面向Windows Vista及后期推出的操作系统。但我们可以编译选项强制编译器生成支持Windows XP的应用程序。详情请参阅https://blogs.msdn.microsoft.com/vcblog/2012/10/08/windows-xp-targeting-with-c-in-visual-studio-2012/

68.2.5 段

所有的可执行文件都可分解为若干段(sections)。段是代码和数据、以及常量和其他数据的组织形式。

PE可执行文件的每个段都可以拥有一个段名称。然而,名称不是段的重要特征。通常来说,代码段的段名称是.text,数据段的段名称是.data,常数段的段名称.rdata(只读数据)。其他类型的常见段名称还有:

经过加密或者压缩处理之后,PE文件section段的段名称通常会被替换或混淆。

此外,开发人员还可以控制MSVC编译器、设定任意段的段名称。有关详情请参阅:https://msdn. microsoft.com/en-us/library/windows/desktop/cc307397.aspx

部分编译器(例如MinGW)和链接器可以在生成的可执行文件中加入带有调试符号、或者其他的调试信息的独立段。然而新近版本的MSVC不再支持这项功能。为了便于专业人员分析和调试应用程序,MSVC推出了全称为“程序数据库”的PDB文件格式。

PE格式的section段的数据结构大体如下:

typedef struct _IMAGE_SECTION_HEADER {
  BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;
  } Misc;
  DWORD VirtualAddress;
  DWORD SizeOfRawData;
  DWORD PointerToRawData;
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD NumberOfRelocations;
  WORD NumberOfLinenumbers;
  DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

上述代码摘自于:https://msdn.microsoft.com/en-us/library/windows/desktop/ms680341 (v=vs.85).aspx

简单的说,上述PointerToRawData(指向原始数据)就是段实体的偏移量Offset,而虚拟地址VirtualAddress就是Hiew里的RVA。

68.2.6 重定向段Relocations(relocs)

至少在Hiew中,它也可以表示为FIXUP-s。

重定位段是自MS-DOS时代起一直存在于可执行文件的实体段,几乎所有的可执行文件都有这个段。

前文介绍过,程序模块可能会被加载到不同的基地址。但是如何处理全局变量等局部共享数据呢?程序必须通过地址指针才能访问这类数据,然而程序又不可能事先知道共享数据的存储地址。为解决这个问题,人们推出了“位置无关代码/PIC”(详情请参阅67.1节)的解决方案。不过PIC用起来并不方便。

于是,人们又推出了基于“重定向表”的地址修正技术。重定向表记录了该文件被加载到不同基地址时需要修正的所有指针。

如果PE文件声明了一个地址为0x410000的全局变量,那么这个变量的寻址指令大致会是:

A1 00 00 41 00        mov        eax,[000410000]

此时,模块的基地址是0x400000(编译器默认值),全局变量的相对虚拟地址RVA是0x10000。

如果这个模块被加载到首地址为0x500000的基地址上,那么这个全局变量的真实地址应当被调整为0x510000。

在上述opcode中,变量地址应当是0xA1(“MOV EAX”指令)之后的那几个字节。为了通知操作系统在重定向时正确处理该地址,PE文件的重定向表必须收录这四个字节的相对地址。

当操作系统的加载器需要把这个模块加载到不同的基地址时,它会逐一枚举重定位表中所有地址,把这些地址所指向的数据当作32位的地址指针,然后减去初始基地址(这样就能获得RVA,也就是相对虚拟地址),并加上新的基地址。

如果模块被加载于其原始的基地址,那么操作系统就不会进行重定向处理。

所有全局变量的处理过程都是如此。

重定向段的数据结构并不唯一。Windows for x86程序的重定向段一般采用IMAGE_REL_BASED_ HIGHLOW的数据结构。

另外,Hiew用暗色区域显示重定向段,如图7.12所示。

而OllyDbg会在内存中用下画线的方式标记重定向段,如图13.11所示。

68.2.7 导出段和导入段

我们都知道,任何可执行文件都必须或多或少地调用由操作系统提供的服务或者DLL动态链接库。

广义地说,由某个模块(一般来说是DLL)声明的所有函数,最终都会被其他模块(.exe文件或者其他DLL)中的某条指令调用,只是调用方式不同而已。

为此,每一个DLL动态链接库文件都有一个“导出/exports“表。导出表声明了该模块定义的函数名称和函数的地址。

同时每个exe文件和DLL文件另有一个“导入”表。这个表声明了执行该模块所需的函数名称、以及相应DLL文件的文件名。

在加载完可执行文件主体的.exe文件之后,操作系统加载器开始处理导入表:它会加载记录在案的DLL文件,接着在DLL的导出表里查找所需函数的函数地址,最后把这些地址写到.exe模块的IAT导入表。

由此可见,操作系统的加载器在进程的加载过程中要比对大量的函数名称。但是检索字符串的效率不会很高。后来人们引入了基于“排行榜”或者“命中率”(对应相关数据结构里的Hints或Ordinal字段)的序号表示办法、把函数名称编排为数字,以此摆脱字符串操作的低效率问题。

因此,DLL文件只需在导出表里标注内部函数的函数“序号”。这显著提升了DLL文件的加载速度 。

举例来说,调用MFC的程序就能够通过函数“序号”调用mfc*.dll动态链接库。而这种程序不再需要导入段的数据表(Import Name Table,INT)中使用字符串存储MFC的函数名称。

当我们在IDA中加载这样的程序后,IDA会询问mfc*.dll文件的路径以获取外部函数的函数名称。如果我们没有指定MFC文件的存储路径,那么函数名称会是mfc80_123之类的字符串。

导入段

编译器通常会给导入表及其相关内容分配一个单独的section段(例如.idata),但这不是一个强制规定。

导入段涉及大量的技术术语,因此理解起来特别困难。本节将通过一个典型的例子进行集中演示。

导入段的主体是数组IMAGE_IMPORT_DESCRIPTOR。它记录着PE文件要导入哪些库文件。

在其元素的数据结构中:

6801{}

图68.1 在PE范畴内与导入段相关的全部数据结构

由加载器解析出来的外部函数地址,至少有两种调用方法:

① 代码可通过call_imp_CreateFileA形式的指令直接调用外部函数。从某种意义上讲,导入函数的函数地址存储到全局变量的存储空间了。考虑到当前模块可能会被加载于与初始值不同的基地址上,那么只要把call指令引用的外部函数目标地址直接追加到reclos重定向表里就好了。

但是要把导入函数的函数地址全部追加到重定向段relocs里,会显著增加重定向表的数据容量。进一步来说,重定向表越大、程序的加载效率就越低。

② 另一种办法就是对每个调用点进行处理。在调用外部函数的时候,只要JMP到“重定向值+外部函数RVA”就可以调用外部函数了。这种调用方式也叫做“形实转换/thunks”。调用外部函数时,程序可以直接CALL 相应的thunk地址。这种调用方式无需进行重定位运算,因为CALL指令本身就能进行相对寻址,因此不必修正RVA地址。

编译器能够分派上述两种调用方法。如果外部函数的调用频率很高,那么链接器Linker很可能会通过thunk调用外部函数。但是在默认情况下,链接器并不创建thunk。

另外,FirstThunk字段里的函数指针数组不必在PE文件的导入地址段(Input Address Table,IAT section)中。笔者曾经编写过一个令.exe文件添加外部函数信息的PE_add_import工具(https://yurichev.com/PE_add_imports.html

)。它就曾经成功的将PE文件引用的外部函数替换为另外一个DLL文件的其他函数。此时生成的指令是:

MOV EAX, [yourdll.dll!function]
JMP EAX

FirstThunk字段存储的是外部函数第一条指令的地址。换而言之,在调用youdll.dll这类自定义动态链接库的时候,加载器把程序代码的相应位置直接替换为外部函数function的函数地址。

需要注意的是代码段一般都是只读的。因此,我们的应用程序将在代码段上增加一个标志IMAGE_ SCN_MEM_WRITE。否则的话,调用期间会出现5号错误(禁止访问)。

可能有读者会问,如果程序只调用一套DLL文件,而且这些DLL文件里所有的函数名称和函数地址保持不变,那么还能否进一步提高进程的加载速度?

答案是:可能的。实现在程序的FirstThunk数组里写入外部函数的函数地址即可。另外需要注意的是IMAGE_IMPORT_DESCRIPTOR结构体里的Timestamp字段。若这个字段有值,则加载器会判断DLL文件的时间戳是否与这个值相等。如果它们相等,那么加载器不做进一步处理,加载速度可能会快些。这就是所谓的“old-style binding(古板的绑定)”。Windows的SDK中有一个名为BIND.EXE的工具可以专门进行这项绑定设置。Matt Pietrek在其发布的《An In-Depth Look into the Win32 Portable Executable File Format》建议,终端用户应当在安装程序之后尽快进行这种时间戳绑定。

PE文件的打包/加密工具也可能会压缩/加密导入表。在这种情况下,Windows的加载器无法加载全部所需的DLL。这时,应由打包程序/加密程序负责加载外部函数。后者一般通过LoadLibrary()和GetProcAddress()来完成这项任务。

在Windows的安装程序的标准动态链接库DLL中,多数文件的导入地址表(Input Address Table,IAT)都位于PE文件的头部。这大概是出于优化的考虑而刻意设计成这样的。当运行一个exe可执行文件时,exe文件并不会被一次性地全部装载进内存(否则大型安装程序的加载速度就快得太离谱了),而是在访问过程中被分部映射到内存里。其目的就是加快exe文件的加载速度。

68.2.8 资源段

位于PE文件资源段里的数据无非就是图表、图形、字符串以及对话框描述等界面信息。这些资源与主程序指令分开存储,大概是为了方便实现多语言的支持:操作系统只需要根据系统的语言设置就可以选取相应的文本或图片。

而这其实也带来了一个副作用:因为PE可执行文件比较容易编辑,不具备PE文件专业知识的人也可以借助工具(ResHack等)直接修改程序资源。相关的介绍可以参见本书68.2.11节。

68.2.9 .NET

.NET的源程序并不会被编译成机器码,而会被编译成一种特殊的字节码。严格地说,这种.exe文件由字节码构成,并不是常规意义上的x86指令代码。但是这种程序的入口点(OEP)确实是一小段x86指令:

jmp           mscoree.dll!\_CorExeMain

.NET格式的PE文件由mscoree.dll处理,它同时是.NET程序的装载器。在Windows XP操作系统之前,.net程序都是通过上述jmp指令交由mscoree.dll处理的。自Windows XP系统起,操作系统的加载器能自动识别.NET格式的文件,即使没有上述JMP指令也可以正常加载.NET程序。有关详情请参阅:

https://msdn.microsoft.com/en-us/library/xh0859k0(v=vs.110).aspx

68.2.10 TLS段

这个段里存储了TLS数据(第65章)的初始化数据(如果需要的话)。当启动一个新的线程时,TLS的数据就是通过本段的数据来初始化的。

除此之外,PE文件规范还约定了“TLS!”的初始化规范,即TLS callbacks/TLS回调函数。如果程序声明了TLS回调函数,那么TLS回调函数会先于OEP执行。这项技术广泛应用于PE文件的压缩和加密程序。

68.2.11 工具
68.2.12 更进一步

68.3 Windows SEH

68.3.1 让我们暂时把MSVC放在一边

Windows操作系统中,结构性例外程序处理机制(Structured Exception Handling,SEH)是用来处理异常情况的响应机制。然而,它是与语言无关的,与C++或者面向对象的编程语言(Oriented Object Programming,OOP)无任何关联。本节将脱离C++以及MSVC的相关特效,单独分析SEH的特性。

每个运行的进程都有一条SEH句柄链,线程信息块(Thread Information Block,TIB)有SHE的最后一个句柄。当出现异外时(比如出现了被零除、地址访问不正确或者程序主动调用RaiseException()函数等情况),操作系统会在线程信息块TIB里寻找SEH的最后一个句柄。并且把出现异常情况时与CPU有关的所有状态(包括寄存器的值等数据)传递给那个SEH句柄。此时异常处理函数开始判断自己能否应对这种异常情况。如果答案是肯定的,那么异常处理函数就会着手接管。如果异常处理函数无法处理这种情况,它就会通知操作系统无法处理它,此后操作系统会逐一尝试异常处理链中的其他处理程序,直到找到能够应对这种情况的异常处理程序为止。

在异常处理链的结尾处有一个大家都接触过的异常处理程序:它显示一个标准的对话框,通知用户进程已经崩溃,崩溃时CPU的状态信息是什么情况,用户是否愿意把这些信息发送给微软的开发人员。

6802

图68.2 Windows XP下的崩溃截图

6803

图68.3 Windows XP下的崩溃细节

6804

图68.4 Windows 7下的崩溃细节

6805

图68.5 Windows 8.1下的崩溃截图

早些时候,这个异常处理程序叫做Dr. Watson。

另外,一些开发人员会在程序里设计自己的异常处理程序,以便收集程序的崩溃信息。这些都是通过系统函数SetUnhandledExceptionFilter()注册的异常处理函数。当操作系统遇到无法应对的异常情况时,它就会调用应用程序自己注册的异常处理函数。Oracle的RDBMS就是十分典型的一个例子:它会在程序崩溃时尽可能地转储CPU以及内存数据。

接下来,我们研究一个初级的异常处理程序。它摘自于https://www.microsoft.com/msj/0197/ Exception/Exception.aspx:

#include <windows.h>
#include <stdio.h>

DWORD new_value=1234;

EXCEPTION_DISPOSITION __cdecl except_handler(
                struct _EXCEPTION_RECORD *ExceptionRecord,
                void * EstablisherFrame,
                struct _CONTEXT *ContextRecord,
                void * DispatcherContext )
{
        unsigned i;
        printf ("%s\n", __FUNCTION__);
        printf ("ExceptionRecord->ExceptionCode=0x%p\n", ExceptionRecord->ExceptionCode);
        printf ("ExceptionRecord->ExceptionFlags=0x%p\n", ExceptionRecord->ExceptionFlags);
        printf ("ExceptionRecord->ExceptionAddress=0x%p\n", ExceptionRecord->ExceptionAddress);

        if (ExceptionRecord->ExceptionCode==0xE1223344)
        {
                printf ("That's for us\n");
                // yes, we "handled" the exception
                return ExceptionContinueExecution;
        }
        else if (ExceptionRecord->ExceptionCode==EXCEPTION_ACCESS_VIOLATION)
        {
                printf ("ContextRecord->Eax=0x%08X\n", ContextRecord->Eax);
                // will it be possible to 'fix' it?
                printf ("Trying to fix wrong pointer address\n");
                ContextRecord->Eax=(DWORD)&new_value;
                // yes, we "handled" the exception
                return ExceptionContinueExecution;
        }
        else
        {
                printf ("We do not handle this\n");
                // someone else's problem
                return ExceptionContinueSearch;
        };
}

int main()
{
        DWORD handler = (DWORD)except_handler; // take a pointer to our handler

        // install exception handler
        __asm
        {                                // make EXCEPTION_REGISTRATION record:
                push    handler          // address of handler function
                push    FS:[0]           // address of previous handler
                mov      FS:[0],ESP      // add new EXECEPTION_REGISTRATION
        }

        RaiseException (0xE1223344, 0, 0, NULL);

        // now do something very bad
        int* ptr=NULL;
        int val=0;
        val=*ptr;
        printf ("val=%d\n", val);

        // deinstall exception handler
        __asm
        {                               // remove our EXECEPTION_REGISTRATION record
                mov     eax,[ESP]       // get pointer to previous record
                mov     FS:[0], EAX     // install previous record
                add     esp, 8          // clean our EXECEPTION_REGISTRATION off stack
        }

        return 0;
}

在Win32环境下,FS:段寄存器里的数据就是线程信息块(Thread Information Block,TIB)的指针。而TIB中的第一个元素正是异常处理指针链里最后一个处理程序的地址。所谓“注册”异常处理程序就是把自定义的异常处理程序的地址链接到这个异常处理指针链里。在注册异常处理程序时,需要使用一种名为_EXCEPTION_REGISTRATION的数据结构。它其实只是一个简单的单向链表,使用栈结构存储各项节点的链数据。

指令清单68.1 MSVC/VC/crt/src/exsup.inc

\_EXCEPTION\_REGISTRATION struc
     prev     dd       ?
     handler  dd       ?
\_EXCEPTION\_REGISTRATION ends

可见,每个节点的“handler”都是一个异常处理程序的启始地址,每个节点的“prev”字段都是上一个节点的地址指针。而最后一个节点的“prev”字段的值为0xFFFFFFFF(-1)。

6805-2{}

在注册好我们自定义的异常处理程序以后,我们调用RaiseException()函数、触发用户异常的处理过程。异常处理程序首先检查异常代码,如果异常代码是0xE1223344,它就返回ExceptionContinueExecution。这个返回值代表“已经纠正CPU的状态”(通常通过调整EIP/ESP寄存器实现)、“操作系统可以继续执行后续指令”。如果把异常代码修改为其他值,那么处理函数的返回值则会变为ExceptionContinueSearch。顾名思义, 操作系统就会逐一尝试其他的异常处理程序—万一没有找到有关问题(并不仅仅是错误代码)的异常处理程序,我们就会看到标准的Windows进程崩溃对话框。

系统异常(system exceptions)和用户异常(user exceptions)之间的区别是什么?系统异常的有关信息是:

由WinBase.h定义的异常状态

在ntstatus.h里的相应状态

错误编号

EXCEPTION_ACCESS_VIOLATION

STATUS_ACCESS_VIOLATION

0xC0000005

EXCEPTION_DATATYPE_MISALIGNMENT

STATUS_DATATYPE_
MISALIGNMENT0x80000002

EXCEPTION_BREAKPOINTSTATUS_
BREAKPOINT0x80000003

EXCEPTION_SINGLE_STEP

STATUS_SINGLE_STEP

0x80000004

EXCEPTION_ARRAY_BOUNDS_EXCEEDED

STATUS_ARRAY_BOUNDS_EXCEEDED

0xC000008C

EXCEPTION_FLT_DENORMAL_OPERAND

STATUS_FLOAT_DENORMAL_OPERAND

0xC000008D

EXCEPTION_FLT_DIVIDE_BY_ZERO

STATUS_FLOAT_DIVIDE_BY_ZERO

0xC000008E

EXCEPTION_FLT_INEXACT_RESULT

STATUS_FLOAT_INEXACT_RESULT

0xC000008F

EXCEPTION_FLT_INVALID_OPERATION

STATUS_FLOAT_INVALID_OPERATION

0xC0000090

EXCEPTION_FLT_OVERFLOW

STATUS_FLOAT_OVERFLOW

0xC0000091

EXCEPTION_FLT_STACK_CHECK

STATUS_FLOAT_STACK_CHECK

0xC0000092

EXCEPTION_FLT_UNDERFLOW

STATUS_FLOAT_UNDERFLOW

0xC0000093

EXCEPTION_INT_DIVIDE_BY_ZERO

STATUS_INTEGER_DIVIDE_BY_ZERO

0xC0000094

EXCEPTION_INT_OVERFLOW

STATUS_INTEGER_OVERFLOW

0xC0000095

EXCEPTION_PRIV_INSTRUCTION

STATUS_PRIVILEGED_INSTRUCTION

0xC0000096

EXCEPTION_IN_PAGE_ERROR

STATUS_IN_PAGE_ERROR

0xC0000006

EXCEPTION_ILLEGAL_INSTRUCTION

STATUS_ILLEGAL_INSTRUCTION

0xC000001D

EXCEPTION_NONCONTINUABLE_EXCEPTION

STATUS_NONCONTINUABLE_EXCEPTION

0xC0000025

EXCEPTION_STACK_OVERFLOW

STATUS_STACK_OVERFLOW

0xC00000FD

EXCEPTION_INVALID_DISPOSITION

STATUS_INVALID_DISPOSITION

0xC0000026

EXCEPTION_GUARD_PAGE

STATUS_GUARD_PAGE_VIOLATION

0x80000001

EXCEPTION_INVALID_HANDLE

STATUS_INVALID_HANDLE

0xC0000008

EXCEPTION_POSSIBLE_DEADLOCK

STATUS_POSSIBLE_DEADLOCK

0xC0000194

CONTROL_C_EXIT

STATUS_CONTROL_C_EXIT

0xC000013A

32位错误代码的具体含义,如下图所示。

图片 1{}

S是最高的两位(第30、31位),为基本的状态代码,一共有4种组合,分别是11、10、01以及00。它们分别代码的意义是:11代表错误,10代表警告,01代表信息,00代表成功。

U是第29位(第29位),它只有0和1两种状态,代表该异常是否属于用户侧异常。

上面我们提到的一个返回值是0xE1223344。最高的4位是0xE(1110)。这几个比特位代表:①这是属于用户态异常;②这是一个错误信息。实事求是地讲,本例这个程序与这些最高位的值没有关系;而考究的异常处理程序应当能够充分利用错误代码的所有信息。

接着,程序试图读取地址为0的内存数据。这个地址实际就是NULL指针指向的地址。访问这个地址必将导致系统错误,因为根据ISO C标准这个地址不应当存放任何数据(硬性规定)。此时操作系统会优先调用程序自己注册的异常处理程序。后者会判断错误代码是否为EXCEPTION_ACCESS_ VIOLATION,从而得知该异常是否是自己可以处理的问题。

读取地址为0的指令大致如下:

指令清单68.2 MSVC2010

        ...
        xor     eax, eax
        mov     eax, DWORD PTR [eax] ; exception will occur here
        push    eax
        push    OFFSET msg
        call    _printf
        add     esp, 8
        ...

我们的程序是不是可以实时处理这个错误以使得程序能继续执行呢?答案是肯定的。我们的异常处理程序能够修正EAX寄存器的值,让操作系统继续执行下去。这就是自定义异常处理程序的功能。字符串显示函数printf显示的数值是1234,因为执行了我们的异常处理函数之后,EAX的数值不再是0了,而是全局变量new_value的值。因此执行流程就得到了恢复。

以下就是程序执行的步骤:首先内存管理器检测出由中央处理器CPU发出的错误信息,接着CPU将此进程挂起,并在Windows的内核中检索异常处理程序的句柄。然后依次调用SEH链的handler。

本例是由MSVC 2010编译的程序。当然,我们并不能保证其他的编译程序同样会使用EAX寄存器存放该指针。

它所演示的地址替换技巧非常的精妙。我经常使用这种技术演示SEH的内部构造。不过,我还不曾使用这种技巧实时修复异常错误。

为什么SEH相关的记录存储于栈,而不是其他的地方?据说,如此一来操作系统就不需要关心这类数据的释放操作,毕竟函数结束以后这些数据都会被自动释放。但是,笔者也不难100%保证这种假说的正确性。这有点像本书5.2.4节讲到的 alloca()函数。

68.3.2 让我们重新回到MSVC

据说,C++语言开发环境已经能够稳妥的处理各种异常情况,只有C语言的开发人员才需要关注代码的异常处理机制。所以微软推出了一个面向MSVC的非标准C扩展。这个扩展组件不适用于C++程序。

有关详情请参阅:https://msdn.microsoft.com/en-us/library/swezty51.aspx

__try
{
    ...
}
__except(filter code)
{
    handler code
}

除了“try-except”语句之外,MSVC还支持“try-finally”语句:

__try
{
    ...
}
__finally
{
    ...
}

前者中的“filter code”是一个用来判断是否执行“handler code(响应指令)”的表达式。如果代码太长、无法表示为一个表达式,那么就得借助于独立的过滤函数。

Windows内核就大量使用这种SEH结构。以WRK(Windows Research Kernel)的某段指令为例:

指令清单68.3 WRK-v1.2/base/ntos/ob/obwait.c

try {

    KeReleaseMutant( (PKMUTANT)SignalObject,
                     MUTANT_INCREMENT,
                     FALSE,
                     TRUE );

} except((GetExceptionCode () == STATUS_ABANDONED ||
          GetExceptionCode () == STATUS_MUTANT_NOT_OWNED)?
             EXCEPTION_EXECUTE_HANDLER :
             EXCEPTION_CONTINUE_SEARCH) {
    Status = GetExceptionCode();

    goto WaitExit;
}

指令清单68.4 WRK-v1.2/base/ntos/cache/cachesub.c

try {

    RtlCopyBytes( (PVOID)((PCHAR)CacheBuffer + PageOffset),
                  UserBuffer,
                  MorePages ?
                    (PAGE_SIZE - PageOffset) :
                    (ReceivedLength - PageOffset) );

} except( CcCopyReadExceptionFilter( GetExceptionInformation(),
                                                            &Status ) ) {

下面也是一组过滤代码。

指令清单68.5 WRK-v1.2/base/ntos/cache/copysup.c

LONG
CcCopyReadExceptionFilter(
    IN PEXCEPTION_POINTERS ExceptionPointer,
    IN PNTSTATUS ExceptionCode
    )

/*++

Routine Description:

    This routine serves as a exception filter and has the special job of
    extracting the "real" I/O error when Mm raises STATUS_IN_PAGE_ERROR
    beneath us.

Arguments:

    ExceptionPointer - A pointer to the exception record that contains
                       the real Io Status.

    ExceptionCode - A pointer to an NTSTATUS that is to receive the real
                    status.
Return Value:

    EXCEPTION_EXECUTE_HANDLER

--*/

{
    *ExceptionCode = ExceptionPointer->ExceptionRecord->ExceptionCode;

    if ( (*ExceptionCode == STATUS_IN_PAGE_ERROR) &&
         (ExceptionPointer->ExceptionRecord->NumberParameters >= 3) ) {

        *ExceptionCode = (NTSTATUS) ExceptionPointer->ExceptionRecord->ExceptionInformation[2];
    }

    ASSERT( !NT_SUCCESS(*ExceptionCode) );

    return EXCEPTION_EXECUTE_HANDLER;
}

从内部来讲,SEH是由操作系统支持的异常处理扩展。但是异常处理函数属于_except_handler3 (SEH3)或_except_handler4(SEH4)。而且响应代码依赖于MSVC编译器。它需要由MSVC的库文件或者msvcr*.dll的动态链接库提供支持。SEH是由MSVC提供的一种机制,这一点至关重要。其他Win32的编译器的异常响应机制可能与SEH完全不同。

SEH3

SEH3定义了一个_except_handler3的异常处理函数,而且还对_EXCEPTION_REGISTRATION表进行了扩充、添加了scope table和previous try level的指针。在此基础上,SEH4对scope table表添加了四个值,以实现缓冲溢出保护。

scope table是一个表,它的元素都由filter表达式和handler code块的指针构成。这个表可以正确处理带有嵌套关系的多级try/except语句。

本书再次强调,操作系统只关心SEH里各节点的prev字段和handle字段,从不关心任何其他数据。函数_except_handler3的作用是读取其他的字段以及scope表的数值,并且判断什么时间执行哪个异常处理函数。

6805-3{}

函数_except_handler3的源代码并不公开。然而,Sanos操作系统(与Win32系统部分兼容),再现了这个函数。在某种程度上说,由Sanos实现的_except_handler3与Windows系统的同名函数十分相似(请参阅其源文件/src/win32/msvcrt/except.c)。除此以外,Wine平台和ReactOS系统也开发了类似的函数。

如果filter指针是空指针NULL,那么handler指针将指向“finally”所在的代码块。

在执行过程中,堆栈中的previous try level改变了,因此函数_except_handler3能从目前的嵌套中获取信息,目的是知道使用哪个scope table表。

执行期间,栈中的previous try level字段将会发生变化。_except_handler3根据这项数据获取当前嵌套级的信息,进而判断要使用scope table中的哪个表项。每一个try块都分配了一个唯一的数作为标识,scopetable表中条目(entry)间的关系则描述了try块的嵌套关系。

SEH3: 一个try/except块的例子

#include <stdio.h>
#include <windows.h>
#include <excpt.h>

int main()
{
    int* p = NULL;
    __try
    {
        printf("hello #1!\n");
        *p = 13; // causes an access violation exception;
        printf("hello #2!\n");
    }
    __except(GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION ?
            EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
    {
        printf("access violation, can't recover\n");
    }
}

指令清单68.6 MSVC 2003

$SG74605 DB     'hello #1!', 0aH, 00H
$SG74606 DB     'hello #2!', 0aH, 00H
$SG74608 DB     'access violation, can''t recover', 0aH, 00H
_DATA    ENDS

; scope table:
CONST      SEGMENT
$T74622    DD    0ffffffffH   ; previous try level
           DD    FLAT:$L74617 ; filter
           DD    FLAT:$L74618 ; handler

CONST     ENDS
_TEXT     SEGMENT
$T74621 = -32 ; size = 4
_p$ = -28     ; size = 4
__$SEHRec$ = -24 ; size = 24
_main     PROC NEAR
    push    ebp
    mov     ebp, esp
    push    -1                             ; previous try level
    push    OFFSET FLAT:$T74622            ; scope table
    push    OFFSET FLAT:__except_handler3  ; handler
    mov     eax, DWORD PTR fs:__except_list
    push    eax                            ; prev
    mov     DWORD PTR fs:__except_list, esp
    add     esp, -16
; 3 registers to be saved:
    push    ebx
    push    esi
    push    edi
    mov     DWORD PTR __$SEHRec$[ebp], esp
    mov     DWORD PTR _p$[ebp], 0
    mov     DWORD PTR __$SEHRec$[ebp+20], 0 ; previous try level
    push    OFFSET FLAT:$SG74605 ; 'hello #1!'
    call    _printf
    add     esp, 4
    mov     eax, DWORD PTR _p$[ebp]
    mov     DWORD PTR [eax], 13
    push    OFFSET FLAT:$SG74606 ; 'hello #2!'
    call    _printf
    add     esp, 4
    mov     DWORD PTR __$SEHRec$[ebp+20], -1 ; previous try level
    jmp     SHORT $L74616

    ; filter code:
$L74617:
$L74627:
    mov     ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov     edx, DWORD PTR [ecx]
    mov     eax, DWORD PTR [edx]
    mov     DWORD PTR $T74621[ebp], eax
    mov     eax, DWORD PTR $T74621[ebp]
    sub     eax, -1073741819; c0000005H
    neg     eax
    sbb     eax, eax
    inc     eax
$L74619:
$L74626:
    ret     0

    ; handler code:
$L74618:
    mov     esp, DWORD PTR __$SEHRec$[ebp]
    push    OFFSET FLAT:$SG74608 ; 'access violation, can''t recover'
    call    _printf
    add     esp, 4
    mov     DWORD PTR __$SEHRec$[ebp+20], -1 ; setting previous try level back to -1
$L74616:
    xor     eax, eax
    mov     ecx, DWORD PTR __$SEHRec$[ebp+8]
    mov     DWORD PTR fs:__except_list, ecx
    pop     edi
    pop     esi
    pop     ebx
    mov     esp, ebp
    pop     ebp
    ret     0
_main     ENDP
_TEXT     ENDS
END

由此可见,SEH在栈里形成了帧结构。而Scope table则是位于文件的CONST段,确实如此,scope table各字段的值确实不会发生变化。值得关注的是previous try level字段的变化过程。其初始值是0xFFFFFFF(-1)。当执行到try语句时,专有一条指令把它赋值为0。而当try语句的主体关闭时,它又被赋值为−1。我们也看到了filter以及handler code的地址。因此,我们能很容易地分析出函数中的try-except语句。

函数序言中的SEH初始化代码可能会被多个函数共享,有时候编译器会在函数序言直接调用SEH_prolog()函数,再在函数尾声处调用SEH_epilog()函数以回收栈空间。

下面我们在tracer跟踪器中运行这个例子:

tracer.exe -l:2.exe --dump-seh

上述指令的输出如下。

指令清单68.7 tracer.exe的输出

EXCEPTION_ACCESS_VIOLATION at 2.exe!main+0x44 (0x401054) ExceptionInformation[0]=1
EAX=0x00000000 EBX=0x7efde000 ECX=0x0040cbc8 EDX=0x0008e3c8
ESI=0x00001db1 EDI=0x00000000 EBP=0x0018feac ESP=0x0018fe80
EIP=0x00401054
FLAGS=AF IF RF
* SEH frame at 0x18fe9c prev=0x18ff78 handler=0x401204 (2.exe!_except_handler3)
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=-1, filter=0x401070  (2.exe!main+0x60) handler=0x401088 ↙ 
    ↘ (2.exe!main+0x78)
* SEH frame at 0x18ff78 prev=0x18ffc4 handler=0x401204 (2.exe!_except_handler3)
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=-1,  filter=0x401531 (2.exe!mainCRTStartup+0x18d) ↙
    ↘ handler=0x401545 (2.exe!mainCRTStartup+0x1a1)
* SEH frame at 0x18ffc4 prev=0x18ffe4 handler=0x771f71f5 (ntdll.dll!__except_handler4)
SEH4 frame. previous trylevel=0
SEH4 header:     GSCookieOffset=0xfffffffe GSCookieXOROffset=0x0
                EHCookieOffset=0xffffffcc EHCookieXOROffset=0x0
scopetable entry[0]. previous try level=-2, filter=0x771f74d0 (ntdll.dll! ↙
    ↘ ___safe_se_handler_table+0x20) handler=0x771f90eb (ntdll.dll!_TppTerminateProcess@4+0x43)
* SEH frame at 0x18ffe4 prev=0xffffffff handler=0x77247428 (ntdll.dll!_FinalExceptionHandler@16)

我们可以看到SEH链含有4个处理函数/handler。

前两个handler是由源代码指定注册的异常处理函数。虽然我们的源代码只定义了一个handler,但是CRT的_mainCRTStartup()函数会自动设置一个配套的handler。后者的功能不多,至少能够处理一些与FPU有关的异常情况。有关源码可以参阅MSVC安装目录里的crt/src/winxfltr.c文件。

第三个handler是由ntdll.dll提供的SEH4,第四个handler也位于ntdll.dll,跟MSVC没什么关系,它的函数名是FinalExceptionHandler。

上述信息表明,SEH链含有三种类型的处理函数:一种是与MSVC彻底无关的自定义handler(即异常处理指针链中的最后一项),另外两种处理程序是由MSVC提供的SEH3和SEH4函数。

SEH3:两个try/except模块例子

#include <stdio.h>
#include <windows.h>
#include <excpt.h>

int filter_user_exceptions (unsigned int code, struct _EXCEPTION_POINTERS *ep)
{
    printf("in filter. code=0x%08X\n", code);
    if (code == 0x112233)
    {
        printf("yes, that is our exception\n");
        return EXCEPTION_EXECUTE_HANDLER;
    }
    else
    {
        printf("not our exception\n");
        return EXCEPTION_CONTINUE_SEARCH;
    };
}
int main()
{
    int* p = NULL;
    __try
    {
        __try
        {
            printf ("hello!\n");
            RaiseException (0x112233, 0, 0, NULL);
            printf ("0x112233 raised. now let's crash\n");
            *p = 13; // causes an access violation exception;
        }
        __except(GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION ?
                EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
        {
            printf("access violation, can't recover\n");
        }
    }
    __except(filter_user_exceptions(GetExceptionCode(), GetExceptionInformation()))
    {
        // the filter_user_exceptions() function answering to the question
        // "is this exception belongs to this block?"
        // if yes, do the follow:
        printf("user exception caught\n");
    }
}

这里,我们可以看到两个try块。因此scope table会有两个元素,分别存储着各try块的相应指针。“Previous try level”字段的值会伴随着进入/退出try语句块而发生相应改变。

指令清单68.8 MSVC 2003

$SG74606 DB     'in filter. code=0x%08X', 0aH, 00H
$SG74608 DB     'yes, that is our exception', 0aH, 00H
$SG74610 DB     'not our exception', 0aH, 00H
$SG74617 DB     'hello!', 0aH, 00H
$SG74619 DB     '0x112233 raised. now let''s crash', 0aH, 00H
$SG74621 DB     'access violation, can''t recover', 0aH, 00H
$SG74623 DB     'user exception caught', 0aH, 00H

_code$ = 8   ; size = 4
_ep$ = 12    ; size = 4
_filter_user_exceptions PROC NEAR
    push   ebp
    mov    ebp, esp
    mov    eax, DWORD PTR _code$[ebp]
    push   eax
    push   OFFSET FLAT:$SG74606 ; 'in filter. code=0x%08X'
    call   _printf
    add    esp, 8
    cmp    DWORD PTR _code$[ebp], 1122867; 00112233H
    jne    SHORT $L74607
    push   OFFSET FLAT:$SG74608 ; 'yes, that is our exception'
    call   _printf
    add    esp, 4
    mov    eax, 1
    jmp    SHORT $L74605
$L74607:
    push   OFFSET FLAT:$SG74610 ; 'not our exception'
    call   _printf
    add    esp, 4
    xor    eax, eax
$L74605:
    pop    ebp
    ret    0
_filter_user_exceptions ENDP

; scope table:
CONST    SEGMENT
$T74644   DD    0ffffffffH   ; previous try level for outer block
          DD    FLAT:$L74634 ; outer block filter
          DD    FLAT:$L74635 ; outer block handler
          DD    00H          ; previous try level for inner block
          DD    FLAT:$L74638 ; inner block filter
          DD    FLAT:$L74639 ; inner block handler
CONST    ENDS

$T74643 = -36     ; size = 4
$T74642 = -32     ; size = 4
_p$ = -28         ; size = 4
__$SEHRec$ = -24  ; size = 24
_main    PROC NEAR
    push   ebp
    mov    ebp, esp
    push   -1 ; previous try level
    push   OFFSET FLAT:$T74644
    push   OFFSET FLAT:__except_handler3
    mov    eax, DWORD PTR fs:__except_list
    push   eax
    mov    DWORD PTR fs:__except_list, esp
    add    esp, -20
    push   ebx
    push   esi
    push   edi
    mov    DWORD PTR __$SEHRec$[ebp], esp
    mov    DWORD PTR _p$[ebp], 0
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; outer try block entered. set previous try level to 0
    mov    DWORD PTR __$SEHRec$[ebp+20], 1 ; inner try block entered. set previous try level to 1
    push   OFFSET FLAT:$SG74617 ; 'hello!'
    call   _printf
    add    esp, 4
    push   0
    push   0
    push   0
    push   1122867 ; 00112233H
    call   DWORD PTR __imp__RaiseException@16
    push   OFFSET FLAT:$SG74619 ; '0x112233 raised. now let''s crash'
    call   _printf
    add    esp, 4
    mov    eax, DWORD PTR _p$[ebp]
    mov    DWORD PTR [eax], 13
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; inner try block exited. set previous try level back to 0
    jmp    SHORT $L74615

; inner block filter:
$L74638:
$L74650:
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov    edx, DWORD PTR [ecx]
    mov    eax, DWORD PTR [edx]
    mov    DWORD PTR $T74643[ebp], eax
    mov    eax, DWORD PTR $T74643[ebp]
    sub    eax, -1073741819; c0000005H
    neg    eax
    sbb    eax, eax
    inc    eax
$L74640:
$L74648:
    ret    0

; inner block handler:
$L74639:
    mov    esp, DWORD PTR __$SEHRec$[ebp]
    push   OFFSET FLAT:$SG74621 ; 'access violation, can''t recover'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; inner try block exited. set previous try level back to 0

$L74615:
    mov    DWORD PTR __$SEHRec$[ebp+20], -1 ; outer try block exited, set previous try level back to -1
    jmp    SHORT $L74633

; outer block filter:
$L74634:
$L74651:
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov    edx, DWORD PTR [ecx]
    mov    eax, DWORD PTR [edx]
    mov    DWORD PTR $T74642[ebp], eax
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    push   ecx
    mov    edx, DWORD PTR $T74642[ebp]
    push   edx
    call   _filter_user_exceptions
    add    esp, 8
$L74636:
$L74649:
    ret    0

; outer block handler:
$L74635:
    mov    esp, DWORD PTR __$SEHRec$[ebp]
    push   OFFSET FLAT:$SG74623 ; 'user exception caught'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], -1 ; both try blocks exited. set previous try level back to -1
$L74633:
    xor    eax, eax
    mov    ecx, DWORD PTR __$SEHRec$[ebp+8]
    mov    DWORD PTR fs:__except_list, ecx
    pop    edi
    pop    esi
    pop    ebx
    mov    esp, ebp
    pop    ebp
    ret    0
_main     ENDP

只要在handler中调用printf()函数的指令那里设置一个断点,就可以观测到SEH handler的添加过程。 或许SEH的内部处理机制与众不同。而这里我们可以看到scope table 包含着两个元素:

tracer.exe -l:3.exe bpx=3.exe!printf --dump-seh

指令清单68.9 tracer.exe输出

(0) 3.exe!printf
EAX=0x0000001b EBX=0x00000000 ECX=0x0040cc58 EDX=0x0008e3c8
ESI=0x00000000 EDI=0x00000000 EBP=0x0018f840 ESP=0x0018f838
EIP=0x004011b6
FLAGS=PF ZF IF
* SEH frame at 0x18f88c prev=0x18fe9c handler=0x771db4ad (ntdll.dll!ExecuteHandler2@20+0x3a)
* SEH frame at 0x18fe9c prev=0x18ff78 handler=0x4012e0 (3.exe!_except_handler3)
SEH3 frame. previous trylevel=1
scopetable entry[0]. previous try level=-1, filter=0x401120 (3.exe!main+0xb0) handler=0x40113b ↙
    ↘ (3.exe!main+0xcb)
scopetable entry[1]. previous try level=0, filter=0x4010e8 (3.exe!main+0x78) handler=0x401100 ↙
    ↘ (3.exe!main+0x90)
* SEH frame at 0x18ff78 prev=0x18ffc4 handler=0x4012e0 (3.exe!_except_handler3)
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=-1, filter=0x40160d (3.exe!mainCRTStartup+0x18d) ↙
    ↘ handler=0x401621 (3.exe!mainCRTStartup+0x1a1)
* SEH frame at 0x18ffc4 prev=0x18ffe4 handler=0x771f71f5 (ntdll.dll!__except_handler4)
SEH4 frame. previous trylevel=0
SEH4 header:    GSCookieOffset=0xfffffffe GSCookieXOROffset=0x0
                EHCookieOffset=0xffffffcc EHCookieXOROffset=0x0
scopetable entry[0]. previous try level=-2, filter=0x771f74d0 (ntdll.dll! ↙
    ↘ ___safe_se_handler_table+0x20) handler=0x771f90eb (ntdll.dll!_TppTerminateProcess@4+0x43)
* SEH frame at 0x18ffe4 prev=0xffffffff handler=0x77247428 (ntdll.dll!_FinalExceptionHandler@16)

SEH4

在遭受缓冲区溢出攻击(请参见本书第18章第2节)以后,地址表scope table的地址可能被重写。MSVC 2005编译器为SEH帧增加了一些缓冲区溢出保护,把SEH3升级成了SEH4。SEH4的scope table表的指针会与security cookie进行异或运算,然后才被写到相应的数据结构里。此外Scope table新增了一个双指针表头,这两个EH cookie指针都是security cookies的指针(GS cookies只有在编译时打开/GS参数才会出现)。EH Cookie的偏移量(offset)都是基于栈帧(EBP)的相对地址,其与security_cookie的异或运算结果会被保存在栈里,充当校验码。异常处理函数的加载过程会读取这个值并检查其正确性。

由于栈内的security cookie是一次性随机值,因此远程攻击者是无法事先预测这个值。

在SEH4中最外层的级别(previous try level)是-2,而不是SEH3的-1。

6805-4{}

这里列出了两个由MSVC 2012编译的SEH4函数。

指令清单68.10 MSVC 2012:一个try块的例子

$SG85485 DB     'hello #1!', 0aH, 00H
$SG85486 DB     'hello #2!', 0aH, 00H
$SG85488 DB     'access violation, can''t recover', 0aH, 00H

; scope table:
xdata$x          SEGMENT
__sehtable$_main DD 0fffffffeH   ; GS Cookie Offset
    DD           00H             ; GS Cookie XOR Offset
    DD           0ffffffccH      ; EH Cookie Offset
    DD           00H             ; EH Cookie XOR Offset
    DD           0fffffffeH      ; previous try level
    DD           FLAT:$LN12@main ; filter
    DD           FLAT:$LN8@main  ; handler
xdata$x          ENDS

$T2 = -36       ; size = 4
_p$ = -32       ; size = 4
tv68 = -28      ; size = 4
__$SEHRec$ = -24 ; size = 24
_main     PROC
    push   ebp
    mov    ebp, esp
    push   -2
    push   OFFSET __sehtable$_main
    push   OFFSET __except_handler4
    mov    eax, DWORD PTR fs:0
    push   eax
    add    esp, -20
    push   ebx
    push   esi
    push   edi
    mov    eax, DWORD PTR ___security_cookie
    xor    DWORD PTR __$SEHRec$[ebp+16], eax ; xored pointer to scope table
    xor    eax, ebp
    push   eax                               ; ebp ^ security_cookie
    lea    eax, DWORD PTR __$SEHRec$[ebp+8] ; pointer to VC_EXCEPTION_REGISTRATION_RECORD
    mov    DWORD PTR fs:0, eax
    mov    DWORD PTR __$SEHRec$[ebp], esp
    mov    DWORD PTR _p$[ebp], 0
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; previous try level
    push   OFFSET $SG85485 ; 'hello #1!'
    call   _printf
    add    esp, 4
    mov    eax, DWORD PTR _p$[ebp]
    mov    DWORD PTR [eax], 13
    push   OFFSET $SG85486 ; 'hello #2!'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], -2 ; previous try level
    jmp    SHORT $LN6@main

; filter:
$LN7@main:
$LN12@main:
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov    edx, DWORD PTR [ecx]
    mov    eax, DWORD PTR [edx]
    mov    DWORD PTR $T2[ebp], eax
    cmp    DWORD PTR $T2[ebp], -1073741819 ; c0000005H
    jne    SHORT $LN4@main
    mov    DWORD PTR tv68[ebp], 1
    jmp    SHORT $LN5@main
$LN4@main:
    mov    DWORD PTR tv68[ebp], 0
$LN5@main:
    mov    eax, DWORD PTR tv68[ebp]
$LN9@main:
$LN11@main:
    ret    0

; handler:
$LN8@main:
    mov    esp, DWORD PTR __$SEHRec$[ebp]
    push   OFFSET $SG85488 ; 'access violation, can''t recover'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], -2 ; previous try level
$LN6@main:
    xor    eax, eax
    mov    ecx, DWORD PTR __$SEHRec$[ebp+8]
    mov    DWORD PTR fs:0, ecx
    pop    ecx
    pop    edi
    pop    esi
    pop    ebx
    mov    esp, ebp
    pop    ebp
    ret    0
_main     ENDP

指令清单68.11 MSVC 2012:两个try块的例子

$SG85486 DB     'in filter. code=0x%08X', 0aH, 00H
$SG85488 DB     'yes, that is our exception', 0aH, 00H
$SG85490 DB     'not our exception', 0aH, 00H
$SG85497 DB     'hello!', 0aH, 00H
$SG85499 DB     '0x112233 raised. now let''s crash', 0aH, 00H
$SG85501 DB     'access violation, can''t recover', 0aH, 00H
$SG85503 DB     'user exception caught', 0aH, 00H

xdata$x     SEGMENT
__sehtable$_main DD 0fffffffeH             ; GS Cookie Offset
                 DD     00H                ; GS Cookie XOR Offset
                 DD     0ffffffc8H         ; EH Cookie Offset
                 DD     00H                ; EH Cookie Offset
                 DD     0fffffffeH         ; previous try level for outer block
                 DD     FLAT:$LN19@main    ; outer block filter
                 DD     FLAT:$LN9@main     ; outer block handler
                 DD     00H                ; previous try level for inner block
                 DD     FLAT:$LN18@main    ; inner block filter
                 DD     FLAT:$LN13@main    ; inner block handler
xdata$x ENDS

$T2 = -40       ; size = 4
$T3 = -36       ; size = 4
_p$ = -32       ; size = 4
tv72 = -28      ; size = 4
__$SEHRec$ = -24 ; size = 24
_main     PROC
    push   ebp
    mov    ebp, esp
    push   -2 ; initial previous try level
    push   OFFSET __sehtable$_main
    push   OFFSET __except_handler4
    mov    eax, DWORD PTR fs:0
    push   eax ; prev
    add    esp, -24
    push   ebx
    push   esi
    push   edi
    mov    eax, DWORD PTR ___security_cookie
    xor    DWORD PTR __$SEHRec$[ebp+16], eax     ; xored pointer to scope table
    xor    eax, ebp                              ; ebp ^ security_cookie
    push   eax
    lea    eax, DWORD PTR __$SEHRec$[ebp+8]      ; pointer to VC_EXCEPTION_REGISTRATION_RECORD
    mov    DWORD PTR fs:0, eax
    mov    DWORD PTR __$SEHRec$[ebp], esp
    mov    DWORD PTR _p$[ebp], 0
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; entering outer try block, setting previous try level=0
    mov    DWORD PTR __$SEHRec$[ebp+20], 1 ; entering inner try block, setting previous try level=1
    push   OFFSET $SG85497 ; 'hello!'
    call   _printf
    add    esp, 4
    push   0
    push   0
    push   0
    push   1122867 ; 00112233H
    call   DWORD PTR __imp__RaiseException@16
    push   OFFSET $SG85499 ; '0x112233 raised. now let''s crash'
    call   _printf
    add    esp, 4
    mov    eax, DWORD PTR _p$[ebp]
    mov    DWORD PTR [eax], 13
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; exiting inner try block, set previous try level back to 0
    jmp    SHORT $LN2@main

; inner block filter:
$LN12@main:
$LN18@main:
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov    edx, DWORD PTR [ecx]
    mov    eax, DWORD PTR [edx]
    mov    DWORD PTR $T3[ebp], eax
    cmp    DWORD PTR $T3[ebp], -1073741819 ; c0000005H
    jne    SHORT $LN5@main
    mov    DWORD PTR tv72[ebp], 1
    jmp    SHORT $LN6@main
$LN5@main:
    mov    DWORD PTR tv72[ebp], 0
$LN6@main:
    mov    eax, DWORD PTR tv72[ebp]
$LN14@main:
$LN16@main:
    ret    0

; inner block handler:
$LN13@main:
    mov    esp, DWORD PTR __$SEHRec$[ebp]
    push   OFFSET $SG85501 ; 'access violation, can''t recover'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; exiting inner try block, setting previous try level back to 0
$LN2@main:
    mov    DWORD PTR __$SEHRec$[ebp+20], -2 ; exiting both blocks, setting previous try level back to -2
    jmp    SHORT $LN7@main

; outer block filter:
$LN8@main:
$LN19@main:
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov    edx, DWORD PTR [ecx]
    mov    eax, DWORD PTR [edx]
    mov    DWORD PTR $T2[ebp], eax
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    push   ecx
    mov    edx, DWORD PTR $T2[ebp]
    push   edx
    call   _filter_user_exceptions
    add    esp, 8
$LN10@main:
$LN17@main:
    ret    0

; outer block handler:
$LN9@main:
    mov    esp, DWORD PTR __$SEHRec$[ebp]
    push   OFFSET $SG85503 ; 'user exception caught'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], -2 ; exiting both blocks, setting previous try level back to -2
$LN7@main:
    xor    eax, eax
    mov    ecx, DWORD PTR __$SEHRec$[ebp+8]
    mov    DWORD PTR fs:0, ecx
    pop    ecx
    pop    edi
    pop    esi
    pop    ebx
    mov    esp, ebp
    pop    ebp
    ret    0
    _main  ENDP

_code$ = 8 ; size = 4
_ep$ = 12  ; size = 4
_filter_user_exceptions PROC
    push   ebp
    mov    ebp, esp
    mov    eax, DWORD PTR _code$[ebp]
    push   eax
    push   OFFSET $SG85486 ; 'in filter. code=0x%08X'
    call   _printf
    add    esp, 8
    cmp    DWORD PTR _code$[ebp], 1122867 ; 00112233H
    jne    SHORT $LN2@filter_use
    push   OFFSET $SG85488 ; 'yes, that is our exception'
    call   _printf
    add    esp, 4
    mov    eax, 1
    jmp    SHORT $LN3@filter_use
    jmp    SHORT $LN3@filter_use
$LN2@filter_use:
    push   OFFSET $SG85490 ; 'not our exception'
    call   _printf
    add    esp, 4
    xor    eax, eax
$LN3@filter_use:
    pop    ebp
    ret    0
_filter_user_exceptions ENDP

Cookie Offset是EBP的值(栈帧栈底)与栈内EBP⊕security_cookie之间的差值。Cookie XOR Offset是EBP⊕security_cookie与栈中数值之间的差值。如果上述各值不符合下列条件,那么整个进程将会因为栈损坏而终止运行:

security_cookie⊕(CookieXOROffset +address_of_saved_EBP) == stack[address_of_saved_EBP + CookieOffset]

如果Cookie Offset的值是−2,就表示这个cookie并不存在(GScookie一般如此)。

笔者编写的tracer程序也能进行Cookies的合法性检查,有关详情请访问https://github.com/ dennis714/tracer/blob/master/SEH.c

在启用“/GS-”选项之后MSVC 2005编译器就会分配SEH3的函数,但是它依然会分配SEH4的CRT代码。

68.3.3 Windows x64

和大家想象的一样,每个函数都在序言中设置SEH栈帧将会降低运行速度。此外,在程序运行过程中不断调整“previous try level”字段同样会增加时间开销。然而在x64程序里,整个情况彻底不同了:所有try块的指针、filter和handler函数的指针都单独存储于可执行文件的.pdata段。操作系统根据.pdata段获取异常处理的全部信息。

我们把上一个章节的两个程序编译为x64程序,可以得到:

指令清单68.12 MSVC 2012

$SG86276 DB     'hello #1!', 0aH, 00H
$SG86277 DB     'hello #2!', 0aH, 00H
$SG86279 DB     'access violation, can''t recover', 0aH, 00H

pdata    SEGMENT
$pdata$main DD  imagerel $LN9
        DD      imagerel $LN9+61
        DD      imagerel $unwind$main
pdata   ENDS
pdata   SEGMENT
$pdata$main$filt$0 DD imagerel main$filt$0
        DD      imagerel main$filt$0+32
        DD      imagerel $unwind$main$filt$0
pdata   ENDS
xdata   SEGMENT
$unwind$main DD 020609H
        DD      030023206H
        DD      imagerel __C_specific_handler
        DD      01H
        DD      imagerel $LN9+8
        DD      imagerel $LN9+40
        DD      imagerel main$filt$0
        DD      imagerel $LN9+40
$unwind$main$filt$0 DD 020601H
        DD      050023206H
xdata   ENDS

_TEXT   SEGMENT
main    PROC
$LN9:
        push    rbx
        sub     rsp, 32
        xor     ebx, ebx
        lea     rcx, OFFSET FLAT:$SG86276 ; 'hello #1!'
        call    printf
        mov     DWORD PTR [rbx], 13
        lea     rcx, OFFSET FLAT:$SG86277 ; 'hello #2!'
        call    printf
        jmp     SHORT $LN8@main
$LN6@main:
        lea     rcx, OFFSET FLAT:$SG86279 ; 'access violation, can''t recover'
        call    printf
        npad    1 ; align next label
$LN8@main:
        xor     eax, eax
        add     rsp, 32
        pop     rbx
        ret     0
main    ENDP
_TEXT   ENDS

text$x  SEGMENT
main$filt$0 PROC
        push    rbp
        sub     rsp, 32
        mov     rbp, rdx
$LN5@main$filt$:
        mov     rax, QWORD PTR [rcx]
        xor     ecx, ecx
        cmp     DWORD PTR [rax], -1073741819; c0000005H
        sete    cl
        mov     eax, ecx
$LN7@main$filt$:
        add     rsp, 32
        pop     rbp
        ret     0
        int     3
main$filt$0 ENDP
text$x ENDS

指令清单68.13 MSVC 2012

$SG86277 DB     'in filter. code=0x%08X', 0aH, 00H
$SG86279 DB     'yes, that is our exception', 0aH, 00H
$SG86281 DB     'not our exception', 0aH, 00H
$SG86288 DB     'hello!', 0aH, 00H
$SG86290 DB     '0x112233 raised. now let''s crash', 0aH, 00H
$SG86292 DB     'access violation, can''t recover', 0aH, 00H
$SG86294 DB     'user exception caught', 0aH, 00H

pdata   SEGMENT
$pdata$filter_user_exceptions DD imagerel $LN6
        DD      imagerel $LN6+73
        DD      imagerel $unwind$filter_user_exceptions
$pdata$main DD  imagerel $LN14
        DD      imagerel $LN14+95
        DD      imagerel $unwind$main
pdata   ENDS
pdata   SEGMENT
$pdata$main$filt$0 DD imagerel main$filt$0
        DD      imagerel main$filt$0+32
        DD      imagerel $unwind$main$filt$0
$pdata$main$filt$1 DD imagerel main$filt$1
        DD      imagerel main$filt$1+30
        DD      imagerel $unwind$main$filt$1
pdata   ENDS

xdata SEGMENT
$unwind$filter_user_exceptions DD 020601H
        DD      030023206H
$unwind$main DD 020609H
        DD      030023206H
        DD      imagerel __C_specific_handler
        DD      02H
        DD      imagerel $LN14+8
        DD      imagerel $LN14+59
        DD      imagerel main$filt$0
        DD      imagerel $LN14+59
        DD      imagerel $LN14+8
        DD      imagerel $LN14+74
        DD      imagerel main$filt$1
        DD      imagerel $LN14+74
$unwind$main$filt$0 DD 020601H
        DD      050023206H
$unwind$main$filt$1 DD 020601H
        DD      050023206H
Xdata   ENDS

_TEXT   SEGMENT
main    PROC
$LN14:
        push    rbx
        sub     rsp, 32
        xor     ebx, ebx
        lea     rcx, OFFSET FLAT:$SG86288 ; 'hello!'
        call    printf
        xor     r9d, r9d
        xor     r8d, r8d
        xor     edx, edx
        mov     ecx, 1122867 ; 00112233H
        call    QWORD PTR __imp_RaiseException
        lea     rcx, OFFSET FLAT:$SG86290 ; '0x112233 raised. now let''s crash'
        call    printf
        mov     DWORD PTR [rbx], 13
        jmp     SHORT $LN13@main
$LN11@main:
        lea     rcx, OFFSET FLAT:$SG86292 ; 'access violation, can''t recover'
        call    printf
        npad    1 ; align next label
$LN13@main:
        jmp     SHORT $LN9@main
$LN7@main:
        lea     rcx, OFFSET FLAT:$SG86294 ; 'user exception caught'
        call    printf
        npad    1 ; align next label
$LN9@main:
        xor     eax, eax
        add     rsp, 32
        pop     rbx
        ret     0
main    ENDP

text$x SEGMENT
main$filt$0 PROC
        push    rbp
        sub     rsp, 32
        mov     rbp, rdx
$LN10@main$filt$:
        mov     rax, QWORD PTR [rcx]
        xor     ecx, ecx
        cmp     DWORD PTR [rax], -1073741819; c0000005H
        sete    cl
        mov     eax, ecx
$LN12@main$filt$:
        add     rsp, 32
        pop     rbp
        ret     0
        int     3
main$filt$0 ENDP

main$filt$1 PROC
        push    rbp
        sub     rsp, 32
        mov     rbp, rdx
$LN6@main$filt$:
        mov     rax, QWORD PTR [rcx]
        mov     rdx, rcx
        mov     ecx, DWORD PTR [rax]
        call    filter_user_exceptions
        npad    1 ; align next label
$LN8@main$filt$:
        add     rsp, 32
        pop     rbp
        ret     0
        int     3
main$filt$1 ENDP
text$x ENDS

_TEXT SEGMENT
code$ = 48
ep$ = 56
filter_user_exceptions PROC
$LN6:
        push    rbx
        sub     rsp, 32
        mov     ebx, ecx
        mov     edx, ecx
        lea     rcx, OFFSET FLAT:$SG86277 ; 'in filter. code=0x%08X'
        call    printf
        cmp     ebx, 1122867; 00112233H
        jne     SHORT $LN2@filter_use
        lea     rcx, OFFSET FLAT:$SG86279 ; 'yes, that is our exception'
        call    printf
        mov     eax, 1
        add     rsp, 32
        pop     rbx
        ret     0
$LN2@filter_use:
        lea     rcx, OFFSET FLAT:$SG86281 ; 'not our exception'
        call    printf
        xor     eax, eax
        add     rsp, 32
        pop     rbx
        ret     0
filter_user_exceptions ENDP
_TEXT ENDS

要想查看更多信息,可以参考Igor Skochinsky撰写的文章“Compiler Internals:Exceptional and RTTL(编译器内幕:例外与RTTL)”。

除了例外信息外,.pdata段还存储着几乎所有函数的起始和结束的地址。由此可见,.pdata段是自动分析的工具的重点分析对象。

68.3.4 关于SEH的更多信息

Igor Skochinsky编写的文章“Compiler Internals:Exceptional and RTTL(编译器内幕:例外与RTTL)”。

Matt Pietrek编写的文章“A Crash Course on the Depths of Win32 Structured Exception Handling(Win32结构性例外进程的崩溃的深度分析)”。

68.4 Windows NT:临界区段

在任何一个多线程的环境下,临界区段(Critical section)是保护数据一致性和操作互斥性的重要手段。临界区段保证了在同一时间内只会有一个线程访问某些数据,阻止其他进程和中断同期操作相关数据。

在Windows NT系统的数据结构中,CRITICAL_SECTION关键段的定义如下。

指令清单68.14 (Windows Research Kernel v1.2) public/sdk/inc/nturtl.h

typedef struct _RTL_CRITICAL_SECTION {
    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;

    //
    // The following three fields control entering and exiting the critical
    // section for the resource
    //

    LONG LockCount;
    LONG RecursionCount;
    HANDLE OwningThread;        // from the thread's ClientId->UniqueThread
    HANDLE LockSemaphore;
    ULONG_PTR SpinCount;        // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

下面的代码描述了函数EnterCriticalSection()的工作原理。

指令清单68.15 Windows 2008/ntdll.dll/x86(开始)

_RtlEnterCriticalSection@4

var_C           = dword ptr -0Ch
var_8           = dword ptr -8
var_4           = dword ptr -4
arg_0           = dword ptr 8

                mov     edi, edi
                push    ebp
                mov     ebp, esp
                sub     esp, 0Ch
                push    esi
                push    edi
                mov     edi, [ebp+arg_0]
                lea     esi, [edi+4] ; LockCount
                mov     eax, esi
                lock btr dword ptr [eax], 0
                jnb     wait ; jump if CF=0

loc_7DE922DD:
                mov     eax, large fs:18h
                mov     ecx, [eax+24h]
                mov     [edi+0Ch], ecx
                mov     dword ptr [edi+8], 1
                pop     edi
                xor     eax, eax
                pop     esi
                mov     esp, ebp
                pop     ebp
                retn    4

... skipped

在代码段中最重要的指令是BTR(及其LOCK前缀):BTR指令把第一个操作数的第0位复制给CF标识位,然后再把这个位清零。由LOCK前缀修饰的都是原子性操作,可以让CPU阻止其他的系统总线读取或修改相关内存地址。如果LockCount的第0位值是1,则将其充值重置并退出函数——CPU现在正处于临界区;否则,则表示其他线程正在占用临界区,CPU将等待相关操作结束。

等待期间运行的函数是WaitForSingleObject()。

下述代码描述了LeaveCriticalSection()函数的工作机理:

指令清单68.16 Windows 2008/ntdll.dll/x86(开始)

_RtlLeaveCriticalSection@4 proc near

arg_0           = dword ptr 8

                mov     edi, edi
                push    ebp
                mov     ebp, esp
                push    esi
                mov     esi, [ebp+arg_0]
                add     dword ptr [esi+8], 0FFFFFFFFh ; RecursionCount
                jnz     short loc_7DE922B2
                push    ebx
                push    edi
                lea     edi, [esi+4] ; LockCount
                mov     dword ptr [esi+0Ch], 0
                mov     ebx, 1
                mov     eax, edi
                lock xadd [eax], ebx
                inc     ebx
                cmp     ebx, 0FFFFFFFFh
                jnz     loc_7DEA8EB7

loc_7DE922B0:
                pop     edi
                pop     ebx

loc_7DE922B2:
                xor     eax, eax
                pop     esi
                pop     ebp
                retn    4

... skipped

XADD指令的功能是:先交换操作数的值,然后再进行加法运算。在本例中,它将LockCount与数字1的和存储于第一个操作数,同时将LockCount的初始值传递给EBX寄存器。但是EBX里的这个值也随即被后面的INC指令递增,最终与LockCount的值同步。因为它带有LOCK前缀,所以属于原子操作。这就意味着所有的其他CPU(不管是几核的)都不能同时访问那片内存区域。

LOCK前缀非常重要。不同的CPU或者CPU核心(core)可能会加载同一个进程的不同线程。若使用无LOCK前缀的指令操作临界区段的数据,很可能发生无法预料的情况。