Hello, World &
nothing else.
Twelve instructions, two system calls, no C runtime. Below is the smallest useful x86-64 program you can write on Linux — and a tour of why every line is there.
The program
1section .data 2 msg db "Hello, World!", 10 ; bytes + LF 3 len equ $ - msg ; length, computed at assembly 4 5section .text 6 global _start 7 8_start: 9 mov rax, 1 ; sys_write 10 mov rdi, 1 ; fd = stdout 11 mov rsi, msg ; buf 12 mov rdx, len ; count 13 syscall 14 15 mov rax, 60 ; sys_exit 16 xor rdi, rdi ; status = 0 17 syscall
Line by line
-
section .data
Marks the start of initialised data. Bytes here live in the binary and are loaded into a read-write page at runtime.
-
msg db "Hello, World!", 10
dbmeans define byte. The string's ASCII bytes are emitted in order, followed by10— the line-feed character.msgis the address of the first byte. -
len equ $ - msg
$is the assembler's "current address." Subtractingmsgyields the byte length.equbinds it as a compile-time constant — no memory, no instruction, just a number the assembler substitutes. -
section .text
Switches to the code segment, which the linker will mark read-execute.
-
global _start
Exposes the symbol so the linker can find the entry point. With no C runtime in the picture, the kernel jumps straight here when the process starts.
-
mov rax, 1
Loads syscall number 1 —
sys_write— intorax. Onsyscallthe kernel readsraxto dispatch. -
mov rdi, 1
First argument: file descriptor. 1 is stdout, inherited from the shell.
-
mov rsi, msg
Second argument: pointer to the buffer to write.
-
mov rdx, len
Third argument: byte count.
-
syscall
Traps into the kernel. The kernel performs the write and returns;
raxnow holds the result — bytes written, or a negative errno. -
mov rax, 60
Syscall 60 is
sys_exit. Without this, the CPU would fall through into whatever bytes follow_startin memory and the process would crash. There is nomainto return to. -
xor rdi, rdi
Idiomatic zero.
xor reg, regassembles to a shorter encoding thanmov reg, 0and breaks the register's dependency chain — the canonical way to clear a register on x86. Sets exit status to 0. -
syscall
Kernel terminates the process. Control never returns.
Syscall ABI
The kernel reads arguments from a fixed set of registers — different from, but related to, the function-call ABI used between user-space functions.
| Slot | Reg | Role |
|---|---|---|
| — | rax | syscall number in · return value out |
| arg 1 | rdi | first argument |
| arg 2 | rsi | second argument |
| arg 3 | rdx | third argument |
| arg 4 | r10 | fourth argument |
| arg 5 | r8 | fifth argument |
| arg 6 | r9 | sixth argument |
The function-call ABI uses rcx as the fourth argument; the syscall ABI uses r10 instead because the syscall instruction itself clobbers rcx and r11 — the CPU stashes rip and rflags there on entry.
Build & run
$nasm -f elf64 hello.asm -o hello.o $ld hello.o -o hello $./hello Hello, World!
The resulting binary is around 8 KB on a current toolchain — almost all of it ELF headers and alignment padding. The actual machine code is roughly 40 bytes.
Footnotes
macOS. Same architecture, different kernel ABI. Syscalls are numbered differently and live in the 0x2000000-prefixed range; the entry symbol is _main with a leading underscore, and you'd typically link against libSystem rather than calling the kernel directly.
GAS syntax. The GNU assembler reverses operand order — movq $1, %rax instead of mov rax, 1 — and prefixes registers with % and immediates with $. Same instructions, different spelling.
Why no ret? Because there is nowhere to return to. The kernel jumps to _start with a stack laid out for argc, argv, envp, and an auxiliary vector — but no return address. Falling off the end is undefined behaviour. sys_exit is mandatory.