Branch instructions on most architectures use PC-relative addressing with a limited range. When the target is too far away, the branch becomes "out of range" and requires special handling.
Consider a large binary where main() at address 0x10000 calls foo() at address 0x8010000-over 128MiB away. On AArch64, the bl instruction can only reach ±128MiB, so this call cannot be encoded directly. Without proper handling, the linker would fail with an error like "relocation out of range." The toolchain must handle this transparently to produce correct executables.
This article explores how compilers, assemblers, and linkers work together to solve the long branch problem.
Compiler (IR to assembly): Handles branches within a function that exceed the range of conditional branch instructions
Assembler (assembly to relocatable file): Handles branches within a section where the distance is known at assembly time
Linker: Handles cross-section and cross-object branches discovered during final layout
Branch range limitations
Different architectures have different branch range limitations. Here's a quick comparison of unconditional branch/call ranges:
Architecture Unconditional Branch Conditional Branch Notes AArch64 ±128MiB ±1MiB Range extension thunks AArch32 (A32) ±32MiB ±32MiB Range extension and interworking veneers AArch32 (T32) ±16MiB ±1MiB Thumb has shorter ranges PowerPC64 ±32MiB ±32KiB Range extension and TOC/NOTOC interworking thunks RISC-V ±1MiB ( jal ) ±4KiB Linker relaxation x86-64 ±2GiB ±2GiB Code models or thunk extension
The following subsections provide detailed per-architecture information, including relocation types relevant for linker implementation.
... continue reading