本文分析了全局对象的构造和析构过程。
1. 测试环境
- Linux ubuntu18arm64 4.15.0-76-generic #86-Ubuntu SMP Fri Jan 17 17:25:58 UTC 2020 aarch64 aarch64 aarch64 GNU/Linux
- gcc version 7.4.0 (Ubuntu/Linaro 7.4.0-1ubuntu1~18.04.1)
- glibc 2.27
- c++11
2. 调试分析
2.1 测试源码
1 | class Base { |
2.2 构造
全局对象的构造函数调用堆栈如下:
1 | (gdb) bt |
下面从#5往上推算全局对象的构造过程。
2.2.1 _start
查看Linux程序的入口点
1 | readelf -h objects |
通过Entry point address: 0xd80, 可以找到程序的入口点位于程序偏移0xd80处
objects程序的内存映射如下
1 | aaaaaaaaa000-aaaaaaaac000 r-xp 00000000 fd:00 2883810 /home/timzhang/project/github/dumphex/cppTestSuite/out/bin/objects |
反汇编入口点地址如下
1 | (gdb) disas 0xaaaaaaaaa000+0xd80 |
可以看到,Linux下用户态程序的入口点是_start()函数
源文件: glibc/ports/sysdeps/aarch64/start.S
1 | .text |
这里需要重点关注的是第4个参数: x3 = __libc_csu_init
2.2.2 __libc_start_main
源文件: glibc/csu/libc-start.c
__libc_start_main()代码较多, 这里简单列出相关实现:
1 | if (init) |
相关实现主要有三点
调用*init(), init就是在_start中传入的x3 = __libc_csu_init
调用main(), 执行程序
exit(): 负责调用退出处理函数,如回调函数或析构函数
2.2.3 __libc_csu_init
源文件: glibc/csu/elf-init.c
1 | /* These functions are passed to __libc_start_main by the startup code. |
_init()函数位于程序的.init section,
1 | (gdb) disas 0xaaaaaaaaa000+0xc98 |
这里看起来,_init()函数什么也没做。
继续看最后的循环部分
由于没有找到init_array_start/init_array_end的相关定义, 这里反汇编__libc_csu_init()来分析
1 | (gdb) disas __libc_csu_init |
__init_array_start = x21 = 0xaaaaaaabb000 + 0xd18 = 0xaaaaaaabbd18
__init_array_end = x20 = 0xaaaaaaabb000 + 0xd28 = 0xaaaaaaabbd28
__init_array_start[]数组主要存储了两个函数
1 | (gdb) x/2xg 0xaaaaaaabbd18 |
每个CPP源文件,都是个编译单元。在该编译单元中,编译器会生成特定的函数初始化当前编译单元的全局对象,而这些函数都统一放在__init_array_start[]数组。
2.2.4 _GLOBAL__sub_I__Z5stackv
此类函数名称类似如下
- _GLOBAL__sub_I__Z5stackv
- _GLOBAL__sub_I_b1
- _GLOBAL__sub_I_b2
- … …
这类函数,会调用另一个函数__static_initialization_and_destruction_0
1 | (gdb) disas |
2.2.5 __static_initialization_and_destruction_0
该函数主要用于完成当前编译单元内的全局对象的构造和注册析构函数。
__static_initialization_and_destruction_0的汇编code如下
1 | (gdb) disas |
全局对象的构造函数调用
1 | 0x0000aaaaaaaab044 <+84>: adrp x0, 0xaaaaaaabc000 |
传给构造函数的参数如下
- 第1个参数是全局对象的地址x0 = 0xaaaaaaabc018
- 第2个参数是w1 = 4
全局对象的析构函数注册
1 | => 0x0000aaaaaaaab054 <+100>: adrp x0, 0xaaaaaaabc000 |
和之前分析的局部静态对象类似,这里调用__cxa_atexit()注册全局对象的析构函数
- 第1个参数x0 = 0x0000aaaaaaaab0bc是Base::~Base()
- 第2个参数x1 = 0xaaaaaaabc018是全局对象的地址
- 第3个参数x2 = 0xaaaaaaabc008是dso handler
2.3 析构
全局对象的析构函数调用堆栈如下:
1 | (gdb) bt |
全局对象的析构和之前分析的局部静态对象的析构类似, 都是在程序退出调用exit()时触发析构函数的调用。
3. 总结
- 全局对象的构造发生在main()之前的__libc_csu_init(), 该函数将调用每个编译单元生成的特殊函数,这些特殊函数调用全局对象的构造函数并注册其析构函数
- 全局对象的析构发生在main()之后的exit(), 调用前面注册的析构函数