Assembly Programming: Fundamentals and ISAs¶
Part 1 of 4 of the Assembly Programming reference (plugin
assembly-programming), covering §0–§5. Sibling skills:assembly-toolchain-performance-and-simd(§6–§10),assembly-systems-crypto-and-inline(§11–§14),assembly-reference(§15–§20). Section numbers are shared across the set; a reference written as §N →skillpoints into that sibling skill.Currency: Verified August 2026. See §17 →
assembly-referencefor the currency snapshot and what goes stale first.How to read this. Reference, not tutorial. Sections are independent. Three markers: - [DURABLE] — machine organization, algorithms, or a lesson that has held since the 1970s. Does not expire. - [ISA] — specific to x86-64, AArch64, RISC-V, or a particular microarchitecture. Verify against the vendor's current manual. - [CONTESTED] — practitioners genuinely disagree, usually about how much hand-written assembly is justified.
⚠️ GOTCHA boxes mark the mistakes that produce silent corruption, ABI violations, or code that is slower than the C you replaced.
The three framings that organize everything below: 1. The instruction set is an interface, not the machine. The CPU you're actually programming is out-of-order, superscalar, speculative, and cached. Your instructions are decoded into µops, reordered, and executed in parallel. Reasoning about assembly as "the CPU does this, then this" is wrong in ways that make your optimizations wrong. 2. Reading assembly is a hundred times more common than writing it, and more valuable. Most of the return here comes from understanding compiler output, debugging an optimized crash, or reverse-engineering a binary. 3. When you write assembly, you are taking on the compiler's job permanently. No retargeting, no auto-vectorization, no new-CPU tuning, and no help from the next twenty years of compiler improvements. That's the real cost, and it's paid in maintenance rather than in the initial write.
§0. Routing¶
0.1 Should you write assembly at all?¶
[DURABLE] For almost all code, no. Modern compilers beat hand-written assembly on anything but small, carefully-chosen kernels — and they retarget for free. The legitimate reasons, in rough order of how often they're actually valid:
| Reason | Notes |
|---|---|
| Reading compiler output | The dominant use. Not writing at all |
| Debugging optimized code / crash dumps | You have no choice; the source is a fiction at -O2 |
| Reverse engineering, malware analysis, security research | Reading, again |
| Instructions the compiler won't emit | Crypto (AES-NI, SHA, carry-less multiply), CRC, special atomics, cache control, hardware-specific instructions |
| Constant-time cryptography | §12 → assembly-systems-crypto-and-inline — the compiler is actively hostile to your requirements here |
| Boot code, context switches, interrupt vectors, syscall stubs | §11 → assembly-systems-crypto-and-inline — there is no C for "set up the stack before there is a stack" |
| Hot kernels after profiling and after intrinsics | Codecs, BLAS, hashing, parsers. And use intrinsics first |
| Extremely constrained targets | Tiny MCUs, boot ROMs, size-limited firmware |
| Compiler bugs / missing optimizations | Real, but verify before assuming |
| Learning how machines work | The best reason of all, and it doesn't need to ship |
[DURABLE] The ladder, and take it in order:
1. Better algorithm ← usually the whole answer
2. Better data layout / memory access ← usually the rest of it
3. Compiler flags, PGO, LTO
4. Restructure C/C++/Rust so the compiler can vectorize
5. Compiler INTRINSICS ← 95% of the benefit, register allocation for free
6. Inline assembly for a specific instruction
7. Hand-written assembly functions
8. Hand-written assembly with microarchitectural scheduling
0.2 The question router¶
| Asked about... | Go to |
|---|---|
| Machine model: registers, memory, flags, endianness | §1 |
| x86-64 specifically | §2 |
| AArch64 / ARM64 specifically | §3 |
| RISC-V specifically | §4 |
| Other ISAs (embedded, GPU, historical) | §5 |
| Calling conventions and ABIs | §6 → assembly-toolchain-performance-and-simd |
| Assemblers, syntax, toolchain, linking | §7 → assembly-toolchain-performance-and-simd |
| Reading disassembly and compiler output | §8 → assembly-toolchain-performance-and-simd |
| Performance: pipelines, latency, caches, branches | §9 → assembly-toolchain-performance-and-simd |
| SIMD and vector programming | §10 → assembly-toolchain-performance-and-simd |
| Systems assembly: interrupts, context switch, boot | §11 → assembly-systems-crypto-and-inline |
| Cryptographic and constant-time assembly | §12 → assembly-systems-crypto-and-inline |
| Inline assembly and intrinsics | §13 → assembly-systems-crypto-and-inline |
| Debugging, testing, verification | §14 → assembly-systems-crypto-and-inline |
| "Don't do this" | §15 → assembly-reference |
| "Which approach is better?" | §16 → assembly-reference (contested) |
| "Is this still current?" | §17 → assembly-reference |
| Books, manuals, people | §18 → assembly-reference |
§1. The Machine Model¶
1.1 What's actually there¶
┌──────────────────────────────────────────────────────────────┐
│ ARCHITECTURAL STATE (what the ISA promises) │
│ general-purpose registers · SIMD/vector registers │
│ program counter · flags/condition codes · stack pointer │
│ control/system registers · memory (virtual address space) │
└──────────────────────────────────────────────────────────────┘
↕ (the ISA is a CONTRACT, not a description)
┌──────────────────────────────────────────────────────────────┐
│ MICROARCHITECTURE (what actually happens) │
│ fetch → decode → µop cache → RENAME (physical regs ≫ arch) │
│ → scheduler/reservation stations → OUT-OF-ORDER EXECUTION │
│ across multiple ports → load/store buffers → RETIRE in │
│ order · branch predictors · L1/L2/L3 caches · TLBs · │
│ prefetchers · store-to-load forwarding · speculation │
└──────────────────────────────────────────────────────────────┘
[DURABLE] Register renaming is why most naive assembly intuitions fail. The CPU has
far more physical registers than architectural ones and renames on the fly, so
write-after-write and write-after-read "dependencies" are free — only true
read-after-write data dependencies cost you. This is why xor eax, eax is faster than
mov eax, 0 (it's recognized as a zeroing idiom and breaks the dependency chain), and why
"reusing a register to save registers" can be actively harmful.
[DURABLE] The three things that actually determine speed (§9 → assembly-toolchain-performance-and-simd): the critical path
through the dependency graph, memory access patterns, and branch predictability.
Instruction count is a distant fourth and is the thing beginners optimize.
1.2 Registers¶
| Class | Purpose |
|---|---|
| General-purpose | Integers, addresses. x86-64: 16 (32 with APX); AArch64: 31 + zero register; RISC-V: 32 (x0 hardwired to zero) |
| SIMD/vector | Packed data. §10 → assembly-toolchain-performance-and-simd |
| Floating-point | Separate on some ISAs, shared with SIMD on others |
| Flags/condition | x86 EFLAGS, ARM NZCV. RISC-V has none — a deliberate design choice |
| Special | PC/IP, SP, link register, TLS base, system/control registers |
[DURABLE] A zero register is a surprisingly large ISA win. AArch64's xzr/wzr and
RISC-V's x0 let one instruction encoding serve many purposes: add rd, rs, x0 is a move,
beq rs, x0, label is branch-if-zero, storing xzr is a memset. x86 has no zero register
and needs distinct encodings for all of it.
1.3 Addressing modes¶
x86-64: [base + index*scale + disp] scale ∈ {1,2,4,8} — very expressive
mov rax, [rbx + rcx*8 + 16]
AArch64: [base], [base, #imm], [base, Xn{, LSL #s}], pre/post-index
ldr x0, [x1, #16]! pre-index: x1 += 16, then load
ldr x0, [x1], #16 post-index: load, then x1 += 16
RISC-V: [base + imm12] ONLY — deliberately minimal
ld a0, 16(a1)
add, which the designers judged a fair price for decode simplicity.
Neither is wrong; they optimize different things.
1.4 Endianness, alignment, and memory ordering¶
- Endianness: x86-64, AArch64 (in practice), and RISC-V are all little-endian
today. Big-endian survives in network byte order, some MIPS/PowerPC/SPARC deployments,
and file formats. Byte-swap instructions exist:
bswap/movbe(x86),rev(ARM),rev8(RISC-V Zbb). - Alignment: x86-64 tolerates unaligned scalar access with a small penalty (and requires alignment for some SIMD instructions and all atomics that must not split a cache line). ARM and RISC-V vary — unaligned may fault, may trap-and-emulate slowly, or may work fine. ⚠️ A split-cache-line access is dramatically slower everywhere, and a split-page access worse still.
- Memory ordering [DURABLE, and the most dangerous area in multicore assembly]:
| ISA | Model |
|---|---|
| x86-64 | TSO (total store order) — strong. Only store→load can reorder. mfence/lock-prefixed ops for the rest |
| AArch64 | Weak, with acquire/release built into instructions: ldar/stlr, plus dmb/dsb/isb barriers |
| RISC-V | Weak (RVWMO), with fence and .aq/.rl suffixes on atomics |
| POWER | Weak, notoriously so |
⚠️ GOTCHA — x86's strong ordering hides bugs that ARM and RISC-V expose. Concurrent code developed and tested only on x86 routinely breaks on AArch64, because the missing barrier never mattered before. This is one of the most common real-world porting failures, and it produces rare, load-dependent corruption rather than a clean crash.
§2. x86-64¶
2.1 The register file¶
64-bit 32-bit 16-bit 8-bit Conventional role (System V AMD64)
rax eax ax al/ah return value; implicit in mul/div
rbx ebx bx bl callee-saved
rcx ecx cx cl 4th arg; implicit shift count
rdx edx dx dl 3rd arg; high half of mul/div
rsi esi si sil 2nd arg; string source
rdi edi di dil 1st arg; string destination
rbp ebp bp bpl frame pointer (callee-saved)
rsp esp sp spl STACK POINTER — never clobber
r8–r15 r8/r9 = 5th/6th args; r12–r15 callee-saved
xmm0–15 / ymm0–15 / zmm0–31 SIMD (§10)
rip instruction pointer (RIP-relative addressing)
⚠️ GOTCHA — the 32-bit zero-extension rule. Writing to a 32-bit register zero-extends into the full 64-bit register; writing to a 16- or 8-bit register does not (it merges, creating a partial-register dependency stall). So
mov eax, 1clears the upper 32 bits ofrax— deliberately, and usefully, because it's a shorter encoding — whilemov ax, 1leaves the top 48 bits and creates a false dependency. Prefer 32-bit operations when the value fits: shorter encoding, and free zeroing.
2.2 The instruction set, honestly¶
x86-64 is variable-length (1–15 bytes), two-operand destructive (add rax, rbx
means rax += rbx — APX changes this, §17 → assembly-reference), and enormous. Practical groupings:
- Data movement:
mov,movzx/movsx(zero/sign extend),lea,push/pop,xchg,cmov. leais the workhorse. It computes an address without accessing memory, so it's a free three-operand add-and-shift:lea rax, [rbx + rcx*4 + 8]. Compilers use it constantly for arithmetic that has nothing to do with addresses.- Arithmetic/logic:
add/adc,sub/sbb,imul/mul,idiv/div(very slow — 20–100 cycles; strength-reduce it),and/or/xor/not, shifts,bt/bts/btr. - BMI1/BMI2:
andn,bextr,blsi,tzcnt,lzcnt,popcnt,pdep/pext(note:pdep/pextare microcoded and glacial on pre-Zen 3 AMD — a classic portability-of-performance trap). - Control:
jmp,jcc,call/ret,loop(don't — slower than the equivalentdec/jnzon modern parts). cmov— conditional move, no branch. Essential for §12 →assembly-systems-crypto-and-inline, and a good idea whenever a branch is unpredictable.- Atomics:
lock-prefixed RMW,cmpxchg,cmpxchg16b,xadd. - Crypto: AES-NI (
aesenc…), SHA extensions,pclmulqdq(carry-less multiply — the basis of fast GCM and CRC).
2.3 Idioms you'll see in every compiler's output¶
xor eax, eax ; rax = 0. Shorter than mov, and BREAKS the dependency chain
test rax, rax ; set flags from rax without a compare-with-zero
lea rax, [rbx+rbx*2]; rax = rbx*3, no multiplier, no flags touched
sete al ; materialize a condition as 0/1 without branching
cdq / cqo ; sign-extend eax→edx:eax before idiv (forgetting this is a classic bug)
endbr64 ; CET indirect-branch landing pad — required at indirect targets
2.4 The x86 tax¶
[DURABLE] Variable-length decoding is genuinely expensive, which is why modern x86 cores have µop caches to bypass the decoder on hot loops. Practical consequences: - Code density matters more than instruction count on x86 — fitting a loop in the µop cache or in fewer 32-byte fetch windows is a real optimization. - Alignment of branch targets to 16 or 32 bytes can matter. - The legacy modes (real, protected, long) and the accumulated 40 years of encodings are the reason a full x86 assembler is a large program.
§3. AArch64 (ARM64)¶
3.1 The register file¶
x0–x30 64-bit GPRs; w0–w30 are the 32-bit views (writing wN zero-extends to xN)
x0–x7 arguments and return values
x8 indirect result location / Linux syscall number
x9–x15 caller-saved (temporary)
x16,x17 IP0/IP1 — intra-procedure-call scratch, may be clobbered by the LINKER's veneers
x18 PLATFORM REGISTER — reserved on some OSes (⚠️ Darwin, Windows). Don't touch
x19–x28 callee-saved
x29 FP (frame pointer)
x30 LR (link register — the return address)
sp stack pointer (⚠️ MUST be 16-byte aligned at any public interface)
xzr/wzr the ZERO REGISTER (reads 0, writes discarded) — encoding 31, context-dependent with sp
pc not directly writable
v0–v31 128-bit SIMD (NEON), also used as scalar FP (s/d/h views)
z0–z31 SVE scalable vectors; p0–p15 predicates (§10)
3.2 The character of the ISA¶
Fixed 32-bit instructions, load/store architecture (arithmetic never touches memory), mostly three-operand and non-destructive, and a genuinely clean encoding.
; the canonical prologue/epilogue
stp x29, x30, [sp, #-16]! ; push FP and LR, pre-decrement sp
mov x29, sp
; ...
ldp x29, x30, [sp], #16 ; pop, post-increment
ret ; branch to x30
; conditional execution without branches
cmp x0, x1
csel x2, x3, x4, lt ; x2 = (x0 < x1) ? x3 : x4 ← constant-time friendly
cinc x2, x2, ne ; conditional increment
cbz x0, label ; compare-and-branch-if-zero: one instruction, no flags
tbz x0, #3, label ; test-bit-and-branch
; loading a 64-bit constant takes up to four instructions
movz x0, #0x1234, lsl #48
movk x0, #0x5678, lsl #32 ; movk = move-keep (doesn't clear other bits)
; ...or, far more often:
adrp x0, symbol ; PC-relative page address (±4 GB)
add x0, x0, :lo12:symbol ; plus the low 12 bits
[DURABLE] The adrp/add pair is the single most characteristic AArch64 idiom and
the thing that confuses people coming from x86's RIP-relative addressing. AArch64 can't
encode a 64-bit address in a 32-bit instruction, so PC-relative addressing is split into
a 4 KB-page-granular part and a 12-bit offset.
Pointer authentication (PAC) and BTI — Armv8.3+/8.5+ security features you'll see in
modern compiler output: paciasp/autiasp sign and authenticate the return address in
the prologue/epilogue (defeating ROP), and bti c marks legal indirect-branch targets.
Don't strip these; on Apple platforms they're mandatory.
Atomics: the classic LL/SC pair ldxr/stxr (load-exclusive / store-exclusive,
with a retry loop), plus the much better LSE atomics from Armv8.1 (ldadd, swp,
cas) which are single instructions and scale far better under contention.
§4. RISC-V¶
4.1 The design philosophy, and why it matters to you¶
[DURABLE] RISC-V is a small base plus modular extensions, which makes it the easiest major ISA to learn and the most annoying to target portably.
RV32I / RV64I base integer (I = 32 registers; E = 16, for embedded)
M multiply/divide A atomics F/D/Q float (single/double/quad)
C compressed (16-bit) V vector (§10) B bit manipulation (Zba/Zbb/Zbs)
Zicsr control regs Zifencei Zk* scalar crypto Zvk* vector crypto
x0/zero hardwired zero x1/ra return address x2/sp stack pointer
x3/gp global pointer x4/tp thread pointer x5–7/t0–2 temporaries
x8/s0/fp saved / frame ptr x9/s1 saved
x10–17/a0–a7 arguments and return values x18–27/s2–11 saved
x28–31/t3–6 temporaries
Notably absent: condition flags. Comparison and branch are fused into one instruction
(beq, bne, blt, bge, bltu, bgeu), and slt/sltu materialize a comparison as
0/1. This removes a serialization point and a rename hazard that x86 and ARM both carry.
addi sp, sp, -16
sd ra, 8(sp)
sd s0, 0(sp)
# ...
ld ra, 8(sp)
ld s0, 0(sp)
addi sp, sp, 16
ret # pseudo-instruction for: jalr x0, 0(ra)
⚠️ Pseudo-instructions are pervasive and you must know they're not real: li, la,
mv, nop, ret, call, j, beqz, not, neg. The assembler expands each into one
or more real instructions, and li with a large constant becomes lui+addi.
4.2 Profiles — the fragmentation fix¶
[VERSIONED] The extension modularity created a real portability problem, and profiles are the answer: a named set of mandatory and optional extensions that software can target.
RVA23 was ratified 21 October 2024 and is the current 64-bit application-processor
profile. What matters for assembly programmers:
- The V (vector) extension is now MANDATORY — it was optional in RVA22. Vectors are no
longer an optional accelerator; they're a baseline capability software can assume.
- RVA23 is the baseline requirement for the Android RISC-V ABI.
- Also newly mandatory in RVA23U64: Zvfhmin (vector half-precision), Zvbb (vector
bit manipulation), Zvkt (vector data-independent execution latency — see §12 → assembly-systems-crypto-and-inline),
Zihintntl, Zicond (integer conditional ops), Zimop/Zcmop, Zcb,
Zfa, and Supm (pointer masking).
- The scalar crypto extensions Zkn and Zks are no longer options — the stated goal is
for hardware and software vendors to move to vector crypto, since vectors are now
mandatory and vector crypto is substantially faster.
- The hypervisor extension is in the S-mode profile.
- Ratified specs are frozen: "No changes are allowed… Ratified extensions are never
revised." Changes go into new extensions.
§5. Other ISAs Worth Knowing¶
| ISA | Where you'll meet it |
|---|---|
| ARM32 / Thumb-2 | Older embedded, Cortex-M. Thumb-2's mixed 16/32-bit encoding is excellent for code density; Cortex-M is Thumb-only |
| AVR | Arduino, 8-bit MCUs. Harvard architecture — separate code and data address spaces, which surprises everyone |
| MSP430, PIC, 8051 | Deeply embedded, still shipping in volume |
| POWER / PowerPC | IBM servers, older consoles. Weak memory model, big-endian heritage |
| MIPS | Networking silicon, older Roku/embedded, and every undergraduate architecture course |
| SPARC | Register windows — a genuinely different idea worth understanding |
| s390x | IBM mainframe. Big-endian, and still absolutely everywhere in banking |
| WebAssembly | A stack machine and a compile target, not hardware. Structured control flow, no registers |
| x86 16/32-bit | Boot code, BIOS/UEFI, DOS-era reverse engineering, retro |
| GPU ISAs (PTX/SASS, RDNA, SPIR-V) | Mostly generated; PTX is a virtual ISA, SASS is the real one |
| 6502, Z80, 68000 | Retro computing and demoscene, and the best teaching ISAs ever made |
Plugin: assembly-programming · View SKILL.md on GitHub