November 27, 2023

Windows-SEH学习笔记(2)

咕了一年终于写完了,泪目……
由于笔者写完实在懒得再看一遍,所以有的地方有可能会有小问题……如果读者有某部分不理解,一定是笔者的问题,绝对与读者本人水平无关,一切问题都怪笔者

翻译链接:https://blog.csdn.net/aa13058219642/article/details/80253609
存档英文原文:https://bytepointer.com/resources/pietrek_crash_course_depths_of_win32_seh.htm
强烈推荐阅读原文……本文只是一些简略的东西以及我的一些垃圾吐槽和sb问题。以及以及现在用的已经是__except_handler4了,而这篇文章大部分讲述还用的是__except_handler3,我会掺杂一丢丢关于__except_handler4部分的更新(以后有闲心了再多补点),原文中一些(我感觉)奇怪的地方我也会做简略说明。

再分析

_except_handler异常回调函数接收操作系统传递的关于异常的信息,使用这些信息来决定下一步做什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//异常回调函数
EXCEPTION_DISPOSITION
__cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame, //指向establisher帧结构
struct _CONTEXT *ContextRecord, //指向CONTEXT结构
void * DispatcherContext
);

typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; //异常代码
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress; //异常发生的地址
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

那么发生错误时操作系统怎么知道去哪里调用这个异常回调函数?答案是一个称为EXCEPTION_REGISTRATION的结构。

1
2
3
4
5
6
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
//指向下一个 EXCEPTION_REGISTRATION_RECORD
PEXCEPTION_DISPOSITION Handler;
//指向异常处理函数
} EXCEPTION_REGISTRATION_RECORD,*PEXCEPTION_REGISTRATION_RECORD;

那么操作系统到哪里去找EXCEPTION_REGISTRATION?要回答这个问题,首先要记住结构化异常处理是基于线程的。线程信息块(TEB)的第一个DWORD就是指向EXCEPTION_REGISTRATION结构的指针。在intel处理器的win32平台上,FS寄存器指向当前TEB,所以FS:[0]就是指向EXCEPTION_REGISTRATION结构的指针。

问题到此清楚。异常发生时,系统找到当前线程的TEB,获取指向EXCEPTION_REGISTRATION结构的指针,在这个结构中找到指向异常回调函数_except_handler的指针,然后调用它。

一个简单的用于描述以上过程的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//==================================================
// MYSEH - Matt Pietrek 1997
// Microsoft Systems Journal, January 1997
// FILE: MYSEH.CPP
// To compile: CL MYSEH.CPP
//==================================================
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>

DWORD scratch;

EXCEPTION_DISPOSITION
__cdecl
_except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext )
{
unsigned i;

// Indicate that we made it to our exception handler
printf( "Hello from an exception handler\n" );

// Change EAX in the context record so that it points to someplace
// where we can successfully write
ContextRecord->Eax = (DWORD)&scratch;

// Tell the OS to restart the faulting instruction
return ExceptionContinueExecution;
}

int main()
{
DWORD handler = (DWORD)_except_handler;

__asm
{
// Build EXCEPTION_REGISTRATION record:
push handler // Address of handler function
push FS:[0] // Address of previous handler
mov FS:[0],ESP // Install new EXECEPTION_REGISTRATION
}

__asm
{
mov eax,0 // Zero out EAX
mov [eax], 1 // Write to EAX to deliberately cause a fault
}

printf( "After writing!\n" );

__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;
}

然而在实际情况中,有很多异常,并非一个异常处理函数就能处理所有情况,所以EXCEPTION_REGISTRATION是一个链表结构。操作系统沿着这个链表寻找,找到能够处理当前异常的异常回调函数。异常回调函数可以选择处理异常,也可以拒绝处理异常。这是一个用于演示异常回调函数拒绝处理异常的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//==================================================
// MYSEH2 - Matt Pietrek 1997
// Microsoft Systems Journal, January 1997
// FILE: MYSEH2.CPP
// To compile: CL MYSEH2.CPP
//==================================================
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>

EXCEPTION_DISPOSITION
__cdecl
_except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext )
{
printf( "Home Grown handler: Exception Code: %08X Exception Flags %X",
ExceptionRecord->ExceptionCode, ExceptionRecord->ExceptionFlags );

if ( ExceptionRecord->ExceptionFlags & 1 ) printf( " EH_NONCONTINUABLE" );
if ( ExceptionRecord->ExceptionFlags & 2 ) printf( " EH_UNWINDING" );
if ( ExceptionRecord->ExceptionFlags & 4 ) printf( " EH_EXIT_UNWIND" );
if ( ExceptionRecord->ExceptionFlags & 8 ) printf( " EH_STACK_INVALID" );
if ( ExceptionRecord->ExceptionFlags & 0x10 ) printf( " EH_NESTED_CALL" );

printf( "\n" );

// Punt... We don't want to handle this... Let somebody else handle it
return ExceptionContinueSearch;
}

void HomeGrownFrame( void )
{
DWORD handler = (DWORD)_except_handler;

__asm
{
// Build EXCEPTION_REGISTRATION record:
push handler // Address of handler function
push FS:[0] // Address of previous handler
mov FS:[0],ESP // Install new EXECEPTION_REGISTRATION
}

*(PDWORD)0 = 0; // Write to address 0 to cause a fault

printf( "I should never get here!\n" );

__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
}
}

int main()
{
_try
{
HomeGrownFrame();
}
_except( EXCEPTION_EXECUTE_HANDLER )
{
printf( "Caught the exception in main()\n" );
}

return 0;
}

_except_handler返回ExceptionContinueSearch告诉操作系统自己不处理异常,接下来是通过_except安装的异常回调函数,它打印Caught the exception in main()。我们在这里只是简单地忽略了这个异常。
首先记住:当一个异常回调函数拒绝处理某个异常时,它实际上也就拒绝了决定程序流程从何处恢复。只有处理某个异常的函数才能决定程序流从何处恢复。由于_except_handler并不处理异常,所以出错代码后的printf永远不会执行。

展开(unwind)

如果运行这个程序,会发现输出有些奇怪。看起来_except_handler调用了两次,这是为什么?

1
2
3
Home Grown handler: Exception Code: C0000005 Exception Flags 0
Home Grown handler: Exception Code: C0000027 Exception Flags 2 EH_UNWINDING
Caught the Exception in main()

