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.

Disasembl
Disasembl Team
·5 min read
Assemble Code Efficiently - Disasembl
Photo by deni_eliashvia Pixabay
Quick AnswerSteps

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
; NASM skeleton for 64-bit Linux global _start section .text global _start _start: mov rax, 60 ; exit xor rdi, rdi ; status 0 syscall
Bash
# Commands to assemble and link (Linux example) nasm -f elf64 hello.asm -o hello.o ld hello.o -o hello

Why 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.

Bash
# 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
PowerShell
# Windows (WSL or Git Bash with Debian-derived tools) sudo apt-get update sudo apt-get install nasm

Adjust 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.

NASM
; 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
Bash
# Assemble and link for Linux nasm -f elf64 hello.asm -o hello.o ld hello.o -o hello # Run the program ./hello

Line-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.

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.

Bash
# 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
Bash
# Inspect the result to confirm linkage file hello readelf -h hello

Why 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.

Bash
# 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'
Bash
# Simple cross-check: inspect a specific address objdump -d --start-address=0x400000 --stop-address=0x4000f0 hello

Common 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.

ASM
# 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
Bash
# GAS style compile and link (using GCC as driver) gcc -nostdlib -nostartfiles hello.s -o hello_gas

Why 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.

ASM
; NASM (Windows 64-bit) example skeleton [BITS 64] global _start _start: mov rax, 1 ; Windows console write setup would go here ret
Bash
# NASM + GoLink (Windows) example commands nasm -f win64 hello.asm -o hello.obj link hello.obj /SUBSYSTEM:CONSOLE /ENTRY:_start /MACHINE:X64 /OUT:hello.exe

Takeaway: 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.

Bash
# Add debug symbols (GCC/GAS style) nasm -f elf64 -g -F dwarf hello.asm -o hello.o gcc hello.o -o hello -no-pie
ASM
; Minimal debug-friendly code fragment (NASM) section .text global _start _start: nop ; placeholder for alignment ret

Pitfalls 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.

Bash
#!/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
ASM
; Example: multi-file NASM snippet ; main.asm extern util section .text global _start _start: call util mov rax, 60 xor rdi, rdi syscall

Takeaway: 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.

ASM
; NASM directive to ensure 64-bit mode BITS 64 global _start _start: nop ret
Bash
# Validate the final binary is indeed 64-bit and linked against the correct runtime readelf -h myapp | grep 64-bit file myapp

Closing 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. 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. 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. 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. 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. 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. 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.
Pro Tip: Keep syntax consistent with the target assembler to avoid misinterpretation.
Warning: Do not mix 32-bit and 64-bit operands in the same function without clear boundaries.
Note: Always verify using a disassembler to ensure encoding correctness.

Prerequisites

Required

Optional

Commands

ActionCommand
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.

Related Articles