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 <__cxa_atexit@plt>        #__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 <__libc_start_main@GLIBC_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函数。

[root@xxxx]# objdump -s simpleClass
......
Contents of section .init_array:
 600dc8 b0054000 00000000 2d064000 00000000  ..@.....-.@.....
......

使用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        #全局析构函数
  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值