Wednesday, April 6, 2011

小技巧: 在C++中实现在main函数之前及之后执行代码 (续)

大概1年前我写过一篇叫做 "小技巧: 在C++中实现在main函数之前及之后执行代码" 的文章, 当时文中只介绍了技巧, 却没有深究技巧实现的原因. 后来 Googol Lee 同学留言 《程序员的自我修养》这本书有详细讲述, 近期刚好把书看完, 便琢磨着自己再总结总结, 给原来的文章续一个结尾.

下面的分析都是基于以前那篇文章里的C++代码, 假设我们编译后生成的可执行文件叫做 alist. 首先祭出反汇编利器 objdump:

$ objdump -sdC alist | less

我们都知道程序在进入 main 函数之前会执行 .init 段里的代码 (这个可以从glibc的入口函数源码中分析得到), 那么我们就从 .init 开始看起:

Disassembly of section .init:

08048508 <_init>:
 8048508:       55                      push   %ebp
 8048509:       89 e5                   mov    %esp,%ebp
 804850b:       53                      push   %ebx
 804850c:       83 ec 04                sub    $0x4,%esp
 804850f:       e8 00 00 00 00          call   8048514 <_init+0xc>
 8048514:       5b                      pop    %ebx
 8048515:       81 c3 e0 1a 00 00       add    $0x1ae0,%ebx
 804851b:       8b 93 fc ff ff ff       mov    -0x4(%ebx),%edx
 8048521:       85 d2                   test   %edx,%edx
 8048523:       74 05                   je     804852a <_init+0x22>
 8048525:       e8 2e 00 00 00          call   8048558 <__gmon_start__@plt>
 804852a:       e8 41 01 00 00          call   8048670 <frame_dummy>
 804852f:       e8 0c 03 00 00          call   8048840 <__do_global_ctors_aux>
 8048534:       58                      pop    %eax
 8048535:       5b                      pop    %ebx
 8048536:       c9                      leave
 8048537:       c3                      ret

重点看红色那行, 从调用的函数名就可以看出一定是此地无银, 跟进 __do_global_ctors_aux 函数 (这个函数的源码包含在GCC中) 看看:

08048840 <__do_global_ctors_aux>:
 8048840:       55                      push   %ebp
 8048841:       89 e5                   mov    %esp,%ebp
 8048843:       53                      push   %ebx
 8048844:       83 ec 04                sub    $0x4,%esp
 8048847:       a1 fc 9e 04 08          mov    0x8049efc,%eax
 804884c:       83 f8 ff                cmp    $0xffffffff,%eax
 804884f:       74 13                   je     8048864 <__do_global_ctors_aux+0x
24>
 8048851:       bb fc 9e 04 08          mov    $0x8049efc,%ebx
 8048856:       66 90                   xchg   %ax,%ax
 8048858:       83 eb 04                sub    $0x4,%ebx
 804885b:       ff d0                   call   *%eax
 804885d:       8b 03                   mov    (%ebx),%eax
 804885f:       83 f8 ff                cmp    $0xffffffff,%eax
 8048862:       75 f4                   jne    8048858 <__do_global_ctors_aux+0x
18>
 8048864:       83 c4 04                add    $0x4,%esp
 8048867:       5b                      pop    %ebx
 8048868:       5d                      pop    %ebp
 8048869:       c3                      ret
 804886a:       90                      nop
 804886b:       90                      nop

红色部分是一个循环, 从 call *%eax 可以看出是在循环调用函数, 且 %eax 指向的是函数指针. 究竟 %eax 中是什么内容, 我们往回看那行蓝色的代码, 一切都引向了 0x8049efc 这个地址, 我们得看看其中包含什么, 再往回在其它段中寻找, 可以发现如下部分:

Contents of section .ctors:
 8049ef8 ffffffff 55870408 00000000           ....U.......

0x8049ef8 加上4正好是 0x8049efc, %eax 这个函数指针的值也就是 0x8048755 (little endian), 再来看看 0x8058755 指向的函数是什么:

