AArch64 ADRP, ADD, and LDR: Address Loading on Raspberry Pi¶
An AArch64 instruction cannot contain an arbitrary 64-bit address. Compilers and assemblers therefore build addresses from PC-relative pages, small offsets, literal pools, or Global Offset Table entries. On Raspberry Pi, the recurring sequences are ADRP plus ADD, ADRP plus LDR, and literal LDR.
This guide shows what each sequence calculates, which relocation the linker applies, and how to verify the result rather than guessing from disassembly.
Quick comparison¶
| Sequence | Result | Typical use |
|---|---|---|
adr x0, label |
Address of a nearby label | Local code or data within about ±1 MiB |
adrp + add |
Address of a symbol | Position-independent address within about ±4 GiB |
adrp + ldr |
Value stored at a symbol | Global data in the same linkage unit |
literal ldr x0, label |
Value at a nearby PC-relative location | Constants and literal pools within about ±1 MiB |
adrp :got: + ldr :got_lo12: |
Address loaded through the GOT | Preemptible or external symbols in PIC/PIE code |
The ranges are architectural addressing ranges, not promises that a final link will always place sections within them. The assembler emits relocations when a symbol's final address is not yet known, and the linker either resolves the pair or reports an overflow.
The page-and-offset model¶
ADRP computes a 4 KiB-aligned page address relative to the page containing the instruction. Its immediate represents a signed page delta. The following ADD supplies the symbol's low 12 address bits:
Conceptually:
After both instructions, x0 contains &object, not the value stored in object.
The GNU assembler documents :pg_hi21: for the ADRP relocation and #:lo12: for the following ADD, LDR, or STR. The :pg_hi21: prefix is optional in common GNU syntax, so adrp x0, object is sufficient.
Load an address with ADRP and ADD¶
Create addressing.s on a 64-bit Raspberry Pi OS system:
Assemble it without linking:
Inspect both instructions and unresolved relocations:
The exact formatting depends on the installed binutils version. Look for an ADRP page relocation such as R_AARCH64_ADR_PREL_PG_HI21 and an ADD low-12 relocation such as R_AARCH64_ADD_ABS_LO12_NC. The pair tells the linker that both instructions refer to the same symbol.
Load the value with ADRP and LDR¶
If you need the 64-bit value 42, not its address, combine the page address with an unsigned load offset:
Here the first instruction produces the page base. The second instruction reads memory at the symbol's scaled low-12 offset. For a 64-bit LDR, the object must have suitable alignment; .balign 8 makes that requirement explicit.
These forms answer different questions:
Literal LDR is different¶
In this instruction, label identifies the nearby memory containing the value:
This is a literal load with a signed PC-relative offset. It does not use a base register and does not mean “load the address of constant_value.”
Do not confuse the real instruction with the assembler pseudo-instruction:
The =expression form asks the assembler to choose an implementation. It may use a literal pool or synthesize the value with other instructions. Check the object code when the exact sequence matters.
ADR versus ADRP¶
ADR adds a signed byte displacement to the current PC and can directly produce the address of a nearby label. ADRP uses page granularity and normally needs a second instruction:
Use the relocation chosen by the compiler or the ABI-compatible sequence required by your linkage model. Replacing a pair manually because the current test binary happens to place a label nearby can create a range failure in a later build.
Position-independent access through the GOT¶
An external or preemptible symbol may be addressed through the Global Offset Table:
After the load, x16 contains the address recorded in the GOT entry. A further load is required to read the object itself:
For a function call through a GOT-resolved address:
Registers x16 and x17 are intra-procedure-call temporaries under AAPCS64 and are commonly used by linkers and veneers. Do not expect their values to survive a call.
See what C generates¶
Create inspect.c:
Build several variants:
Compare the relocation names as well as the mnemonics. PIC code may use the GOT because symbol interposition and shared-library linkage change how the final address is obtained.
Verify the final address in GDB¶
Link the assembly with a minimal C harness:
Useful GDB commands are:
Before the ADRP, record the PC. After it, x0 should be page-aligned. After the ADD, x0 should equal the symbol address shown by p &counter when the symbol is visible to GDB.
Common mistakes¶
Treating ADRP as a complete address¶
ADRP discards the symbol's offset within its 4 KiB page. Use the matching low-12 relocation in the next instruction.
Pairing relocations from different symbols¶
This is not a general way to combine arbitrary labels:
It works only by accident when both symbols have a compatible page relationship. Keep the high and low relocations paired to the same expression.
Reading an address when you wanted a value¶
ADRP plus ADD returns an address. Add LDR or use the ADRP plus LDR form to read memory.
Ignoring scaled load offsets¶
Unsigned LDR/STR immediate offsets are scaled by access size. Use the assembler relocation and correct alignment instead of calculating an unverified encoded immediate by hand.
Looking only at a linked disassembly¶
The final executable may hide the relocation that explained the original intent. Inspect both the relocatable object (objdump -dr, readelf -rW) and the linked file.
Assuming every compiler emits the same sequence¶
Optimization, visibility, PIE/PIC flags, code model, linker relaxation, and symbol preemption all affect address formation. Describe the sequence you actually observed and include the build command.
Reproducible inspection checklist¶
Record these details when reporting an address-loading problem:
Include the compiler flags and whether the failing file is an object, PIE executable, static executable, or shared library. An instruction without its relocation and linkage context is often ambiguous.
FAQ¶
Does ADRP use the current instruction address?¶
It uses the 4 KiB page containing the instruction as the PC-relative base, then applies a signed page delta.
Why is the low offset 12 bits?¶
A 4 KiB page contains 4096 bytes, so bits 0–11 select a byte position within that page.
Does LDR x0, label load the label address?¶
No. The literal form loads the value stored at a PC-relative memory location. Use ADR or ADRP plus ADD when you need an address.
Why does PIC code use the GOT?¶
The GOT provides runtime-resolved addresses for symbols whose final location or interposition cannot be represented as a simple local link-time address.
Can I hard-code the address shown by GDB?¶
Do not do so in normal Linux user-space code. ASLR, PIE, shared libraries, and link layout can change addresses between builds or executions.