C++异常处理之底层逻辑

文章深入探讨了C++异常处理的底层机制,包括如何通过__cxa_throw抛出异常,如何通过__cxa_begin_catch和__cxa_end_catch捕获异常,以及personality函数的作用。文章还介绍了如何解析LSDA和gcc_except_table来确定异常处理的细节。最后,讨论了异常处理如何影响栈回滚和析构函数的执行。
摘要由CSDN通过智能技术生成

近来想写一篇文章LLVM处理异常的底层逻辑,兜兜转转在网上看到一些介绍C++异常文章,文章是以GCC为编译讲解,写的很不错,因此把它们整理下来,感谢原作者的分享!
原文地址:
C++异常的幕后(1)
C++异常的幕后2:一个小AB
C++异常的幕后3:取悦链接器的ABI
C++异常的幕后4:捕捉你抛出的异常
C++异常的幕后5:围绕__cxa_begin_catch与__cxa_end_catch的魔术
C++异常的幕后6:gcc_except_table与personality函数
C++异常的幕后7:好的personality
C++异常的幕后8:两阶段处理
C++异常的幕后9:捕捉我们第一个异常
C++异常的幕后10:_Unwind_与调用帧信息
C++异常的幕后11:阅读CFI表
C++异常的幕后12:C++里的突然反射
C++异常的幕后13:为着陆垫设置上下文
C++异常的幕后14:多个着陆垫与大师的教导
C++异常的幕后15:找到正确的着陆垫
C++异常的幕后16:在着陆垫里找到正确的捕捉
C++异常的幕后17:异常类型的发射以及读.gcc_except_table
C++异常的幕后18:获取正确的栈帧
C++异常的幕后19:在着陆垫里获取正确的捕捉
C++异常的幕后20:在回滚时运行析构函数
C++异常的幕后21:总结与一些最后的思考

C++异常的幕后(1)

每个人都知道良好的异常处理是困难的。在异常“生命期”的每个层面,出现这种情况的原因有许多:编写异常安全的代码是困难的,异常可能从不期望的位置抛出(双关语),理解设计不良的异常架构是复杂的,因为幕后发生了许多巫术,它是慢的;因为不正确地抛出异常可能导致调用不可原谅的std::terminate,它是危险的。虽然每个曾经与“异常”程序斗争的人可能知道这,造成这种混乱的原因并不广为人知。

我们要问自己的第一个问题是,这一切是如何工作的。这是应该长系列的第一篇文章,在这个系列里我将讨论在C++里,异常在幕后是如何实现的(实际上在x86平台上使用gcc编译C++,不过这也可能适用于其他平台)。在这些文章中,将详细解释抛出与捕捉异常的过程,但对那些没有耐心的人这里有小的一个文章摘要:在gcc/x86中如何抛出异常:

当我们编写一条throw语句时,编译器把它翻译为对libstdc++的一对调用:分配异常,然后通过调用libstdc开始栈回滚过程。
对每条catch语句,编译器将在这个方法主体后写下一些特殊信息,一张这个方法可以捕捉的异常表以及清理表(稍后再解释清理表)。
随着回滚器穿过栈,它将调用libstdc++提供的一个特殊函数(称为personality例程),这个函数检查在栈里的每个函数可以捕捉哪些异常。
如果没有找到与这个异常相符的捕捉,调用std::terminate。
如果找到相符的捕捉,回滚器现在在栈顶开始启动。
随着回滚器第二次穿过栈,它将要求personality例程为这个方法执行清理。
这个personality例程将检查当前方法上的清理表。如果有任何清理活动要运行,它将“跳转”到当前栈帧并运行清理代码。这将为在当前作用域里分配的每个对象运行析构函数。
一旦回滚器到达可以处理这个异常的栈帧,它将跳转到合适的catch语句里。
在完成catch语句的执行时,将调用一个清理函数来释放该异常持有的内存。
这看起来已经相当复杂,而我们甚至还没开始;对异常处理的所有复杂性而言,这是一个短的、不准确的描述。

为了学习发生在幕后的所有细节,在下一篇文章我们将开始实现我们自己的迷你libstdlibc++。但不是全部,仅是处理异常的部分。实际上甚至还不是这些全部,仅是使得简单throw/catch语句工作所需的最低要求。将需要一些汇编,但不是很有趣。恐怕需要很多耐心。

如果你实在好奇且希望开始阅读有关异常处理的实现,那么你可以从这里开始,在后续几篇文章里我们将实现一个完整的规范。我将尝试使得这些文章更有指导性,更容易遵循,期望下次开始我们的ABI时再见!

免责声明:我一点也不精通抛出异常时的魔术。这个系列将试图揭开面纱,在过程中学习一些东西,同时我希望其中一些是正确的,毫无疑问有许多不太准确的细节。如果你认为我该改正什么,请告诉我。

C++异常的幕后2:一个小ABI

#include "throw.h"
int main()  {
    seppuku();
    return 0;
}

如果现在我们尝试编译并链接这个代码会发生什么?

g++ -c -o throw.o -O0 -ggdb throw.cpp
gcc -c -o main.o -O0 -ggdb main.c

注意:你可以从我的github库里下载这个项目的完整源代码。

目前还好。G++与gcc都陶醉在它们的小世界里。然而一旦我们尝试链接它们,混乱接踵而至:

$ gcc main.o throw.o -o app
throw.o: In function `foo()':
Throw.cpp:4: undefined reference to `__cxa_allocate_exception'
throw.cpp:4: undefined reference to `__cxa_throw'
throw.o:(.rodata._ZTI9Exception[typeinfo for Exception]+0x0): undefined reference to `vtable for __cxxabiv1::__class_type_info'
collect2: ld returned 1 exit status

确实,gcc抱怨缺少C++符号。虽然这些是非常特殊的C++符号。检查最后一行错误:缺少用于cxxabiv1的vtable。定义在libstdc++中,cxxabi援引用于C++的应用程序二进制接口。因此现在我们了解到异常处理是在带有C++ABI定义接口的标准C++库的辅助下完成。

C++ ABI定义了一个标准二进制格式,因此我们可以在一个程序里将对象链接起来;如果我们使用两个编译器编译一个.o文件,这些编译器使用不同的ABI,我们将不能把.o文件链接进应用程序。ABI也将定义其他一些格式,例如执行栈回滚或异常抛出的接口。在这个情形里,ABI在C++与我们程序里其他某些处理栈回滚的库之间定义了一个接口(不一定二进制格式,只是一个接口),即ABI定义了C++特定的内容,因此它可与非C++库交谈:这使得在C++里能捕捉从其他语言抛出的异常,除了别的之外。

无论如何,链接器将我们指向幕后异常处理的第一层:一个我们必须自己实现的接口,cxxabi。在下一篇我们将开始我们自己的小ABI,就像定义在C++ ABI里那样。

C++异常的幕后3:取悦链接器的ABI

在我们理解异常的路程上,我们发现重担在libstdc++里完成,如C++ ABI说明的那样。阅读了一些链接器错误,我们上次推断要处理异常我们需要C++ ABI的辅助;我们创建了一个抛出异常的C++程序,把它与一个C程序链接,发现编译器有时把我们的throw指令翻译为某些现在调用几个libstdc++函数的对象来实际抛出异常。已经迷失了?你可以在我的github repo里检查这个项目的源代码。

无论如何,我们希望确切理解异常是如何抛出的,因此我们将尝试实现我们自己的小ABI,能够抛出异常。要做到这,需要许多RTFM,不过在这里可以找到一个用于LLVM的完整ABI接口。让我们先回忆一下这些缺少的函数是什么开始:

$ gcc main.o throw.o -o app
 throw.o: In function `foo()':
 throw.cpp:4: undefined reference to `__cxa_allocate_exception'
 throw.cpp:4: undefined reference to `__cxa_throw'
 throw.o:(.rodata._ZTI9Exception[typeinfo for Exception]+0x0): undefined reference to `vtable for __cxxabiv1::__class_type_info'
 collect2: ld returned 1 exit status
__cxa_allocate_exception

我觉得名字不言自明。__cxa_allocate_exception接受一个size_t,分配足够的内存来保存要抛出的异常。这比你想象的要复杂得多:在要抛出一个异常时,栈会发生一些神奇的事情,因此在这里分配资源不是一个好主意。不过在堆上分配内存也不是一个好主意,因为如果我们耗尽内存,我们可能要抛出异常。静态分配同样不是好主意,因为我们需要这是线程安全的(否则两个线程同时访问同样悲剧)。鉴于这些限制,绝大多数实现看起来在一个局部线程储存(堆)上分配内存,如果内存耗尽转向一个紧急储存(大概是静态的)。当然我们不希望操心那些丑陋的细节,因此如果愿意我们可以只有一个静态缓冲。

__cxa_throw

这个函数执行所有的抛出魔术!根据ABI文献,一旦创建了异常,__cxa_throw将被调用。这个函数将负责启动栈回滚。这的一个重要后果是:__cxa_throw从不预期会返回。它要么把执行委托给正确的catch块来处理异常,要么(缺省)调用std::terminate,但它从不返回。

用于__cxxabiv1::__class_type_info的vtable

一件离奇的事……__class_type_info显然是某种RTTI,但它究竟是什么?现在这是不容易回答的,并且对我们的小ABI而言它不是特别重要;我们把它放在附录里,留待我们完成抛出异常过程分析之后,现在我们只说这是ABI定义的入口,以(在运行时)知晓两个类型是否相同。这是调用来确定一个catch(父亲)是否能处理一个throw孩子的函数。目前我们将关注在基础:我们需要给它一个地址用于链接器(即定义它是不足够的,我们需要具现它),并且它必须有一个vtable(即,它必须有虚函数)。

