How to Assemble Assembly Code: Step-by-Step Guide
Learn how to assemble assembly code with NASM: write source, assemble to object code, link to an executable, and verify results with disassembly. Practical steps, flags, and tips for cross-platform assembly.

To assemble assembly code, start by writing your source in NASM syntax, then assemble to an object file with nasm -f elf64, and link to an executable with ld. Finally, verify the machine code with a disassembly tool like objdump. This quick workflow applies across Linux and Windows with compatible toolchains.
Introduction: what it means to assemble assembly code
In this article we answer the practical question: how to assemble assembly code? “Assemble” means translating human-readable assembly into machine instructions that a processor understands. The Disasembl team emphasizes a repeatable toolchain: write clean NASM syntax, assemble to object files, link into an executable, then inspect the resulting machine code. This section sets the stage for a concrete workflow that works on major x86-64 targets. The keyword to anchor this guide is how to assemble assembly code, because a solid assembly process starts with well-structured sources and ends with verifiable binaries. To begin, we show a minimal NASM skeleton you can adapt for your project.
; NASM skeleton for 64-bit Linux
global _start
section .text
global _start
_start:
mov rax, 60 ; exit
xor rdi, rdi ; status 0
syscall# Commands to assemble and link (Linux example)
nasm -f elf64 hello.asm -o hello.o
ld hello.o -o helloWhy this matters: a clear separation between source, object, and executable helps debugging and optimization. This section also demonstrates the standard ABI conventions that guide your code, which is crucial when you move from hello world to real applications.
Prerequisites and toolchain setup
Before you can answer how to assemble assembly code in practice, you need the right tools installed and configured. This block walks through a minimal setup you can adapt to your OS. The NASM assembler handles Intel syntax well, while the linker (ld or gcc) produces a runnable binary. You’ll also gain a baseline understanding of file formats (ELF on Linux, PE on Windows) and the expected output object files. Disasembl’s guidance emphasizes consistency across environments to reduce surprises when switching from Linux to Windows or macOS.
# NASM (64-bit) and GCC toolchain on Debian/Ubuntu
sudo apt-get update
sudo apt-get install nasm build-essential
# macOS (Homebrew)
brew install nasm
xcode-select --install# Windows (WSL or Git Bash with Debian-derived tools)
sudo apt-get update
sudo apt-get install nasmAdjust for your platform: Windows users may prefer NASM with MinGW-w64 or MSVC-compatible steps; macOS users will link with ld or clang as needed. The goal is to have a consistent sequence: write -> assemble -> link -> verify.
Write your first NASM program and test locally
A practical way to learn how to assemble assembly code is to start with a tiny program that writes to stdout and exits. This block shows a minimal Hello, world style example in NASM syntax, plus a brief explanation of the related directives and how they map to the Linux x86-64 ABI. The code demonstrates the data and text sections, proper global entry, and a simple system call path. You’ll then see how the assembler interprets these directives and produces an object file that the linker can turn into an executable.
; hello.asm - minimal Linux x86-64 program
section .data
msg db 'Hello, Disasembl!', 0x0A
len equ $ - msg
section .text
global _start
_start:
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, msg ; pointer to message
mov rdx, len ; message length
syscall
mov rax, 60 ; sys_exit
xor rdi, rdi ; status 0
syscall# Assemble and link for Linux
nasm -f elf64 hello.asm -o hello.o
ld hello.o -o hello
# Run the program
./helloLine-by-line breakdown:
- The data section defines a message and its length; the text section contains the entry point _start.
- System calls are made with the syscall instruction, following the Linux x86-64 ABI.
- The exit code returns 0 to the shell. This pattern establishes a repeatable baseline for more complex programs.
Assemble to object file and link to executable
Once you have a NASM source file, the next two steps are to assemble it into an object file and then link that object into an executable. The commands shown here are the core workflow for Linux and similar for Windows with a compatible toolchain. You’ll see how the ELF format is used for object files and how the linker resolves symbols, relocations, and runtime entry points. This block also introduces common flags and how they influence debugging symbols and optimization levels.
# Assemble to an ELF64 object file
nasm -f elf64 hello.asm -o hello.o
# Link to create an executable; on some systems you may need -dynamic-linker or -static
ld hello.o -o hello
# If you prefer GCC driver for automatic libc and startup code linking
# gcc hello.o -o hello# Inspect the result to confirm linkage
file hello
readelf -h helloWhy these steps matter: object files contain relocation entries and symbol tables required by the linker. You’ll adjust your code to satisfy the linker’s expectations (entry points, ABI compliance) as you expand from a tiny hello program to a real application.
Verifying machine code: disassembly and inspection
Disassembling the executable is a critical part of the assembly workflow. It helps you confirm that the machine code matches your intended instructions and ABI conventions. This block demonstrates how to use objdump or a modern alternative like radare2 or xxd for low-level inspection. You’ll learn to locate the text segment, confirm function prologues, and verify instruction encoding. Such verification is essential when optimizing or porting code between compilers and assemblers. The examples below show common commands and interpretation tips.
# Show the disassembly of the executable
objdump -d hello | sed -n '1,60p'
# Quick symbol/table check and section layout
readelf -S hello | sed -n '1,80p'# Simple cross-check: inspect a specific address
objdump -d --start-address=0x400000 --stop-address=0x4000f0 helloCommon variations: for Windows PE targets, you might use dumpbin or höher-level tools; for macOS, use otool -tvV. The essential idea remains: compare your intended assembly with the actual emitted binary to catch off-by-one errors, incorrect immediates, or wrong register usage.
Exploring GAS AT&T syntax and cross-assembler variations
Not all projects stick to NASM syntax. This block introduces GAS (GNU Assembler) with AT&T syntax, a common alternative on Unix-like systems. You’ll see how labels, directives, and operand order differ, and you’ll compare how to express the same logic using Intel vs. AT&T style. Understanding both helps you adapt codebases and validates portable assembly concepts. Included are two code examples: NASM-style and GAS-style, both assembled and linked to the same final binary, highlighting the translation between syntaxes.
# GAS AT&T syntax (Linux x86-64)
.intel_syntax noprefix
.global _start
_start:
mov rax, 1
mov rdi, 1
mov rsi, msg
mov rdx, len
syscall# GAS style compile and link (using GCC as driver)
gcc -nostdlib -nostartfiles hello.s -o hello_gasWhy it helps: cross-syntax familiarity prevents vendor lock-in and expands portability. If you must interact with third-party assemblers, this awareness is invaluable for debugging and maintenance.
Windows-focused assembly: NASM and MASM basics
Cross-platform development requires awareness of platform-specific assemblers. This block briefly outlines Windows workflows using NASM and MASM. You’ll see Windows-specific directives, and a simple example to produce a console application. The process mirrors Linux: write source, assemble to object, and link to an executable, but targets and system calls differ (for example, using WriteFile or WriteConsole instead of Linux sys_write). The commands demonstrate a minimal Windows-ready path to the same logic as the Linux hello.asm example.
; NASM (Windows 64-bit) example skeleton
[BITS 64]
global _start
_start:
mov rax, 1
; Windows console write setup would go here
ret# NASM + GoLink (Windows) example commands
nasm -f win64 hello.asm -o hello.obj
link hello.obj /SUBSYSTEM:CONSOLE /ENTRY:_start /MACHINE:X64 /OUT:hello.exeTakeaway: the assembly process fundamentally remains: source -> object -> executable, but the system interfaces (syscalls, libraries) differ by platform. Knowing both paths reduces risk when porting modules between Linux and Windows.
Debugging, optimization, and common mistakes
A robust approach to how to assemble assembly code includes debugging and optimization strategies. This section covers adding debug symbols, exploring optimization opportunities, and avoiding common mistakes that slow down development. Key ideas include enabling symbol generation during assembly or linking, verifying calling conventions, and keeping code readable to facilitate future changes. You’ll also see a compact example that adds a simple debug message using a GNU toolchain, which illustrates how to attach extra data without breaking ABI compatibility.
# Add debug symbols (GCC/GAS style)
nasm -f elf64 -g -F dwarf hello.asm -o hello.o
gcc hello.o -o hello -no-pie; Minimal debug-friendly code fragment (NASM)
section .text
global _start
_start:
nop ; placeholder for alignment
retPitfalls to avoid: mismatched bitness (32 vs 64-bit), wrong linking order, and neglecting the correct entry point. Disasembl emphasizes verifying that the final binary is what you intended, especially after porting or upgrading toolchains. When in doubt, reassemble with minimal changes, then incrementally reintroduce features.
Advanced topics: using build scripts and integration tips
As you scale up from toy examples to real projects, you’ll want to automate the assemble-and-link process. This block presents a lightweight build script approach that triggers NASM and the linker, plus a simple CI-friendly workflow. You’ll see how to parameterize architecture forms, handle multiple source files, and emit atomic object files that can be linked together during a final stage build. The included code snippets demonstrate a repeatable pattern you can adapt to large projects.
#!/usr/bin/env bash
set -euo pipefail
SRC=(main.asm util.asm)
OBJ=()
for f in "${SRC[@]}"; do
obj="${f%.asm}.o"
nasm -f elf64 "$f" -o "$obj" -g -F dwarf
OBJ+=("$obj")
done
ld ${OBJ[@]} -o myapp; Example: multi-file NASM snippet
; main.asm
extern util
section .text
global _start
_start:
call util
mov rax, 60
xor rdi, rdi
syscallTakeaway: build automation is not optional for larger projects. The more you codify the assemble-and-link steps, the more reliable and portable your binaries become. This approach also simplifies reproducible debugging across environments and CI pipelines.
Common variations, optimization tips, and final thoughts
Finally, the article closes with practical tips to optimize your assembly workflow and avoid common mistakes. You’ll learn how to choose between NASM, GAS, MASM, and other assemblers based on target platforms, how to tune linker options for reduced binary size, and how to structure code for readability and maintainability. The goal is a repeatable, dependable process for producing correct and efficient machine code that you can explain to teammates or auditors.
; NASM directive to ensure 64-bit mode
BITS 64
global _start
_start:
nop
ret# Validate the final binary is indeed 64-bit and linked against the correct runtime
readelf -h myapp | grep 64-bit
file myappClosing tip: keep your source modular, document ABI decisions, and use disassembly checks as part of your reviews. A disciplined workflow is your best defense against subtle bugs that only reveal themselves at runtime.
Steps
Estimated time: 20-40 minutes
- 1
Install and configure the toolchain
Install NASM and a compatible linker. Ensure the shell can invoke nasm, ld, and objdump. Verify versions to avoid compatibility issues.
Tip: Use the latest stable NASM and a well-supported linker for your platform. - 2
Write a minimal NASM program
Create a small source file with sections and a simple exit path. Keep the first version readable and well-commented to ease later changes.
Tip: Comment instructions to map to the intended ABI behavior. - 3
Assemble to object file
Run nasm to produce an object file in the proper format (ELF64 on Linux).
Tip: Check for syntax errors and confirm that the object file exists. - 4
Link to an executable
Link the object file to produce a runnable binary; ensure correct entry point and runtime.
Tip: Optionally include -pie or -no-pie depending on your target. - 5
Verify with disassembly
Disassemble the binary to verify that the emitted machine code matches expectations.
Tip: Focus on the _text_ section and the prologue/epilogue of functions. - 6
Iterate and optimize
Refine instructions, test behavior, and re-check with disassembly after each change.
Tip: Keep performance goals in mind and profile if needed.
Prerequisites
Required
- Required
- Required
- A POSIX-like shell environment (Linux/macOS; WSL on Windows is fine)Required
- Basic knowledge of x86-64 assembly and the target ABIRequired
Optional
- A text editor (VS Code, Vim, or similar)Optional
- Optional
Commands
| Action | Command |
|---|---|
| Assemble source to objectELF64 format on Unix-like systems; adjust to your target (COFF for Windows). | nasm -f elf64 file.asm -o file.o |
| Link object to executableLD linker; on macOS or Windows, prefer appropriate toolchain (clang or gowin). | ld file.o -o program |
| Inspect disassembly of the executableCross-check instruction encoding and ABI compliance. | objdump -d program |
Got Questions?
What is the difference between NASM and GAS syntax?
NASM uses Intel syntax with a straightforward directive style, while GAS often uses AT&T syntax by default. Both can produce the same machine code if you translate instructions correctly. The choice depends on your project and toolchain
NASM uses Intel syntax. GAS uses AT&T syntax by default. Both can assemble the same code when you adjust directives and syntax.
Do I always need to link with a C runtime?
Not for tiny demos. If your program uses system calls directly (like Linux write), you can link with a minimal startup. Most real projects link against libc or provide a custom runtime.
No, not always. Tiny demos can avoid libc by using direct system calls; larger projects usually link a runtime.
Which platforms support NASM?
NASM supports Linux, macOS, and Windows with appropriate toolchains. You can build for ELF64 or PE32/PE64 formats depending on the target.
NASM works on Linux, macOS, and Windows with the right linker and format.
What is the best practice for debugging assembly?
Use a debugger and disassembler to step through instructions and inspect registers. Enable debug symbols when possible and keep code modular for easier testing.
Debug with a debugger and inspect registers; enable symbols when you can.
How do I verify the ABI compliance of my code?
Ensure proper calling conventions, register usage, and stack alignment as dictated by the target ABI. Review the platform’s documentation and compare against a reference implementation.
Check the calling convention, registers, and stack alignment per the platform ABI.
What to Remember
- Write clear NASM source before assembling.
- Assemble and link in distinct steps to control the workflow.
- Use disassembly to verify machine code matches intent.
- Adapt commands for your platform’s ABI and toolchain.