在理解PIC概念以前,先了解一下动态连接库的载入时重定位概念。ios
载入时重定位:安全
咱们知道,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
this
对于动态库来讲,不像静态库(静态库是在连接可执行文件时,代码段和数据段直接拷贝到可执行文件中),是在运行时加载动态库代码,所以没法在编译和连接阶段获取代码段的符号地址(代码段的符号包括引用的全局数据,调用的函数等)。在调用动态库中的函数时,动态加载器动态分配一段进程地址空间,将动态库加载到该地址空间后,再修改代码段的符号地址。至于须要修改的哪些地址,连接器在动态库的文件头中预先写好,供加载器读取修改,动态库的重定位节举例以下:spa
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类型的转换。这就是载入时重定位的基本概念和过程。进程
载入时重定位的缺点:string
(1)动态库的代码段不能在进程间共享:多个进程加载同一个动态库到各自不一样的地址空间,致使代码段须要不一样的重定位,因此最终每一个引用该动态库的进程拥有一份该动态库代码段的不一样拷贝。it
(2)代码段必须是可写的,增长了被攻击风险。io
为了解决载入时重定位的问题,引入了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 的意义。