如何从`callq func@PLT`获取`func`的实际地址

5
在我的Linux程序中,我需要一个函数来获取地址addr并检查位于addrcallq指令是否调用了从共享库加载的特定函数func。我的意思是,我需要检查是否有像callq func@PLT这样的内容在addr处。
那么,在Linux上,如何从callq func@PLT指令中找到函数func的真实地址呢?
1个回答

12
你只能在运行时通过动态链接器解析实际加载地址来了解这一点。
警告:接下来的内容略微深奥...
为了说明正在发生的事情,请使用调试器:
#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的角度来看,这是由“解释器”(也就是动态链接器实现)的细节决定的。

非常感谢这个完整的示例,非常清晰。但我有另一个问题:是否有任何方法可以在程序加载时解析特定函数?我的意思是,您是否知道任何gcc属性可以使函数不被惰性解析?因为如果我真的明白了,必须首次调用函数才能解析其地址。然而,在我的程序中,我需要知道当前函数之后是否将执行特定函数“func”。但是,“func”可能以前没有被调用过,因此其在PLT上的引用可能尚未解析。 - LuisABOL
2
是的,有的;我在上一个段落中没有提到链接器实现细节...但是它已经被记录了,请参见ld.so的manpage中的LD_BIND_NOW环境变量,http://www.kernel.org/doc/man-pages/online/pages/man8/ld.so.8.html。 - FrankH.
“LD_BIND_NOW” 看起来可以满足我的需求,但它会解析所有符号,而我只需要解析一个。因此,“LD_BIND_NOW” 可能会导致不必要的开销。你知道有没有办法在加载时仅绑定特定函数? - LuisABOL
我会“欺骗”程序在main()函数之前调用那个函数,方法是将其调用放入构造函数中或类似的位置。很抱歉,我不知道有任何方法可以仅限于特定符号来限制LD_BIND_NOW - FrankH.
真遗憾!在调用 main() 函数之前调用该函数将仅更新程序自身的 PLT,但我需要更新任何其他动态链接或加载的库的 PLT。无论如何,谢谢。 - LuisABOL

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接