Assembly (x86-64, AArch64, RISC-V) Module 4 – Moving data: registers, memory and addresses
Static data and PC-relative addresses
Place data in .rodata, .data and .bss, and form its address position-independently on x86-64, AArch64 and RISC-V. Fix three classic address errors.
What you will learn
- Choose .rodata, .data or .bss for a constant, a variable or a zero-filled buffer
- Explain how RIP-relative lea, adrp/add and auipc/addi reach data from any load address
- Diagnose and fix three classic address errors (NASM DEFAULT ABS, macOS text relocations, RISC-V gp relaxation)
Before you start
What you’ll learn · Before you start
What you will learn
- Choose .rodata, .data or .bss for a constant, a variable or a zero-filled buffer
- Explain how RIP-relative lea, adrp/add and auipc/addi reach data from any load address
- Diagnose and fix three classic address errors (NASM DEFAULT ABS, macOS text relocations, RISC-V gp relaxation)
Before you start
On this page
A string literal or a global variable is not in a register; it sits in the program’s static data, and the code must
form its address at run time. Modern executables are position-independent code: Code that reaches its data and other code relative to the program counter rather than by absolute address, so the loader may place it at any base address. Formed with RIP-relative operands on x86-64, adrp/add on AArch64 and auipc/addi on RISC-V.,
so the loader may place them anywhere, and the code can no longer hard-code where its data lives. Instead it computes
each address relative to the program counter, and the three ISAs do so with lea [rip + x], adrp/add and
auipc/addi.
This lesson places data in the right section with the right alignment, forms a data address position-independently,
and explains three address errors that cost real time: NASM’s deprecated DEFAULT ABS, the macOS text-relocation
refusal, and the RISC-V global-pointer relaxation that can crash a startup which never set gp. The AArch64 program is
built and run, and returns 42; the NASM sessions show the deprecation warning and the clean, RIP-relative fix.
Where static data lives: .rodata, .data and .bss
The assembler puts your data into a named section: A named region of an object or executable file, such as .text, .rodata, .data or .bss. The linker merges matching sections across objects and the loader groups them into segments at run time. More in “From source to executable: objects and linking”, and the section decides the data’s
permissions and whether it takes space in the file. A directive: A line that gives an order to the assembler instead of naming a processor instruction: switch section, export a symbol, put bytes into the data, define a constant, pad to a boundary. GNU as directives start with a dot (.data, .globl, .byte); NASM writes section, global, db and equ. More in “Assembler syntax: NASM, GNU as, Intel and AT&T” such as .section,
.data or .p2align chooses the section and its alignment.
| Section | Holds | Writable | In the file? |
|---|---|---|---|
.rodata (__TEXT,__const on Mach-O) |
constants, string literals | no | yes |
.data |
initialised globals | yes | yes |
.bss |
zero-initialised globals | yes | no (allocated and zeroed by the loader) |
The last row is the useful surprise: .bss: The section that holds writable data initialised to zero. It records only a size in the executable and takes no file space; the loader allocates and zeroes its pages when the program starts. takes no room in the executable. A one-million-entry
zeroed array adds a size field to the file, not a million zero bytes; the loader allocates and zeroes the pages when
the program starts. Alignment still matters: a directive like .p2align 4 (16-byte alignment) puts data where a
vector load can reach it, which the previous lesson showed some hardware requires.
Reaching data relative to the program counter
With position-independent code the address of a global is not known until the loader places the program, so the code
builds it from the current PC. x86-64 writes lea rsi, [rip + msg], a single instruction. AArch64 needs two: adrp
loads the base of the 4 KiB page that holds the symbol, and add with @PAGEOFF adds the offset within the page.
RISC-V also uses a pair: auipc adds a 20-bit PC-relative upper immediate, and addi adds the low 12 bits.
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- Start from the PC. The CPU knows where the current instruction is, but not where
valueended up. Position-independent code works out the address from the PC. - adrp finds the page. adrp loads the base of the 4 KiB page that holds
value, measured relative to the PC. It gives the page, not the exact address. - add finishes the address. add with value@PAGEOFF adds the offset of
valuewithin its page, so x0 now holds the complete runtime address. - ldr reads the value. With the address in x0, ldr loads the 64-bit value. Nothing in the file hard-codes where value lives, so the loader may place the program anywhere.
The AArch64 program below puts 42 in .data, forms its address with adrp/add, loads it and returns it. The
disassembly shows exactly the three instructions the figure describes, and the run returns 42.
// Put a value in writable static data, form its address position-independently with
// adrp + add, load it and return it. adrp loads the 4 KiB page base relative to the PC;
// add with @PAGEOFF adds the offset within the page. The value 42 becomes the exit status.
.section __DATA,__data
.p2align 3
value:
.quad 42
.text
.globl _main
.p2align 2
_main:
adrp x0, value@PAGE // page base of `value`, PC-relative
add x0, x0, value@PAGEOFF // add the offset inside the page -> full address
ldr x0, [x0] // load the 64-bit value
ret // exit status = low byte = 42 """Form a static-data address the right way (adrp + add) and the wrong way (ldr =label) on macOS.
The adrp/add program runs and returns 42 as its exit status. The ldr =value program is pre-assembled to a fixed
object, then linked; the link fails with a text-relocation error, which macOS raises for absolute addresses in code.
The sed filter trims the object's directory from the message so only its name shows.
"""
import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session
session.run(
"""
# Build and run the position-independent version; it loads 42 and returns it.
clang -o aarch64/adrp_add aarch64/adrp_add.s
./aarch64/adrp_add; echo "exit=$?"
# The three instructions: adrp for the page, add for the offset, ldr for the value.
/opt/homebrew/opt/llvm/bin/llvm-objdump -d aarch64/adrp_add | sed -n '/<_main>:/,/ret/p'
# The wrong way: assemble the ldr =value version, then try to link it.
clang -c aarch64/ldr_literal.s -o aarch64/ldr_literal.o
clang -o aarch64/ldr_literal aarch64/ldr_literal.o 2>&1 | sed 's#(/[^)]*/#(#'
"""
) Output
# Build and run the position-independent version; it loads 42 and returns it. $ clang -o aarch64/adrp_add aarch64/adrp_add.s $ ./aarch64/adrp_add; echo "exit=$?" exit=42 # The three instructions: adrp for the page, add for the offset, ldr for the value. $ /opt/homebrew/opt/llvm/bin/llvm-objdump -d aarch64/adrp_add | sed -n '/<_main>:/,/ret/p' 0000000100000370 <_main>: 100000370: 90000020 adrp x0, 0x100004000 <value> 100000374: 91000000 add x0, x0, #0x0 100000378: f9400000 ldr x0, [x0] 10000037c: d65f03c0 ret # The wrong way: assemble the ldr =value version, then try to link it. $ clang -c aarch64/ldr_literal.s -o aarch64/ldr_literal.o $ clang -o aarch64/ldr_literal aarch64/ldr_literal.o 2>&1 | sed 's#(/[^)]*/#(#' Illegal text-relocations: text-relocation in '_main'+0x10 (ldr_literal.o) to 'value' ld: Found illegal text-relocations clang: error: linker command failed with exit code 1 (use -v to see invocation)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 aarch64_static_session.py
RISC-V’s lla pseudo-instruction expands to the same kind of pair. The disassembly shows auipc followed by an
addi (printed as mv while its offset is still zero), each with its relocation, plus the R_RISCV_RELAX markers
that let the linker shrink the pair when it can.
# lla (load local address) is a pseudo-instruction that expands to a PC-relative auipc +
# addi pair. GNU ld may relax such a pair to a single gp-relative addi when
# __global_pointer$ is in range, which is why startup code must load gp first (or the
# access faults). The linked program runs on a riscv64 Linux machine (or under qemu-user).
.section .data
.p2align 3
value:
.dword 42
.text
.globl load_value
load_value:
lla a0, value # -> auipc a0, %pcrel_hi(value); addi a0, a0, %pcrel_lo
ld a0, 0(a0)
ret """Assemble the RISC-V lla source and show the PC-relative auipc + addi pair with its relocations.
lla expands to auipc + addi; the R_RISCV_PCREL_HI20 and R_RISCV_PCREL_LO12_I relocations carry the two halves of the
offset, and the R_RISCV_RELAX markers let GNU ld shrink the pair to one gp-relative addi. The driver stops at the
disassembly; the linked program would run on a riscv64 Linux machine (or under qemu-user on another host).
"""
import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session
session.run(
"""
# Assemble the RISC-V source into an ELF object for rv64gc.
/opt/homebrew/opt/llvm/bin/clang -c -target riscv64-linux-gnu -march=rv64gc riscv/la.s -o riscv/la.o
# Disassemble with relocations: the auipc + addi pair and the RELAX markers that enable gp relaxation.
/opt/homebrew/opt/llvm/bin/llvm-objdump -d -r riscv/la.o
"""
) Output
# Assemble the RISC-V source into an ELF object for rv64gc.
$ /opt/homebrew/opt/llvm/bin/clang -c -target riscv64-linux-gnu -march=rv64gc riscv/la.s -o riscv/la.o
# Disassemble with relocations: the auipc + addi pair and the RELAX markers that enable gp relaxation.
$ /opt/homebrew/opt/llvm/bin/llvm-objdump -d -r riscv/la.o
riscv/la.o: file format elf64-littleriscv
Disassembly of section .text:
0000000000000000 <load_value>:
0: 00000517 auipc a0, 0x0
0000000000000000: R_RISCV_PCREL_HI20 value
0000000000000000: R_RISCV_RELAX *ABS*
4: 00050513 mv a0, a0
0000000000000004: R_RISCV_PCREL_LO12_I .Lpcrel_hi0
0000000000000004: R_RISCV_RELAX *ABS*
8: 6108 ld a0, 0x0(a0)
a: 8082 ret
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 riscv_la_session.py
Key idea
Checkpoint
Check: Where static data lives · PC-relative addressing
Three address errors, and their fixes
Three mistakes around addresses are common enough to recognise on sight. The first two appear in the sessions above
and below; the third follows from the RISC-V psABI’s rule for gp.
NASM’s implicit DEFAULT ABS. In 64-bit mode, NASM still defaults [label] to an absolute address unless told
otherwise. NASM 3.x deprecates that default: the session shows absrel.asm earning implicit DEFAULT ABS is deprecated, while absrel_fixed.asm
with a default rel line assembles clean and disassembles to a RIP-relative mov. The absolute form also cannot be
linked into a position-independent executable: it needs an R_X86_64_32S relocation, which stores the address in 32
signed bits, and a PIE may be loaded anywhere in the 64-bit space, so the linker refuses the relocation outright. The
default rel line fixes both the warning and the link.
; 64-bit NASM defaults to absolute addressing for [label], which NASM 3.x deprecates
; and which fails to link as a position-independent executable.
section .data
value: dq 42
section .text
global _start
_start:
mov rax, [value] ; absolute [label] -> DEFAULT ABS warning, non-PIE relocation
ret ; The fix: `default rel` makes [label] RIP-relative. No DEFAULT ABS warning, and the
; object links as a position-independent executable.
default rel
section .data
value: dq 42
section .text
global _start
_start:
mov rax, [value] ; RIP-relative now: lea-style PC-relative access
ret """Assemble the absolute and the fixed x86-64 NASM sources and show the difference in warnings.
NASM 3.x deprecates the implicit DEFAULT ABS of 64-bit mode: absrel.asm earns the warning, absrel_fixed.asm (with
`default rel`) assembles clean. Linking the absolute object into a position-independent executable is the next step
(it fails with an R_X86_64_32S relocation error, per the x86-64 psABI), done with GNU ld or ld.lld on an x86-64
Linux machine; the session stops at the two assembles and the clean disassembly.
"""
import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session
session.run(
"""
# Assemble the absolute version: NASM warns that implicit DEFAULT ABS is deprecated.
nasm -f elf64 x86_64/absrel.asm -o x86_64/absrel.o
# Assemble the fixed version with `default rel`: no warning, and it would link as a PIE.
nasm -f elf64 x86_64/absrel_fixed.asm -o x86_64/absrel_fixed.o
# Confirm the fixed object disassembles to a RIP-relative load.
/opt/homebrew/opt/llvm/bin/llvm-objdump -d x86_64/absrel_fixed.o
"""
) Output
# Assemble the absolute version: NASM warns that implicit DEFAULT ABS is deprecated.
$ nasm -f elf64 x86_64/absrel.asm -o x86_64/absrel.o
x86_64/absrel.asm:8: warning: implicit DEFAULT ABS is deprecated [-w+implicit-abs-deprecated]
# Assemble the fixed version with `default rel`: no warning, and it would link as a PIE.
$ nasm -f elf64 x86_64/absrel_fixed.asm -o x86_64/absrel_fixed.o
# Confirm the fixed object disassembles to a RIP-relative load.
$ /opt/homebrew/opt/llvm/bin/llvm-objdump -d x86_64/absrel_fixed.o
x86_64/absrel_fixed.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_start>:
0: 48 8b 05 00 00 00 00 movq (%rip), %rax # 0x7 <_start+0x7>
7: c3 retq
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 x86_64_absrel_session.py
macOS text relocations. macOS arm64 refuses absolute addresses in code. The session above already showed it:
ldr x1, =value asks for an absolute address through a literal pool, and the link fails with
Found illegal text-relocations. The fix is the adrp/add pair.
RISC-V global-pointer relaxation. GNU ld relaxes an auipc/addi pair into one gp-relative addi when
__global_pointer$ is within range, which is why the R_RISCV_RELAX markers appear in the disassembly. The RISC-V
psABI assumes startup code loads __global_pointer$ into gp before any relaxed access runs. A bare _start that
never does so leaves gp holding whatever value it entered with, so the relaxed gp-relative access computes its
address from the wrong base. When that address is not mapped, Linux sends the process SIGSEGV (signal 11), which
ends it, and a shell such as Bash reports that as status 139, which is 128 plus the signal number. Setting
gp under .option norelax, or linking with --no-relax so the pair is left alone, avoids it.
| Error | Where | Symptom | Fix |
|---|---|---|---|
| Implicit DEFAULT ABS | x86-64 NASM | deprecation warning, PIE link fails | add a default rel line |
| Text relocation | macOS arm64 | Found illegal text-relocations |
use adrp/add, not ldr =label |
| gp relaxation crash | RISC-V bare _start |
SIGSEGV (status 139) |
set gp, or link --no-relax |
Checkpoint
Check: Three address errors and their fixes
Practise
Each question gives its reason, right or wrong, from the three errors above and the section rules.
Practice that adapts to you
Practise this lesson
6 questions about 3 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: why is modern code position-independent, and how does it reach a global variable without knowing its address?
A position-independent executable or shared library can be loaded at any base address the operating system chooses,
which lets address-space layout randomisation place it unpredictably and lets a shared library be mapped wherever
there is room. Because the final address of a global is not fixed until load time, the code cannot hard-code it.
Instead it computes the address from the program counter: the distance from the current instruction to the data is
known at link time, even though the absolute address is not. x86-64 does this with a RIP-relative lea or memory
operand; AArch64 uses adrp to get the page base and add for the offset within the page; RISC-V uses auipc for
the PC-relative upper bits and addi for the lower bits. The linker fills in the PC-relative displacement, so the same
bytes work wherever the program lands.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Compare how x86-64, AArch64 and RISC-V form the address of a global variable in position-independent code. Why does one ISA need a single instruction and two need a pair?
-
Fresher to mid
A program declares a 100 MB array of zeros and its executable is only a few kilobytes. A teammate is sure the file should be 100 MB. Explain who is right and why.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Where does an assembler put read-only data, writable data and zero-initialised data?
In .rodata, .data and .bss. .data is stored in the file with its values; .bss records only a size, takes no file space, and the loader hands it zeroed pages at start-up.
-
How does position-independent code form the address of a global on each ISA?
From the program counter: one RIP-relative lea on x86-64, an adrp and add pair on AArch64, an auipc and addi pair on RISC-V. The distance to the data is known at link time even when the absolute address is not.
-
Why is modern code position-independent?
So the loader may place it at any base address: address-space layout randomisation can then put it somewhere unpredictable, and a shared library can be mapped wherever there is room.
-
In NASM 64-bit code, what does default rel change?
It makes a plain [label] operand RIP-relative instead of absolute, so the object can link into a position-independent executable. NASM 3.x deprecates the implicit absolute default.
-
What happens on macOS arm64 if code contains an absolute address?
The link fails with a text-relocation error: the platform refuses code that the loader would have to patch with absolute addresses.
-
Why does RISC-V code depend on the gp register being set up?
The linker may relax a PC-relative pair into a single gp-relative access to data near gp, so the code then works only if gp holds the value the linker assumed.
Key takeaways
- The assembler places data in
.rodata(read-only),.data(writable, in the file) or.bss(writable, zeroed, taking no file space), and alignment directives decide where the data starts. - Position-independent code forms a data address from the program counter: one
leaon x86-64, anadrp/addpair on AArch64, anauipc/addipair on RISC-V. - NASM 3.x deprecates the implicit DEFAULT ABS of 64-bit mode; a
default relline makes[label]RIP-relative and lets the object link as a PIE. - macOS arm64 rejects absolute addresses in code (a text-relocation error), and RISC-V code depends on
gpbeing set because the linker relaxes PC-relative pairs togp-relative addressing.
Terms in this lesson
- .bss
- The section that holds writable data initialised to zero. It records only a size in the executable and takes no file space; the loader allocates and zeroes its pages when the program starts.
- position-independent code
- Code that reaches its data and other code relative to the program counter rather than by absolute address, so the loader may place it at any base address. Formed with RIP-relative operands on x86-64, adrp/add on AArch64 and auipc/addi on RISC-V.
References
- NASM Manual: effective addresses and default rel (The NASM development team)
- Writing ARM64 code for Apple platforms (Apple Inc.)
- Arm A64 Instruction Set Architecture: ADRP and ADD (immediate) (Arm Limited)
- RISC-V ELF psABI specification (global pointer and linker relaxation) (RISC-V International)
- GNU assembler (as) manual: sections and alignment directives (Free Software Foundation)
- System V ABI, x86-64 processor supplement (relocation types) (x86 psABI maintainers)
- signal(7): overview of signals (Linux manual page) (The Linux man-pages project)
- Bash Reference Manual: exit status (Free Software Foundation)
Report a problem with this lesson
Kept only in this browser. Your Learn progress