August 29, 2022

C++异常处理

C++异常初探(当然了,自己学的还不是很明白

原文地址:
C++异常处理(1)
C++异常处理(2)

大多是对原文的一些总结和引用,建议阅读原文。后面的一些测试🌰是自己思考的一些东西。

异常处理过程

  1. 抛出异常
  2. 若没有在当前函数内被catch,就沿着调用链继续往上抛,直到走完整个调用链
  3. 如果走完调用链都没有找到相应的 catch,那么 std::terminate() 就会被调用(用于直接终止程序);如果找到相应的catch,就进入该catch执行相应的代码(称为Landing pad)。

stack unwind(栈展开):从抛异常开始到执行landing pad的整个过程。
栈展开做的工作就是从抛出异常的地方开始,沿调用链找catch,然后从抛异常的地方开始,清理调用链上栈帧里的局部变量。
栈展开可以简单看成函数调用的逆过程。
这个过程在实现上由一个专门的 stack unwind 库来进行,在 intel 平台上,它属于 Itanium ABI 接口中的一部分,且与具体的语言无关,由系统提供实现,任何上层语言都可以在这个接口的基础上实现各自的异常处理,GCC 就基于这个接口来实现 c++ 的异常处理。

Itanium C++ ABI

Itanium ABI 定义了一系列函数及相应的数据结构来建立整个异常处理的流程及框架。

_Unwind_RaiseException()

_Unwind_RaiseException() 函数用于进行 stack unwind,它在用户执行 throw 时被调用,主要功能是从当前函数开始,对调用链上每个函数都调用一个叫作 personality routine 的函数(__gxx_personality_v0),该函数由上层的语言定义及提供实现。
_Unwind_RaiseException() 会在内部把当前函数栈的调用现场重建,然后传给 personality routine
personality routine主要负责做两件事情:

1
2
3
4
5
6
7
_Unwind_Reason_Code (*__personality_routine)
(int version,
_Unwind_Action actions,//用来告诉 personality routine,当前处于 stack unwind 的哪个阶段
uint64 exceptionClass,
struct _Unwind_Exception *exceptionObject,
struct _Unwind_Context *context);
//它的参数则主要用来传递与异常相关的信息及当前函数的上下文

每个函数在 unwind 的过程中都会被 personality routine 遍历两次。
一次是寻找catch,一次是清理局部变量。

如下伪代码展示了 _Unwind_RaiseException() 内部的大概实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_Unwind_RaiseException(exception)
{
bool found = false;
while (1)
{
// 建立上个函数的上下文
context = build_context();
if (!context) break;
found = personality_routine(exception, context, SEARCH);
if (found or reach the end) break;
}

while (found)
{
context = build_context();
if (!context) break;
personality_routine(exception, context, UNWIND);
if (reach_catch_function) break;
}
}

ABI 中的函数使用到了两个自定义的数据结构,用来传递一些内部的信息。

1
2
3
4
5
6
7
8
struct _Unwind_Context;//一个对调用者透明的结构,用于表示程序运行时的上下文

struct _Unwind_Exception {//在 unwind 库内用于表示一个异常
uint64 exception_class;
_Unwind_Exception_Cleanup_Fn exception_cleanup;
uint64 private_1;
uint64 private_2;
};

C++ ABI

基于前面介绍的 Itanium ABI,编译器层面也定义了一系列的 ABI 来与之交互。当我们在代码中写下 “throw xxx” 时,编译器会分配一个数据结构来表示该异常,该异常有一个头部,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct __cxa_exception { 
std::type_info * exceptionType;
void (*exceptionDestructor) (void *);
unexpected_handler unexpectedHandler;
terminate_handler terminateHandler;
__cxa_exception * nextException;

int handlerCount;
int handlerSwitchValue;
const char * actionRecord;
const char * languageSpecificData;
void * catchTemp;
void * adjustedPtr;

_Unwind_Exception unwindHeader;//Itanium 接口里提到的接口内部用的结构体
};

当用户throw一个异常,编译器会调用相应的函数分配出如下一个结构:

__cxa_exception
exception obj

其中 _cxa_exception 就是头部,exception_obj 则是 “throw xxx” 中的 xxx,这两部分在内存中是连续的。异常对象由函数 __cxa_allocate_exception() 进行创建,最后由 __cxa_free_exception() 进行销毁。

当我们在程序里执行了抛出异常后,编译器为我们做了如下的事情:

  1. 调用 __cxa_allocate_exception 函数,分配一个异常对象。
  2. 调用 __cxa_throw 函数,这个函数会将异常对象做一些初始化。
  3. __cxa_throw() 调用 Itanium ABI 里的 _Unwind_RaiseException() 从而开始 unwind。
  4. _Unwind_RaiseException() 对调用链上的函数进行 unwind 时,调用 personality routine。
  5. 该异常如能被处理(有相应的 catch),则 personality routine 会依次对调用链上的函数进行清理。
  6. _Unwind_RaiseException() 将控制权转到相应的catch代码。
  7. unwind 完成,用户代码继续执行。

不同编译器的实现

g++ x64

测试代码:

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
#include <stdio.h>


void test_func3()
{
throw 3;

puts( "test func3" );
}

void test_func2()
{
puts( "test func2" );
try
{
test_func3();
}
catch (int)
{
puts( "catch 2" );
}
}

void test_func1()
{
puts( "test func1" );
try
{
test_func2();
}
catch (...)
{
puts( "catch 1" );
}
}

int main()
{
test_func1();
return 0;
}
/* output:
test func1
test func2
catch 2
*/

fun3的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.text:0000000000401550 ; =============== S U B R O U T I N E =======================================
.text:0000000000401550
.text:0000000000401550 ; Attributes: noreturn bp-based frame
.text:0000000000401550
.text:0000000000401550 ; void __cdecl test_func3()
.text:0000000000401550 public _Z10test_func3v
.text:0000000000401550 _Z10test_func3v proc near ; CODE XREF: test_func2(void)+1A↓p
.text:0000000000401550 ; DATA XREF: .pdata:000000000040506C↓o
.text:0000000000401550 push rbp
.text:0000000000401551 mov rbp, rsp
.text:0000000000401554 sub rsp, 20h
.text:0000000000401558 mov ecx, 4 ; thrown_size
.text:000000000040155D call __cxa_allocate_exception
.text:0000000000401562 mov dword ptr [rax], 3
.text:0000000000401568 mov r8d, 0 ; void (__fastcall *)(void *)
.text:000000000040156E mov rdx, cs:_refptr__ZTIi ; lptinfo
.text:0000000000401575 mov rcx, rax ; void *
.text:0000000000401578 call __cxa_throw
.text:0000000000401578 ; ---------------------------------------------------------------------------
.text:000000000040157D align 2
.text:000000000040157D _Z10test_func3v endp

可以看到fun3中的throw对应了上述两个过程,即调用__cxa_allocate_exception分配一个异常对象,以及调用 __cxa_throw 函数对异常对象初始化。

fun2的结构:

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
.text:000000000040157E ; void __cdecl test_func2()
.text:000000000040157E public _Z10test_func2v
.text:000000000040157E _Z10test_func2v proc near ; CODE XREF: test_func1(void)+1A↓p
.text:000000000040157E ; DATA XREF: .pdata:000000000040506C↓o ...
.text:000000000040157E
.text:000000000040157E var_14 = dword ptr -14h
.text:000000000040157E
.text:000000000040157E ; __unwind { // __gxx_personality_seh0
.text:000000000040157E push rbp
.text:000000000040157F push rbx
.text:0000000000401580 sub rsp, 38h
.text:0000000000401584 lea rbp, [rsp+80h]
.text:000000000040158C lea rcx, Buffer ; "test func2"
.text:0000000000401593 call puts
.text:0000000000401598 ; try {
.text:0000000000401598 call _Z10test_func3v ; test_func3(void)
.text:0000000000401598 ; } // starts at 401598
.text:000000000040159D ; ---------------------------------------------------------------------------
.text:000000000040159D jmp short loc_4015E1
.text:000000000040159F ; ---------------------------------------------------------------------------
.text:000000000040159F ; catch(int) // owned by 401598
.text:000000000040159F cmp rdx, 1
.text:00000000004015A3 jz short loc_4015AD
.text:00000000004015A5 mov rcx, rax
.text:00000000004015A8 call _Unwind_Resume
.text:00000000004015AD ; ---------------------------------------------------------------------------
.text:00000000004015AD
.text:00000000004015AD loc_4015AD: ; CODE XREF: test_func2(void)+25↑j
.text:00000000004015AD mov rcx, rax ; void *
.text:00000000004015B0 call __cxa_begin_catch
.text:00000000004015B5 mov eax, [rax]
.text:00000000004015B7 mov [rbp-40h+var_14], eax
.text:00000000004015BA lea rcx, aCatch2 ; "catch 2"
.text:00000000004015C1 ; try {
.text:00000000004015C1 call puts
.text:00000000004015C1 ; } // starts at 4015C1
.text:00000000004015C6 call __cxa_end_catch
.text:00000000004015CB jmp short loc_4015E1
.text:00000000004015CD ; ---------------------------------------------------------------------------
.text:00000000004015CD ; cleanup() // owned by 4015C1
.text:00000000004015CD mov rbx, rax
.text:00000000004015D0 call __cxa_end_catch
.text:00000000004015D5 mov rax, rbx
.text:00000000004015D8 mov rcx, rax
.text:00000000004015DB call _Unwind_Resume
.text:00000000004015DB ; ---------------------------------------------------------------------------
.text:00000000004015E0 db 90h
.text:00000000004015E1 ; ---------------------------------------------------------------------------
.text:00000000004015E1
.text:00000000004015E1 loc_4015E1: ; CODE XREF: test_func2(void)+1F↑j
.text:00000000004015E1 ; test_func2(void)+4D↑j
.text:00000000004015E1 add rsp, 38h
.text:00000000004015E5 pop rbx
.text:00000000004015E6 pop rbp
.text:00000000004015E7 retn
.text:00000000004015E7 ; } // starts at 40157E
.text:00000000004015E7 _Z10test_func2v endp

try块包裹fun3,不抛出异常的话就跳转到loc_4015E1来正常结束函数。
抛出异常的话检测异常是否为int类型,不是的话调用_Unwind_Resume恢复现场,是的话调用__cxa_begin_catch开始处理异常。
调用puts的代码编译器自动生成了一个try块来包裹,下面的cleanup()基本上是相当于它的catch块吧。
若正常调用结束,就按流程往下走,调用__cxa_end_catch来结束异常处理,然后正常结束fun2;
若发生异常,就调用__cxa_end_catch来结束异常处理,调用_Unwind_Resume恢复现场,然后正常结束fun2。
注意此处__cxa_end_catch处理的应该是上层try块里的fun3中抛出的异常,不是puts函数抛出的异常。如果puts抛出异常,那就相当于是只执行到了抛异常的部分。而cleanup()中的_Unwind_Resume恢复的现场是由puts抛出的异常。
这样就能够处理catch块中又抛出异常的情况。

g++ x86

与x64基本一样。

MSVC x64

康不懂,爬了

MSVC x86

跟这个不一样,学了再来补。

先贴个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.text:00401100 ?test_func3@@YAXXZ proc near            ; CODE XREF: test_func2(void)+3E↑p
.text:00401100
.text:00401100 pExceptionObject= dword ptr -4
.text:00401100
.text:00401100 push ebp
.text:00401101 mov ebp, esp
.text:00401103 push ecx
.text:00401104 mov [ebp+pExceptionObject], 3
.text:0040110B push offset __TI1H ; pThrowInfo
.text:00401110 lea eax, [ebp+pExceptionObject]
.text:00401113 push eax ; pExceptionObject
.text:00401114 call __CxxThrowException@8 ; _CxxThrowException(x,x)
.text:00401114 ?test_func3@@YAXXZ endp
.text:00401114
.text:00401119 ; ---------------------------------------------------------------------------
.text:00401119 push offset aTestFunc3 ; "test func3"
.text:0040111E call ds:__imp__puts
.text:00401124 add esp, 4
.text:00401127 mov esp, ebp
.text:00401129 pop ebp
.text:0040112A retn
.text:0040112A ; ---------------------------------------------------------------------------
.text:0040112B align 10h
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
.text:00401080 ; void __cdecl test_func2()
.text:00401080 ?test_func2@@YAXXZ proc near ; CODE XREF: test_func1(void)+3E↑p
.text:00401080
.text:00401080 var_10 = dword ptr -10h
.text:00401080 var_C = dword ptr -0Ch
.text:00401080 var_4 = dword ptr -4
.text:00401080 arg_4 = dword ptr 0Ch
.text:00401080
.text:00401080 ; FUNCTION CHUNK AT .text:00401E30 SIZE 0000001D BYTES
.text:00401080
.text:00401080 ; __unwind { // __ehhandler$?test_func2@@YAXXZ
.text:00401080 push ebp
.text:00401081 mov ebp, esp
.text:00401083 push 0FFFFFFFFh
.text:00401085 push offset __ehhandler$?test_func2@@YAXXZ
.text:0040108A mov eax, large fs:0
.text:00401090 push eax
.text:00401091 push ecx
.text:00401092 push ebx
.text:00401093 push esi
.text:00401094 push edi
.text:00401095 mov eax, ___security_cookie
.text:0040109A xor eax, ebp
.text:0040109C push eax
.text:0040109D lea eax, [ebp+var_C]
.text:004010A0 mov large fs:0, eax
.text:004010A6 mov [ebp+var_10], esp
.text:004010A9 push offset aTestFunc2 ; "test func2"
.text:004010AE call ds:__imp__puts
.text:004010B4 add esp, 4
.text:004010B7 ; try {
.text:004010B7 mov [ebp+var_4], 0
.text:004010BE call ?test_func3@@YAXXZ ; test_func3(void)
.text:004010C3 ; ---------------------------------------------------------------------------
.text:004010C3 jmp short loc_4010D9
.text:004010C5 ; ---------------------------------------------------------------------------
.text:004010C5
.text:004010C5 __catch$?test_func2@@YAXXZ$0: ; DATA XREF: .rdata:stru_402638↓o
.text:004010C5 ; catch(int) // owned by 4010B7
.text:004010C5 push offset aCatch2 ; "catch 2"
.text:004010CA call ds:__imp__puts
.text:004010D0 add esp, 4
.text:004010D3 mov eax, offset $LN7_0
.text:004010D8 retn
.text:004010D8 ; } // starts at 4010B7
.text:004010D9 ; ---------------------------------------------------------------------------
.text:004010D9
.text:004010D9 loc_4010D9: ; CODE XREF: test_func2(void)+43↑j
.text:004010D9 mov [ebp+var_4], 0FFFFFFFFh
.text:004010E0 jmp short loc_4010E9
.text:004010E2 ; ---------------------------------------------------------------------------
.text:004010E2
.text:004010E2 $LN7_0: ; CODE XREF: test_func2(void)+58↑j
.text:004010E2 ; DATA XREF: test_func2(void)+53↑o
.text:004010E2 mov [ebp+var_4], 0FFFFFFFFh
.text:004010E9
.text:004010E9 loc_4010E9: ; CODE XREF: test_func2(void)+60↑j
.text:004010E9 mov ecx, [ebp+var_C]
.text:004010EC mov large fs:0, ecx
.text:004010F3 pop ecx
.text:004010F4 pop edi
.text:004010F5 pop esi
.text:004010F6 pop ebx
.text:004010F7 mov esp, ebp
.text:004010F9 pop ebp
.text:004010FA retn
.text:004010FA ; ---------------------------------------------------------------------------
.text:004010FB align 10h
.text:004010FB ; } // starts at 401080
.text:004010FB ?test_func2@@YAXXZ endp

.eh_frame区域

刚开始疑惑了好久为什么我g++编译出来的程序没有.eh_frame段。
后来发现,啊,只有ELF文件有这玩意啊,那煤逝了。

对一个elf文件通过如下命令:readelf -Wwf xxx,可以读取其中关于 .eh_frame 的数据

🕊️🕊️🕊️
后面的学了再来补充

关于本文

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