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] #
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值