Assembly Toolchain & First Program

Assembly is written for a toolchain, not for an IDE. You need two programs — an assembler and a linker — and once you know the three commands that turn text into a running process, every later lesson becomes a variation on the same loop.

What You Need

A bare-metal assembly program needs exactly two tools. Nothing else — no IDE, no framework, no package manager.

The Assembler: NASM

NASM (the Netwide Assembler) turns your text into an object file — machine code plus a list of symbols that are not yet resolved. It is small, fast, and its Intel-syntax manual is the reference used throughout this track. Two flags matter from the start: -f chooses the output format, and -o names the file it writes.

The alternatives are GNU as (AT&T syntax, part of binutils) and MASM/YASM (Microsoft-oriented). If you read Linux kernel code you will see GAS output; if you use MSVC you will see MASM. The ideas are identical — only the spelling changes.

The Linker

An object file cannot run. Addresses inside it are relative or named, and something must decide where everything finally lives and connect your code to the operating system. That "something" is the linker:

  • ld — the raw linker. Use it for syscall-only programs. It assumes the entry point is _start and adds no runtime.
  • gcc / clang — a compiler driver that also links. Use it when your program calls C library functions; it adds the C runtime and looks for main.

Installing the Tools

Install NASM and binutils with your system's package manager, then verify the version:

# Debian / Ubuntu / WSL2
sudo apt update && sudo apt install nasm binutils gdb

# Fedora / RHEL
sudo dnf install nasm binutils gdb

# Arch
sudo pacman -S nasm binutils gdb

# macOS (Homebrew) — assembles Mach-O, not ELF
brew install nasm

# Verify
nasm -v        # should print something like: NASM version 2.16.01
ld --version   # GNU ld (GNU Binutils)

gdb is not needed until the debugging lesson, but installing it now saves a step later. objdump, readelf, and nm all arrive with binutils — they are your window into what the assembler actually produced.

Your First Program

Here is a complete, runnable Linux x86-64 program that prints a line and exits. It uses the kernel directly — no C library — so you can see every byte of what it does.

The Source File

; hello.asm — write a line to stdout, then exit (Linux x86-64, no libc)
; Build:  nasm -f elf64 hello.asm -o hello.o
; Link:   ld hello.o -o hello
; Run:    ./hello

section .rodata                     ; read-only data
    msg     db  "Hello, world!", 10 ; 10 is the newline character '\n'
    msg_len equ $ - msg             ; constant = byte length of msg

section .text                       ; executable code
    global _start                   ; the linker's entry symbol

_start:
    mov     rax, 1                  ; rax = syscall number 1 = write
    mov     rdi, 1                  ; rdi = file descriptor 1 = stdout
    mov     rsi, msg                ; rsi = address of the first byte to write
    mov     rdx, msg_len            ; rdx = number of bytes to write
    syscall                         ; transfer control to the kernel

    mov     rax, 60                 ; rax = syscall number 60 = exit
    xor     rdi, rdi                ; rdi = exit status 0
                                    ; xor of a register with itself is the
                                    ; cheapest way to load the constant 0
    syscall                         ; exit; this never returns

Read it as a conversation with the kernel. First you place arguments in registers — the syscall number in rax, and up to six parameters in rdi, rsi, rdx, r10, r8, r9. Then syscall switches the CPU into kernel mode, performs the request, and returns with a result in rax.

The Same Program with libc

Once you link against the C library, you may use main and printf instead of raw syscalls. The program becomes shorter and portable, at the cost of no longer seeing exactly which system calls happen.

; hello_libc.asm — the same output through libc's printf
; Build:  nasm -f elf64 hello_libc.asm -o hello_libc.o
; Link:   gcc hello_libc.o -o hello_libc      (gcc adds the C runtime)
; Run:    ./hello_libc

section .rodata
    fmt     db  "Hello from libc, world!", 10, 0   ; 0 terminates the string

section .text
    global main                 ; gcc's entry point is main, not _start
    extern printf               ; printf is provided by the C library

main:
    push    rbp                 ; save the caller's frame pointer
    mov     rbp, rsp            ; establish our own frame (see Subprograms)

    lea     rdi, [rel fmt]      ; arg 1: pointer to the format string
                                ; [rel ...] keeps the code position-independent
    xor     eax, eax            ; AL = 0: no vector arguments are passed
    call    printf              ; call the C library

    xor     eax, eax            ; return 0 to the runtime
    pop     rbp                 ; restore the frame pointer
    ret                         ; return to the C runtime, which exits

The push rbp / mov rbp, rsp pair is not decoration — the System V ABI requires it, and printf will misbehave without a properly aligned stack. Stack frames are the subject of Subprograms & the Stack.

