5

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

uvuv
  • 368
  • 1
  • 7

1 Answers1

3

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 <printf@got.plt>
   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.

Employed Russian
  • 199,314
  • 34
  • 295
  • 362
  • Thanks for the detailed answer! Did the "mostly" refer to lazy vs non-lazy or was I missing another thing? – uvuv Oct 10 '21 at 11:00
  • 1
    @uvuv Yes: the "loads libraries and then performs relocations" implied to me that "loader performs all relocations before running the binary", which isn't true in the lazy case. But maybe you didn't mean that? – Employed Russian Oct 10 '21 at 15:03
  • I did mean that, thanks for correcting! was just curious on whether something else was off – uvuv Oct 12 '21 at 13:56