gcc/g++编译选项: -fPIC

旧时模样 提交于 2019-11-27 05:11:13

    在理解PIC概念之前,先了解一下动态链接库的载入时重定位概念。

    载入时重定位:

        我们知道,Linux的可执行文件一般是elf格式的,在这个可执行文件的头部包含了很多重要的信息:如文件格式,加载地址,符号表等。当连接器链接生成可执行文件时,会将程序的加载地址写入可执行文件头。在程序运行时,动态加载器将可执行文件载入文件头指定的加载地址处,并加载该地址,开始从该地址处运行。由此可见,可执行文件的起始地址是在编译时就决定的:

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x4005b0           // 程序入口地址
  Start of program headers:          64 (bytes into file)
  Start of section headers:          4472 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 27
 

        对于动态库来说,不像静态库(静态库是在链接可执行文件时,代码段和数据段直接拷贝到可执行文件中),是在运行时加载动态库代码,因此无法在编译和链接阶段获取代码段的符号地址(代码段的符号包括引用的全局数据,调用的函数等)。在调用动态库中的函数时,动态加载器动态分配一段进程地址空间,将动态库加载到该地址空间后,再修改代码段的符号地址。至于需要修改的哪些地址,链接器在动态库的文件头中预先写好,供加载器读取修改,动态库的重定位节举例如下:

Relocation section '.rela.text' at offset 0x8f8 contains 11 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000a  000e0000000a R_X86_64_32       0000000000000000 _ZSt4cout + 0
00000000000f  000f00000002 R_X86_64_PC32     0000000000000000 _ZStlsISt11char_traits - 4
000000000014  00100000000a R_X86_64_32       0000000000000000 _ZSt4endlIcSt11char_tr + 0
00000000001c  001100000002 R_X86_64_PC32     0000000000000000 _ZNSolsEPFRSoS_E - 4
000000000040  00040000000a R_X86_64_32       0000000000000000 .bss + 0
000000000045  001200000002 R_X86_64_PC32     0000000000000000 _ZNSt8ios_base4InitC1E - 4
00000000004a  00130000000a R_X86_64_32       0000000000000000 __dso_handle + 0
00000000004f  00040000000a R_X86_64_32       0000000000000000 .bss + 0
000000000054  00140000000a R_X86_64_32       0000000000000000 _ZNSt8ios_base4InitD1E + 0
000000000059  001500000002 R_X86_64_PC32     0000000000000000 __cxa_atexit - 4

    以上每一项对应着代码段中的一处重定位:在代码段的Offset处,进行Type类型的转换。这就是载入时重定位的基本概念和过程。

    载入时重定位的缺点:

    (1)动态库的代码段不能在进程间共享:多个进程加载同一个动态库到各自不同的地址空间,导致代码段需要不同的重定位,所以最终每个引用该动态库的进程拥有一份该动态库代码段的不同拷贝。

    (2)代码段必须是可写的,增加了被攻击风险。

  

    为了解决载入时重定位的问题,引入了PIC的概念,即位置无关代码。

    PIC实现原理:

    (1)GOT:在动态库的数据段增加GOT(Global Offset Table),该表的每一项是符号到地址的绝对映射。由于代码段到数据段的偏移是固定的,因此可以在编译时确定代码段中的某个符号到GOT特定项之间的偏移。这样,代码段中的符号偏移就可以在编译时确定了,在加载时也无需修改代码段的内容,只需要填写位于数据段的GOT的所有项的符号的绝对地址就完成了。因为数据段本来就是进程间不共享,每个进程独立的一份,因此GOT的设计完全解决了以上两个问题,从而达到两个目的:1,代码段可以在多进程间共享;2,代码段是只读的。

    (2)PLT:PLT是 Program Linkage Table 的缩写,即程序链接表,PLT的出现是为了延时定位的目的。一个动态库中的函数往往要远多于全局变量,并且被调用的函数往往少于定义的函数。GOT中包含了该动态库中的所有的全局变量的映射,并且在连接器加载时解析所有的全局变量的地址。如果用同样的方式去处理函数调用符号,则开销会非常大。因此在代码段设计了一个PLT表,每一项其实是个代码段,用于执行如下逻辑:首次访问时,解析参数和向GOT填写函数地址,后续访问直接访问GOT中的函数地址。如此达到了延时定位的目的。

    因此,一个PIC的动态库中,对全局变量使用GOT来映射,对函数调用使用PLT+GOT来映射,从而达到共享库代码段复用,代码段安全访问的目的。而这些就是 PIC 的意义。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!