在这些函数上发生了很多事情,但让我们尝试实现尽可能简单的异常抛出器:当一个异常抛出时,调用exit。我们的应用程序几乎没有问题,但缺少某些ABI内容,因此让我们创建一个mycppabi.cpp。阅读我们的ABI规范,可以得出__cxa_allocate_exception与__cxa_throw的署名:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h> 

namespace __cxxabiv1 {
    struct __class_type_info {
        virtual void foo() {}
    } ti;
}

#define EXCEPTION_BUFF_SIZE 255
char exception_buff[EXCEPTION_BUFF_SIZE];

extern "C" {
void* __cxa_allocate_exception(size_t thrown_size) {
    printf("alloc ex %i\n", thrown_size);
    if (thrown_size > EXCEPTION_BUFF_SIZE) printf("Exception too big");
    return &exception_buff;
}

void __cxa_free_exception(void *thrown_exception);
 
#include <unwind.h>

void __cxa_throw(
          void* thrown_exception,
          struct type_info *tinfo,
          void (*dest)(void*)) {
    printf("throw\n");
    exit(0);     // __cxa_throw never returns
}
} // extern "C"

备注:你可以从我的github repo下载完整的源代码。

如果我们现在编译mycppabi.cpp并把它与其他两个.o文件链接,我们将得到一个可工作的二进制文件,它将打印“alloc ex 1\nthrow”,然后退出。相当简单,但这是一个惊人的壮举:我们设法抛出一个异常而没有调用libc++。我们已经编写了C++ ABI一个(非常小的)部分!

通过创建我们自己的小ABI,我们获得的另一个重要的知识:throw关键字被编译为libstdc++的两个函数。这里没有双关语,它实际上是相当简单的翻译。我们甚至可以反汇编我们的抛出函数来验证它。让我们运行这个命令“g++ -S throw.cpp”。

seppuku:
.LFB3:
    [...]
    call    __cxa_allocate_exception
    movl    $0, 8(%esp)
    movl    $_ZTI9Exception, 4(%esp)
    movl    %eax, (%esp)
    call    __cxa_throw
    [...]

更神奇的事情发生了:在throw关键字被翻译为这个两个调用,编译器甚至不知道怎样处理异常。因为libstdc++是定义__cxa_throw及其朋友的地方,且libstdc++是在运行时动态链接的,在第一次运行我们的可执行文件时,可以选择异常处理方法。

现在我们看到了一些进展,但我们仍然有很长的路要走。我们的ABI仅能抛出异常。我们可以扩展它来处理一个捕捉吗?我们下一节来看。

C++异常的幕后4:捕捉你抛出的异常

在这个关于异常处理的系列里,通过检查编译器与链接器错误,我们已经发现不少异常抛出的秘密,但目前为止我们还看到任何关于异常处理的东西。让我们总结一下学到的异常抛出:

一个throw语句将被编译器翻译为两个调用,__cxa_allocate_exception与__cxa_throw。
__cxa_allocate_exception与__cxa_throw“活”在libstdc++中。
__cxa_allocate_exception将为新异常分配内存。
_cxa_throw将准备一大堆东西并把这个异常转发给_Unwind,一组libstdc++里的函数,并执行真正的栈回滚(这个ABI定义了这些函数的接口)。
目前为止相当简单,但异常捕捉有点复杂,特别因为它要求某种程度的反射(即,程序分析自己源代码的能力)。让我们继续在旧代码上尝试,在我们代码添加一些catch语句,编译它并看发生了什么:

#include "throw.h"
#include <stdio.h>

// Notice we're adding a second exception type
struct Fake_Exception {}; 

void raise() {
    throw Exception();
}

// We will analyze what happens if a try block doesn't catch an exception
void try_but_dont_catch() {
    try {
        raise();
    } catch(Fake_Exception&) {
        printf("Running try_but_dont_catch::catch(Fake_Exception)\n");
    }
    printf("try_but_dont_catch handled an exception and resumed execution");
}

// And also what happens when it does
void catchit() {
    try {
        try_but_dont_catch();
    } catch(Exception&) {
        printf("Running try_but_dont_catch::catch(Exception)\n");
    } catch(Fake_Exception&) {
        printf("Running try_but_dont_catch::catch(Fake_Exception)\n");
    }
    printf("catchit handled an exception and resumed execution");
} 

extern "C" {
    void seppuku() {
        catchit();
    }
}

备注:你可以从我的github repo下载完整的源代码。

就像之前那样,我们的seppuku函数把C世界与C++世界链接起来,只是这次我们添加了另外一些函数调用来使得我们的栈更有趣,加上我们已经添加了一组try/catch块,因此我们可用分析libstdc++如何处理它们。

像以前那样,我们得到了一些关于缺失ABI函数的链接器错误:

 $g++ -c -o throw.o -O0 -ggdb throw.cpp
 $gcc main.o throw.o mycppabi.o -O0 -ggdb -o app
throw.o: In function `try_but_dont_catch()':
throw.cpp:12: undefined reference to `__cxa_begin_catch'
throw.cpp:12: undefined reference to `__cxa_end_catch'
throw.o: In function `catchit()':
throw.cpp:20: undefined reference to `__cxa_begin_catch'
throw.cpp:20: undefined reference to `__cxa_end_catch'
throw.o:(.eh_frame+0x47): undefined reference to `__gxx_personality_v0'
collect2: ld returned 1 exit status

同样这里我们看到了许多有趣的东西。对__cxa_begin_catch与__cxa_end_catch的调用可能是我们期望的东西:我们尚不了解它们,但我们可以假定它们等同于throw/__cxa_allocate/throw转换(记得我们的throw关键字被翻译为一对__cxa_allocate_exception与__cxa_throw函数,对吧?)。不过__gxx_personality_v0是新玩意,它是后面几章的中心。

Personality函数做什么用呢?在介绍这个系列时,我们谈论过它,但我们将进一步探究它,连同我们的两个新朋友,__cxa_begin_catch与__cxa_end_catch。

C++异常的幕后5:围绕__cxa_begin_catch与__cxa_end_catch的魔术

在学习了异常如何抛出后,现在我们正在学习如何捕捉它们的过程中。上次我们在我们的例子里添加了一组try/catch语句来它们做什么,果然我们得到了一组链接器错误,就像在我们尝试找出throw语句做什么时那样。在尝试处理throw.o时,链接器输出了这些:

$g++ -c -o throw.o -O0 -ggdb throw.cpp
$gcc main.o throw.o mycppabi.o -O0 -ggdb -o app
throw.o: In function `try_but_dont_catch()':
throw.cpp:12: undefined reference to `__cxa_begin_catch'
throw.cpp:12: undefined reference to `__cxa_end_catch'
throw.o: In function `catchit()':
throw.cpp:20: undefined reference to `__cxa_begin_catch'
throw.cpp:20: undefined reference to `__cxa_end_catch' 
throw.o:(.eh_frame+0x47): undefined reference to `__gxx_personality_v0'
collect2: ld returned 1 exit status

备注:你可以从我的github repo下载完整的源代码。

当然,我们的理论是catch语句被编译器翻译为libstdc++里的一对__cxa_begin_catch/end_catch调用,加上我们现在一无所知的称为personality函数的东西。

我们从检查我们关于__cxa_begin_catch与__cxa_end_catch是否成立开始。我们使用-S编译throw.cpp并分析汇编代码。有很多可看,但如果我把它减到最小,这是我得到的:

_Z5raisev:
    call    __cxa_allocate_exception
    call    __cxa_throw

目前还好:对raise()我们得到相同的旧定义,只是抛出一个异常:

_Z18try_but_dont_catchv:
    .cfi_startproc
    .cfi_personality 0,__gxx_personality_v0
    .cfi_lsda 0,.LLSDA1

try_but_dont_catch()的定义,由编译器重整。不过有些新的东西:对__gxx_personality_v0以及称为LSDA对象的引用。这些看似无关的声明,但它们实际上相当重要:

链接器将根据CFI规范使用这些;CFI代表调用帧信息,这里是完整的规范。它主要用于栈展开。
另一方面LSDA表示语言特定数据区,它将由personality函数用来了解这个函数可以处理哪些异常。
在下一篇文章里我们将更多讨论CFI与LSDA;不要忘了它们,不过现在让我们继续:

call    _Z5raisev
jmp .L8

另一件容易的事情:只是调用raise,然后跳转到L8;这个函数将正常返回到L8。如果raise不能正确执行,那么执行(不过,我们还不知道怎么做)不会在下一条指令继续,而是在异常处理句柄里继续(这在ABI术语里称为着陆垫,landing pads。稍后细说)。

.LBB2_1:                                # %lpad
    cmpl    $1, %edx
    je  .L5
    
.LEHB1:
    call    _Unwind_Resume
.LEHE1:

.L5:
    call    __cxa_begin_catch
    call    __cxa_end_catch

这相当难理解,但它实际上相当直截了当。在这里大部分奇迹将会发生:首先我们检查这是否我们可以处理的异常,如果不能,我们通过调用_Unwind_Resume来表示,如果能,我们调用__cxa_begin_catch与__cxa_end_catch;在调用这些函数后,执行将正常继续,因此将执行L8(即,L8就在我们的catch块下面):

.L8:
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc

只是从我们函数的正常返回……带有一些CFI内容。

这就是异常捕捉,虽然我们尚不知道__cxa_begin/end_catch如何工作,我们有一个概念,这些对形成称为着陆垫的东西,函数里处理抛出异常的地方。我们尚不知道的是如何找到着陆垫。_Unwind_必须以某种方法经历栈上所有的调用,检查是否有一个调用(准确说,栈帧)带有可以捕捉异常、并在那里重新开始执行的着陆垫的有效try块。

这是一个不小的壮举,我们将在下一次看它是如何工作的。

C++异常的幕后6:gcc_except_table与personality函数

