Search code examples
assemblyx86-64static-linkingrelocationposition-independent-code

How are relocations supposed to work in static PIE binaries?


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?


Solution

  • 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.