(译)MSVC++ 逆向(一) —— 异常处理

原文: http://www.openrce.org/articles/full_view/21

摘要

MSVC++ 是编写 Win32 应用程序最常用的编译器,所以在 Win32 平台的逆向工作中,懂得其底层工作原理,对逆向工程师来说至关重要。掌握 VC++ 程序的底层原理之后,便能在逆向过程中精准、快速识别编译器生成的胶水代码(Glue Code),这样可以让逆向工程师快速聚焦于二进制文件背后的真实程序和真实逻辑。另外,这对还原程序中高层次的结构(译注:面向对象的数据结构和程序组织结构、异常相关数据结构等)也有莫大帮助。

本文只是系列文章的上半部分(下半部分见: (译)MSVC++ 逆向(二)—— 类、方法和 RTTI),主要讲栈展开、异常处理以及 MSVC 编译生成相关的数据结构。阅读本文需要有汇编、寄存器和调用约定相关的知识储备,当然,MSVC++ 的编程基础知识也是必要的。

名词解释

  • 栈帧(Stack Frame) 中为函数所用的一个 片段,里面通常包含函数相关的参数(Arguments)、返回地址(Return-to-Caller Address)、保存的寄存器状态本地变量(Local Variables)和一些其他的数据。在 x86 架构(以及其他多数架构)中,栈里调用和被调用的函数,栈帧通常是连续的;
  • 帧指针(Frame Pointer):一个指向栈中特定位置的指针,指针值通常保存在寄存器或某个变量中。访问函数中的数据,一般通过帧指针和特定偏移量来实现。在 x86 架构里,帧指针通常保存在寄存器 ebp中,并且,在栈布局结构中位于返回地址的下方;
  • 对象(Object):C++ 类的实例;
  • 可销毁对象(Unwindable Object):又叫局部对象,一个具有 auto 作用域的本地对象,当帧指针访问其作用域之外的位置时该对象会被销毁(译注:函数内部本地变量的默认作用域就是 auto,函数被调用的时候其内部变量及其他数据被生成到栈上,调用完毕就会销毁这段栈的片段,其内部变量也就随之被销毁);
  • 栈展开(Stack Unwinding):触发异常的时候,将暂停当前函数的执行,根据 C++try/throw/catch 异常处理流程或 SEH 异常处理机制,会在栈段中线性搜索对应的异常处理函数,如果当前栈帧中没有相应的异常处理模块,就会退出当前函数,释放当前函数的内存并销毁局部对象,继续到上层调用函数中寻找对应的异常处理模块,直到找到可以处理该异常的模块……这个过程就是栈展开

在 C/C++ 程序中,可用的异常处理机制有两种:

  • SEH 异常:即结构化异常处理(Structured Exception Handling),也称作 Win32 异常系统异常,这部分内容在 Matt Pietrek 的 Paper[1] 里有详尽的讲解。该机制是 C 程序仅有的异常处理机制,在编译器层面支持的关键字有 __try/__except/finally 等等;
  • C++ 异常:实现于 SEH 链的顶层,并且支持任意类型的 throwcatch。该异常处理机制一个非常重要的特性是在异常处理过程中的自动栈展开,MSVC++ 为了支持这一特性,在底层做了非常复杂的处理。

译注
根据 VC++ 中的异常处理 | MSDN 所述,自 MFC3.0 起,MFC 程序中可以使用 MFC 特有的异常处理机制——MFC 异常

内存中,栈是由高地址向低地址方向增长的,所以在 IDA 中看到的栈,是向上增长的。

栈的内存布局

基础的栈布局如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
---------------
局部变量
---------------
其它寄存器的值
---------------
原栈基址 %ebp
---------------
返回地址
---------------
函数参数
---------------
……

具体点,如下图所示:
基础栈内存布局

NOTE:
如果设置了 FPO(Frame Pointer Omission, 框架指针省略),原栈基址 %ebp 可能就不存在了。

SEH

当涉及到编译器层面的 SEH__try/__except/__finally) 时,栈的内存布局就会变的复杂一些:
SEH3 站内存布局