上次我们了解到,就像throw语句被翻译为一对__cxa_allocate_exception/throw调用,catch块被翻译为一对__cxa_begin/end_catch调用,加上称为CFI(调用帧信息)的对象,来查找着陆垫——函数处理异常之处。

我们尚不知道的是_Unwind_如何知道着陆垫在哪里。在一个异常被抛出时,栈上有一组函数;所有CFI内容将让Unwind知道这些是什么函数,但知道每个函数提供哪些着陆垫也是必须的,因此我们可以调用每个函数,核查它是否希望处理这个异常(我们忽略带有多个try/catch块的函数)。

要知道着陆垫在哪里,要使用称为gcc_except_table的东西。在函数末尾可以找到这(带有一组CFI):

.LFE1:
    .globl  __gxx_personality_v0
    .section    .gcc_except_table,"a",@progbits
    [...]

.LLSDACSE1:
    .long   _ZTI14Fake_Exception

节.gcc_except_table是所有定位着陆垫信息保存的地方,一旦我们设法分析了personality函数后,我们再来更多地了解它;目前,我们只是说LSDA表示语言特定数据区域,它是personality函数检查对一个函数是否存在任何着陆垫的地方(在回滚栈时,它也用于运行析构函数)。

总之:对每个至少找到一个catch的函数,编译器将把这个语句翻译为一对__cxa_begin_catch/ __cxa_end_catch调用,接着personality函数,被__cxa_throw调用,将为栈上每个函数读gcc_except_table,查找称为LSDA的东西。然后Personality函数将在LSDA里检查一个catch是否能处理一个异常,是否有清理代码运行(这就是在需要时触发析构函数的原因)。

这里我们还可以得到一个有趣的结论:如果我们使用nothrow说明符(或者空的throw指示符),编译器可以为这个方法忽略gcc_except_table。Gcc实现异常的方式,不会对性能产生大的影响,但确实将减小代码大小。Catch是什么?如果在指明nothrow时抛出一个异常,不存在LSDA,personality函数不知道怎么做。在personality函数不知道做什么时,它将调用缺省的异常处理句柄,意味着在大多数情形里从一个nothrow方法抛出将最终调用std::terminate。

现在我们了解了personality函数是什么,我们可以实现它吗?下一节我们来看如何做。

C++异常的幕后7:好的personality

在我们学习异常的旅程中,目前我们已经了解到如何完成抛出,称为“调用帧信息”的东西辅助称为Unwind的库执行栈回滚,编译器写入 称为LSDA的 语言特定数据区,了解一个方法可以处理哪些异常。现在我们知道许多魔术在personality函数上完成;不过我们还未看过它的实际作用。让我们更详细地回顾一下异常如何抛出与捕捉(或者,更准确地,目前为止我们知道它是如何被抛出、捕捉):

编译器将我们的throw语句翻译为一对__cxa_allocate_exception/__cxa_throw
__cxa_allocate_exception将在内存里创建异常
__cxa_throw将初始化一堆东西,通过调用_Unwind_RaiseException把这个异常转发底层的unwind库
Unwind将使用CFI来了解哪些函数在栈上(即知道如何开始栈回滚)
每个函数将有一个LSDA(语言特定数据区)部分,加上.gcc_except_table
Unwind将以当前栈帧以及LSDA调用personality函数;这个函数将回复unwind这个栈是否能处理这个异常
了解到这,是时候实现我们自己的personality方法了。在抛出异常时,我们的ABI过去输出这:

alloc ex 1
__cxa_throw called
no one handled __cxa_throw, terminate!

让我们回到mycppabi并添加像这样的内容(链接到完整的mycppabi.cpp文件):

void __gxx_personality_v0() {
    printf("Personality function FTW\n");
}

备注:你可以从我的github repo下载完整的源代码。

的确,在我们运行它时,我们将看到我们的personality函数被调用。我们知道我们在正确的道路上,现在我们已经知道我们希望什么样的personality函数;让我们开始使用对这个函数合适的定义:

_Unwind_Reason_Code __gxx_personality_v0 (
                     int version, _Unwind_Action actions, uint64_t exceptionClass,
                     _Unwind_Exception* unwind_exception, _Unwind_Context* context);

如果我们把这放入我们的mycppabi.cpp文件,我们得到:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

namespace __cxxabiv1 {
    struct __class_type_info {
        virtual void foo() {}
    } ti;
}

#define EXCEPTION_BUFF_SIZE 255
char exception_buff[EXCEPTION_BUFF_SIZE];

extern "C" {
void* __cxa_allocate_exception(size_t thrown_size) {
    printf("alloc ex %i\n", thrown_size);
    if (thrown_size > EXCEPTION_BUFF_SIZE) printf("Exception too big");
    return &exception_buff;
}

void __cxa_free_exception(void *thrown_exception);

#include <unwind.h>
typedef void (*unexpected_handler)(void);
typedef void (*terminate_handler)(void);

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

void __cxa_throw(void* thrown_exception, struct type_info *tinfo, void (*dest)(void*)) {
    printf("__cxa_throw called\n");
    __cxa_exception *header = ((__cxa_exception *) thrown_exception - 1);
    _Unwind_RaiseException(&header->unwindHeader);
    
    // __cxa_throw never returns
    printf("no one handled __cxa_throw, terminate!\n");
    exit(0);
}

void __cxa_begin_catch() {
    printf("begin FTW\n");
} 

void __cxa_end_catch() {
    printf("end FTW\n");
}

_Unwind_Reason_Code __gxx_personality_v0 (
                     int version, _Unwind_Action actions, uint64_t exceptionClass,
                     _Unwind_Exception* unwind_exception, _Unwind_Context* context) {
    printf("Personality function FTW!\n");
}
}

备注:你可以从我的github repo下载完整的源代码。

让我们编译及链接,然后运行它,在gdb的辅助下分析这个函数的每个参数:

Breakpoint 1, __gxx_personality_v0 (version=1, actions=1, exceptionClass=134514792, unwind_exception=0x804a060, context=0xbffff0f0)

版本以及exceptionClass与语言/ABI/编译器工具链/原生或非原生异常等相关。我们无需为我们的小ABI担心这些,我们将处理所有的异常。
行动:这是_Unwind_用来告诉personality函数它应该做什么的(后面详细论述)
Unwind_exception:由__cxa_allocate_exception分配的异常(有点儿……进行了许多指针算术,不过该指针可用于访问我们最初的异常)
Context:这保存了当前栈帧的所有信息,例如语言特定数据区(LSDA)。这是我们将用来检测栈是否可以处理抛出异常的(也用于检测我们是否需要运行任何析构函数)。
好了,一个可工作的(额,可链接的)personality函数。不过做不了什么,因此下一次我们将从添加某个真实行为开始,尝试使它处理一个异常。

C++异常的幕后8:两阶段处理

上一章以添加一个_Unwind_能够调用的personality函数而结束。它没做什么,但它在那里。我们已经实现的ABI现在可以抛出异常,捕捉也已经完成一半,但需要正确选择catch块(着陆垫)的personality函数目前有点傻。我们通过尝试理解personality函数接受什么参数开始新的一章,下次我们将向__gxx_personality_v0添加某些真实的行为:在调用__gxx_personality_v0时,我们应该说“是的,这个栈帧确实可以处理这个异常”。

我们已经说过我们不在乎我们小ABI的版本或exceptionClass。现在让我们也忽略上下文:我们将仅处理抛出函数上的第一个栈帧;注意这意味着紧靠着抛出函数之上的函数必须有一个try/catch块,否则一切都被打破。这也意味着这个catch将忽略其异常规范,有效地将它变成一个catch(…)。我们如何让_Unwind_知道我们希望处理当前的异常?

_Unwind_Reason_Code是personality函数的返回值;这告诉_Unwind_我们是否找到一个着陆垫来处理该异常。让我们实现我们的personality函数返回_URC_HANDLER_FOUND,然后看会发生什么:

alloc ex 1
__cxa_throw called
Personality function FTW
Personality function FTW
no one handled __cxa_throw, terminate!

看到了吗?我们告诉_Unwind_我们找到了一个处理句柄,它再次调用personality函数!那里发生了什么?

记得活动参数吗?这是_Unwind告诉我们它期望什么的方式,这是因为异常捕捉分两阶段处理:查找与清理(或者_UA_SEARCH_PHASE与_UA_CLEANUP_PHASE)。让我们再次重温异常抛出与捕捉的方法:

__cxa_throw/__cxa_allocate_exception将创建异常,并通过调用_Unwind_RaiseException把它转发给底层unwind库
Unwind将使用CFI来了解栈上有哪些函数(即知道如何启动栈回滚)
每个函数有一个LSDA(语言特定数据区)部分,加上称为.gcc_except_table的部分
Unwind将尝试定位用于该异常的着陆垫:
Unwind以_UA_SEARCH_PHASE以及指向当前栈帧的上下文指针调用personality函数。
通过分析LSDA,personality函数检查当前栈帧是否能处理抛出的异常。
如果可以处理该异常,它返回_URC_HANDLER_FOUND。
如果不能处理该异常,它将返回_URC_CONTINUE_UNWIND,然后Unwind尝试下一个栈帧。
如果没有找到着陆垫,调用缺省异常处理句柄(通常是std::terminate)。
如果找到一个着陆垫:
Unwind将再次迭代栈,以_UA_CLEANUP_PHASE调用personality函数。
这个personality函数将再次检查它是否能处理当前异常:
如果这个帧可以处理这个异常,那么它将运行一个由LSDA描述的清理函数,并告诉Unwind继续下一个帧(实际上这是非常重要的一步:清理函数将运行在这个栈帧里分配对象的析构函数)!
如果这个帧不能处理这个异常,不运行任何清理代码:告诉Unwind我们希望在这个着陆垫上恢复执行。
这里有两个重要的信息需要注意:

