大概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