glibc全局构造与析构(解释了_do_global_ctors_aux)----写的相当不错!!!___do_global_ctors_aux-程序员宅基地

技术标签: Linux/Ubuntu/Fedora  

原文地址::http://www.xuebuyuan.com/839077.html

11.4  C++全局构造与析构

在C++的世界里,入口函数还肩负着另一个艰巨的使命,那就是在main的前后完成全局变量的构造与析构。本节将介绍在glibc和MSVCRT的努力下,这件事是如何完成的。

11.4.1  glibc全局构造与析构(1)

在前面介绍glibc的启动文件时已经介绍了".init"和".finit"段,我们知道这两个段中的代码最终会被拼成两个函数_init()和_finit(),这两个函数会先于/后于main函数执行。但是它们具体是在什么时候被执行的呢?由谁来负责调用它们呢?它们又是如何进行全局对象的构造和析构的呢?为了解决这些问题,这一节将继续沿着本章第一节从_start入口函数开始的那条线进行摸索,顺藤摸瓜地找到这些问题的答案。

为了表述方便,下面使用这样的代码编译出来的可执行文件进行分析:

class HelloWorld
{
public:
HelloWorld();
~HelloWorld();
};
HelloWorld Hw;
HelloWorld::HelloWorld()
{
......
}
HelloWorld::~HelloWorld()
{
......
}
int main()
{
return 0;    
}

为了了解全局对象的构造细节,对程序的启动过程进行更深一步的研究是必须的。在本章的第一节里,由_start传递进来的init函数指针究竟指向什么?通过对地址的跟踪,init实际指向了__libc_csu_init函数。这个函数位于Glibc源代码目录的csu/Elf-init.c,让我们来看看这个函数的定义:

_start -> __libc_start_main -> __libc_csu_init:
void __libc_csu_init (int argc, char **argv, char **envp)
{
…
_init ();

 

const size_t size = __init_array_end - 
__init_array_start;
for (size_t i = 0; i < size; i++)
(*__init_array_start [i]) (argc, argv, envp);
}

这段代码调用了_init函数。那么_init()是什么呢?是不是想起来前面介绍过的定义在crti.o的_init()函数呢?没错,__libc_csu_init里面调用的正是".init"段,也就是说,用户所有放在".init"段的代码就将在这里被执行。

看到这里,似乎我们的线索要断了,因为"_init"函数的实际内容并不定义在Glibc里面,它是由各个输入目标文件中的".init"段拼凑而来的。不过除了分析源代码之外,还有一个终极必杀就是反汇编目标代码,我们随意反汇编一个可执行文件就可以发现_init()函数的内容:

_start -> __libc_start_main -> __libc_csu_init -> _init:
Disassembly of section .init:
80480f4 <_init>:
80480f4:       55                      push   %ebp
80480f5:       89 e5                   mov    %esp,%ebp
80480f7:       53                      push   %ebx
80480f8:       83 ec 04                sub    $0x4,%esp
80480fb:       e8 00 00 00 00          call   8048100 <_init+0xc>
8048100:       5b                      pop    %ebx
8048101:       81 c3 9c 39 07 00       add    $0x7399c,%ebx
8048107:       8b 93 fc ff ff ff       mov    -0x4(%ebx),%edx
804810d:       85 d2                   test   %edx,%edx
804810f:       74 05                   je     8048116 <_init+0x22>
8048111:       e8 ea 7e fb f7          call   0 <_nl_current_LC_CTYPE>
8048116:       e8 95 00 00 00          call   80481b0 <frame_dummy>
804811b:       e8 b0 6e 05 00          call   809efd0 <__do_global_ctors_aux>
8048120:       58                      pop    %eax
8048121:       5b                      pop    %ebx
8048122:       c9                      leave
8048123:       c3                      ret

