Installing Assemblers
NASM (Netwide Assembler)
NASM is the recommended assembler for this series—it's cross-platform, well-documented, and uses Intel syntax.
Linux (Debian/Ubuntu)
sudo apt update
sudo apt install nasm
# Verify installation
nasm --version
macOS
# Install via Homebrew (macOS ships with an older NASM)
brew install nasm
# Verify installation
nasm --version
Windows
# Using Chocolatey (run as Administrator)
choco install nasm
# Or download installer from https://www.nasm.us/
# Important: Add NASM to your PATH after manual installation
nasm --version
MASM (Microsoft Macro Assembler)
MASM is Microsoft's assembler, tightly integrated with Visual Studio. It's the standard choice for Windows assembly development and uses Intel syntax.
Two MASM Versions: MASM comes in two flavors: ml.exe (32-bit) and ml64.exe (64-bit). Both are included with Visual Studio's C++ workload.
Option 1: Install with Visual Studio
Windows Only
# During Visual Studio installation, select:
1. "Desktop development with C++" workload
2. Under "Individual Components", ensure:
- MSVC v143 (or latest) build tools
- C++ ATL for latest build tools (x86 & x64)
# After installation, MASM is located at:
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\[version]\bin\Hostx64\x64\ml64.exe
Option 2: Visual Studio Build Tools (Smaller Download)
If you don't need the full IDE, install just the build tools:
Windows Only
# Download Visual Studio Build Tools from:
# https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022
# After install, open "x64 Native Tools Command Prompt"
# This sets up the environment with correct paths
# Test MASM
ml64 /?
MASM Hello World Example
; hello_masm.asm - 64-bit Windows Console
INCLUDELIB kernel32.lib
; External Windows API functions
EXTRN GetStdHandle:PROC
EXTRN WriteConsoleA:PROC
EXTRN ExitProcess:PROC
.data
msg db "Hello from MASM!", 13, 10, 0
msgLen equ $ - msg
.data?
written dq ?
.code
main PROC
; Get stdout handle
mov ecx, -11 ; STD_OUTPUT_HANDLE
call GetStdHandle
mov rbx, rax ; Save handle
; Write to console
sub rsp, 40 ; Shadow space + 5th arg
mov rcx, rbx ; Handle
lea rdx, msg ; Buffer
mov r8d, msgLen ; Length
lea r9, written ; Bytes written
mov qword ptr [rsp+32], 0 ; Reserved
call WriteConsoleA
add rsp, 40
; Exit
xor ecx, ecx
call ExitProcess
main ENDP
END
Windows (x64 Native Tools Command Prompt)
ml64 /c hello_masm.asm
link hello_masm.obj /subsystem:console /entry:main
hello_masm.exe
GNU Assembler (GAS)
GAS is part of the GNU Binutils and comes pre-installed with GCC. It uses AT&T syntax by default but can also use Intel syntax.
AT&T vs Intel Syntax: GAS traditionally uses AT&T syntax (source, destination order with % and $ prefixes). Most assembly programmers find Intel syntax more readable. Fortunately, GAS supports both.
Installing GAS
Linux GAS comes with binutils (usually pre-installed):
sudo apt install binutils
# Verify - GAS is invoked via 'as' command
as --version
macOS
# Xcode Command Line Tools includes 'as'
xcode-select --install
# Or install GNU binutils via Homebrew
brew install binutils
# Important: macOS 'as' is Clang's integrated assembler, not GNU as
# For GNU as, use the Homebrew version explicitly:
# /usr/local/opt/binutils/bin/as (Intel Mac)
# /opt/homebrew/opt/binutils/bin/as (Apple Silicon)
as --version
GAS Syntax Comparison
| Feature |
AT&T Syntax (Default) |
Intel Syntax |
| Register prefix |
%rax |
rax |
| Immediate prefix |
$10 |
10 |
| Operand order |
Source, Dest (mov %rax, %rbx) |
Dest, Source (mov rbx, rax) |
| Size suffix |
movl, movq |
Inferred or explicit |
| Memory access |
(%rax) |
[rax] |
GAS with Intel Syntax
# hello_gas.s - GAS with Intel syntax
.intel_syntax noprefix # Enable Intel syntax
.section .data
msg: .ascii "Hello from GAS!\n"
len = . - msg
.section .text
.global _start
_start:
# Write syscall (Linux x86-64)
mov rax, 1 # sys_write
mov rdi, 1 # stdout
lea rsi, [rip + msg] # message pointer (RIP-relative)
mov rdx, len # length
syscall
# Exit syscall
mov rax, 60 # sys_exit
xor rdi, rdi # status 0
syscall
Linux
# Assemble and link
as hello_gas.s -o hello_gas.o
ld hello_gas.o -o hello_gas
./hello_gas
macOS Difference: macOS uses the
Mach-O binary format instead of ELF. The
as command on macOS is Clang's assembler, and linking requires different flags:
# macOS: Use Mach-O format and different linker flags
as hello_gas.s -o hello_gas.o
ld -macos_version_min 10.13 -e _start -static hello_gas.o -o hello_gas
Key differences: macOS requires underscore-prefixed symbols (_start → __start), uses Mach-O instead of ELF, and syscall numbers differ from Linux (macOS x86-64 syscalls are offset by 0x2000000).
GAS via GCC (Recommended for C Interop)
# Compile assembly with GCC (handles linking automatically)
gcc -nostdlib hello_gas.s -o hello_gas
# Or for inline assembly in C
gcc -c -masm=intel program.c -o program.o
Assembler Comparison Quick Reference
Decision Guide
| Use Case |
Recommended Assembler |
| Learning assembly |
NASM (clean Intel syntax, excellent documentation) |
| Windows-specific development |
MASM (best Windows API integration) |
| Mixed C and assembly |
GAS (seamless GCC integration) |
| Linux kernel modules |
GAS (Linux kernel standard) |
| Cross-platform projects |
NASM (works on all major platforms) |
For detailed assembler comparison including FASM, YASM, UASM, and LLVM-AS, see Part 6: Complete Assembler Comparison.
Compilers & Linkers
Even though we're writing assembly, compilers and linkers are essential tools. Compilers help with C interop and disassembly analysis, while linkers combine object files into executables.
Real-World Analogy: Think of the assembler as a translator who converts your instructions to a foreign language (machine code). The linker is like a book publisher who takes all the translated chapters, adds the table of contents, and binds them into a complete book (executable).
GCC (GNU Compiler Collection)
GCC is more than a C compiler—it's a complete toolchain for compiling and linking programs. It integrates seamlessly with assembly.
Linux (Debian/Ubuntu)
sudo apt install build-essential
# Install both 32-bit and 64-bit support
sudo apt install gcc-multilib
# Verify installation
gcc --version
ld --version # GNU linker comes with GCC
macOS
# Xcode Command Line Tools (includes Clang aliased as 'gcc')
xcode-select --install
# Important: macOS 'gcc' is actually Clang
gcc --version # Shows "Apple clang version ..."
# For real GNU GCC, install via Homebrew
brew install gcc
gcc-14 --version # Use versioned binary (e.g., gcc-14)
# Note: gcc-multilib is NOT available on macOS
# Use -m32 flag with Clang or cross-compile via Docker/QEMU
Windows
# Option 1: MSYS2 (recommended for GNU toolchain on Windows)
# Download from https://www.msys2.org/
# In MSYS2 UCRT64 terminal:
pacman -S mingw-w64-ucrt-x86_64-gcc
# Option 2: Chocolatey
choco install mingw
gcc --version
Using GCC with Assembly
# Compile C file to see assembly output
gcc -S -fverbose-asm -O2 program.c -o program.s
# Use Intel syntax (easier to read)
gcc -S -masm=intel -O2 program.c -o program.s
# Compile assembly to object file using GCC
gcc -c program.s -o program.o
# Link assembly with C runtime (for printf, etc.)
gcc program.o -o program
# Compile and link in one step (recommended for beginners)
gcc main.c helper.asm -o program
Practical Exercise: Examine Compiler Output
Understanding compiler output helps you write better assembly:
// simple.c
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(5, 3);
return result;
}
# Generate assembly with different optimization levels
gcc -S -masm=intel -O0 simple.c -o simple_O0.s # No optimization
gcc -S -masm=intel -O2 simple.c -o simple_O2.s # Standard optimization
gcc -S -masm=intel -O3 simple.c -o simple_O3.s # Aggressive optimization
# Compare the outputs - you'll learn optimization patterns!
Clang / LLVM
Clang is an alternative to GCC with better error messages and faster compile times. LLVM provides a modular compiler infrastructure.
Linux
sudo apt install clang llvm
clang --version
macOS Clang is the default compiler (pre-installed with Xcode tools):
# Usually pre-installed; if not:
xcode-select --install
clang --version
Windows
# Included with Visual Studio C++ workload, or standalone:
# Download from https://releases.llvm.org/download.html
# Or via Chocolatey:
choco install llvm
clang --version
All Platforms Usage is identical once installed:
# Clang works like GCC for assembly workflows
clang -S -masm=intel program.c -o program.s
clang program.s -o program
Linkers: ld, lld, and link.exe
The linker combines object files, resolves symbols, and creates the final executable. Understanding linkers helps debug "undefined reference" errors.
What Linkers Do:
- Combine multiple object files (
.o or .obj)
- Resolve external symbol references (e.g.,
printf)
- Apply relocations (adjust addresses for final layout)
- Create executable headers (ELF, PE) with entry point
GNU ld (Standard Linux Linker)
Linux / WSL
# Direct invocation for standalone assembly
ld program.o -o program
# Link 32-bit binary
ld -m elf_i386 program.o -o program
# Link with libraries
ld program.o -lc -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o program
# Specify entry point (default is _start)
ld program.o -e main -o program
# Create position-independent executable (PIE)
ld -pie program.o -o program
lld (LLVM Linker - Faster Alternative)
Linux macOS
# Install
sudo apt install lld # Linux (Debian/Ubuntu)
brew install llvm # macOS (lld included with LLVM)
# Direct usage
ld.lld program.o -o program
# Use through GCC/Clang (recommended)
gcc -fuse-ld=lld program.o -o program
clang -fuse-ld=lld program.o -o program
Microsoft Linker (link.exe)
Windows Only
# From x64 Native Tools Command Prompt
link program.obj /OUT:program.exe /SUBSYSTEM:CONSOLE /ENTRY:main
# Common options:
# /SUBSYSTEM:CONSOLE - Console application
# /SUBSYSTEM:WINDOWS - GUI application (no console)
# /ENTRY:symbol - Override entry point
# /DEBUG - Generate debug info (PDB)
# /INCREMENTAL:NO - Disable incremental linking
Common Linker Scripts
For bare-metal and bootloader development, you'll need custom linker scripts:
/* minimal.ld - Simple linker script for bootloader */
OUTPUT_FORMAT(binary)
OUTPUT_ARCH(i386)
ENTRY(_start)
SECTIONS {
. = 0x7C00; /* BIOS loads boot sector here */
.text : {
*(.text) /* Code section */
}
.data : {
*(.data) /* Initialized data */
}
.bss : {
*(.bss) /* Uninitialized data */
}
/* Boot signature at offset 510 */
. = 0x7C00 + 510;
.sig : {
SHORT(0xAA55)
}
}
# Use the linker script
ld -T minimal.ld bootloader.o -o bootloader.bin
IDEs & Editors
A good editor with syntax highlighting, snippets, and integrated debugging transforms your assembly development experience. Here are the top choices:
Visual Studio Code (Recommended)
VS Code strikes the perfect balance between a lightweight editor and a full IDE. With the right extensions, it's the best choice for assembly development.
Essential Extensions for Assembly
# Install from command line or Extensions marketplace
code --install-extension 13xforever.language-x86-64-assembly # NASM syntax
code --install-extension maziac.asm-code-lens # ASM code lens
code --install-extension ms-vscode.cpptools # C/C++ (for GDB)
code --install-extension webfreak.debug # Native Debug (GDB)
VS Code Configuration for Assembly
Create .vscode/tasks.json in your project for build automation:
{
"version": "2.0.0",
"tasks": [
{
"label": "Build (NASM + ld)",
"type": "shell",
"command": "nasm",
"args": [
"-f", "elf64",
"-g",
"-F", "dwarf",
"${file}",
"-o", "${fileDirname}/${fileBasenameNoExtension}.o"
],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": []
},
{
"label": "Link",
"type": "shell",
"command": "ld",
"args": [
"${fileDirname}/${fileBasenameNoExtension}.o",
"-o", "${fileDirname}/${fileBasenameNoExtension}"
],
"dependsOn": "Build (NASM + ld)"
}
]
}
Create .vscode/launch.json for debugging:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Assembly (GDB)",
"type": "cppdbg",
"request": "launch",
"program": "${fileDirname}/${fileBasenameNoExtension}",
"args": [],
"stopAtEntry": true,
"cwd": "${fileDirname}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable disassembly view",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "Set disassembly flavor",
"text": "set disassembly-flavor intel",
"ignoreFailures": true
}
],
"preLaunchTask": "Link"
}
]
}
Platform-Specific VS Code Configuration: The tasks.json and launch.json above target
Linux / WSL (ELF format, GNU ld, GDB). Adjust for other platforms:
- macOS Change
-f elf64 → -f macho64, set "MIMode": "lldb" in launch.json
- Windows Change
-f elf64 → -f win64, replace ld with link.exe or gcc (MinGW), use "type": "cppvsdbg" for MSVC debugger
Pro Tip: Press Ctrl+Shift+B (Cmd+Shift+B on macOS) to build and F5 to debug. VS Code will automatically run the configured tasks.
Vim / Neovim
For the command-line warrior, Vim offers unparalleled efficiency once mastered. Neovim adds modern features like LSP support and Lua configuration.
Basic Vim Setup for Assembly
" Add to ~/.vimrc or ~/.config/nvim/init.vim
" Enable syntax highlighting
syntax on
filetype plugin indent on
" x86 assembly settings
autocmd FileType asm setlocal tabstop=8 shiftwidth=8 noexpandtab
autocmd FileType nasm setlocal tabstop=4 shiftwidth=4 expandtab
" Recognize .asm files as NASM
autocmd BufRead,BufNewFile *.asm set filetype=nasm
autocmd BufRead,BufNewFile *.s set filetype=asm
" Build command (press F5)
autocmd FileType nasm nnoremap <F5> :!nasm -f elf64 -g % -o %:r.o && ld %:r.o -o %:r<CR>
autocmd FileType nasm nnoremap <F6> :!./%:r<CR>
" Quick comment toggle
autocmd FileType nasm nnoremap <leader>c I; <ESC>
autocmd FileType nasm vnoremap <leader>c :s/^/; /<CR>
Neovim with LSP (Advanced)
-- For Neovim - add to ~/.config/nvim/init.lua
-- Requires Packer or lazy.nvim plugin manager
-- Install asm-lsp for assembly language server
-- First: cargo install asm-lsp
require('lspconfig').asm_lsp.setup{
filetypes = { "asm", "nasm", "vmasm" },
}
Visual Studio
Visual Studio is the premier choice for Windows-centric assembly development with MASM. The integrated debugger is particularly powerful.
Creating an Assembly Project
# Creating a MASM project in Visual Studio 2022:
1. File → New → Project
2. Search "Empty Project" (C++)
3. Name your project (e.g., "AsmProject")
4. Right-click project → Build Dependencies → Build Customizations
5. Check "masm(.targets, .props)" → OK
6. Add assembly file:
- Right-click Source Files → Add → New Item
- Select C++ File but name it "main.asm"
7. Configure assembly file properties:
- Right-click main.asm → Properties
- Item Type: Microsoft Macro Assembler
- Click OK
8. Configure linker (for 64-bit console app):
- Project → Properties → Linker → System
- SubSystem: Console (/SUBSYSTEM:CONSOLE)
- Linker → Advanced → Entry Point: main
Visual Studio MASM Template
; main.asm - Visual Studio x64 MASM template
.code
main PROC
; Your code here
mov rax, 42 ; Return value
ret
main ENDP
END
Build with Ctrl+Shift+B, debug with F5. Visual Studio's debugger shows registers and memory natively.
Useful Visual Studio Features for Assembly
- Disassembly Window: Debug → Windows → Disassembly (Ctrl+Alt+D)
- Registers Window: Debug → Windows → Registers (Ctrl+Alt+G)
- Memory Window: Debug → Windows → Memory
- Watch Window: Monitor specific addresses or register values
- Step Into Assembly: Enable "Show Disassembly" when stepping through C code
Editor Comparison Summary:
| VS Code |
Best overall; cross-platform, extensible, great for NASM + GDB |
| Vim/Neovim |
Fastest for experts; SSH-friendly, minimal resource usage |
| Visual Studio |
Best for Windows/MASM; unmatched debugging, heavy resource usage |
Debuggers
A debugger is your most important tool after the assembler. It lets you step through code instruction by instruction, examine registers, and understand exactly what the CPU is doing.
Debug Symbols Are Essential: Always assemble with debug information enabled. Without symbols, debugging becomes guesswork.
# Linux: NASM with DWARF debug symbols (ELF format)
nasm -f elf64 -g -F dwarf program.asm -o program.o
# macOS: Mach-O format with DWARF debug symbols
nasm -f macho64 -g program.asm -o program.o
# Windows: Win64 format with CodeView debug symbols
nasm -f win64 -g -F cv8 program.asm -o program.obj
GDB (GNU Debugger)
GDB is the standard debugger for Linux and works well with any assembler. Mastering GDB is essential for any serious assembly programmer.
Installing GDB
Linux
sudo apt install gdb
# Highly recommended: Install GEF (GDB Enhanced Features)
# GEF provides better visualization for assembly debugging
bash -c "$(curl -fsSL https://gef.blah.cat/sh)"
# Alternative: pwndbg (popular for CTF/security)
# git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh
macOS
# macOS default debugger is LLDB (recommended over GDB)
lldb --version
# GDB is available but requires code-signing to work on macOS
brew install gdb
# After install, you MUST code-sign GDB for macOS security:
# 1. Open Keychain Access → Certificate Assistant → Create Certificate
# Name: "gdb-cert", Type: Code Signing, check "Let me override defaults"
# 2. Sign GDB:
codesign -s "gdb-cert" $(which gdb)
# 3. Restart your Mac for changes to take effect
macOS: LLDB vs GDB Quick Reference — LLDB is recommended on macOS (works out of the box, no code-signing needed):
| GDB Command | LLDB Equivalent |
run | run |
break _start | b _start |
info registers | register read |
x/10i $rip | disassemble -c 10 |
x/16xb $rsp | memory read -s1 -c16 -fx $rsp |
stepi / si | si |
nexti / ni | ni |
set $rax = 42 | register write rax 42 |
disassemble | disassemble --frame |
Essential GDB Commands Reference
| Command |
Short |
Description |
run [args] |
r |
Start program execution |
break *address |
b |
Set breakpoint at address (use * for raw addresses) |
break _start |
b |
Set breakpoint at symbol |
stepi |
si |
Step one instruction (into calls) |
nexti |
ni |
Step one instruction (over calls) |
continue |
c |
Continue until next breakpoint |
info registers |
i r |
Display all registers |
info registers rax |
i r rax |
Display specific register |
x/10i $rip |
— |
Examine 10 instructions at RIP |
x/16xb $rsp |
— |
Examine 16 hex bytes at stack pointer |
x/s address |
— |
Examine as string |
disassemble |
disas |
Disassemble current function |
set $rax = 42 |
— |
Modify register value |
quit |
q |
Exit GDB |
GDB Memory Examination Format
# x/[count][format][size] address
# Format: x=hex, d=decimal, i=instruction, s=string, c=char
# Size: b=byte, h=halfword(2), w=word(4), g=giant(8)
x/10xg $rsp # 10 giant words (64-bit) in hex from stack
x/20i $rip # 20 instructions from instruction pointer
x/s 0x402000 # String at address
x/32xb &buffer # 32 bytes in hex from buffer label
Complete GDB Session Example
Linux
# Assemble with debug info
nasm -f elf64 -g -F dwarf hello.asm -o hello.o
ld hello.o -o hello
# Start debugging
gdb ./hello
(gdb) set disassembly-flavor intel # Intel syntax
(gdb) break _start # Break at entry
(gdb) run # Start execution
# Now we're at _start
(gdb) info registers # Show all registers
(gdb) x/5i $rip # Show next 5 instructions
(gdb) stepi # Execute one instruction
(gdb) info registers rax rbx # Check specific registers
(gdb) x/s 0x402000 # Examine string data
(gdb) continue # Run to completion
(gdb) quit # Exit
GDB Configuration (~/.gdbinit)
Linux macOS
# Add to ~/.gdbinit for persistent settings
set disassembly-flavor intel
set pagination off
set confirm off
# Show registers after each step (useful for learning)
define hook-stop
info registers rax rbx rcx rdx rsi rdi rsp rbp rip
x/3i $rip
end
# Custom command to show stack
define stack
x/16xg $rsp
end
WinDbg (Windows Debugger)
WinDbg is Microsoft's debugger, essential for Windows assembly and kernel debugging. It's more complex than GDB but offers unmatched Windows debugging capabilities.
Installing WinDbg
Windows Only
# Install the modern "WinDbg Preview" from Microsoft Store
# Or download from:
# https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/
# For older WinDbg, install Windows SDK and select:
# "Debugging Tools for Windows"
Essential WinDbg Commands
| Command |
Description |
g |
Go (continue execution) |
p |
Step over (one instruction) |
t |
Trace into (step into calls) |
bp address |
Set breakpoint |
bl |
List breakpoints |
r |
Display registers |
r rax=42 |
Modify register |
u address |
Unassemble (disassemble) |
db address |
Display bytes |
dq address |
Display quad words (64-bit) |
da address |
Display ASCII string |
k |
Display call stack |
lm |
List loaded modules |
q |
Quit |
WinDbg Symbol Setup: Configure symbol path for meaningful stack traces:
.sympath srv*C:\Symbols*https://msdl.microsoft.com/download/symbols
.symfix
.reload
Debugger Comparison
GDB vs WinDbg Decision Guide
| Scenario |
Recommended |
| Learning assembly on Linux |
GDB with GEF |
| Windows userland debugging |
Visual Studio Debugger or WinDbg |
| Windows kernel debugging |
WinDbg (only option) |
| Reverse engineering (CTF) |
GDB with pwndbg |
| Mixed C and assembly |
Visual Studio or VS Code + GDB |
Build Automation
As your assembly projects grow, manually typing build commands becomes tedious and error-prone. Build automation tools like Make and CMake ensure consistent, reproducible builds.
Real-World Analogy: A Makefile is like a recipe book. Instead of remembering every step to bake a cake, you just say "make cake" and the recipe book handles all the steps in the right order.
Makefiles
Make is the classic build tool. A Makefile contains rules that describe dependencies and commands to build your project.
Example main.asm for Each Platform
Create this main.asm file to test your Makefile. Note the critical differences between platforms:
Linux / WSL main.asm
; main.asm - x64 NASM assembly (Linux)
global _start
section .text
_start:
mov rax, 60 ; Linux syscall: exit (60)
mov rdi, 42 ; Exit code
syscall ; Call kernel
macOS (Intel & Apple Silicon via Rosetta) main.asm
; main.asm - x64 NASM assembly (macOS)
global _main
section .text
_main:
mov rax, 42 ; Exit code
mov rdi, rax ; Move exit code to rdi (first argument)
mov rax, 0x2000001 ; macOS syscall: exit (1 + 0x2000000)
syscall ; Call kernel
3 Critical Platform Differences:
- Entry point: Linux uses
_start, macOS uses _main (underscore prefix convention)
- Syscall numbers: Linux
exit = 60, macOS exit = 0x2000001 (syscall number + 0x2000000 offset)
- Binary format: Linux uses ELF (
-f elf64), macOS uses Mach-O (-f macho64)
Basic Makefile for Assembly
Linux / WSL
# Makefile for NASM assembly projects (Linux)
# Assembler and linker settings
ASM = nasm
ASMFLAGS = -f elf64 -g -F dwarf # ELF64 format, DWARF debug symbols
LD = ld
LDFLAGS =
# Source and output files
SRC = main.asm
OBJ = $(SRC:.asm=.o)
TARGET = main
# Default target
all: $(TARGET)
# Link object files to create executable
$(TARGET): $(OBJ)
$(LD) $(LDFLAGS) -o $@ $^
# Compile assembly to object files
%.o: %.asm
$(ASM) $(ASMFLAGS) -o $@ $<
# Run the program
run: $(TARGET)
./$(TARGET)
# Debug with GDB
debug: $(TARGET)
gdb ./$(TARGET)
# Clean build artifacts
clean:
rm -f $(OBJ) $(TARGET)
# Phony targets (not actual files)
.PHONY: all clean run debug
macOS
# Makefile for NASM assembly projects (macOS)
# Key differences from Linux:
# - Format: macho64 (Mach-O) instead of elf64 (ELF)
# - Debug: -g only (DWARF is default for Mach-O, no -F flag needed)
# - Linker: Use system ld with macOS-specific flags
# - Symbols: Entry point requires underscore prefix (_main)
# - Debugger: lldb instead of gdb
ASM = nasm
ASMFLAGS = -f macho64 -g # Mach-O 64-bit format
LD = ld
LDFLAGS = -macos_version_min 10.13 -e _main -static
SRC = main.asm
OBJ = $(SRC:.asm=.o)
TARGET = main
all: $(TARGET)
$(TARGET): $(OBJ)
$(LD) $(LDFLAGS) -o $@ $^
%.o: %.asm
$(ASM) $(ASMFLAGS) -o $@ $<
run: $(TARGET)
./$(TARGET)
# macOS uses LLDB by default
debug: $(TARGET)
lldb ./$(TARGET)
clean:
rm -f $(OBJ) $(TARGET)
.PHONY: all clean run debug
macOS Makefile Differences:
| Setting | Linux | macOS |
| Object format | -f elf64 | -f macho64 |
| Debug format | -g -F dwarf | -g (DWARF is default) |
| Linker flags | (none needed) | -macos_version_min 10.13 -e _main -static |
| Entry point label | _start | _main (underscore prefix) |
| Debugger | gdb | lldb |
| Disassembler | objdump -d -M intel | otool -tvV or objdump -d |
Using the Makefile
Linux macOS
# Build the project
make
# Build and run
make run
# Build and debug
make debug
# Clean up
make clean
# Rebuild from scratch
make clean && make
Windows: GNU Make is not installed by default. Options:
- MSYS2:
pacman -S make (use from MSYS2 terminal)
- Chocolatey:
choco install make
- WSL: Use Make natively in Linux environment
- nmake: Microsoft's Make variant (different syntax):
nmake /f Makefile.win
Advanced Makefile with Multiple Files
Cross-Platform This Makefile auto-detects the OS and sets the correct flags:
# Makefile for larger assembly projects (cross-platform)
ASM = nasm
LD = ld
# Auto-detect platform and set format/flags accordingly
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S), Linux)
ASMFLAGS = -f elf64
DBGFLAGS = -g -F dwarf
LDFLAGS =
DEBUGGER = gdb
DISASM = objdump -d -M intel
else ifeq ($(UNAME_S), Darwin)
ASMFLAGS = -f macho64
DBGFLAGS = -g
LDFLAGS = -macos_version_min 10.13 -e _main -static
DEBUGGER = lldb
DISASM = otool -tvV
endif
# Find all .asm files automatically
SRCS = $(wildcard *.asm)
OBJS = $(SRCS:.asm=.o)
TARGET = program
# Build modes
DEBUG ?= 1
ifeq ($(DEBUG), 1)
ASMFLAGS += $(DBGFLAGS)
else
ASMFLAGS += -O2
endif
all: $(TARGET)
$(TARGET): $(OBJS)
$(LD) $(LDFLAGS) -o $@ $^
%.o: %.asm
$(ASM) $(ASMFLAGS) -o $@ $<
# Disassemble the binary
disasm: $(TARGET)
$(DISASM) $(TARGET)
# Show symbols
symbols: $(TARGET)
nm $(TARGET)
# Debug with platform-appropriate debugger
debug: $(TARGET)
$(DEBUGGER) ./$(TARGET)
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: all clean disasm symbols debug
Makefile for Mixed C and Assembly
Linux / WSL
# Makefile for C + Assembly projects (Linux)
CC = gcc
CFLAGS = -Wall -g -O2
ASM = nasm
ASMFLAGS = -f elf64 -g -F dwarf # Linux ELF64
# Separate C and ASM sources
C_SRCS = main.c utils.c
ASM_SRCS = math_routines.asm fast_memcpy.asm
C_OBJS = $(C_SRCS:.c=.o)
ASM_OBJS = $(ASM_SRCS:.asm=.o)
OBJS = $(C_OBJS) $(ASM_OBJS)
TARGET = hybrid_program
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) -o $@ $^ -no-pie
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
%.o: %.asm
$(ASM) $(ASMFLAGS) -o $@ $<
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: all clean
macOS changes: Replace ASMFLAGS = -f elf64 -g -F dwarf with ASMFLAGS = -f macho64 -g, and the -no-pie flag is not needed on macOS (use -Wl,-no_pie if required). macOS gcc is Clang — use clang explicitly or install GNU GCC via Homebrew.
CMake
CMake is a meta-build system that generates Makefiles (or Ninja, Visual Studio projects, etc.). It's more portable and scales better for large projects.
CMake for Assembly Projects
Cross-Platform This CMake file auto-detects the platform:
# CMakeLists.txt for NASM assembly projects (cross-platform)
cmake_minimum_required(VERSION 3.16)
project(AsmProject ASM_NASM)
# Enable NASM support
enable_language(ASM_NASM)
# Set platform-specific NASM flags
if(APPLE)
# macOS: Mach-O 64-bit format
set(CMAKE_ASM_NASM_FLAGS "${CMAKE_ASM_NASM_FLAGS} -f macho64 -g")
elseif(UNIX)
# Linux: ELF 64-bit format with DWARF debug symbols
set(CMAKE_ASM_NASM_FLAGS "${CMAKE_ASM_NASM_FLAGS} -f elf64 -g -F dwarf")
elseif(WIN32)
# Windows: PE/COFF 64-bit format with CodeView debug symbols
set(CMAKE_ASM_NASM_FLAGS "${CMAKE_ASM_NASM_FLAGS} -f win64 -g -F cv8")
endif()
# Define executable
add_executable(main main.asm)
# Platform-specific linker flags
if(UNIX AND NOT APPLE)
set_target_properties(main PROPERTIES LINK_FLAGS "-no-pie")
elseif(APPLE)
set_target_properties(main PROPERTIES LINK_FLAGS "-e _main -static")
endif()
# Build with CMake (all platforms)
mkdir build && cd build
cmake ..
make # Linux/macOS
# Or use Ninja (faster, all platforms)
cmake -G Ninja ..
ninja
# Windows: Generate Visual Studio project instead
# cmake -G "Visual Studio 17 2022" ..
CMake for Mixed C and Assembly
Cross-Platform
# CMakeLists.txt for C + NASM projects (cross-platform)
cmake_minimum_required(VERSION 3.16)
project(HybridProject C ASM_NASM)
# NASM settings - auto-detect platform
enable_language(ASM_NASM)
if(APPLE)
set(CMAKE_ASM_NASM_FLAGS "-f macho64 -g")
elseif(UNIX)
set(CMAKE_ASM_NASM_FLAGS "-f elf64 -g -F dwarf")
elseif(WIN32)
set(CMAKE_ASM_NASM_FLAGS "-f win64 -g -F cv8")
endif()
# C settings
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -g")
# Source files
set(C_SOURCES
src/main.c
src/utils.c
)
set(ASM_SOURCES
src/math_ops.asm
src/simd_ops.asm
)
# Create executable
add_executable(hybrid_program ${C_SOURCES} ${ASM_SOURCES})
# Link options (platform-specific)
if(UNIX AND NOT APPLE)
target_link_options(hybrid_program PRIVATE -no-pie)
endif()
Build Tool Comparison:
| Make |
Simple, universal, perfect for small projects |
| CMake |
Cross-platform, IDE integration, scales to large projects |
| Ninja |
Fastest builds, use with CMake for best results |
QEMU Setup
QEMU is an essential tool for assembly development. It emulates complete systems, allowing you to test bootloaders, kernels, and bare-metal code without risking your actual hardware.
Real-World Analogy: QEMU is like a flight simulator for your code. Just as pilots practice dangerous maneuvers in simulators before real flights, you can test risky low-level code in QEMU without crashing your actual computer.
Installing QEMU
Linux (Debian/Ubuntu)
sudo apt install qemu-system-x86 qemu-user
macOS
brew install qemu
Windows
# Using Chocolatey (run as Administrator)
choco install qemu
# Or download from https://www.qemu.org/download/
# Add QEMU to your PATH after installation
Userland Emulation (qemu-user)
QEMU can run Linux binaries compiled for different architectures. This is useful when cross-compiling or testing 32-bit code on 64-bit systems.
Linux Only (qemu-user is not available on macOS or Windows)
# Run 32-bit ELF on 64-bit Linux (requires multilib)
qemu-i386 ./program_32bit
# Run ARM binary on x86 Linux
qemu-arm ./program_arm
# Cross-compile and test ARM code
arm-linux-gnueabi-as program.s -o program.o
arm-linux-gnueabi-ld program.o -o program
qemu-arm ./program
Note: For most x86 assembly learning, you won't need qemu-user. It's primarily for cross-architecture development.
This is where QEMU shines for assembly developers. You can boot raw binary files, test bootloaders, and develop kernels.
Running a Bootloader
# Assemble a 512-byte boot sector
nasm -f bin bootloader.asm -o bootloader.bin
# Run in QEMU (floppy disk emulation)
qemu-system-x86_64 -fda bootloader.bin
# Prefer newer AHCI disk interface (-drive)
qemu-system-x86_64 -drive format=raw,file=bootloader.bin
# Boot from CD-ROM image (for larger systems)
qemu-system-x86_64 -cdrom os.iso
Essential QEMU Options
| Option |
Description |
-m 256M |
Set RAM size (256 MB) |
-cpu host |
Use host CPU features (with KVM) |
-enable-kvm |
Hardware acceleration (Linux) |
-nographic |
No GUI, serial output to terminal |
-serial stdio |
Connect serial port to terminal |
-d int |
Debug: log interrupts |
-D logfile |
Write debug output to file |
-s |
Start GDB server on port 1234 |
-S |
Pause CPU at startup (wait for GDB) |
Hardware Acceleration Differs by Platform:
- Linux
-enable-kvm — KVM (fastest, requires Intel VT-x/AMD-V)
- macOS
-accel hvf — Hypervisor.framework (Apple's native virtualisation)
- Windows
-accel whpx — Windows Hypervisor Platform (requires Hyper-V)
Tip: Without acceleration, QEMU uses TCG (software emulation), which is ~10–100× slower. Always enable hardware acceleration when available.
Debugging with QEMU + GDB
The combination of QEMU and GDB is incredibly powerful for debugging bootloaders and kernels:
All Platforms (on macOS, use lldb instead of gdb if GDB is not code-signed)
# Terminal 1: Start QEMU with GDB server, paused
qemu-system-x86_64 -drive format=raw,file=bootloader.bin -s -S
# Terminal 2: Connect GDB
gdb -ex "target remote localhost:1234" \
-ex "set architecture i8086" \
-ex "break *0x7c00" \
-ex "continue"
# Now you can debug your bootloader with full GDB support!
Complete Bootloader Debug Session
# Create a minimal bootloader (boot.asm)
cat > boot.asm << 'EOF'
[bits 16]
[org 0x7c00]
mov ax, 0x0003 ; Set video mode 3 (80x25 text)
int 0x10
mov ah, 0x0e ; BIOS teletype output
mov al, 'H'
int 0x10
mov al, 'i'
int 0x10
mov al, '!'
int 0x10
jmp $ ; Infinite loop
times 510-($-$$) db 0
dw 0xAA55 ; Boot signature
EOF
# Assemble
nasm -f bin boot.asm -o boot.bin
# Run with debug server
qemu-system-x86_64 -drive format=raw,file=boot.bin -s -S &
# Connect GDB (in new terminal)
gdb << 'GDBCMDS'
target remote localhost:1234
set architecture i8086
break *0x7c00
continue
stepi
info registers
x/10i $pc
GDBCMDS
QEMU Monitor Commands
Press Ctrl+Alt+2 in QEMU to access the monitor console:
# Useful monitor commands
info registers # Show CPU registers
info mem # Show memory mapping
info cpus # Show CPU state
xp /10xb 0x7c00 # Examine physical memory
gdbserver 1234 # Start GDB server
quit # Exit QEMU
Workflow & Best Practices
A well-organized project structure and version control discipline will save you hours of debugging and make your code maintainable.
Project Directory Structure
Here's a recommended directory layout for assembly projects:
my-asm-project/
├── src/ # Source files
│ ├── main.asm # Entry point
│ ├── io/ # I/O routines
│ │ ├── print.asm
│ │ └── input.asm
│ ├── math/ # Math routines
│ │ └── arithmetic.asm
│ └── utils/ # Utility functions
│ └── string.asm
├── include/ # Header/macro files
│ ├── macros.inc # Shared macros
│ └── constants.inc # Project-wide constants
├── tests/ # Test programs
│ ├── test_print.asm
│ └── test_math.asm
├── build/ # Generated files (gitignored)
│ ├── *.o
│ └── executable
├── docs/ # Documentation
│ └── README.md
├── scripts/ # Build/utility scripts
│ └── debug.sh
├── Makefile # Build configuration
├── .gitignore # Git ignore rules
└── README.md # Project description
Example .gitignore for Assembly Projects
# Build artifacts
build/
*.o
*.obj
*.bin
*.exe
# Editor files
.vscode/settings.json
*.swp
*~
# Debug files
*.lst
*.map
core
# OS files
.DS_Store
Thumbs.db
Include File Organization
Use include files to share macros and constants across multiple source files:
; include/macros.inc - Shared macros
; Exit program with status code
%macro exit 1
mov rax, 60 ; sys_exit
mov rdi, %1 ; exit code
syscall
%endmacro
; Print null-terminated string
%macro print_string 2 ; 1=address, 2=length
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
mov rsi, %1 ; buffer
mov rdx, %2 ; length
syscall
%endmacro
; Save all caller-saved registers
%macro push_all 0
push rax
push rcx
push rdx
push rsi
push rdi
push r8
push r9
push r10
push r11
%endmacro
%macro pop_all 0
pop r11
pop r10
pop r9
pop r8
pop rdi
pop rsi
pop rdx
pop rcx
pop rax
%endmacro
; src/main.asm - Using include files
%include "include/macros.inc"
%include "include/constants.inc"
section .data
msg: db "Hello, Assembly!", 10
len: equ $ - msg
section .text
global _start
_start:
print_string msg, len ; Use macro
exit 0 ; Use macro
Git Best Practices for Assembly
Version control is crucial when experimenting with low-level code. A broken bootloader can lock up your emulator—having version history is a lifesaver.
Commit Message Convention
# Format: type(scope): description
feat(bootloader): implement A20 line enabling
fix(syscall): correct register preservation order
docs(readme): add build instructions for Windows
refactor(math): optimize multiply routine with shifts
test(io): add unit test for keyboard input handler
# Types:
# feat - New feature or functionality
# fix - Bug fix
# docs - Documentation only
# refactor - Code change without functionality change
# test - Adding or modifying tests
# chore - Build system, tooling changes
Branching Strategy
# Main development
git checkout -b feature/keyboard-driver
# Experimental/risky changes
git checkout -b experiment/new-memory-layout
# Bug fixes
git checkout -b fix/stack-corruption
# Keep main branch stable
git checkout main
git merge --no-ff feature/keyboard-driver
Tagging Milestones
# Tag working states before major changes
git tag -a v0.1-bootloader-working -m "Basic boot sector loads second stage"
git tag -a v0.2-protected-mode -m "Successfully enters protected mode"
git tag -a v0.3-interrupts -m "IDT and basic interrupt handling"
# List tags
git tag -l
# Checkout old working state if needed
git checkout v0.1-bootloader-working
Development Workflow
Recommended Workflow:
- Branch: Create feature branch for new work
- Develop: Write code, test in QEMU frequently
- Commit: Small, logical commits with clear messages
- Test: Verify with multiple test cases
- Debug: Use GDB if issues arise
- Document: Add comments explaining WHY, not just WHAT
- Merge: Merge back to main when stable
Documenting Assembly Code
Assembly code needs more comments than high-level code. Document intent, not just actions:
; BAD: Comment restates the code
mov ax, bx ; Move bx to ax
; GOOD: Comment explains WHY
mov ax, bx ; Preserve sector count before BIOS call clobbers bx
; EVEN BETTER: Block comment for complex sequences
; Enable A20 line using keyboard controller method
; This allows accessing memory above 1MB by disabling
; the legacy 8086 address wraparound behavior.
; Note: Fast A20 (port 0x92) is faster but not universal.
wait_kbd:
in al, 0x64
test al, 0x02
jnz wait_kbd
mov al, 0xD1 ; Write output port command
out 0x64, al
; ... rest of A20 enable code
Critical Tip: Commit working code before making changes. In assembly, a single-byte error can cause hard-to-debug crashes. Having a "last known good" commit is invaluable.
Next Steps
With your development environment set up, you're ready to begin writing assembly code. In the next article, we'll cover assembly language fundamentals and write our first programs.
Continue the Series
Part 1: Assembly Language Fundamentals & Toolchain Setup
Learn what assembly really is, understand the build pipeline, and write your first programs for Linux and Windows.
Read Article
Part 2: x86 CPU Architecture Overview
Understand x86 evolution, execution modes, privilege rings, and CPU internals for effective assembly programming.
Read Article
Part 3: Registers – Complete Deep Dive
Master all x86/x64 registers including general-purpose, segment, control, debug, and model-specific registers.
Read Article