11.4.1 glibc全局构造与析构(3)
析构
对于早期的glibc和GCC,在完成了对象的构造之后,在程序结束之前,crt还要进行对象的析构。实际上正常的全局对象析构与前面介绍的构造在过程上是完全类似的,而且所有的函数、符号名都一一对应,比如".init"变成了".finit"、"__do_global_ctor_aux"变成了"__do_global_dtor_aux"、"__CTOR_LIST__"变成了"__DTOR_LIST__"等。在前面介绍入口函数时我们可以看到,__libc_start_main将"__libc_csu_fini"通过__cxa_exit()注册到退出列表中,这样当进程退出前exit()里面就会调用"__libc_csu_fini"。"_fini"的原理和"_init"基本是一样的,在这里不再一一赘述了。
不过这样做的好处是为了保证全局对象构造和析构的顺序(即先构造后析构),链接器必须包装所有的".dtor"段的合并顺序必须是".ctor"的严格反序,这增加了链接器的工作量,于是后来人们放弃了这种做法,采用了一种新的做法,就是通过__cxa_atexit()在exit()函数中注册进程退出回调函数来实现析构。
这就要回到我们之前在每个编译单元的全局构造函数GLOBAL__I_Hw()中看到的神秘函数。编译器对每个编译单元的全局对象,都会生成一个特殊的函数来调用这个编译单元的所有全局对象的析构函数,它的调用顺序与GLOBAL__I_Hw()调用构造函数的顺序刚好相反。例如对于前面的例子中的代码,编译器生成的所谓的神秘函数内容大致是:
static void __tcf_1(void) //这个名字由编译器生成 { Hw.~HelloWorld(); } |
此函数负责析构Hw对象,由于在GLOBAL__I_Hw中我们通过__cxa_exit()注册了__tcf_1,而且通过__cxa_exit()注册的函数在进程退出时被调用的顺序满足先注册后调用的属性,与构造和析构的顺序完全符合,于是它就很自然被用于析构函数的实现了。
当然在本节中介绍glibc/GCC的全局对象构造和析构时,省略了不少我们认为超出了本书所要强调的范围细节,真正的构造和析构过程比上面介绍的要复杂一些,并且在动态链接和静态链接不同的情况下,构造和析构还略有不同。但是不管哪种情况,基本的原理都是相通的,按照上面介绍的步骤和路径,相信读者也能够自己重新根据真实的情况梳理清楚这条调用路线。
由于全局对象的构建和析构都是由运行库完成的,于是在程序或共享库中有全局对象时,记得不能使用"-nonstartfiles"或"-nostdlib"选项,否则,构建与析构函数将不能正常执行(除非你很清楚自己的行为,并且手工构造和析构全局对象)。
Collect2
我们在第2章时曾经碰到过collect2这个程序,在链接时它代替ld成为了最终链接器,一般情况下就可以简单地将它看成ld。实际上collect2是ld的一个包装,它最终还是调用ld完成所有的链接工作,那么collect2这个程序的作用是什么呢?
在有些系统上,汇编器和链接器并不支持本节中所介绍的".init"".ctor"这种机制,于是为了实现在main函数前执行代码,必须在链接时进行特殊的处理。Collect2这个程序就是用来实现这个功能的,它会"收集"(collect)所有输入目标文件中那些命名特殊的符号,这些特殊的符号表明它们是全局构造函数或在main前执行,collect2会生成一个临时的.c文件,将这些符号的地址收集成一个数组,然后放到这个.c文件里面,编译后与其他目标文件一起被链接到最终的输出文件中。
在这些平台上,GCC编译器也会在main函数的开始部分产生一个__main函数的调用,这个函数实际上就是负责collect2收集来的那些函数。__main函数也是GCC所提供的目标文件的一部分,如果我们使用"-nostdlib"编译程序,可能得到__main函数未定义的错误,这时候只要加上"-lgcc"把它链接上即可。
【责任编辑:
云霞 TEL:(010)68476606】