Back to Technology

x86 Assembly Series Part 5: NASM Syntax, Directives & Macros

February 6, 2026 Wasil Zafar 28 min read

Master the NASM (Netwide Assembler) syntax including sections, data directives, labels, symbols, macros, conditional assembly, and producing ELF32/ELF64 binaries for Linux development.

Table of Contents

  1. NASM Syntax Rules
  2. Sections
  3. Data Directives
  4. Labels, Scope & Symbols
  5. Constants & Equates
  6. Macros
  7. Conditional Assembly
  8. Producing ELF Binaries

NASM Syntax Rules

NASM Basics: NASM uses Intel syntax (destination, source) and is case-insensitive for instructions/registers but case-sensitive for labels and symbols.

x86 Assembly Mastery

Your 25-step learning path • Currently on Step 6
Development Environment, Tooling & Workflow
IDEs, debuggers, build tools, workflow setup
Assembly Language Fundamentals & Toolchain Setup
Syntax basics, assemblers, linkers, object files
x86 CPU Architecture Overview
Instruction pipeline, execution units, microarchitecture
Registers – Complete Deep Dive
GPRs, segment, control, flags, MSRs
Instruction Encoding & Binary Layout
Opcode bytes, ModR/M, SIB, prefixes, encoding schemes
6
NASM Syntax, Directives & Macros
Sections, labels, EQU, %macro, conditional assembly
You Are Here
7
Complete Assembler Comparison
NASM vs MASM vs GAS vs FASM, syntax differences
8
Memory Addressing Modes
Direct, indirect, indexed, base+displacement, RIP-relative
9
Stack Internals & Calling Conventions
Push/pop, stack frames, cdecl, System V ABI, fastcall
10
Control Flow & Procedures
Jumps, loops, conditionals, CALL/RET, function design
11
Integer, Bitwise & Arithmetic Operations
ADD, SUB, MUL, DIV, AND, OR, XOR, shifts, rotates
12
Floating Point & SIMD Foundations
x87 FPU, IEEE 754, SSE scalar, precision control
13
SIMD, Vectorization & Performance
SSE, AVX, AVX-512, data-parallel processing
14
System Calls, Interrupts & Privilege Transitions
INT, SYSCALL, IDT, ring transitions, exception handling
15
Debugging & Reverse Engineering
GDB, breakpoints, disassembly, binary analysis, IDA
16
Linking, Relocation & Loader Behavior
ELF/PE formats, symbol resolution, dynamic linking, GOT/PLT
17
x86-64 Long Mode & Advanced Features
64-bit extensions, RIP addressing, canonical addresses
18
Assembly + C/C++ Interoperability
Inline assembly, calling C from ASM, ABI compliance
19
Memory Protection & Security Concepts
DEP, ASLR, stack canaries, ROP, mitigations
20
Bootloaders & Bare-Metal Programming
BIOS/UEFI, MBR, real mode, protected mode transition
21
Kernel-Level Assembly
Context switching, interrupt handlers, TSS, GDT/LDT
22
Complete Emulator & Simulator Guide
QEMU, Bochs, instruction-level simulation, debugging VMs
23
Advanced Optimization & CPU Internals
Pipeline hazards, branch prediction, cache optimization, ILP
24
Real-World Assembly Projects
Shellcode, drivers, cryptography, signal processing
25
Assembly Mastery Capstone
Final project, comprehensive review, advanced techniques

Intel Syntax

Syntax

Intel vs AT&T Syntax

; NASM (Intel Syntax): destination, source
mov rax, rbx        ; Copy RBX to RAX
mov [rax], 42       ; Store 42 at address in RAX

; AT&T Syntax (GAS): source, destination
; movq %rbx, %rax   ; Same operation

Line Format

label:    instruction operands    ; comment
_start:   mov        rax, 60      ; syscall number for exit

Sections

Diagram of NASM program sections showing .text, .data, and .bss memory layout
NASM program sections — .text (executable code), .data (initialized data), and .bss (uninitialized reservations) — mapped into an ELF binary.

.text Section (Code)

section .text
    global _start

_start:
    ; Executable code goes here
    mov rax, 60     ; exit syscall
    xor rdi, rdi    ; return 0
    syscall
Save & Compile: exit.asm

Linux