08048755 <global constructors keyed to a>:
 8048755:       55                      push   %ebp
 8048756:       89 e5                   mov    %esp,%ebp
 8048758:       83 ec 18                sub    $0x18,%esp
 804875b:       c7 44 24 04 ff ff 00    movl   $0xffff,0x4(%esp)
 8048762:       00
 8048763:       c7 04 24 01 00 00 00    movl   $0x1,(%esp)
 804876a:       e8 7d ff ff ff          call   80486ec <__static_initialization_and_destruction_0(int, int)>
 804876f:       c9                      leave
 8048770:       c3                      ret
 8048771:       90                      nop

继续跟进 __static_initialization_and_destruction_0(int, int) 函数:

080486ec <__static_initialization_and_destruction_0(int, int)>:
 80486ec:       55                      push   %ebp
 80486ed:       89 e5                   mov    %esp,%ebp
 80486ef:       83 ec 18                sub    $0x18,%esp
 80486f2:       83 7d 08 01             cmpl   $0x1,0x8(%ebp)
 80486f6:       75 5b                   jne    8048753 <__static_initialization_
and_destruction_0(int, int)+0x67>
 80486f8:       81 7d 0c ff ff 00 00    cmpl   $0xffff,0xc(%ebp)
 80486ff:       75 52                   jne    8048753 <__static_initialization_
and_destruction_0(int, int)+0x67>
 8048701:       c7 04 24 d5 a0 04 08    movl   $0x804a0d5,(%esp)
 8048708:       e8 5b fe ff ff          call   8048568 <std::ios_base::Init::Init()@plt>
 804870d:       b8 88 85 04 08          mov    $0x8048588,%eax
 8048712:       c7 44 24 08 28 a0 04    movl   $0x804a028,0x8(%esp)
 8048719:       08
 804871a:       c7 44 24 04 d5 a0 04    movl   $0x804a0d5,0x4(%esp)
 8048721:       08
 8048722:       89 04 24                mov    %eax,(%esp)
 8048725:       e8 1e fe ff ff          call   8048548 <__cxa_atexit@plt>
 804872a:       c7 04 24 d4 a0 04 08    movl   $0x804a0d4,(%esp)
 8048731:       e8 3c 00 00 00          call   8048772 <A::A()>
 8048736:       b8 9e 87 04 08          mov    $0x804879e,%eax
 804873b:       c7 44 24 08 28 a0 04    movl   $0x804a028,0x8(%esp)
 8048742:       08
 8048743:       c7 44 24 04 d4 a0 04    movl   $0x804a0d4,0x4(%esp)
 804874a:       08
 804874b:       89 04 24                mov    %eax,(%esp)
 804874e:       e8 f5 fd ff ff          call   8048548 <__cxa_atexit@plt>
 8048753:       c9                      leave
 8048754:       c3                      ret

终于找到了调用类的构造函数的地方, 在调用完构造函数之后还通过 __cxa_atexit 函数 (这是glibc用于内部调用的函数, 功能和我们熟知的 atexit 函数类似) 注册了一个回调函数, 函数地址是 0x804879e, 可以猜测这个函数就是析构函数, 找到这个地址:

0804879e <A::~A()>:
 804879e:       55                      push   %ebp
 804879f:       89 e5                   mov    %esp,%ebp
 80487a1:       83 ec 18                sub    $0x18,%esp
...

果然如我们所想, 这样就保证了构造函数和析构函数的配对使用. 总结一下C++中全局变量初始化的过程:

_init -> __do_global_ctors_aux -> global constructors keyed to a -> __static_initialization_and_destruction_0(int, int) -> A::A()

这里只是从反汇编的角度进行分析, 如果需要更深入了解, 还得结合glibc和GCC的源码. 关于 .ctors 段也没有详细讲述, 这些都可以在《程序员的自我修养》中找到.

No comments:

Post a Comment