概述
本文不一定具有很好的说教性,仅作为自我学习的笔记。不妨可参阅国外大神博文C++ exceptions under the hood链接中包含了大量的例子。
偶有在对ELF做分析的时候看到如下图一些注释,部分关键字看不懂,比如什么FDE
, unwind
, __gxx_personality_v0
,__cxa_end_catch
,__cxa_start_catch
等是什么?
由于笔者在Android开发领域,因此偏向ARM下做实现,部分源码有时候会参阅AOSP或者linux版本。
操作系统环境如下:
站在编译器角度学习
Itanium曾提出了相关异常规范,在很多GUN编译器都遵循相关ABI规范,具体可参阅:
Itanium C++ ABI: Exception Handling ($Revision: 1.22 $)
其中有两个术语要进行同步:
landing pad:异常处理中会进行两个操作,其中一个是运行资源回收代码,另一个是异常捕获处理代码。这两个代码块我们称为landing pad。
personality routines:异常处理的核心函数,用于判断当前函数是否可以处理异常或者进行栈回溯操作(unwind)在我们c++中这个函数名字为 __gxx_personality_v0
。
个人理解如下:
//mythrow.cpp
#include <stdio.h>
class MyClass
{
public:
~MyClass(){}
};
class MyFakeExeception{};
class MyFakeExeception2{};
void testThrow(int param)
{
if (param == 1)
{
throw MyFakeExeception();
}
else if (param == 2)
{
throw MyFakeExeception2();
}
}
void myCleanFun(void *p)
{
printf("myCleanFun invoke \r\n");
}
void testCatch(int param)
{
printf("testCatch start \r\n");
try
{
MyClass __attribute__((cleanup(myCleanFun))) mypro;
//...
// do thing with mypro ;
testThrow(param);
}
catch (const MyFakeExeception &e)
{
printf("testCatch catch MyFakeExeception\r\n");
}
catch (const MyFakeExeception2 &e)
{
printf("testCatch catch MyFakeExeception2 \r\n");
}
catch (...)
{
printf("testCatch catch ...\r\n");
}
printf("testCatch done \r\n");
}
我们以testCatch
函数举例。在调用testThrow
函数会触发异常那么调用到personality routines
函数此时假设判断函数可以捕获异常,那么首先我们需要释放掉try函数块的资源,在本例中需要调用MyClass
析构函数和myCleanFun
函数,这部分代码便是一个landing pad
,在处理完成后需要调用catch
块也是一个landing pad
。
那么编译器是如何完成异常的识别和栈回溯(unwind)的呢?
我们在创建一个mymain.cpp
文件使其可以完整运行。
//mymain.cpp
#include <stdio.h>
extern void testCatch(int param);
int main(int argc, char **args)
{
testCatch(argc);
return 0;
}
//编译成中间文件
g++ -O0 -ggdb -c mythrow.cpp -o mythrow.o
//编译成中间文件
g++ -O0 -ggdb -c mymain.cpp -o mymain.o
//链接成可执行文件
g++ -O0 -ggdb mythrow.o mymain.o -o app
当我们运行app输出:
testCatch start
myCleanFun invoke
testCatch catch MyFakeExeception
testCatch done
当我们输出输出app文件的节头 其中有几个非常有趣的节区:
readelf -S -W app
输出:
.eh_frame_hdr
和.eh_frame
用于栈回溯使用。
.gcc_except_table
也被称为LSDA(Language Specific Data Area)
节,这个节专门用于存储跟语言相关的特性,这里用于存储函数可以捕获哪些异常已经try catch
的信息(可以理解为java
字节码中异常表)。
有了这几个节我们便可以在异常时候进行栈回溯
以及landing pad
寻找。
栈回溯是什么
我们知道异常发生时候需要进行分支
操作,即跳转别的地方处理异常,处理完成后跳转回正常业务流程代码。那么这个流程就需要进行上下文的恢复和存储等(注:栈回溯在正常的函数调用也是需要的而非局限于异常)。
使用如下命令可查看.eh_frame_hdr
和.eh_frame
存储的内容
readelf -wF app
.eh_frame_hdr
和.eh_frame
里面就包含了被称为CIE
和FDE
东西,可以辅助我们调试或者栈回溯
的时候进行上下文恢复。
如果你有兴趣了解更多可以参阅(参考文献是x86架构不过不影响理解):
linux 栈回溯(x86_64 )
另一个问题编译器是如何构造这个节的呢?
这是通过编译器的CFI directives
,指令列表可参阅CFI-directives
我们使用下列命令仅进行汇编
g++ -O0 -ggdb -S mythrow.cpp
调用throw会发生什么
我们继续如法炮制查看对应的汇编函数
g++ -O0 -ggdb -S mythrow.cpp
我们查看testThrow函数对应的汇编
_Z9testThrowi:
.LFB3:
.loc 1 19 1
.cfi_startproc
stp x29, x30, [sp, -32]!
.cfi_def_cfa_offset 32
.cfi_offset 29, -32
.cfi_offset 30, -24
mov x29, sp
str w0, [sp, 28]
.loc 1 20 5
ldr w0, [sp, 28]
cmp w0, 1
bne .L3
.loc 1 22 32
mov x0, 1
bl __cxa_allocate_exception
mov x3, x0
mov x2, 0
adrp x0, _ZTI16MyFakeExeception
add x1, x0, :lo12:_ZTI16MyFakeExeception
mov x0, x3
bl __cxa_throw
.L3:
.loc 1 24 10
ldr w0, [sp, 28]
cmp w0, 2
bne .L5
.loc 1 26 33
mov x0, 1
bl __cxa_allocate_exception
mov x3, x0
mov x2, 0
adrp x0, _ZTI17MyFakeExeception2
add x1, x0, :lo12:_ZTI17MyFakeExeception2
mov x0, x3
bl __cxa_throw
.L5:
.loc 1 28 1
nop
ldp x29, x30, [sp], 32
.cfi_restore 30
.cfi_restore 29
.cfi_def_cfa_offset 0
ret
.cfi_endproc
注:testThrow
函数经过c++编译器名称粉碎(name mangling)之后变为_Z9testThrowi
。
上面的汇编的代码可以看到我们可以知道对于c++
的每个throw
关键字,会生成一对__cxa_allocate_exception
和__cxa_throw
函数的调用。
__cxa_allocate_exception
函数用于分配异常对象创建,而__cxa_throw
就是完成异常抛出给personality routines
。
我们在IDA PRO查看这两个函数发现都在plt节中
所以这两个函数在SO中被动态加载,使用ldd查看所依赖的so。
ldd app
这里函数具体位于libstdc++.so.6
中
nm -D /lib/aarch64-linux-gnu/libstdc++.so.6 | grep -i "__cxa_allocate_exception"
nm -D /lib/aarch64-linux-gnu/libstdc++.so.6 | grep -i "__cxa_throw"
当然我们最重要的 __gxx_personality_v0
也在这个库里面。
我查看linux源码的时候可以对应如下的源码
void
__cxa_throw(void *thrown_object, std::type_info *tinfo, void (_LIBCXXABI_DTOR_FUNC *dest)(void *)) {
//...
_Unwind_RaiseException(&exception_header->unwindHeader);
//...
//如果没找到可以处理异常landing pad就结束程序
failed_throw(exception_header);
}
我们重点下_Unwind_RaiseException
做了什么
_Unwind_Reason_Code LIBGCC2_UNWIND_ATTRIBUTE
_Unwind_RaiseException(struct _Unwind_Exception *exc)
{
struct _Unwind_Context this_context, cur_context;
_Unwind_Reason_Code code;
unsigned long frames;
/* Set up this_context to describe the current stack frame. */
uw_init_context (&this_context);
cur_context = this_context;
/* Phase 1: Search. Unwind the stack, calling the personality routine
with the _UA_SEARCH_PHASE flag set. Do not modify the stack yet. */
while (1)
{
_Unwind_FrameState fs;
/* Set up fs to describe the FDE for the caller of cur_context. The
first time through the loop, that means __cxa_throw. */
code = uw_frame_state_for (&cur_context, &fs);
if (code == _URC_END_OF_STACK)
/* Hit end of stack with no handler found. */
return _URC_END_OF_STACK;
if (code != _URC_NO_REASON)
/* Some error encountered. Usually the unwinder doesn't
diagnose these and merely crashes. */
return _URC_FATAL_PHASE1_ERROR;
/* Unwind successful. Run the personality routine, if any. */
if (fs.personality)
{
code = (*fs.personality) (1, _UA_SEARCH_PHASE, exc->exception_class,
exc, &cur_context);
if (code == _URC_HANDLER_FOUND)
break;
else if (code != _URC_CONTINUE_UNWIND)
return _URC_FATAL_PHASE1_ERROR;
}
/* Update cur_context to describe the same frame as fs. */
uw_update_context (&cur_context, &fs);
}
/* Indicate to _Unwind_Resume and associated subroutines that this
is not a forced unwind. Further, note where we found a handler. */
exc->private_1 = 0;
exc->private_2 = uw_identify_context (&cur_context);
cur_context = this_context;
code = _Unwind_RaiseException_Phase2 (exc, &cur_context, &frames);
if (code != _URC_INSTALL_CONTEXT)
return code;
uw_install_context (&this_context, &cur_context, frames);
}
上面的代码将异常处理分为两个阶段:搜索阶段和回溯处理阶段。
在搜索阶段借助.eh_frame_hdr
,.eh_frame
以及.gcc_except_table
判断调用栈是否存在一个处理异常的调用__gxx_personality_v0
判断。如果没有就返回到上一级函数__cxa_throw
结束程序。
如果找到调用_Unwind_RaiseException_Phase2
设置好上下文进行栈回溯的流程直到到异常处理块(期间会不断释放栈资源)。
我们使用gdb做一个有趣的实验查看对应的调用栈,
//给个性函数设置断点
(gdb) break __gxx_personality_v0
(gdb) r
(gdb) bt
输出如下
可见和我们猜测差不多。
调用try catch会发生什么
如法炮制查看testCatch
函数汇编
g++ -O0 -ggdb -S mythrow.cpp
对于没有耐心的同学可以跳过这段代码看结论
_Z9testCatchi:
.LFB5:
.loc 1 35 1
.cfi_startproc
.cfi_personality 0x9b,DW.ref.__gxx_personality_v0
.cfi_lsda 0x1b,.LLSDA5
stp x29, x30, [sp, -80]!
.cfi_def_cfa_offset 80
.cfi_offset 29, -80
.cfi_offset 30, -72
mov x29, sp
stp x19, x20, [sp, 16]
.cfi_offset 19, -64
.cfi_offset 20, -56
str w0, [sp, 44]
.loc 1 35 1
adrp x0, :got:__stack_chk_guard
ldr x0, [x0, #:got_lo12:__stack_chk_guard]
ldr x1, [x0]
str x1, [sp, 72]
mov x1, 0
.loc 1 36 11
adrp x0, .LC1
add x0, x0, :lo12:.LC1
.LEHB0:
bl puts
.LEHE0:
.LBB2:
.loc 1 43 18
ldr w0, [sp, 44]
.LEHB1:
bl _Z9testThrowi
.LEHE1:
.loc 1 40 54
add x0, sp, 48
.LEHB2:
bl _Z10myCleanFunPv
.LEHE2:
.loc 1 40 54 is_stmt 0 discriminator 1
add x0, sp, 48
bl _ZN7MyClassD1Ev
.L13:
.LBE2:
.loc 1 57 11 is_stmt 1
adrp x0, .LC2
add x0, x0, :lo12:.LC2
.LEHB3:
bl puts
.LEHE3:
.loc 1 58 1
nop
adrp x0, :got:__stack_chk_guard
ldr x0, [x0, #:got_lo12:__stack_chk_guard]
ldr x2, [sp, 72]
ldr x1, [x0]
subs x2, x2, x1
mov x1, 0
beq .L17
b .L23
.L18:
.LBB3:
.loc 1 40 54
mov x20, x0
mov x19, x1
add x0, sp, 48
bl _Z10myCleanFunPv
add x0, sp, 48
bl _ZN7MyClassD1Ev
mov x0, x20
mov x1, x19
b .L9
.L19:
.L9:
.LBE3:
.loc 1 45 5
cmp x1, 1
beq .L10
cmp x1, 2
beq .L11
b .L24
.L10:
.LBB4:
.loc 1 45 36 discriminator 1
bl __cxa_begin_catch
str x0, [sp, 64]
.loc 1 47 15 discriminator 1
adrp x0, .LC3
add x0, x0, :lo12:.LC3
.LEHB4:
bl puts
.LEHE4:
.loc 1 48 5
bl __cxa_end_catch
b .L13
.L11:
.LBE4:
.LBB5:
.loc 1 49 37
bl __cxa_begin_catch
str x0, [sp, 56]
.loc 1 51 15
adrp x0, .LC4
add x0, x0, :lo12:.LC4
.LEHB5:
bl puts
.LEHE5:
.loc 1 52 5
bl __cxa_end_catch
b .L13
.L24:
.LBE5:
.loc 1 53 12
bl __cxa_begin_catch
.loc 1 55 15
adrp x0, .LC5
add x0, x0, :lo12:.LC5
.LEHB6:
bl puts
.LEHE6:
.LEHB7:
.loc 1 56 5
bl __cxa_end_catch
b .L13
.L20:
.LBB6:
.loc 1 48 5
mov x19, x0
bl __cxa_end_catch
mov x0, x19
bl _Unwind_Resume
.L21:
.LBE6:
.LBB7:
.loc 1 52 5
mov x19, x0
bl __cxa_end_catch
mov x0, x19
bl _Unwind_Resume
.LEHE7:
.L22:
.LBE7:
.loc 1 56 5
mov x19, x0
bl __cxa_end_catch
mov x0, x19
.LEHB8:
bl _Unwind_Resume
.LEHE8:
.L23:
.loc 1 58 1
bl __stack_chk_fail
.L17:
ldp x19, x20, [sp, 16]
ldp x29, x30, [sp], 80
.cfi_restore 30
.cfi_restore 29
.cfi_restore 19
.cfi_restore 20
.cfi_def_cfa_offset 0
ret
.cfi_endproc
每个catch会生成如下三个函数的调用:
__cxa_begin_catch
__cxa_end_catch
_Unwind_Resume
__cxa_begin_catch
: 主要将异常对象放到栈上,方便异常处理代码使用。
__cxa_end_catch
:异常代码运行后,进行异常资源清理操作
_Unwind_Resume
:异常处理完成需要回到正常业务逻辑代码上。
我们查看Android下__cxa_begin_catch
对应的源码
extern "C" void* __cxa_begin_catch(void* exc) {
_Unwind_Exception *exception = static_cast<_Unwind_Exception*>(exc);
__cxa_exception* header = reinterpret_cast<__cxa_exception*>(exception+1)-1;
__cxa_eh_globals* globals = __cxa_get_globals();
if (!isOurCxxException(exception->exception_class)) {
if (globals->caughtExceptions) {
fatalError("Can't handle non-C++ exception!");
}
}
// Check rethrow flag
header->handlerCount = (header->handlerCount < 0) ?
(-header->handlerCount+1) : (header->handlerCount+1);
if (header != globals->caughtExceptions) {
header->nextException = globals->caughtExceptions;
globals->caughtExceptions = header;
}
globals->uncaughtExceptions -= 1;
//返回异信息。然后会被放入栈上
return header->adjustedPtr;
}
相关源码可参阅cxxabi.cc
gcc_except_table相关
gcc_except_table
也是在汇编文件生成指定数据
g++ -O0 -ggdb -S mythrow.cpp
当你阅读mythrow.s
文件的时候会看到一个定义节directives指令
.section .gcc_except_table,"a",@progbits
.align 2
.LLSDA5:
.byte 0xff
.byte 0x9b
.uleb128 .LLSDATT5-.LLSDATTD5
.LLSDATTD5:
.byte 0x1
.uleb128 .LLSDACSE5-.LLSDACSB5
.LLSDACSB5:
.uleb128 .LEHB0-.LFB5
.uleb128 .LEHE0-.LEHB0
.uleb128 0
.uleb128 0
....
具体解析可以参阅其他文献。
使用nothrow会怎么样(或者noexcept)?
我们给抛出异常的函数添加不会抛出异常
未声明前相关区大小
声明后节区大小
运行后
testCatch start
terminate called after throwing an instance of 'MyFakeExeception'
Aborted (core dumped)
声明不会抛出异常的函数会减少生成相关文件大小使其更紧凑,但需要注意代码是否会违背规则的情况存在。
参考文献:
C++ exceptions under the hood
c++ 异常处理(上)
c++ 异常处理(下)
Itanium C++ ABI: Exception Handling ($Revision: 1.22 $)
What is the “C++ ABI Specification” referred to in GCC’s manual?
gcc_except_table
CPP 异常处理机制初探
Unwind 栈回溯详解
Chapter 8. Exception Frames
Serial- / Socket IO and GCC nothrow attribute
Itanium C++ ABI
C++对象模型之RTTI的实现原理
Android cxxabi