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

Posted 吴跃前

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了链接脚本在编程中的高级运用之二——运行时库和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运行时库只是一些动态链接的库文件吗? [关闭]

如何仅部署单个可执行文件

PID在编程中怎么应用啊?

错误 MSB8024:不支持使用静态版本的 C++ 运行时库

什么叫做反射,反射在编程中起什么作用?

分形在编程中的实际应用