在 gcc 和 ld 中,与位置无关的可执行文件的-fPIE 选项是什么?

它将如何改变代码,例如函数调用?

121772 次浏览

PIE 在可执行文件中支持 位址空间配置随机载入(ASLR)

在创建 PIE 模式之前,程序的可执行文件不能放置在内存中的随机地址,只有位置独立的代码(PIC)动态库可以重新定位到随机偏移量。它的工作原理非常类似于 PIC 对动态库所做的工作,不同之处在于没有创建过程链接表(PLT) ,而是使用了与 PC 相关的重定位。

在 gcc/linkers 中启用 PIE 支持后,程序主体将被编译并作为地址无关代码链接。动态链接器在程序模块上执行完整的重定位处理,就像动态库一样。全局数据的任何使用都通过全局偏移量表转换为访问,并添加了 GET 重定位。

馅饼在 这个 OpenBSD PIE 演示文稿中得到了很好的描述。

函数的更改显示为 在这张幻灯片里(PIE vs PIC)。

X86照片对馅饼

局部全局变量和函数在饼中进行优化

外部全局变量和函数与 ic 相同

这张幻灯片中(PIE 与旧式链接)

X86饼与无旗(固定)

局部全局变量和函数类似于固定变量

外部全局变量和函数与 ic 相同

注意,PIE 可能与 -static不兼容

最小可运行示例: GDB 两次执行可执行文件

对于那些希望看到一些操作的人,让我们看看 ASLR 在 PIE 可执行文件上的工作,以及跨运行更改地址:

总机

#include <stdio.h>


int main(void) {
puts("hello");
}

主要,嘘

#!/usr/bin/env bash
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
for pie in no-pie pie; do
exe="${pie}.out"
gcc -O0 -std=c99 "-${pie}" "-f${pie}" -ggdb3 -o "$exe" main.c
gdb -batch -nh \
-ex 'set disable-randomization off' \
-ex 'break main' \
-ex 'run' \
-ex 'printf "pc = 0x%llx\n", (long  long unsigned)$pc' \
-ex 'run' \
-ex 'printf "pc = 0x%llx\n", (long  long unsigned)$pc' \
"./$exe" \
;
echo
echo
done

对于 -no-pie来说,一切都很无聊:

Breakpoint 1 at 0x401126: file main.c, line 4.


Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x401126


Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x401126

在开始执行之前,break main0x401126设置一个断点。

然后,在两次执行期间,run停在地址 0x401126

然而,-pie的那个要有趣得多:

Breakpoint 1 at 0x1139: file main.c, line 4.


Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x5630df2d6139


Breakpoint 1, main () at main.c:4
4           puts("hello");
pc = 0x55763ab2e139

在开始执行之前,GDB 只获取可执行文件中的一个“虚拟”地址: 0x1139

但是,在启动之后,GDB 智能地注意到动态加载程序将程序放置在不同的位置,并且第一个中断在 0x5630df2d6139处停止。

然后,第二次运行也明智地注意到可执行文件再次移动,并最终在 0x55763ab2e139处中断。

echo 2 | sudo tee /proc/sys/kernel/randomize_va_space确保 ASLR 处于打开状态(Ubuntu 17.10中的默认设置) : 如何暂时禁用 ASLR (位址空间配置随机载入) ? | 问 Ubuntu

否则就需要 set disable-randomization off,顾名思义,GDB 在默认情况下关闭进程的 ASLR,以便在运行期间提供固定的地址,从而改善调试体验: Gdb 地址和“实际”地址之间的区别? | 堆栈溢出

readelf分析

此外,我们还可以观察到:

readelf -s ./no-pie.out | grep main

给出实际的运行时加载地址(pc 指向下面的指令后4个字节) :

64: 0000000000401122    21 FUNC    GLOBAL DEFAULT   13 main

同时:

readelf -s ./pie.out | grep main

给出的只是一个抵消:

65: 0000000000001135    23 FUNC    GLOBAL DEFAULT   14 main

