2013年9月14日星期六

gcc中的constructor属性和destructor属性

   constructor属性可以使函数在main()函数之前执行,destructor属性会让函数在main()函数完成或调用exit()之后被执行。这些属性可以用来在程序运行之前初始化所需的数据,非常有用。而且这两个属性都还可以指定优先级,控制使用修饰的函数的执行顺序,优先级的值必须大于100,因为0到100之间的优先级由gcc来使用,优先级的值越小,优先级越高,会优先执行。另外还有一点需要注意,如果是接收到信号退出,例如SIGSEGV或者SIGKILL信号,destructor属性修饰的函数则不会被调用。具体可以参见《Declaring Attributes of Functions》
我们先来看看不指定优先级时,调用的顺序是什么样的,示例程序如下:
#include <stdio.h>
#include <stdlib.h>

static void __attribute__((constructor)) pre_main1(void)
{
printf("Come to %s\n", __func__);
}

static void __attribute__((constructor)) pre_main2(void)
{
printf("Come to %s\n", __func__);
}

static void __attribute__((constructor)) pre_main3(void)
{
printf("Come to %s\n", __func__);
}

int main(void)
{
printf("Exiting.....\n");
return 0;
}

static void __attribute__((destructor)) back_main1(void)
{
printf("Come to %s\n", __func__);
}

static void __attribute__((destructor)) back_main2(void)
{
printf("Come to %s\n", __func__);
}
编译后执行,输出结果如下:

Come to pre_main3
Come to pre_main2
Come to pre_main1
Exiting.....
Come to back_main1
Come to back_main2
这个结果比较意外,因为我之前测试的时候,没有指定优先级时,执行的顺序和函数在源码中的顺序一样,而这里constructor属性修饰的函数,其执行顺序刚好相反。我又找了一个在线的编译器,发现输出结果和前面的也不一样。
看来如果没有指定优先级时constructor属性和destructor属性修饰的函数的执行顺序和编译器、系统有关。不过也让我很好奇,这些函数是怎么被调用的,所以就继续深入下去。
constructor属性和destructor属性修饰的函数的地址会分别存入.ctors section和.dctors section,这两个段中存放的内容都被当作函数处理,段中的函数分别由__do_global_ctors_aux函数和__do_global_dtors_aux函数去调用执行。下面的讨论主要围绕这两个函数展开。注意,后面看到的一些地址都只是我的机器上看到的,不同的机器可能不一样。
首先来看.ctors section的内容,如下所示:
[root@CentOS_190 debug]# objdump -s -j .ctors a.out

a.out: file format elf64-x86-64

Contents of section .ctors:
600868 ffffffff ffffffff 04054000 00000000 ..........@.....
600878 21054000 00000000 3e054000 00000000 !.@.....>.@.....
600888 00000000 00000000 ........
[root@CentOS_190 debug]#
从上面我们可以看到,.ctors section的开始位置和结束位置的地址分别为0x600868和0x600888(这两个地址非常重要,后面会讲到),对应的值为0xffffffffffffffff和0x0000000000000000,这两个值相当于也是.ctors section的开始和结束标志。在.ctors section中有三个函数地址,分别为0x0000000000400504、0x0000000000400521和0x000000000040053e。如果你对这里的三个地址有疑惑,请注意前面输出的文件格式elf64-x86-64,也就是说这里生成的可执行程序是在x86架构下,x86下的字节模式是小端模式,即数据的低位保存在低地址,高位保存在高地址,所以真实的数据要颠倒过来看。
在我们的示例程序中,pre_main1、pre_main2和pre_main3三个函数使用了constructor属性,所以这三个函数的地址会放在.ctors section中,但是现在还不知道存放的顺序是什么。先来确定这三个函数的地址,如下所示:
[root@CentOS_190 debug]# objdump -d a.out | grep pre_main*
0000000000400504 <pre_main1>:
0000000000400521 <pre_main2>:
000000000040053e <pre_main3>:
[root@CentOS_190 debug]#
现在我们可以确定.ctors section中的结构,如下所示:

