Macros & Directives

The assembler is a small programming language in its own right. Its macros, constants, and conditional directives let you generate instruction sequences — so you write a pattern once and expand it everywhere, with zero run-time cost.

Multiline Macros

A macro is a named block of source text that the assembler pastes in wherever you write its name, substituting the arguments you supply. It is a compile-time mechanism: the expansion is invisible at run time.

%macro write_str 2          ; name, number of parameters
    mov  rax, 1             ; syscall 1 = write
    mov  rdi, 1             ; fd 1 = stdout
    mov  rsi, %1            ; %1 = first argument: pointer to the string
    mov  rdx, %2            ; %2 = second argument: length
    syscall
%endmacro

section .rodata
    msg db "Macro magic", 10

section .text
    global _start
_start:
    write_str msg, 13       ; expands to the five instructions above
    mov  rax, 60
    xor  rdi, rdi
    syscall

Parameters are referenced as %1, %2, and so on. Because the substitution is textual, a macro can accept anything — a number, a label, or even an expression.

Local Labels Inside Macros: %%

If a macro contains a label and you expand it twice, you get a duplicate-symbol error. Prefixing the label with %% makes it unique on each expansion.

%macro delay 1
    mov  rcx, %1
%%wait:                     ; %% generates a fresh name every time
    dec  rcx
    jnz  %%wait
%endmacro

    ; Both expansions coexist, because each %%wait becomes a different label:
    delay 1000
    delay 500

Macro or Subroutine?

MacroSubroutine (call)
When it happensAssembly time (text expansion)Run time (a jump and a return)
Code sizeGrows with each useWritten once
Run-time costNone — no call, no returnOne call plus one ret, plus stack use
Good forShort sequences used a few times; constants; code generationLarger routines used many times

Choose a macro when the sequence is short and speed matters; choose a subroutine when the code is long enough that duplicating it would bloat the executable or hurt instruction-cache performance.

Conditional Assembly

The assembler can include or exclude whole blocks of source based on symbols you define. This is how one source file produces debug and release builds, or targets several platforms.

%define DEBUG 1

%ifdef DEBUG
    ; This block is assembled only when DEBUG is defined.
%macro trace 1
    push rax
    push rcx
    push rdx
    push rsi
    push rdi
    ; ... write a diagnostic line ...
    pop  rdi
    pop  rsi
    pop  rdx
    pop  rcx
    pop  rax
%endmacro
%else
    %macro trace 1
        ; nothing: trace calls compile away entirely in release builds
    %endmacro
%endif

%ifndef VERSION
    %define VERSION 1
%endif

Because the untaken branch is never assembled, a disabled trace macro expands to nothing at all — no instructions, no data, no cost. That is a cleaner arrangement than a run-time if (debug) check in a language without a preprocessor.

Constants: equ and %define

equ — a Value for the Assembler

equ binds a name to a constant expression. The name is not a label and occupies no storage; it disappears after assembly, replaced everywhere by its value.

    BUFFER_SIZE equ 4096
    SYS_WRITE   equ 1
    SYS_EXIT    equ 60
    STDOUT      equ 1

    ; equ values can be used in any expression, including addresses:
    msg_len equ end_of_msg - msg

%define — a Text Substitution

%define is more primitive: it replaces text, not a value, and it can take parameters. That power is also its danger — an unparenthesized substitution can change the meaning of an expression.

%define TWO 2
%define SQUARE(x) ((x) * (x))     ; ALWAYS parenthesize macro parameters
%define MAX(a, b) ((a) > (b) ? (a) : (b))   ; not valid in NASM — see note

    mov  rax, SQUARE(rbx)         ; expands to: mov rax, ((rbx) * (rbx))
    mov  rcx, TWO * 3             ; expands to: mov rcx, 2 * 3
Note: NASM's %define is a simple text substitution and does not evaluate conditional expressions the way the C preprocessor does. For anything involving conditions or loops, use %if and %rep instead of trying to emulate C macros.

Rule of thumb: use equ for numbers you compute with, and %define for token substitutions such as register aliases or names of system calls.

Includes, Repetition, and Structure

%include — Splitting a Project

Once a program outgrows one file, put the shared macros and constants in a header and include it. This is exactly the role of a C .h file.

; ---- file: syscalls.inc ----
%define SYS_WRITE 1
%define SYS_EXIT  60
%define STDOUT    1

%macro exit 1
    mov  rax, SYS_EXIT
    mov  rdi, %1
    syscall
%endmacro

; ---- file: program.asm ----
%include "syscalls.inc"      ; pull in the definitions above

section .text
    global _start
_start:
    ; ... produce output ...
    exit 0                   ; expand the macro; argument 0 is the status

Search paths can be extended with -I on the command line: nasm -I include/ -f elf64 program.asm.

%rep — Compile-Time Repetition

%rep expands a block several times at assembly time. Unlike times, which repeats data, %rep repeats anything, and the counter %$ is available inside the block. That makes generated tables possible.

%assign i 0
%rep 16
    dd i * i            ; emit i squared, sixteen times: 0, 1, 4, 9, ... 225
%assign i i+1
%endrep

Summary

  • equ defines an assembly-time constant; %define is a text substitution that also accepts parameters.
  • %macro name n … %endmacro defines a macro with n parameters, referenced as %1, %2, …
  • Use %%label inside macros to create a fresh local label on every expansion — otherwise the second expansion collides with the first.
  • %if/%ifdef/%ifndef select code at assembly time; the untaken branch is never assembled.
  • Macros expand into real instructions, so they cost nothing at run time; subroutines (call) cost one call, but can be used many times for far less code size.

Next: Input/Output & Syscalls — making a program talk to the outside world.