Skip to content

Addresses and Relocations

A symbol's address and the value stored at that symbol are different results. Inspect relocations before linking, then check both results from C.

Before you start

Complete memory and arrays. You can distinguish a pointer from a pointed-to value and build a C/assembly executable.

Outcome: explain a page-plus-offset address and locate the linker work in an object file. Run in a fresh directory on the Pi.

Choose the address operation

Form Result
adr x0, symbol Nearby PC-relative byte address
adrp x0, symbol PC-relative 4 KiB page base
add x0, x0, :lo12:symbol Add the low page offset
ldr x0, [x0] Read eight bytes from an already formed address
ldr x0, symbol Literal load of the value at a nearby label
ldr x0, =symbol GNU pseudo-instruction requesting an address constant

ADR has a signed 21-bit byte displacement; ADRP uses a signed 21-bit displacement scaled by 4096. The 4 KiB unit is an instruction encoding unit, not a promise about the Linux system page size. Range and relocation validity still depend on the linked layout.

Build the address lab

The data symbol below is local to the assembly file, so the linker can bind it directly in this PIE executable. This is not a template for interposable symbols in arbitrary shared libraries.

// file: addresses.S
.section .rodata
.p2align 3
local_value:
    .quad 42

.text
.global value_address
.type value_address, %function
value_address:
    adrp x0, local_value
    add x0, x0, :lo12:local_value
    ret
.size value_address, .-value_address

.global read_value
.type read_value, %function
read_value:
    adrp x0, local_value
    add x0, x0, :lo12:local_value
    ldr x0, [x0]
    ret
.size read_value, .-read_value

.global literal_value
.type literal_value, %function
literal_value:
    ldr x0, local_value
    ret
.size literal_value, .-literal_value
.section .note.GNU-stack,"",%progbits
/* file: addresses.c */
#include <stdint.h>
#include <stdio.h>
extern const uint64_t *value_address(void);
extern uint64_t read_value(void);
extern uint64_t literal_value(void);
int main(void)
{
    const uint64_t *p = value_address();
    if (*p != 42 || read_value() != 42 || literal_value() != 42) return 1;
    puts("PASS addresses: pointer, dereference and literal value");
    return 0;
}
1
2
3
4
5
6
gcc -g -c addresses.S -o addresses.o
objdump -dr addresses.o
readelf -rW addresses.o
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -g -fPIE -pie addresses.c addresses.o -o addresses
./addresses
objdump -d addresses | sed -n '/<value_address>:/,+5p'
PASS addresses: pointer, dereference and literal value

Before linking, look for relocations including R_AARCH64_ADR_PREL_PG_HI21 and R_AARCH64_ADD_ABS_LO12_NC. The linked disassembly has resolved locations; a linker may relax a sequence. Do not require every tool version to display identical addresses or aliases.

Why a plausible address can be wrong

ADRP alone omits the low offset. An offset from a different symbol forms the wrong address. A literal load returns the stored bytes, not their address. An absolute pointer copied from yesterday's GDB output is not a symbol reference.

For a preemptible external object in a shared library, the compiler may use a GOT entry instead of directly binding the object's page. Compare gcc -S -fPIC output for an extern variable and inspect relocations; do not paste a GOT sequence into this local-symbol lab without identifying which pointer it loads.

If linking reports an out-of-range relocation, investigate symbol visibility and layout. Do not suppress the error with arbitrary address constants. See GNU AArch64 relocations for assembler notation.

Check your understanding

If value_address returned 42 rather than a pointer, why would the caller's dereference be invalid?

Compare your reasoning

The caller expects a readable address. The integer 42 is the contents, not the location of that contents; dereferencing it violates the contract.

Continue the course

Previous: Memory, Arrays and Loads · Course roadmap · Next: Functions, Stack and the ABI

Donate