这篇文章是早前跟某同事探讨Windows SEH中__finally实现时研究的内容, 根据某书介绍, 异常处理函数都是通过_EXCEPTION_REGISTRATION_RECORD内的回调函数Handler实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;
__try
{
...
}
__except( XXX )
{
// 这里是对应那个_EXCEPTION_REGISTRATION_RECORD 的第二个成员Handler
...
}

但是对于__finally却没有介绍具体的实现方式. 通过调试器trace不到__finally的调用路径.

限于当时搜商有限, 没有找到比较好的资料, 只能结合<<软件调试>>中介绍的内容自己进行摸索实验.
写了一段包含__try, __finally的简单代码进行反汇编, 发现VC对SEH做了些扩展, 往ExceptionList中添加的链表节点并不是书上描述的_EXCEPTION_REGISTRATION_RECORD,而是_EH3_EXCEPTION_REGISTRATION, 并且多向栈里压了8个字节. 同时异常处理函数也不是__except或者__finally里面的代码,而是统一的__except_handler3(老的编译器可能是__except_handler2, 还有那个某网站上贴的代码是__except_handler4, 不同版本VC扩展的结构不太一样, 但基本原理都差不多), 真正的__finally__except中的处理代码放在_EXCEPTION_REGISTRATION_RECORD里的_SCOPETABLE_ENTRY中:

1
2
3
4
5
6
7
8
struct _EH3_EXCEPTION_REGISTRATION
{
// 前两个字段同_EXCEPTION_REGISTRATION_RECORD
_EH3_EXCEPTION_REGISTRATION * Next;
void * ExceptionHandler;
_SCOPETABLE_ENTRY * ScopeTable;
DWORD TryLevel;
};
1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _SCOPETABLE_ENTRY
{
DWORD EnclosingLevel;
// 这个是过滤函数的地址, 就是__except(xxx)括号中得那部分代码.
// 可以是函数指针或者一个常数, 如果是常数的话一般是mov + ret这两条指令
// 另外如果FilterFunc是NULL, 那下面HandlerFunc中指向的就是__finally而不是__except
PVOID FilterFunc;
// 这里保存的是__finally或__except包含的代码块
PVOID HandlerFunc;
} SCOPETABLE_ENTRY, *PSCOPETABLE_ENTRY;

下面整段 __try, __finally的反汇编:

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
int func_in()
{
009F13C0 push ebp
009F13C1 mov ebp,esp
// 从这里开始向ExceptionList加入新的节点, 先压栈的是TryLevel
009F13C3 push 0FFFFFFFFh
// 接着压栈ScopeTable,保存了__finally里的代码段
009F13C5 push offset ___rtc_tzz+108h (9F6B40h)
// 后面压入的就是_EXCEPTION_REGISTRATION_RECORD里的两个字段, 首先是回调函数, 一直是__except_handler3
009F13CA push offset @ILT+115(__except_handler3) (9F1078h)
// 最后压入的是ExceptionList的头结点
009F13CF mov eax,dword ptr fs:[00000000h]
009F13D5 push eax
009F13D6 mov dword ptr fs:[0],esp
009F13DD add esp,0FFFFFF38h
009F13E3 push ebx
009F13E4 push esi
009F13E5 push edi
009F13E6 lea edi,[ebp-0D8h]
009F13EC mov ecx,30h
009F13F1 mov eax,0CCCCCCCCh
009F13F6 rep stos dword ptr es:[edi]
__try
009F13F8 mov dword ptr [ebp-4],0
{
printf("fin try!\n");
009F13FF mov esi,esp
009F1401 push offset string "fin try!\n" (9F574Ch)
009F1406 call dword ptr [__imp__printf (9F82C0h)]
009F140C add esp,4
009F140F cmp esi,esp
009F1411 call @ILT+320(__RTC_CheckEsp) (9F1145h)
__leave;
009F1416 jmp func_in+62h (9F1422h)
*(int*)0=0;
009F1418 mov dword ptr ds:[0],0
}
__finally
009F1422 mov dword ptr [ebp-4],0FFFFFFFFh
009F1429 call $LN5 (9F1430h)
009F142E jmp $LN8 (9F1448h)
{
printf("fin finally!\n");
// 从这里开始是__finally中代码块的起始地址, 注意地址是0x9F1430
009F1430 mov esi,esp
009F1432 push offset string "fin finally!\n" (9F573Ch)
009F1437 call dword ptr [__imp__printf (9F82C0h)]
009F143D add esp,4
009F1440 cmp esi,esp
009F1442 call @ILT+320(__RTC_CheckEsp) (9F1145h)
$LN6:
009F1447 ret
}
return 0;
009F1448 xor eax,eax
}

函数在进入__try块前, 首先在栈上安装了自己的异常处理函数, 并把__finally或者__except的放入ScopeTable做为参数同样压栈, 通过调试器分析压入的ScopeTable地址0x9F6B40处的内容:

SEH Memory Dump

0x9F6B40 + 8也就是_SCOPETABLE_ENTRY::HandlerFunc正好指向0x009f1430, 对应__finally里代码段的地址. 如果是__except, 那这里对应的就是__except中的代码段.

<<软件调试>>中有一部分对SEH的分析, 该书的确是本好书, 即使不做Windows开发拿来学习操作系统也是相当不错的. 并且作者十分负责, 出版之后发布了一个电子版的补编, 补全了很多没有写入书中的内容(包括该__finally的实现).