11.4 C++(www.cppentry.com)全局构造与析构
在C++(www.cppentry.com)的世界里,入口函数还肩负着另一个艰巨的使命,那就是在main的前后完成全局变量的构造与析构。本节将介绍在glibc和MSVCRT的努力下,这件事是如何完成的。
11.4.1 glibc全局构造与析构(1)
在前面介绍glibc的启动文件时已经介绍了".init"和".finit"段,我们知道这两个段中的代码最终会被拼成两个函数_init()和_finit(),这两个函数会先于/后于main函数执行。但是它们具体是在什么时候被执行的呢?由谁来负责调用它们呢?它们又是如何进行全局对象的构造和析构的呢?为了解决这些问题,这一节将继续沿着本章第一节从_start入口函数开始的那条线进行摸索,顺藤摸瓜地找到这些问题的答案。
为了表述方便,下面使用这样的代码编译出来的可执行文件进行分析:
class HelloWorld { public: HelloWorld(); ~HelloWorld(); }; HelloWorld Hw; HelloWorld::HelloWorld() { ...... } HelloWorld::~HelloWorld() { ...... } int main() { return 0; } |
为了了解全局对象的构造细节,对程序的启动过程进行更深一步的研究是必须的。在本章的第一节里,由_start传递进来的init函数指针究竟指向什么?通过对地址的跟踪,init实际指向了__libc_csu_init函数。这个函数位于Glibc源代码目录的csu\Elf-init.c,让我们来看看这个函数的定义:
_start -> __libc_start_main -> __libc_csu_init: void __libc_csu_init (int argc, char **argv, char **envp) { … _init ();
const size_t size = __init_array_end - __init_array_start; for (size_t i = 0; i < size; i++) (*__init_array_start [i]) (argc, argv, envp); }
|
这段代码调用了_init函数。那么_init()是什么呢?是不是想起来前面介绍过的定义在crti.o的_init()函数呢?没错,__libc_csu_init里面调用的正是".init"段,也就是说,用户所有放在".init"段的代码就将在这里被执行。
看到这里,似乎我们的线索要断了,因为"_init"函数的实际内容并不定义在Glibc里面,它是由各个输入目标文件中的".init"段拼凑而来的。不过除了分析源代码之外,还有一个终极必杀就是反汇编目标代码,我们随意反汇编一个可执行文件就可以发现_init()函数的内容:
_start -> __libc_start_main -> __libc_csu_init -> _init: Disassembly of section .init: 80480f4 <_init>: 80480f4: 55 push %ebp 80480f5: 89 e5 mov %esp,%ebp 80480f7: 53 push %ebx 80480f8: 83 ec 04 sub $0x4,%esp 80480fb: e8 00 00 00 00 call 8048100 <_init+0xc> 8048100: 5b pop %ebx 8048101: 81 c3 9c 39 07 00 add $0x7399c,%ebx 8048107: 8b 93 fc ff ff ff mov -0x4(%ebx),%edx 804810d: 85 d2 test %edx,%edx 804810f: 74 05 je 8048116 <_init+0x22> 8048111: e8 ea 7e fb f7 call 0 <_nl_current_LC_CTYPE> 8048116: e8 95 00 00 00 call 80481b0 <frame_dummy> 804811b: e8 b0 6e 05 00 call 809efd0 <__do_global_ctors_aux> 8048120: 58 pop %eax 8048121: 5b pop %ebx 8048122: c9 leave 8048123: c3 ret |
可以看到_init调用了一个叫做__do_global_ctors_aux的函数,如果你在glibc源代码里面查找这个函数,是不可能找到它的。因为它并不属于glibc,而是来自于GCC提供的一个目标文件crtbegin.o。我们在上一节中也介绍过,链接器在进行最终链接时,有一部分目标文件是来自于GCC,它们是那些与语言密切相关的支持函数。很明显,C++(www.cppentry.com)的全局对象构造是与语言密切相关的,相应负责构造的函数来自于GCC也非常容易理解。
即使它在GCC的源代码中,我们也把它揪出来。它位于gcc/Crtstuff.c,把它简化以后代码如下:
_start -> __libc_start_main -> __libc_csu_init -> _init -> __do_global_ctors_aux: void __do_global_ctors_aux(void) { /* Call constructor functions. */ unsigned long nptrs = (unsigned long) __CTOR_LIST__[0]; unsigned i; for (i = nptrs; i >= 1; i--) __CTOR_LIST__[i] (); } |
上面这段代码首先将__CTOR_LIST__数组的第一个元素当做数组元素的个数,然后将第一个元素之后的元素都当做是函数指针,并一一调用。这段代码的意图非常明显,我们都可以猜到__CTOR_LIST__里面存放的是什么,没错,__CTOR_LIST__里面存放的就是所有全局对象的构造函数的指针。那么接下来的焦点很明显就是__CTOR_LIST__了,这个数组怎么来的,由谁负责构建这个数组?
这里不得不暂时放下__CTOR_LIST__的身世来历,从GCC方面再追究__CTOR_LIST__未免有些乏味,我们不妨从问题的另一端,也就是从编译器如何生产全局构造函数的角度来看看全局构造函数是怎么实现的。
对于每个编译单元(.cpp),GCC编译器会遍历其中所有的全局对象,生成一个特殊的函数,这个特殊函数的作用就是对本编译单元里的所有全局对象进行初始化。我们可以通过对本节开头的代码进行反汇编得到一些粗略的信息,可以看到GCC在目标代码中生成了一个名为_GLOBAL__I_Hw的函数,由这个函数负责本编译单元所有的全局\静态对象的构造和析构,它的代码可以表示为:
static void GLOBAL__I_Hw(void) { Hw::Hw(); // 构造对象 atexit(__tcf_1); // 一个神秘的函数叫做__tcf_1被注册到了exit }
|
【责任编辑:
云霞 TEL:(010)68476606】