我们在链接脚本在编程中的高级运用之中的一个可变长数组中已经讲述了编译链接的原理,并且以uboot命令为例具体介绍链接脚本怎样实现可变长数组。
本章在前者的基础上继续讲述链接脚本在执行时库中的高级应用技巧。以及编译器怎样支持类对象的构造和析构函数。本章的应用原则上类似于可变长数组,但本章更加側重讲述执行时库的实现原理,其不仅通过链接脚本的section来实现可变长数组去支持随意多类对象的构造函数和析构函数,并且还支持特定函数体的“可变长”。
一、执行时库和类对象的构造、析构函数
非常多程序猿以为程序的開始就是main。其实main仅仅是程序的中间的一部分。在main之前和之后都要完毕非常多工作。当中就包含下面几个基本的部分:
- 类对象的构造函数须要在main函数运行前完毕,而类对象的析构函数须要在main函数运行后完毕。
- 我们都知道现代操作系统都有多进程多线程的概念,而main函数怎么没看到相关的数据结构呢?这些都是执行时库的工作。
- 程序里面能够直接printf将数据输出到0相应的显示控制台,这个设备文件怎么初始化的。是不是应该先初始化和先打开才干用的。
执行时库才是程序的真正開始和真正结束的地方。
本章重点链接脚本怎样支持类对象的构造和析构函数。其它特性内容分析留待以后再做解说。
下面是基于X86架构的ubuntu64平台对Glibc的执行时库进行分析。
二、程序演演示样例程
1.程序
2.执行结果
给某函数加入__attribute__((constructor))属性,编译器会将该函数名指针加入到名为.ctors的section,
加入__attribute__((destructor))属性,则会将函数名指针加入到.dtors。
即是将函数名地址加入到.ctors和.dtors指示的可变长数组。
记住,仅仅是函数名地址。而不是函数体。
另外,classTest()是类classTest的同名函数,是构造函数,编译器也会将该函数地址填入到.ctors,即编译器判定某个函数为类的构造函数后。自己主动给该函数加入__attribute__((constructor))属性;同理。对析构函数~classTest()加入__attribute__((destructor))属性,将该函数地址加入.dtors。
3.
默认链接脚本
通过ld –verbose能够得到默认链接脚本的内容,我们截取一部分,印证在链接脚本中存在.ctors和.dtors。
编译器和链接器共同对.ctors和.dtors负责,而保证构造函数先于main函数完毕和析构函数后于main执行则是执行时库来保证。
三、执行时库的组成
- 执行时库有ctr1.o、crti.o、crtbegin.o、crtend.o、crtn.o五个重要的库文件。
- crt1.o提供程序的真正入口,在该文件的启动函数中。会创建主线程及相关的数据结构,并调用初始化总入口,接着调用main函数(所以main就是主线程),等main退出会调用释放总入口,最后再做线程清理相关的工作。
- crti.o负责程序的初始化,crtn.o负责程序的资源释放工作。
- crtbegin.o负责支持.ctors属性先于main运行这个特性;crtend.o负责支持.dtors后于main运行这个特性。
- ctr1.o、crti.o、crtn.o
由标准C库提供,
路径通常是/usr/lib/x86_64-linux-gnu; crtbegin.o
和crtend.o主要是为了支持c++语法,由编译器厂商提供,路径通常是/usr/lib/gcc/x86_64-linux-gnu-4.4.7. - 链接时使用的默认脚本会定义链接次序,是ctr1.o、crti.o、crtbegin.o、用户程序编译成的.o文件、crtend.o、crtn.o,这个顺序是有要求的。不能更改。
四、执行时库的代码分析
1. crtbegin.o
objdump –D crtbegin.o > crtbegin.S
得到crtbegin的反汇编代码。有下面数据和代码段:
a.Disassembly of section .ctors:
0000000000000000 <__CTOR_LIST__>: 0x00000000ffffffff
即有一个属性为.ctors的函数指针,可是非常明显0x00000000ffffffff是一个标识符,而不是一个特别的函数地址。
b.Disassembly of section .dtors:
0000000000000000 <__DTOR_LIST__>: 0x00000000ffffffff
即有一个属性为.dtors的函数指针,可是非常明显0x00000000ffffffff是一个标识符,而不是一个特别的函数地址。
c.
Disassembly of section .text:
0000000000000000 <__do_global_dtors_aux>:
该函数会遍历__DTOR_LIST__,对.dtors数组的函数指针进行调用。对于析构函数来说,其调用的顺序应该跟在链接脚本中出现的顺序相反,即最先链接到.dtors
section的析构函数应该是最后被运行的。依据链接脚本。crtbein.o最先出如今.dtors
section中。因此crtbegin产生的.dtors属性的函数指针肯定是最后被运行的,即推断到0x00000000ffffffff即表示析构调用结束。
d.Disassembly of section .fini:
0000000000000000 <.fini>:
0: e8 00 00 00 00 callq 5 <__do_global_dtors_aux+0x5>
该代码会链接到.fini section,记住call
__do_global_dtors_aux 仅仅是调用__do_global_dtors_aux这个函数,可是这个调用本身没有返回值,所以不能称为call
__do_global_dtors_aux为一个函数。仅仅能说是一个代码片段。正常的C函数调用的反汇编肯定有ret返回指令的。
2. crtend.o
objdump –D crtend.o > crtend.S得到crtend的反汇编代码。
有下面数据和代码段:
a.Disassembly of section .ctors:
0000000000000000 <__CTOR_END__>:
因为crtend后于用户程序进行链接。因此crtend的__CTOR_END__代表着构造.ctors的结束。
以下提到的__do_global_ctors_aux即从__CTOR_LIST__開始逐一调用构造函数。直到__CTOR_END__。
b.
Disassembly of section .dtors:
0000000000000000 <__DTOR_END__>:
因为crtend后于用户程序进行链接。因此__DTOR_END__会出如今.dtors的最后。__do_global_dtors_aux即从__DTOR_END__開始向前进行逐一析构调用。
c.Disassembly of section .text:
0000000000000000 <__do_global_ctors_aux>:
__do_global_ctors_aux即从__CTOR_LIST__開始逐一调用构造函数,直到__CTOR_END__。
d.Disassembly of section .init:
0000000000000000 <.init>:
0:e8 00 00 00 00 callq 5 <__do_global_ctors_aux+0x5>
该代码会链接到.init section,记住call
__do_global_ctors_aux 仅仅是调用__do_global_ctors_aux这个函数。可是这个调用本身没有返回值。
3. ctri.o
objdump –D crti.o > crti.S
得到crti.o的反汇编代码。
有下面代码段:
a.Disassembly of section .init:
0000000000000000 <_init>:
0: 48 83 ec 08 sub $0x8,%rsp
4: e8 00 00 00 00 callq call_gmon_start //这个是动态库的处理。
b.
Disassembly of section .fini:
0000000000000000 <_fini>:
0: 48 83 ec 08 sub $0x8,%rsp
能够看出crti.o有.init和.fini代码段,并且_init和_fini这两个函数都是不完整的。仅仅见到入口对栈的处理,也没有返回指令。
读到这里。能想到总会有个文件的.init和.fini段有返回指令了吧?没错,接下来的crtn.o就有了。
4. crtn.o
objdump –D crtn.o > crtn.S得到crtn.o的反汇编代码。有下面代码段:
a.Disassembly of section .init:
0000000000000000 <.init>:
0: 48 83 c4 08 add $0x8,%rsp
4: c3 retq //返回指令
b.
Disassembly of section .fini:
0000000000000000 <.fini>:
0: 48 83 c4 08 add $0x8,%rsp
4: c3 retq //返回指令
不用解释了吧。
5.
总结.init和.fini
依据链接脚本的链接顺序,.init段的_init代码由ctri.o,
ctrbegin.o, crtend.o和crtn.o的init段组成,例如以下:
_init:
sub $0x8,%rsp
callq call_gmon_start
callq __do_global_ctors_aux
add $0x8,%rsp
retq
而.fini段的_fini代码由ctri.o,
ctrbegin.o, crtend.o和crtn.o的.fini段组成,例如以下
_fini:
sub $0x8,%rsp
cal __do_global_dtors_aux
add $0x8,%rsp
retq
6. _init和_fini两个函数是由ctr1.o的代码进行控制和调用的。
五、一些思考和验证
1.为什么__do_global_ctors_aux函数会出如今crtn.o,而__do_global_dtors_aux会出如今crti.o?这是由于对于__do_global_ctors_aux来说,其进行构造的初始化。须要知道.ctors变长数组的结束标识符在哪里,而crtn.o的.ctors就是结束的地方。同理,__do_global_dtors_aux从后往前调用,其须要知道.dtors的最開始地方,而crti.o的.dtors即意味着開始。
2.之前我们说默认的链接顺序是ctr1.o、crti.o、crtbegin.o、用户程序编译成的.o文件、crtend.o、crtn.o。到这里应该没有疑问了吧。
3.
给函数添加__attribute__((constructor))即是将该函数指针放到.ctors段。是否能直接加入这个section属性__attribute__(section(“.ctors”))?其实。通过验证,编译器不同意用户直接自己定义.ctors属性,应该是将该.ctors作为section的保留名了。假设想实如今main之前完毕函数调用,就仅仅能添加__attribute__((constructor))属性,编译就会正常处理。
4.
我们从上面的分析能够看出,将某些代码设置为.init属性,也是能够被预先处理的。记住。是某些代码,也就是call
指令才行,假设是一个函数。那编译后会有返回指令。即_init会提前返回,破坏了程序的执行过程,终于会出现segment fault错误,导致core
down。所以还是老老实实地用__attribute__((constructor))吧。除非你用汇编来写一段不用返回的代码,不用那么折腾吧J
请关注本人微信公众号:嵌入式企鹅圈
百分百原创,分享嵌入式和Linux相关的经验总结,谢谢!