5

在我的 Linux 程序中,我需要一个函数来获取地址addr并检查callq放置的指令是否addr正在调用func从共享库加载的特定函数。我的意思是,我需要检查我是否有类似callq func@PLTat的东西addr

func那么,在 Linux 上,如何从callq func@PLT指令中获取函数的真实地址呢?

4

1 回答 1

11

只有在动态链接器解析实际加载地址之后,您才能在运行时找到相关信息。
警告:接下来是更深层次的魔法......

为了说明正在发生的事情,请使用调试器:

#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_+0x8> >
  40040e: ff 25 a4 04 10 00 jmpq *1049764(%rip) # 5008b8 <_GLOBAL_OFFSET_TABLE_+0x10>
[ ... ]
0000000000400428 <puts@plt>:
  400428: ff 25 9a 04 10 00 jmpq *1049754(%rip) # 5008c8 <_GLOBAL_OFFSET_TABLE_+0x20>
  40042e: 68 01 00 00 00 推 0x1
  400433: e9 d0 ff ff ff jmpq    400408  <_init+0x18>
[ ... ]
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 <puts@plt>
  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_+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:无
(gdb) x/gx 0x5008b0
0x5008b0 <_GLOBAL_OFFSET_TABLE_+8>:0x0000000000000000
(gdb) x/gx 0x5008b8
0x5008b8 <_GLOBAL_OFFSET_TABLE_+16>:0x0000000000000000

函数地址和参数尚未初始化。
这是程序加载后、执行前的状态。现在开始执行它:

(gdb) 中断主要
0x400500 处的断点 1
(gdb) 运行
启动程序:tcc
(未找到调试符号)
(未找到调试符号)

断点 1, 0x0000000000400500 in main()
(gdb) x/i 0x400428
0x400428: jmpq *1049754(%rip) # 0x5008c8 <_GLOBAL_OFFSET_TABLE_+32>
(gdb) x/gx 0x5008c8
0x5008c8 <_GLOBAL_OFFSET_TABLE_+32>:0x000000000040042e

所以这还没有改变-目标(初始化的GOT内容libc)现在不同了:

(gdb) x/gx 0x5008b0
0x5008b0 <_GLOBAL_OFFSET_TABLE_+8>:0x0000002a9566b9a8
(gdb) x/gx 0x5008b8
0x5008b8 <_GLOBAL_OFFSET_TABLE_+16>:0x0000002a955609f0
(gdb)disas 0x0000002a955609f0
函数_dl_runtime_resolve的汇编代码转储:
0x0000002a955609f0 <_dl_runtime_resolve+0> : 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_+32>
(gdb) x/gx 0x5008c8
0x5008c8:0x0000002a956c8870
(gdb)disas 0x0000002a956c8870函数puts
的汇编代码转储:
0x0000002a956c8870 <puts+0>: 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的可写数据部分中的 和 必须分别替换__libc_start_main和的加载地址。puts

从 ELF 的角度来看,这究竟是如何发生的,取决于解释器的细节(也就是动态链接器的实现)。

于 2013-02-26T10:52:06.480 回答