获取ELF二进制文件的加载地址,dlopen函数不按预期工作

12

我正在尝试获取一个ELF二进制文件的加载地址,但是dlopen不像预期那样工作:

void *elf = (char *)dlopen (0, RTLD_NOW);
printf ("%p\n", elf);
sleep (100);

它打印出0xb772d918,但从/proc/1510/maps中得知,它并不指向dlfn二进制文件的加载地址,而是ld-2.15.so的地址。

08048000-08049000 r-xp 00000000 fc:00 1379       /root/dlfn
08049000-0804a000 r--p 00000000 fc:00 1379       /root/dlfn
0804a000-0804b000 rw-p 00001000 fc:00 1379       /root/dlfn
b7550000-b7552000 rw-p 00000000 00:00 0 
b7552000-b76f5000 r-xp 00000000 fc:00 9275       /lib/i386-linux-gnu/libc-2.15.so
b76f5000-b76f7000 r--p 001a3000 fc:00 9275       /lib/i386-linux-gnu/libc-2.15.so
b76f7000-b76f8000 rw-p 001a5000 fc:00 9275       /lib/i386-linux-gnu/libc-2.15.so
b76f8000-b76fb000 rw-p 00000000 00:00 0 
b76fb000-b76fe000 r-xp 00000000 fc:00 9305       /lib/i386-linux-gnu/libdl-2.15.so
b76fe000-b76ff000 r--p 00002000 fc:00 9305       /lib/i386-linux-gnu/libdl-2.15.so
b76ff000-b7700000 rw-p 00003000 fc:00 9305       /lib/i386-linux-gnu/libdl-2.15.so
b7708000-b770b000 rw-p 00000000 00:00 0 
b770b000-b770c000 r-xp 00000000 00:00 0          [vdso]
b770c000-b772c000 r-xp 00000000 fc:00 9299       /lib/i386-linux-gnu/ld-2.15.so
b772c000-b772d000 r--p 0001f000 fc:00 9299       /lib/i386-linux-gnu/ld-2.15.so
b772d000-b772e000 rw-p 00020000 fc:00 9299       /lib/i386-linux-gnu/ld-2.15.so
bfc34000-bfc55000 rw-p 00000000 00:00 0          [stack]

除了解析/proc/pid/maps之外,是否有一种方法可以检索ELF二进制文件的加载地址?(在这种情况下为0x0848000)

1个回答

19
在Linux上,dlopen不会返回ELF二进制文件加载的地址。它返回struct link_map,其中有一个.l_addr成员。因此,您需要类似以下的内容:

在Linux上,dlopen不会返回ELF二进制文件加载的地址。它返回struct link_map,其中包含.l_addr成员。因此,您需要执行以下操作:

struct link_map *lm = (struct link_map*) dlopen(0, RTLD_NOW);
printf("%p\n", lm->l_addr);

然而,尽管在 /usr/include/link.h 中的注释所说的不同,.l_addr 实际上也不是一个加载地址。相反,它是 ELF 映像链接到加载的位置和实际加载位置之间的差异。

对于非 PIE 主可执行文件,这种差异总是为 0。对于未经预链接的共享库,这种差异总是加载地址(因为未经预链接的 ELF 共享库被链接以在地址 0 加载)。

那么如何找到主可执行文件的基地址?最简单的方法是使用此代码(链接到主可执行文件中):

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <link.h>
#include <stdio.h>
#include <stdlib.h>

static int
callback(struct dl_phdr_info *info, size_t size, void *data)
{
  int j;
  const char *cb = (const char *)&callback;
  const char *base = (const char *)info->dlpi_addr;
  const ElfW(Phdr) *first_load = NULL;

  for (j = 0; j < info->dlpi_phnum; j++) {
    const ElfW(Phdr) *phdr = &info->dlpi_phdr[j];

    if (phdr->p_type == PT_LOAD) {
      const char *beg = base + phdr->p_vaddr;
      const char *end = beg + phdr->p_memsz;

      if (first_load == NULL) first_load = phdr;
      if (beg <= cb && cb < end) {
        // Found PT_LOAD that "covers" callback().
        printf("ELF header is at %p, image linked at 0x%zx, relocation: 0x%zx\n",
               base + first_load->p_vaddr, first_load->p_vaddr, info->dlpi_addr);
        return 1;
      }
    }
  }
  return 0;
}

int
main(int argc, char *argv[])
{
  dl_iterate_phdr(callback, NULL);
  exit(EXIT_SUCCESS);
}

以下是在32位系统上应该看到的内容:

$ gcc -g t.c -ldl -m32  && ./a.out
ELF header is at 0x8048000, image linked at 0x8048000, relocation: 0x0
$ gcc -g t.c -ldl -m32 -pie -fPIE  && ./a.out
ELF header is at 0xf779a000, image linked at 0x0, relocation: 0xf779a000

(最后一个地址:0xf779a000 在启用地址随机化的情况下每次运行都会变化(应该启用地址随机化)。)


有趣的是...这甚至在dlopen(3)中都没有提到。谢谢! - daisy
2
非常有用。如果你想在Android上完成这个任务,从平台版本21开始,dl_iterate_phdr是可用的。 - sheltond
如果第一个PT_LOAD段不包含此函数,该怎么办? - Kelvin Hu
@KelvinHu,我已经更新了代码,使其在那种情况下也能正常工作。 - Employed Russian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接