通过关闭 ASLR (使用 randomize_va_spaceset disable-randomization off) ,GDB 总是向 main提供地址: 0x5555555547a9,因此我们推断 -pie地址由以下部分组成:

0x555555554000 + random offset + symbol offset (79a)

在 Linux kernel/glibc loader/where 中,0x555555554000硬编码在哪里

最小程序集示例

我们可以做的另一件很酷的事情是使用一些汇编代码来更具体地理解 PIE 的含义。

我们可以使用 Linux x86 _ 64独立组装 hello world:

总部

.text
.global _start
_start:
asm_main_after_prologue:
/* write */
mov $1, %rax   /* syscall number */
mov $1, %rdi   /* stdout */
mov $msg, %rsi  /* buffer */
mov $len, %rdx /* len */
syscall


/* exit */
mov $60, %rax   /* syscall number */
mov $0, %rdi    /* exit status */
syscall
msg:
.ascii "hello\n"
len = . - msg

GitHub 上游

它可以很好地组装和运行:

as -o main.o main.S
ld -o main.out main.o
./main.out

但是,如果我们试图将其链接为 PIE (--no-dynamic-linker是必需的,详见: 如何在 Linux 中创建一个静态链接的位置独立的可执行 ELF?) :

ld --no-dynamic-linker -pie -o main.out main.o

那么链接将会失败:

ld: main.o: relocation R_X86_64_32S against `.text' can not be used when making a PIE object; recompile with -fPIC
ld: final link failed: nonrepresentable section on output

因为台词是:

mov $msg, %rsi  /* buffer */

mov操作数中硬编码消息地址,因此不是位置独立的。

如果我们用一种独立的方式来写:

lea msg(%rip), %rsi

然后 PIE 链接工作正常,GDB 向我们展示了可执行文件每次都在内存的不同位置加载。

这里的不同之处在于,由于 rip语法,leamsg相对于当前 PC 地址的地址进行了编码,参见: 如何在64位汇编程序中使用 RIP 相对寻址?

我们也可以通过以下方法来分解这两个版本:

objdump -S main.o

它们分别给出:

e:   48 c7 c6 00 00 00 00    mov    $0x0,%rsi
e:   48 8d 35 19 00 00 00    lea    0x19(%rip),%rsi        # 2e <msg>


000000000000002e <msg>:
2e:   68 65 6c 6c 6f          pushq  $0x6f6c6c65

因此,我们可以清楚地看到,lea已经拥有完全正确的 msg地址,并将其编码为当前地址 + 0x19。

然而,mov版本将地址设置为 00 00 00 00,这意味着重定位将在那里执行: 链接器是做什么的?ld错误消息中隐藏的 R_X86_64_32S是实际需要的重定位类型,而这种重定位在 PIE 可执行文件中是不可能发生的。

我们可以做的另一件有趣的事情是把 msg放在数据部分,而不是 .text:

.data
msg:
.ascii "hello\n"
len = . - msg

现在,.o组装成:

e:   48 8d 35 00 00 00 00    lea    0x0(%rip),%rsi        # 15 <_start+0x15>

所以 RIP 偏移量现在是 0,我们猜测重定位已经被汇编程序请求了。我们确认:

readelf -r main.o

它给出了:

Relocation section '.rela.text' at offset 0x160 contains 1 entry:
Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000011  000200000002 R_X86_64_PC32     0000000000000000 .data - 4

所以很明显,R_X86_64_PC32是一个 PC 相对重定位,ld可以处理 PIE 可执行文件。

这个实验告诉我们,连接器本身检查程序可以是 PIE,并将其标记为 PIE。

然后在用 GCC 编译时,-pie告诉 GCC 生成位置无关的程序集。

但是如果我们自己编写汇编,我们必须手动确保我们已经实现了位置独立性。

在 ARMv8 aarch64中,可以使用 ADR 指令实现位置独立的 hello world。

如何确定 ELF 是否位置独立?

除了在 GDB 中运行之外,还提到了一些静态方法:

在 Ubuntu 18.10中测试。