在stackoverflow上看了What is the -fPIE option for position-independent executables in gcc and ld?,对其中的答案甚是满意,所以转载到博客中。

1. Overview

PIE(position-independent executables) is to support address space layout randomization (ASLR) in executable files.

Before the PIE mode was created, the program’s executable could not be placed at a random address in memory, only position independent code (PIC) dynamic libraries could be relocated to a random offset. It works very much like what PIC does for dynamic libraries, the difference is that a Procedure Linkage Table (PLT) is not created, instead PC-relative relocation is used.

After enabling PIE support in gcc/linkers, the body of program is compiled and linked as position-independent code. A dynamic linker does full relocation processing on the program module, just like dynamic libraries. Any usage of global data is converted to access via the Global Offsets Table (GOT) and GOT relocations are added.

2. Example

2.1 Minimal runnable example: GDB the executable twice

Let’s see ASLR work on the PIE executable and change addresses across runs:

main.c

1
2
3
4
5
#include <stdio.h>

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

main.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/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 -q \
-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

For the one with -no-pie, everything is boring:

1
2
3
4
5
6
7
8
9
Breakpoint 1 at 0x40052a: file main.c, line 4.

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

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

Before starting execution, break main sets a breakpoint at 0x40052a.

Then, during both executions, run stops at address 0x40052a.

The one with -pie however is much more interesting:

1
2
3
4
5
6
7
8
9
Breakpoint 1 at 0x754: file main.c, line 4.

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

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

Before starting execution, GDB just takes a “dummy” address that is present in the executable: 0x754.

After it starts however, GDB intelligently notices that the dynamic loader placed the program in a different location, and the first break stopped at 0x56093aa99754.

Then, the second run also intelligently noticed that the executable moved again, and ended up breaking at 0x55615b05e754.

echo 2 | sudo tee /proc/sys/kernel/randomize_va_space ensures that ASLR is on: How can I temporarily disable ASLR (Address space layout randomization)? | Ask Ubuntu.

set disable-randomization off is needed otherwise GDB, as the name suggests, turns off ASLR for the process by default to give fixed addresses across runs to improve the debugging experience: Difference between gdb addresses and “real” addresses? | Stack Overflow.

readelf analysis

Furthermore, we can also observe that:

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

gives the actual runtime load address (pc pointed to the following instruction 4 bytes after):

1
68: 0000000000400526    21 FUNC    GLOBAL DEFAULT   14 main

while:

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

gives just an offset:

1
68: 0000000000000750    23 FUNC    GLOBAL DEFAULT   14 main

By turning ASLR off (with either randomize_va_space or set disable-randomization off), GDB always gives main the address: 0x555555554754, so we deduce that the -pie address is composed from:

1
0x555555554000 + random offset + symbol offset (750)

2.2 Minimal assembly example

Another cool thing we can do is to play around with some assembly code to understand more concretely what PIE means.

We can do that with a Linux x86_64 freestanding assembly hello world:

main.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
        .global _start

.text
_start:
# write(1, message, 13)
mov $1, %rax # system call 1 is write
mov $1, %rdi # file handle 1 is stdout
mov $message, %rsi # address of string to output
mov $13, %rdx # number of bytes
syscall # invoke operating system to do the write

# exit(0)
mov $60, %rax # system call 60 is exit
xor %rdi, %rdi # we want return code 0
syscall # invoke operating system to exit
message:
.ascii "Hello, world\n"

and it assembles and runs fine with:

1
2
3
as -o main.o main.S
ld -o main.out main.o
./main.out

However, if we try to link it as PIE with (--no-dynamic-linker is required as explained at: How to create a statically linked position independent executable ELF in Linux?):

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

then link will fail with:

1
2
ld: main.o: relocation R_X86_64_32S against `.text' can not be used when making a shared object; recompile with -fPIC
main.o: error adding symbols: Bad value

Because the line:

1
mov     $message, %rsi          # address of string to output

hardcodes the message address in the mov operand, and is therefore not position independent.

If we instead write it in a position independent way:

1
lea 	message(%rip), %rsi	# address of string to output

then PIE link works fine, and GDB shows us that the executable does get loaded at a different location in memory every time.

The difference here is that lea encoded the address of msg relative to the current PC address due to the rip syntax, see also:How to use x64 RIP-relative addressing

Another fun thing that we can do is to put the msg in the data section instead of .text with:

1
2
3
		.data
message:
.ascii "Hello, world\n"

Now the .o assembles to:

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

so the RIP offset is now 0, and we guess that a relocation has been requested by the assembler. We confirm that with:

1
readelf -r main.o

which gives:

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

so clearly R_X86_64_PC32 is a PC relative relocation that ld can handle for PIE executables.

This experiment taught us that the linker itself checks the program can be PIE and marks it as such.

Then when compiling with GCC, -pie tells GCC to generate position independent assembly.

But if we write assembly ourselves, we must manually ensure that we have achieved position independence.