可以看到_init调用了一个叫做__do_global_ctors_aux的函数,如果你在glibc源代码里面查找这个函数,是不可能找到它的。因为它并不属于glibc,而是来自于GCC提供的一个目标文件crtbegin.o。我们在上一节中也介绍过,链接器在进行最终链接时,有一部分目标文件是来自于GCC,它们是那些与语言密切相关的支持函数。很明显,C++的全局对象构造是与语言密切相关的,相应负责构造的函数来自于GCC也非常容易理解。

即使它在GCC的源代码中,我们也把它揪出来。它位于gcc/Crtstuff.c,把它简化以后代码如下:

_start -> __libc_start_main -> __libc_csu_init 
-> _init -> __do_global_ctors_aux:
void __do_global_ctors_aux(void)
{
/* Call constructor functions.  */
unsigned long nptrs = (unsigned long) __CTOR_LIST__[0]; 
unsigned i;
    for (i = nptrs; i >= 1; i--)
__CTOR_LIST__[i] ();
}

上面这段代码首先将__CTOR_LIST__数组的第一个元素当做数组元素的个数,然后将第一个元素之后的元素都当做是函数指针,并一一调用。这段代码的意图非常明显,我们都可以猜到__CTOR_LIST__里面存放的是什么,没错,__CTOR_LIST__里面存放的就是所有全局对象的构造函数的指针。那么接下来的焦点很明显就是__CTOR_LIST__了,这个数组怎么来的,由谁负责构建这个数组?

__CTOR_LIST__

这里不得不暂时放下__CTOR_LIST__的身世来历,从GCC方面再追究__CTOR_LIST__未免有些乏味,我们不妨从问题的另一端,也就是从编译器如何生产全局构造函数的角度来看看全局构造函数是怎么实现的。

对于每个编译单元(.cpp),GCC编译器会遍历其中所有的全局对象,生成一个特殊的函数,这个特殊函数的作用就是对本编译单元里的所有全局对象进行初始化。我们可以通过对本节开头的代码进行反汇编得到一些粗略的信息,可以看到GCC在目标代码中生成了一个名为_GLOBAL__I_Hw的函数,由这个函数负责本编译单元所有的全局/静态对象的构造和析构,它的代码可以表示为:

static void GLOBAL__I_Hw(void)
{
Hw::Hw(); // 构造对象
atexit(__tcf_1); // 一个神秘的函数叫做__tcf_1被注册到了exit
}

 

 

 

11.4.1  glibc全局构造与析构(2)

我们暂且不管这里的神秘函数__tcf_1,它将在本节的最后部分讲到。GLOBAL__I_Hw作为特殊的函数当然也享受特殊待遇,一旦一个目标文件里有这样的函数,编译器会在这个编译单元产生的目标文件(.o)的".ctors"段里放置一个指针,这个指针指向的便是GLOBAL__I_Hw。

那么把每个目标文件的复杂全局/静态对象构造的函数地址放在一个特殊的段里面有什么好处呢?当然不为别的,为的是能够让链接器把这些特殊的段收集起来,收集齐所有的全局构造函数后就可以在初始化的时候进行构造了。

在编译器为每个编译单元生成一份特殊函数之后,链接器在连接这些目标文件时,会将同名的段合并在一起,这样,每个目标文件的.ctors段将会被合并为一个.ctors段,其中的内容是各个目标文件的.ctors段的内存拼接而成。由于每个目标文件的.ctors段都只存储了一个指针(指向该目标文件的全局构造函数),因此拼接起来的.ctors段就成为了一个函数指针数组,每一个元素都指向一个目标文件的全局构造函数。这个指针数组不正是我们想要的全局构造函数的地址列表吗?如果能得到这个数组的地址,岂不是构造的问题就此解决了?

没错,得到这个数组的地址其实也不难,我们可以效仿前面".init"和".finit"拼凑的办法,对".ctor"段也进行拼凑。还记得在链接的时候,各个用户产生的目标文件的前后分别还要链接上一个crtbegin.o和crtend.o吧?这两个glibc自身的目标文件同样具有.ctors段,在链接的时候,这两个文件的.ctors段的内容也会被合并到最终的可执行文件中。那么这两个文件的.ctors段里有什么呢?

