Assembly Language on Linux: A Starter Guide
Write x86-64 assembly on Linux from scratch: install NASM and GAS, learn syscalls, assemble and link a working program, then inspect and debug it.
Before you start
- ▸64-bit Linux installation (any modern distro)
- ▸Basic comfort with the terminal and a text editor
- ▸General understanding of how programs execute (processes, memory segments)
- ▸Root or sudo access to install packages
Writing assembly on Linux is the shortest path to understanding what your CPU actually does. You bypass compilers, talk directly to the kernel via syscalls, and control every byte. This guide covers x86-64 assembly using both NASM and GNU Assembler (GAS), walks through the Linux syscall interface, and gets a working program assembled, linked, and running.
Choosing Your Assembler: NASM vs GAS
Two assemblers dominate Linux x86-64 work:
- NASM (Netwide Assembler) uses Intel syntax. Most tutorials and textbooks use it. Errors are clear, macros are sane.
- GAS (GNU Assembler,
as) ships withbinutilsand is what GCC emits. Defaults to AT&T syntax (source before destination), though.intel_syntax noprefixswitches it to Intel.
This guide teaches both. Pick whichever fits your toolchain. If you are inspecting GCC output with objdump, GAS/AT&T familiarity is essential.
Installing the Tools
Debian / Ubuntu
sudo apt update
sudo apt install nasm binutils build-essential
Fedora / RHEL / Rocky
sudo dnf install nasm binutils gcc
Arch
sudo pacman -S nasm binutils base-devel
binutils provides the linker (ld) and objdump. Confirm versions after install:
nasm --version
ld --version
x86-64 Fundamentals You Must Know
x86-64 (also called AMD64) extends the 32-bit x86 ISA. Key concepts before writing a single line:
- Registers: 16 general-purpose 64-bit registers:
rax,rbx,rcx,rdx,rsi,rdi,r8–r15, plusrsp(stack pointer) andrbp(base pointer). Each has 32-bit (eax), 16-bit (ax), and 8-bit (al/ah) aliases. - Stack: grows downward in memory.
pushdecrementsrsp;popincrements it. Always 16-byte aligned before acallper the System V ABI. - Instruction pointer:
rip— not directly writable, but used in RIP-relative addressing. - Flags register (
rflags): carry, zero, sign, overflow flags set by arithmetic instructions and tested by conditional jumps.
Linux Syscalls on x86-64
The kernel exposes services through a syscall table. In x86-64 Linux you invoke them with the syscall instruction. The calling convention:
rax— syscall numberrdi,rsi,rdx,r10,r8,r9— arguments (in order)- Return value lands in
rax; negative values indicate errors (negated errno).
Key syscall numbers for beginners (x86-64 Linux):
| Number | Name | Purpose |
|---|---|---|
| 0 | read | Read from file descriptor |
| 1 | write | Write to file descriptor |
| 60 | exit | Terminate process |
| 231 | exit_group | Terminate all threads |
The full table lives at /usr/include/asm/unistd_64.h or online at the Linux kernel source.
Writing "Hello, World" in NASM
Create hello.asm:
cat > hello.asm <<'EOF'
section .data
msg db "Hello, World!", 0x0a ; message + newline
msglen equ $ - msg ; length calculated at assemble time
section .text
global _start
_start:
; write(1, msg, msglen)
mov rax, 1 ; syscall: write
mov rdi, 1 ; fd: stdout
mov rsi, msg ; pointer to string
mov rdx, msglen ; byte count
syscall
; exit(0)
mov rax, 60 ; syscall: exit
xor rdi, rdi ; status: 0
syscall
EOF
Assemble and Link with NASM
nasm -f elf64 -o hello.o hello.asm
ld -o hello hello.o
-f elf64 tells NASM to produce a 64-bit ELF object. ld links it into an executable with no C runtime — the entry point is _start, not main.
Run It
./hello
Output: Hello, World!
The Same Program in GAS (AT&T Syntax)
Create hello_gas.s:
cat > hello_gas.s <<'EOF'
.section .data
msg:
.ascii "Hello, World!\n"
.equ msglen, . - msg
.section .text
.global _start
_start:
movq $1, %rax # syscall: write
movq $1, %rdi # fd: stdout
leaq msg(%rip),%rsi # pointer to string (RIP-relative)
movq $msglen, %rdx # byte count
syscall
movq $60, %rax # syscall: exit
xorq %rdi, %rdi # status: 0
syscall
EOF
Notice AT&T differences: register names are prefixed with %, immediates with $, and operand order is source, destination (reversed from Intel). The q suffix on instructions means quadword (64-bit).
Assemble and Link with GAS
as -o hello_gas.o hello_gas.s
ld -o hello_gas hello_gas.o
Inspecting Your Binary
After building, use these tools to understand what you produced:
Disassemble with objdump
objdump -d -M intel hello
The -M intel flag switches objdump to Intel syntax even for GAS-assembled files. Output shows each instruction with its address and hex encoding — invaluable for debugging.
Check the ELF Headers
readelf -h hello
readelf -S hello
-h shows the ELF header including entry point address. -S lists sections (.text, .data, etc.) with their sizes and offsets.
Inspect the Final Binary Size
size hello
ls -lh hello
A no-libc x86-64 hello-world typically lands under 1 KB — compare that to a compiled C version linked dynamically.
Verification: Checking the Exit Code
After running your binary, confirm the kernel received the correct exit status:
./hello ; echo "Exit status: $?"
You should see Hello, World! followed by Exit status: 0. If you change xor rdi, rdi to mov rdi, 42, the exit status will be 42 — a simple way to verify your register manipulation is working.
Troubleshooting
Segmentation Fault Immediately
Most often caused by a missing or wrong entry point. Confirm global _start is present in NASM, or .global _start in GAS. Also check you linked with ld and not gcc without -nostdlib — GCC expects main and will inject startup code that crashes without it.
"Cannot find entry symbol _start"
The linker cannot locate the entry point. Either the label is missing, misspelled, or not declared global. In NASM, global _start must appear in the .text section before the label.
Wrong Syscall Number / Nothing Printed
Syscall numbers differ between 32-bit and 64-bit Linux. On x86-64, write is syscall 1. If you are referencing 32-bit tables, write appears as 4 — wrong on a 64-bit kernel when using the syscall instruction. Never mix 32-bit tables with syscall; they are for int 0x80.
Using strace to Debug Syscalls
strace ./hello
strace intercepts every syscall your program makes and prints the arguments and return values. It is the single most useful debugging tool for assembly programs short of a full debugger. Install with apt/dnf/pacman install strace if not already present.
Debugging with GDB
nasm -f elf64 -g -F dwarf -o hello.o hello.asm
ld -o hello hello.o
gdb ./hello
The -g -F dwarf flags embed DWARF debug info. Inside GDB, use layout regs for a live register view, si to step one instruction, and x/s $rsi to inspect memory at a register address.
Frequently asked questions
- Can I call C library functions like printf from assembly?
- Yes, but you must link against libc with ld --dynamic-linker /lib64/ld-linux-x86-64.so.2 -lc, declare the function extern, and follow the System V AMD64 ABI calling convention including 16-byte stack alignment before the call. It is simpler to learn raw syscalls first.
- What is the difference between the syscall instruction and int 0x80?
- int 0x80 is the 32-bit Linux syscall mechanism with a separate syscall table. On x86-64 you should always use the syscall instruction with 64-bit syscall numbers. Mixing int 0x80 with 64-bit code can technically work for simple cases but truncates 64-bit pointers to 32 bits and is never correct practice.
- Why does my program crash when I call a C function from assembly?
- The most common cause is a misaligned stack. The System V AMD64 ABI requires rsp to be 16-byte aligned immediately before a call instruction. Since call itself pushes an 8-byte return address, rsp must be 16-byte aligned at the point you execute call. Use sub rsp, 8 to pad if needed.
- How do I pass command-line arguments to my assembly program?
- On Linux, at program entry (_start), rsp points to argc (a 64-bit integer on the stack), followed by argv pointers. Load argc with mov rdi, [rsp] and the argv array starts at rsp+8. No special setup is needed; the kernel places this on the stack before transferring control.
- Is it worth learning assembly in 2024 with modern compilers?
- For writing production code, compilers win almost every time. Assembly remains essential for security research, exploit development, embedded/bare-metal work, understanding CPU behavior, writing compiler backends, and performance-critical hot paths where you need to hand-tune SIMD intrinsics or verify compiler output.
Related guides
AI and Artificial-Life Tools on Linux
Set up open-source AI/ML and artificial-life toolkits on Linux: PyTorch, JAX, DEAP, Avida, NetLogo, and RL environments with GPU driver guidance.
How to Benchmark Disk Performance with fio
Learn to benchmark Linux disk performance with fio: writing job files, testing latency and throughput, and interpreting IOPS and percentile output correctly.
The Linux Boot Process Explained
Trace the full Linux boot sequence from UEFI firmware through GRUB2, the kernel, initramfs, and systemd to your login prompt — with diagnostics at each stage.
Btrfs Basics and Snapshots
Learn Btrfs subvolumes, instant copy-on-write snapshots, and safe system rollback — with both manual btrfs commands and Snapper automation.