一个函数中如果没有 __try 语句块(只有 __finally),Saved ESP 就不会存在。另外,作用域描述表scopetable )是一个记录每一个 try 块及其关系描述的数组:

1
2
3
4
5
struct _SCOPETABLE_ENTRY {
DWORD EnclosingLevel;
void* FilterFunc;
void* HandlerFunc;
}

更多 SEH 具体的实现细节可以查阅参考资料[1]。为了恢复 try 语句块,需要监控 try 层面的变量变化。 SEH 为每一个 try 语句块分配了一个编号,语句块之间的相互联系用上面的 scopetable 结构体来描述。举个栗子,假设编号为 iscopetable ,其中属性 EnclosingLevel 值为 j,那么编号为 jtry 语句块会把 i 闭合在自己的作用域内。然后该函数的 try level 可以认为是 -1 。具体例子可以参考附录1

栈越界(溢出)保护

Whidbey 编译器(即 MSVC-2005)为栈中的 SEH 帧添加了缓冲区溢出保护机制,如此一来,栈内存布局就变成了下图这样:
SEH4 栈内存布局

EH Cookie 会一直存在,而 GS cookie 段只有在编译时开启了 /GS 选项才会出现。SEH4 的作用域描述表( scopetable )跟 SEH3 的差不多,不同的是多了以下两组头部字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct _EH4_SCOPETABLE {
DWORD GSCookieOffset;
DWORD GSCookieXOROffset;
DWORD EHCookieOffset;
DWORD EHCookieXOROffset;
_EH4_SCOPETABLE_RECORD ScopeRecord[1];
};
struct _EH4_SCOPETABLE_RECORD {
DWORD EnclosingLevel;
long (*FilterFunc)();
union {
void (*HandlerAddress)();
void (*FinallyFunc)();
};
};

GSCookieOffset = -2 表示没有启用 GS cookieEH cookie 会一直启用,并且访问时用到的偏移都是相对于 %ebp 来计算的。对 security_cookie 的校验方式为:

1
(ebp+CookieXOROffset) ^ [ebp+CookieOffset] == _security_cookie

栈中指向 scopetable 的指针也要与 _security_cookie 进行异或计算。另外,SEH4 中最外层的作用域层级(scope level)是 -2 ,而不是像 SEH3 中那样的 -1

C++ 异常模型实现

如果函数中实现了 C++ 的异常处理,或者可销毁的局部对象,栈的内存布局就会变得更加复杂起来:
C++ 异常n内存布局

不同于 SEH,C++ 每一个函数中的异常处理内存布局都不相同,通常是如下形式:

1
2
3
// (VC7+)
mov eax, OFFSET __ehfuncinfo
jmp ___CxxFrameHandler

其中,__ehfuncinfo 是一个 FuncInfo 结构对象,该结构中囊括了函数中所有的 try/catch 块的描述以及可销毁对象信息:

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
struct FuncInfo {
// 编译器版本
// 0x19930520: 低于 VC6; 0x19930521: VC7.x(2002-2003);0x19930522: VC8 (2005)
DWORD magicNumber;
// 栈展开描述表中的入口数量
// number of entries in unwind table
int maxState;
// 栈展开处理方法绑定表
// table of unwind destructors
UnwindMapEntry* pUnwindMap;
// 函数中的 try 语句块数量
DWORD nTryBlocks;
// try-catch 映射表
// mapping of catch blocks to try blocks
TryBlockMapEntry* pTryBlockMap;
// x86 架构上不可用
// not used on x86
DWORD nIPMapEntries;
// not used on x86
void* pIPtoStateMap;
// VC7 及以上版本可用,期望异常列表
// VC7+ only, expected exceptions list (function "throw" specifier)
ESTypeList* pESTypeList;
// VC8 及以上版本可用,但以 /EHs 选项编译时会置 零
// VC8+ only, bit 0 set if function was compiled with /EHs
int EHFlags;
};

栈展开映射(Unwind map)类似 SEH 作用域描述表(SEH scopetable),只是少了过滤函数:

1
2
3
4
struct UnwindMapEntry {
int toState; // target state
void (*action)(); // 栈展开时调用的处理函数
};

