11.4.1 glibc全局构造与析构(2)
我们暂且不管这里的神秘函数__tcf_1,它将在本节的最后部分讲到。GLOBAL__I_Hw作为特殊的函数当然也享受特殊待遇,一旦一个目标文件里有这样的函数,编译器会在这个编译单元产生的目标文件(.o)的".ctors"段里放置一个指针,这个指针指向的便是GLOBAL__I_Hw。
那么把每个目标文件的复杂全局/静态对象构造的函数地址放在一个特殊的段里面有什么好处呢?当然不为别的,为的是能够让链接器把这些特殊的段收集起来,收集齐所有的全局构造函数后就可以在初始化的时候进行构造了。
在编译器为每个编译单元生成一份特殊函数之后,链接器在连接这些目标文件时,会将同名的段合并在一起,这样,每个目标文件的.ctors段将会被合并为一个.ctors段,其中的内容是各个目标文件的.ctors段的内存拼接而成。由于每个目标文件的.ctors段都只存储了一个指针(指向该目标文件的全局构造函数),因此拼接起来的.ctors段就成为了一个函数指针数组,每一个元素都指向一个目标文件的全局构造函数。这个指针数组不正是我们想要的全局构造函数的地址列表吗?如果能得到这个数组的地址,岂不是构造的问题就此解决了?
没错,得到这个数组的地址其实也不难,我们可以效仿前面".init"和".finit"拼凑的办法,对".ctor"段也进行拼凑。还记得在链接的时候,各个用户产生的目标文件的前后分别还要链接上一个crtbegin.o和crtend.o吧?这两个glibc自身的目标文件同样具有.ctors段,在链接的时候,这两个文件的.ctors段的内容也会被合并到最终的可执行文件中。那么这两个文件的.ctors段里有什么呢?
crtbegin.o:作为所有.ctors段的开头部分,crtbegin.o的.ctor段里面存储的是一个4字节的 1(0xFFFFFFFF),由链接器负责将这个数字改成全局构造函数的数量。然后这个段还将起始地址定义成符号__CTOR_LIST__,这样实际上__CTOR_LIST__所代表的就是所有.ctor段最终合并后的起始地址了。
crtend.o:这个文件里面的.ctors内容就更简单了,它的内容就是一个0,然后定义了一个符号__CTOR_END__,指向.ctor段的末尾。
在前面的章节中已经介绍过了,链接器在链接用户的目标文件的时候,crtbegin.o总是处在用户目标文件的前面,而crtend.o则总是处在用户目标文件的后面。例如链接两个用户的目标文件a.o和b.o时,实际链接的目标文件将是(按顺序)ld crti.o crtbegin.o a.o b.o crtend.o crtn.o。在这里我们忽略crti.o和crtn.o,因为这两个目标文件和全局构造无关。在合并crtbegin.o、用户目标文件和crtend.o时,链接器按顺序拼接这些文件的.ctors段,因此最终形成.ctors段的过程将如图11-10所示。
|
| (点击查看大图)图11-10 .ctor段的形成 |
在了解了可执行文件的.ctors段的结构之后,再回过头来看__do_global_ctor_aux的代码就很容易了。__do_global_ctor_aux从__CTOR_LIST__的下一个位置开始,按顺序执行函数指针,直到遇上NULL(__CTOR_END__)。如此每个目标文件的全局构造函数都能被调用。
【小实验】
在main前调用函数:
glibc的全局构造函数是放置在.ctors段里的,因此如果我们手动在.ctors段里添加一些函数指针,就可以让这些函数在全局构造的时候(main之前)调用:
#include <stdio.h> void my_init(void) { printf("Hello "); }
typedef void (*ctor_t)(void); //在.ctors段里添加一个函数指针 ctor_t __attribute__((section (".ctors"))) my_init_p = &my_init;
int main() { printf("World!\n"); return 0; }
|
如果运行此程序,结果将打印出:Hello World!
当然,事实上,gcc里有更加直接的办法来达到相同的目的,那就是使用__attribute__((constructor))
示例如下:
#include <stdio.h> void my_init(void) __attribute__ ((constructor)); void my_init(void) { printf("Hello "); } int main() { printf("World!\n"); return 0; }
|
【责任编辑:
云霞 TEL:(010)68476606】