nasm -f elf64 exit.asm -o exit.o
ld exit.o -o exit
./exit

macOS (change _start_main, use syscall 0x2000001 for exit)

nasm -f macho64 exit.asm -o exit.o
ld -macos_version_min 10.13 -e _main -static exit.o -o exit

Windows (use ExitProcess API instead of syscall 60)

nasm -f win64 exit.asm -o exit.obj
link /subsystem:console /entry:_start exit.obj /out:exit.exe

.data Section (Initialized Data)

section .data
    msg     db  "Hello, World!", 10    ; String with newline
    len     equ $ - msg                 ; Calculate length
    num     dd  12345                   ; 32-bit integer
    pi      dq  3.14159                 ; 64-bit float

.bss Section (Uninitialized Data)

section .bss
    buffer  resb 256    ; Reserve 256 bytes
    count   resd 1      ; Reserve 1 dword (4 bytes)
    array   resq 100    ; Reserve 100 qwords (800 bytes)

Data Directives

Visual comparison of NASM data directives db, dw, dd, and dq showing byte sizes
NASM data definition directives (db, dw, dd, dq) and their corresponding byte sizes in memory.

Define Data (db, dw, dd, dq)

Reference

Data Definition Directives

DirectiveSizeExample
db1 bytedb 0x41, 'A', 65
dw2 bytes (word)dw 0x1234
dd4 bytes (dword)dd 0x12345678
dq8 bytes (qword)dq 0x123456789ABCDEF0

Reserve Space (resb, resw, resd, resq)

Reserve directives allocate uninitialized space in the .bss section:

section .bss
    buffer resb 256       ; Reserve 256 bytes
    count  resd 1         ; Reserve 1 dword (4 bytes)
    array  resq 100       ; Reserve 100 qwords (800 bytes)
    flags  resw 16        ; Reserve 16 words (32 bytes)

; .bss is NOT stored in the executable file!
; The OS zeros this memory when loading the program
Define vs Reserve:
  • db, dw, dd, dq — Define data with initial values (goes in .data or .rodata)
  • resb, resw, resd, resq — Reserve uninitialized space (goes in .bss)
; Calculating sizes
struc Point
    .x: resd 1
    .y: resd 1
endstruc

section .bss
    points resb Point_size * 100  ; Array of 100 Point structures
    ; Point_size is automatically defined by NASM

Labels, Scope & Symbols

Diagram showing NASM label scoping with global and local dot-prefixed labels
Label scope in NASM — global labels visible to the linker and local dot-prefixed labels scoped to their parent.

Global Labels

; Global labels are visible to the linker
global _start              ; Export symbol (visible to linker)
global my_function         ; Another exported symbol

extern printf              ; Import symbol from another object
extern malloc

_start:                    ; Define the label
    call my_function
    ; ...

my_function:
    push rbp
    ; ...
    ret

Local Labels (Dot Prefix)

; Local labels start with a dot and belong to the previous global label
process_array:
    xor rcx, rcx           ; i = 0
.loop:                     ; Local to process_array
    cmp rcx, rax
    jge .done              ; Jump to process_array.done
    ; Process element...
    inc rcx
    jmp .loop
.done:
    ret

process_string:
    ; Can reuse .loop and .done names!
.loop:                     ; Local to process_string (different from above)
    ; ...
.done:
    ret

Anonymous Labels ($$ and $)

; $ = current address, $$ = section start
section .text
message db "Hello", 0
msg_len equ $ - message    ; Length calculation

; Boot sector example
times 510 - ($ - $$) db 0  ; Pad to 510 bytes
dw 0xAA55                  ; Boot signature

Symbol Visibility for Shared Libraries

; Control symbol visibility for dynamic linking
global public_api:function           ; Visible, typed as function  
global internal_helper:function hidden ; Hidden from dynamic linker

; In ELF output, you can also use:
global my_data:data

Exercise: Label Scope

; What labels are accessible where?
func_a:
    jmp .loop    ; Valid - same function
func_a.loop:
    jmp func_b.loop  ; Valid - fully qualified name
    ret

func_b:
    jmp .loop    ; Valid - different .loop
.loop:
    ret

Constants & Equates

EQU — Assemble-Time Constants

