Steps for building your application as PIE
In Xcode, select your target in the "Targets" section, then click the "Build Settings" tab to view its settings.
For iOS apps, set iOS Deployment Target to iOS 4.3 or later. For Mac apps, set OS X Deployment Target to OS X 10.7 or later.
Verify that Generate Position-Dependent Code is set at to NO.
Verify that Don't Create Position Independent Executables is set to NO.
复制代码
PIE是什么
PIE(position-independent executable)是一种生成地址无关可执行程序的技术。如果编译器在生成可执行程序的过程中使用了PIE,那么当可执行程序被加载到内存中时其加载地址存在不可预知性。
PIE还有个孪生兄弟PIC(position-independent code)。其作用和PIE相同,都是使被编译后的程序能够随机的加载到某个内存地址。区别在于PIC是在生成动态链接库时使用(Linux中的so),PIE是在生成可执行文件时使用。
PIE的作用
安全性
PIE可以提高缓冲区溢出攻击的门槛。它属于ASLR(Address space layout randomization)的一部分。ASLR要求执行程序被加载到内存时,它其中的任意部分都是随机的。包括 Stack, Heap ,Libs and mmap, Executable, Linker, VDSO。通过PIE我们能够实现Executable 内存随机化
节约内存使用空间
除了安全性,地址无关代码还有一个重要的作用是提高内存使用效率。
一个共享库可以同时被多个进程装载,如果不是地址无关代码(代码段中存在绝对地址引用),每个进程必须结合其自生的内存地址调用动态链接库。导致不得不将共享库整体拷贝到进程中。如果系统中有100个进程调用这个库,就会有100份该库的拷贝在内存中,这会照成极大的空间浪费。
相反如果被加载的共享库是地址无关代码,100个进程调用该库,则该库只需要在内存中加载一次。这是因为PIE将共享库中代码段须要变换的内容分离到数据段。使得代码段加载到内存时能做到地址无关。多个进程调用共享库时只需要在自己的进程中加载共享库的数据段,而代码段则可以共享。
PIE工作原理简介
我们先从实际的例子出发,观察PIE和NO-PIE在可执行程序表现形式上的区别。管中窥豹探索地址无关代码的实现原理。
例子一
定义如下C代码:
#include <stdio.h>
int global;
void main()
{
printf("global address = %x\n", &global);
}
复制代码
程序中定义了一个全局变量global并打印其地址。我们先用普通的方式编译程序。
gcc -o sample1 sample1.c
运行程序可以观察到global加载到内存的地址每次都一样。
$./sample1
global address = 6008a8
$./sample1
global address = 6008a8
$./sample1
global address = 6008a8
复制代码
接着用PIE方式编译 sample1.c
gcc -o sample1_pie sample1.c -fpie -pie
运行程序观察global的输出结果:
./sample1_pie
global address = 1ce72b38
./sample1_pie
global address = 4c0b38
./sample1_pie
global address = 766dcb38
复制代码
每次运行地址都会发生变换,说明PIE使执行程序每次加载到内存的地址都是随机的。
例子二
在代码中声明一个外部变量global。但这个变量的定义并未包含进编译文件中。
#include <stdio.h>
extern int global;
void main()
{
printf("extern global address = %x\n", &global);
}
复制代码
首先使用普通方式编译 extern_var.c。在编译选项中故意不包含有global定义的源文件。
gcc -o extern_var extern_var.c
发现不能编译通过, gcc提示:
/tmp/ccJYN5Ql.o: In function `main':
extern_var.c:(.text+0xa): undefined reference to `global'
collect2: ld returned 1 exit status
复制代码
编译器在链接阶段有一步重要的动作叫符号解析与重定位。链接器会将所有中间文件的数据,代码,符号分别合并到一起,并计算出链接后的虚拟基地址。比如 “.text”段从 0x1000开始,”.data”段从0x2000开始。接着链接器会根据基址计算各个符号(global)的相对虚拟地址。
当编译器发现在符号表中找不到global的地址时就会报出 undefined reference to global
.说明在静态链接的过程中编译器必须在编译链接阶段完成对所有符号的链接。
如果使用PIE方式将extern_var.c编译成一个share library会出现什么情况呢?
gcc -o extern_var.so extern_var.c -shared -fPIC
程序能够顺利编译通过生成extern_var.so。但运行时会报错,因为装载时找不到global符号目标地址。这说明-fPIC选项生成了地址无关代码。将静态链接时没有找到的global符号的链接工作推迟到装载阶段。
那么在编译链接阶段,链接器是如何将这个缺失的目标地址在代码段中进行地址引用的呢?
链接器巧妙的用一张中间表GOT(Global Offset Table)来解决被引用符号缺失目标地址的问题。如果在链接阶段(jing tai)发现一个不能确定目标地址的符号。链接器会将该符号加到GOT表中,并将所有引用该符号的地方用该符号在GOT表中的地址替换。到装载阶段动态链接器会将GOT表中每个符号对应的实际目标地址填上。
当程序执行到符号对应的代码时,程序会先查GOT表中对应符号的位置,然后根据位置找到符号的实际的目标地址。
地址无关代码的生成方式 所谓地址无关代码要求程序被加载到内存中的任意地址都能够正常执行。所以程序中对变量或函数的引用必须是相对的,不能包含绝对地址。
比如如下伪汇编代码:
PIE方式:代码可以运行在地址100或1000的地方
100: COMPARE REG1, REG2
101: JUMP_IF_EQUAL CURRENT+10
...
111: NOP
复制代码
Non-PIE: 代码只能运行在地址100的地方
100: COMPARE REG1, REG2
101: JUMP_IF_EQUAL 111
...
111: NOP
复制代码
因为可执行程序的代码段只有读和执行属性没有写属性,而数据段具有读写属性。要实现地址无关代码,就要将代码段中须要改变的绝对值分离到数据段中。在程序加载时可以保持代码段不变,通过改变数据段中的内容,实现地址无关代码。
PIE和Non-PIE程序在内存中映射方式
在Non-PIE时程序每次加载到内存中的位置都是一样的。
执行程序会在固定的地址开始加载。系统的动态链接器库ld.so会首先加载,接着ld.so会通过.dynamic段中类型为DT_NEED的字段查找其他需要加载的共享库。并依次将它们加载到内存中。注意:因为是Non-PIE模式,这些动态链接库每次加载的顺序和位置都一样。
而对于通过PIE方式生成的执行程序,因为没有绝对地址引用所以每次加载的地址也不尽相同。
不仅动态链接库的加载地址不固定,就连执行程序每次加载的地址也不一样。这就要求ld.so首先被加载后它不仅要负责重定位其他的共享库,同时还要对可执行文件重定位。