计算函数在内存中的偏移量

Computing offset of a function in memory

我正在阅读 uprobe tracer 的文档,其中有一条说明如何计算函数在内存中的偏移量。我在这里引用它。

Following example shows how to dump the instruction pointer and %ax register at the probed text address. Probe zfree function in /bin/zsh:

# cd /sys/kernel/debug/tracing/
# cat /proc/`pgrep zsh`/maps | grep /bin/zsh | grep r-xp
00400000-0048a000 r-xp 00000000 08:03 130904 /bin/zsh
# objdump -T /bin/zsh | grep -w zfree
0000000000446420 g    DF .text  0000000000000012  Base        zfree

0x46420 is the offset of zfree in object /bin/zsh that is loaded at 0x00400000.

我不知道为什么,但他们将输出 0x446420 减去 0x400000 得到 0x46420。这对我来说是一个错误。为什么是 0x400000?

我已经尝试在我的 Fedora 23 和 4.5.6-200 内核上做同样的事情。

首先我关闭了内存地址随机化

echo 0 > /proc/sys/kernel/randomize_va_space

然后我想出了二进制文件在内存中的位置

$ cat /proc/`pgrep zsh`/maps | grep /bin/zsh | grep r-xp
555555554000-55555560f000 r-xp 00000000 fd:00 2387155                    /usr/bin/zsh

取了偏移量

marko@fedora:~ $ objdump -T /bin/zsh | grep -w zfree
000000000005dc90 g    DF .text  0000000000000012  Base        zfree

并通过 gdb 找出 zfree 的位置

$ gdb -p 21067 --batch -ex 'p zfree'
 = {<text variable, no debug info>} 0x5555555b1c90 <zfree>

marko@fedora:~ $ python
Python 2.7.11 (default, Mar 31 2016, 20:46:51) 
[GCC 5.3.1 20151207 (Red Hat 5.3.1-2)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(0x5555555b1c90-0x555555554000)
'0x5dc90'

你看,我得到了与 objdump 相同的结果,没有减去任何东西。

但后来我在另一台装有 SLES 的机器上尝试了同样的操作,结果与 uprobe 文档中的相同。

为什么会有这样的差异?那么我该如何计算正确的偏移量呢?

据我所知,差异可能仅由检查二进制文件的构建方式引起。更准确地说——ELF 是否有固定的加载地址。让我们做简单的实验。我们有简单的测试代码:

int main(void) { return 0; }

然后,通过两种方式构建它:

$ gcc -o t1 t.c      # create image with fixed load address
$ gcc -o t2 t.c -pie # create load-base independent image

现在,让我们检查这两个图像的加载基地址:

$ readelf -l --wide t1 | grep LOAD
  LOAD           0x000000 0x0000000000400000 0x0000000000400000 0x00067c 0x00067c R E 0x200000
  LOAD           0x000680 0x0000000000600680 0x0000000000600680 0x000228 0x000230 RW  0x200000
$ readelf -l --wide t2 | grep LOAD
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x0008cc 0x0008cc R E 0x200000
  LOAD           0x0008d0 0x00000000002008d0 0x00000000002008d0 0x000250 0x000258 RW  0x2000

这里可以看到第一张图片需要固定加载地址-0x400000,第二张根本没有地址要求。

现在我们可以比较 objdump 告诉 main 的地址:

$ objdump -t t1 | grep ' main'
00000000004004b6 g     F .text  000000000000000b              main
$ objdump -t t2 | grep ' main'
0000000000000710 g     F .text  000000000000000b              main

我们看到,address是一个完整的虚拟地址,main的第一个字节如果在address加载图片,会占用这个虚拟地址,存放在程序头中。当然,第二张图片永远不会加载到 0x0,而是加载到另一个随机选择的位置,这将偏移实际函数位置。