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

原文地址:https://monoinfinito.wordpress.com/2013/02/19/c-exceptions-under-the-hood-3-an-abi-to-appease-the-linker/

作者:nicolasbrailo 

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

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

1. > gcc main.o throw.o -o app

2. throw.o: In function `foo()':

3. throw.cpp:4: undefined reference to `__cxa_allocate_exception'

4. throw.cpp:4: undefined reference to `__cxa_throw'

5. throw.o:(.rodata._ZTI9Exception[typeinfo for Exception]+0x0): undefined reference to `vtable for __cxxabiv1::__class_type_info'

6. 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_infovtable

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

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

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

#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");

    // __cxa_throw never returns

    exit(0);

}

 

} // extern "C"

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

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

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

1

2

3

4

5

6

7

8

9

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仅能抛出异常。我们可以扩展它来处理一个捕捉吗?我们下一节来看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值