crtbegin.o:作为所有.ctors段的开头部分,crtbegin.o的.ctor段里面存储的是一个4字节的 1(0xFFFFFFFF),由链接器负责将这个数字改成全局构造函数的数量。然后这个段还将起始地址定义成符号__CTOR_LIST__,这样实际上__CTOR_LIST__所代表的就是所有.ctor段最终合并后的起始地址了。

crtend.o:这个文件里面的.ctors内容就更简单了,它的内容就是一个0,然后定义了一个符号__CTOR_END__,指向.ctor段的末尾。

在前面的章节中已经介绍过了,链接器在链接用户的目标文件的时候,crtbegin.o总是处在用户目标文件的前面,而crtend.o则总是处在用户目标文件的后面。例如链接两个用户的目标文件a.o和b.o时,实际链接的目标文件将是(按顺序)ld crti.o crtbegin.o a.o b.o crtend.o crtn.o。在这里我们忽略crti.o和crtn.o,因为这两个目标文件和全局构造无关。在合并crtbegin.o、用户目标文件和crtend.o时,链接器按顺序拼接这些文件的.ctors段,因此最终形成.ctors段的过程将如图11-10所示。

 
(点击查看大图)图11-10  .ctor段的形成

在了解了可执行文件的.ctors段的结构之后,再回过头来看__do_global_ctor_aux的代码就很容易了。__do_global_ctor_aux从__CTOR_LIST__的下一个位置开始,按顺序执行函数指针,直到遇上NULL(__CTOR_END__)。如此每个目标文件的全局构造函数都能被调用。

【小实验】

在main前调用函数:

glibc的全局构造函数是放置在.ctors段里的,因此如果我们手动在.ctors段里添加一些函数指针,就可以让这些函数在全局构造的时候(main之前)调用:

#include <stdio.h>
void my_init(void) 
{
printf("Hello ");
}

 

typedef void (*ctor_t)(void); 
//在.ctors段里添加一个函数指针
ctor_t __attribute__((section (".ctors"))) my_init_p = &my_init;

 

int main() 
{
printf("World!/n");
return 0;
}

如果运行此程序,结果将打印出:Hello World!

当然,事实上,gcc里有更加直接的办法来达到相同的目的,那就是使用__attribute__((constructor))

示例如下:

#include <stdio.h>
void my_init(void) __attribute__ ((constructor));
void my_init(void) 
{
printf("Hello ");
}
int main() 
{
printf("World!/n");
return 0;
}

11.4.1  glibc全局构造与析构(3)

析构

对于早期的glibc和GCC,在完成了对象的构造之后,在程序结束之前,crt还要进行对象的析构。实际上正常的全局对象析构与前面介绍的构造在过程上是完全类似的,而且所有的函数、符号名都一一对应,比如".init"变成了".finit"、"__do_global_ctor_aux"变成了"__do_global_dtor_aux"、"__CTOR_LIST__"变成了"__DTOR_LIST__"等。在前面介绍入口函数时我们可以看到,__libc_start_main将"__libc_csu_fini"通过__cxa_exit()注册到退出列表中,这样当进程退出前exit()里面就会调用"__libc_csu_fini"。"_fini"的原理和"_init"基本是一样的,在这里不再一一赘述了。

不过这样做的好处是为了保证全局对象构造和析构的顺序(即先构造后析构),链接器必须包装所有的".dtor"段的合并顺序必须是".ctor"的严格反序,这增加了链接器的工作量,于是后来人们放弃了这种做法,采用了一种新的做法,就是通过__cxa_atexit()在exit()函数中注册进程退出回调函数来实现析构。

这就要回到我们之前在每个编译单元的全局构造函数GLOBAL__I_Hw()中看到的神秘函数。编译器对每个编译单元的全局对象,都会生成一个特殊的函数来调用这个编译单元的所有全局对象的析构函数,它的调用顺序与GLOBAL__I_Hw()调用构造函数的顺序刚好相反。例如对于前面的例子中的代码,编译器生成的所谓的神秘函数内容大致是:

 

