只有在动态链接器解析实际加载地址之后,您才能在运行时了解这一点。
警告:接下来是更深层次的魔法......
为了说明正在发生的事情,请使用调试器:
#include <stdio.h>
int main(int argc, char **argv) { printf("Hello, World!\n"); return 0; }
编译它 (gcc -O8 ...)。二进制显示上的objdump -d(printf() 的优化被puts() 替换为无法承受的纯字符串...):
.init 部分的反汇编:
[ ... ]
.plt 部分的反汇编:
0000000000400408 __libc_start_main@plt-0x10>:
400408: ff 35 a2 04 10 00 pushq 1049762(%rip) # 5008b0 <_global_offset_table_>>
40040e: ff 25 a4 04 10 00 jmpq *1049764(%rip) # 5008b8 <_global_offset_table_>
[ ... ]
0000000000400428 :
400428: ff 25 9a 04 10 00 jmpq *1049754(%rip) # 5008c8 <_global_offset_table_>
40042e: 68 01 00 00 00 推 0x1
400433: e9 d0 ff ff ff jmpq 400408 <_init>
[ ... ]
0000000000400500 :
400500: 48 83 ec 08 sub $0x8,%rsp
400504: bf 0c 06 40 00 移动 $0x40060c,%edi
400509: e8 1a ff ff ff callq 400428
40050e: 31 c0 xor %eax,%eax
400510: 48 83 c4 08 添加 $0x8,%rsp
400514:c3 retq
现在将其加载到gdb。那么:
$ gdb ./tcc
GNU gdb 红帽 Linux (6.3.0.0-0.30.1rh)
[ ... ]
(gdb) x/3i 0x400428
0x400428: jmpq *1049754(%rip) # 0x5008c8 <_global_offset_table_>
0x40042e:pushq $0x1
0x400433:jmpq 0x400408
(gdb) x/gx 0x5008c8
0x5008c8 <_global_offset_table_>:0x000000000040042e
请注意,此值指向直接跟在第一个 jmpq 之后的指令;这意味着 puts@plt 插槽在第一次调用时将简单地“通过”到:
(gdb) x/3i 0x400408
0x400408: pushq 1049762(%rip) # 0x5008b0 <_global_offset_table_>
0x40040e: jmpq *1049764(%rip) # 0x5008b8 <_global_offset_table_>
0x400414:无
(gdb) x/gx 0x5008b0
0x5008b0 <_global_offset_table_>:0x0000000000000000
(gdb) x/gx 0x5008b8
0x5008b8 <_global_offset_table_>:0x0000000000000000
函数地址和参数尚未初始化。
这是程序加载后、执行前的状态。现在开始执行它:
(gdb) 中断主要
0x400500 处的断点 1
(gdb) 运行
启动程序:tcc
(未找到调试符号)
(未找到调试符号)
断点 1, 0x0000000000400500 in main()
(gdb) x/i 0x400428
0x400428: jmpq *1049754(%rip) # 0x5008c8 <_global_offset_table_>
(gdb) x/gx 0x5008c8
0x5008c8 <_global_offset_table_>:0x000000000040042e
所以这还没有改变 - 但是 目标(libc 初始化的 GOT 内容)现在不同了:
(gdb) x/gx 0x5008b0
0x5008b0 <_global_offset_table_>:0x0000002a9566b9a8
(gdb) x/gx 0x5008b8
0x5008b8 <_global_offset_table_>:0x0000002a955609f0
(gdb)disas 0x0000002a955609f0
函数_dl_runtime_resolve的汇编代码转储:
0x0000002a955609f0
<_dl_runtime_resolve>: sub $0x38,%rsp
[ ... ]
即在程序加载时,动态链接器将首先解析“init”部分。它将GOT 引用替换为重定向到动态链接代码的指针。
因此,当第一次通过.plt 引用调用外部二进制函数时,它会再次跳转到链接器。让它这样做,然后检查程序 - 状态再次改变:
(gdb) 中断 *0x0000000000400514
0x400514 处的断点 2
(gdb) 继续
继续。
你好世界!
断点 2, 0x0000000000400514 in main()
(gdb) x/i 0x400428
0x400428: jmpq *1049754(%rip) # 0x5008c8 <_global_offset_table_>
(gdb) x/gx 0x5008c8
0x5008c8:0x0000002a956c8870
(gdb)disas 0x0000002a956c8870
puts 函数的汇编代码转储:
0x0000002a956c8870
: mov %rbx,0xffffffffffffffe0(%rsp)
[ ... ]
因此,您现在可以直接重定向到 libc - PLT 对 puts() 的引用终于得到解决。
链接器的指令在哪里插入实际的函数加载地址(我们已经看到它对_dl_runtime_resolve 所做的操作来自 ELF 二进制文件中的特殊部分:
$ readelf -a tcc
[ ... ]
程序标题:
类型 偏移 VirtAddr PhysAddr
FileSiz MemSiz 标志对齐
[ ... ]
中断 0x00000000000000200 0x0000000000400200 0x0000000000400200
0x000000000000001c 0x000000000000001c R 1
[请求程序解释器:/lib64/ld-linux-x86-64.so.2]
[ ... ]
偏移 0x700 处的动态部分包含 21 个条目:
标签类型名称/值
0x0000000000000001(需要)共享库:[libc.so.6]
[ ... ]
偏移 0x3c0 处的重定位节“.rela.plt”包含 2 个条目:
偏移信息类型 Sym。价值符号。姓名+加号
0000005008c0 000100000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main + 0
0000005008c8 000200000007 R_X86_64_JUMP_SLO 0000000000000000 看跌期权 + 0
ELF 不仅仅是上述内容,但这三部分告诉内核的二进制格式处理程序“这个 ELF 二进制文件有一个 解释器”(它是动态链接器)需要加载 /首先初始化,它需要libc.so.6,并且程序的可写数据部分中的0x5008c0和0x5008c8的偏移量必须替换为@987654342的加载地址@和puts,分别是在实际执行动态链接步骤时。
从 ELF 的角度来看,具体如何发生取决于 解释器(也称为动态链接器实现)的细节。