关于PIE和PIC
2026-02-26 03:44:18 # compile # pic

关于PIE和PIC

前言

自从升级了高本版编译器,-pie成了默认的链接行为,我深受其害,今天终于可以考虑下掉它了,写篇文章记录一下。

关于PIC

相较PIE (Position-indendendent executable ),可能大家对PIC (Position-independent code ) 更加熟悉一些。前者是作用于可执行程序的链接,后缀用于动态链接的。个人第一次遇到PIC,也是在动态库的链接。

运行时重定向

对于程序内部的变量或函数访问,可以直接填绝对地址。因为程序在链接时,知道所有信息,能够事先知道,程序在装载时,会被放到哪个内存地址上。但如果对于动态库而言,并不能预知,动态库会被加载到哪个内存地址上。这就需要预留一个空间,等到程序运行时,做重定向。这带来一个坏处,

  • 代码段在内存中不能共享;
  • 所有的重定向条目,全在程序启动时修改,有一定的开销,拖慢程序启动。

与位置无关代码(PIC)

运行时重定向的问题在于,链接时仅预留了一个空间,时间的内存地址,需要在重新装载时进行修改。这导致每个程序,即使加载相同的动态库,也需要各自有一份独立的地址空间。

PIC要做的处理就是,将代码段中,所有和位置有关的代码,迁移到其他段中。这样,代码段就是只读的了,让代码段可以被所有进程共享。

全局变量访问

PIC模式下,访问变量的原理如下:

虽然,动态库无法在链接时知道自己被装载的地址空间,即无法知道绝对地址,但是指令和数据的相对地址是固定的,所以很容易可以想到的是,通过当前指令地址 + 相对偏移,就能得到变量的绝对地址了。为此编译器,引入了一个叫GOT(Global Offset Table)的东西,存放变量地址,通过指令地址 + GOT表相对偏移,来间接得到变量的实际地址。

这样做有两个好处:

  • 代码段,运行时不需要再重定向,是与位置无关的(PIC),可以在内存中被共享;
  • 不再需要对所有变量的访问做重定向,一个变量只需要重定向一个GOT表条目,效率更高。

下面是一个全局变量的访问,在 no-pic\ pic 下的编译结果。pic模式下,需要借助GOT表得到变量地址,再从变量地址加载变量,多了一条指令

1
2
3
4
5
int global_var = 1;

int func() {
return global_var + 1;
}
1
2
3
4
5
6
7
8
9
10
# global_var + 1 对应不同选项的汇编

# -fno-pic
mov eax, dword ptr [global_var]
add eax, 1

# -fpic
mov rax, qword ptr [rip + global_var@GOTPCREL]
mov eax, dword ptr [rax]
add eax, 1

函数调用

函数调用和变量访问略有不同。从一些文章了解到的信息是,相比全局变量, 函数的数量通常远多于变量 ,更关键的是 很多函数实际上可能没有发生调用 。所以没有像变量一样,直接在启动时重定向所有函数的GOT表条目,而是引入了一个PLT(Procedure Linkage Table),来实现懒加载。

个人感觉上面的结论不是特别靠谱,应该是函数它本身就会发生调用操作,所以可以在运行时,调用一个处理函数,来实现懒加载。但是变量不会,如果也像函数调用一样,在运行时重定向的话,额外开销很大。

具体示例如下:

  1. 函数的GOT表条目,不会在启动时直接进行重定向,而是在发生调用时,由动态加载器进行设置;
  2. 对于一个函数首次调用时,会call 对应函数的PLT;
  3. PLT中执行的第一条指令是 jump *GOT[n],因为函数的GOT表并没有在启动时重定向,初始时保存的是下一条指令地址;
  4. 继续执行,会进入PLT[0],通过动态加载器设置GOT[n]的值,并调用实际函数。
    由于上一步,已经修改了GOT表项的值,下一次再调用func时,jump *GOT[n],会直接调整到函数所在地址。
图片描述

关于PIE

有何作用

前面扯了一堆PIC,它有一个优势在于,能够让代码段在内存中共享,对于一些普遍使用到的动态库,是可以节省很多内存的。对于可执行程序似乎就没有这样的优势了,并且pic的代码,相比直接通过绝对地址访问,多少还是有些开销的。那这里pie的意义是啥呢?继续学习了下,了解到了一个ASLR(Address Space Layout Randomization,地址空间布局随机化)的机制。它是在程序加上时,随机化内存基地址,这就要求代码段要是 PIC 的,不能直接使用绝对地址。

为何新版本的编译器默认开启

从llvm 社区的讨论,了解到,开启ASLR是一个趋势,Linux发行版中,已经把编译器默认使用-pie的选项打开了。clang也是为了跟进社区步伐,默认开启了这个。

这里是指Linux发行版,在编译器编译器时,设置了选项,让编译出来的编译器,默认链接使用-pie。而不是对编译器本身

遇到的问题

这里提一下,因为pie导致的问题

  • brpc 因为pie链接,导致tls变量导致的异常更容易触发了。(具体文章有时间单独整理下)
  • glog的backtrace 等依赖 addr2line 的工具链,全部需要做适配。(因为前面提到的ASLR,程序运行时被加载到随机的虚拟地址。)

参考

undefined

https://mropert.github.io/2018/02/02/pic_pie_sanitizers/