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

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

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

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

  1. 类对象的构造函数须要在main函数运行前完毕,而类对象的析构函数须要在main函数运行后完毕。

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

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

执行时库才是程序的真正開始和真正结束的地方。

本章重点链接脚本怎样支持类对象的构造和析构函数。其它特性内容分析留待以后再做解说。

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

二、程序演演示样例程

1.程序

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVlcWlhbl9zY3V0/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">

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相关的经验总结,谢谢!

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

  1. 理解函数式编程中的函数组合--Monoids(二)

    使用函数式语言来建立领域模型--类型组合 理解函数式编程语言中的组合--前言(一) 理解函数式编程中的函数组合--Monoids(二) 继上篇文章引出<范畴论>之后,我准备通过几篇文章,来 ...

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

    在KVM虚拟机中使用spice系列之二(USB映射,SSL,密码,多客户端支持) 发布时间: 2015-02-27 00:16 1.spice的USB重定向 1.1 介绍 使用usb重定向,在clie ...

  3. 链接脚本(Linker Script)用法解析(二) clear_table & copy_table

    可执行文件中的.bss段和.data段分别存放未赋初值的全局变量和已赋初值的全局变量,两者的特点分别为: (1).bss段:①无初值,所以不占ROM空间:②运行时存储于RAM:③默认初值为0 (2). ...

  4. Qt多线程编程中的对象线程与函数执行线程

    近来用Qt编写一段多线程的TcpSocket通信程序,被其中Qt中报的几个warning搞晕了,一会儿是说“Cannot create children for a parent that is in ...

  5. Python中Cookie的处理(二)cookielib库

    Python中cookielib库(python3中为http.cookiejar)为存储和管理cookie提供客户端支持. 该模块主要功能是提供可存储cookie的对象.使用此模块捕获cookie并 ...

  6. js中使用Timer来计时程序执行时 - [javascript] - [开发]

    在我们开发过程中,我们也在不断的学习,以及优化自己的代码质量. 我们时常需要一个计时器,来对代码某段或者某些段执行进行计时,以评估代码运行质量,考虑是否优化. 以及优化后的直观对比. JavaScri ...

  7. (十一) 一起学 Unix 环境高级编程 (APUE) 之 高级 IO

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

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

    PHP是一种非常流行的开源服务器端脚本语言,你在万维网看到的大多数网站都是使用php开发的.本篇经将为大家介绍PHP开发中10个最常见的问题,希望能够对朋友有所帮助. 错误1:foreach循环后留下 ...

  9. 四、u-boot 链接脚本

    4.1 C语言中的段 编译器在编译程序的时候,将程序中的所有的元素分成了一些组成部分,各部分构成一个段,所以说段是可执行程序的组成部分. 代码段:代码段就是程序中的可执行部分,直观理解代码段就是函数堆 ...

随机推荐

  1. 优化UITableView

    在iOS应用中,UITableView应该是使用率最高的视图之一了.iPod.时钟.日历.备忘录.Mail.天气.照片.电话.短信.Safari.App Store.iTunes.Game Cente ...

  2. 【转载】Sql语句用left join 解决多表关联问题(关联套关联,例子和源码)

    csdn中高手帮我给解决了,其实就是别名,给自己上了一堂别名的课,所谓别人是高手,其实就是自己是菜鸟吧! 表1:------------------------------ [人事表]     表名: ...

  3. centos7 rsync+inotify软件实现集群服务的数据备份(一)

    一.rsync软件的说明: 1.1 什么是rsync rsync是一个远程数据同步工具,可通过LAN/WAN快速同步多台主机间的文件.它使用所谓的“Rsync演算法”来使本地和远程两个主机之间的文件达 ...

  4. LEFT JOIN结果集可能变大。。。。。

    SELECT A.*,B.* FROM A LEFT JOIN B ON A.ID = B.ID

  5. virsh 命令

    virsh是用与管理虚拟化环境中的客户机和Hypervisor的命令行工具,与virt-manager等工具类似,也是调用libvirt API来实现虚拟化的管理. 在使用virsh命令行进行虚拟化管 ...

  6. Mysql 参数优化

  7. STM32F407 独立看门狗 个人笔记

    什么是看门狗 如果程序跑飞了怎么办? 可以用看门狗来监控. 看门狗是: 一个递减的计数器,如果不按时给计数器赋值,计数器的值减到一定程度,就会使系统复位. 也就是说如果程序运行异常,无法正常给计数器赋 ...

  8. hdu 2686最小费用最大流问题

    #include<stdio.h> #include<queue> #include<string.h> using namespace std; #define ...

  9. [转]制作一个64M的U盘启动盘(mini linux + winpe +dos toolbox)

    自己动手定制winpe+各类dos工具箱U盘启动盘+minilinux 由于一个64M老U盘,没什么用,拿来发挥余热.如果U盘够大,可以使用功能更强大的mini linux和带更多工具的winpe.这 ...

  10. BZOJ2099: [Usaco2010 Dec]Letter 恐吓信

    给两个长度不超过50000的串,A串可每次截连续一段复制出来,求最少复制几次能得到B串. 方法一:SAM.不会. 嗯好会了. #include<stdio.h> #include<s ...