小窥__gxx_personality

前几天在Hopper的pesudo code里面看到这样一句,

r0 = *__gxx_personality_sj0;

当时没明白是做什么的,今天正好有空,搬来了梯子请出Google老师。

  • 它与Exception Handling相关

在Google之后,我发现了一个stackoverflow上的问题[1],鉴于该问题中提到的编译器给出的错误提示,

out/kernel.o:(.eh_frame+0x11):undefined reference to '__gxx_personality_v0'

可以推断出__gxx_personality和.eh_frame是有关的。

那么什么是.eh_frame呢?
根据Ervin在他博客[2]中收集的信息,我们可以知道.eh_frame是处理Exception Handling的,于是果断写一段代码测试之。

#include <iostream>

using namespace std;
int main(int argc, char *argv[]) {
    throw exception();
}

T1-Terminal

在MachOView中,我们可以看到的确是生成了.eh_frame这个section(如图T1-MachOView),但不幸的是在Hopper中并没有找到__gxx_personality相关的内容(如图T1-Hopper)。

T1-MachOView
T1-MachOView
T1-Hopper
T1-Hopper

修改一下程序,

#include <iostream>

using namespace std;
int main(int argc, char *argv[]) {
    try {
        throw exception();
    } catch (const std::exception& e) {
    
    }
}

编译后在Hopper中验证(如图T2-Hopper),的确找到了__gxx_personality。

T2-Hopper
T2-Hopper

也就是说,只要存在try {} catch ()结构或者是有throw都会生成.eh_frame。但当且仅当throw位于try block内部时,编译器才会使用到__gxx_personality。

如果有兴趣可以读一下.eh_frame section的格式细节[3]

  • __gxx_personality

刚才确认了__gxx_personality是用于Exception Handling的,那么它是怎么工作的呢,或者说,它的作用是什么呢?

既然提到了Exception Handling,那就少不了stack unwinding。先来对stack unwinding做一个简短的介绍吧。

stack unwinding的其中一个作用就是每当离开一个{ }时,将{ }的local var清空的过程(不包含使用new运算符生成的变量),比如调用destructor,这个过程与这些local var的constructor过程是反向的。如下图。

leak

可以看到

Constructor f1
Constructor f2
Constructor f3
Constructor f4
Deconstructor f4
Deconstructor f3
exception

f4和f3在离开try block之后就被析构了,而f1和f2没能被delete,内存泄漏了。如下图。

leaks

那么要怎么做才能保证不造成内存泄漏呢?稍后介绍,现在先让我们回到__gxx_personality的问题上来。
根据NCTU上的一份记录[4],我们可以将__gxx_personality和C personality routine等同起来。__gxx_personality的前缀gxx表明它是C++专属。

Personality routine主要做什么呢?

1)检查当前函数是否有相应的catch语句。
2)清理当前函数中的局部变量。

在unwind.h中可以看到Personality routine需要的参数有五个,

typedef _Unwind_Reason_Code(*_Unwind_Personality_Fn)
            (int,
             _Unwind_Action,
             _Unwind_Exception_Class,
             struct _Unwind_Exception *, 
             struct _Unwind_Context *);
typedef _Unwind_Personality_Fn __personality_routine;

在刚才NCTU的记录中,iant给出了详细的stack unwinding步骤[5]

而要做这两件事还需要编译器在编译时分析出的数据进行协助,而这些信息位于.gcc_except_table section里,至于unwinder和personality routine具体是怎么干的,可以参考twoon的博文《c++ 异常处理(2)》[6]

说了这么多,要怎么处理刚才有可能的泄漏呢?

其实很简单,只需要利用一下cleanup这个__attribute__就行,修改后的程序如下,

#include <iostream>
#include <string>
 
using namespace std;
 
class A {
public:
    A(string name) : name(name) {
        printf("Constructor %s\n", name.c_str());
    }
    ~A() {
        printf("Deconstructor %s\n", name.c_str());
    }
private:
    string name;
};

void clean(A ** x) {
    delete *x;
    printf("delete\n");
}

void mayThrow() {
    printf("throw!\n");
    throw exception();
}

int main(int argc, char *argv[]) {
    try {
        A * obj1 __attribute__((cleanup(clean))) = new A("f1");
        A * obj2 __attribute__((cleanup(clean))) = new A("f2");
        A obj3("f3");
        A obj4("f4");
        // 可能会抛出异常, 但是不会发生泄漏
        // 由于我们已经给obj1, obj2附上了cleanup的属性
        // 我们将在clean函数里面进行delete, 此处不再需要delete
        mayThrow();
    } catch (exception e) {
        printf("Exception\n");
    }
    printf("No leakage\n");
}

  • 总结

__gxx_personality就是Personality routine在C++下的实现,它是一个函数指针。

它的主要作用就是在Exception Handling时,清除当前stack frame上的local var,并且配合.gcc_except_table里的数据重构上一个stack frame。

大致的过程正如CesarB在stackoverflow上的答案[7]中所写,

Most of it is hidden within __cxa_throw, which must:

  • Walk the stack with the help of the exception tables until it finds a handler for that exception.
  • Unwind the stack until it gets to that handler.
  • Actually call the handler.
  • References

[1]: What is __gxx_personality_v0 for?
[2]: .eh_frame的一些资料
[3]: .eh_frame sections
[4]: richi-2011-12-16.txt
[5]: .gcc_except_table
[6]: c++ 异常处理(2)
[7]: CesarB's anwser

  • Extended

[1]: Deep Wizardry: Stack Unwinding
[2]: How a C++ compiler implements exception handling
[3]: Itanium C++ ABI: Exception Handling

Leave a Reply

Your email address will not be published. Required fields are marked *

19 − 10 =