Updated: Jul 11, 2026
| 11 min

x86-64 Assembly Hello World with NASM on Linux

Write and run Hello World in x86-64 assembly with NASM on Linux. Learn syscalls, Docker setup, assembling, linking, GDB, and Apple Silicon support.

Docker whale at a desk with containers for an x86-64 assembly development environment

In this beginner-friendly x86-64 assembly tutorial, you will write a Hello World program with NASM on Linux, call the Linux kernel directly with system calls, and build the executable with nasm and ld.

You will also create a reproducible Docker development environment that works on Linux, Windows through WSL, and Apple Silicon Macs through linux/amd64 emulation.

By the end of the tutorial, you will understand how source code becomes an object file, how the linker creates an executable, and how an assembly program prints text without a standard library.

What You Will Build

The final program prints one line and exits successfully:

Hello, World!

The finished project will contain these files:

hello-world-asm/
├── Dockerfile
├── docker.sh
├── hello.asm
└── Makefile

The build process has three stages:

  1. Assemble hello.asm into the object file hello.o.
  2. Link hello.o into the executable file hello.
  3. Run the executable inside the Linux development environment.

Why Learn x86-64 Assembly Language?

High-level languages are the right choice for most application development, but assembly language remains useful when you need to understand exactly what a processor is doing.

Learning assembly can help you understand:

  • how compilers translate source code into machine instructions;
  • how operating systems expose services through system calls;
  • how registers, memory addresses, and the stack work;
  • how debuggers inspect a running program;
  • how executable files and linkers work;
  • how reverse engineering and low-level security analysis are performed;
  • why certain pieces of code are faster or slower than others.

Most production software is not written entirely in assembly. Developers normally use it selectively for low-level routines, hardware-specific code, boot code, performance analysis, or situations where precise control is required.

Prerequisites for This NASM Tutorial

You need the following tools:

  • Docker Desktop or Docker Engine;
  • a terminal;
  • a text editor such as Visual Studio Code;
  • basic familiarity with files and command-line commands.

This tutorial uses NASM syntax, the x86-64 architecture, the ELF64 object format, and the Linux system-call interface. The generated executable is therefore a Linux x86-64 program, not a native Windows or macOS executable.

Create the project directory and enter it:

mkdir hello-world-asm
cd hello-world-asm

Set Up an x86-64 Assembly Environment with Docker

Docker gives every reader the same Linux toolchain, regardless of the host operating system. It also avoids installing NASM, GNU ld, make, and GDB directly on the host.

Create the Dockerfile

Inside hello-world-asm, create a file named Dockerfile:

