Search code examples
gcclinkerriscvbare-metal

What kind of executable is produced by gcc wtih `-static-pie`?


The question is specific (but not limited to) to a bare-metal Risc-V code, I am trying to understand what would be required in order to compile a position-independent firmware binary. I tried to compile some simple code with -fpie passed to gcc and -static-pie passed to the linker, and it appears that the produced binary is a self-contained position-independent executable, containing .got, .got.plt sections, and seemingly onlu pc-relative references in the code. The documentation is quite obscure, and the specific -static-pie option is rarely mentioned. Most of pie/pic documentation is discussing dynamic relocations. Am I correct that with this option the produced binary is a true position-independent executable, which does not require any special relocation logic from the loader (or any loader at all, assuming it is making it's way somehow into the executable memory of course). Or I am missing something?


Solution

  • The GCC manual seems quite clear:

    -static-pie

    Produce a static position independent executable on targets that support it. A static position independent executable is similar to a static executable, but can be loaded at any address without a dynamic linker. For predictable results, you must also specify the same set of options used for compilation (-fpie, -fPIE, or model suboptions) when you specify this linker option.

    To satisfy yourself that your -static-pie program does not require a dynamic linker/loader you can use the fact that an ELF program that does require one has to have an INTERP program header that nominates the one required. Thus:

    $ cat main.c
    #include <stdio.h>
    
    int main(void)
    {
        puts("Hello World!");
        return 0;
    }
    
    $ gcc main.c
    $ readelf --program-headers --wide a.out | grep -A1 INTERP; echo Done
      INTERP         0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R   0x1
          [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
    Done
    

    which is reflected in:

    $ ldd a.out
        linux-vdso.so.1 (0x00007ffe65fd3000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000722244e00000)
        /lib64/ld-linux-x86-64.so.2 (0x00007222451f3000)
    

    and at runtime:

    $ LD_DEBUG=files ./a.out
         55403: 
         55403: file=libc.so.6 [0];  needed by ./a.out [0]
         55403: file=libc.so.6 [0];  generating link map
         55403:   dynamic: 0x00007d72d8202940  base: 0x00007d72d8000000   size: 0x0000000000211d90
         55403:     entry: 0x00007d72d802a390  phdr: 0x00007d72d8000040  phnum:                 14
         55403: 
         55403: 
         55403: calling init: /lib64/ld-linux-x86-64.so.2
         55403: 
         55403: 
         55403: calling init: /lib/x86_64-linux-gnu/libc.so.6
         55403: 
         55403: 
         55403: initialize program: ./a.out
         55403: 
         55403: 
         55403: transferring control: ./a.out
         55403: 
    Hello World!
         55403: 
         55403: calling fini:  [0]
         55403: 
         55403: 
         55403: calling fini: /lib/x86_64-linux-gnu/libc.so.6 [0]
         55403: 
         55403: 
         55403: calling fini: /lib64/ld-linux-x86-64.so.2 [0]
         55403:
    

    The dynamic linker starts first and finishes last.

    Whereas:

    $ gcc -fPIE -static-pie main.c
    $ readelf --program-headers --wide a.out | grep -A1 INTERP; echo Done
    Done
    
    $ ldd a.out
        statically linked
        
    $ LD_DEBUG=files ./a.out
    Hello World!
    

    The dynamic linker is not there.