Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

glibc的构造函数和析构函数.md #19

Open
chenpengcong opened this issue Aug 11, 2018 · 3 comments
Open

glibc的构造函数和析构函数.md #19

chenpengcong opened this issue Aug 11, 2018 · 3 comments

Comments

@chenpengcong
Copy link
Owner

ELF可执行程序默认入口是_start,定义在glibc/sysdeps/x86_64/start.S

_start在执行main函数前后会执行一些初始化和结尾工作,可以使用__attribute__((constructor))__attribute__((section(".init")))让函数在main函数调用之前调用,使用__attribute__((destructor))__attribute__((section(".fini")))让函数在main函数返回之后或调用exit()时调用

接下来对具体实现过程进行分析

使用__attribute__((constructor)) 声明普通函数foo

测试代码


#include <stdio.h>

void
__attribute__((constructor)) foo()
{
    printf("%s...\n", __FUNCTION__);
}


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

查看.text段汇编代码,可以看到foo函数存储在.text段


Disassembly of section .text:

. . .

0000000000000720 <foo>:

 720:    55                       push   %rbp

 721:    48 89 e5                 mov    %rsp,%rbp

 724:    48 8d 35 ec 00 00 00     lea    0xec(%rip),%rsi        # 817 <__FUNCTION__.2210>

 72b:    48 8d 3d d6 00 00 00     lea    0xd6(%rip),%rdi        # 808 <_IO_stdin_used+0x8>

 732:    b8 00 00 00 00           mov    $0x0,%eax

 737:    e8 94 fe ff ff           callq  5d0 <printf@plt>

 73c:    90                       nop

 73d:    5d                       pop    %rbp

 73e:    c3                       retq

. . .

那么foo是怎么被调用的呢,使用gdb看下调用堆栈

$ gdb a.out
(gdb) b foo
Breakpoint 1 at 0x724: file test.c, line 6.
(gdb) start
Temporary breakpoint 2 at 0x762: file test.c, line 30.
Starting program: /home/cike/a.out 

Breakpoint 1, foo () at test.c:6
6        printf("%s...\n", __FUNCTION__);
(gdb) bt
#0  foo () at test.c:6
#1  0x00005555555547cd in __libc_csu_init ()
#2  0x00007ffff7a5a240 in __libc_start_main (main=0x55555555475e <main>, argc=1, argv=0x7fffffffcd58, 
    init=0x555555554780 <__libc_csu_init>, fini=<optimized out>, rtld_fini=<optimized out>, 
    stack_end=0x7fffffffcd48) at ../csu/libc-start.c:247
#3  0x000055555555461a in _start ()

可以看到foo最终被__libc_csu_init调用

__libc_csu_init在glibc/csu/elf-init.c,其中调用foo函数为以下几行代码

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

__init_array_end__init_array_start符号是在链接控制脚本中定义的

使用$ ld -verbose查看当前使用的链接控制脚本

...
  .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 = .);
  }
...

它们的值分别是.init_array的起始地址的结束地址

查看.init_array的16进制数据

$ readelf -x .init_array a.out

Hex dump of section '.init_array':
  0x00200dc8 f0060000 00000000 20070000 00000000 ........ .......

可以看到0x2007(小端)即是函数foo的地址0x720,即.init_array存放的就是构造函数的地址值

接下来分析将函数放在.init段情况,使用__attribute__((section(".init")))声明普通函数bar,测试代码如下

void
__attribute__((section(".init"))) bar()
{
    printf("%s...\n", __FUNCTION__);
}

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

查看.init段内容,可以看到该函数存在于.init段


$ objdump -d -j .init a.out 

a.out:     file format elf64-x86-64

Disassembly of section .init:

0000000000000598 <_init>:

 598:    48 83 ec 08              sub    $0x8,%rsp

 59c:    48 8b 05 3d 0a 20 00     mov    0x200a3d(%rip),%rax        # 200fe0 <__gmon_start__>

 5a3:    48 85 c0                 test   %rax,%rax

 5a6:    74 02                    je     5aa <bar>

 5a8:    ff d0                    callq  *%rax



00000000000005aa <bar>:

 5aa:    55                       push   %rbp

 5ab:    48 89 e5                 mov    %rsp,%rbp

 5ae:    48 8d 35 86 02 00 00     lea    0x286(%rip),%rsi        # 83b <__FUNCTION__.2213>

 5b5:    48 8d 3d 6c 02 00 00     lea    0x26c(%rip),%rdi        # 828 <_IO_stdin_used+0x8>

 5bc:    b8 00 00 00 00           mov    $0x0,%eax

 5c1:    e8 2a 00 00 00           callq  5f0 <printf@plt>

 5c6:    90                       nop

 5c7:    5d                       pop    %rbp

 5c8:    c3                       retq   

 5c9:    48 83 c4 08              add    $0x8,%rsp

 5cd:    c3                       retq

使用gdb查看调用堆栈


(gdb) b _init

Breakpoint 1 at 0x598

(gdb) b bar

Breakpoint 2 at 0x5ae: file test.c, line 15.

(gdb) start

Temporary breakpoint 3 at 0x782: file test.c, line 30.

Starting program: /home/cike/a.out 

