用-fPIC

时间:2019-06-26 03:55:20

标签: 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标准:

  

在可执行文件和共享对象文件中,st_value保存一个虚拟地址。使这些文件的符号对运行时更有用   链接器,节偏移量(文件解释)让位给   分区号所在的虚拟地址(内存解释)   是无关紧要的。

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

1 个答案:

答案 0 :(得分: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之间的偏移量(否则二进制文件将无法正常工作)。因此,它将在内核选择的地址(通过mmap(0, ...))上创建一个大型映射(覆盖两个mprotect段)。然后PT_LOAD从第一个mmap的结尾开始直到整个映射的结尾(无访问权限)为止的区域。最后,它使用PT_LOAD标志将MAP_FIXED的第二个0段{期望的地址} PT_LOAD留在两个映射之间。

  

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

您可以从偏移量中轻松分辨。偏移量为00001000的映射对应于第一个PT_LOAD,孔不对应任何对象,偏移量为lib.so的映射对应于第二个mmap(0, ...)。 / p>

  

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

正确:这是整个dli_fbase; /* Base address at which shared object is loaded */ ELF图像的重定位(由第一个from multiprocessing import Pool def func(x,i): dftmp=traindat.iloc[x,4:28].mean() return pd.DataFrame(dftmp).transpose() pool = mp.Pool(processes=3) new_rows = pool.map(func, [(row,idx) for idx,row in indices.iterrows()]) pool.close() pool.join() data_all_new = pd.concat(new_rows) 确定)。该重定位将应用于图像中的每个符号。

  

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

没有标准。但是您可以使用dladdr来查找“基地址”(重定位)。特别是import sys sys.modules['__main__'].__file__ = 'ipython' from multiprocessing import Pool def f(x): return x*x if __name__ == '__main__': p = Pool(5) print(p.map(f, [1, 2, 3]))