Strip away every abstraction. See what really happens when a program runs.
Most "Hello World" examples hide what actually happens.
This series removes those layers one by one.
┌──────────────────────────────────────────────────────┐
│ Your Program │
│ │
│ printf("Hello") ← Part 1 (high-level C) │
│ │ │
│ ▼ │
│ write(1, msg, 14) ← Part 2 (C without printf) │
│ │ │
│ ▼ │
│ mov rax, 1 ← Part 3 & 4 (pure assembly) │
│ syscall │
│ │ │
│ ▼ │
│ ┌───────────┐ │
│ │ KERNEL │ → writes to stdout (fd 1) │
│ └───────────┘ │
└──────────────────────────────────────────────────────┘
You will learn: Kernel vs user space · file descriptors · exit codes · Linux syscalls · how C maps to assembly
hello_world/
├── 📄 README.md
├── 📄 LICENSE
├── 📄 resources.txt
└── 📁 code/
├── hello_w_printf.c ← C with printf (the familiar way)
├── hello_wo_printf.c ← C with write() (closer to the OS)
└── hello_world.asm ← Pure x86-64 assembly (nothing hidden)
Concepts only — no code yet.
Introduces the core ideas that underpin everything:
| Concept | What You'll Learn |
|---|---|
| Kernel vs User Space | Where your code actually runs |
| File Descriptors | 0 = stdin · 1 = stdout · 2 = stderr |
| Exit Codes | How the OS knows if your program succeeded |
| System Calls | The only way to talk to the kernel |
📄 See:
code/hello_w_printf.candcode/hello_wo_printf.c
We move from printf() to write() — removing the C standard library layer:
printf("Hello") write(1, "Hello\n", 14)
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ C stdlib │ ──────────► │ direct fd │
│ buffered │ │ unbuffered │
└──────────┘ └──────────────┘
│ │
└──────────┬─────────────────┘
▼
┌──────────────┐
│ sys_write │
│ (kernel) │
└──────────────┘
Both end up in the same syscall — but the second version shows it explicitly.
Key concepts before writing assembly:
| Mnemonic | Meaning | Purpose |
|---|---|---|
mov |
Move | Load a value into a register |
db |
Define Byte | Declare raw data (strings) |
global |
Global | Export a symbol for the linker |
syscall |
System Call | Trap into the kernel |
xor |
Exclusive OR | Fast way to zero a register |
Syscall calling convention (x86-64 Linux):
┌──────────┬──────────────────────────────┐
│ Register │ Role │
├──────────┼──────────────────────────────┤
│ rax │ syscall number │
│ rdi │ 1st argument │
│ rsi │ 2nd argument │
│ rdx │ 3rd argument │
└──────────┴──────────────────────────────┘
sys_write = 1 → write(fd, buf, count)
sys_exit = 60 → exit(code)
📄 See:
code/hello_world.asm
Everything comes together. The full path:
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ idea │───▶│ registers │───▶│ kernel │───▶│ output │
│ │ │ │ │ │ │ │
│ "print │ │ rax = 1 │ │ sys_write │ │ Hello, │
│ hello" │ │ rdi = 1 │ │ executes │ │ World! │
│ │ │ rsi = msg │ │ │ │ │
└────────────┘ └────────────┘ └────────────┘ └────────────┘
Every register is set by hand. No C runtime. No libraries. Just you and the kernel.
| Tool | Purpose |
|---|---|
| 🐧 Linux x86-64 | Operating system |
| 🔧 NASM | Netwide Assembler |
| 🔗 ld | GNU linker |
# Assemble
nasm -f elf64 code/hello_world.asm -o hello.o
# Link
ld hello.o -o hello
# Run
./helloExpected output:
Hello, World!
✅ You want to understand what really happens when a program runs
✅ You want to learn Linux system calls from the ground up
✅ You want to bridge the gap between C and assembly
✅ You want strong low-level fundamentals
❌ This is not for you if you only want quick results.
Ever wonder what the CPU actually sees when you write this?
mov rax, 1The CPU doesn't understand English words like mov or register names like rax.
Every instruction and register has a numeric code defined in the Instruction Set Architecture (ISA) — a specification published by the CPU manufacturer (Intel/AMD) that assigns a unique number to every operation and register the chip supports.
Suppose we look up the ISA and find:
| Component | Name | Numeric Code (decimal) |
|---|---|---|
| Opcode (instruction) | mov |
2 |
| Register | rax |
1 |
| Immediate (value) | 1 |
1 |
📝 These are simplified for illustration. Real x86-64 encoding is more complex, but the principle is the same — every part of an instruction becomes a number.
So mov rax, 1 is really just:
instruction = 2, register = 1, value = 1
Each of those decimal numbers maps directly to hex:
Decimal Hex
───────────────────────
2 ──────► 0x02 (mov)
1 ──────► 0x01 (rax)
1 ──────► 0x01 (immediate value)
Lined up, the instruction in hex becomes:
┌──────┬──────┬──────┐
│ 02 │ 01 │ 01 │
│ mov │ rax │ 1 │
└──────┴──────┴──────┘
Now convert each hex byte to its 8-bit binary form:
Hex Binary
────────────────────────────
0x02 ──────► 00000010 (mov)
0x01 ──────► 00000001 (rax)
0x01 ──────► 00000001 (immediate value)
The full instruction as the CPU sees it:
┌──────────┬──────────┬──────────┐
│ 00000010 │ 00000001 │ 00000001 │
│ mov │ rax │ 1 │
└──────────┴──────────┴──────────┘
┌─────────────────────────────────────────────────────────────────┐
│ │
│ ASSEMBLY mov rax 1 │
│ (what we write) │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ DECIMAL 2 1 1 │
│ (ISA lookup) │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ HEX 02 01 01 │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ BINARY 00000010 00000001 00000001 │
│ (what the CPU actually sees) │
│ │
└─────────────────────────────────────────────────────────────────┘
💡 Key insight: the human-readable instruction
mov rax, 1is just a convenience.
The assembler (NASM) translates it into raw numbers. The CPU reads those numbers as electrical signals — nothing else. No names, no syntax, just voltage levels representing0and1.
- Linux Syscall Table — complete reference for x86-64 syscall numbers
If high-level code feels like magic, this series shows you the machinery behind it.
No abstractions · No shortcuts · Just the system