两次异常标志不同,第二次的异常标志是2,而这是由于 展开(unwind)
当一个异常处理回调函数拒绝处理某个异常时,它会被再一次调用。
当异常发生时,系统遍历EXCEPTION_REGISTRATION结构链表,直到它找到一个处理这个异常的处理程序。一旦找到,系统就再次遍历这个链表,直到处理这个异常的结点为止。在这第二次遍历中,系统将再次调用每个异常处理函数。关键的区别是,在第二次调用中,异常标志被设置为2。这个值被定义为EH_UNWINDING。
为何要这样设置(即调用两次未处理异常的函数)呢?这是为了给这个函数最后一个清理的机会。一个绝好的例子是C++类的析构函数。当一个函数的异常处理程序拒绝处理某个异常时,通常执行流程并不会正常地从那个函数退出。现在,想像一个定义了一个C++类的实例作为局部变量的函数。C++规范规定析构函数必须被调用。这带EH_UNWINDING标志的第二次回调就给这个函数一个机会去做一些类似于调用析构函数和__finally块之类的清理工作。
在异常已经被处理完毕,并且所有前面的异常帧都已经被展开之后,流程从处理异常的那个回调函数决定的地方开始继续执行。一定要记住,仅仅把指令指针设置到所需的代码处就开始执行是不行的。流程恢复执行处的代码的堆栈指针和栈帧指针(在Intel CPU上是ESP和EBP)也必须被恢复成它们在处理这个异常的函数的栈帧上的值。因此,这个处理异常的回调函数必须负责把堆栈指针和栈帧指针恢复成它们在包含处理这个异常的SEH代码的函数的堆栈上的值。
通常,展开操作导致堆栈上处理异常的帧以下的堆栈区域上的所有内容都被移除了,就好像我们从来没有调用过这些函数一样。展开的另外一个效果就是 EXCEPTION_REGISTRATION结构链表上处理异常的那个结构之前的所有EXCEPTION_REGISTRATION结构都被移除了。这很好理解,因为这些EXCEPTION_REGISTRATION结构通常都被创建在堆栈上。在异常被处理后,堆栈指针和栈帧指针在内存中比那些从 EXCEPTION_REGISTRATION结构链表上移除的EXCEPTION_REGISTRATION结构高。
链表的最后一个节点是操作系统提供的默认异常处理函数,它总是会选择处理异常。这个函数是在用户代码执行前插入的。下为BaseProcessStart函数写的伪代码,它是Windows NT KERNEL32.DLL的一个内部例程。这个函数带一个参数——线程入口点函数的地址。BaseProcessStart运行在新进程的环境中,并且它调用这个进程的第一个线程的入口点函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
BaseProcessStart(PVOID lpfnEntryPoint)
{
DWORD retValue;
DWORD currentESP;
DWORD exceptionCode;
currentESP = ESP;
__try
{
NtSetInformationThread(GetCurrentThread(),
ThreadQuerySetWin32StartAddress,
&lpfnEntryPoint,
sizeof(lpfnEntryPoint));
retValue = lpfnEntryPoint();
ExitThread(retValue);
}
__except ( //过滤器表达式代码
exceptionCode = GetExceptionInformation(),
UnhandledExceptionFilter(GetExceptionInformation()))
{
ESP = currentESP;
if (!_BaseRunningInServerProcess) // 普通进程
ExitProcess(exceptionCode);
else // 服务
ExitThread(exceptionCode);
}
}

