Search code examples
linuxelfdynamic-linkingrelocation

Who performs runtime relocations?


I'm trying to better understand runtime relocations in Linux, specifically who performs them in different situations. Below is my current understanding, is it accurate?

  • Position-dependent statically-linked executable - no runtime relocations needed
  • Dynamically-linked executable - the dynamic linker (ld.so) loads libraries and then performs relocations
  • Statically linked PIE - the libc startup code performs relocations
  • The dynamic linker itself - ld.so is a self-relocating binary

Thanks


Solution

  • Your answers are all (mostly) correct.

    You can observe where the relocation is happening using a debugger, and confirm your understanding.

    Example:

    #include <stdio.h>
    
    int main()
    {
      printf("%d\n", 123);
      return 0;
    }
    

    Let's start with position-dependent, dynamically linked binary.

    gcc -g -fno-pie -no-pie t.c
    gdb -q ./a.out
    Reading symbols from ./a.out...
    (gdb) starti
    Starting program: /tmp/a.out
    
    Program stopped.
    0x00007ffff7fd2090 in _start () from /lib64/ld-linux-x86-64.so.2
    (gdb) disas main
    Dump of assembler code for function main:
       0x0000000000401126 <+0>:     push   %rbp
       0x0000000000401127 <+1>:     mov    %rsp,%rbp
       0x000000000040112a <+4>:     mov    $0x7b,%esi
       0x000000000040112f <+9>:     mov    $0x402010,%edi
       0x0000000000401134 <+14>:    mov    $0x0,%eax
       0x0000000000401139 <+19>:    call   0x401030 <printf@plt>
       0x000000000040113e <+24>:    mov    $0x0,%eax
       0x0000000000401143 <+29>:    pop    %rbp
       0x0000000000401144 <+30>:    ret
    End of assembler dump.
    
    (gdb) disas 0x401030
    Dump of assembler code for function printf@plt:
       0x0000000000401030 <+0>:     jmp    *0x2fe2(%rip)        # 0x404018 <[email protected]>
       0x0000000000401036 <+6>:     push   $0x0
       0x000000000040103b <+11>:    jmp    0x401020
    End of assembler dump.
    

    Here we can see that the address to be relocated is 0x404018. Let's see where that address gets updated:

    (gdb) watch *(void**)0x404018
    Hardware watchpoint 1: *(void**)0x404018
    (gdb) c
    Continuing.
    
    Hardware watchpoint 1: *(void**)0x404018
    
    Old value = (void *) 0x401036 <printf@plt+6>
    New value = (void *) 0x7ffff7e54270 <printf>
    0x00007ffff7fe0f10 in _dl_fixup (l=<optimized out>, reloc_arg=<optimized out>) at dl-runtime.c:146
    146     dl-runtime.c: No such file or directory.
    (gdb) bt
    #0  0x00007ffff7fe0f10 in _dl_fixup (l=<optimized out>, reloc_arg=<optimized out>) at dl-runtime.c:146
    #1  0x00007ffff7fe84fe in _dl_runtime_resolve_xsavec () at ../sysdeps/x86_64/dl-trampoline.h:126
    #2  0x000000000040113e in main () at t.c:5
    
    (gdb) info symbol $pc
    _dl_fixup + 288 in section .text of /lib64/ld-linux-x86-64.so.2
    

    So in the dynamically linked non-pie case, it is indeed the dynamic loader performs relocations.

    Note: this is a lazy relocation, and is happening on-demand, after main is already running.

    If we make it non-lazy, it will be performed by ld.so before invoking main:

    gcc -g -fno-pie -no-pie t.c -Wl,-z,now
    

    Repeat above steps and observe the relocation happening before main:

    (gdb) run
    Starting program: /tmp/a.out
    
    Hardware watchpoint 1: *(void**)0x403fe8
    
    Old value = (void *) 0x401036 <printf@plt+6>
    New value = (void *) 0x7ffff7e54270
    elf_machine_rela (skip_ifunc=<optimized out>, reloc_addr_arg=<optimized out>, version=<optimized out>, sym=<optimized out>, reloc=<optimized out>, map=<optimized out>)
        at ../sysdeps/x86_64/dl-machine.h:464
    464     ../sysdeps/x86_64/dl-machine.h: No such file or directory.
    (gdb) bt
    #0  elf_machine_rela (skip_ifunc=<optimized out>, reloc_addr_arg=<optimized out>, version=<optimized out>, sym=<optimized out>, reloc=<optimized out>, map=<optimized out>)
        at ../sysdeps/x86_64/dl-machine.h:464
    #1  elf_dynamic_do_Rela (skip_ifunc=<optimized out>, lazy=<optimized out>, nrelative=<optimized out>, relsize=<optimized out>, reladdr=<optimized out>, map=0x7ffff7ffe1a0)
        at do-rel.h:137
    #2  _dl_relocate_object (l=l@entry=0x7ffff7ffe1a0, scope=<optimized out>, reloc_mode=<optimized out>, consider_profiling=<optimized out>, consider_profiling@entry=0)
        at dl-reloc.c:274
    #3  0x00007ffff7fd555b in dl_main (phdr=<optimized out>, phnum=<optimized out>, user_entry=<optimized out>, auxv=<optimized out>) at rtld.c:2341
    #4  0x00007ffff7fec1a2 in _dl_sysdep_start (start_argptr=start_argptr@entry=0x7fffffffe380, dl_main=dl_main@entry=0x7ffff7fd34c0 <dl_main>) at ../elf/dl-sysdep.c:252
    #5  0x00007ffff7fd3021 in _dl_start_final (arg=0x7fffffffe380) at rtld.c:504
    #6  _dl_start (arg=0x7fffffffe380) at rtld.c:597
    #7  0x00007ffff7fd2098 in _start () from /lib64/ld-linux-x86-64.so.2
    

    Repeat for other build combinations.