运行两阶段异常处理过程意味着在没有找到句柄时,缺省异常处理句柄可以获得最初异常的栈追踪(如果我们一边处理一边回滚栈,将得不到栈追踪,或者我们需要持有它的一个拷贝)!
运行_UA_CLEANUP_PHASE并第二次调用每个帧,即使我们已经知道这个帧将处理这个异常,也是重要的:personality函数将利用这个机会为在这个作用域上构建的对象运行所有的析构函数。正是这使得RAII成为一个异常安全的习惯用法。
现在我们理解了catch查找阶段如何工作,我们可以继续实现我们的personality函数。

C++异常的幕后9:捕捉我们第一个异常

在上一章我们添加一个_Unwind_可以调用的personality函数,并分析这个personality函数接受的参数。现在是时候向__gxx_personality_v0添加某些真实的行为:在调用__gxx_personality_v0时,我们应该说“是的,这个栈帧确实能够处理这个异常”。

到目前为止,我们已经积累了相当多的经验:我们第一次可以实现一个能够检测何时抛出异常,并宣称“是的,我将处理这个异常”的personality函数。为此,我们必须学习两阶段查找如何工作,因此现在我们可以重新实现我们的personality函数以及我们的抛出测试文件:

#include <stdio.h>
#include "throw.h"
struct Fake_Exception {};

void raise() {
    throw Exception();
}

void try_but_dont_catch() {
    try {
        raise();
    } catch(Fake_Exception&) {
        printf("Caught a Fake_Exception!\n");
    }
    printf("try_but_dont_catch handled the exception\n");
}

void catchit() {
    try {
        try_but_dont_catch();
    } catch(Exception&) {
        printf("Caught an Exception!\n");
    }
    printf("catchit handled the exception\n");
}

extern "C" {
    void seppuku() {
        catchit();
    }
}

我们的personality函数:

_Unwind_Reason_Code __gxx_personality_v0 (
                     int version, _Unwind_Action actions, uint64_t exceptionClass,
                     _Unwind_Exception* unwind_exception, _Unwind_Context* context) {
    if (actions & _UA_SEARCH_PHASE) {
        printf("Personality function, lookup phase\n");
        return _URC_HANDLER_FOUND;
    } else if (actions & _UA_CLEANUP_PHASE) {
        printf("Personality function, cleanup\n");
        return _URC_INSTALL_CONTEXT;
    } else {
        printf("Personality function, error\n");
        return _URC_FATAL_PHASE1_ERROR;
    }
}

备注:你可以从我的github repo下载完整的源代码。

让我们运行看发生什么:

alloc ex 1
__cxa_throw called
Personality function, lookup phase
Personality function, cleanup
try_but_dont_catch handled the exception
catchit handled the exception

它能工作,但缺少一些东西:在catch/try块里的catch没有执行!这是因为personality函数告诉Unwind“安装一个上下文”(即重新执行),但它从不说是哪个上下文。在这个情形里,在着陆垫后恢复执行是可能的。下次我们将看到我们如何可以使用.gcc_except_table里的信息(我们的老朋友,LSDA)从指定着陆垫恢复执行。

C++异常的幕后10:_Unwind_与调用帧信息

我们让我们的小ABI项目(链接)能够抛出异常了,现在我们着力捕捉它们;上次我们实现了一个能够检测并处理异常的personality函数,不过它仍然有点不完整:即使它能正确地通知栈回滚器它何时应该停止,但我们版本的__gxx_personality_v0不能运行catch块里的代码。有人会说这总比coredump要好,但要成为有用的异常处理ABI,仍然有长的路要走。我们能改进它吗?

我们如何能告诉_Unwind_我们的着陆垫在哪里,使得我们可以执行catch语句里的代码?回到ABI规范,有一些上下文管理函数可能对我们有用:

_Unwind_GetLanguageSpecificData,为这个栈帧获取LSDA(Language Specific Data)。使用它,我们应该能够找到要运行的着陆垫与析构函数。
_Unwind_GetRegionStart,为当前被personality函数分析的栈帧,获取函数开头的指令指针(即,当前栈帧的函数指针)。
_Unwind_GetIP,获取当前栈帧里的指令指针(指向对下一个栈帧函数调用完成处的指针。下面的例子应该会更清楚)。
备注:你可以从我的github repo下载完整的源代码。

让我们通过gdb检查这些函数。在我的机器上:

Breakpoint 1, __gxx_personality_v0 (version=1, actions=6, exceptionClass=134515400, unwind_exception=0x804a060,

context=0xbffff0f0) at mycppabi.cpp:77
const uint8_t* lsda = (const uint8_t*)_Unwind_GetLanguageSpecificData(context);
uintptr_t ip = _Unwind_GetIP(context) - 1;
uintptr_t funcStart = _Unwind_GetRegionStart(context);
uintptr_t ipOffset = ip - funcStart;

检查这些变量,看到_Unwind_GetRegionStart确实指向当前栈帧(try_but_dont_catch),_Unwind_GetIP是下一个栈帧调用完成位置的IP。_Unwind_GetRegionStart把我们指向异常第一次被抛出的地方;解释起来有点复杂,我们后面会用到它,不是现在。同样,这里我们没有看到LSDA,但我们可以推断它在函数代码后,因为_Unwind_GetLanguageSpecificData直接指向函数结尾:

_Unwind_GetIP = (void *) 0x804861d
_Unwind_GetRegionStart = (void *) 0x8048612
_Unwind_GetLanguageSpecificData = (void *) 0x8048e3c
function pointer to try_but_dont_catch = 0x8048612 <try_but_dont_catch()> 

(gdb) disassemble /m try_but_dont_catch

Dump of assembler code for function try_but_dont_catch():
10  void try_but_dont_catch() {
        [...]
11      try {
12          raise();
   0x08048619 <+7>:   call   0x80485e8 <raise()>
13      } catch(Fake_Exception&) {
   0x08048651 <+63>:  call   0x804874a <__cxa_begin_catch()>
   0x08048665 <+83>:  call   0x804875e <__cxa_end_catch()>
   0x0804866a <+88>:  jmp    0x804861e <try_but_dont_catch()+12>
14          printf("Caught a Fake_Exception!\n");
   0x08048659 <+71>:  movl   $0x8048971,(%esp)
   0x08048660 <+78>:  call   0x80484c0 <puts@plt>
15      }
16 
17      printf("try_but_dont_catch handled the exception\n");
   0x0804861e <+12>:  movl   $0x8048948,(%esp)
   0x08048625 <+19>:  call   0x80484c0 <puts@plt>
18  }
   0x0804862a <+24>:  add    $0x24,%esp

在_Unwind_的帮助下,现在我们能够获得关于当前栈帧的足够信息来决定是否可以处理异常,以及我们应该如何处理它。在我们可以检测我们希望的着陆垫前,需要更多一步:我们需要解析在函数末尾的CFI(调用帧信息)。这是DWARF规范的部分,gdb也用于调试目的,这不是容易实现的规范。就像对我们的ABI那样,我们把它维持在最小程度。

C++异常的幕后11:阅读CFI表

要从我们已经为我们的ABI实现的personality函数里正确处理异常,我们需要阅读LSDA(语言特定数据区)来了解哪个调用帧(即哪个函数)可以处理哪个异常,以及了解哪里可以找到着陆垫(catch块)。LSDA是CFI格式的,我们将在本章里学习如何读它。

读CFI数据是相当直截了当的,但有一些我们需要首先考虑的陷阱。实际上,两个:

关于.gcc_except_table格式的文档非常少(实际上,我仅找到关于它的几封邮件),因此我们将需要读大量的源代码,并反汇编来理解它。
虽然格式本身不是特别复杂,它使用一个使得读这个表不那么直截了当的LEB编码。
就我所知,大多数DWARF代价像这样编码,使用LEB格式,对一头雾水的程序员看起来不错,在编码任意长的整数时节省代码空间。幸运地,在这里我们可以耍个小花招:大多数时间里,可以uint8_t来读LEB编码的数字,因为我们不准备处理大的异常表或任何类似的东西。

备注:你可以从我的github repo下载完整的源代码。

让我们直接从汇编分析CFI数据,然后看我们是否能构建某些在我们的personality函数上读它的东西。我将重命名这些标记,使它们对我们更友好些。LSDA有三部分,试着在下面找出它们:

.local_frame_entry:
    .globl  __gxx_personality_v0
    .section    .gcc_except_table,"a",@progbits
    .align 4

这个非常简单:它只是声明我们将使用__gxx_personality_v0作为全局对象,并让链接器知道我们准备为.gcc_except_table节声明内容的头部。继续:

.local_lsda_1:
    # This declares the encoding type. We don't care.
    .byte   0xff
    
    # This specifies the landing pads start; if zero, the func's ptr is
    # assumed (_Unwind_GetRegionStart)
    .byte   0 

    # Length of the LSDA area: check that LLSDATT1 and LLSDATTD1 point to the
    # end and the beginning of the LSDA, respectively
    .uleb128 .local_lsda_end - .local_lsda_call_site_table_header

现在这有更多一些信息。这些标签相当模糊,但确实遵循一个模式。LSDA表示语言特定数据区,前面的L表示本地,因此这是本地(对于编译单元,.o文件)语言特定数据区编号1。其他标记遵循类似的模式,但我还没有时间把它们算出来。不过,我们确实不需要。

.local_lsda_call_site_table_header:
    # Encoding of items in the landing pad table. Again, we don't care.
    .byte   0x1.

    # The length of the call site table (ie the landing pads)
    .uleb128 .local_lsda_call_site_table_end - .local_lsda_call_site_table

另一个单调的头。继续:

.local_lsda_call_site_table:
    .uleb128 .LEHB0-.LFB1
    .uleb128 .LEHE0-.LEHB0
    .uleb128 .L8-.LFB1
    .uleb128 0x1
    .uleb128 .LEHB1-.LFB1
    .uleb128 .LEHE1-.LEHB1
    .uleb128 0
    .uleb128 0
    .uleb128 .LEHB2-.LFB1
    .uleb128 .LEHE2-.LEHB2
    .uleb128 .L9-.LFB1
    .uleb128 0
.local_lsda_call_site_table_end:

这有趣得多,现在我们看到调用表本身。不管怎样,在所有这些项里,我们应该能够找到我们的着陆垫。根据一些网页,每个调用项的格式应该是:

struct lsda_call_site_entry {    
    size_t cs_start;    // Start of the IP range   
    size_t cs_len;      // Length of the IP range
    size_t cs_lp;        // Landing pad address
    size_t cs_action; // Offset into action table 
};

看起来了我们在正轨上,虽然我们还不知道为什么在我们仅定义了一个着陆垫时,有3个调用项。在任何情形里,我们可以耍点小花招:通过查看汇编,我们可以推断CFI上的所有值将小于128,这意味着在LEB编码中,它们可以作为uchar来读。这使得我们读CFI的代码大为容易,下次我们将看到如何在personality函数里使用它。

C++异常的幕后12:C++里的突然反射

我们的小ABI项目(链接)能够抛出异常了,现在我们致力于捕捉它们;上次我们实现了一个能够检测及处理异常的personality函数,但它仍然有点不完整:即使在应该停止时它能正确地通知栈回滚器,但我们版本的__gxx_personality_v0不能执行catch块里的代码。上次我们学会了如何读LSDA,因此现在仅有的问题是,把各部分拼起来,在我们的personality函数里读.gcc_except_table。

让我们回顾一下:我们弄明白了我们用于有我们希望运行捕捉的函数的LSDA具有下面的调用表(即,下面着陆垫【即,下面的catch块】):

.local_lsda_call_site_table:
    .uleb128 .LEHB0-.LFB1
    .uleb128 .LEHE0-.LEHB0
    .uleb128 .L8-.LFB1
    .uleb128 0x1

    .uleb128 .LEHB1-.LFB1
    .uleb128 .LEHE1-.LEHB1
    .uleb128 0
    .uleb128 0
    
    .uleb128 .LEHB2-.LFB1
    .uleb128 .LEHE2-.LEHB2
    .uleb128 .L9-.LFB1
    .uleb128 0
.local_lsda_call_site_table_end:

在我们函数的汇编代码里,所有这些表可以被映射到不同的位置,但对一篇博文这有点太混乱了(我建议你自己反汇编函数并尝试匹配每个标记,这样做可以学到很多东西)。同样,归功于某些网页,我们学到了这个表的格式。

让我们这样做来看我们是否在正轨上(小心读对齐问题,记住像这样定义CFI仅能对uint8工作,并可能不可移植):

struct LSDA_Header {
    uint8_t lsda_start_encoding;
    uint8_t lsda_type_encoding;
    uint8_t lsda_call_site_table_length;
};
struct LSDA_Call_Site_Header {
    uint8_t encoding;
    uint8_t length;
};
struct LSDA_Call_Site { 
    LSDA_Call_Site(const uint8_t *ptr) {
        cs_start = ptr[0];
        cs_len = ptr[1];
        cs_lp = ptr[2];
        cs_action = ptr[3];
    }
    uint8_t cs_start;
    uint8_t cs_len;
    uint8_t cs_lp;
    uint8_t cs_action;
};

_Unwind_Reason_Code __gxx_personality_v0 (
                     int version, _Unwind_Action actions, uint64_t exceptionClass,
                     _Unwind_Exception* unwind_exception, _Unwind_Context* context) {
    if (actions & _UA_SEARCH_PHASE) {
        printf("Personality function, lookup phase\n");
        return _URC_HANDLER_FOUND;
    } else if (actions & _UA_CLEANUP_PHASE) {
        printf("Personality function, cleanup\n");
        const uint8_t* lsda = (const uint8_t*)
                                    _Unwind_GetLanguageSpecificData(context);
        LSDA_Header *header = (LSDA_Header*)(lsda);
        LSDA_Call_Site_Header *cs_header = (LSDA_Call_Site_Header*)
                                                (lsda + sizeof(LSDA_Header));
        size_t cs_in_table = cs_header->length / sizeof(LSDA_Call_Site);

        // We must declare cs_table_base as uint8, otherwise we risk an unaligned access
        const uint8_t *cs_table_base = lsda + sizeof(LSDA_Header)
                                            + sizeof(LSDA_Call_Site_Header);
                                            + 
        // Go through every entry on the call site table
        for (size_t i=0; i < cs_in_table; ++i)  {
            const uint8_t *offset = &cs_table_base[i * sizeof(LSDA_Call_Site)];
            LSDA_Call_Site cs(offset);
            printf("Found a CS:\n");
            printf("\tcs_start: %i\n", cs.cs_start);
            printf("\tcs_len: %i\n", cs.cs_len);
            printf("\tcs_lp: %i\n", cs.cs_lp);
            printf("\tcs_action: %i\n", cs.cs_action);
        }
        uintptr_t ip = _Unwind_GetIP(context);
        uintptr_t funcStart = _Unwind_GetRegionStart(context);
        uintptr_t ipOffset = ip - funcStart;
        return _URC_INSTALL_CONTEXT;
    } else {
        printf("Personality function, error\n");
        return _URC_FATAL_PHASE1_ERROR;
    }
}

备注:你可以从我的github repo下载完整的源代码。

正如你看到的,如果你运行这个代码,调用表里所有的项是相关的。与什么相关?函数的开头。这意味着如果我们希望得到特定着陆垫的EIP,我们要做的是_Unwind_GetRegionStart + LSDA_Call_Site.cs_lp!

现在我们应该能解决我们的异常问题:让我们尝试修改我们的personality函数来运行正确的着陆垫。现在我们需要恢复执行:_Unwind_SetIP。让我们再次修改personality函数来运行第一个可用的着陆垫,这通过查看汇编我们已经知道我们想要的那个:

...
const uint8_t *cs_table_base = lsda + sizeof(LSDA_Header)
                                    + sizeof(LSDA_Call_Site_Header);
for (size_t i=0; i < cs_in_table; ++i) {
    const uint8_t *offset = &cs_table_base[i * sizeof(LSDA_Call_Site)];
    LSDA_Call_Site cs(offset);

    if (cs.cs_lp) {
        uintptr_t func_start = _Unwind_GetRegionStart(context);
        _Unwind_SetIP(context, func_start + cs.cs_lp);
        break;
    }
}
return _URC_INSTALL_CONTEXT;

尝试运行它,看到一个漂亮的无限循环。你能猜到哪里错了?答案在下一篇。

C++异常的幕后13:为着陆垫设置上下文

上次我们终于写出了几乎能工作的personality函数。我们可以检测每个有着陆垫可用的栈帧,然后告诉_Unwind_我们希望运行一个特定的着陆垫。我们遇到了一个小问题:虽然我们对_Unwind_设置了上下文以继续在正确的着陆垫上执行,我们没有在寄存器上设置正确的异常。结果,这意味着着陆垫将不知道应该处理哪个异常,因此它将说“我不能处理这个”。_Unwind_将说“请尝试下一个着陆垫”,但我们的ABI是如此简单,它不知道怎么找到下一个,只是尝试同一个。一次又一次。我们可能已经发明了一段时间以来最做作的例子(真的)!

让我们为着陆垫设置正确的上下文,并稍微整理一下我们的ABI:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

namespace __cxxabiv1 {
    struct __class_type_info {
        virtual void foo() {}
    } ti;
}

#define EXCEPTION_BUFF_SIZE 255
char exception_buff[EXCEPTION_BUFF_SIZE]; 