到这里👴实际上已经有点迷糊了(
那展开的意思是_except_handler会被调用两次,那么自己写个正常插入的异常试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <string.h>
#include <windows.h>

LONG MyFilter1()
{
puts("1");
return EXCEPTION_CONTINUE_SEARCH;
}

LONG MyFilter2()
{
puts("2");
return EXCEPTION_EXECUTE_HANDLER;
}

int main()
{
char* a;
a = 0;
__try {
__try {
*a = 0;
puts("try~");
}
__except (MyFilter1()) {
printf("catch 1\n");
}
}
__except (MyFilter2()) {
printf("catch 2\n");
}

return 0;
}
/*
1
2
catch 2
*/

然鹅并没有传说中的调用两次
马萨卡是因为编译器并8是像手写汇编那样插入_except_handler的🐎
8过我注意到原文作者用到的宏跟我常用的不一样,于是把EXCEPTION_CONTINUE_SEARCH改成ExceptionContinueSearch试试(

1
2
1
catch 1

👴直接人傻了。。。问chatgpt它说这俩常量是一个意思……于是👴紧急加了一句printf("ExceptionContinueSearch: %#x\nEXCEPTION_CONTINUE_SEARCH: %#x\nEXCEPTION_EXECUTE_HANDLER: %#x\n", ExceptionContinueSearch, EXCEPTION_CONTINUE_SEARCH, EXCEPTION_EXECUTE_HANDLER);,结果是:

1
2
3
ExceptionContinueSearch: 0x1
EXCEPTION_CONTINUE_SEARCH: 0
EXCEPTION_EXECUTE_HANDLER: 0x1

☁️:❓
☁️:怎么会是捏❓

fine,本来的疑问没解决,结果多了一堆新的疑问………
算了,暂且放下这些问题继续往下看

编译器层面的SEH

关于编译器级的SEH我已经在第一篇关于SEH的学习笔记中写过了,此处总结几个要点。

基于帧的异常处理程序模型

简单地说,在一个函数中,一个__try块中的所有代码就通过创建在这个函数的堆栈帧上的一个EXCEPTION_REGISTRATION结构来保护。在函数的入口处,这个新的EXCEPTION_REGISTRATION结构被放在异常处理程序链表的头部。在__try块结束后,相应的 EXCEPTION_REGISTRATION结构从这个链表的头部被移除。正如前面所说,异常处理程序链表的头部被保存在FS:[0]处。因此,如果你在调试器中单步跟踪时看到类似MOV DWORD PTR FS:[00000000],ESP或者MOV DWORD PTR FS:[00000000],ECX的指令时,就能非常确定这段代码正在进入或退出一个__try/__except块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct _EXCEPTION_REGISTRATION
{
struct _EXCEPTION_REGISTRATION *prev; //ebp-0x10
void *handler; //ebp-0x0c 异常处理函数
struct scopetable_entry *scopetable;//ebp-8 类型为 scopetable_entry 的数组
typedef struct _SCOPETABLE
{
DWORD previousTryLevel; //定位前一个try块的索引值
DWORD lpfnFilter; //当前try块的过滤函数
DWORD lpfnHandler; //当前try块的终止函数
}SCOPETABLE, *PSCOPETABLE;

int trylevel; //ebp-4 数组下标,用来索引 scopetable 中的数组成员
int _ebp; //ebp 包含该 _EXCEPTION_REGISTRATION 结构体创建之前的栈帧指针
PEXCEPTION_POINTERS xpointers;
};

各个EXCEPTION_REGISTRATION结构的handler域都指向了同一个函数,这个函数调用了我们的异常过滤器。原文中说这个函数是__except_handler3,实际上现在自己编译一个程序的话会发现已经是__except_handler4🌶️(

以下是chatgpt关于这两个函数的说明:
__except_handler4__except_handler3 是 Microsoft 编译器提供的异常处理相关的内部函数。这些函数通常与 SEH(Structured Exception Handling)相关,用于处理 Windows 程序中的异常。

  1. 版本号:

    • __except_handler4 是用于处理 SEH 异常的第四个版本的处理器。
    • __except_handler3 是用于处理 SEH 异常的第三个版本的处理器。
  2. 支持的 Windows 版本:

    • __except_handler4 是在 Windows Vista 和更高版本上引入的。
    • __except_handler3 可能在 Windows XP 上使用。

这两个函数都是内部实现,通常不直接由开发人员调用。它们在异常处理的内部工作中发挥作用,例如在 C/C++ 程序中的 __try__except 块中。

最后,在使用SEH的任何函数中只创建一个EXCEPTION_REGISTRATION结构。换句话说,你可以在一个函数中使用多个__try/__except块,但是在堆栈上只创建一个EXCEPTION_REGISTRATION结构。同样,你可以在一个函数中嵌套使用__try块,但Visual C++仍旧只是创建一个EXCEPTION_REGISTRATION结构。

扩展的异常处理帧

个人认为这里是最难理解的重点,可以多看几遍

_ebp域成为扩展的EXCEPTION_REGISTRATION结构的一部分并非偶然。它是通过PUSH EBP这条指令被包含进这个结构中的,而大多数函数开头都是这条指令(通常编译器并不为使用FPO优化的函数生成标准的堆栈帧,这样其第一条指令可能不是PUSH EBP。但是如果使用了SEH的话,那么无论你是否使用了FPO优化,编译器一定生成标准的堆栈帧)。这条指令可以使EXCEPTION_REGISTRATION结构中所有其它的域都可以用一个相对于栈帧指针(EBP)的负偏移来访问。例如 trylevel域在[EBP-04]处,scopetable指针在[EBP-08]处,等等。(也就是说,这个结构是从[EBP-10H]处开始的。)
紧跟着扩展的EXCEPTION_REGISTRATION结构下面,Visual C++压入了另外两个值。紧跟着(即[EBP-14H]处)的一个DWORD,是为一个指向EXCEPTION_POINTERS结构(一个标准的Win32 结构)的指针所保留的空间。
这里说两个特殊的编译器内联函数:

1
2
3
MOV EAX, DWORD PTR[EBP - 14]    // 执行完毕,EAX指向EXCEPTION_POINTERS结构
MOV EAX, DWORD PTR[EAX] // 执行完毕,EAX指向EXCEPTION_RECORD结构
MOV EAX, DWORD PTR[EAX] // 执行完毕,EAX中是ExceptionCode的值

现在回到扩展的EXCEPTION_REGISTRATION结构上来。在这个结构开始前的8个字节处(即[EBP-18H]处),Visual C++保留了一个DWORD来保存所有prolog代码执行完毕之后的堆栈指针(ESP)的值(实际生成的指令为MOV DWORD PTR [EBP-18H], ESP)。这个DWORD中保存的值是函数执行时ESP寄存器的正常值。
Visual C++为使用结构化异常处理的函数生成的标准异常堆栈帧如下:

1
2
3
4
5
6
7
EBP-00 _ebp
EBP-04 trylevel
EBP-08 scopetable数组指针
EBP-0C handler函数地址
EBP-10 指向前一个EXCEPTION_REGISTRATION结构
EBP-14 GetExceptionInformation
EBP-18 栈帧中的标准ESP

在操作系统看来,只存在组成原始EXCEPTION_REGISTRATION结构的两个域:即[EBP-10h]处的prev指针和[EBP-0Ch]处的handler函数指针。栈帧中的其它所有内容是针对于Visual C++的。

真正实现编译器层面SEH的这个Visual C++运行时库例程——__except_handler3函数的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
int __except_handler3(
struct _EXCEPTION_RECORD * pExceptionRecord,
struct EXCEPTION_REGISTRATION * pRegistrationFrame,
struct _CONTEXT *pContextRecord,
void * pDispatcherContext)
{
LONG filterFuncRet;
LONG trylevel;
EXCEPTION_POINTERS exceptPtrs;
PSCOPETABLE pScopeTable;
CLD // 将方向标志复位(不测试任何条件!)

// 如果没有设置EXCEPTION_UNWINDING标志或EXCEPTION_EXIT_UNWIND标志
// 表明这是第一次调用这个处理程序(也就是说,并非处于异常展开阶段)

if (!(pExceptionRecord->ExceptionFlags
& (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND)))
{
// 在堆栈上创建一个EXCEPTION_POINTERS结构
exceptPtrs.ExceptionRecord = pExceptionRecord;
exceptPtrs.ContextRecord = pContextRecord;

// 把前面定义的EXCEPTION_POINTERS结构的地址放在比
// establisher栈帧低4个字节的位置上。参考前面我讲
// 的编译器为GetExceptionInformation生成的汇编代码
*(PDWORD)((PBYTE)pRegistrationFrame - 4) = &exceptPtrs;

// 获取初始的“trylevel”值
trylevel = pRegistrationFrame->trylevel;

// 获取指向scopetable数组的指针
scopeTable = pRegistrationFrame->scopetable;

search_for_handler:
if (pRegistrationFrame->trylevel != TRYLEVEL_NONE)
{
if (pRegistrationFrame->scopetable[trylevel].lpfnFilter)
{
PUSH EBP // 保存这个栈帧指针

// !!!非常重要!!!切换回原来的EBP。正是这个操作才使得
// 栈帧上的所有局部变量能够在异常发生后仍然保持它的值不变。
EBP = &pRegistrationFrame->_ebp;

// 调用过滤器函数
filterFuncRet = scopetable[trylevel].lpfnFilter();

POP EBP // 恢复异常处理程序的栈帧指针

if (filterFuncRet != EXCEPTION_CONTINUE_SEARCH)
{
if (filterFuncRet < 0) // EXCEPTION_CONTINUE_EXECUTION
return ExceptionContinueExecution;

// 如果能够执行到这里,说明返回值为EXCEPTION_EXECUTE_HANDLER
scopetable = pRegistrationFrame->scopetable;

// 让操作系统清理已经注册的栈帧,这会使本函数被递归调用
__global_unwind2(pRegistrationFrame);

// 一旦执行到这里,除最后一个栈帧外,所有的栈帧已经
// 被清理完毕,流程要从最后一个栈帧继续执行
EBP = &pRegistrationFrame->_ebp;
__local_unwind2(pRegistrationFrame, trylevel);

// NLG = "non-local-goto" (setjmp/longjmp stuff)
__NLG_Notify(1); // EAX = scopetable->lpfnHandler

// 把当前的trylevel设置成当找到一个异常处理程序时
// SCOPETABLE中当前正在被使用的那一个元素的内容
pRegistrationFrame->trylevel = scopetable->previousTryLevel;
// 调用__except {}块,这个调用并不会返回
pRegistrationFrame->scopetable[trylevel].lpfnHandler();
}
}
scopeTable = pRegistrationFrame->scopetable;
trylevel = scopeTable->previousTryLevel;
goto search_for_handler;
}
else // trylevel == TRYLEVEL_NONE
{
return ExceptionContinueSearch;
}
}
else // 设置了EXCEPTION_UNWINDING标志或EXCEPTION_EXIT_UNWIND标志
{
PUSH EBP // 保存EBP
EBP = &pRegistrationFrame->_ebp; // 为调用__local_unwind2设置EBP

__local_unwind2(pRegistrationFrame, TRYLEVEL_NONE)

POP EBP // 恢复EBP

return ExceptionContinueSearch;
}
}

__except_handler3大体上可以由第一个if语句分为两部分。这是由于这个函数可以在两种情况下被调用,一次是正常调用,另一次是在展开阶段。其中大部分是在非展开阶段的回调。
__except_handler3一开始就在堆栈上创建了一个EXCEPTION_POINTERS结构,并用它的两个参数来对这个结构进行初始化。我在伪代码中把这个结构称为 exceptPrts,它的地址被放在[EBP-14h]处。你回忆一下前面我讲的编译器为GetExceptionInformation和 GetExceptionCode函数生成的汇编代码就会意识到,这实际上初始化了这两个函数使用的指针。
原文似乎并没有解释EXCEPTION_POINTERS到底是个什么东西,不过根据上下文可以推测出来是一个指向_EXCEPTION_RECORD的指针。在扩展的异常处理帧中,它指向当前的_EXCEPTION_RECORD。
接着,__except_handler3从EXCEPTION_REGISTRATION帧中获取当前的trylevel(在[EBP-04h]处)。trylevel变量实际是scopetable数组的索引,而正是这个数组才使得一个函数中的多个__try块和嵌套的__try块能够仅使用一个 EXCEPTION_REGISTRATION结构。每个scopetable元素结构如下:

1
2
3
4
5
typedef struct _SCOPETABLE{
DWORD previousTryLevel;//定位前一个try块的索引值
DWORD lpfnFilter; //过滤器表达式代码的地址
DWORD lpfnHandler; //相应的__except块的地址
} SCOPETABLE, *PSCOPETABLE;

总之一句话,它用于嵌套的__try块。这里的关键是函数中的每个__try块都有一个相应的SCOPETABLE结构。
当前的trylevel指定了要使用的scopetable数组的哪一个元素,最终也就是指定了过滤器表达式和__except块的地址。
previousTryLevel给出前一个__try块的索引,这是为了在嵌套的__try块中找到外层的__try块(前一个__try块实际上相当于外层的__try块)。这种机制将嵌套的__try块简化成一种数组的形式,让我们可以嵌套任意的__try块。
如果trylevel的值为0xFFFFFFFF(实际上就是-1,这个值在 EXSUP.INC中被定义为TRYLEVEL_NONE),标志着这个链表结束。
如果过滤器表达式返回EXCEPTION_EXECUTE_HANDLER, 这意味着异常应该由相应的__except块处理。它同时也意味着所有前面的EXCEPTION_REGISTRATION帧都应该从链表中移除,并且相应的__except块都应该被执行。第一个任务通过调用__global_unwind2来完成。跳过这中间的一些清理代码,流程离开__except_handler3转向__except块。令人奇怪的是,流程并不从__except块中返回,虽然是 __except_handler3使用CALL指令调用了它。__except_handler3是如何做到既通过CALL指令调用__except块而又不让执行流程返回呢?由于CALL指令要向堆栈中压入了一个返回地址,你可以想象这有可能破坏堆栈。如果你检查一下编译器为__except块生成的代码,你会发现它做的第一件事就是将EXCEPTION_REGISTRATION结构下面8个字节 处(即[EBP-18H]处)的一个DWORD值加载到ESP寄存器中(实际代码为MOV ESP,DWORD PTR [EBP-18H]),这个值是在函数的prolog代码中被保存在这个位置的(实际代码为MOV DWORD PTR [EBP-18H],ESP)。
当前的trylevel值是如何被设置的呢?它实际上是由编译器隐含处理的。编译器修改当前EXCEPTION_REGISTRATION结构中的trylevel域的值(原文这里翻译得有点奇怪,实际上看这句代码pRegistrationFrame->trylevel = scopetable->previousTryLevel;就很容易能理解了)。如果你检查编译器为使用SEH的函数生成的汇编代码,就会在不同的地方都看到修改这个位于[EBP-04h]处的trylevel域的值的代码。

现在可以回答上面那个问题:为什么自己使用标准的__except块时不会由于展开而调用两次_except_handler?这是因为作者在之前的代码中重写了_except_handler函数,而标准的_except_handler3函数(实际上,这个函数依然会因为展开而被第二次调用)在展开阶段直接调用__local_unwind2来最后一次清理栈帧,而不是重新调用__except块。
当然了👴依旧并8能理解关于EXCEPTION_CONTINUE_SEARCH和ExceptionContinueSearch这两个变量MSVC到底在抽什么疯

ShowSEHFrames程序

前面说得好像很乱 而我已经在尽力捋清楚逻辑了 当然了最好理解的办法当然还是自己写程序来应用这些理论,如果按照我们设想的那样运行,那就证明我们的理解是对的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
//=========================================================
// ShowSEHFrames - Matt Pietrek 1997
// Microsoft Systems Journal, February 1997
// FILE: ShowSEHFrames.CPP
// 使用命令行CL ShowSehFrames.CPP进行编译//=========================================================
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdio.h>
#pragma hdrstop

//-------------------------------------------------------------------
// 本程序仅适用于Visual C++,它使用的数据结构是特定于Visual C++的
//-------------------------------------------------------------------

#ifndef _MSC_VER
#error Visual C++ Required (Visual C++ specific information is displayed)
#endif

//-------------------------------------------------------------------
// 结构定义
//-------------------------------------------------------------------

// 操作系统定义的基本异常帧
struct EXCEPTION_REGISTRATION
{
EXCEPTION_REGISTRATION* prev;
FARPROC handler;
};

// Visual C++扩展异常帧指向的数据结构
struct scopetable_entry
{
DWORD previousTryLevel;
FARPROC lpfnFilter;
FARPROC lpfnHandler;
};

// Visual C++使用的扩展异常帧
struct VC_EXCEPTION_REGISTRATION : EXCEPTION_REGISTRATION
{
scopetable_entry * scopetable;
int trylevel;
int _ebp;
};

//----------------------------------------------------------------
// 原型声明
//----------------------------------------------------------------

// __except_handler3是Visual C++运行时库函数,我们想打印出它的地址
// 但是它的原型并没有出现在任何头文件中,所以我们需要自己声明它。
extern "C" int _except_handler3(PEXCEPTION_RECORD,
EXCEPTION_REGISTRATION *,
PCONTEXT,
PEXCEPTION_RECORD);

//-------------------------------------------------------------
// 代码
//-------------------------------------------------------------

// 显示一个异常帧及其相应的scopetable的信息
void ShowSEHFrame(VC_EXCEPTION_REGISTRATION * pVCExcRec)
{
printf("Frame: %08X Handler: %08X Prev: %08X Scopetable: %08X\n",
pVCExcRec, pVCExcRec->handler, pVCExcRec->prev,
pVCExcRec->scopetable);
scopetable_entry * pScopeTableEntry = pVCExcRec->scopetable;
for (unsigned i = 0; i <= pVCExcRec->trylevel; i++)
{
printf(" scopetable[%u] PrevTryLevel: %08X "
"filter: %08X __except: %08X\n", i,
pScopeTableEntry->previousTryLevel,
pScopeTableEntry->lpfnFilter,
pScopeTableEntry->lpfnHandler);
pScopeTableEntry++;
}
printf("\n");
}

// 遍历异常帧的链表,按顺序显示它们的信息
void WalkSEHFrames(void)
{
VC_EXCEPTION_REGISTRATION * pVCExcRec;

// 打印出__except_handler3函数的位置
printf("_except_handler3 is at address: %08X\n", _except_handler3);
printf("\n");

// 从FS:[0]处获取指向链表头的指针
__asm mov eax, FS:[0]
__asm mov[pVCExcRec], EAX

// 遍历异常帧的链表。0xFFFFFFFF标志着链表的结尾
while (0xFFFFFFFF != (unsigned)pVCExcRec)
{
ShowSEHFrame(pVCExcRec);
pVCExcRec = (VC_EXCEPTION_REGISTRATION *)(pVCExcRec->prev);
}
}

void Function1(void)
{
// 嵌套3层__try块以便强制为scopetable数组产生3个元素
__try
{
__try
{
__try
{
WalkSEHFrames(); // 现在显示所有的异常帧的信息
}
__except (EXCEPTION_CONTINUE_SEARCH)
{
}
}
__except (EXCEPTION_CONTINUE_SEARCH)
{
}
}
__except (EXCEPTION_CONTINUE_SEARCH)
{
}
}

int main()
{
int i;

// 使用两个__try块(并不嵌套),这导致为scopetable数组生成两个元素
__try
{
i = 0x1234;
}
__except (EXCEPTION_CONTINUE_SEARCH)
{
i = 0x4321;
}
__try
{
Function1(); // 调用一个设置更多异常帧的函数
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
// 应该永远不会执行到这里,因为我们并没有打算产生任何异常
printf("Caught Exception in main\n");
}
return 0;
}

运行结果如下:

你可能想知道为什么明明ShowSEHFrames程序只有两个函数使用SEH,但是却有三个异常处理帧使用__except_handler3作为它们的异常回调函数。实际上第三个帧来自Visual C++运行时库。Visual C++运行时库源代码中的CRT0.C文件清楚地表明了对main或WinMain的调用也被一个__try/__except块封装着。这个__try 块的过滤器表达式代码可以在WINXFLTR.C文件中找到。
回到ShowSEHFrames程序,注意到最后一个帧的异常处理程序的地址是77F3AB6C,这与其它三个不同。仔细观察一下,你会发现这个地址在 KERNEL32.DLL中。这个特别的帧就是由KERNEL32.DLL中的BaseProcessStart函数安装的,这在前面我已经说过。

现在对这个代码的结果分析一下,也算是一个简单的总结:每个函数中使用__try/__except块,就会在异常链表中增加一个结点。
异常链表头(由BaseProcessStart函数安装),这是最后一个栈帧,此后对main函数的调用也由__try/__except块封装,这是倒数第二个栈帧。然后是main函数中的栈帧:2个__try块生成了2个scopetable,由于__try块并没有嵌套,所以PrevTryLevel的值都为0xffffffff,表示当前__try块没有前一个__try块(即没有嵌套),这是第2个栈帧。最后是第一个栈帧,即Function1中的栈帧,由于__try块嵌套了3次,所以PrevTryLevel的值都代表嵌套的前一个__try块。
感觉这个可以出一道有意思的题 看我到时候给带🔥整个活

下面基本都是ctrl c + ctrl v了,直接看原文就🆗(

__except_handler的展开(Unwinding)

正如你在Visual C++的__except_handler3函数中看到的那样,展开是由__global_unwind2这个运行时库(RTL)函数来完成的。这个函数只是对RtlUnwind这个未公开的API进行了非常简单的封装。(现在这个API已经被公开了,但给出的信息极其简单,详细信息可以参考最新的Platform SDK文档。)

1
2
3
4
5
__global_unwind2(void * pRegistFrame)
{
_RtlUnwind( pRegistFrame, &__ret_label, 0, 0 );
__ret_label:
}

虽然从技术上讲RtlUnwind是一个KERNEL32函数,但它只是转发到了NTDLL.DLL中的同名函数上。图12是此函数的伪代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
void _RtlUnwind(
PEXCEPTION_REGISTRATION pRegistrationFrame,
PVOID returnAddr, // 并未使用!(至少是在i386机器上)
PEXCEPTION_RECORD pExcptRec,
DWORD _eax_value)
{
DWORD stackUserBase;
DWORD stackUserTop;
PEXCEPTION_RECORD pExcptRec;
EXCEPTION_RECORD exceptRec;
CONTEXT context;

// 从FS:[4]和FS:[8]处获取堆栈的界限
RtlpGetStackLimits(&stackUserBase, &stackUserTop);

if (0 == pExcptRec) // 正常情况
{
pExcptRec = &excptRec;
pExcptRec->ExceptionFlags = 0;
pExcptRec->ExceptionCode = STATUS_UNWIND;
pExcptRec->ExceptionRecord = 0;

//获取返回地址
pExcptRec->ExceptionAddress = RtlpGetReturnAddress();
pExcptRec->ExceptionInformation[0] = 0;
}
if (pRegistrationFrame)
pExcptRec->ExceptionFlags |= EXCEPTION_UNWINDING;
else // 这两个标志合起来被定义为EXCEPTION_UNWIND_CONTEXT
pExcptRec->ExceptionFlags |= (EXCEPTION_UNWINDING | EXCEPTION_EXIT_UNWIND);

context.ContextFlags = (CONTEXT_i486 | CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS);

RtlpCaptureContext(&context);

context.Esp += 0x10;
context.Eax = _eax_value;

PEXCEPTION_REGISTRATION pExcptRegHead;

pExcptRegHead = RtlpGetRegistrationHead(); // 返回FS:[0]的值

// 开始遍历EXCEPTION_REGISTRATION结构链表
while (-1 != pExcptRegHead)
{
EXCEPTION_RECORD excptRec2;
if (pExcptRegHead == pRegistrationFrame)
{
NtContinue(&context, 0);
}
else
{
// 如果存在某个异常帧在堆栈上的位置比异常链表的头部还低
// 说明一定出现了错误
if (pRegistrationFrame && (pRegistrationFrame <= pExcptRegHead))
{
// 生成一个异常
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;
excptRec2.ExceptionCode = STATUS_INVALID_UNWIND_TARGET;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;

RtlRaiseException(&exceptRec2);
}
}
PVOID pStack = pExcptRegHead + 8; // 8 = sizeof(EXCEPTION_REGISTRATION)

// 确保pExcptRegHead在堆栈范围内,并且是4的倍数
if ((stackUserBase <= pExcptRegHead)
&& (stackUserTop >= pStack)
&& (0 == (pExcptRegHead & 3)))
{
DWORD pNewRegistHead;
DWORD retValue;

retValue = RtlpExecutehandlerForUnwind(pExcptRec, pExcptRegHead, &context,
&pNewRegistHead, pExceptRegHead->handler);

if (retValue != DISPOSITION_CONTINUE_SEARCH)
{
if (retValue != DISPOSITION_COLLIDED_UNWIND)
{
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;
excptRec2.ExceptionCode = STATUS_INVALID_DISPOSITION;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;

RtlRaiseException(&excptRec2);
}
else
pExcptRegHead = pNewRegistHead;
}

PEXCEPTION_REGISTRATION pCurrExcptReg = pExcptRegHead;
pExcptRegHead = pExcptRegHead->prev;

RtlpUnlinkHandler(pCurrExcptReg);
}
else // 堆栈已经被破坏!生成一个异常
{
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;
excptRec2.ExceptionCode = STATUS_BAD_STACK;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;

RtlRaiseException(&excptRec2);
}
}

// 如果执行到这里,说明已经到了EXCEPTION_REGISTRATION
// 结构链表的末尾,正常情况下不应该发生这种情况。
//(因为正常情况下异常应该被处理,这样就不会到链表末尾)
if (-1 == pRegistrationFrame)
NtContinue(&context, 0);
else
NtRaiseException(pExcptRec, &context, 0);
}

//RtlUnwind函数的伪代码到这里就结束了,以下是它调用的几个函数的伪代码:

PEXCEPTION_REGISTRATION RtlpGetRegistrationHead(void)
{
return FS:[0];
}
RtlpUnlinkHandler(PEXCEPTION_REGISTRATION pRegistrationFrame)
{
FS:[0] = pRegistrationFrame->prev;
}
void RtlpCaptureContext(CONTEXT * pContext)
{
pContext->Eax = 0;
pContext->Ecx = 0;
pContext->Edx = 0;
pContext->Ebx = 0;
pContext->Esi = 0;
pContext->Edi = 0;
pContext->SegCs = CS;
pContext->SegDs = DS;
pContext->SegEs = ES;
pContext->SegFs = FS;
pContext->SegGs = GS;
pContext->SegSs = SS;
pContext->EFlags = flags; // 它对应的汇编代码为__asm{ PUSHFD / pop [xxxxxxxx] }
pContext->Eip = 此函数的调用者的调用者的返回地址 // 读者看一下这个函数的
pContext->Ebp = 此函数的调用者的调用者的EBP // 汇编代码就会清楚这一点
pContext->Esp = pContext->Ebp + 8;
}

虽然RtlUnwind函数的规模看起来很大,但是如果你按一定方法把它分开,其实并不难理解。它首先从FS:[4]和FS:[8]处获取当前线程堆栈的界限。它们对于后面要进行的合法性检查非常重要,以确保所有将要被展开的异常帧都在堆栈范围内。
RtlUnwind接着在堆栈上创建了一个空的EXCEPTION_RECORD结构并把STATUS_UNWIND赋给它的ExceptionCode域,同时把 EXCEPTION_UNWINDING标志赋给它的ExceptionFlags域。指向这个结构的指针作为其中一个参数被传递给每个异常回调函数。然后,这个函数调用RtlCaptureContext函数来创建一个空的CONTEXT结构,这个结构也变成了在展开阶段调用每个异常回调函数时传递给它们的一个参数。
RtlUnwind函数的其余部分遍历EXCEPTION_REGISTRATION结构链表。对于其中的每个帧,它都调用 RtlpExecuteHandlerForUnwind函数,后面我会讲到这个函数。正是这个函数带EXCEPTION_UNWINDING标志调用了异常处理回调函数。每次回调之后,它调用RtlpUnlinkHandler移除相应的异常帧。
RtlUnwind函数的第一个参数是一个帧的地址,当它遍历到这个帧时就停止展开异常帧。上面所说的这些代码之间还有一些安全性检查代码,它们用来确保不出问题。如果出现任何问题,RtlUnwind就引发一个异常,指示出了什么问题,并且这个异常带有EXCEPTION_NONCONTINUABLE标志。当一个进程被设置了这个标志时,它就不允许再运行,必须终止。

未处理异常

UnhandledExceptionFilter,即KERNEL32中进行默认异常处理的过滤器表达式,前面BaseProcessStart函数的伪代码已经表明了这一点。
下为为UnhandledExceptionFilter函数写的伪代码。这个API有点奇怪(至少在我看来是这样)。如果异常的类型是 EXCEPTION_ACCESS_VIOLATION,它就调用_BasepCheckForReadOnlyResource。虽然我没有提供这个函数的伪代码,但可以简要描述一下。如果是因为要对EXE或DLL的资源节(.rsrc)进行写操作而导致的异常,_BasepCurrentTopLevelFilter就改变出错页面正常的只读属性,以便允许进行写操作。如果是这种特殊的情况,UnhandledExceptionFilter返回EXCEPTION_CONTINUE_EXECUTION,使系统重新执行出错指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
UnhandledExceptionFilter(STRUCT _EXCEPTION_POINTERS *pExceptionPtrs)
{
PEXCEPTION_RECORD pExcptRec;
DWORD currentESP;
DWORD retValue;
DWORD DEBUGPORT;
DWORD dwTemp2;
DWORD dwUseJustInTimeDebugger;
CHAR szDbgCmdFmt[256]; // 从AeDebug这个注册表键值返回的字符串
CHAR szDbgCmdLine[256]; // 实际的调试器命令行参数(已填入进程ID和事件ID)
STARTUPINFO startupinfo;
PROCESS_INFORMATION pi;
HARDERR_STRUCT harderr; // ???
BOOL fAeDebugAuto;
TIB * pTib; // 线程信息块

pExcptRec = pExceptionPtrs->ExceptionRecord;

if ((pExcptRec->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
&& (pExcptRec->ExceptionInformation[0]))
{
retValue = BasepCheckForReadOnlyResource(pExcptRec->ExceptionInformation[1]);

if (EXCEPTION_CONTINUE_EXECUTION == retValue)
return EXCEPTION_CONTINUE_EXECUTION;
}

// 查看这个进程是否运行于调试器下
retValue = NtQueryInformationProcess(GetCurrentProcess(), ProcessDebugPort, &debugPort, sizeof(debugPort), 0);

if ((retValue >= 0) && debugPort) // 通知调试器
return EXCEPTION_CONTINUE_SEARCH;

// 用户调用SetUnhandledExceptionFilter了吗?
// 如果调用了,那现在就调用他安装的异常处理程序
if (_BasepCurrentTopLevelFilter)
{
retValue = _BasepCurrentTopLevelFilter(pExceptionPtrs);

if (EXCEPTION_EXECUTE_HANDLER == retValue)
return EXCEPTION_EXECUTE_HANDLER;
if (EXCEPTION_CONTINUE_EXECUTION == retValue)
return EXCEPTION_CONTINUE_EXECUTION;

// 只有返回值为EXCEPTION_CONTINUE_SEARCH时才会继续执行下去
}

// 调用过SetErrorMode(SEM_NOGPFAULTERRORBOX)吗?
if (0 == (GetErrorMode() & SEM_NOGPFAULTERRORBOX))
{
harderr.elem0 = pExcptRec->ExceptionCode;
harderr.elem1 = pExcptRec->ExceptionAddress;

if (EXCEPTION_IN_PAGE_ERROR == pExcptRec->ExceptionCode)
harderr.elem2 = pExcptRec->ExceptionInformation[2];
else
harderr.elem2 = pExcptRec->ExceptionInformation[0];

dwTemp2 = 1;
fAeDebugAuto = FALSE;

harderr.elem3 = pExcptRec->ExceptionInformation[1];

pTib = FS:[18h];

DWORD someVal = pTib->pProcess->0xC;

if (pTib->threadID != someVal)
{
__try
{
char szDbgCmdFmt[256];
retValue = GetProfileStringA("AeDebug", "Debugger", 0, szDbgCmdFmt, sizeof(szDbgCmdFmt) - 1);

if (retValue)
dwTemp2 = 2;

char szAuto[8];

retValue = GetProfileStringA("AeDebug", "Auto", "0", szAuto, sizeof(szAuto) - 1);

if (retValue)
if (0 == strcmp(szAuto, "1"))
if (2 == dwTemp2)
fAeDebugAuto = TRUE;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
ESP = currentESP;
dwTemp2 = 1;
fAeDebugAuto = FALSE;
}
}

if (FALSE == fAeDebugAuto)
{
retValue = NtRaiseHardError(STATUS_UNHANDLED_EXCEPTION | 0x10000000, 4, 0, &harderr, _BasepAlreadyHadHardError ? 1 : dwTemp2, &dwUseJustInTimeDebugger);
}
else
{
dwUseJustInTimeDebugger = 3;
retValue = 0;
}

if (retValue >= 0 && (dwUseJustInTimeDebugger == 3) && (!_BasepAlreadyHadHardError) && (!_BaseRunningInServerProcess))
{
_BasepAlreadyHadHardError = 1;

SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr), 0, TRUE };

HANDLE hEvent = CreateEventA(&secAttr, TRUE, 0, 0);

memset(&startupinfo, 0, sizeof(startupinfo));

sprintf(szDbgCmdLine, szDbgCmdFmt, GetCurrentProcessId(), hEvent);

startupinfo.cb = sizeof(startupinfo);
startupinfo.lpDesktop = "Winsta0\Default";

CsrIdentifyAlertableThread(); // ???

retValue = CreateProcessA(
0, // 应用程序名称
szDbgCmdLine, // 命令行
0, 0, // 进程和线程安全属性
1, // bInheritHandles
0, 0, // 创建标志、环境
0, // 当前目录
&statupinfo, // STARTUPINFO
&pi); // PROCESS_INFORMATION

if (retValue && hEvent)
{
NtWaitForSingleObject(hEvent, 1, 0);
return EXCEPTION_CONTINUE_SEARCH;
}
}

if (_BasepAlreadyHadHardError)
NtTerminateProcess(GetCurrentProcess(), pExcptRec->ExceptionCode);
}

return EXCEPTION_EXECUTE_HANDLER;
}

LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter)
{
// _BasepCurrentTopLevelFilter是KERNEL32.DLL中的一个全局变量
LPTOP_LEVEL_EXCEPTION_FILTER previous = _BasepCurrentTopLevelFilter;

// 设置为新值
_BasepCurrentTopLevelFilter = lpTopLevelExceptionFilter;

return previous; // 返回以前的值
}

简而言之,UnhandledExceptionFilter先确定进程是否在调试器下,如果处于调试状态则唤醒调试器,告诉它被调试程序产生了一个异常。如果没有,UnhandledExceptionFilter接下来调用用户安装的未处理异常过滤器(如果存在的话)。通常情况下,用户并没有安装回调函数,但是用户可以调用 SetUnhandledExceptionFilter这个API来安装。上面我也提供了这个API的伪代码。这个函数只是简单地用用户安装的回调函数的地址来替换一个全局变量,并返回替换前的值。
有了初步的准备之后,UnhandledExceptionFilter就开始做它的主要工作:用一个令人血压骤升的应用程序错误对话框来通知你犯了低级的编程错误。 (原文是“时髦的应用程序错误对话框”,但👴觉得现在的形容词更贴切一些) UnhandledExceptionFilter调用NTDLL.DLL中的 NtRaiseHardError函数。正是这个函数产生了应用程序错误对话框。这个对话框等待你单击“确定”按钮来终止进程,或者单击“取消”按钮来调试它。
如果你单击“确定”,UnhandledExceptionFilter就返回EXCEPTION_EXECUTE_HANDLER。调用UnhandledExceptionFilter 的进程通常通过终止自身来作为响应(正像你在BaseProcessStart的伪代码中看到的那样)。这就产生了一个有趣的问题——大多数人都认为是系统终止了产生未处理异常的进程,而实际上更准确的说法应该是,系统进行了一些设置使得产生未处理异常的进程将自身终止掉了。
UnhandledExceptionFilter执行时真正有意思的部分是当你单击应用程序错误对话框中的“取消”按钮,此时系统将调试器附加(attach)到出错进程上。这段代码首先调用 CreateEvent来创建一个事件内核对象,调试器成功附加到出错进程之后会将此事件对象变成有信号状态。这个事件句柄以及出错进程的ID都被传到sprintf函数,由它将其格式化成一个命令行,用来启动调试器。一切就绪之后,UnhandledExceptionFilter就调用 CreateProcess来启动调试器。如果CreateProcess成功,它就调用NtWaitForSingleObject来等待前面创建的那个事件对象。此时这个调用被阻塞,直到调试器进程将此事件变成有信号状态,以表明它已经成功附加到出错进程上。 UnhandledExceptionFilter函数中还有一些其它的代码,我在这里只讲重要的。

最后

如果你已经走了这么远,不把整个过程讲完对你有点不公平。我已经讲了当异常发生时操作系统是如何调用用户定义的回调函数的。我也讲了这些回调的内部情况,以及编译器是如何使用它们来实现__try和__except的。我甚至还讲了当某个异常没有被处理时所发生的情况以及系统所做的扫尾工作。剩下的就只有异常回调过程最初是从哪里开始的这个问题了。好吧,让我们深入系统内部来看一下结构化异常处理的开始阶段吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
KiUserExceptionDispatcher(PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext)
{
DWORD retValue;

// 注意:如果异常被处理,那么RtlDispatchException函数就不会返回
if (RtlDispatchException(pExceptRec, pContext))
retValue = NtContinue(pContext, 0);
else
retValue = NtRaiseException(pExceptRec, pContext, 0);

EXCEPTION_RECORD excptRec2;
excptRec2.ExceptionCode = retValue;
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.NumberParameters = 0;

RtlRaiseException(&excptRec2);
}

int RtlDispatchException(PEXCEPTION_RECORD pExcptRec, CONTEXT * pContext)
{
DWORD stackUserBase;
DWORD stackUserTop;
PEXCEPTION_REGISTRATION pRegistrationFrame;
DWORD hLog;

// 从FS:[4]和FS:[8]处获取堆栈的界限
RtlpGetStackLimits(&stackUserBase, &stackUserTop);

pRegistrationFrame = RtlpGetRegistrationHead();

while (-1 != pRegistrationFrame)
{
PVOID justPastRegistrationFrame = &pRegistrationFrame + 8;
if (stackUserBase > justPastRegistrationFrame)
{
pExcptRec->ExceptionFlags |= EH_STACK_INVALID;
return DISPOSITION_DISMISS; // 0
}

if (stackUsertop < justPastRegistrationFrame)
{
pExcptRec->ExceptionFlags |= EH_STACK_INVALID;
return DISPOSITION_DISMISS; // 0
}

if (pRegistrationFrame & 3) // 确保堆栈按DWORD对齐
{
pExcptRec->ExceptionFlags |= EH_STACK_INVALID;
return DISPOSITION_DISMISS; // 0
}

if (someProcessFlag)
{
hLog = RtlpLogExceptionHandler(pExcptRec, pContext, 0, pRegistrationFrame, 0x10);
}

DWORD retValue, dispatcherContext;

retValue = RtlpExecuteHandlerForException(pExcptRec, pRegistrationFrame,
pContext, &dispatcherContext,
pRegistrationFrame->handler);

if (someProcessFlag)
RtlpLogLastExceptionDisposition(hLog, retValue);

if (0 == pRegistrationFrame)
{
pExcptRec->ExceptionFlags &= ~EH_NESTED_CALL; // 关闭标志
}

EXCEPTION_RECORD excptRec2;

DWORD yetAnotherValue = 0;

if (DISPOSITION_DISMISS == retValue)
{
if (pExcptRec->ExceptionFlags & EH_NONCONTINUABLE)
{
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.ExceptionNumber = STATUS_NONCONTINUABLE_EXCEPTION;
excptRec2.ExceptionFlags = EH_NONCONTINUABLE;
excptRec2.NumberParameters = 0;
RtlRaiseException(&excptRec2);
}
else
return DISPOSITION_CONTINUE_SEARCH;
}
else if (DISPOSITION_CONTINUE_SEARCH == retValue)
{
}
else if (DISPOSITION_NESTED_EXCEPTION == retValue)
{
pExcptRec->ExceptionFlags |= EH_EXIT_UNWIND;
if (dispatcherContext > yetAnotherValue)
yetAnotherValue = dispatcherContext;
}
else // DISPOSITION_COLLIDED_UNWIND
{
excptRec2.ExceptionRecord = pExcptRec;
excptRec2.ExceptionNumber = STATUS_INVALID_DISPOSITION;
excptRec2.ExceptionFlags = EH_NONCONTINUABLE;
excptRec2.NumberParameters = 0;
RtlRaiseException(&excptRec2);
}

pRegistrationFrame = pRegistrationFrame->prev; // 转到前一个帧
}

return DISPOSITION_DISMISS;
}

_RtlpExecuteHandlerForException: // 处理异常(第一次)
MOV EDX, XXXXXXXX
JMP ExecuteHandler

RtlpExecutehandlerForUnwind : // 处理展开(第二次)
MOV EDX, XXXXXXXX

int ExecuteHandler(
PEXCEPTION_RECORD pExcptRec,
PEXCEPTION_REGISTRATION pExcptReg,
CONTEXT * pContext,
PVOID pDispatcherContext,
FARPROC handler) // 实际上是指向_except_handler()的指针
{
// 安装一个EXCEPTION_REGISTRATION帧,EDX指向相应的handler代码
PUSH EDX
PUSH FS : [0]
MOV FS : [0], ESP

// 调用异常处理回调函数
EAX = handler(pExcptRec, pExcptReg, pContext, pDispatcherContext);

// 移除EXCEPTION_REGISTRATION帧
MOV ESP, DWORD PTR FS : [00000000]
POP DWORD PTR FS : [00000000]

return EAX;
}

_RtlpExecuteHandlerForException使用的异常处理程序:
{
// 如果设置了展开标志,返回DISPOSITION_CONTINUE_SEARCH
// 否则,给pDispatcherContext赋值并返回DISPOSITION_NESTED_EXCEPTION
return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT
? DISPOSITION_CONTINUE_SEARC
: (*pDispatcherContext = pRegistrationFrame->scopetable,
DISPOSITION_NESTED_EXCEPTION);
}

_RtlpExecuteHandlerForUnwind使用的异常处理程序:
{
// 如果设置了展开标志,返回DISPOSITION_CONTINUE_SEARCH
// 否则,给pDispatcherContext赋值并返回DISPOSITION_COLLIDED_UNWIND
return pExcptRec->ExceptionFlags & EXCEPTION_UNWIND_CONTEXT
? DISPOSITION_CONTINUE_SEARCH
: (*pDispatcherContext = pRegistrationFrame->scopetable,
DISPOSITION_COLLIDED_UNWIND);
}

KiUserExceptionDispatcher的核心是对RtlDispatchException的调用。这拉开了搜索已注册的异常处理程序的序幕。如果某个处理程序处理这个异常并继续执行,那么对 RtlDispatchException的调用就不会返回。如果它返回了,只有两种可能:或者调用了NtContinue以便让进程继续执行,或者产生了新的异常。如果是这样,那异常就不能再继续处理了,必须终止进程。
现在把目光对准RtlDispatchException函数的代码,这就是我通篇提到的遍历异常帧的代码。这个函数获取一个指向EXCEPTION_REGISTRATION结构链表的指针,然后遍历此链表以寻找一个异常处理程序。由于堆栈可能已经被破坏了,所以这个例程非常谨慎。在调用每个EXCEPTION_REGISTRATION结构中指定的异常处理程序之前,它确保这个结构是按DWORD对齐的,并且是在线程的堆栈之中,同时在堆栈中比前一个EXCEPTION_REGISTRATION结构高。
RtlDispatchException并不直接调用EXCEPTION_REGISTRATION结构中指定的异常处理程序。相反,它调用 RtlpExecuteHandlerForException来完成这个工作。根据RtlpExecuteHandlerForException的执行情况,RtlDispatchException或者继续遍历异常帧,或者引发另一个异常。这第二次的异常表明异常处理程序内部出现了错误,这样就不能继续执行下去了。
RtlpExecuteHandlerForException的代码与RtlpExecuteHandlerForUnwind的代码极其相似。你可能会回忆起来在前面讨论展开时我提到过它。这两个“函数”都只是简单地给EDX寄存器加载一个不同的值然后就调用ExecuteHandler函数。也就是说,RtlpExecuteHandlerForException和RtlpExecuteHandlerForUnwind都是ExecuteHanlder这个公共函数的前端。
ExecuteHandler查找EXCEPTION_REGISTRATION结构的handler域的值并调用它。令人奇怪的是,对异常处理回调函数的调用本身也被一个结构化异常处 理程序封装着。在SEH自身中使用SEH看起来有点奇怪,但你思索一会儿就会理解其中的含义。如果在异常回调过程中引发了另外一个异常,操作系统需要知道这个情况。根据异常发生在最初的回调阶段还是展开回调阶段,ExecuteHandler或者返回DISPOSITION_NESTED_EXCEPTION,或者返回DISPOSITION_COLLIDED_UNWIND。这两者都是“红色警报!现在把一切都关掉!”类型的代码。
如果你像我一样,那不仅理解所有与SEH有关的函数非常困难,而且记住它们之间的调用关系也非常困难。为了帮助我自己记忆,我画了一个调用关系图(图15)。
现在要问:在调用ExecuteHandler之前设置EDX寄存器的值有什么用呢?这非常简单。如果ExecuteHandler在调用用户安装的异常处 理程序的过程中出现了什么错误,它就把EDX指向的代码作为原始的异常处理程序。它把EDX寄存器的值压入堆栈作为原始的 EXCEPTION_REGISTRATION结构的handler域。这基本上与我在MYSEH和MYSEH2中对原始的结构化异常处理的使用情况一 样。

结论

系统层面的SEH就是围绕着简单的回调在打转。如果你理解了回调的本质,在此基础上分层理解,系统层面的结构化异常处理也不是那么难掌握。
SEH太有意思🌶️!
SEH系列完结撒花!

关于本文

本文作者 云之君, 许可由 CC BY-NC 4.0.