Try it: change 10 in msg_len equ $ - msg to 5 and rebuild. You will see only Hello — proof that the kernel writes exactly as many bytes as you tell it, no more and no less.

Anatomy of the Source File

Before writing anything, read a source file the way the assembler does: a list of lines, each belonging to a section, each either a directive that shapes the file or an instruction that becomes a byte.

Sections

A section tells the linker what kind of memory the following bytes belong in. You will use four of them:

SectionContentsWritable?
section .textInstructions — the code itselfNo (read + execute)
section .rodataConstants and strings that never changeNo (read only)
section .dataInitialized global variablesYes
section .bssUninitialized variables, reserved at run timeYes

Putting a string in .rodata instead of .data is not cosmetic: the operating system can share a read-only page between processes, and a stray write there crashes loudly instead of silently corrupting data.

The Entry Point

The global directive exports a symbol so the linker can see it, and the symbol named _start is where the process begins when you link with ld. Without it you get cannot find entry symbol _start. There is no main function and no runtime in front of you — the first instruction at _start is the first instruction executed.

section .text
    global _start      ; export this symbol; the linker uses it as the entry point

_start:                ; execution begins at this exact byte
    ; instructions go here

Labels and Constants

A label is a name for an address. Written as name: it marks the current position; written inside an expression it takes the value of that address. The special symbol $ means "the address here", which makes length calculations trivial.

section .rodata
    msg     db  "Hello", 10    ; msg is a label pointing at these bytes
    msg_len equ $ - msg        ; equ defines a compile-time constant:
                               ; ($) current address minus (msg) = 6 bytes
    newline db  10             ; a second label for a single byte

section .data
    counter dd  0              ; a 32-bit writable variable, initialized to 0
    buffer  times 64 db 0      ; 64 zero bytes (times repeats the definition)

equ creates a constant that exists only while assembling — it costs no memory and produces no bytes. That is the difference between equ and db: one is a number for the assembler, the other is a byte for the program.

The Edit–Assemble–Run Loop

The whole development cycle is four steps. Run them in a shell in the directory that holds your source file:

# 1. Assemble: text -> object file (.o).  -f elf64 says "64-bit Linux ELF".
nasm -f elf64 hello.asm -o hello.o

# 2. Link: object file -> executable.  -o names the output.
ld hello.o -o hello

# 3. Run it.
./hello

# 4. Look at what you produced (optional but educational).
objdump -d hello | less

Add -g -F dwarf to the nasm line while learning. That embeds debug information so GDB can show your source lines instead of raw addresses, and it changes nothing about how the program runs.

If you link with gcc instead of ld, the entry point becomes main rather than _start, and the C runtime is initialized for you before main is called. That is the version you want as soon as you need printf or any other library function.

Linux, macOS, Windows

The assembly source is nearly identical across systems; the differences are the object-file format and the way you ask the operating system for services.

SystemNASM formatLinkerEntry point
Linux x86-64elf64ld or gcc_start (or main)
macOS (Intel or Apple silicon)macho64ld (via clang)_main — note the underscore
Windows (native)win64link.exe / golinkmain
Windows (WSL2)elf64ld or gcc_start

The recommendation for this track is Linux or WSL2. Syscall numbers and the System V calling convention are the same everywhere on Linux, the toolchain installs with one command, and every example here has been written against that target.

Troubleshooting Common Errors

Every one of these messages has a boring, specific cause. Learn them once and you will never lose an hour to them again.

MessageWhat it really meansFix
parser: instruction expectedA line starts with something NASM does not recognize as an instruction, usually a label without its colon.Write start: not start.
ld: warning: cannot find entry symbol _startThe linker has no idea where execution begins.Add global _start in the source.
relocation R_X86_64_32S against ... can not be usedYou used an absolute address while producing a position-independent executable.Add default rel and write lea rdi, [rel msg].
Segmentation faultA pointer was wrong — bad address, wrong syscall argument, or you forgot to exit.Check operands with a debugger; verify the number of bytes passed to write.
Exec format errorThe object file was built for a different operating system or architecture.Rebuild with the format that matches your platform (elf64, macho64, win64).
Permission deniedYou tried to execute a file that is not marked executable.chmod +x ./hello

Summary

  • You need exactly two tools: an assembler (nasm) that makes an object file, and a linker (ld or gcc) that makes an executable.
  • The three commands are nasm -f <format>, ld (or gcc), then ./program.
  • A source file is organised into sections; execution starts at the symbol the linker is told about (_start or main).
  • Link with ld for a bare syscall program, and with gcc when you want to call the C library.

Next: Numbers & Memory — the binary and hexadecimal arithmetic that every instruction is built on.