接下来我们看看__do_global_ctors_aux()函数是如何来处理.ctors section的。__do_global_ctors_aux函数由.init section的_init函数调用,其反汇编结果如下所示:
0000000000400650 <__do_global_ctors_aux>:
400650: 55 push %rbp
400651: 48 89 e5 mov %rsp,%rbp
400654: 53 push %rbx
400655: 48 83 ec 08 sub $0x8,%rsp
400659: 48 8b 05 20 02 20 00 mov 0x200220(%rip),%rax # 600880 <__CTOR_LIST__+0x18>
400660: 48 83 f8 ff cmp $0xffffffffffffffff,%rax
400664: 74 19 je 40067f <__do_global_ctors_aux+0x2f>
400666: bb 80 08 60 00 mov $0x600880,%ebx
40066b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
400670: 48 83 eb 08 sub $0x8,%rbx
400674: ff d0 callq *%rax
400676: 48 8b 03 mov (%rbx),%rax
400679: 48 83 f8 ff cmp $0xffffffffffffffff,%rax
40067d: 75 f1 jne 400670 <__do_global_ctors_aux+0x20>
40067f: 48 83 c4 08 add $0x8,%rsp
400683: 5b pop %rbx
400684: c9 leaveq
400685: c3 retq
400686: 90 nop
400687: 90 nop
地址为400659处的指令将.ctors section结尾处(结尾处的值为0x0000000000000000)的前一个位置的值保存到rax寄存器中。如果.ctors section为空,则rax中的值为0xffffffffffffffff,即.ctors section开始位置的值。如果.ctors section为空,则400660处的指令比较后会执行je指令,跳转到40067f处执行,然后直接从__do_global_ctors_aux()函数退出。如果.ctors section不为空,则rax中存储的是一个函数的地址,结合上面的.ctors section的结构图,我们不难看出,此时rax中的值就是pre_main3()函数的地址值。如果不为空,则开始执行400666处的指令,这条指令将0x600880这个常数值保存在rbx寄存器中,这个常数值就是pre_main3()函数的地址值在.ctors section中的地址值。注意,此时rbx中只是存储的是一个地址值,并不是具体的函数地址值。接着调用sub指令,将rbx减8,此时rbx的值为0x600878,也就是pre_main2()函数的地址值在.ctors section中的地址值。接着会使用callq指令,调用rax寄存器中保存的函数地址,此时的函数地址是pre_main3()函数的地址。在调用完函数后,将rbx寄存器中存储的地址中的数值保存到rax中,这里是pre_main2()函数的地址值,也就是说在调用完pre_main3()函数后,rax中存储的是pre_main2()函数的地址。依次类推,在执行完pre_main1()后,rax中存储的是.ctors section的开始位置的值,即0x0xffffffffffffffff,此时在执行完400679处的判断后,会接着后面的指令执行然后退出。分析到这里,不难理解为什么函数的执行顺序和在源码位置中的相反。
现在来分析.dtors section的处理。过程和前面的类似,先来看.dtors section的内容,如下所示:
[root@CentOS_190 debug]# objdump -s -j .dtors a.out

a.out: file format elf64-x86-64

Contents of section .dtors:
600890 ffffffff ffffffff 70054000 00000000 ........p.@.....
6008a0 8d054000 00000000 00000000 00000000 ..@.............
[root@CentOS_190 debug]#
接着确定函数的地址,如下所示:
[root@CentOS_190 debug]# objdump -d a.out | grep back_main*
0000000000400570 <back_main1>:
000000000040058d <back_main2>:
[root@CentOS_190 debug]#
同样我们可以得到.dtors section的结构,如下所示:

