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

时间:2013-02-25 12:13:32

标签: linux function assembly shared-libraries

在我的Linux程序中,我需要一个带有地址addr的函数,并检查放置在callq的{​​{1}}指令是否正在调用从中加载的特定函数addr共享库。我的意思是,我需要检查func处是否有类似callq func@PLT的内容。

那么,在Linux上,如何从addr指令到达函数func的实际地址?

1 个答案:

答案 0 :(得分:9)

在动态链接器解析实际加载地址后,您只能在运行时找到它。
警告:接下来是稍微深一点的魔法......

为了说明发生了什么,请使用调试器:

#include <stdio.h>

int main(int argc, char **argv) { printf("Hello, World!\n"); return 0; }

编译它(gcc -O8 ...)。二进制显示objdump -dprintf()的优化被puts()替换为无法承受的普通字符串...):

Disassembly of section .init:
[ ... ]
Disassembly of section .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          pushq  $0x1
  400433:  e9 d0 ff ff ff          jmpq   400408 <_init+0x18>
[ ... ]
0000000000400500 <main>:
  400500:  48 83 ec 08             sub    $0x8,%rsp
  400504:  bf 0c 06 40 00          mov    $0x40060c,%edi
  400509:  e8 1a ff ff ff          callq  400428 <puts@plt>
  40050e:  31 c0                   xor    %eax,%eax
  400510:  48 83 c4 08             add    $0x8,%rsp
  400514:  c3                      retq

现在将其加载到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插槽将简单地通过&#34;到:

(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) break main
Breakpoint 1 at 0x400500
(gdb) run
Starting program: tcc
(no debugging symbols found)
(no debugging symbols found)

Breakpoint 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
Dump of assembler code for function _dl_runtime_resolve:
0x0000002a955609f0 <_dl_runtime_resolve+0>:     sub    $0x38,%rsp
[ ... ]

即。在程序加载时,动态链接器将解析&#34; init&#34;零件首先。它将GOT引用替换为重定向到动态链接代码的指针。

因此,当首次通过.plt引用调用外部到二进制函数时,它会再次跳转到链接器。让它做到这一点,然后检查程序 - 状态再次改变:

(gdb) break *0x0000000000400514
Breakpoint 2 at 0x400514
(gdb) continue
Continuing.
Hello, World!

Breakpoint 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
Dump of assembler code for function puts:
0x0000002a956c8870 <puts+0>:    mov    %rbx,0xffffffffffffffe0(%rsp)
[ ... ]

因此,您现在重定向到libc - PLTputs()的引用最终得到解决。

链接器的指令插入实际的函数加载地址(我们已经看到它为_dl_runtime_resolve做的指令来自ELF二进制文件中的特殊部分:

$ readelf -a tcc
[ ... ]
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
[ ... ]
  INTERP         0x0000000000000200 0x0000000000400200 0x0000000000400200
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
[ ... ]
Dynamic section at offset 0x700 contains 21 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
[ ... ]
Relocation section '.rela.plt' at offset 0x3c0 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
0000005008c0  000100000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main + 0
0000005008c8  000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts + 0

ELF比上面更多,但是这三个部分告诉内核的二进制格式处理程序&#34;这个ELF二进制文件有一个解释器&#34 ; (这是动态链接器)需要先加载/初始化,需要 libc.so.6,并且在程序中抵消0x5008c00x5008c8&# 39;当实际执行动态链接的步骤时,可写数据部分必须分别由__libc_start_mainputs的加载地址替换

从ELF的角度来看,究竟是怎么发生的,取决于解释器(又名动态链接器实现)的细节。