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

时间:2010-03-17 14:43:50

标签: gcc linker

如何更改代码,例如函数调用?

2 个答案:

答案 0 :(得分:69)

PIE是在可执行文件中支持address space layout randomization (ASLR)

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

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

PIE在this OpenBSD PIE presentation中有详细描述。

对功能的更改显示为in this slide(PIE与PIC)。

  

x86 pic vs pie

     

本地全局变量和函数在pie

中进行了优化      

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

this slide(PIE vs旧式链接)

  

x86 pie vs no-flags(已修复)

     

本地全局变量和函数类似于fixed

     

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

注意,PIE可能与-static

不兼容

答案 1 :(得分:18)

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

对于那些想要执行某些操作的用户,让我们看看ASLR在PIE可执行文件上工作并在运行之间更改地址:

main.c

#include <stdio.h>

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

main.sh

#!/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中的默认设置):How can I temporarily disable ASLR (Address space layout randomization)? | Ask Ubuntu

set disable-randomization off是必需的,否则,顾名思义,GDB会默认关闭进程的ASLR,以在运行中提供固定地址以改善调试体验:Difference between gdb addresses and "real" addresses? | Stack Overflow

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)

TODO在Linux内核/ glibc loader /哪里的0x555555554000硬编码在哪里? How is the address of the text section of a PIE executable determined in Linux?

最小装配示例

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

我们可以使用Linux x86_64独立程序集hello world来做到这一点:

main.S

.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

它可以通过以下命令组装并正常运行:

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

但是,如果我们尝试将其作为PIE链接到:

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操作数中硬编码消息地址,因此与位置无关。

--no-dynamic-linker是必需的,如以下内容所述:How to create a statically linked position independent executable ELF in Linux?

如果我们改为以与位置无关的方式编写它:

lea msg(%rip), %rsi

然后PIE链接可以正常工作,GDB向我们表明,可执行文件确实每次都加载到内存中的其他位置。

此处的区别在于,lea的语法由于msg相对于当前PC地址而对rip的地址进行了编码,另请参见:How to use RIP Relative Addressing in a 64-bit assembly program?

我们还可以通过以下两种方式将这两个版本拆解:

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,这意味着将在此处执行重定位:What do linkers do? {{1}中的神秘R_X86_64_32S }错误消息是必需的实际重定位类型,在PIE可执行文件中不会发生。

我们可以做的另一件有趣的事情是将ld放在数据部分而不是msg上:

.text

现在.data msg: .ascii "hello\n" len = . - msg 会组装为:

.o

因此RIP偏移现在为e: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 15 <_start+0x15> ,并且我们猜测汇编器已请求重定位。我们通过以下方式确认这一点:

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 是PC的相对重定位,R_X86_64_PC32可以处理PIE可执行文件。

该实验告诉我们,链接器本身会检查程序是否为PIE并将其标记为这样。

然后,在使用GCC进行编译时,ld告诉GCC生成与位置无关的程序集。

但是,如果我们自己编写程序集,则必须手动确保实现位置独立性。

在ARMv8 aarch64中,可以使用ADR instruction实现与位置无关的问候世界。

如何确定ELF是否与位置无关?

除了通过GDB运行它之外,还提到了一些静态方法:

在Ubuntu 18.10中进行了测试。