static void __tcf_1(void) //这个名字由编译器生成
{
Hw.~HelloWorld();
}

此函数负责析构Hw对象,由于在GLOBAL__I_Hw中我们通过__cxa_exit()注册了__tcf_1,而且通过__cxa_exit()注册的函数在进程退出时被调用的顺序满足先注册后调用的属性,与构造和析构的顺序完全符合,于是它就很自然被用于析构函数的实现了。

当然在本节中介绍glibc/GCC的全局对象构造和析构时,省略了不少我们认为超出了本书所要强调的范围细节,真正的构造和析构过程比上面介绍的要复杂一些,并且在动态链接和静态链接不同的情况下,构造和析构还略有不同。但是不管哪种情况,基本的原理都是相通的,按照上面介绍的步骤和路径,相信读者也能够自己重新根据真实的情况梳理清楚这条调用路线。

由于全局对象的构建和析构都是由运行库完成的,于是在程序或共享库中有全局对象时,记得不能使用"-nonstartfiles"或"-nostdlib"选项,否则,构建与析构函数将不能正常执行(除非你很清楚自己的行为,并且手工构造和析构全局对象)。

Collect2

我们在第2章时曾经碰到过collect2这个程序,在链接时它代替ld成为了最终链接器,一般情况下就可以简单地将它看成ld。实际上collect2是ld的一个包装,它最终还是调用ld完成所有的链接工作,那么collect2这个程序的作用是什么呢?

在有些系统上,汇编器和链接器并不支持本节中所介绍的".init"".ctor"这种机制,于是为了实现在main函数前执行代码,必须在链接时进行特殊的处理。Collect2这个程序就是用来实现这个功能的,它会"收集"(collect)所有输入目标文件中那些命名特殊的符号,这些特殊的符号表明它们是全局构造函数或在main前执行,collect2会生成一个临时的.c文件,将这些符号的地址收集成一个数组,然后放到这个.c文件里面,编译后与其他目标文件一起被链接到最终的输出文件中。

在这些平台上,GCC编译器也会在main函数的开始部分产生一个__main函数的调用,这个函数实际上就是负责collect2收集来的那些函数。__main函数也是GCC所提供的目标文件的一部分,如果我们使用"-nostdlib"编译程序,可能得到__main函数未定义的错误,这时候只要加上"-lgcc"把它链接上即可。




版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/xqhrs232/article/details/79866584

智能推荐

Docker 快速上手学习入门教程_docker菜鸟教程-程序员宅基地

文章浏览阅读2.5w次,点赞6次,收藏50次。官方解释是,docker 容器是机器上的沙盒进程,它与主机上的所有其他进程隔离。所以容器只是操作系统中被隔离开来的一个进程,所谓的容器化,其实也只是对操作系统进行欺骗的一种语法糖。_docker菜鸟教程

电脑技巧:Windows系统原版纯净软件必备的两个网站_msdn我告诉你-程序员宅基地

文章浏览阅读5.7k次,点赞3次,收藏14次。该如何避免的,今天小编给大家推荐两个下载Windows系统官方软件的资源网站,可以杜绝软件捆绑等行为。该站提供了丰富的Windows官方技术资源,比较重要的有MSDN技术资源文档库、官方工具和资源、应用程序、开发人员工具(Visual Studio 、SQLServer等等)、系统镜像、设计人员工具等。总的来说,这两个都是非常优秀的Windows系统镜像资源站,提供了丰富的Windows系统镜像资源,并且保证了资源的纯净和安全性,有需要的朋友可以去了解一下。这个非常实用的资源网站的创建者是国内的一个网友。_msdn我告诉你

vue2封装对话框el-dialog组件_<el-dialog 封装成组件 vue2-程序员宅基地

文章浏览阅读1.2k次。vue2封装对话框el-dialog组件_

MFC 文本框换行_c++ mfc同一框内输入二行怎么换行-程序员宅基地

