AArch64 ADRP, ADD, ADR, and LDR Literal Explained¶
On 64-bit Raspberry Pi, ADR, ADRP, ADD, and LDR often appear together because A64 instructions are fixed at 32 bits and cannot embed an arbitrary 64-bit address directly. The correct sequence depends on whether you need an address, the value stored at that address, or an entry from the Global Offset Table.
Quick comparison¶
| Form | Result | Approximate PC-relative range | Typical use |
|---|---|---|---|
adr x0, symbol |
Address of symbol |
±1 MiB | Nearby code or data |
adrp x0, symbol |
4 KiB page address containing symbol |
±4 GiB | First half of a page-relative address |
add x0, x0, :lo12:symbol |
Adds offset within the 4 KiB page | 0–4095 | Completes ADRP address calculation |
ldr x0, symbol |
64-bit value stored near symbol |
±1 MiB | Literal load from nearby data |
ldr x0, =symbol |
Address/value selected by assembler pseudo-instruction | Depends on expansion | Convenient literal-pool syntax |
The most common address sequence is:
The first instruction computes the page base. The second adds the symbol's low 12-bit offset.
Address versus value¶
This distinction prevents most mistakes.
Given:
Load the address of counter:
Load the 64-bit value stored at counter:
After the first sequence, x0 == &counter. After the second, x1 == counter, which is 42.
How ADR works¶
ADR adds a signed PC-relative immediate to the address of the instruction and writes the result to a general-purpose register:
Use it when the linker can place the target within approximately ±1 MiB. It produces the address; it does not dereference memory.
Whether this links depends on final section placement. A small source file does not guarantee that .text and .rodata remain within range in the final executable.
How ADRP works¶
ADRP is page-relative. Conceptually:
Both the current address and target are considered at 4 KiB page granularity. This provides an approximate ±4 GiB range.
At this point, x0 contains the base of the 4 KiB page holding message, not the exact address of message.
Complete the address with:
The assembler emits relocations that allow the linker to fill in the page delta and low 12-bit offset.
ADRP + ADD for an address¶
Complete example callable from C:
x0 is the AAPCS64 integer/pointer return register, so C can declare:
Build on a 64-bit Raspberry Pi:
ADRP + LDR for a value¶
When the target is data and you want its contents:
The LDR addressing mode adds the relocated low offset to the page base and loads memory. It does not leave the address in x0; it replaces x0 with the 64-bit value.
Store a new value with the same address construction:
LDR literal is different¶
This syntax:
is an A64 literal load. It uses a PC-relative address and loads the 64-bit contents at the label. Its signed scaled immediate provides approximately ±1 MiB of range.
This returns 42, not the address of constant_42.
LDR x0, =expression is a pseudo-instruction¶
An equals sign changes the meaning:
GNU as treats these as pseudo-instructions. It may place a value in a literal pool and emit a PC-relative load, or choose another valid expansion. The exact machine instructions are not guaranteed by the source spelling.
Inspect the result:
Use .ltorg only when you understand literal-pool placement. A literal pool placed outside the instruction's range causes an assembly or link failure.
Position-independent code and the GOT¶
For a symbol that can be interposed or whose address must come through the Global Offset Table, compilers commonly use a GOT sequence:
This loads the address stored in the GOT entry. A further LDR is needed if you want the value at that external address.
Do not replace a compiler-generated GOT sequence with direct ADRP + ADD unless symbol visibility and the linking model make that transformation valid.
See what GCC generates¶
Create a small C file:
Compile position-independent assembly:
Then compare with non-PIC output:
Compiler output varies by GCC version, optimisation, code model, visibility, and linker relaxation. Record those conditions when explaining a sequence.
Inspect relocations before and after linking¶
Object-file disassembly alone can show zero or placeholder immediates because the linker has not applied relocations.
Typical relocation names include page-high and low-12 forms. Exact names depend on the assembler syntax and whether the access is direct or through the GOT.
The linker may relax an ADRP + ADD sequence when the final target is close enough. Judge the linked executable, not only the relocatable object.
Common mistakes¶
Using ADRP as though it returned the exact address¶
Wrong:
x0 points to the page base. Add :lo12:message before passing it to puts.
Confusing ldr x0, label with ldr x0, =label¶
- Without
=, literalLDRloads memory at the label. - With
=, the assembler pseudo-instruction materialises the expression, commonly through a literal pool.
Always inspect disassembly when teaching or optimising the distinction.
Loading 32 bits when the data is 64 bits¶
ldr w0, [...] loads 32 bits and zero-extends into x0. Use ldr x0, [...] for a 64-bit object.
Ignoring symbol visibility¶
Direct access may be invalid for an interposable external symbol in position-independent code. Use the compiler's GOT sequence or constrain visibility deliberately.
Copying macOS syntax into GNU/Linux¶
Mach-O, ELF, GNU as, and Clang integrated assembler use related but not identical relocation spellings. This guide targets GNU/Linux ELF on 64-bit Raspberry Pi OS.
Range summary¶
| Instruction | Immediate model | Effective range |
|---|---|---|
ADR |
Signed 21-bit byte offset | approximately ±1 MiB |
ADRP |
Signed 21-bit page offset, scaled by 4096 | approximately ±4 GiB |
LDR literal |
Signed 19-bit offset, scaled by 4 | approximately ±1 MiB |
ADD :lo12: |
Unsigned offset within page | 0–4095 bytes |
Arm's A64 overview documents the PC-relative forms and their ranges. For calling convention details, use the current AAPCS64 specification.
FAQ¶
Why does AArch64 use ADRP followed by ADD?¶
Together they construct a PC-relative address across a wide range while keeping each instruction 32 bits. ADRP finds the target page and ADD supplies the offset within it.
When should I use ADR instead?¶
Use ADR when the target is guaranteed to remain within its smaller range and the linking model permits direct access. The linker may also relax a longer sequence automatically.
Does LDR literal load an address?¶
It loads data stored at a PC-relative location. The pseudo-instruction LDR x0, =symbol is different and may arrange for the symbol address to be stored in a literal pool.
Why does objdump show different instructions after linking?¶
Relocations are resolved and linker relaxation can replace a sequence with a shorter equivalent. Compare both objdump -dr file.o and objdump -d executable.