extern "C" {
void* __cxa_allocate_exception(size_t thrown_size) {
    printf("alloc ex %i\n", thrown_size);
    if (thrown_size > EXCEPTION_BUFF_SIZE) printf("Exception too big");
    return &exception_buff;
}
void __cxa_free_exception(void *thrown_exception);


#include <unwind.h>
typedef void (*unexpected_handler)(void);
typedef void (*terminate_handler)(void);

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

void __cxa_throw(void* thrown_exception,
                 struct type_info *tinfo,
                 void (*dest)(void*)) {
    printf("__cxa_throw called\n");
    __cxa_exception *header = ((__cxa_exception *) thrown_exception - 1);
    _Unwind_RaiseException(&header->unwindHeader);
    
    // __cxa_throw never returns
    printf("no one handled __cxa_throw, terminate!\n");
    exit(0);
}

void __cxa_begin_catch() {
    printf("begin FTW\n");
}
void __cxa_end_catch() {
    printf("end FTW\n");
}

/***********************************************************************/
/**
 * The LSDA is a read only place in memory; we'll create a typedef for
 * this to avoid a const mess later on; LSDA_ptr refers to readonly and
 * &LSDA_ptr will be a non-const pointer to a const place in memory
 */

typedef const uint8_t* LSDA_ptr;

struct LSDA_Header {
    /**
     * Read the LSDA table into a struct; advances the lsda pointer
     * as many bytes as read
     */
    LSDA_Header(LSDA_ptr *lsda) {
        LSDA_ptr read_ptr = *lsda;        
        start_encoding = read_ptr[0];  // Copy the LSDA fields
        type_encoding = read_ptr[1];
        ttype = read_ptr[2];       
        *lsda = read_ptr + sizeof(LSDA_Header);  // Advance the lsda pointer
    }
    uint8_t start_encoding;
    uint8_t type_encoding;
    uint8_t ttype;
};

struct LSDA_CS_Header {
    // Same as other LSDA constructors
    LSDA_CS_Header(LSDA_ptr *lsda) {
        LSDA_ptr read_ptr = *lsda;
        encoding = read_ptr[0];
        length = read_ptr[1];
        *lsda = read_ptr + sizeof(LSDA_CS_Header);
    }
    uint8_t encoding;
    uint8_t length;
};

struct LSDA_CS {
    // Same as other LSDA constructors
    LSDA_CS(LSDA_ptr *lsda) {
        LSDA_ptr read_ptr = *lsda;
        start = read_ptr[0];
        len = read_ptr[1];
        lp = read_ptr[2];
        action = read_ptr[3];
        *lsda = read_ptr + sizeof(LSDA_CS);
    }

    // Note start, len and lp would be void*'s, but they are actually relative
    // addresses: start and lp are relative to the start of the function, len
    // is relative to start
    // Offset into function from which we could handle a throw    
    uint8_t start;     
    
    uint8_t len; // Length of the block that might throw 
    uint8_t lp; // Landing pad

    // Offset into action table + 1 (0 means no action) Used to run destructors
   uint8_t action;
};

/*********************************************************************/

_Unwind_Reason_Code __gxx_personality_v0 (
                             int version,
                             _Unwind_Action actions,
                             uint64_t exceptionClass,
                             _Unwind_Exception* unwind_exception,
                             _Unwind_Context* context) {
    if (actions & _UA_SEARCH_PHASE) {
        printf("Personality function, lookup phase\n");
        return _URC_HANDLER_FOUND;
    } else if (actions & _UA_CLEANUP_PHASE) {
        printf("Personality function, cleanup\n");
        
        // Pointer to the beginning of the raw LSDA
        LSDA_ptr lsda = (uint8_t*)_Unwind_GetLanguageSpecificData(context);
        
        // Read LSDA headerfor the LSDA
        LSDA_Header header(&lsda);

        // Read the LSDA CS header
        LSDA_CS_Header cs_header(&lsda);

        // Calculate where the end of the LSDA CS table is
        const LSDA_ptr lsda_cs_table_end = lsda + cs_header.length;

        // Loop through each entry in the CS table
        while (lsda < lsda_cs_table_end) {
            LSDA_CS cs(&lsda);
            if (cs.lp)  {
                int r0 = __builtin_eh_return_data_regno(0);
                int r1 = __builtin_eh_return_data_regno(1);
                _Unwind_SetGR(context, r0, (uintptr_t)(unwind_exception));

                // Note the following code hardcodes the exception type;
                // we'll fix that later on
                _Unwind_SetGR(context, r1, (uintptr_t)(1));

                uintptr_t func_start = _Unwind_GetRegionStart(context);
                _Unwind_SetIP(context, func_start + cs.lp);
                break;
            }
        }
        return _URC_INSTALL_CONTEXT;
    } else {
        printf("Personality function, error\n");
        return _URC_FATAL_PHASE1_ERROR;
    }
}
}

备注:LSDA更细致的模式戳这里,完整代码在我的github repo。

最后,它工作了。如果运行它你应该看到像这样的东西:

$./app
alloc ex 1
__cxa_throw called
Personality function, lookup phase
Personality function, cleanup
begin FTW
Caught a Fake_Exception!
end FTW
try_but_dont_catch handled the exception
catchit handled the exception

当然我们有点依赖_Unwind_:这里我们说我们将处理每个异常,不管是什么。这把我们的catch(Exception&)转换为catch(…),如果调用帧里的第一个函数没有catch语句,就要天下大乱了。不过,仍然,对一个非常简单的ABI,我们到达了第一个里程碑。

现在我们能改进它,使得它在正确的帧上处理正确的异常吗?也许下次。

C++异常的幕后14:多个着陆垫与大师的教导

在大量困难的工作后,上次我们最终得到了可工作的personality函数,它无需libstdc++就能处理异常。它将不加区分地处理所有异常,但它能工作。我们尚有一个大问题还没回答:如果我们回到LSDA(语言特定数据区),我们将看到像这样的东西:

.local_lsda_call_site_table:
    .uleb128 .LEHB0-.LFB1
    .uleb128 .LEHE0-.LEHB0
    .uleb128 .L8-.LFB1
    .uleb128 0x1
 
    .uleb128 .LEHB1-.LFB1
    .uleb128 .LEHE1-.LEHB1
    .uleb128 0
    .uleb128 0

    .uleb128 .LEHB2-.LFB1
    .uleb128 .LEHE2-.LEHB2
    .uleb128 .L9-.LFB1
    .uleb128 0
.local_lsda_call_site_table_end:

这里定义了3个着陆垫,即使我们仅写了一条try/catch语句。这里发生了什么?

如果你仔细阅读这个话题的上一篇文章,你可能注意到我向struct LSDA_CS的定义增加了一些注释:

struct LSDA_CS {
    // Note start, len and lp would be void*'s, but they are actually relative
    // addresses: start and lp are relative to the start of the function, len
    // is relative to start
    // Offset into function from which we could handle a throw
    uint8_t start;
    uint8_t len;    // Length of the block that might throw
    uint8_t lp;     // Landing pad
    uint8_t action;  // Offset into action table + 1 (0 means no action) Used to run destructors
};

备注:你可以从我的github repo下载完整的源代码。

这里发生了一些有趣的事情,但让我们首先通过下面的例子按域分析这个结构体:

void foo() {
    L0:
        try {
            do_something();
    L1:
        } catch (const Exception1& ex) {
            ...
        } catch (const Exception2& ex) {
            ...
        } catch (const ExceptionN& ex) {
            ...
        } catch (...) {
        }
    L2:
}

lp:从函数开头到着陆垫开头的偏移。下面例子的lp的值将是L1 – addr_of(foo)
action:到action表的偏移。这用于在回滚栈的同时执行清理活动。我们还没研究到这一点,目前可以忽略它。
start:从函数开头到try块开始的偏移:在这个例子里,这是L0 – addr_of(foo)
len:try块的长度。在这个例子里这将是L1 – L0
现在感兴趣的域是start与len:在有多个try/catch块的函数里,通过检查当前帧的指令指针是否在start与start + len之间,我们可以知道是否应该处理一个异常。

这解决了带有多个try/catch块的函数如何可以处理异常的谜案,但我们仍然有另一个问题:为什么在我们仅声明一个着陆垫时,有三个调用点?另外三个是可能抛出异常的地方,因此它们被添加为清理活动或着陆垫可能的地方。如果我们从GOTW学到了什么,那就是异常会在我们最意想不到的地方抛出。在调用表中对我们的抛出有一项,因为它是可能抛出的块;编译器也检测到了另外三个。

现在我们知道了start与field域作何用处,让我们修改personality函数,使得正确的着陆垫可以处理抛出的异常。前进吧。我的实现在下一篇文章里。

C++异常的幕后15:找到正确的着陆垫

这是我在这个博客里所写的最长系列文章中的第15篇;目前我们已经了解到异常如何抛出,并且已经编写了通过某种反射,能够检测catch块(在异常术语里,着陆垫)在何处的personality函数。在上一篇文章里,我们编写了可以处理异常的personality函数,但它仅对栈上第一个调用帧的第一个着陆垫这样做。让我们稍作改进,使得personality函数能够在具有多个着陆垫的函数里选择正确的那个。

以一个TDD风格,我们首先为ABI构建一个测试。让我们修改我们的测试程序,throw.cpp,以拥有两个try/catch块:

#include <stdio.h>
#include "throw.h"
struct Fake_Exception {};

void raise() {
    throw Exception();
} 

void try_but_dont_catch() {
    try {
        printf("Running a try which will never throw.\n");
    } catch(Fake_Exception&) {
        printf("Exception caught... with the wrong catch!\n");
    }
    
    try {
        raise();
    } catch(Fake_Exception&) {
        printf("Caught a Fake_Exception!\n");
    }
    printf("try_but_dont_catch handled the exception\n");
}

 

void catchit() {
    try {
        try_but_dont_catch();
    } catch(Fake_Exception&) {
        printf("Caught a Fake_Exception!\n");
    } catch(Exception&) {
        printf("Caught an Exception!\n");
    }
    printf("catchit handled the exception\n");
}
extern "C" {
    void seppuku() {
        catchit();
    }
}

在测试之前,尝试想一下运行这个测试时将会发生什么。关注在try_but_dont_catch函数:第一个try/catch块不抛出异常,而第二个块将抛出。因为我们的ABI相当蠢,第一个块将处理第二个块的异常。在第一个块处理完异常后将发生什么?执行将从catch/try结束的地方恢复,因而再次进入第二个try/catch块。无限循环!我们又重新发明了一个非常复杂的while(真的)。

让我们使用调用表(LSDA)中start/length域的知识来正确选择着陆垫。对此,我们需要知道在抛出异常是指令指针是什么,我们可以使用我们已经知道的_Unwind_函数:_Unwind_GetIP。要理解_Unwind_GetIP将返回什么,让我们看一个例子:

void f1() {}
void f2() { throw 1; }
void f3() {}
void foo() {
L1:
    try{ f1(); } catch(...) {}
L2:
    try{ f2(); } catch(...) {}
L3:
    try{ f3(); } catch(...) {}
}

在这个情形里,将对f2的catch块调用我们的personality函数,栈将像这样:

+---------------------------------+

|   IP: f2  stack frame: f2    |

+---------------------------------+

|   IP: L3 stack frame: foo   |

+---------------------------------+

注意IP将在L3,但异常将在L2抛出;这是因为IP将指向要执行的下一条指令。这也意味着我们需要减一,如果需要找出异常抛出处的IP,否则_Unwind_GetIP的结果将不在我们着陆垫的范围里。回到我们的personality函数:

