GDB调试延迟绑定流程

在程序加载so动态库时,如果不是延迟绑定,那么在加载动态库时,操作系统的链接器会重定位库中的全局变量、全局函数等,势必会影响程序的启动速度。有没有一种方式能够延迟变量或者函数的重定位呢?这时候延迟绑定隆重登场!!延迟绑定实际上就是如果没有用到某个变量或者函数,会在第一次引用变量或者调用函数时,再进行重定位的工作。后续再引用变量或者调用函数时,就不需要再重定位。

对于这个流程设计也是十分巧妙的,通过GDB单步调试,对于理解这个过程起到事半功倍的作用。这个过程其实也很简单,不需要死记硬背,后续通过阅读反汇编代码就能够游刃有余地叙述整个过程。

首先新建一个源文件ex_module.c,编译成动态库libex_module.so

int res;

int add(int num1, int num2)
{
    return num1 + num2;
}
gcc ex_module.c  -fPIC -shared -o libex_module.so

再创建一个源文件,并调用libex_module.so的函数和变量:

#include<stdio.h>
extern int res;
extern int add(int, int);

int main(void)
{
    res = add(10, 20);
    printf("Hello world, res is %d!\n", res);
    printf("Hello world again!");
    return 0;
}
gcc test.c -L./ -lex_module -g -o test -no-pie

2 使用GDB调试,分析延迟绑定的流程

使用readelf -r test查看重定位节的条目。可以看到.rela.dyn实际上是关于变量的重定位节,而.rela.plt是函数的重定位节,两者是分开的。

重定位节 '.rela.dyn' at offset 0x4e0 contains 3 entries:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000403ff0  000300000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000403ff8  000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000404038  000500000005 R_X86_64_COPY     0000000000404038 res + 0

重定位节 '.rela.plt' at offset 0x528 contains 2 entries:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000404018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 add + 0
000000404020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0

2.1 使用GDB开始调试

使用下面命令启动GDB:

gdb-multiarch ./test

先反汇编查看下代码,看下可以在哪里打断点:

(gdb) disassemble main
Dump of assembler code for function main:
   0x0000000000401156 <+0>:     endbr64 
   0x000000000040115a <+4>:     push   %rbp
   0x000000000040115b <+5>:     mov    %rsp,%rbp
   0x000000000040115e <+8>:     mov    $0x14,%esi
   0x0000000000401163 <+13>:    mov    $0xa,%edi
   0x0000000000401168 <+18>:    callq  0x401050 <add@plt>
   0x000000000040116d <+23>:    mov    %eax,0x2ec5(%rip)        # 0x404038 <res>
   0x0000000000401173 <+29>:    mov    0x2ebf(%rip),%eax        # 0x404038 <res>
   0x0000000000401179 <+35>:    mov    %eax,%esi
   0x000000000040117b <+37>:    lea    0xe82(%rip),%rdi        # 0x402004
   0x0000000000401182 <+44>:    mov    $0x0,%eax
   0x0000000000401187 <+49>:    callq  0x401060 <printf@plt>
   0x000000000040118c <+54>:    lea    0xe8a(%rip),%rdi        # 0x40201d
   0x0000000000401193 <+61>:    mov    $0x0,%eax
   0x0000000000401198 <+66>:    callq  0x401060 <printf@plt>
   0x000000000040119d <+71>:    mov    $0x0,%eax
   0x00000000004011a2 <+76>:    pop    %rbp
   0x00000000004011a3 <+77>:    retq  

我们当前关注点有两个:

  1. 0x0000000000401168 <+18>: callq 0x401050 <add@plt>是add函数的重定位

  2. 0x000000000040116d <+23>: mov %eax,0x2ec5(%rip) # 0x404038 <res>是res变量的重定位

我们把断点设置在这两个地方:

(gdb) b *0x0000000000401168
Breakpoint 1 at 0x401168: file test.c, line 7.
(gdb) b *0x000000000040116d
Breakpoint 2 at 0x40116d: file test.c, line 7.

接下来使用run命令,使程序运行到第一个断点处:

(gdb) r
Starting program: /home/cambricon/code/OS/relocation_test/test 

Breakpoint 1, 0x0000000000401168 in main () at test.c:7
7           res = add(10, 20);

2.1.1 跳转到rela.plt查询内存地址

0x0000000000401168处的代码是要跳转到0x401050处,但是这个是什么段的内存地址呢?我们可以使用objdump -d test查看下程序的反汇编(这里我只展示和延迟绑定相关的section):

Disassembly of section .plt:

0000000000401020 <.plt>:
  401020:       ff 35 e2 2f 00 00       pushq  0x2fe2(%rip)        # 404008 <_GLOBAL_OFFSET_TABLE_+0x8>
  401026:       f2 ff 25 e3 2f 00 00    bnd jmpq *0x2fe3(%rip)        # 404010 <_GLOBAL_OFFSET_TABLE_+0x10>
  40102d:       0f 1f 00                nopl   (%rax)
  401030:       f3 0f 1e fa             endbr64 
  401034:       68 00 00 00 00          pushq  $0x0
  401039:       f2 e9 e1 ff ff ff       bnd jmpq 401020 <.plt>
  40103f:       90                      nop
  401040:       f3 0f 1e fa             endbr64 
  401044:       68 01 00 00 00          pushq  $0x1
  401049:       f2 e9 d1 ff ff ff       bnd jmpq 401020 <.plt>
  40104f:       90                      nop