try 语句块描述结构体,描述一个 try{} 块对应的 catch{} 块的映射信息:

1
2
3
4
5
6
7
struct TryBlockMapEntry {
int tryLow;
int tryHigh; // this try {} covers states ranging from tryLow to tryHigh
int catchHigh; // highest state inside catch handlers of this try
int nCatches; // number of catch handlers
HandlerType* pHandlerArray; //catch handlers table
};

catch 语句块描述表,描述对应某个 catch{} 块的单个 try{} 块的相关信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct HandlerType {
// 0x01: const, 0x02: volatile, 0x08: reference
DWORD adjectives;
// RTTI descriptor of the exception type. 0=any (ellipsis)
TypeDescriptor* pType;
// ebp-based offset of the exception object in the function stack.
// 0 = no object (catch by type)
int dispCatchObj;
// address of the catch handler code.
// returns address where to continues execution (i.e. code after the try block)
void* addressOfHandler;
};

期望异常列表(MSVC 中默认关闭,需要用 /d1ESrt 编译选项开启):

1
2
3
4
5
6
7
struct ESTypeList {
// number of entries in the list
int nCount;
// list of exceptions; it seems only pType field in HandlerType is used
HandlerType* pTypeArray;
};

RTTIRun-Time Type Information,运行时类型识别)类型描述表,描述 C++ 中的类型信息,这里会用 catch 块中的类型去匹配 throw 出来的异常的类型:

1
2
3
4
5
6
7
8
9
10
struct TypeDescriptor {
// vtable of type_info class
const void * pVFTable;
// used to keep the demangled name returned by type_info::name()
void* spare;
// mangled type name, e.g. ".H" = "int", ".?AUA@@" = "struct A", ".?AVA@@" = "class A"
char name[0];
};

前面说过,不同于 SEH,C++ 每一个函数中的异常处理内存布局都不相同。编译器不仅会在 进/出 try 语句块的时候改变状态值,在创建/销毁一个对象的时候状态值也会做出相应改变。这样一来,异常被触发的时候就可以知道哪一个对象应该被栈展开而销毁。并且,我们还可以通过检查相关状态变化和 try 语句块处理句柄的返回地址来最终恢复 try 语句块的边界(详见 附录2)。

抛出 C++ 异常

C++ 中的 throw 表达式在底层会转换为对 _CxxThrowException() 的调用,这个调用会以特征码 0xE06D7363'msc'|0xE0000000) 抛出一个 Win32 异常(即 SEH 异常)。SEH 异常的自定义参数里有异常对象及其对应的 ThrowInfo 结构体对象,其中 ThrowInfo 结构描述了被抛出来的异常的类型,异常处理句柄可以拿此类型与 catch 块中的期望异常类型做匹配检索。下面是 ThrowInfo 的结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct ThrowInfo {
// 0x01: const, 0x02: volatile
DWORD attributes;
// exception destructor
void (*pmfnUnwind)();
// forward compatibility handler
int (*pForwardCompat)();
// list of types that can catch this exception.
// i.e. the actual type and all its ancestors.
CatchableTypeArray* pCatchableTypeArray;
};
struct CatchableTypeArray {
// number of entries in the following array
int nCatchableTypes;
CatchableType* arrayOfCatchableTypes[0];
};

其中,CatchableType 定义了可以 catch 这种异常的类型:

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
struct CatchableType {
// 0x01: simple type (can be copied by memmove), 0x02: can be caught by reference only, 0x04: has virtual bases
DWORD properties;
// see above
TypeDescriptor* pType;
// how to cast the thrown object to this type
PMD thisDisplacement;
// object size
int sizeOrOffset;
// copy constructor address
void (*copyFunction)();
};
// Pointer-to-member descriptor.
struct PMD {
// member offset
int mdisp;
// offset of the vbtable (-1 if not a virtual base)
int pdisp;
// offset to the displacement value inside the vbtable
int vdisp;
};

我们会在下一篇深入阐述这一方面的内容。

序言结语(Prologs & Epilogs)

