cv.cm

Coding · Programming languages

Assembly language tutorial for beginners (x86-64, NASM)

9 min read

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.

Open Base converter

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
    syscall
nasm -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.

RegisterCommon roleSmaller parts
raxReturn value, system call numbereax (32-bit), ax (16), al (8)
rdi, rsi, rdxFirst three function or system call argumentsedi, esi, edx
rcx, r8, r9Fourth to sixth function argumentsecx, r8d, r9d
rbx, rbp, r12–r15Values that must survive function calls (callee-saved)ebx, r12d
rspStack pointer: address of the top of the stackDo not use for data
ripInstruction 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.

InstructionMeaning
mov rax, rbxCopy rbx into rax
add, sub, imul, inc, decInteger arithmetic
cmp rax, 10Compare (computes rax - 10 and sets flags only)
je, jne, jl, jg, jle, jgeJump if equal, not equal, less, greater... (signed)
jmp labelJump unconditionally
push, popPut a value on the stack, take it off
call, retCall 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
    syscall
nasm -f elf64 sum.asm -o sum.o && ld sum.o -o sum
./sum; echo $?
# 55

Labels 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
    syscall

mov 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 progbits
nasm -f elf64 square.asm -o square.o
gcc square.o -o square
./square
# 7 squared is 49

Linking 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 descriptor

A 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 5050

AT&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.

AspectIntel (NASM)AT&T (GNU as)
Operand ordermov rax, 1 (destination first)movq $1, %rax (source first)
Registers and constantsrax, 5%rax, $5
Memory operand[rbx + rcx*8 + 16]16(%rbx,%rcx,8)
Operand sizemov qword [rbx], 0movq $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     #0

Where 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.

All coding tutorials