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.

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

.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

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

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

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.

%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