【问题标题】:how to get the actual address of `func` from `callq func@PLT`如何从`callq func@PLT`中获取`func`的实际地址
【发布时间】:2013-02-25 12:13:32
【问题描述】:

在我的 Linux 程序中,我需要一个函数,它接受地址 addr 并检查位于 addrcallq 指令是否正在调用从共享库加载的特定函数 func。我的意思是,我需要检查在addr 上是否有类似callq func@PLT 的信息。

那么,在 Linux 上,如何通过 callq func@PLT 指令到达函数 func 的真实地址?

【问题讨论】:

    标签: linux function assembly shared-libraries


    【解决方案1】:

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

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

    #include <stdio.h>
    
    int main(int argc, char **argv) { printf("Hello, World!\n"); return 0; }
    

    编译它 (gcc -O8 ...)。二进制显示上的objdump -dprintf() 的优化被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 - PLTputs() 的引用终于得到解决。

    链接器的指令在哪里插入实际的函数加载地址(我们已经看到它对_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,并且程序的可写数据部分中的0x5008c00x5008c8的偏移量必须替换为@987654342的加载地址@和puts,分别是在实际执行动态链接步骤时。

    从 ELF 的角度来看,具体如何发生取决于 解释器(也称为动态链接器实现)的细节。

    【讨论】:

    • 非常感谢您提供这个完整的示例。很清楚,但我还有一个问题:有没有办法在程序加载时解析特定函数,我的意思是你知道任何 gcc 属性来使函数非延迟解析吗?因为,如果我真的明白了,必须第一次调用一个函数来解析它的地址。但是,在我的程序中,我需要知道特定函数func 是否会在当前函数之后执行。但是,func之前可能没有被调用过,因此它在 PLT 的引用可能还没有被解析。
    • 是的,有;我在最后一段中省略了链接器实现细节......但它已记录在案,请参阅手册页中的 LD_BIND_NOW 环境变量 ld.sokernel.org/doc/man-pages/online/pages/man8/ld.so.8.html
    • LD_BIND_NOW 似乎可以满足我的需要,但它可以解析所有符号,而我只需要解析一个。所以LD_BIND_NOW 可能会导致不必要的开销。您知道在加载时仅绑定特定功能的任何方法吗?
    • 我会“诱骗”程序调用该函数一次before main(),方法是将对该函数的调用放入构造函数部分或类似的部分。不知道有任何方法将LD_BIND_NOW 仅限于特定符号,抱歉:(
    • 真可怜!在main() 之前调用该函数只会更新程序自己的PLT,但我也需要更新任何其他动态链接或加载的库的PLT。无论如何,谢谢。
    猜你喜欢
    • 1970-01-01
    • 2016-11-13
    • 1970-01-01
    • 1970-01-01
    • 2010-10-27
    • 1970-01-01
    • 1970-01-01
    • 2021-03-23
    • 1970-01-01
    相关资源
    最近更新 更多