On this page

Linker Scripts and Relocations

Coverage: ld script syntax → relocation types → GOT/PLT relocation → linker relaxation → LLD vs GNU ld vs gold → R_AARCH64_ADR_GOT_PAGE Applicable to: binutils ld, LLD, static/dynamic linking

Relocations: The Linker's Job

Compile time (gcc -c):
  .o files contain RELA entries → recording "this address needs filling, target symbol is this"
  Example: R_X86_64_PLT32: This 4-byte offset needs to be filled with the symbol's PLT address

Link time (ld):
  Iterate through all .o RELA entries → resolve symbols → fill in addresses
  If dynamic symbol → generate .rela.plt (resolved at runtime by ld.so)

Common Relocation Types

// x86-64 (AMD64):
R_X86_64_64:        Direct 64-bit absolute address
R_X86_64_PC32:      32-bit PC-relative offset (call/jmp target)
R_X86_64_PLT32:     Similar to PC32 but target is in the PLT
R_X86_64_GOTPCREL:  PC-relative offset to GOT entry
R_X86_64_COPY:      Copy variable from shared library to main program .bss

// ARM64 (AArch64):
R_AARCH64_ABS64:    Absolute address
R_AARCH64_ADR_PREL_PG_HI21: PC-relative, page granularity (ADR)
R_AARCH64_ADD_ABS_LO12_NC:  In-page offset (ADD)
R_AARCH64_LDST64_ABS_LO12_NC: Load/store, in-page offset
R_AARCH64_ADR_GOT_PAGE:       PC-relative to GOT entry, page granularity + load

ld Script: Linker Script

/* Default ld script: /usr/lib/ldscripts/elf_x86_64.x */

SECTIONS
{
    /* Start: Code segment (Read + Execute) */
    . = 0x400000 + SIZEOF_HEADERS;  /* Non-PIE */
    .text : { *(.text.unlikely .text.*_unlikely .text.unlikely.*)
              *(.text.exit .text.exit.*)
              *(.text.startup .text.startup.*)
              *(.text.hot .text.hot.*)
              *(.text .stub .text.* .gnu.linkonce.t.*) }

    /* Read-only data */
    .rodata : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
    . = ALIGN(0x1000);
    __init_array_start = .;
    .init_array : { *(.init_array) }
    __init_array_end = .;

    /* Read-write segment (starts on next page) */
    . = ALIGN(0x1000);
    .data : { *(.data .data.* .gnu.linkonce.d.*) }
    .bss : { *(.bss .bss.* .gnu.linkonce.b.*) }
}

LLD vs GNU ld vs gold

GNU ld (BFD)goldLLD (LLVM)
SpeedBaseline2-3x faster3-5x faster
MemoryHighLowerLow
LTOSupported (GCC LTO)Supported (GCC LTO)Supported (ThinLTO)
Linker ScriptsFull supportPartialMost
DefaultTraditionalUnmaintainedFuture

Linker Relaxation

Certain relocations can be optimized via "relaxation":
  Supported by RISC-V / ARM64:
    Example: R_RISCV_CALL → At link time, the target distance is visible → Use shorter instruction sequences
    → call (32-bit offset) → jal (20-bit offset) if distance is sufficient
    → Shorter + faster code

  x86 does not support relaxation (fixed instruction sizes, nothing to optimize)

Debugging

# View relocation table
readelf -r file.o

# View default ld script
ld --verbose | less

# Trace link process
ld -Map output.map -o prog *.o  # Write detailed map file
gcc -Wl,--trace -o prog *.o     # Print each linked file

# Undefined symbols
ld -o prog *.o 2>&1 | grep undefined

References

  • Documentation: info ld, ELF spec (System V ABI, Chapter 4)
  • Source Code: binutils/ld/, lld/ELF/
  • LWN: "Linker relaxation"

Keywords: relocation, R_X86_64_PLT32, R_AARCH64_ADR_GOT_PAGE, ld script, LLD, gold, linker relaxation