Consider this GNU Assembler program for AMD64 Linux:
.globl _start
_start:
movl $59, %eax # SYS_execve
leaq .pathname(%rip), %rdi # position-independent addressing
leaq .argv(%rip), %rsi
movq (%rsp), %rdx
leaq 16(%rsp,%rdx,8), %rdx
syscall
movl $60, %eax # SYS_exit
movl $1, %edi
syscall
.section .data
.argv:
.quad .argv0 # Absolute address as static data
.quad .argv1
.quad 0
.pathname:
.ascii "/bin/"
.argv0:
.asciz "echo"
.argv1:
.asciz "hello"
When I build it with gcc -nostdlib -static-pie
and run it, it fails, and strace shows me that this happens:
execve("/bin/echo", [0x301d, 0x3022], 0x7fff9bbe5a08 /* 28 vars */) = -1 EFAULT (Bad address)
It works fine if I build it as a static non-PIE binary or as a dynamic PIE binary, though. It looks like the problem is that relocations aren't getting processed.
In dynamic PIE binaries, the dynamic linker does that, and in non-PIE static binaries, you don't need runtime relocations; static addresses are link time constants.
But how are static PIE binaries supposed to work? Are they just not supposed to have any relocations at all, or is something else supposed to process them?
Apparently static-PIE still leaves runtime relocation to user-space. If you omit CRT startup code (with -nostdlib
), it doesn't happen at all. That's presumably why gcc -nostdlib
doesn't make a static-PIE by default.
If you do link with glibc's CRT start code, it will handle it for you with code specifically for that purpose.
Test case: your code with _start
changed to main
, or Nate's C example from comments. (I changed the global var names to be long and easy to find in searching readelf
or nm
output.)
#include <stdio.h>
int global_static_a = 7;
int *static_ptr = &global_static_a;
int main(void) {
printf("%d\n", *static_ptr); // load and deref the statically-initialized pointer
}
Compile with gcc -g -fpie -static-pie print.c
(I used gcc 10.1.0 with glibc 2.31-5 on Arch GNU/Linux for x86-64)
Run gdb ./a.out
. In GDB:
starti
(I wanted to make sure GDB could see the correct addresses before setting watch points, in case that's necessary)
watch static_ptr
continue
The watchpoint was hit by _dl_relocate_static_pie+540 mov QWORD PTR [rcx],rdx
.
Hardware watchpoint 2: static_ptr
Old value = (int *) 0xb7130
New value = (int *) 0x7ffff7ffb130 <global_static_a>
The fact that a function called _dl_relocate_static_pie
got linked into my executable is pretty clear evidence that glibc provided that code.