你只能在运行时通过动态链接器解析实际加载地址来了解这一点。
警告:接下来的内容略微深奥...
为了说明正在发生的事情,请使用调试器:
#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 将地址 1049762(%rip)(全局偏移表项 _GLOBAL_OFFSET_TABLE_+0x8)的内容压入栈中
40040e: ff 25 a4 04 10 00 跳转至地址 1049764(%rip)(全局偏移表项 _GLOBAL_OFFSET_TABLE_+0x10)的内容
[ ... ]
0000000000400428 <puts@plt>:
400428: ff 25 9a 04 10 00 跳转至地址 1049754(%rip)(全局偏移表项 _GLOBAL_OFFSET_TABLE_+0x20)的内容
40042e: 68 01 00 00 00 将常数 0x1 压入栈中
400433: e9 d0 ff ff ff 跳转至地址 400408(_init+0x18)
[ ... ]
0000000000400500 <main>:
400500: 48 83 ec 08 将栈指针向下移动 0x8 个字节
400504: bf 0c 06 40 00 将常数 0x40060c 移动到寄存器 %edi 中
400509: e8 1a ff ff ff 调用 puts 函数(地址为 0x400428)
40050e: 31 c0 将寄存器 %eax 清零
400510: 48 83 c4 08 将栈指针向上移动 0x8 个字节
400514: c3 返回
现在将其加载到gdb
中。然后:
$ gdb ./tcc
GNU gdb Red Hat Linux(6.3.0.0-0.30.1rh)
[ ... ]
(gdb) x/3i 0x400428
0x400428: jmpq *1049754(%rip) # 0x5008c8 <_GLOBAL_OFFSET_TABLE_+32>
0x40042e: pushq $0x1
0x400433: jmpq 0x400408
(gdb) x/gx 0x5008c8
0x5008c8 <_GLOBAL_OFFSET_TABLE_+32>: 0x000000000040042e
注意,此值指向第一个jmpq
直接后面的指令;这意味着在第一次调用时,puts@plt
槽位将简单地“落入”:
(gdb) x/3i 0x400408
0x400408: pushq 1049762(%rip) # 0x5008b0 <_GLOBAL_OFFSET_TABLE_+8>
0x40040e: jmpq *1049764(%rip) # 0x5008b8 <_GLOBAL_OFFSET_TABLE_+16>
0x400414: nop
(gdb) x/gx 0x5008b0
0x5008b0 <_GLOBAL_OFFSET_TABLE_+8>: 0x0000000000000000
(gdb) x/gx 0x5008b8
0x5008b8 <_GLOBAL_OFFSET_TABLE_+16>: 0x0000000000000000
函数地址和参数尚未初始化。
这是程序加载后但尚未执行的状态。现在开始执行它:
(gdb) 断点 main 函数
断点1位于 0x400500 处
(gdb) 运行
正在启动程序:tcc
(未找到调试符号)
(未找到调试符号)
在 main 函数处,断点1被触发,地址为 0x0000000000400500
(gdb) 查看 0x400428 处的汇编代码
0x400428: jmpq *1049754(%rip) # 0x5008c8 <_GLOBAL_OFFSET_TABLE_+32>
(gdb) 查看 0x5008c8 处的内容
0x5008c8 <_GLOBAL_OFFSET_TABLE_+32>: 0x000000000040042e
因此,这部分内容尚未改变,但是目标(用于 libc 初始化的 GOT 内容)现在已经不同了:
(gdb) x/gx 0x5008b0
0x5008b0 <_GLOBAL_OFFSET_TABLE_+8>: 0x0000002a9566b9a8
(gdb) x/gx 0x5008b8
0x5008b8 <_GLOBAL_OFFSET_TABLE_+16>: 0x0000002a955609f0
(gdb) disas 0x0000002a955609f0
Dump of assembler code for function _dl_runtime_resolve:
0x0000002a955609f0 <_dl_runtime_resolve+0>: sub $0x38,%rsp
[ ... ]
即在程序加载时,动态链接器将首先解析“init”部分。它将GOT引用替换为指向动态链接代码的指针。
因此,当首次通过“.plt”引用调用二进制外部函数时,它将再次跳转到链接器。让它这样做,然后检查程序-状态已经再次改变:
(gdb) break *0x0000000000400514
在地址 0x400514 处设置断点
Breakpoint 2 at 0x400514
(gdb) continue
继续执行
Hello, World!
Breakpoint 2, 0x0000000000400514 in main ()
(gdb) x/i 0x400428
查看地址 0x400428 的指令
0x400428: jmpq *1049754(%rip) # 0x5008c8 <_GLOBAL_OFFSET_TABLE_+32>
(gdb) x/gx 0x5008c8
查看地址 0x5008c8 的值
0x5008c8 : 0x0000002a956c8870
(gdb) disas 0x0000002a956c8870
反汇编函数 puts:
Dump of assembler code for function puts:
0x0000002a956c8870 <puts+0>: mov %rbx,0xffffffffffffffe0(%rsp)
[ ... ]
现在你已经直接跳转到了 libc
中 - 对于 puts()
的 PLT
引用最终得到了解析。
链接器插入实际函数加载地址的指令(我们已经看到它为 _dl_runtime_resolve
执行的操作)来自 ELF 二进制文件中的特殊部分:
$ readelf -a tcc
[ ... ]
程序头:
类型 偏移量 虚拟地址 物理地址
文件大小 内存大小 标志 对齐
[ ... ]
INTERP 0x0000000000000200 0x0000000000400200 0x0000000000400200
0x000000000000001c 0x000000000000001c R 1
[请求程序解释器:/lib64/ld-linux-x86-64.so.2]
[ ... ]
动态段偏移量为0x700,包含21个条目:
标签 类型 名称/值
0x0000000000000001 (NEEDED) 共享库: [libc.so.6]
[ ... ]
重定位段'.rela.plt'偏移量为0x3c0,包含2个条目:
偏移量 信息 类型 符号值 符号名称 + 添加值
0000005008c0 000100000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main + 0
0000005008c8 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts + 0
这段话涉及到ELF格式的内容,但其中三个部分告诉内核二进制格式处理程序:“该ELF二进制文件有一个解释器”(即动态链接器),需要首先加载/初始化它,它“需要”libc.so.6,并且在实际执行动态链接步骤时,程序的可写数据段中的偏移量0x5008c0和0x5008c8必须由__libc_start_main和puts的加载地址进行“替换”。从ELF的角度来看,这是由“解释器”(也就是动态链接器实现)的细节决定的。
ld.so
的manpage中的LD_BIND_NOW
环境变量,http://www.kernel.org/doc/man-pages/online/pages/man8/ld.so.8.html。 - FrankH.main()
函数之前调用那个函数,方法是将其调用放入构造函数中或类似的位置。很抱歉,我不知道有任何方法可以仅限于特定符号来限制LD_BIND_NOW
。 - FrankH.main()
函数之前调用该函数将仅更新程序自身的 PLT,但我需要更新任何其他动态链接或加载的库的 PLT。无论如何,谢谢。 - LuisABOL