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

我们在链接脚本在编程中的高级运用之中的一个可变长数组中已经讲述了编译链接的原理,并且以uboot命令为例具体介绍链接脚本怎样实现可变长数组。

本章在前者的基础上继续讲述链接脚本在执行时库中的高级应用技巧。以及编译器怎样支持类对象的构造和析构函数。本章的应用原则上类似于可变长数组,但本章更加側重讲述执行时库的实现原理,其不仅通过链接脚本的section来实现可变长数组去支持随意多类对象的构造函数和析构函数,并且还支持特定函数体的“可变长”。

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

非常多程序猿以为程序的開始就是main。其实main仅仅是程序的中间的一部分。在main之前和之后都要完毕非常多工作。当中就包含下面几个基本的部分:

  1. 类对象的构造函数须要在main函数运行前完毕,而类对象的析构函数须要在main函数运行后完毕。
  2. 我们都知道现代操作系统都有多进程多线程的概念,而main函数怎么没看到相关的数据结构呢?这些都是执行时库的工作。
  3. 程序里面能够直接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执行则是执行时库来保证。

三、执行时库的组成

  1. 执行时库有ctr1.o、crti.o、crtbegin.o、crtend.o、crtn.o五个重要的库文件。
  2. crt1.o提供程序的真正入口,在该文件的启动函数中。会创建主线程及相关的数据结构,并调用初始化总入口,接着调用main函数(所以main就是主线程),等main退出会调用释放总入口,最后再做线程清理相关的工作。
  3. crti.o负责程序的初始化,crtn.o负责程序的资源释放工作。
  4. crtbegin.o负责支持.ctors属性先于main运行这个特性;crtend.o负责支持.dtors后于main运行这个特性。
  5. 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.

  6. 链接时使用的默认脚本会定义链接次序,是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相关的经验总结,谢谢!

时间: 2024-10-06 19:35:39

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

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

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

链接脚本在编程中的高级运用之一:可变长数组

作为嵌入式软件工程师,应该要清楚程序的每一条指令在哪里,什么时候会被加载到内存,什么时候会被执行.链接脚本会明确告诉你程序的代码和数据在内存中的分布.精确控制代码和数据在内存中的分布是高效利用内存资源的前提.自定义链接脚本是资深嵌入式软件工程师的必备技能,更是嵌入式架构师的最基本要求.此外,灵活定制链接脚本在编程方面有更高级的应用. 一.编译链接原理 简单讲述编译链接的基本原理有助于后面内容的理解. a. 简单点说,一个可执行程序包括文件头.代码段(.text).数据段(.bss).符号段等信息

andriod编程中如何获取一段语音的时长?

在android有关语音的应用中,我们可能需要录音的长度,这个长度很好获取,只要在刚刚开始录音的时候获取本地时间,录音结束的时候获取本地时间,之后一减就可以得到他的时间长度. 代码:first = (int)(System.currentTimeMillis()/1000);//当点击录音的时候获取本地时间,除以1000得到时间单位是秒,否则是毫秒. second = (int)(System.currentTimeMillis()/1000);//录音结束的时候获取本地时间         

在KVM虚拟机中使用spice系列之二(USB映射,SSL,密码,多客户端支持)

1.spice的USB重定向 1.1 介绍 使用usb重定向,在client上插入的U盘会被重定向到虚拟机中. 其有两种实现方式,自动重定向(所有插入client中的U盘都被重定向),或者手动选择需要重定向的U盘 USB重定向需要为虚拟机添加USB2 EHCI驱动,以及若干个Spice channels,Spice channels的个数决定了客户端一次可以有多少个USB设备被重定向到guest 更多参考: http://people.freedesktop.org/~teuf/spice-do

重定位与链接脚本

1.为什么需要重定位 位置无关编码(PIC,position independent code):汇编源文件被编码成二进制可执行程序时编码方式与位置(内存地址)无关. 位置有关编码:汇编源码编码成二进制可执行程序后和内存地址是有关的. 我们在设计一个程序时,会给这个程序指定一个运行地址(链接地址).就是说我们在编译程序时其实心里是知道我们程序将来被运行时的地址(运行地址)的,而且必须给编译器链接器指定这个地址(链接地址)才行.最后得到的二进制程序理论上是和你指定的运行地址有关的,将来这个程序被执

PHP 编程中 10 个最常见的错误,你犯过几个?

错误1:foreach循环后留下悬挂指针 在foreach循环中,如果我们需要更改迭代的元素或是为了提高效率,运用引用是一个好办法: $arr = array(1,2,3,4); foreach($arr as&$value){    $value = $value *2; } // $arr is now array(2, 4, 6, 8) 这里有个问题很多人会迷糊. 错误1:foreach循环后留下悬挂指针 在foreach循环中,如果我们需要更改迭代的元素或是为了提高效率,运用引用是一个好

shell编程中for/while循环命令

一.for命令 在shell编程中,有时我们需要重复执行一直命令直至达到某个特定的条件,bash shell中,提供了for命令,允许你创建一个遍历一系列值的循环,每次迭代都通过一个该系列中的值执行一组预定义的命令. for的基本格式: for var in list do commands done 在list中,你提供了迭代中要用的一系列值.在每个迭代中,变量var包含列表中的当前值,第一个迭代会适用列表中的第一个值,第二个迭代使用第二个值,以此类推,直至列表中的所有值都过一遍. 1.1读取

PHP编程中10个最常见的错误

PHP是一种非常流行的开源服务器端脚本语言,你在万维网看到的大多数网站都是使用php开发的.本篇经将为大家介绍PHP开发中10个最常见的问题,希望能够对朋友有所帮助. 错误1:foreach循环后留下悬挂指针 在foreach循环中,如果我们需要更改迭代的元素或是为了提高效率,运用引用是一个好办法: 1 2 3 4 5 $arr = array(1, 2, 3, 4); foreach ($arr as &$value) {     $value = $value * 2; } // $arr

Java 编程中关于异常处理的 10 个最佳实践

异常处理是书写 强健 Java应用的一个重要部分.它是关乎每个应用的一个非功能性需求,是为了优雅的处理任何错误状况,比如资源不可访问,非法输入,空输入等等.Java提供了几个异常处理特性,以try,catch和finally 关键字的形式内建于语言自身之中.Java编程语言也允许你创建新的异常,并通过使用  throw 和 throws关键字抛出它们.事实上,异常处理不仅仅是知道语法.书写一个强健的代码更多的是一门艺术而不仅仅是一门科学,这里我们将讨论一些关于异常处理的Java最佳实践.这些 J