C++全局对象的构造与析构
C++全局对象构造与析构
先上代码:
//simpleClass.cpp
class A{
int val;
public:
A(int n){
val = n;
}
~A(){
val = 0;
}
};
A a1(1);
int main(){
A a2(2);
return 0;
}
抛出问题:局部对象a2和全局对象a1分别在何处调用构造函数和析构函数?
将上述cpp文件编译成可一致性文件:
g++ -o simpleClass simpleClass.cpp
使用objdump查看其反汇编代码:
objdump -d simpleClass
先看main函数部分汇编代码:
......
0000000000400642 <_ZN1AC1Ei>:
400642: 55 push %rbp
400643: 48 89 e5 mov %rsp,%rbp
400646: 48 89 7d f8 mov %rdi,-0x8(%rbp) #rdi寄存器保存是this指针
40064a: 89 75 f4 mov %esi,-0xc(%rbp) #esi中是传入的参数
40064d: 48 8b 45 f8 mov -0x8(%rbp),%rax
400651: 8b 55 f4 mov -0xc(%rbp),%edx
400654: 89 10 mov %edx,(%rax)
400656: 90 nop
400657: 5d pop %rbp
400658: c3 retq
400659: 90 nop
000000000040065a <_ZN1AD1Ev>:
40065a: 55 push %rbp
40065b: 48 89 e5 mov %rsp,%rbp
40065e: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400662: 48 8b 45 f8 mov -0x8(%rbp),%rax
400666: c7 00 00 00 00 00 movl $0x0,(%rax)
40066c: 90 nop
40066d: 5d pop %rbp
40066e: c3 retq
40066f: 90 nop
......
00000000004005b6 <main>:
4005b6: 55 push %rbp
4005b7: 48 89 e5 mov %rsp,%rbp
4005ba: 53 push %rbx
4005bb: 48 83 ec 18 sub $0x18,%rsp
4005bf: 48 8d 45 ec lea -0x14(%rbp),%rax #得到局部对象a2在栈上的地址
4005c3: be 02 00 00 00 mov $0x2,%esi #传入构造函数的参数2
4005c8: 48 89 c7 mov %rax,%rdi #传入的第二个参数是局部对象地址
4005cb: e8 72 00 00 00 callq 400642 <_ZN1AC1Ei> #调用A类构造函数
4005d0: bb 00 00 00 00 mov $0x0,%ebx
4005d5: 48 8d 45 ec lea -0x14(%rbp),%rax
4005d9: 48 89 c7 mov %rax,%rdi
4005dc: e8 79 00 00 00 callq 40065a <_ZN1AD1Ev> #调用A类析构函数
4005e1: 89 d8 mov %ebx,%eax
4005e3: 48 83 c4 18 add $0x18,%rsp
4005e7: 5b pop %rbx
4005e8: 5d pop %rbp
4005e9: c3 retq
从上述汇编代码可以看出,A类的构造函数被C++ name-mangling机制命名为了_ZN1AC1Ei,析构函数为_ZN1AD1Ev,详情看注释。
现在已经知道main函数中的局部对象a2的构造和析构都在main函数中进行。那么全局对象a1的构造和析构又在什么地方进行呢?
细心的小朋友一定会发现,在反汇编代码中还有这样一段代码:
......
00000000004005ea <_Z41__static_initialization_and_destruction_0ii>: #从函数名可以看出端倪
4005ea: 55 push %rbp
4005eb: 48 89 e5 mov %rsp,%rbp
4005ee: 48 83 ec 10 sub $0x10,%rsp
4005f2: 89 7d fc mov %edi,-0x4(%rbp)
4005f5: 89 75 f8 mov %esi,-0x8(%rbp)
4005f8: 83 7d fc 01 cmpl $0x1,-0x4(%rbp)
4005fc: 75 2c jne 40062a <_Z41__static_initialization_and_destruction_0ii+0x40>
4005fe: 81 7d f8 ff ff 00 00 cmpl $0xffff,-0x8(%rbp)
400605: 75 23 jne 40062a <_Z41__static_initialization_and_destruction_0ii+0x40>
400607: be 01 00 00 00 mov $0x1,%esi
40060c: bf 28 10 60 00 mov $0x601028,%edi
400611: e8 2c 00 00 00 callq 400642 <_ZN1AC1Ei> #这里调用了构造函数
400616: ba 00 07 40 00 mov $0x400700,%edx
40061b: be 28 10 60 00 mov $0x601028,%esi
400620: bf 5a 06 40 00 mov $0x40065a,%edi #析构函数的地址
400625: e8 96 fe ff ff callq 4004c0 <[email protected]> #__cxa_atexit的作用是注册一个回调函数,在exit时执行
40062a: 90 nop
40062b: c9 leaveq
40062c: c3 retq
000000000040062d <_GLOBAL__sub_I_a1>:
40062d: 55 push %rbp
40062e: 48 89 e5 mov %rsp,%rbp
400631: be ff ff 00 00 mov $0xffff,%esi
400636: bf 01 00 00 00 mov $0x1,%edi
40063b: e8 aa ff ff ff callq 4005ea <_Z41__static_initialization_and_destruction_0ii>
400640: 5d pop %rbp
400641: c3 retq
......
可以看到_Z41__static_initialization_and_destruction_0ii函数名的意义已经很明显了:静态对象(全局对象)初始化和析构。在这个函数中调用了静态对象(全局对象)的构造函数,并且将其析构函数通过_cxa_atexit函数注册,使其能在exit时调用。
_GLOBAL__sub_I_a1函数负责本编译单元所有全局\静态对象的构造和析构,那么这个函数在哪里被调用的呢?首先我们需要从程序的入口开始了解。
程序的入口
既然已经知道全局对象的构造和析构不在main函数中,那么至少说明了一个事情:main函数并不是程序的入口。
实际上在Linux环境下,glibc程序的入口地址是_start,这个入口是由ld链接器默认的链接脚本所指定的,当然也可以通过相关参数设定自己的入口。
直接看_start函数的汇编代码:
00000000004004d0 <_start>:
4004d0: f3 0f 1e fa endbr64
4004d4: 31 ed xor %ebp,%ebp
4004d6: 49 89 d1 mov %rdx,%r9
4004d9: 5e pop %rsi
4004da: 48 89 e2 mov %rsp,%rdx
4004dd: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
4004e1: 50 push %rax
4004e2: 54 push %rsp
4004e3: 49 c7 c0 e0 06 40 00 mov $0x4006e0,%r8 # 0x4006e0 是__libc_csu_fini函数的地址
4004ea: 48 c7 c1 70 06 40 00 mov $0x400670,%rcx # 0x400670 是__libc_csu_init函数的地址
4004f1: 48 c7 c7 b6 05 40 00 mov $0x4005b6,%rdi # 0x4006b6 是main函数的入口地址
4004f8: ff 15 ea 0a 20 00 callq *0x200aea(%rip) # 600fe8 <[email protected]_2.2.5>
4004fe: f4 hlt
这里简单解释一下,__libc_csu_init函数是在main函数调用前调用的函数,全局对象的构造函数就是在这个过程被执行的,而__libc_csu_fini函数是在main调用后调用的函数,全局对象的析构就是在这个过程被执行的。
从_start的汇编代码可以看出,实际上_start函数里面调用了__libc_start_main函数,并把__libc_csu_init函数和__libc_csu_fini函数以及main函数的地址作为参数传递进去。
由于__libc_start_main函数是在glibc动态链接库里的函数,所以可执行文件的反汇编代码中并没有这一部分的代码,不过我们只需要大概了解其中先后调用关系如下:
__libc_csu_init
main
__libc_csu_fint
__libc_csu_init函数
0000000000400670 <__libc_csu_init>:
400670: f3 0f 1e fa endbr64
400674: 41 57 push %r15
400676: 49 89 d7 mov %rdx,%r15
400679: 41 56 push %r14
40067b: 49 89 f6 mov %rsi,%r14
40067e: 41 55 push %r13
400680: 41 89 fd mov %edi,%r13d
400683: 41 54 push %r12
400685: 4c 8d 25 3c 07 20 00 lea 0x20073c(%rip),%r12 # 600dc8 <__frame_dummy_init_array_entry>
40068c: 55 push %rbp
40068d: 48 8d 2d 44 07 20 00 lea 0x200744(%rip),%rbp # 600dd8 <__init_array_end>
400694: 53 push %rbx
400695: 4c 29 e5 sub %r12,%rbp
400698: 48 83 ec 08 sub $0x8,%rsp
40069c: e8 ef fd ff ff callq 400490 <_init> #调用_init段的代码
4006a1: 48 c1 fd 03 sar $0x3,%rbp
4006a5: 74 1f je 4006c6 <__libc_csu_init+0x56>
4006a7: 31 db xor %ebx,%ebx
4006a9: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
4006b0: 4c 89 fa mov %r15,%rdx
4006b3: 4c 89 f6 mov %r14,%rsi
4006b6: 44 89 ef mov %r13d,%edi
4006b9: 41 ff 14 dc callq *(%r12,%rbx,8) #这个地方是重点
4006bd: 48 83 c3 01 add $0x1,%rbx
4006c1: 48 39 dd cmp %rbx,%rbp
4006c4: 75 ea jne 4006b0 <__libc_csu_init+0x40>
4006c6: 48 83 c4 08 add $0x8,%rsp
4006ca: 5b pop %rbx
4006cb: 5d pop %rbp
4006cc: 41 5c pop %r12
4006ce: 41 5d pop %r13
4006d0: 41 5e pop %r14
4006d2: 41 5f pop %r15
4006d4: c3 retq
光看汇编代码有点难以理解,我们可以去查看glibc的源代码中的__libc_csu_init函数,其中关键代码部分:
void __libc_csu_init (int argc, char **argv, char **envp){
...
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_array_satrt数组中就有全局对象构造函数的地址
}
...
}
可以看出,__libc_csu_init函数中会将__init_array_start数组中每个指针指向的函数执行一遍。
现在回到原来的问题,负责本编译单元所有全局\静态对象的构造和析构的_GLOBAL__sub_I_a1函数的指针被保存在__init_array_start数组中,也就是在,__libc_csu_init函数中被调用的。
那么_GLOBAL__sub_I_a1函数的指针怎么被放进__init_array_start数组的呢?答案是,一旦一个目标文件里有一个这样的函数,编译器会在这个编译单元产生的目标文件(.o)文件的“.init_array”段中放置一个指针,这个指针指向的就是_GLOBAL__sub_I_a1函数。
[[email protected]]# objdump -s simpleClass
......
Contents of section .init_array:
600dc8 b0054000 00000000 2d064000 00000000 [email protected]@.....
......
使用objdump查看.init_array段中的数据发现一个指针2d064000,大小端交换后为0x0040062d,就是_GLOBAL__sub_I_a1函数位置。
在gcc 4.7之前,_GLOBAL__sub_I_a1函数的指针存放在.ctors段中,在之后的版本中都存放在.init_array段中。
析构
在__libc_start_main函数中执行完main函数之后,执行exit函数:
void exit(int status){
while(__exit_func != NULL){
...
__exit_funcs = __exit_funcs->next; //__exit_funcs是存储由_cxa_atexit组成的函数的链表,
//这里的while循环则遍历该链表并逐个调用这些注册的函数
}
...
_exit(status); //调用exit系统调用,进程直接结束
}
总结
现在总结一下程序从启动到结束的过程,全局/静态对象的构造和析构的位置就一目了然了。
_start
---> _libc_start_main
------> _libc_csu_init
---------> _GLOBAL_sub_I_a1
------------> _Z41__static_initialization_and_destruction_0ii
---------------> _ZN1AC1Ei #全局构造函数
------> main #main函数
------> exit
---------> _ZN1AD1Ev #全局析构函数
下一篇: php ftp怎么实现删除文件