$linuxjunkies
>

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.

AdvancedUbuntuDebianFedoraArch12 min readUpdated June 7, 2026

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 with binutils and is what GCC emits. Defaults to AT&T syntax (source before destination), though .intel_syntax noprefix switches 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, r8r15, plus rsp (stack pointer) and rbp (base pointer). Each has 32-bit (eax), 16-bit (ax), and 8-bit (al/ah) aliases.
  • Stack: grows downward in memory. push decrements rsp; pop increments it. Always 16-byte aligned before a call per 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 number
  • rdi, 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):

NumberNamePurpose
0readRead from file descriptor
1writeWrite to file descriptor
60exitTerminate process
231exit_groupTerminate 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
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).

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.

tested on:Ubuntu 24.04Fedora 40Arch 2024.05.01Debian 12

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