_Unwind_Reason_Code __gxx_personality_v0 (
                             int version,
                             _Unwind_Action actions,
                             uint64_t exceptionClass,
                             _Unwind_Exception* unwind_exception,
                             _Unwind_Context* context) {
    if (actions & _UA_SEARCH_PHASE) {
        printf("Personality function, lookup phase\n");
        return _URC_HANDLER_FOUND;
    } else if (actions & _UA_CLEANUP_PHASE) {
        printf("Personality function, cleanup\n");

        // Calculate what the instruction pointer was just before the
        // exception was thrown for this stack frame
        uintptr_t throw_ip = _Unwind_GetIP(context) - 1;

        // Pointer to the beginning of the raw LSDA
        LSDA_ptr lsda = (uint8_t*)_Unwind_GetLanguageSpecificData(context);

        // Read LSDA headerfor the LSDA
        LSDA_Header header(&lsda);

        // Read the LSDA CS header
        LSDA_CS_Header cs_header(&lsda);

        // Calculate where the end of the LSDA CS table is
        const LSDA_ptr lsda_cs_table_end = lsda + cs_header.length;

        // Loop through each entry in the CS table
        while (lsda < lsda_cs_table_end) {
            LSDA_CS cs(&lsda);
            // If there's no LP we can't handle this exception; move on
            if (not cs.lp) 
              continue;            
            uintptr_t func_start = _Unwind_GetRegionStart(context);

            // Calculate the range of the instruction pointer valid for this
            // landing pad; if this LP can handle the current exception then
            // the IP for this stack frame must be in this range
            uintptr_t try_start = func_start + cs.start;
            uintptr_t try_end = func_start + cs.start + cs.len;
            
            // Check if this is the correct LP for the current try block
            if (throw_ip < try_start) continue;
            if (throw_ip > try_end) continue;
            
            // We found a landing pad for this exception; resume execution
            int r0 = __builtin_eh_return_data_regno(0);
            int r1 = __builtin_eh_return_data_regno(1);
            _Unwind_SetGR(context, r0, (uintptr_t)(unwind_exception));

            // Note the following code hardcodes the exception type;
            // we'll fix that later on
            _Unwind_SetGR(context, r1, (uintptr_t)(1));
            _Unwind_SetIP(context, func_start + cs.lp);
            break;
        }
        return _URC_INSTALL_CONTEXT;
    } else {
        printf("Personality function, error\n");
        return _URC_FATAL_PHASE1_ERROR;
    }
}

备注:你可以从我的github repo下载完整的源代码。

再测试这个例子,没有无限循环了!这是一个简单改动,现在我们可以选择正确的着陆垫。下次我们将尝试使得我们的personality函数还能挑选正确的栈帧,而不是选择第一个。

C++异常的幕后16:在着陆垫里找到正确的捕捉

应我们要求第16章要实现能够处理异常的小ABI;上次我们实现了personality函数,使它能够处理有多个着陆垫的函数。现在我们尝试使它识别某个着陆垫是否能处理指定的异常,因此我们可以在catch语句上使用异常说明。

当然,要知道一个着陆垫是否能处理一个异常是困难的任务。你还想要什么吗?目前要克服的最大问题是:

首先:我们如何找出一个catch块接受的类型?
假设我们可以找出一个catch的类型,我们怎样处理一个catch(…)?
对带有多条catch语句的着陆垫,我们怎样知道所有可能的catch类型?
考虑下面的例子:

struct Base {};
struct Child : public Base {};
void foo() { throw Child; }

void bar() {
    try { foo(); }
    catch(const Base&){ ... }
}

我们不仅要检查着陆垫是否接受当前异常,还要检查它是否接受当前异常的任意父类!
要使我们的工作更轻松,假设我们现在只使用有一个catch的着陆垫,且在我们程序里没有继承。同样,我们怎样找出着陆垫接受的类型?

在.gcc_except_table里有一个地方我们还没分析:活动表。让我们反汇编我们的throw.cpp对象,看那里有什么,就在调用表之后,对我们的“try but don’t catch“函数:

备注:你可以从我的github repo下载完整的源代码。

.LLSDACSE1:
    .byte   0x1
    .byte   0
    .align 4
    .long   _ZTI14Fake_Exception
.LLSDATT1:

看起来不太像,但一定有一个指针(一个众所周知的真实指针)指向具有我们异常名字的某个东西。让我们去到_ZTI14Fake_Exception的定义:

_ZTI14Fake_Exception:
    .long   _ZTVN10__cxxabiv117__class_type_infoE+8
    .long   _ZTS14Fake_Exception
    .weak   _ZTS9Exception
    .section    .rodata._ZTS9Exception,"aG",@progbits,_ZTS9Exception,comdat
    .type   _ZTS9Exception, @object
    .size   _ZTS9Exception, 11

我们得到了一些非常有趣的结果。你能识别出它吗?这是用于结构体Fake_Exception的std:: type_info!

现在我们知道确实有一种方法可以让指针指向我们异常的某种反射信息。我们能通过编程找到它吗?下次见。

C++异常的幕后17:异常类型的发射以及读.gcc_except_table

目前为止我们知道在抛出一个异常时,通过阅读本地储存区,即.gcc_except_table,我们可以得到许多反射信息;读这个表我们已经能够实现能决定在抛出一个异常时运行哪个着陆垫的personality函数。我们还知道如何读LSDA的活动表部分,因此我们应该能够修改personality函数在带有多个catch的着陆垫内选择正确的catch语句。

上次我们留下了我们的ABI实现,并把一些时间用于分析.gcc_except_table的汇编来发掘我们如何找出catch可以处理的类型。我们发现这张表的部分确实保存了一个类型列表,可以找到这个消息。让我们尝试在清理阶段读它,但首先让我们回忆LSDA头的定义:

struct LSDA_Header {
    uint8_t start_encoding;
    uint8_t type_encoding;

    // This is the offset, from the end of the header, to the types table
    uint8_t type_table_offset;
};

最后一个域是新的(对我们):它向我们给出了类型表的一个偏移。让我们回忆一下每个调用点的定义:

struct LSDA_CS {
    uint8_t start; // Offset into function from which we could handle a throw
    uint8_t len;   // Length of the block that might throw
    uint8_t lp;    // Landing pad
    uint8_t action;    // Offset into action table + 1 (0 means no action)
};

检查最后这个域“action”。这向我们给出活动表的一个偏移。这意味着我们可以对一个特定CS找到该活动。这里的技巧是对存在一个catch的着陆垫,这个活动将保存该类型表的偏移;我们可以使用类型表指针的偏移,这我们可以从头部获得。相当拗口:让我们更好地讨论代码:

// Pointer to the beginning of the raw LSDA
LSDA_ptr lsda = (uint8_t*)_Unwind_GetLanguageSpecificData(context);

// Read LSDA headerfor the LSDA
LSDA_Header header(&lsda);
const LSDA_ptr types_table_start = lsda + header.type_table_offset;

// Read the LSDA CS header
LSDA_CS_Header cs_header(&lsda);

// Calculate where the end of the LSDA CS table is
const LSDA_ptr lsda_cs_table_end = lsda + cs_header.length;

// Get the start of action tables
const LSDA_ptr action_tbl_start = lsda_cs_table_end;

// Get the first call site
LSDA_CS cs(&lsda);

// cs.action is the offset + 1; that way cs.action == 0
// means there is no associated entry in the action table
const size_t action_offset = cs.action - 1;
const LSDA_ptr action = action_tbl_start + action_offset;

// For a landing pad with a catch the action table will
// hold an index to a list of types
int type_index = action[0];

// types_table_start actually points to the end of the table, so
// we need to invert the type_index. There we'll find a ptr to
// the std::type_info for the specification in our catch
const void* catch_type_info = types_table_start[ -1 * type_index ];
const std::type_info *catch_ti = (const std::type_info *) catch_type_info;

// If everything went OK, this should print something like Fake_Exception
printf("%s\n", catch_ti->name());

代码看起来复杂,因为在到达结构体type_info之前有几层间接层,不过这并不复杂:它只是读我们在汇编里找到的.gcc_except_table。

打印类型名是正确方向上前进的一大步。同样,我们的personality函数变得有点混乱。读LSDA的大部分复杂性可以隐藏在底下,几乎没有代价。你可以在这里检查我的实现。

下次我们将看看是否能把新发现的类型匹配到原有的异常。

C++异常的幕后18:获取正确的栈帧

我们最新的personality函数知道它是否可以处理一个异常(假设每个try块仅有一条catch语句,并假设没有使用继承),不过要使得这个知识有用,我们首先需要检查我们可以处理的异常是否匹配抛出的异常。我们试着做一下。

当然,我们首先需要知道异常类型。要这样做,在调用__cxa_throw时,我们需要保存异常类型(这是ABI给予我们设置所有自定义数据的机会):

备注:你可以从我的github repo下载完整的源代码。

void __cxa_throw(void* thrown_exception,
                 std::type_info *tinfo,
                 void (*dest)(void*)) {
    __cxa_exception *header = ((__cxa_exception *) thrown_exception - 1); 

    // We need to save the type info in the exception header _Unwind_ will
    // receive, otherwise we won't be able to know it when unwinding
    header->exceptionType = tinfo;
    _Unwind_RaiseException(&header->unwindHeader);
}

现在我们可以在personality函数里读异常类型,很容易检查是否异常类型是否匹配(异常名是C++字符串,因此执行一次==足够了):

// Get the type of the exception we can handle
const void* catch_type_info = lsda.types_table_start[ -1 * type_index ];
const std::type_info *catch_ti = (const std::type_info *) catch_type_info;

// Get the type of the original exception being thrown
__cxa_exception* exception_header = (__cxa_exception*)(unwind_exception+1) - 1;
std::type_info *org_ex_type = exception_header->exceptionType;

printf("%s thrown, catch handles %s\n",
            org_ex_type->name(),
            catch_ti->name()); 

