linux - 使用 -fPIC 编译的共享库的内存映射和变量位置

标签 linux gcc shared-libraries elf position-independent-code

我正在使用Linux机器,我想在运行时找出位置无关代码共享库中符号的地址,现在根据一些观察我可以实现这一点,但是,我仍然有一些关于程序/库加载(是的,我知道如何加载,但我不知道为什么)。假设我们有以下两个 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. 符号global_field的值为0000000000201028 (参见 readelf -sW lib.so 的输出),但是,根据 ELF 标准:

In executable and shared object files, st_value holds a virtual address. To make these files' symbols more useful for the runtime linker, the section offset (file interpretation) gives way to a virtual address (memory interpretation) for which the section number is irrelevant.

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

最佳答案

As you see, there are TWO LOAD segments in program headers, but there are FOUR memory mappings, why there are two more mappings?

因为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 )创建一个大映射(覆盖 mmap(0, ...) 段)。然后mprotect s 从第一个 PT_LOAD 末尾开始的区域,直到整个映射结束且无法访问。最后是mmap是第二个PT_LOAD使用MAP_FIXED在所需地址进行段标志,在两个映射之间留下一个洞。

For the two LOAD segments, how to figure out which segment maps to which memory region? Is there any standard or any manual?

您可以很容易地从偏移量中看出。偏移量 0 的映射对应第一个PT_LOAD ,这个洞不对应任何东西,并且偏移量为 00001000 的映射对应第二个PT_LOAD .

it seems that the offset is based on the beginning address of the lowest memory mapping

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

However, is there any standard telling us that, how to detect the symbols' value type (virtual address or offset) programmatically?

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

关于linux - 使用 -fPIC 编译的共享库的内存映射和变量位置,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56764936/

相关文章:

c - fork() 返回值错误

c++ - 如何在没有 main 的情况下初始化 google 测试?

windows - linux下如何处理CP-1252编码的密码?

c - 有什么方法可以在 msp430 中进行多精度运算(使用大于 64 位的整数)?

c++ - 为什么简单的内联汇编函数不能在 GCC 中正常工作?

c - 如何在 clang 中使用 -std=c99 强制 Werror=declaration-after-statement

c++ - 为什么不能从信号处理程序调用 dlclose()?

c++ - 静态与共享库安全

c++ - Linux C++ : multiple processes reading same serial port?

linux - 检索 RPM 的当前架构