The linker has two related tasks: arranging layout and patching references. Scripts determine how input sections enter the output and where they are located, while relocation records determine what value should be filled into a specific instruction or data field. File positions, runtime addresses, and load-time copy locations must be discussed separately.
What Relocation Records Carry
ELF relocations specify at least the location to be patched, the type, and the associated symbol. The RELA form explicitly stores the addend, while REL provides it implicitly via the field being patched. Which form is used depends on the target ABI; you cannot write all platforms using the same RELA format.
Let S be the symbol address, A be the addend, and P be the relocation field position. Typical x86-64 formulas include:
Type
Main Calculation
Explanation
R_X86_64_64
S + A
Write 64-bit absolute value
R_X86_64_PC32
S + A - P
Write 32-bit PC-relative value with range check
R_X86_64_PLT32
L + A - P
L is the PLT entry; can bind directly to the target if conditions are met
R_X86_64_GOTPCREL
GOT entry address + A - P
Get the entry location storing the target address
For a 5-byte relative call, the displacement field is located 1 byte after the start of the instruction, and the CPU uses the address of the next instruction. A common A=-4 in RELA adjusts P to this baseline. For example, if the instruction is at 0x1000, P=0x1001, and the target is at 0x1100, the displacement is 0x1100 - 4 - 0x1001 = 0xfb; at execution time 0x1005 + 0xfb = 0x1100. See Linking and Loading for a complete linking example.
PLT32 does not necessarily mean the final call goes through the PLT: when the target can be bound directly, the linker can resolve it to a direct call. Dynamic linking is also not "all unresolved references go into .rela.plt"; data, GOT, TLS, etc., use corresponding dynamic relocations, and some references are resolved during static linking. AMD64 ABI
From Placeholders in .o to Final Displacement
The following experiment targets x86-64 ELF using GNU-style assembly. Save caller.s in a separate directory:
.text
.globl caller
caller:
call callee
ret
.section .note.GNU-stack,"",@progbits
Then save callee.s:
.text
.globl callee
callee:
ret
.section .note.GNU-stack,"",@progbits
layout.elf is only for observing layout; do not execute it directly: the ret in caller is not a valid process start/exit protocol. The displacement field in the input caller.o is at offset 1 in .text, with a common record of R_X86_64_PLT32 callee - 4. In this experiment, callee can be bound directly, following the 6-byte code of caller in input order, resulting in a final address of 0x1006.
The final call displacement is 1, corresponding to little-endian bytes 01 00 00 00. When reading the .o file, offset 1 is relative to the position within that input section; only after final linking can P=0x1001 be determined in conjunction with the output layout. Do not unconditionally treat the input relocation table's offset as a process virtual address.
If the final target distance exceeds the signed displacement range, the linker must handle it according to the target mechanism or report an error; it cannot simply truncate high bits to "force an executable." Changing the code model, layout, or choosing an appropriate indirect sequence is the semantically correct fix.
Which four bytes does A=-4 account for?
The default target yields 0xfb, encoded fb 00 00 00. Move the target backward to see two’s-complement displacement. Then move the whole block: S and P shift together, leaving D unchanged. This invariant explains position-relative references.
D=S+A−P,A=−4,RIPnext=P+4,RIPnext+D=S
Preparing the visual
Which four bytes does A=-4 account for?
P points to the four-byte displacement field while CPU RIP-relative execution uses the address after it. A=-4 connects the bases; common translation leaves displacement unchanged.
AArch64: Page Address and Page Offset Coordination
AArch64's ADRP constructs addresses at page granularity, followed by ADD or load instructions handling the page-internal part. R_AARCH64_ADR_PREL_PG_HI21 corresponds to the page-relative calculation of ADRP, not mixing ADR and ADRP into the same instruction.
R_AARCH64_ADD_ABS_LO12_NC provides the lower 12 bits; R_AARCH64_LDST64_ABS_LO12_NC corresponds to the scaled immediate number for 64-bit loads or stores; do not ignore alignment and scaling. R_AARCH64_ADR_GOT_PAGE only handles the GOT page-related part; usually, a matching load low-bit relocation is also needed. It is not completed by a single record for "page address plus load." Arm ELF ABI
How do ADRP and ADD cross a page boundary?
The default target is 0x1fff with low bits 0xfff. One byte forward to 0x2000 resets the low part while ADRP’s page base increases by 0x1000. This page is ADRP’s fixed address granule; other OS page sizes do not change those 12 bits.
ADRP forms a page-relative base, ADD supplies low 12 bits. Across a boundary the low part resets and page delta grows while the final address stays continuous.
The output section name itself does not determine runtime page permissions; program headers, attributes, page alignment, and linker layout together form the loading view. When checking layout, look at both readelf -SW and readelf -lW; do not assume read/write/execute permissions are correct just because you see .text and .data.
The position counter . represents the current output position, ALIGN adjusts alignment, and wildcard patterns collect input sections. *(.text) does not automatically include .text.foo; when enabling per-function sections, .text.* is usually also needed.
The complete default script also handles startup arrays, exception unwinding, TLS, dynamic info, etc. Do not directly replace the default layout for ordinary Linux programs with a teaching script containing only .text/.data/.bss. First, run ld --verbose to view the default script for the current target; do not assume a fixed installation path. GNU ld Scripts
Adding a Custom Section to the Default Layout
The following GNU ld extension script preserves the default layout, only inserting application metadata. Save as extra.ld:
KEEP prevents matched sections from being deleted by linker garbage collection, but it does not guarantee the compiler will generate the original variable, so the source code also uses the used attribute. Save as meta.c:
INSERT causes this script to enhance the default SECTIONS, rather than the behavior of a normal -T replacing the entire default script. The two exported symbols describe addresses but do not automatically allocate C variable storage; when using them in C, declare and take addresses according to linker symbol conventions. GNU ld INSERT
This example applies to the mentioned GNU ld usage; switching linkers requires checking support details and artifacts. LLD is compatible with many GNU script behaviors but has explicit implementation differences. LLD Script Documentation
Three Different Hurdles: Generating Variables, Extracting Archives, Preserving Sections
used, archive extraction, and KEEP occur at different stages. used requires the compiler to preserve the definition, while KEEP protects matched sections that have already entered the linking process; if the definition is hidden in a static archive, that member has not yet been extracted, and KEEP cannot replace extraction conditions.
Prepare registry.c separately, containing only the metadata definition; prepare entry.c as the application entry:
The first application has no undefined references to app_version, so archive members are generally not extracted. The second uses -u to introduce symbol requirements, extracting registry.o from the archive, which is then preserved by KEEP for the metadata section. Large plugin registries also encounter such issues; --whole-archive can expand the extraction scope, but you should promptly limit the scope with --no-whole-archive to avoid pulling in all unrelated archives. GNU ld Options
You can also copy extra.ld to drop.ld, simply removing the KEEP wrapper, directly linking registry.o and entry.c with --gc-sections. In this case, the compiler indeed generated the variable, and the object is already input to the linker, but .appmeta without reachable references can still be garbage collected; this third situation cannot be fixed by used.
VMA and LMA: Runtime Location Does Not Necessarily Equal Load Source
Embedded firmware may store initialization data in Flash and copy it to RAM during startup. The VMA of this data section is the runtime address, while the LMA is the load address of the initial content, with the relationship specified via AT or AT>. Scripts only describe layout; startup code still needs to execute the copy and zero-initialize .bss.
Linux ELF user programs mainly complete mapping via program headers; do not directly apply the bare-metal Flash/RAM copy model to dynamic loaders. NOLOAD, SHT_NOBITS, file size, and memory size should also be checked separately according to the tool and loading model. GNU ld: Output Section LMA
Understanding Initialization Copy via Address Accounting
Assume a .data section has a runtime address (VMA) of 0x20000000, with initial content in Flash at LMA 0x08004000, size 16 bytes. After power-on, variables are accessed using the former address; startup code should copy the 16 bytes starting from the latter address to the former. If you only change the VMA to RAM without corresponding initial content and copy actions, variables will not automatically get their compile-time initial values.
Object
Requires Initial Bytes in Image
Startup Phase Action
Initialized .data
Yes
Copy from load source to runtime location
Zero-initialized .bss
Usually represented by NOBITS, does not save equal-sized zero bytes
Zero-initialize according to target runtime model
.text / Read-only constants
Yes
Execute/read in place, or copy as required by platform
Scripts can export these boundaries using LOADADDR, ADDR, SIZEOF, and startup code uses them according to target conventions. For ordinary Linux ELF, check how PT_LOAD's p_filesz and p_memsz define file and memory ranges; dynamic loaders do not universally execute this Flash/RAM copy table. Load Address of Output Sections
Relaxation: Rewriting Instruction Sequences After Final Layout is Known
Link relaxation uses final distances, symbol bindings, and relocation types to replace certain sequences with equivalent and more suitable ones. For example, when the target can be bound directly, some GOT accesses in x86-64 can be relaxed into direct address calculations; RISC-V far-call sequences can be shortened when distances are appropriate.
x86 instructions are variable-length encoded; it is not "fixed size so no relaxation." Whether relaxation is possible depends on the target ABI, relocation markers, linker support, and options; a reduction in instruction count does not guarantee the entire program will be faster.
First confirm which symbol is referenced and which file provides the definition, then check input order, visibility, version, archive extraction, and layout. Relocation overflow usually means the encoding range or code model is insufficient; do not arbitrarily truncate results. If symbols are resolved but misaligned at runtime, also check ABI, PIE, loading mappings, and dynamic relocations.
When choosing GNU ld, LLD, or other linkers, base your decision on the target platform, scripts, LTO format, diagnostics, and actual link resource measurements, avoiding fixed conclusions like "several times faster."