文章浏览阅读4.7k次,点赞5次,收藏6次。MFC 文本框换行 标签: it mfc 文本框1.将Multiline属性设置为True2.换行是使用"\r\n" (宽字符串为L"\r\n")3.如果需要编辑并且按Enter键换行,还要将 Want Return 设置为 True4.如果需要垂直滚动条的话将Vertical Scroll属性设置为True,需要水平滚动条的话将Horizontal Scroll属性设_c++ mfc同一框内输入二行怎么换行

redis-desktop-manager无法连接redis-server的解决方法_redis-server doesn't support auth command or ismis-程序员宅基地

文章浏览阅读832次。检查Linux是否是否开启所需端口,默认为6379,若未打开,将其开启:以root用户执行iptables -I INPUT -p tcp --dport 6379 -j ACCEPT如果还是未能解决,修改redis.conf,修改主机地址:bind 192.168.85.**;然后使用该配置文件,重新启动Redis服务./redis-server redis.conf..._redis-server doesn't support auth command or ismisconfigured. try

实验四 数据选择器及其应用-程序员宅基地

文章浏览阅读4.9k次。济大数电实验报告_数据选择器及其应用

随便推点

灰色预测模型matlab_MATLAB实战|基于灰色预测河南省社会消费品零售总额预测-程序员宅基地

文章浏览阅读236次。1研究内容消费在生产中占据十分重要的地位,是生产的最终目的和动力,是保持省内经济稳定快速发展的核心要素。预测河南省社会消费品零售总额,是进行宏观经济调控和消费体制改变创新的基础,是河南省内人民对美好的全面和谐社会的追求的要求,保持河南省经济稳定和可持续发展具有重要意义。本文建立灰色预测模型,利用MATLAB软件,预测出2019年~2023年河南省社会消费品零售总额预测值分别为21881...._灰色预测模型用什么软件

log4qt-程序员宅基地

文章浏览阅读1.2k次。12.4-在Qt中使用Log4Qt输出Log文件,看这一篇就足够了一、为啥要使用第三方Log库,而不用平台自带的Log库二、Log4j系列库的功能介绍与基本概念三、Log4Qt库的基本介绍四、将Log4qt组装成为一个单独模块五、使用配置文件的方式配置Log4Qt六、使用代码的方式配置Log4Qt七、在Qt工程中引入Log4Qt库模块的方法八、获取示例中的源代码一、为啥要使用第三方Log库,而不用平台自带的Log库首先要说明的是,在平时开发和调试中开发平台自带的“打印输出”已经足够了。但_log4qt

100种思维模型之全局观思维模型-67_计算机中对于全局观的-程序员宅基地

文章浏览阅读786次。全局观思维模型,一个教我们由点到线,由线到面,再由面到体,不断的放大格局去思考问题的思维模型。_计算机中对于全局观的

线程间控制之CountDownLatch和CyclicBarrier使用介绍_countdownluach于cyclicbarrier的用法-程序员宅基地

文章浏览阅读330次。一、CountDownLatch介绍CountDownLatch采用减法计算;是一个同步辅助工具类和CyclicBarrier类功能类似,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。二、CountDownLatch俩种应用场景: 场景一:所有线程在等待开始信号(startSignal.await()),主流程发出开始信号通知,既执行startSignal.countDown()方法后;所有线程才开始执行;每个线程执行完发出做完信号,既执行do..._countdownluach于cyclicbarrier的用法

自动化监控系统Prometheus&Grafana_-自动化监控系统prometheus&grafana实战-程序员宅基地

文章浏览阅读508次。Prometheus 算是一个全能型选手,原生支持容器监控,当然监控传统应用也不是吃干饭的,所以就是容器和非容器他都支持,所有的监控系统都具备这个流程,_-自动化监控系统prometheus&grafana实战

React 组件封装之 Search 搜索_react search-程序员宅基地

文章浏览阅读4.7k次。输入关键字,可以通过键盘的搜索按钮完成搜索功能。_react search