Search code examples
assemblyx86nasmelfposition-independent-code

Why can nasm assembly with unqualified references to _GLOBAL_OFFSET_TABLE_ apparently be assembled and linked as PIC?


Why can I assemble and link get_got.asm as position-independent code when it contains a reference to the absolute address of its GOT?

get_got.asm

extern _GLOBAL_OFFSET_TABLE_

section .text
global get_got
get_got:
        mov     rax, _GLOBAL_OFFSET_TABLE_
        ret

main.c

#include <stdio.h>

void* get_got(void);

int main(int argc, char* argv[]) {
    printf("%p\n", get_got());
}

assembling, compiling, linking, running:

nasm -felf64 -o get_got.o get_got.asm
gcc -fPIC -shared -o get_got.so get_got.o
gcc -Wl,-rpath=\$ORIGIN -o main main.c get_got.so
./main
0x148ba1cba000

What's going on here? It looks to me like get_got.so somehow has a baked-in absolute address for a GOT that won't have a known address until runtime. Disassembling get_got.so shows that the mov does in fact contain an immediate (0x201000). Obviously I have a major misunderstanding of something. I expected this to cause nasm to generate a relocation that the linker would choke on.


Solution

  • I built your code and a modified version using lea rax, [rel _GLOBAL_OFFSET_TABLE_].

    I diffed the readelf -a output. There's a lot of noise from different addresses, though.
    readelf -a get_got.so | diff -u - <(readelf -a get_got_rel.so) | less

    The most interesting difference is:

    --- readelf -a get_got.so
    +++ readelf -a get_got_rel.so
    ....
    
    -Dynamic section at offset 0xe40 contains 22 entries:
    +Dynamic section at offset 0xe50 contains 21 entries:
    ...
    
    - 0x0000000000000016 (TEXTREL)            0x0
      0x000000006ffffffe (VERNEED)            0x3b0
      0x000000006fffffff (VERNEEDNUM)         1
      0x000000006ffffff0 (VERSYM)             0x398
    - 0x000000006ffffff9 (RELACOUNT)          4
    + 0x000000006ffffff9 (RELACOUNT)          3
    

    So the absolute version has a text relocation. I didn't know Linux / ELF dynamic linking could apply fixups after mapping shared libraries. But apparently it can. (It's better not to, because it dirties the memory page so it's no longer just backed by the file on disk.)

    But I checked with GDB, and that's what's going on: set a breakpoint in get_got and run it:

    (gdb) disas
    Dump of assembler code for function get_got:
    => 0x00007f9e77b235b0 <+0>:     movabs rax,0x7f9e77d24000
       0x00007f9e77b235ba <+10>:    ret    
    

    objdump -dRC -Mintel get_got.so: (note line wrapping without -w):

    00000000000005b0 <get_got>:
     5b0:   48 b8 00 10 20 00 00    movabs rax,0x201000
     5b7:   00 00 00 
                            5b2: R_X86_64_RELATIVE  *ABS*+0x201000
     5ba:   c3                      ret    
    

    Thanks @Jester for the -R tip; I normally use objdump -dr ..., not -R, and lower-case r doesn't print any relocations for the .so.
    On get_got.o, -r shows movabs rax,0x0 2: R_X86_64_64 _GLOBAL_OFFSET_TABLE_.


    gcc -nostdlib -pie will link 64-bit absolute relocations into PIE executables as well. (A PIE executable is an ELF shared object).

    What isn't allowed in PIC / PIE are 32-bit absolute relocations: 32-bit absolute addresses no longer allowed in x86-64 Linux?. You get a linker error. Addressing modes like array[rcx*4] aren't usable in PIC/PIE code, you need a separate instruction to get the address into a register.

    lea rdi, [rel array] is a much better choice than a 64-bit immediate absolute, because it's smaller and more friendly to the uop cache, and doesn't require a fixup when loading.