码迷,mamicode.com
首页 > 编程语言 > 详细

链接脚本在编程中的高级运用之二——运行时库和C++特性支持

时间:2015-06-03 11:59:24      阅读:307      评论:0      收藏:0      [点我收藏+]

标签:链接脚本   ld   运行时库   c++构造和析构   

我们在链接脚本在编程中的高级运用之一可变长数组中已经讲述了编译链接的原理,并且以uboot命令为例详细介绍链接脚本如何实现可变长数组。本章在前者的基础上继续讲述链接脚本在运行时库中的高级应用技巧,以及编译器如何支持类对象的构造和析构函数。本章的应用原则上类似于可变长数组,但本章更加侧重讲述运行时库的实现原理,其不仅通过链接脚本的section来实现可变长数组去支持任意多类对象的构造函数和析构函数,而且还支持特定函数体的“可变长”。

一、运行时库和类对象的构造、析构函数

很多程序员以为程序的开始就是main,事实上main只是程序的中间的一部分,在main之前和之后都要完成很多工作。其中就包括以下几个主要的部分:

  1. 类对象的构造函数需要在main函数执行前完成,而类对象的析构函数需要在main函数执行后完成。

  2. 我们都知道现代操作系统都有多进程多线程的概念,而main函数怎么没看到相关的数据结构呢?这些都是运行时库的工作。

  3. 程序里面可以直接printf将数据输出到0对应的显示控制台,这个设备文件怎么初始化的,是不是应该先初始化和先打开才能用的。

运行时库才是程序的真正开始和真正结束的地方。本章重点链接脚本如何支持类对象的构造和析构函数。其他特性内容分析留待以后再做讲解。

以下是基于X86架构的ubuntu64平台对Glibc的运行时库进行分析。

二、程序演示例程

1.程序

技术分享

2.运行结果

技术分享

给某函数添加__attribute__((constructor))属性,编译器会将该函数名指针添加到名为.ctorssection, 添加__attribute__((destructor))属性,则会将函数名指针添加到.dtors。即是将函数名地址添加到.ctors.dtors指示的可变长数组。记住,只是函数名地址,而不是函数体。

另外,classTest()是类classTest的同名函数,是构造函数,编译器也会将该函数地址填入到.ctors,即编译器判定某个函数为类的构造函数后,自动给该函数添加__attribute__((constructor))属性;同理,对析构函数~classTest()添加__attribute__((destructor))属性,将该函数地址加入.dtors。

3. 默认链接脚本

通过ld –verbose可以得到默认链接脚本的内容,我们截取一部分,印证在链接脚本中存在.ctors.dtors。

技术分享

编译器和链接器共同对.ctors.dtors负责,而保证构造函数先于main函数完成和析构函数后于main执行则是运行时库来保证。

三、运行时库的组成
  1. 运行时库有ctr1.ocrti.ocrtbegin.ocrtend.ocrtn.o五个重要的库文件。

  2. crt1.o提供程序的真正入口,在该文件的启动函数中,会创建主线程及相关的数据结构,并调用初始化总入口,接着调用main函数(所以main就是主线程),等main退出会调用释放总入口,最后再做线程清理相关的工作。

  3. crti.o负责程序的初始化,crtn.o负责程序的资源释放工作。

  4. crtbegin.o负责支持.ctors属性先于main执行这个特性;crtend.o负责支持.dtors后于main执行这个特性。

  5. ctr1.ocrti.ocrtn.o 由标准C库提供, 路径一般是/usr/lib/x86_64-linux-gnu; crtbegin.o crtend.o主要是为了支持c++语法,由编译器厂商提供,路径一般是/usr/lib/gcc/x86_64-linux-gnu-4.4.7.

  6. 链接时使用的默认脚本会定义链接次序,是ctr1.ocrti.ocrtbegin.o、用户程序编译成的.o文件、crtend.ocrtn.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即表示析构调用结束。

dDisassembly 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.ocrtn.oinit段组成,如下:

_init:

sub $0x8,%rsp

callq call_gmon_start

callq __do_global_ctors_aux

add $0x8,%rsp

retq

.fini段的_fini代码由ctri.o, ctrbegin.o, crtend.ocrtn.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.ocrti.ocrtbegin.o、用户程序编译成的.o文件、crtend.ocrtn.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相关的经验总结,谢谢!
技术分享



链接脚本在编程中的高级运用之二——运行时库和C++特性支持

标签:链接脚本   ld   运行时库   c++构造和析构   

原文地址:http://blog.csdn.net/yueqian_scut/article/details/46341711

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!