// Check if the exception being thrown is of the same type
// than the exception we can handle
if (org_ex_type->name() != catch_ti->name())
    continue;

这里有新修改的完整源代码。当然如果我们加入这,将有问题(你能看出来吗)。如果在两阶段里抛出异常,并且我们说在第一阶段我们能处理它,那么我们不能在第二阶段再说我们不想要它。我不知道_Unwind_是否根据任何文档处理这个情形,但这最有可能要求未定义的行为,因此只是说我们将处理一切是不足够的。

因为我们给予了personality函数知道着陆垫是否可以处理要抛出异常的能力,我们一直欺骗_Unwind_我们可以处理的异常;即使在我们的ABI 9上我们说处理所有的异常,真相是我们不知道我们是否能够处理它。这很容易修改,我们可以像这样做:

_Unwind_Reason_Code __gxx_personality_v0 (...) {
    printf("Personality function, searching for handler\n"); 

    // ...
    foreach (call site entry in lsda)  {
        if (call site entry.not_good()) continue;
        
        // We found a landing pad for this exception; resume execution
        // If we are on search phase, tell _Unwind_ we can handle this one
        if (actions & _UA_SEARCH_PHASE) return _URC_HANDLER_FOUND;

        // If we are not on search phase then we are on _UA_CLEANUP_PHASE
        /* set everything so the landing pad can run */
        return _URC_INSTALL_CONTEXT;
    }
    return _URC_CONTINUE_UNWIND;
}

同样,在我的github repo里获取项目的源代码。

好了,如果运行带有这个修改的personality函数我们会得到什么?失败了,这就是我们得到的!记得我们的抛出函数吗?这个应该捕捉我们的异常:

void catchit() {
    try {
        try_but_dont_catch();
    } catch(Fake_Exception&) {
        printf("Caught a Fake_Exception!\n");
    } catch(Exception&) {
        printf("Caught an Exception!\n");
    }
    printf("catchit handled the exception\n");
}

不幸,我们的personality函数仅检查着陆垫可以处理的第一个类型。如果我们删除Fake_Exception块再尝试,我们得到另一位故事:最终,成功了!我们的personality函数现在可以在正确的帧里选择正确的catch,只要没有带有多个catch的try块。

下一次我们将进一步改进它。

C++异常的幕后19:在着陆垫里获取正确的捕捉

关于C++异常处理的第19篇文章:我们已经编写了一个personality函数。目前为止,它通过读LSDA,能够在正确的栈帧上选择正确的着陆垫以处理抛出异常,但在一个着陆垫里找出正确的catch有些困难。为了最终得到一个合适的personality合适,我们需要仔细查阅.gcc_except_table里的所有活动表,查看异常可以处理的所有类型。

记得活动表吗?让我们再来看它,不过这次对一个带有多个catch块的try。

 **Call site table**

.LLSDACSB2:
    # Call site 1
    .uleb128 ip_range_start
    .uleb128 ip_range_len
    .uleb128 landing_pad_ip
    .uleb128 (action_offset+1) => 0x3
     
    # Rest of call site table
 **Action table start**

.LLSDACSE2:
    # Action 1
    .byte   0x2
    .byte   0

    # Action 2
    .byte   0x1
    .byte   0x7d
    .align 4
    .long   _ZTI9Exception
    .long   _ZTI14Fake_Exception

.LLSDATT2:

 **Types table start**

如果我们旨在上面的例子里读着陆垫支持的异常(顺便提一下,就是catchit函数的LSDA),我们需要像这样做:

从调用表获取活动偏移,2:记住你将实际读偏移加1,因此0意味着没有活动。
去往活动偏移2,获取类型索引1。类型表以反序索引(即我们有一个指向它末尾的指针,我们需要使用-1 * index来访问每个元素)。
去到types_table[-1];对Fake_Exception你讲得到一个指向type_info的指针。
Fake_Exception不是要抛出的当前异常;得到我们当前活动(0x7d)下一个活动的偏移
以uleb128读0x7d实际产生-3;从我们读这个偏移的位置回退3字节找到下一个活动
读类型索引2
这次得到Exception的type_infp;它匹配要抛出的当前异常,因此我们可以设置着陆垫
这听起来复杂,因为每一步有许多间接成分,但你可以在我的github repo里查看这个项目的完整代码。

在上面的链接里你还将看到一个红利:修改personality函数正确地检测及使用catch(…)块。这是一个简单的改变,因为personality函数知道如何读类型表:带有一个空指针的类型(即在这个表里的位置不是保存std::type_info的有效指针,而是空指针)表示一个catch all块。这有一个有趣的副作用:catch(T)将能够仅处理原生(即来自C++)异常,而catch(…)也能捕捉不是从C++里抛出的异常。

最终我们知道异常如何抛出,栈如何回滚,personality函数如何选择正确的栈帧来处理异常,以及如何选择着陆垫里正确的catch,但我们仍然有更多的问题要解决:运行析构函数。下次我们将修改personality函数来支持RAII对象。

C++异常的幕后20:在回滚时运行析构函数

上次我们编写的小ABI版本11能够处理相当多处理异常的基本情况:我们有一个(几乎奏效的)能够抛出与捕捉异常的ABI,但它仍然不能正确运行析构函数。如果我们希望编写异常安全代码,这相当重要。通过我们对.gcc_except_table的了解,运行析构函数小菜一碟,我们只需看一点汇编:

# Call site table

.LLSDACSB2:

    # Call site 1

    .uleb128 ip_range_start

    .uleb128 ip_range_len

    .uleb128 landing_pad_ip

    .uleb128 (action_offset+1) => 0x3

     

    # Rest of call site table

 

# Action table start
.LLSDACSE2:
    # Action 1
    .byte   0
    .byte   0

    # Action 2
    .byte   0x1
    .byte   0x7d
    .align 4
    .long   _ZTI14Fake_Exception

.LLSDATT2:
# Types table start

在一个规范的着陆垫上,当一个活动有大于0的类型索引时,它意味着我们看到类型表的一个索引,我们可以使用它来了解该catch可以处理哪些类型;对于具有值0的类型索引,这意味着我们看到一个清理块,无论如何都要运行它。虽然着陆垫不能处理异常,它将仍然能够执行清理,在回滚时这预期发生。当然,在完成清理时,着陆垫将调用_Unwind_Resume,这将继续正常的栈回滚过程。

我已经将这个最新版本上传到我的githut repo里,不过有有些新的坏消息:记得我们如何通过说uleb128 == char来欺骗的吗?一旦我们开始增加块来运行析构函数,.gcc_except_table开始变得相当大(“大”意味着我们有超过127字节长度的偏移),这个假设不再成立。

对这个ABI的下一个版本,我们必须重写读LSDA的函数来读正确的uleb128码。不是一个大的改动,但此时我们不会有太大的收获,我们已经实现了我们的目标:一个无需libcxxabi辅助能够处理异常的工作的最小ABI。

存在我们还没涉及的部分,像处理非原生异常,捕捉衍生类型以及编译器与链接器间的互操作。也许在其他时间,在这个相当长的系列里,我们已经学到相当多的了C++里底层如何处理异常的知识。

C++异常的幕后21:总结与一些最后的思考

在撰写了二十余篇关于C++底层异常处理的文章之后,是时候回顾并做出有些最终思考。我们学了什么,异常如何抛出,以及如何捕捉它?

抛开读.gcc_except_table令人恶心的细节,它可能是这些文章里最大的部分,我们可以把整个过程总结成这样:

C++编译器实际上对处理异常贡献甚少,大多数魔术发生在libstdc++里。
不过编译器做了一些事。即:
它创建了CFI信息来回滚栈。
它创建称为.gcc_except_table的事物,带有着陆垫的信息(try/catch块)。类似反射信息。
在我们写一条throw语句时,编译器将把它翻译为一对libstdc++函数的调用,它分配异常,然后通过调用libstdc开始栈回滚过程。
当在运行时抛出一个异常时,将调用__cxa_throw,它把栈回滚委托给libstdc。
随着回滚器通过栈,它将调用libstdc++提供的一个特殊函数(称为personality例程),它检查栈上的每个函数有哪些可以捕捉异常。
如果没有找到该异常匹配的catch,调用std::terminate。
如果找到一个匹配的catch,回滚器在栈定再次开始。
随着回滚器第二次通过栈,它将要求personality例程为这个方法执行清理。
Personality例程为当前方法检查.gcc_except_table。如果有任何清理活动要执行,它将“跳入”当前栈帧并运行清理代码。这将执行在当前作用域里分配的每个对象的析构函数。
一旦回滚器到达可以处理该异常的栈帧,它将跳到正确的catch语句。
在完成catch语句的执行后,将调用一个清理函数来释放该异常的内存。
学习了异常如何工作,现在我们可以更好地回答为什么编写异常安全代码是困难的。

虽然概念上清晰,异常相当程度上是“鬼魅般的超视距作用”。抛出与捕捉异常涉及相当程度的反射(就程序必须分析自身而言),这对C++应用程序不常见。

即使我们讨论更高级的语言,抛出一个异常意味着我们不能依赖再依赖于对正常程序执行流应该如何工作的理解:我们习惯于具有一些条件操作符分支或调用其他函数的相当线性的执行流。使用异常,这不再成立:不是我们应用程序代码的实体控制着执行,并且它在程序到处走动,这里执行某些块,那里执行某些块,不遵循如何普通的规则。指令指针被每个着陆垫改变,栈以我们不能控制的方式回滚,最终幕后发生了许多魔术。

进一步总结:异常之所以困难,仅仅是因为它们破坏了我们所理解的程序的自然流程。这不表示它们内在是坏的,因为正确使用异常肯定会得到更干净的代码,但它们总是应该小心使用。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值