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

智能推荐

汇编上机实验-程序员宅基地

文章浏览阅读998次,点赞24次,收藏9次。5.从DS:1000H开始存放有一个字符串”This is a string”,要求把这个字符串从后往前传送到DS:2000H开始的内存区域中(即传送结束后,从DS:2000H开始的内存单元的内容为”gnirts a si sihT”),试编写程序段并上机验证之。1.设AX寄存器中的内容为1111H,BX寄存器中的内容为2222H,DS:0010H单元中的内容为3333H。将AX寄存器中的内容与BX寄存器中的内容交换,然后再将BX寄存器中的内容与DS:0010H单元中的内容进行交换。

webUpload组件实现文件上传功能和下载功能_webuploader 0.1.5-程序员宅基地

文章浏览阅读392次。参考文章:http://blog.ncmem.com/wordpress/2023/09/14/webupload%e7%bb%84%e4%bb%b6%e5%ae%9e%e7%8e%b0%e6%96%87%e4%bb%b6%e4%b8%8a%e4%bc%a0%e5%8a%9f%e8%83%bd%e5%92%8c%e4%b8%8b%e8%bd%bd%e5%8a%9f%e8%83%bd/systemConfig.properties文件。java后端downloadServlet代码。_webuploader 0.1.5

TSINGSEE青犀AI智能分析网关V4的人员摔倒检测算法及应用-程序员宅基地

文章浏览阅读782次,点赞18次,收藏9次。在安全监控领域,人员摔倒检测算法可以应用于工厂、工地等场所,实时监控工人的安全状况。一旦发生摔倒等意外事件,系统可以迅速发出警报,提醒管理人员及时采取救援措施,保障工人的生命安全。

c语言加密字符串正序变倒序,MD5加密(自能加密,不能解密)-程序员宅基地

文章浏览阅读462次。1.必须导入系统的头文件2.//MD5加密 为字符串加密-(void)md5{//1.需要加密的内容NSString str = @"我的iOS老师是最帅";//2.将数据转换为data类型 因为系统提供的md5加密的函数为C语言函数,所以需要将OC的字符串转换成C语言的字符串const char sourceData = [str UTF8String];//3.C语言的char类型的数组,..._md5_digest_length

java练习题-计算器_java程序模拟简单计算器定义名为number-程序员宅基地

文章浏览阅读309次。java练习题-计算器问题:编写Java程序模拟简单的计算器。定义名为Number的类其中有两个整型数据成员n1和n2应声明为私有。编写构造方法赋予n1和n2初始值再为该类定义加addition()、减substation()、乘multiplication()、除division()等公有实例方法分别对两个成员变量执行加、减、乘、除的运算。在main方法中创建Number类的对象调用各个方法并显示计算结果_java程序模拟简单计算器定义名为number

什么是超市LED显示屏-程序员宅基地

文章浏览阅读203次。在商场内,LED显示屏主要应用于投放商家提供的广告,如商铺展示、商品展示、餐饮食品展示、服饰展示、化妆品展示等。超市LED显示屏主要用于超市播放广告促销等信息,显示文字信息方面,主要包括室内LED显示屏、LED透明屏、LED圆柱屏以及户外大屏幕LED显示屏等类型。常见的商场led大屏都有哪些?以上就是小编为大家做的总结了,相信大家看了之后都有了一定的了解,如果有这方面的需求,可以直接咨询迈普光彩的技术人员,都会为大家一一解答的。超市主可使用LED显示屏发布一些日常宣传信息,如申请会员、折扣、打折等。

随便推点

Java基础——java算术运算符_关系运算符,逻辑运算符结果为-程序员宅基地

文章浏览阅读438次。Java算术运算符包括:关系(比较)运算符, 逻辑运算符1、关系运算符:6个(结果为布尔值)>判断a>b?<判断a<b?<=判断a<=b?>=判断a>=b?==判断a==b?!=判断a!=b2、逻辑运算符:3个 (逻辑运算符左右两边都为布尔值,逻辑运算结果也是布尔值)&&am..._关系运算符,逻辑运算符结果为

JVM虚拟机调优指导(四)——类加载机制与编译优化_symbol: class jvmproxyconfigurer-程序员宅基地

文章浏览阅读309次。1 类文件结构1.1 MyDemo1.2 字节码1.2.1 字节码表1.2.2 魔数1.2.3 版本号1.3 常量池1.3.1 常量池容量计算1.3.2 常量类型和结构1.3.3 第一个常量1.3.4 第二个常量1.3.5 javap编译字节码1.4 描述符1.4.1 字段描述符1.4.2 方法描述符1.5 实例:字符串拼接2 类加载机制2.1 类加载时机2.2 类加载过程2.2.1 加载2.2.2 验证2.2.3 准备2.2.4 解析2.2.5 初始化2.3_symbol: class jvmproxyconfigurer

springmvc: bindException 的single-type-import已定义具有相同简名的类型_single-type-import 已定义具有相同简名的类型-程序员宅基地

文章浏览阅读6.1k次。最近学习有关springmvc的相关知识,按照下面的网址做一个简单例子:https://netbeans.org/kb/docs/web/quickstart-webapps-spring_zh_CN.html但是最后完成运行时达不到效果,http://localhost:8080/HelloSpring/hello.htm 显示正常: Enter Your Name_single-type-import 已定义具有相同简名的类型

LINUX配置ODBC环境连接达梦数据库常见错误_configure: error: in `/bigdata/apps/doris-master/t-程序员宅基地

文章浏览阅读2.7k次。1.执行./configure时报错checking for a BSD-compatible install... /usr/bin/install -cchecking whether build environment is sane... yeschecking for a thread-safe mkdir -p... /usr/bin/mkdir -pchecking for gawk... gawkchecking whether make sets $(MAKE)... yesc_configure: error: in `/bigdata/apps/doris-master/thirdparty/src/unixodbc-2.3

用Python绘制动态变化的曲线_python绘制动态曲线-程序员宅基地

文章浏览阅读1.1w次,点赞16次,收藏153次。无论从什么角度来说,圆锥曲线都非常适合动态演示,尤其是其中优美的几何关系,更能在动态变化中得到淋漓尽致的表现。_python绘制动态曲线

全国景区数据_全国旅游景点数据库-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏12次。全国景区数据汇总整理了全国31个省份及直辖市的1A-5A的景区信息,涵盖了景区名称、省份、景区级别、地址、经纬度、简介等字段,为相关研究助力! _全国旅游景点数据库

推荐文章

热门文章

相关标签