; EQU creates a constant that cannot be changed later
BUFFER_SIZE equ 4096
MAX_ITEMS   equ 100
NULL        equ 0

; EQU with expressions
HEADER_SIZE equ 16
DATA_OFFSET equ HEADER_SIZE + 4

; System call numbers
SYS_READ    equ 0
SYS_WRITE   equ 1  
SYS_EXIT    equ 60

; Use them like constants
mov rax, SYS_WRITE
mov rdi, BUFFER_SIZE

%define — Preprocessor Macros

; %define creates text substitution (like C #define)
%define STDIN  0
%define STDOUT 1
%define STDERR 2

; Can include expressions
%define KB(n) ((n) * 1024)
%define MB(n) ((n) * 1024 * 1024)

mov rax, KB(4)         ; Expands to ((4) * 1024) = 4096

; String constants
%define NEWLINE 10
%define GREETING "Hello, World!", NEWLINE, 0

; How GREETING expands:
; When you write:   db GREETING
; NASM substitutes: db "Hello, World!", 10, 0
;
; This produces 15 bytes in memory:
;   48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21  ("Hello, World!")
;   0A                                         (10 = newline '\n')
;   00                                         (0 = null terminator)
;
; The comma-separated list works because db accepts multiple values:
;   db "text", byte, byte  →  string bytes followed by individual bytes
;
; This is a common pattern for defining printable C-style strings:
;   %define CR    13       ; Carriage return '\r'
;   %define LF    10       ; Line feed '\n'
;   %define CRLF  CR, LF   ; Windows-style line ending
;   %define NULL  0        ; Null terminator
;
; Usage in .data section:
;   section .data
;       msg db GREETING           ; "Hello, World!\n\0" (15 bytes)
;       err db "Error", CRLF, 0   ; "Error\r\n\0" (8 bytes)

EQU vs %define

Aspect EQU %define
Evaluation time Assembly time (once) Preprocessor (text substitution)
Can redefine? No Yes (with %undef first)
Parameters? No Yes (macro-like)
Use case Numeric constants Flexible substitutions
Best Practice: Use equ for simple numeric constants (syscall numbers, sizes). Use %define for parametric macros or when you need text substitution.

Macros

Flowchart of NASM macro expansion from definition to assembled output
NASM macro expansion process — from %define and %macro definitions through preprocessor substitution to final assembled code.

Single-Line Macros (%define)

; Simple text substitution
%define EXIT_SUCCESS 0
%define EXIT_FAILURE 1

; Parametric macros
%define SYSCALL(n) mov rax, n
%define ALIGN16(x) (((x) + 15) & ~15)

SYSCALL(60)              ; Expands to: mov rax, 60
mov rdi, ALIGN16(13)     ; Expands to: mov rdi, (((13) + 15) & ~15) = 16

Multi-Line Macros (%macro / %endmacro)

; Basic multi-line macro: syscall wrapper
%macro exit 1              ; Name 'exit', takes 1 parameter
    mov rax, 60            ; sys_exit
    mov rdi, %1            ; First parameter
    syscall
%endmacro

; Usage
exit 0                     ; Expands to: mov rax, 60 / mov rdi, 0 / syscall
exit EXIT_FAILURE          ; Works with constants too

Macro with Multiple Parameters

; Write string to file descriptor  
%macro write 3             ; fd, buffer, length
    mov rax, 1             ; sys_write
    mov rdi, %1            ; fd
    mov rsi, %2            ; buffer
    mov rdx, %3            ; length
    syscall
%endmacro

; Usage
write STDOUT, message, msg_len
write 2, error_msg, err_len  ; Write to stderr

Local Labels in Macros

; Use %%label for labels local to each macro expansion
%macro repeat_char 2       ; char, count
    mov rcx, %2
%%loop:
    mov al, %1
    ; ... print character ...
    loop %%loop            ; Each expansion gets unique %%loop
%endmacro

; Multiple uses don't conflict:
repeat_char 'A', 5         ; %%loop becomes ..@1.loop
repeat_char 'B', 3         ; %%loop becomes ..@2.loop

Macro Overloading

; Same name, different parameter counts
%macro print 1             ; print buffer (null-terminated)
    mov rsi, %1
    ; ... calculate length and print ...
%endmacro

%macro print 2             ; print buffer, length
    mov rsi, %1
    mov rdx, %2
    ; ... print ...
%endmacro

; NASM selects based on argument count
print message              ; Calls 1-parameter version
print buffer, 100          ; Calls 2-parameter version

Exercise: Create a Stack Frame Macro

; Create macros for function prologue/epilogue
%macro prologue 1          ; local_bytes
    push rbp
    mov rbp, rsp
    sub rsp, %1            ; Reserve stack space
%endmacro

%macro epilogue 0
    leave                  ; mov rsp, rbp; pop rbp
    ret
%endmacro

; Usage:
my_function:
    prologue 32            ; Reserve 32 bytes
    ; ... function body ...
    epilogue

Conditional Assembly

Conditionally include or exclude code at assembly time, useful for multi-platform or debug/release builds.

Decision tree for NASM conditional assembly with %if, %ifdef, and platform-specific code paths
Conditional assembly flow in NASM — %if/%ifdef directives select code paths at assembly time for multi-platform builds.

%if / %elif / %else / %endif

; Compile-time conditionals based on expressions
%define TARGET_BITS 64

%if TARGET_BITS == 64
    mov rax, [rbx]         ; 64-bit code
%elif TARGET_BITS == 32
    mov eax, [ebx]         ; 32-bit code
%else
    %error "Unsupported target"
%endif

%ifdef / %ifndef (Check if Defined)

; Check if a symbol is defined
%define DEBUG 1

%ifdef DEBUG
    ; Include debug output
    call debug_print
    call dump_registers
%endif

%ifndef RELEASE
    ; Extra checks for non-release builds
    call validate_input
%endif

; Can also use command line: nasm -DDEBUG program.asm

Platform-Specific Code

; Assemble different code for Linux vs Windows
%ifdef LINUX
    %define SYS_WRITE 1
    %define SYS_EXIT 60
%elifdef WINDOWS
    ; Windows uses different approach (WinAPI)
    extern WriteFile
    extern ExitProcess
%else
    %error "Define LINUX or WINDOWS"
%endif

; Build: nasm -DLINUX -f elf64 program.asm
;    or: nasm -DWINDOWS -f win64 program.asm

%ifid / %ifnum / %ifstr (Type Checking)

; Check argument types in macros
%macro safe_mov 2
    %ifnum %2
        mov %1, %2              ; Immediate value
    %else
        lea %1, [%2]            ; Memory reference (assume address)
    %endif
%endmacro

safe_mov rax, 42             ; Expands to: mov rax, 42
safe_mov rax, my_buffer      ; Expands to: lea rax, [my_buffer]

Repetition: %rep / %endrep

; Generate repeated code or data
%assign i 0
%rep 10
    db i                       ; Generates: db 0, db 1, ..., db 9
    %assign i i+1
%endrep

; Unrolled loop
%rep 4
    add rax, [rsi]
    add rsi, 8
%endrep
%assign vs EQU: Use %assign for mutable preprocessor variables (can be reassigned). Use equ for fixed constants that never change.

Producing ELF Binaries

NASM Output Formats (-f)

Format Option Use Case
ELF64 -f elf64 Linux 64-bit executable/object
ELF32 -f elf32 or -f elf Linux 32-bit
Win64 -f win64 Windows 64-bit COFF object
Win32 -f win32 Windows 32-bit COFF object
Binary -f bin Raw binary (boot sectors, firmware)
Mach-O 64 -f macho64 macOS 64-bit

Complete Build Example (Linux)

# Simple standalone program
nasm -f elf64 hello.asm -o hello.o
ld hello.o -o hello
./hello

# With debug symbols
nasm -f elf64 -g -F dwarf hello.asm -o hello.o
ld hello.o -o hello
gdb ./hello

# Linking with libc
nasm -f elf64 program.asm -o program.o
gcc program.o -o program -no-pie  # Use GCC to link with C runtime

# Or with ld (specify C library path)
ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 \
   -lc program.o -o program

Complete Build Example (Windows)

# Windows with MSVC linker
nasm -f win64 hello_win.asm -o hello_win.obj
link /SUBSYSTEM:CONSOLE /ENTRY:main hello_win.obj kernel32.lib

# Windows with GoLink (simpler)
nasm -f win64 hello_win.asm -o hello_win.obj
golink /console /entry main hello_win.obj kernel32.dll

Complete Build Example (macOS)

macOS Key Differences:
  • Object format: macho64 (Mach-O) instead of elf64
  • Entry point: Labels must be prefixed with _ (e.g., _main not _start)
  • Syscall numbers: Add 0x2000000 offset (e.g., write = 0x2000004, exit = 0x2000001)
  • Apple Silicon (M1/M2/M3/M4): Uses ARM64 natively — x86-64 NASM code runs via Rosetta 2
  • No 32-bit support: macOS Catalina+ removed all 32-bit binary execution
# Simple standalone program (Intel Mac or Rosetta 2 on Apple Silicon)
nasm -f macho64 hello.asm -o hello.o
ld -macos_version_min 10.13 -e _main -static hello.o -o hello
./hello

# On Apple Silicon — verify Rosetta is running your binary
file hello              # Should show: Mach-O 64-bit executable x86_64
arch -x86_64 ./hello    # Force x86_64 execution explicitly

# With debug symbols (use lldb on macOS, not gdb)
nasm -f macho64 -g hello.asm -o hello.o
ld -macos_version_min 10.13 -e _main -static hello.o -o hello
lldb ./hello

# Linking with libc (use _main as entry, gcc handles C runtime)
nasm -f macho64 program.asm -o program.o
gcc program.o -o program       # GCC/Clang links with libc automatically
# Note: macOS gcc is actually Clang — no -no-pie needed

# Disassemble with macOS-native tools
otool -t -v hello               # Disassemble .text section
otool -l hello                  # Show load commands (Mach-O segments)
nm hello                        # List symbols

macOS Syscall Number Reference

SyscallLinux (RAX)macOS (RAX)Args
exit600x2000001RDI = status
fork570x2000002(none)
read00x2000003RDI=fd, RSI=buf, RDX=len
write10x2000004RDI=fd, RSI=buf, RDX=len
open20x2000005RDI=path, RSI=flags, RDX=mode
close30x2000006RDI=fd
mmap90x20000C5RDI=addr, RSI=len, RDX=prot, ...

macOS syscall = 0x2000000 + BSD_syscall_number. Full list: /usr/include/sys/syscall.h or search XNU syscalls.

Apple Silicon Makefile (Rosetta 2)

# Makefile for macOS (Intel & Apple Silicon via Rosetta 2)
ASM      = nasm
ASMFLAGS = -f macho64
LD       = ld
LDFLAGS  = -macos_version_min 10.13 -e _main -static

SRC = hello.asm
OBJ = $(SRC:.asm=.o)
BIN = hello

all: $(BIN)

$(BIN): $(OBJ)
	$(LD) $(LDFLAGS) $(OBJ) -o $(BIN)

%.o: %.asm
	$(ASM) $(ASMFLAGS) $< -o $@

debug: ASMFLAGS += -g
debug: clean all
	lldb ./$(BIN)

disasm: $(BIN)
	otool -t -v $(BIN)

ndisasm: $(OBJ)
	ndisasm -b 64 $(OBJ)

clean:
	rm -f $(OBJ) $(BIN)

.PHONY: all clean debug disasm ndisasm

Listing Files and Debugging

# Generate listing file (shows assembled bytes)
nasm -f elf64 -l listing.lst program.asm

# Listing file shows:
# Line   Address   Machine Code    Source
#   10   00000000  B802000000      mov eax, 2
#   11   00000005  48C7C102000000  mov rcx, 2

# Generate map file (symbol addresses)
ld -Map=program.map program.o -o program

Quick Reference: Build Commands

# Linux standalone (no libc)
nasm -f elf64 prog.asm -o prog.o && ld prog.o -o prog

# Linux with libc
nasm -f elf64 prog.asm -o prog.o && gcc prog.o -o prog -no-pie

# Linux shared library
nasm -f elf64 -DPIC lib.asm -o lib.o
gcc -shared lib.o -o libmylib.so

# Boot sector (raw binary)
nasm -f bin boot.asm -o boot.bin
qemu-system-x86_64 -drive format=raw,file=boot.bin

# Debug build
nasm -f elf64 -g -F dwarf prog.asm -o prog.o
ld prog.o -o prog && gdb ./prog