为了避免向函数体部分注入设置栈帧的代码,编译器通常会选择用一些序言Prologs)和结语epilog)函数做一些处理。不过形式多种多样,不同类型的序言或结语作用于不同的函数类型:

Name Type EH Cookie GS Cookie Catch Handlers
_SEH_prolog/_SEH_epilog SEH3 - -
_SEH_prolog4/_SEH_epilog4 S EH4 + -
_SEH_prolog4_GS/_SEH_epilog4_GS SEH4 + +
_EH_prolog C++ EH - - +/-
_EH_prolog3/_EH_epilog3 C++ EH + - -
_EH_prolog3_catch/_EH_epilog3 C++ EH + - +
_EH_prolog3_GS/_EH_epilog3_GS C++ EH + + -
_EH_prolog3_catch_GS/_EH_epilog3_catch_GS C++ EH + + +

SEH2

当然,这个在早期的 MSVC1.xx 中才会用到(从 crtdll.dll 中导出),在一些运行在旧版 NT 上的程序中会碰到。其内存布局如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Saved edi
Saved esi
Saved ebx
Next SEH frame
Current SEH handler (__except_handler2)
Pointer to the scopetable
Try level
Saved ebp (of this function)
Exception pointers
Local variables
Saved ESP
Local variables
Callee EBP
Return address
Function arguments

附录 1:SEH 程序示例

参考以下反汇编出来的结果:

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
func1 proc near
_excCode = dword ptr -28h
buf = byte ptr -24h
_saved_esp = dword ptr -18h
_exception_info = dword ptr -14h
_next = dword ptr -10h
_handler = dword ptr -0Ch
_scopetable = dword ptr -8
_trylevel = dword ptr -4
str = dword ptr 8
push ebp
mov ebp, esp
push -1
push offset _func1_scopetable
push offset _except_handler3
mov eax, large fs:0
push eax
mov large fs:0, esp
add esp, -18h
push ebx
push esi
push edi
; --- end of prolog ---
mov [ebp+_trylevel], 0 ;trylevel -1 -> 0: beginning of try block 0
mov [ebp+_trylevel], 1 ;trylevel 0 -> 1: beginning of try block 1
mov large dword ptr ds:123, 456
mov [ebp+_trylevel], 0 ;trylevel 1 -> 0: end of try block 1
jmp short _endoftry1
_func1_filter1: ; __except() filter of try block 1
mov ecx, [ebp+_exception_info]
mov edx, [ecx+EXCEPTION_POINTERS.ExceptionRecord]
mov eax, [edx+EXCEPTION_RECORD.ExceptionCode]
mov [ebp+_excCode], eax
mov ecx, [ebp+_excCode]
xor eax, eax
cmp ecx, EXCEPTION_ACCESS_VIOLATION
setz al
retn
_func1_handler1: ; beginning of handler for try block 1
mov esp, [ebp+_saved_esp]
push offset aAccessViolatio ; "Access violation"
call _printf
add esp, 4
mov [ebp+_trylevel], 0 ;trylevel 1 -> 0: end of try block 1
_endoftry1:
mov edx, [ebp+str]
push edx
lea eax, [ebp+buf]
push eax
call _strcpy
add esp, 8
mov [ebp+_trylevel], -1 ; trylevel 0 -> -1: end of try block 0
call _func1_handler0 ; execute __finally of try block 0
jmp short _endoftry0
_func1_handler0: ; __finally handler of try block 0
push offset aInFinally ; "in finally"
call _puts
add esp, 4
retn
_endoftry0:
; --- epilog ---
mov ecx, [ebp+_next]
mov large fs:0, ecx
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
retn
func1 endp
_func1_scopetable
;try block 0
dd -1 ;EnclosingLevel
dd 0 ;FilterFunc
dd offset _func1_handler0 ;HandlerFunc
;try block 1
dd 0 ;EnclosingLevel
dd offset _func1_filter1 ;FilterFunc
dd offset _func1_handler1 ;HandlerFunc

