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
我们当前关注点有两个:
-
0x0000000000401168 <+18>: callq 0x401050 <add@plt>是add函数的重定位 -
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 $0x0和401020: 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 总结

-
可执行程序调用外部函数会先查询plt段中函数的重定位条目在内存中的偏移
-
查询rela.plt节中外部函数调用地址的内存偏移。如果是首次调用的话,需要通过
_dl_runtime_resolve_xsavec函数查找到函数的地址,同时会把地址填入到rela.plt节所对应的内存偏移中,并跳转到外部函数开始执行。 -
如果不是首次调用,则查询到rela.plt中条目的内存偏移保存的跳转地址,然后跳转到该地址执行。

浙公网安备 33010602011771号