欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++全局对象的构造与析构

程序员文章站 2022-03-23 12:01:32
...

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        #全局析构函数
相关标签: C++ c++