注意,上面的 0try 块并没有过滤器,所以它的处理句柄是 __finally{} 语句块。try1EnclosingLevel0,所以它被 0try 块所闭合。由此以来,我们可以大概构造出上面程序的大概结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void func1 (char* str)
{
char buf[12];
__try // try block 0
{
__try // try block 1
{
*(int*)123=456;
}
__except(GetExceptCode() == EXCEPTION_ACCESS_VIOLATION)
{
printf("Access violation");
}
strcpy(buf,str);
}
__finally
{
puts("in finally");
}
}

附录 2:带 SEH 异常的 C++ 程序示例

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
func1 proc near
_a1 = dword ptr -24h
_exc = dword ptr -20h
e = dword ptr -1Ch
a2 = dword ptr -18h
a1 = dword ptr -14h
_saved_esp = dword ptr -10h
_next = dword ptr -0Ch
_handler = dword ptr -8
_state = dword ptr -4
push ebp
mov ebp, esp
push 0FFFFFFFFh
push offset func1_ehhandler
mov eax, large fs:0
push eax
mov large fs:0, esp
push ecx
sub esp, 14h
push ebx
push esi
push edi
mov [ebp+_saved_esp], esp
; --- end of prolog ---
lea ecx, [ebp+a1]
call A::A(void)
mov [ebp+_state], 0 ; state -1 -> 0: a1 constructed
mov [ebp+a1], 1 ; a1.m1 = 1
mov byte ptr [ebp+_state], 1 ; state 0 -> 1: try {
lea ecx, [ebp+a2]
call A::A(void)
mov [ebp+_a1], eax
mov byte ptr [ebp+_state], 2 ; state 2: a2 constructed
mov [ebp+a2], 2 ; a2.m1 = 2
mov eax, [ebp+a1]
cmp eax, [ebp+a2] ; a1.m1 == a2.m1?
jnz short loc_40109F
mov [ebp+_exc], offset aAbc ; _exc = "abc"
push offset __TI1?PAD ; char *
lea ecx, [ebp+_exc]
push ecx
call _CxxThrowException ; throw "abc";
loc_40109F:
mov byte ptr [ebp+_state], 1 ; state 2 -> 1: destruct a2
lea ecx, [ebp+a2]
call A::~A(void)
jmp short func1_try0end
; catch (char * e)
func1_try0handler_pchar:
mov edx, [ebp+e]
push edx
push offset aCaughtS ; "Caught %s\n"
call ds:printf ;
add esp, 8
mov eax, offset func1_try0end
retn
; catch (...)
func1_try0handler_ellipsis:
push offset aCaught___ ; "Caught ...\n"
call ds:printf
add esp, 4
mov eax, offset func1_try0end
retn
func1_try0end:
mov [ebp+_state], 0 ; state 1 -> 0: }//try
push offset aAfterTry ; "after try\n"
call ds:printf
add esp, 4
mov [ebp+_state], -1 ; state 0 -> -1: destruct a1
lea ecx, [ebp+a1]
call A::~A(void)
; --- epilog ---
mov ecx, [ebp+_next]
mov large fs:0, ecx
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
retn
func1 endp
func1_ehhandler proc near
mov eax, offset func1_funcinfo
jmp __CxxFrameHandler
func1_ehhandler endp
func1_funcinfo
dd 19930520h ; magicNumber
dd 4 ; maxState
dd offset func1_unwindmap ; pUnwindMap
dd 1 ; nTryBlocks
dd offset func1_trymap ; pTryBlockMap
dd 0 ; nIPMapEntries
dd 0 ; pIPtoStateMap
dd 0 ; pESTypeList
func1_unwindmap
dd -1
dd offset func1_unwind_1tobase ; action
dd 0 ; toState
dd 0 ; action
dd 1 ; toState
dd offset func1_unwind_2to1 ; action
dd 0 ; toState
dd 0 ; action
func1_trymap
dd 1 ; tryLow
dd 2 ; tryHigh
dd 3 ; catchHigh
dd 2 ; nCatches
dd offset func1_tryhandlers_0 ; pHandlerArray
dd 0
func1_tryhandlers_0
dd 0 ; adjectives
dd offset char * 'RTTI Type Descriptor' ; pType
dd -1Ch ; dispCatchObj
dd offset func1_try0handler_pchar ; addressOfHandler
dd 0 ; adjectives
dd 0 ; pType
dd 0 ; dispCatchObj
dd offset func1_try0handler_ellipsis ; addressOfHandler
func1_unwind_1tobase proc near
a1 = byte ptr -14h
lea ecx, [ebp+a1]
call A::~A(void)
retn
func1_unwind_1tobase endp
func1_unwind_2to1 proc near
a2 = byte ptr -18h
lea ecx, [ebp+a2]
call A::~A(void)
retn
func1_unwind_2to1 endp

