Call relocation types
Most architectures encode direct branch/call instructions with aPC-relative displacement. This post discusses a specific category ofbranch relocations: those used for direct function calls and tail calls.Some architectures use two ELF relocation types for a callinstruction:
1 |
# i386, x86-64 |
This post describes why I think this happened.
Static linking: one typesuffices
In the static linking model, all symbols are resolved at link time:every symbol is either defined in a relocatable object file or anundefined weak symbol. A branch instruction with a PC-relativedisplacement—x86 call, m68k bsr.l, s390brasl—can reuse the same PC-relative data relocation typeused for data references.
- i386:
R_386_PC32for bothcall fooand.long foo - . - x86-64:
R_X86_64_PC32for bothcall fooand.long foo - . - m68k:
R_68K_PC32forbsr.l foo,move.l var,%d0, and.long foo - . - s390x:
R_390_PC32DBLforbrasl %r14, foo,larl %r1, var, and.long foo - .
No separate "call" relocation type is needed. The linker simplypatches the displacement to point to the symbol address.
Dynamic linking changes thepicture
With System V Release 4 style shared libraries, variable access andfunction calls diverge.
For variables and function addresses, a referencefrom one component to a symbol defined in another cannot use a plainPC-relative relocation, because the distance between the two componentsis not known at link time. The -fno-pic relocatable files.) To satisfy the pointerequality requirement, a PC-relative data relocation in anexecutable must resolve to the same address as its counterpart in ashared object—this is why GOT indirection is used for symbols not knownat compile time to be preemptible.
For direct function calls, the situation isdifferent. A call instruction has "transfer control there by any means"semantics - the caller usually doesn't care how the callee isreached, only that it gets there. This allows the linker to interpose aPLTstub when the target is in another component, without any specialcode sequence at the call site. Alternatively, some architecturessupport an indirect call sequence that bypasses PLT entirely: -fno-plton x86 and -mno-plton MIPS (o32/n32 non-PIC).
Variable accesses do not have the same semantics - so the PC-relativedata relocation type cannot be reused on a call instruction.
This is why separate branch relocation types were introduced:R_386_PLT32, R_68K_PLT32,R_390_PLT32DBL, and so on. The relocation type carries thesemantic information: "this is a function call that can use PLTindirection."
Misleading names
The @plt notation in assembly and the PLT32relocation type names are misleading. They suggest that a PLT entry isinvolved, but that is often not the case - when the callee is defined inthe same component, the linker resolves the branch directly—no PLT entryis created.
R_386_CALL32 and R_X86_64_CALL32 would havebeen a better name.
In addition, the @plt notation itself is problematic asa
Architecture comparison
Single type (clean design). Some architecturesrecognized from the start that one call relocation type is sufficient.The linker can decide whether a PLT stub is needed based on the symbol'sbinding and visibility.
- AArch64:
R_AARCH64_CALL26forblandR_AARCH64_JUMP26forb. - PowerPC64 ELFv2:
R_PPC64_REL24forbl.
These architectures never had the naming confusion—there is no "PLT"in the relocation name, and no redundant pair.
Redundant pairs (misguided). Some architecturesintroduced separate "PLT" and "non-PLT" call relocation types, creatinga distinction without a real difference.
- SPARC:
R_SPARC_WPLT30alongsideR_SPARC_WDISP30. The assembler decides at assembly timebased on PIC mode and symbol preemptivity, when ideally the linkershould make these decisions. - PPC32:
R_PPC_REL24(non-PIC) andR_PPC_PLTREL24(PIC) have genuinely different semantics(the addend ofR_PPC_PLTREL24encodes the r30 GOT pointersetup). However,R_PPC_LOCAL24PCis entirely useless—alloccurrences can be replaced withR_PPC_REL24. - RISC-V:
R_RISCV_CALL_PLTalongside the now-removedR_RISCV_CALL. The community recognized that only onerelocation is needed.R_RISCV_CALL_PLTis kept (despite thename, does not mandate a PLT entry).
x86-64 started with R_X86_64_PC32 forcall foo (inherited from the static-linking mindset) andR_X86_64_PLT32 for call foo@plt (symbols notcompile-time known to be non-preemptible). In 2018, binutils R_X86_64_PLT32 for call foo. LLVMintegrated assembler followed suit.
This means R_X86_64_PC32 is now effectively reserved fordata references, and R_X86_64_PLT32 marks all calls—a cleanseparation achieved by convention.
However, GNU Assembler still produces R_X86_64_PC32 whencall foo references an STB_LOCAL symbol. I'vesent a patch to fix this:
GCC's s390 port seems to always generate @plt (even forhidden visibility functions), leading to R_390_PLT32DBLrelocations.
Range extension thunks
When a branch target is out of range, some architectures allow thelinker to insert a
On x86-64, the ±2GiB range of call/jmp hasbeen sufficient so far, but as executables grow, R_X86_64_PC32would not be suitable (due to pointer equality requirement), makingconsistent use of R_X86_64_PLT32 for calls all the moreimportant.
Recommendation forfuture architectures
For a specific instruction or pseudo instruction for function callsand tail calls, use a single call relocation type—no "PLT" vs. "non-PLT"distinction. The assembler should emit the same relocation, and thelinker, which knows whether the symbol is preemptible, decides whether aPLT stub is needed. Optionally enable range extension thunks for therelocation type. AArch64's R_AARCH64_CALL26 and PowerPC64ELFv2's R_PPC64_REL24 demonstrate this approach well.
This discussion does not apply to intra-function branches, whichtarget local labels.
See also
- Allabout Procedure Linkage Table for how PLT works
- All aboutGlobal Offset Table for the data-access counterpart
Copyrelocations, canonical PLT entries and protected visibility for thevariable-access complications Relocationgeneration in assemblers for how assemblers decide which relocationsto emit Longbranches in compilers, assemblers, and linkers for range extensionthunks