Disassembly of section .plt.sec:

0000000000401050 <add@plt>:
  401050:       f3 0f 1e fa             endbr64 
  401054:       f2 ff 25 bd 2f 00 00    bnd jmpq *0x2fbd(%rip)        # 404018 <add>
  40105b:       0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

0000000000401060 <printf@plt>:
  401060:       f3 0f 1e fa             endbr64 
  401064:       f2 ff 25 b5 2f 00 00    bnd jmpq *0x2fb5(%rip)        # 404020 <printf@GLIBC_2.2.5>
  40106b:       0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

跳转到0x401050开始执行后,紧接着会执行jmpq *0x2fbd(%rip)跳转到0x404018所指向的内存地址处。而这个内存地址是最开始使用readelf -r test查询到的rela.plt的内存地址:

  偏移量          信息           类型           符号值        符号名称 + 加数
000000404018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 add + 0

注意:jmpq *0x2fbd(%rip)表示的是跳转到0x2fbd(%rip)所指向的内存地址

再使用下面命令查询所指向的内存地址:

(gdb) p /x *0x00000404018
$1 = 0x401030

执行单步执行命令,会跳转到0x401030内存所指向的指令执行:

(gdb) si
0x0000000000401050 in add@plt ()
(gdb) si
0x0000000000401054 in add@plt ()
(gdb) si
0x0000000000401030 in ?? ()
(gdb) 

2.1.2 重定位获取add函数的内存地址

0x0401030地址所对应的指令实际上可以在前面的objdump查询到指令:

0000000000401020 <.plt>:
  401020:       ff 35 e2 2f 00 00       pushq  0x2fe2(%rip)        # 404008 <_GLOBAL_OFFSET_TABLE_+0x8>
  401026:       f2 ff 25 e3 2f 00 00    bnd jmpq *0x2fe3(%rip)        # 404010 <_GLOBAL_OFFSET_TABLE_+0x10>
  40102d:       0f 1f 00                nopl   (%rax)
  401030:       f3 0f 1e fa             endbr64 
  401034:       68 00 00 00 00          pushq  $0x0
  401039:       f2 e9 e1 ff ff ff       bnd jmpq 401020 <.plt>
  40103f:       90                      nop
  401040:       f3 0f 1e fa             endbr64 
  401044:       68 01 00 00 00          pushq  $0x1
  401049:       f2 e9 d1 ff ff ff       bnd jmpq 401020 <.plt>
  40104f:       90                      nop

重定位会调用_dl_runtime_resolve_xsavec函数进行计算,

void _dl_runtime_resolve_xsavec(link_map *l, Elf64_Word reloc_index);

0x0401030内存处的指令实际上就是把函数的两个参数入栈,然后跳转到0x404010所指向的内存地址:

(gdb) p /x *0x404010
$1 = 0xf7fe7bc0
(gdb) si
_dl_runtime_resolve_xsavec () at ../sysdeps/x86_64/dl-trampoline.h:67
67      ../sysdeps/x86_64/dl-trampoline.h: 没有那个文件或目录.
(gdb) 

ps:是通过401034: 68 00 00 00 00 pushq $0x0401020: ff 35 e2 2f 00 00 pushq 0x2fe2(%rip)传入参数的。

通过下面命令查询该函数的实现:

(gdb) disassemble *$pc
Dump of assembler code for function _dl_runtime_resolve_xsavec:
   0x00007ffff7fe7bc0 <+0>:     endbr64 
   0x00007ffff7fe7bc4 <+4>:     push   %rbx
   0x00007ffff7fe7bc5 <+5>:     mov    %rsp,%rbx
   0x00007ffff7fe7bc8 <+8>:     and    $0xffffffffffffffc0,%rsp