我们来看上述程序示例,FuncInfo 结构体中的 maxState 域值为 4,这表明栈展开映射表中有 4 个入口点,编号 0-3 。检查映射表,可以看到在栈展开过程中会执行的相应动作有以下 4 个:

  • state 3 -> state 0 (NOP)
  • state 2 -> state 1 (析构 a2 )
  • state 1 -> state 0 (NOP)
  • state 0 -> state -1 (析构 a1 )

再看 try 语句映射表,我们可以推断, state 1state2 对应 try 语句块的执行逻辑,而 sttate3 对应 catch 语句块的执行逻辑。这样一来,state 0 -> 1 的变化代表了 try 语句块的开始state 1 - >0 代表 try 语句块的结束。另外,我们还可以推断 state -1 --> 0 代表创建 a1state 1 -> 2 代表创建 a2。具体的状态装换和相应的程序执行逻辑如下图所示:

看到这里可能会心生疑惑:1 -> 3 那个箭头是从哪儿来的?其实这是在异常处理句柄内部发生的,我们从函数代码FuncInfo 结构体中都看不出来罢了。如果一个异常在 try 块内部被触发,异常处理句柄在调用相应的 catch 块之前,首先要做的就是把栈展开到 tryLow 那一层(上面例子中的 state 1),然后把状态值 state 设置为 tryHigh+1(即 2+1=3)。

try 语句块对应 2 个 catch 句柄。第一个有一个 catch 的类型( char* ),并且在栈 -1Ch 处获取到异常对象。第二个没有对应的异常类型,什么也不做,相当于忽略异常。两个句柄都会返回函数继续执行的地址,这个地址其实就紧随 try 块之后。这样我们试着还原一下该函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void func1 ()
{
A a1;
a1.m1 = 1;
try {
A a2;
a2.m1 = 2;
if (a1.m1 == a1.m2) throw "abc";
}
catch(char* e)
{
printf("Caught %s\n",e);
}
catch(...)
{
printf("Caught ...\n");
}
printf("after try\n");
}

附录 3: IDC 辅助脚本

我编写了一个 IDC 脚本用来辅助对 MSVC 程序的逆向分析。它会在整个程序中搜索 SEH/EH 的代码段,并且为所有相关的结构体和结构体元素添加注释。可以被注释的项有栈变量异常处理句柄异常类型及其他相关元素。它还能尝试修复 IDA 中误判的函数边界。该脚本的下载链接: MS SEH/EH 逆向辅助脚本

参考资料

原文参考资料:

  1. http://www.microsoft.com/msj/0197/exception/exception.aspx
  2. http://blogs.msdn.com/branbray/archive/2003/11/11/51012.aspx
  3. http://blogs.msdn.com/cbrumme/archive/2003/10/01/51524.aspx
  4. http://www.codeproject.com/cpp/exceptionhandler.asp
  5. http://www.cs.arizona.edu/computer.help/policy/DIGITAL_unix/AA-PY8AC-TET1_html/callCH5.html

翻译参考资料

  1. http://www.cnblogs.com/samo/articles/3092895.html
  2. http://www.cnblogs.com/Winston/archive/2009/04/19/1439184.html
  3. https://msdn.microsoft.com/en-us/library/8dbf701c(v=vs.80).aspx
  4. http://www.nynaeve.net/?p=91
  5. http://www.cnblogs.com/awpatp/archive/2009/11/04/1595988.html
  6. https://en.wikibooks.org/wiki/C%2B%2B_Programming/RTTI