在 ARM 上获取错误的 glibc 函数地址

Getting wrong glibc function address on ARM

我想获取一个函数的地址。使用函数名称在 x86 上获取正确的地址,包括本地函数和 glibc 函数。

但是在ARM上,局部函数地址是正确的,而glibc函数地址是错误的。

这是我的简单程序:

#include <stdio.h>
int sum(int a, int b)
{
    return a + b;
}
int main(int argc, char *argv[])
{
    char buffer[32] = { '[=10=]' };
    sprintf(buffer, "cat /proc/%d/maps", getpid());
    printf("sum = %p\n", sum);
    printf("fopen = %p\n", fopen);
    system(buffer);
    return 0;
}

# x-compile it to an ARM executable:
$ arm-linux-gnueabihf-4.9.1-gcc -g -o misc misc.c

# debug on ARM
/home # ./gdb ./misc
GNU gdb (GDB) 7.5.1
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "arm-linux".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/misc...done.
(gdb) b 16
Breakpoint 1 at 0x8534: file misc.c, line 16.
(gdb) r
Starting program: /home/misc 
sum = 0x8491
fopen = 0x835c
00008000-00009000 r-xp 00000000 00:13 1703976    /home/misc
00010000-00011000 rw-p 00000000 00:13 1703976    /home/misc
76ed9000-76fd0000 r-xp 00000000 1f:08 217        /lib/libc-2.19-2014.06.so
76fd0000-76fd7000 ---p 000f7000 1f:08 217        /lib/libc-2.19-2014.06.so
76fd7000-76fd9000 r--p 000f6000 1f:08 217        /lib/libc-2.19-2014.06.so
76fd9000-76fda000 rw-p 000f8000 1f:08 217        /lib/libc-2.19-2014.06.so
76fda000-76fdd000 rw-p 00000000 00:00 0 
76fdd000-76ff7000 r-xp 00000000 1f:08 199        /lib/ld-2.19-2014.06.so
76ffb000-76ffe000 rw-p 00000000 00:00 0 
76ffe000-76fff000 r--p 00019000 1f:08 199        /lib/ld-2.19-2014.06.so
76fff000-77000000 rw-p 0001a000 1f:08 199        /lib/ld-2.19-2014.06.so
7efdf000-7f000000 rw-p 00000000 00:00 0          [stack]
ffff0000-ffff1000 r-xp 00000000 00:00 0          [vectors]

Breakpoint 1, main (argc=1, argv=0x7efffe64) at misc.c:16
16      misc.c: No such file or directory.
(gdb) p fopen
 = {<text variable, no debug info>} 0x76f26a50 <fopen>
(gdb) 

注意 glibc 文本段被映射到地址 76ed9000,所以 fopen 怎么会在像 0x835c 这样的有线地址?

然而,下一行 (gdb) p fopen,gdb 给出了正确的地址。

无法保证指针的值确实为您提供了您要查找的内容的 in-memory 地址。对于函数指针,您实际上更有可能拥有完全不同的值。

下面完全是过头了,删了这个解释就可惜了。所以这是一个简短的版本:函数指针只保证与另一个函数指针的比较将比较相等,当涉及共享库时,这会很快变得复杂。

这里发生的事情与动态链接有关。当您链接程序时,链接器不知道 libc 在内存中的位置,这只能由动态链接器在 运行 时解决。一种天真的方法是只重写程序代码中的函数地址,但这是低效的,因为这意味着程序的每次执行都无法共享 executable 与其他 运行s。取而代之的是一种叫做 PLT 的东西。当您对动态链接函数进行函数调用时,运行s 的实际代码是跳转到程序中的本地函数,然后从 table 加载函数的实际地址并跳转到那(这在不同的体系结构上非常不同,但这是一般的想法)。

我在 amd64 上构建了你的程序,让我们看看它的实际效果:

(gdb) break system
Breakpoint 1 at 0x400510
(gdb) run
Starting program: /home/art/./foo
sum = 0x400660
fopen = 0x400550
[...]

因此,正如您在 amd64 上看到的那样,fopen 的值也很可疑。让我们看看那个地址潜伏着什么代码:

(gdb) x/i 0x400550
   0x400550 <fopen@plt>:    jmpq   *0x200aea(%rip)        # 0x601040 <fopen@got.plt>

我们可以注意到的第一件事是 gdb 已经知道这实际上不是 fopen,但内存中的这个特定位置称为 fopen@plt。它只是一条指令:跳转到指令指针加上 0x200aea 处的指针值(linux/amd64 几乎完成所有相对于指令指针的寻址),gdb 很好地告诉我们地址 0x601040 并且恰好被命名为 fopen@got.plt。 GOT 代表 Global Offset Table,PLT 代表 Procedure Linkage Table.

让我们进入兔子洞:

(gdb) x/g 0x601040
0x601040 <fopen@got.plt>:   0x0000000000400556
(gdb) x/i 0x0000000000400556
   0x400556 <fopen@plt+6>:  pushq  [=12=]x5
(gdb)
   0x40055b <fopen@plt+11>: jmpq   0x4004f0
(gdb) x/i 0x4004f0
   0x4004f0:    pushq  0x200b12(%rip)        # 0x601008
(gdb)
   0x4004f6:    jmpq   *0x200b14(%rip)        # 0x601010
(gdb) x/g 0x601010
0x601010:   0x00007ffff7df0290
(gdb) x/i 0x00007ffff7df0290
   0x7ffff7df0290 <_dl_runtime_resolve>:    sub    [=12=]x78,%rsp
(gdb)

这里发生了一些奇怪的事情。 fopen@got.plt 中的地址只是返回到 fopen@plt 之后的一条指令,然后将某些内容压入堆栈并跳转到其他代码,这些代码将更多内容压入堆栈并跳转以从 [= 获取另一个奇怪的地址=45=] 最终我们得到 _dl_runtime_resolve。发生的事情是惰性绑定。动态链接器的开发人员发现,动态库和程序包含的大部分链接信息将永远不会被使用。当您 运行 您的程序从 libc 调用两个函数时,您不想解析 libc 内部执行的所有成千上万个动态函数调用,这是浪费时间。此外,对于大多数程序,我们重视快速启动而不是快速 运行 时间。所以默认情况下你所有的功能都没有真正解决。他们会在 运行 时间解决,您第一次打电话给他们。这就是 _dl_runtime_resolve 所做的。向堆栈的推送很可能是 non-standard 向该函数传递参数的方式,因为不允许此代码使用任何寄存器(调用代码认为它只是正常调用 fopen)。

但等一下。 C 标准规定,如果两个函数指针指向同一个函数,则它们应该比较相等。如果其中一个指针可能来自您的程序而另一个指针来自动态库,那将如何工作?好吧,这在很大程度上取决于体系结构,但经过一些挖掘后,我发现在我的体系结构中,即使库 returns 是一个函数指针,该函数指针也会被转换为我主程序中的 PLT 函数。为什么?不知道。有人决定在某个时候以这种方式实施它。