=> 0x00007ffff7fe7bcc <+12>:    sub    0x14b35(%rip),%rsp        # 0x7ffff7ffc708 <_rtld_global_ro+232>
   0x00007ffff7fe7bd3 <+19>:    mov    %rax,(%rsp)
   0x00007ffff7fe7bd7 <+23>:    mov    %rcx,0x8(%rsp)
   0x00007ffff7fe7bdc <+28>:    mov    %rdx,0x10(%rsp)
   0x00007ffff7fe7be1 <+33>:    mov    %rsi,0x18(%rsp)
   0x00007ffff7fe7be6 <+38>:    mov    %rdi,0x20(%rsp)
   0x00007ffff7fe7beb <+43>:    mov    %r8,0x28(%rsp)
   0x00007ffff7fe7bf0 <+48>:    mov    %r9,0x30(%rsp)
   0x00007ffff7fe7bf5 <+53>:    mov    $0xee,%eax
   0x00007ffff7fe7bfa <+58>:    xor    %edx,%edx
   0x00007ffff7fe7bfc <+60>:    mov    %rdx,0x250(%rsp)
   0x00007ffff7fe7c04 <+68>:    mov    %rdx,0x258(%rsp)
   0x00007ffff7fe7c0c <+76>:    mov    %rdx,0x260(%rsp)
   0x00007ffff7fe7c14 <+84>:    mov    %rdx,0x268(%rsp)
   0x00007ffff7fe7c1c <+92>:    mov    %rdx,0x270(%rsp)
   0x00007ffff7fe7c24 <+100>:   mov    %rdx,0x278(%rsp)
   0x00007ffff7fe7c2c <+108>:   xsavec 0x40(%rsp)
   0x00007ffff7fe7c31 <+113>:   mov    0x10(%rbx),%rsi
   0x00007ffff7fe7c35 <+117>:   mov    0x8(%rbx),%rdi
   0x00007ffff7fe7c39 <+121>:   callq  0x7ffff7fe00c0 <_dl_fixup>
   0x00007ffff7fe7c3e <+126>:   mov    %rax,%r11
   0x00007ffff7fe7c41 <+129>:   mov    $0xee,%eax
--Type <RET> for more, q to quit, c to continue without paging--
   0x00007ffff7fe7c46 <+134>:   xor    %edx,%edx
   0x00007ffff7fe7c48 <+136>:   xrstor 0x40(%rsp)
   0x00007ffff7fe7c4d <+141>:   mov    0x30(%rsp),%r9
   0x00007ffff7fe7c52 <+146>:   mov    0x28(%rsp),%r8
   0x00007ffff7fe7c57 <+151>:   mov    0x20(%rsp),%rdi
   0x00007ffff7fe7c5c <+156>:   mov    0x18(%rsp),%rsi
   0x00007ffff7fe7c61 <+161>:   mov    0x10(%rsp),%rdx
   0x00007ffff7fe7c66 <+166>:   mov    0x8(%rsp),%rcx
   0x00007ffff7fe7c6b <+171>:   mov    (%rsp),%rax
   0x00007ffff7fe7c6f <+175>:   mov    %rbx,%rsp
   0x00007ffff7fe7c72 <+178>:   mov    (%rsp),%rbx
   0x00007ffff7fe7c76 <+182>:   add    $0x18,%rsp
   0x00007ffff7fe7c7a <+186>:   bnd jmpq *%r11
End of assembler dump.

函数结尾会0x00007ffff7fe7c7a <+186>: bnd jmpq *%r11跳转到寄存器r11所指向的内存地址,可以在此处设下断点

(gdb) b *0x00007ffff7fe7c7a
Breakpoint 3 at 0x7ffff7fe7c7a: file ../sysdeps/x86_64/dl-trampoline.h, line 153.
(gdb) c
Continuing.

Breakpoint 3, _dl_runtime_resolve_xsavec ()
    at ../sysdeps/x86_64/dl-trampoline.h:153
153     in ../sysdeps/x86_64/dl-trampoline.h

触发到断点后我们再查看下寄存器r11的内容,以及该内容所对应的内存地址是什么:

(gdb) p /x $r11
$2 = 0x7ffff7fc50f9
(gdb) si
0x00007ffff7fc50f9 in add () from ./libex_module.so

这里可以看到_dl_runtime_resolve_xsavec函数会获取到add函数的地址,并且跳转执行。那么会有个疑问:这个函数是不是也会把add函数的地址存放到rela.plt section当中呢?

(gdb) p /x *0x00000404018
$4 = 0xf7fc50f9
(gdb) 

通过查询确实会把add的函数地址回填到rela.plt section。

2.1.3 全局变量重定位

全局变量的重定位相比较于函数的延迟绑定流程简单很多。直接访问rela.dyn中变量在内存中的偏移,然后把对应值写入到该内存中即可。

   0x0000000000401168 <+18>:    callq  0x401050 <add@plt>
=> 0x000000000040116d <+23>:    mov    %eax,0x2ec5(%rip)        # 0x404038 <res>

根据上面反汇编的结果,在调用完add函数,得到结果存放在eax寄存器中,然后这里会把eax的值写入到内存地址。

3 总结

image

  1. 可执行程序调用外部函数会先查询plt段中函数的重定位条目在内存中的偏移

  2. 查询rela.plt节中外部函数调用地址的内存偏移。如果是首次调用的话,需要通过_dl_runtime_resolve_xsavec函数查找到函数的地址,同时会把地址填入到rela.plt节所对应的内存偏移中,并跳转到外部函数开始执行

  3. 如果不是首次调用,则查询到rela.plt中条目的内存偏移保存的跳转地址,然后跳转到该地址执行。

4 reference

  1. 深入理解-dl_runtime_resolve - unr4v31 - 博客园

  2. 2024-巅峰极客 easyblind 分析 (1) | CoLin's BLOG

  3. 深入浅出 PLT/GOT Hook与原理实践 - zxzhang - 博客园

  4. GOT&PLT 延迟绑定 | Mask's Blog

posted @ 2025-01-23 17:53  cockpunctual  阅读(124)  评论(0)    收藏  举报