Breakpoint 1, _init (argc=1, argv=0x7fffffffcd58, envp=0x7fffffffcd68) at ../csu/init-first.c:52

52    ../csu/init-first.c: No such file or directory.

(gdb) c

Continuing.

Breakpoint 1, 0x0000555555554598 in _init ()

(gdb) bt

#0  0x0000555555554598 in _init ()

#1  0x00005555555547d1 in __libc_csu_init ()

#2  0x00007ffff7a5a240 in __libc_start_main (main=0x55555555477e <main>, argc=1, argv=0x7fffffffcd58, 

    init=0x5555555547a0 <__libc_csu_init>, fini=<optimized out>, rtld_fini=<optimized out>, 

    stack_end=0x7fffffffcd48) at ../csu/libc-start.c:247

#3  0x000055555555463a in _start ()

(gdb) s

Single stepping until exit from function _init,

which has no line number information.

bar () at test.c:14

14    {

(gdb) bt

#0  bar () at test.c:14

#1  0x00007fffffffcd58 in ?? ()

#2  0x00005555555547d1 in __libc_csu_init ()

#3  0x00007ffff7a5a240 in __libc_start_main (main=0x55555555477e <main>, argc=1, argv=0x7fffffffcd58, 

    init=0x5555555547a0 <__libc_csu_init>, fini=<optimized out>, rtld_fini=<optimized out>, 

    stack_end=0x7fffffffcd48) at ../csu/libc-start.c:247

#4  0x000055555555463a in _start ()

可以看出调用堆栈为

#0 func1() 
#1 _init()
#2 __libc_csu_init ()
#3 __libc_start_main()
#4 _start ()

上面gdb在_init打断点再使用s命令继续执行是为了证明#1 0x00007fffffffcd58 in ?? ()??_init

_init函数定义在glibc/sysdeps/x86_64/crti.S

且从__libc_csu_init代码可以看出.init段中的函数是在__init_array_start函数指针调用之前调用的

void __libc_csu_init (int argc, char **argv, char **envp)
{
...
  _init ();
  const size_t size = __init_array_end - __init_array_start;
  for (size_t i = 0; i < size; i++)
      (*__init_array_start [i]) (argc, argv, envp);
...
}

值得注意的是,将bar函数指定存放在.init段执行会导致程序崩溃

《程序员的自我修养》第11.2.3节:

普通函数放在.init是会破坏它们的结构的,因为函数的返回指令使得_init()函数会提前返回,必须使用汇编指令,不能让编译器产生ret指令

分析了__attribute__((constructor))和__attribute__((section(".init"))),其实__attribute__((destructor))和__attribute__((section(".fini")))的实现原理也类似,.fini对应.init, .fini_array对应.init_array,且这些函数都是通过__cxa_atexit函数进行注册,最终在程序调用exit()时被调用

在学习过程中还了解到.ctors和.dctors段,但是在实际编译过程却未见到这两个段,应该是跟gcc版本有关系,可参考以下链接
__do_global_ctors_aux not shown in objdump
Why does GCC put calls to constructors of global instances into different sections (depending on the target)?
Can't find .dtors and .ctors in binary

参考
<程序员自我修养>

@SharpZKing
Copy link

普通函数放在.init是会破坏它们的结构的,因为函数的返回指令使得_init()函数会提前返回,必须使用汇编指令,不能让编译器产生ret指令

你好,关于这部分如何使用汇编将函数写入init节中有简单的demo吗,比如在init节中调用c语言的一个函数

@chenpengcong
Copy link
Owner Author

普通函数放在.init是会破坏它们的结构的,因为函数的返回指令使得_init()函数会提前返回,必须使用汇编指令,不能让编译器产生ret指令

你好,关于这部分如何使用汇编将函数写入init节中有简单的demo吗,比如在init节中调用c语言的一个函数

可以在.init中添加一段调用指定C函数的汇编代码call hello_init,参考How can I use .init and .finit section of ELF shared object?这篇文章

示例代码如下

__asm__ (".section .init \n call bar \n");

void bar() {
        printf("%s...\n", __FUNCTION__);
}

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

这里解释一下:__attribute__((section(".init"))) bar()会崩溃而上述代码不会崩溃是因为上述代码使用了call指令调用bar函数而不是je,call会将函数调用返回地址入栈,所以bar函数的retq指令可以正确返回

@SharpZKing
Copy link

普通函数放在.init是会破坏它们的结构的,因为函数的返回指令使得_init()函数会提前返回,必须使用汇编指令,不能让编译器产生ret指令
你好,关于这部分如何使用汇编将函数写入init节中有简单的demo吗,比如在init节中调用c语言的一个函数

可以在.init中添加一段调用指定C函数的汇编代码call hello_init,参考How can I use .init and .finit section of ELF shared object?这篇文章

示例代码如下

__asm__ (".section .init \n call bar \n");

void bar() {
        printf("%s...\n", __FUNCTION__);
}

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

这里解释一下:__attribute__((section(".init"))) bar()会崩溃而上述代码不会崩溃是因为上述代码使用了call指令调用bar函数而不是je,call会将函数调用返回地址入栈,所以bar函数的retq指令可以正确返回

非常感谢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants