Coding · Programming languages
Assembly language tutorial for beginners (x86-64, NASM)
Short answer
Assembly language is a human-readable form of a processor's machine instructions: each line usually becomes one instruction such as mov rax, 1 or add rax, rbx. You work directly with registers, memory addresses and the stack, with no variables, types or loops beyond what you build from jumps. This tutorial uses x86-64 on Linux with the NASM assembler: write a .asm file, assemble it with nasm -f elf64, and link it with ld.
What assembly is and who uses it
Every processor family has its own instruction set and therefore its own assembly language. x86-64 (also called AMD64) runs most desktop PCs and servers; ARM (AArch64) runs most phones, Apple silicon Macs and many servers. An assembler translates assembly into machine code, and a linker combines the result into an executable.
Few programs are written entirely in assembly today. It is used in operating system kernels and bootloaders, performance-critical routines (cryptography, codecs, math libraries), embedded firmware, compiler development, security research and reverse engineering. Most often, developers read assembly to understand what a compiler produced or to debug a crash.
Install NASM and run Hello, world
On Debian or Ubuntu, install the tools with sudo apt install nasm binutils (binutils provides the linker ld). This program calls the Linux kernel directly through system calls, so it needs no C library.
; hello.asm - x86-64 Linux, NASM syntax
section .data
msg db "Hello, world!", 10 ; 10 = newline
msglen equ $ - msg ; length computed by the assembler
section .text
global _start
_start:
mov rax, 1 ; system call number 1 = write
mov rdi, 1 ; file descriptor 1 = stdout
mov rsi, msg ; address of the bytes
mov rdx, msglen ; number of bytes
syscall
mov rax, 60 ; system call number 60 = exit
xor rdi, rdi ; exit code 0
syscallnasm -f elf64 hello.asm -o hello.o
ld hello.o -o hello
./hello
# Hello, world!section .data holds initialized data, section .text holds code, and _start is the entry point that ld looks for. Comments start with ;. Without the final exit system call, the processor would keep executing whatever bytes follow and the program would crash.
Registers and data sizes
Registers are small, very fast storage locations inside the CPU. x86-64 has 16 general-purpose 64-bit registers. Each can also be used in smaller parts.
| Register | Common role | Smaller parts |
|---|---|---|
rax | Return value, system call number | eax (32-bit), ax (16), al (8) |
rdi, rsi, rdx | First three function or system call arguments | edi, esi, edx |
rcx, r8, r9 | Fourth to sixth function arguments | ecx, r8d, r9d |
rbx, rbp, r12–r15 | Values that must survive function calls (callee-saved) | ebx, r12d |
rsp | Stack pointer: address of the top of the stack | Do not use for data |
rip | Instruction pointer (changed only by jumps, calls, returns) | Not directly writable |
Data in memory is declared with db (byte, 8 bits), dw (word, 16), dd (doubleword, 32) and dq (quadword, 64). Uninitialized buffers go in section .bss with resb, resq and so on. Writing to a 32-bit register such as eax also clears the upper 32 bits of rax, which is why xor eax, eax is a common way to set rax to zero.
Basic instructions and control flow
Most instructions take a destination first and a source second: add rax, rbx means rax = rax + rbx. There are no if or while statements; you compare values with cmp, which sets flags, and then use a conditional jump.
| Instruction | Meaning |
|---|---|
mov rax, rbx | Copy rbx into rax |
add, sub, imul, inc, dec | Integer arithmetic |
cmp rax, 10 | Compare (computes rax - 10 and sets flags only) |
je, jne, jl, jg, jle, jge | Jump if equal, not equal, less, greater... (signed) |
jmp label | Jump unconditionally |
push, pop | Put a value on the stack, take it off |
call, ret | Call a function, return from it |
; sum.asm - add 1..10, return the result as the exit code
section .text
global _start
_start:
xor rax, rax ; sum = 0
mov rcx, 1 ; i = 1
.loop:
cmp rcx, 10
jg .done ; if i > 10, leave the loop
add rax, rcx ; sum += i
inc rcx ; i++
jmp .loop
.done:
mov rdi, rax ; exit code = sum
mov rax, 60 ; exit
syscallnasm -f elf64 sum.asm -o sum.o && ld sum.o -o sum
./sum; echo $?
# 55Labels starting with a dot, like .loop, are local to the previous normal label. jl and jg treat values as signed; jb and ja are the unsigned versions.
Memory addressing and arrays
Square brackets mean "the memory at this address". An address can combine a base register, an index register multiplied by 1, 2, 4 or 8, and a constant: [base + index*scale + offset]. This makes array access a single instruction.
section .data
nums dq 3, 1, 4, 1, 5 ; five 64-bit integers
count equ ($ - nums) / 8
section .text
global _start
_start:
xor rax, rax ; total = 0
xor rcx, rcx ; index = 0
.next:
add rax, [nums + rcx*8] ; total += nums[index]
inc rcx
cmp rcx, count
jl .next
mov rdi, rax ; exit code 14
mov rax, 60
syscallmov rax, nums loads the address, while mov rax, [nums] loads the value stored there. lea rsi, [rbx + 8] computes an address without reading memory. When the size is not clear from a register, state it: mov qword [rbx], 0.
Functions and the calling convention
On Linux, functions follow the System V AMD64 ABI. Integer arguments go in rdi, rsi, rdx, rcx, r8, r9; the return value is in rax. A function must preserve rbx, rbp, r12–r15 and rsp, and rsp must be a multiple of 16 just before a call. Following these rules lets assembly call C functions such as printf, and lets C call assembly.
; square.asm - define a function and call printf from the C library
extern printf
global main
section .data
fmt db "%ld squared is %ld", 10, 0
section .text
square: ; long square(long x)
mov rax, rdi
imul rax, rdi
ret
main:
push rbx ; save rbx; also aligns rsp to 16
mov rbx, 7
mov rdi, rbx
call square ; rax = 49
lea rdi, [rel fmt] ; argument 1: format string
mov rsi, rbx ; argument 2
mov rdx, rax ; argument 3
xor eax, eax ; varargs: no vector registers used
call printf wrt ..plt
xor eax, eax ; return 0 from main
pop rbx
ret
section .note.GNU-stack noalloc noexec nowrite progbitsnasm -f elf64 square.asm -o square.o
gcc square.o -o square
./square
# 7 squared is 49Linking with gcc adds the C runtime, which calls main. [rel fmt] and wrt ..plt produce position-independent code, which modern Linux distributions use by default. The last line marks the stack as non-executable and avoids a linker warning.
Error handling
Assembly has no exceptions. A Linux system call that fails returns a negative error number in rax (for example -2, ENOENT, "no such file"), so you test the sign after each call. C library functions report errors through their return value and errno. Hardware faults, such as reading an invalid address or dividing by zero, stop the program with a signal (segmentation fault or floating point exception).
mov rax, 2 ; system call 2 = open
lea rdi, [rel path] ; path is a zero-terminated string
xor esi, esi ; flags: O_RDONLY
syscall
test rax, rax
js .open_failed ; negative result = -errno
mov r12, rax ; success: rax is the file descriptorA small complete program: print a number in decimal
Printing a number means converting it to digits yourself. This program computes 1 + 2 + ... + 100, divides repeatedly by 10 to get the digits from right to left, and writes them with one system call.
section .bss
buf resb 32
section .text
global _start
_start:
xor rax, rax
mov rcx, 100
.sum:
add rax, rcx
loop .sum ; rcx -= 1, repeat while rcx != 0
lea rsi, [buf + 31]
mov byte [rsi], 10 ; newline at the end of the buffer
mov rbx, 10
.digit:
xor rdx, rdx ; div uses rdx:rax as the dividend
div rbx ; rax = quotient, rdx = remainder
add dl, '0' ; remainder to ASCII digit
dec rsi
mov [rsi], dl
test rax, rax
jnz .digit
mov rdx, buf + 32
sub rdx, rsi ; length = end - start
mov rax, 1 ; write(1, rsi, rdx)
mov rdi, 1
syscall
mov rax, 60 ; exit(0)
xor rdi, rdi
syscall
; prints 5050AT&T vs Intel syntax, and ARM
The same x86-64 instruction can be written in two syntaxes. NASM and most Intel and Microsoft documentation use Intel syntax. The GNU assembler (as), GCC output and objdump use AT&T syntax by default; objdump -d -M intel and gcc -S -masm=intel switch to Intel syntax.
| Aspect | Intel (NASM) | AT&T (GNU as) |
|---|---|---|
| Operand order | mov rax, 1 (destination first) | movq $1, %rax (source first) |
| Registers and constants | rax, 5 | %rax, $5 |
| Memory operand | [rbx + rcx*8 + 16] | 16(%rbx,%rcx,8) |
| Operand size | mov qword [rbx], 0 | movq $0, (%rbx) (suffix b, w, l, q) |
ARM processors use a different instruction set. AArch64 has 31 general-purpose 64-bit registers (x0–x30), fixed 32-bit instruction encodings, and a load/store design: arithmetic works only on registers, and memory is accessed with ldr and str. On Linux, the system call number goes in x8 and the call is made with svc #0.
// AArch64 Linux, GNU as syntax: exit(0)
mov x0, #0 // exit status
mov x8, #93 // system call 93 = exit
svc #0Where to go next
- The NASM manual for directives, macros and output formats.
- Intel 64 and IA-32 Architectures Software Developer's Manuals for the exact behavior of every instruction.
- The System V AMD64 ABI document for calling conventions and stack layout.
- Compile small C functions with
gcc -O2 -S -masm=intel(or an online compiler explorer) and read the generated assembly.
FAQ
Is assembly language still worth learning?
Yes, for understanding how computers work, debugging crashes, reading compiler output, security work and embedded development. Writing whole applications in assembly is rarely practical today.
Is x86 assembly the same as ARM assembly?
No. They are different instruction sets with different registers, instructions and encodings. The concepts (registers, memory, jumps, calling conventions) transfer, but the code does not.
What is the difference between NASM and GAS?
NASM is a standalone assembler that uses Intel syntax. GAS, the GNU assembler, is used by GCC and defaults to AT&T syntax, although it also accepts Intel syntax with .intel_syntax noprefix.
Does this code run on Windows or macOS?
Not unchanged. The instructions are the same on any x86-64 system, but system call numbers, the executable format and the calling convention differ. Windows uses a different calling convention, and Apple silicon Macs use ARM instead of x86-64.