gcc x64 环境中默认链接脚本分析之 preinit_array、constructor、destructor 相关分析

preinit_array 段脚本

  .preinit_array     :
  {
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array))
    PROVIDE_HIDDEN (__preinit_array_end = .);
  }

preinit_array section 与 init_array section 功能类似,不过此 section 中的函数地址对应的函数在 init_array section 中的函数执行前执行,目前没有看到过相关的应用。

gcc constructor 与 destructor 属性

c 代码中使用 gcc attribute((constructor)) 属性修饰的函数,其函数地址将会被放到 init_array section 中,这些函数在 main 函数执行前执行,进行一些必要的初始化工作。

与之类似的的还有 destructor 属性,这个 destructor 属性修饰的函数会将函数地址放到 fini_array section 中,在 main 函数执行完成后或在 exit 函数中被调用。

也可以向 constructor 与 destructor 属性修饰的函数指定一个可选的整型优先级。对于 constructor 函数来说小数字对应高优先级,相关的函数优先执行,destructor 正相反。

destructor 一般很少使用,我在 glibc 中找到了一个示例函数,其代码如下:

209 static void
210 __attribute__ ((destructor))
211 fini (void)
212 {
213   check_free (&last_result);
214 }

map 文件中的相关信息如下:

4727  .fini_array    0x00000000004a2118        0x8 /usr/lib/gcc/x86_64-linux-gnu/8/../../../x86_64-linux-gnu/libc.a(sdlerror.o)

根据 map 文件的信息确定此 fini 函数的地址被放到了 fini_array section 中。

需要额外说明,当 attribute((constructor)) 修饰的函数被编译到动态库中时,链接相应动态库时,处理方法与这里描述的内容有所区别。

init_array 与 fini_array 段脚本

  .init_array     :
  {
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
    KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
    PROVIDE_HIDDEN (__init_array_end = .);
  }
  .fini_array     :
  {
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)))
    KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors))
    PROVIDE_HIDDEN (__fini_array_end = .);
  }

上面的脚本内容中,使用了 KEEP 命令,这是由于 finit_array section 并不会被程序直接使用,为了避免被移除需要添加 KEEP 命令。

同时 init_array 与 fini_array 都通过 PROVIDE_HIDDEN 在 section 内容前后设定了开始与结束标号的地址,这两个地址在 libc 的初始化函数中被使用,通过遍历这两个标号地址中的函数地址并执行就完成了任务。

相关代码如下:

 86   const size_t size = __init_array_end - __init_array_start;
 87   for (size_t i = 0; i < size; i++)
 88       (*__init_array_start [i]) (argc, argv, envp);

这里使用 PROVIDE_HIDDEN 方式设定一个标号有两个意义:

  1. 用户程序不能获取标号的地址(所谓的隐藏)
  2. 用户程序可以重新定义这些标号

注意这里的第二点内容。由于用户程序能够重新定义这些标号,而这些标号又在 main 函数执行前被使用,那一旦用户程序重新定义,在 glibc 中引用的这些标号将使用用户程序中定义的值,这会带来严重的问题

我用下面这个非常简单的 hello world 程序来说明这个问题,程序代码如下:

#include <stdio.h>

void __init_array_start(void)
{
	return;
}

int main(void)
{
	printf("hello world\n");

	return 0;
}

可以看到程序中定义了 __init_array_start 函数,这个函数将会覆盖链接脚本中的定义。编译并使用 gdb 执行此程序会触发段错误,打出的堆栈信息如下:

#0  0x00005555555551a1 in __libc_csu_init ()
#1  0x00007ffff7e0e02a in __libc_start_main (main=0x55555555513c <main>, argc=1, argv=0x7fffffffdab8, init=0x555555555160 <__libc_csu_init>, fini=<optimized out>, 
    rtld_fini=<optimized out>, stack_end=0x7fffffffdaa8) at ../csu/libc-start.c:264
#2  0x000055555555507a in _start ()

这里 __libc_csu_init 中就调用了上面遍历 __init_array_start 到 __init_array_end 之间的地址执行初始化函数的逻辑,这时它将 __init_array_start 函数中的指令作为函数来执行,一定会出问题欧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值