2

我正在使用 Linux 机器,我想在运行时找出 Position-Independent-Code 共享库中符号的地址,现在我可以根据一些观察来实现,但是,我仍然对程序/库有一些疑问加载(是的,我知道怎么做,但我不知道为什么)。假设我们有以下两个 C 源文件:

// file: main.c
#include <stdio.h>

extern int global_field;
void main() {
    printf("global field(%p) = %d\n", &global_field, global_field);
}

// file: lib.c
int global_field = 1;

我们用下面的命令编译上面的代码:

gcc -fPIC -g -c lib.c -o lib.o      # note the -fPIC flag here
gcc -fPIC -g -c main.c -o main.o    # note the -fPIC flag here
gcc -shared -o lib.so lib.o
gcc -o main main.o ./lib.so

readelf -sW lib.so显示global_field符号:

Num:    Value          Size Type    Bind   Vis      Ndx Name
  ...
  8: 0000000000201028     4 OBJECT  GLOBAL DEFAULT   21 global_field
  ...

readelf -lW lib.so输出以下程序头:

...
Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x00065c 0x00065c R E 0x200000
  LOAD           0x000df8 0x0000000000200df8 0x0000000000200df8 0x000234 0x000238 RW  0x200000
  DYNAMIC        0x000e18 0x0000000000200e18 0x0000000000200e18 0x0001c0 0x0001c0 RW  0x8
  NOTE           0x000190 0x0000000000000190 0x0000000000000190 0x000024 0x000024 R   0x4
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10
  GNU_RELRO      0x000df8 0x0000000000200df8 0x0000000000200df8 0x000208 0x000208 R   0x1

现在我们运行程序,它输出以下内容:

global field(0x7ffff7dda028) = 1

cat /proc/<pid>/maps输出以下内容:

...
7ffff7bd9000-7ffff7bda000 r-xp 00000000 fd:02 18650951    /.../lib.so
7ffff7bda000-7ffff7dd9000 ---p 00001000 fd:02 18650951    /.../lib.so
7ffff7dd9000-7ffff7dda000 r--p 00000000 fd:02 18650951    /.../lib.so
7ffff7dda000-7ffff7ddb000 rw-p 00001000 fd:02 18650951    /.../lib.so
...

抱歉,这里的代码太多了......现在我的问题是:

  1. 如您所见,程序头中有两个 LOAD段,但内存映射有四个,为什么还有两个映射?

  2. 对于这两个LOAD段,如何确定哪个段映射到哪个内存区域?有没有标准或手册?

  3. 但是,根据 ELF 标准,符号global_field的值是0000000000201028(参见 的输出):readelf -sW lib.so

在可执行文件和共享对象文件中,st_value拥有一个虚拟地址。为了使这些文件的符号对运行时链接器更有用,节偏移(文件解释)让位于与节号无关的虚拟地址(内存解释)。

我知道这是与位置无关的代码,它不能是虚拟地址,并且必须是某种偏移量。global_field用符号的值减去的地址: 0x7ffff7dda028 - 0x201028 = 0x7ffff7bd9000,似乎偏移量是基于最低内存映射的起始地址(参见 的输出cat /proc/<pid>/maps)。但是,是否有任何标准告诉我们,如何以编程方式检测符号的值类型(虚拟地址或偏移量)?如果它是一个偏移量,为什么偏移量应该基于它,为什么它不基于它自己的内存区域(我猜它自己的区域是最后一个,因为它有写权限)?

4

1 回答 1

3

如您所见,程序头中有两个 LOAD 段,但内存映射有四个,为什么还有两个映射?

因为GNU_RELRO告诉动态加载器使0x208第二个段的第一个字节PT_LOAD只读。

如果您将库与 链接gcc -shared -o lib.so lib.o -Wl,-z,norelro,您将只能获得 3 个映射......这仍然留下了为什么有 3 个而不是 2 个的问题?

你会注意到这个映射:

7ffff7bda000-7ffff7dd9000 ---p 00001000 fd:02 18650951    /.../lib.so

实际上是进程空间中的一个“洞”(不允许访问)。您还会注意到,第二个PT_LOAD(实际上对于两者)的对齐非常大:0x200000.

这样做是为了适应运行 1MB 页面的可能性。

如果您再次使用 重新链接gcc -shared -o lib.so lib.o -Wl,-z,norelro,-z,max-page-size=4096,您现在将只有您期望的两个映射。

默认情况下实际发生的情况是加载程序必须保留第一个和第二个之间的偏移量PT_LOAD(否则二进制文件将无法正常工作)。所以它PT_LOAD在内核选择的地址(via)上创建了一个大的映射(覆盖两个段mmap(0, ...))。然后mprotect是从 first 结束的区域PT_LOAD,直到整个映射结束,无访问权。最后它是使用标志的所需地址mmap的第二个PT_LOAD段,MAP_FIXED在两个映射之间留下一个洞。

对于两个 LOAD 段,如何确定哪个段映射到哪个内存区域?有没有标准或手册?

你可以很容易地从偏移量中分辨出来。具有 offset 的映射0对应于 first PT_LOAD,hole 不对应于任何东西,具有 offset 的映射00001000对应于 second PT_LOAD

似乎偏移量是基于最低内存映射的起始地址

正确:它是整个lib.soELF 图像的重定位(由第一个 确定mmap(0, ...)。该重定位应用于图像中的每个符号。

但是,是否有任何标准告诉我们,如何以编程方式检测符号的值类型(虚拟地址或偏移量)?

没有标准。但是您可以使用dladdr找出“基地址”(重定位)。特别是,dli_fbase; /* Base address at which shared object is loaded */

于 2019-06-28T20:01:15.523 回答