.dtors section中的内容由 __do_global_dtors_aux()函数处理,其反汇编结果如下所示:
0000000000400470 <__do_global_dtors_aux>:
400470: 55 push %rbp
400471: 48 89 e5 mov %rsp,%rbp
400474: 53 push %rbx
400475: 48 83 ec 08 sub $0x8,%rsp
400479: 80 3d 08 06 20 00 00 cmpb $0x0,0x200608(%rip) # 600a88 <completed.6349>
400480: 75 4b jne 4004cd <__do_global_dtors_aux+0x5d>
400482: bb a8 08 60 00 mov $0x6008a8,%ebx
400487: 48 8b 05 02 06 20 00 mov 0x200602(%rip),%rax # 600a90 <dtor_idx.6351>
40048e: 48 81 eb 90 08 60 00 sub $0x600890,%rbx
400495: 48 c1 fb 03 sar $0x3,%rbx
400499: 48 83 eb 01 sub $0x1,%rbx
40049d: 48 39 d8 cmp %rbx,%rax
4004a0: 73 24 jae 4004c6 <__do_global_dtors_aux+0x56>
4004a2: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
4004a8: 48 83 c0 01 add $0x1,%rax
4004ac: 48 89 05 dd 05 20 00 mov %rax,0x2005dd(%rip) # 600a90 <dtor_idx.6351>
4004b3: ff 14 c5 90 08 60 00 callq *0x600890(,%rax,8)
4004ba: 48 8b 05 cf 05 20 00 mov 0x2005cf(%rip),%rax # 600a90 <dtor_idx.6351>
4004c1: 48 39 d8 cmp %rbx,%rax
4004c4: 72 e2 jb 4004a8 <__do_global_dtors_aux+0x38>
4004c6: c6 05 bb 05 20 00 01 movb $0x1,0x2005bb(%rip) # 600a88 <completed.6349>
4004cd: 48 83 c4 08 add $0x8,%rsp
4004d1: 5b pop %rbx
4004d2: c9 leaveq
4004d3: c3 retq
4004d4: 66 66 66 2e 0f 1f 84 data32 data32 nopw %cs:0x0(%rax,%rax,1)
前面的部分我们不关心,直接从400482处的指令开始。400482处的指令将常数0x6008a8存储到ebx寄存器(相当于是rax寄存器)中,结合.dtors section的结构图,我们可以看到0x6008a8是.dtors section结束位置的地址。400487处的指令将dtor_index.6351变量(后面直接叫dtor_index)的值保存到rax寄存器中,dtor_index是在.bss section中,这个section中存放的是为初始化的全局变量和静态变量,在程序执行之前.bss section会自动清零,所以dtor_index的值为0,rax中的值也是0。40048e处的sub指令相当于是rbx=rbx-0x600890,0x600890是.dtors section的开始位置的地址值,计算的结果是0x18,rbx的值也是0x18。接着的sar指令将rbx的值右移3位,rbx中的值变为0x3,然后又使用sub指令将rbx减1,此时rbx的值变为2,也就是.dtors section中函数地址的个数。初始化操作完成之后,比较rax和rbx的值,如果rax大于等于rbx,则跳转到退出函数的位置执行。到这里已经可以明白是怎么回事了,rax就相当于是执行循环时的索引,rbx是循环的次数。4004a8处的指令将rax加1,运算后的值存储到dtor_index中,然后调用callq函数执行地址0x600890+rax*8处存储的函数。第一次循环时rax的值为1,所以计算的结果为0x600898,这个地址处存储的是back_main1()函数的地址值(参见.dtor section的结构图)。下次循环的时候,rax的值为2,计算的结果为0x6008a0,这个地址存储的是back_main2()函数的地址值。两个函数执行完之后,退出循环。
通过前面的分析,对constructor和destructor属性以及.dtors section和.ctors section都有了较深的理解和认识,下面我们结合前面了解到的内容,写一个程序,主动去调用.ctors section和.dtors section中的内容。参考的这篇文章《of ctors and dtors》 ,示例程序如下:

#include <stdio.h>
#include <stdlib.h>

static void empty(void)
{
/* empty */
}

typedef void (*fptr)(void);

static fptr ctor_list[1] __attribute((section(".ctors"))) = { (fptr)-1 };
static fptr dtor_list[1] __attribute((section(".dtors"))) = { (fptr)empty };

static int ctor_list_enable;
static int dtor_list_enable;

static void invoke_ctors(void)
{
fptr *ptr;

ctor_list_enable = 1;

for (ptr = ctor_list + 1; *ptr != NULL; ptr++) {
(**ptr)();
}
}

static void invoke_dtors(void)
{
fptr *ptr;

dtor_list_enable = 1;

for (ptr = dtor_list + 1; *ptr != NULL; ptr++) {
(**ptr)();
}
}

static __attribute((constructor)) void pre_main(void)
{
if (ctor_list_enable) {
printf("Come to %s, called by invoke_ctors\n", __func__);

} else {
printf("Come to %s\n", __func__);

}
}

static __attribute((constructor)) void pre_main2(void)
{
if (ctor_list_enable) {
printf("Come to %s, called by invoke_ctors\n", __func__);

} else {
printf("Come to %s\n", __func__);

}
}

static __attribute((destructor)) void back_main(void)
{
if (ctor_list_enable) {
printf("Come to %s, called by invoke_ctors\n", __func__);

} else {
printf("Come to %s\n", __func__);

}
}

int main(void)
{
invoke_ctors();
printf("Exiting....\n");
invoke_dtors();
exit(0);
}
这里之所以在定义ctor_list时指定的是-1,而不是其他值,是因为我们前面讲了,.ctors section中的处理是在检测到0xffffffffffffffff后停止,如果是NULL或者是其他不是合法函数的地址,则会产生段错误。定义dtor_list时没有使用-1,而是定义了一个空函数,是因为.dtors section的处理和.ctors section的处理不一样,它是先根据偏移计算出段中函数指针的个数,然后再去逐个执行每个函数,如果设置成-1,则会产生段错误。
示例程序的输出如下图所示:
[root@CentOS_190 debug]# ./a.out
Come to pre_main2
Come to pre_main
Come to pre_main, called by invoke_ctors
Come to pre_main2, called by invoke_ctors
Exiting....
Come to back_main, called by invoke_ctors
Come to back_main, called by invoke_ctors
[root@CentOS_190 debug]#

没有评论:

发表评论