The Basics of Assembly Language on x86

Nobody writes application code in raw assembly anymore, but understanding it demystifies what every compiler is actually doing on your behalf, and it’s genuinely useful the moment you need to read a disassembly during debugging or reverse engineering.

Registers: The CPU’s Scratch Space

Before touching memory, the CPU works with a small set of extremely fast on-chip registers. On x86-64, the general-purpose ones include rax, rbx, rcx, rdx, plus rsp (stack pointer) and rbp (base pointer).

A Minimal Program

section .data
    result dq 0

section .text
    global _start

_start:
    mov rax, 5          ; move 5 into register rax
    add rax, 10          ; rax = rax + 10  ->  rax now holds 15
    mov [result], rax    ; store the result in memory

    ; exit(0) syscall
    mov rax, 60
    xor rdi, rdi
    syscall
nasm -f elf64 add.asm -o add.o
ld add.o -o add
./add

Seeing What Your Compiler Actually Generates

The fastest way to build intuition is comparing simple C against its compiled output — Compiler Explorer (godbolt.org) does this interactively, but you can do it locally too:

echo 'int add(int a, int b) { return a + b; }' > add.c
gcc -O2 -c add.c -o add.o
objdump -d add.o
# lea eax, [rdi + rsi]  <- the compiler folded add + move into one instruction
# ret

Why This Is Still Worth Knowing

Understanding registers and the stack directly explains concepts that otherwise feel abstract in higher-level languages — function call overhead, why recursion has a depth limit, why certain optimizations (like inlining) exist, and how a debugger like gdb can actually inspect a running program’s state at the machine level.