FROM ubuntu:24.04

ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        binutils \
        gdb \
        make \
        nasm \
    && rm -rf /var/lib/apt/lists/*

RUN useradd --create-home --shell /bin/bash dev

USER dev
WORKDIR /workspace

This image contains four important packages:

  • nasm assembles NASM source code into an object file;
  • binutils provides the GNU linker ld and related binary tools;
  • make automates the build commands;
  • gdb lets you inspect registers and step through instructions.

The container runs as the non-root user dev, and the project directory will be mounted at /workspace.

Create the Docker Helper Script

Create a file named docker.sh:

#!/usr/bin/env bash
set -euo pipefail

IMAGE_NAME="${IMAGE_NAME:-nasm-dev}"
CONTAINER_NAME="${CONTAINER_NAME:-nasm-dev-container}"
PLATFORM="${PLATFORM:-linux/amd64}"
WORK_DIR="/workspace"
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

build_image() {
    docker build \
        --platform "$PLATFORM" \
        --tag "$IMAGE_NAME" \
        "$PROJECT_ROOT"
}

start_container() {
    if docker container inspect "$CONTAINER_NAME" >/dev/null 2>&1; then
        docker start "$CONTAINER_NAME" >/dev/null 2>&1 || true
        docker exec -it "$CONTAINER_NAME" bash
        return
    fi

    docker run \
        --platform "$PLATFORM" \
        --name "$CONTAINER_NAME" \
        --hostname nasm-dev \
        --interactive \
        --tty \
        --cap-add SYS_PTRACE \
        --security-opt seccomp=unconfined \
        --volume "$PROJECT_ROOT:$WORK_DIR" \
        --workdir "$WORK_DIR" \
        "$IMAGE_NAME" \
        bash
}

stop_container() {
    docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
}

clean_environment() {
    docker rm --force "$CONTAINER_NAME" >/dev/null 2>&1 || true
    docker image rm "$IMAGE_NAME" >/dev/null 2>&1 || true
}

print_usage() {
    echo "Usage: ./docker.sh {build|start|stop|clean}"
}

case "${1:-}" in
    build) build_image ;;
    start) start_container ;;
    stop) stop_container ;;
    clean) clean_environment ;;
    *) print_usage ;;
esac

Make the script executable:

chmod +x docker.sh

The script supports four commands:

./docker.sh build
./docker.sh start
./docker.sh stop
./docker.sh clean

The SYS_PTRACE capability and relaxed seccomp option allow GDB to trace the program inside the container. They are included for local development and debugging.

Build and Start the Container

Build the Docker image:

./docker.sh build

Start the development container:

./docker.sh start

Your terminal should now be inside /workspace, which maps to the project directory on your computer. Files created in the container will remain available on the host.

Run x86-64 Assembly on Apple Silicon

Apple Silicon processors use the ARM64 architecture, while this tutorial creates an x86-64 Linux executable. The helper script therefore uses this target platform by default:

linux/amd64

Docker Desktop can emulate that platform on an Apple Silicon Mac. The same setting also works natively on an x86-64 Linux or Windows host.

If you remove the --platform linux/amd64 setting on an ARM64 host, the container may use ARM64 tools and the resulting x86-64 workflow may fail with an architecture or executable-format error.

Optional: Install the Tools Directly on Linux

Docker is not required when you already use an x86-64 Linux system. On Ubuntu or Debian, install the toolchain directly with:

sudo apt update
sudo apt install nasm binutils make gdb

You can then run the remaining commands in your normal terminal.

Understand the Structure of a NASM Assembly File

Assembly programs are divided into sections. Each section has a different purpose.

The .data Section

The .data section stores initialized writable data. Our message will be placed here because its bytes are known when the program is assembled.

section .data

Read-only constants can also be placed in a .rodata section. We will use .data to keep the first example simple.

The .bss Section

The .bss section reserves space for data that does not need an initial value in the executable file. Programs commonly use it for buffers and variables that start at zero.

This Hello World program does not need a .bss section.

The .text Section

The .text section contains executable machine instructions:

section .text

Our program begins at a label named _start. Because we link directly with ld and do not use the C runtime, _start is the process entry point instead of main.

Write Hello World in x86-64 Assembly

Create a file named hello.asm:

global _start

section .data
    message         db "Hello, World!", 10
    message_length  equ $ - message

section .text
_start:
    ; write(STDOUT, message, message_length)
    mov eax, 1
    mov edi, 1
    lea rsi, [rel message]
    mov edx, message_length
    syscall

    ; exit(0)
    mov eax, 60
    xor edi, edi
    syscall

This program does not call printf, use a standard library, or start through a language runtime. It communicates directly with the Linux kernel.

Define the Hello World Message with db

The db directive means define byte. NASM places the supplied byte values into the object file:

message db "Hello, World!", 10

The characters in the string are stored as bytes. The final value, 10, is the ASCII line-feed character, which moves the terminal cursor to the next line.

The label message represents the address of the first byte.

Calculate the String Length with equ

The program must tell Linux how many bytes to write. NASM can calculate the length during assembly:

message_length equ $ - message

Here:

  • $ is NASM’s current assembly position;
  • message is the address where the string begins;
  • $ - message is the number of bytes between those positions;
  • equ defines a constant and does not allocate additional memory.

Calculating the length this way prevents the value from becoming incorrect when the text changes.

How Linux x86-64 System Calls Work

A system call requests a service from the Linux kernel. This program uses two system calls:

  1. write sends bytes to standard output;
  2. exit terminates the process.

For Linux x86-64 system calls, the program places the system-call number in rax. The first arguments are placed in rdi, rsi, and rdx, followed by additional registers when needed. The syscall instruction transfers control to the kernel.

Call write to Print the Message

Conceptually, the program makes this request:

write(1, message, message_length);

The three arguments are:

  • 1: the file descriptor for standard output;
  • message: the address of the bytes to print;
  • message_length: the number of bytes to print.

The corresponding assembly is:

mov eax, 1
mov edi, 1
lea rsi, [rel message]
mov edx, message_length
syscall

Register usage:

RegisterValuePurpose
rax1System-call number for write
rdi1Standard-output file descriptor
rsiAddress of messageBuffer to write
rdxmessage_lengthNumber of bytes

The instruction lea rsi, [rel message] loads the address of the message into rsi using a relative reference.

Call exit to End the Process

After printing the text, the program must terminate:

mov eax, 60
xor edi, edi
syscall

The value 60 selects the Linux x86-64 exit system call. Its first argument is the process exit status.

The instruction:

xor edi, edi

sets edi to zero efficiently, producing an exit status of 0, which conventionally means success.

Source code cannot run directly. NASM first converts the assembly source into an ELF64 object file. GNU ld then combines that object file into a Linux executable.

Step 1: Assemble hello.asm

Run:

nasm -f elf64 hello.asm -o hello.o

The options mean:

  • -f elf64: generate a 64-bit ELF object file;
  • hello.asm: read this source file;
  • -o hello.o: write the object file to hello.o.

An object file contains machine code and metadata, but it is not yet the final executable.

Run:

ld hello.o -o hello

The linker resolves symbols, selects the entry point, lays out the executable sections, and creates the file named hello.

Step 3: Run the Executable

Run:

./hello

The terminal should display:

Hello, World!

Check the exit status:

echo $?

The output should be:

0

Automate the Build with a Makefile

Typing the assembler and linker commands manually is useful for learning, but a Makefile makes repeated builds faster and less error-prone.

Create a file named Makefile:

TARGET := hello
SOURCE := hello.asm
OBJECT := hello.o

NASM := nasm
LD := ld

NASMFLAGS := -f elf64

.PHONY: all run clean

all: $(TARGET)

$(OBJECT): $(SOURCE)
	$(NASM) $(NASMFLAGS) $< -o $@

$(TARGET): $(OBJECT)
	$(LD) $< -o $@

run: $(TARGET)
	./$(TARGET)

clean:
	rm -f $(OBJECT) $(TARGET)

The command lines under each target must begin with a tab character.

Build the program:

make

Build and run it:

make run

Remove the generated object and executable files:

make clean

make only rebuilds a target when one of its dependencies has changed. Editing hello.asm and running make again will therefore reassemble and relink the program automatically.

Debug x86-64 Assembly with GDB

GDB lets you pause the program, inspect registers, and execute one machine instruction at a time.

Start GDB:

gdb ./hello

At the GDB prompt, set a breakpoint at the entry point and run the program:

break _start
run

Useful commands include:

info registers
x/s &message
disassemble _start
stepi
continue
quit
  • info registers displays the current register values;
  • x/s &message examines the string in memory;
  • disassemble _start shows the instructions in the program;
  • stepi executes one instruction;
  • continue resumes normal execution.

Stepping through the program is one of the fastest ways to understand how values move into registers before a system call.

Common NASM, Docker, and Linking Errors

nasm: command not found

The toolchain is not installed in the current environment. Build and enter the Docker container:

./docker.sh build
./docker.sh start

When working directly on Ubuntu or Debian, install the nasm package.

cannot execute binary file or Exec format error

The executable architecture does not match the environment running it. On Apple Silicon, confirm that both the Docker build and container use:

linux/amd64

The helper script already applies that platform by default.

ld: cannot find hello.o

The object file has not been created, or the assembler command failed. Run:

nasm -f elf64 hello.asm -o hello.o

Fix any NASM error before running ld again.

make: Nothing to be done for 'all'

The executable is already up to date. Change hello.asm, or rebuild from scratch:

make clean
make

The Docker Script Is Not Executable

Add execute permission:

chmod +x docker.sh

Then run it with:

./docker.sh start

On Windows, run the Bash script through WSL or Git Bash rather than directly in standard PowerShell.

Frequently Asked Questions

What is NASM?

NASM is an assembler for the x86 and x86-64 architectures. It uses an Intel-style assembly syntax and can generate several object formats, including ELF64 for 64-bit Linux programs.

Does NASM create the final executable?

NASM creates the object file. A linker such as GNU ld creates the final executable from one or more object files.

In this tutorial:

hello.asm --nasm--> hello.o --ld--> hello

Why does the program use _start instead of main?

main is normally called by language-runtime startup code. This tutorial links directly with ld and does not include the C runtime, so execution begins at the lower-level _start symbol.

Can this Linux assembly program run directly on macOS?

No. The executable uses the Linux ELF format and Linux system calls. Docker can run it inside a Linux x86-64 container on macOS, including Apple Silicon Macs through emulation.

Why use 32-bit register names such as eax and edi in a 64-bit program?

Writing to a 32-bit general-purpose register clears the upper 32 bits of its corresponding 64-bit register. The system-call numbers, file descriptor, string length, and exit status in this example all fit within 32 bits.

The message address requires a 64-bit register, so the program loads it into rsi.

Is assembly always faster than C or C++?

No. Modern compilers perform sophisticated optimizations and often generate highly efficient machine code. Assembly is most valuable when precise hardware control, low-level integration, binary analysis, or carefully measured optimization is required.

Next Steps After Hello World

You have now written, assembled, linked, executed, and debugged a complete x86-64 Linux assembly program.

Good next exercises include:

  1. change the message and verify that message_length updates automatically;
  2. print two separate strings with two write system calls;
  3. read text from standard input with the read system call;
  4. inspect the executable with objdump and readelf;
  5. learn how the stack stores return addresses and local data;
  6. call an assembly function from C;
  7. explore loops, comparisons, and conditional jumps.

Although Hello World is small, it introduces the complete low-level path from source text to CPU instructions: NASM creates the machine code, the linker creates the executable, Linux loads the process, and system calls connect the program to the kernel.