Lesson 12-Macro Assembly Techniques and Conditional Assembly

If you’ve ever found yourself copying and pasting the same five lines of assembly code in twenty different places, then changing one instruction in that block and having to update all twenty copies — you already understand why macros exist. But assembly macros are so much more than simple text substitution. They’re a compile-time programming language hiding inside your assembler, capable of generating complex code patterns, making platform-specific decisions, and even building entire data tables programmatically.

In this article, we’re going to explore the full power of macro assembly and conditional assembly. By the time we’re done, you’ll be writing macros that would make a C++ template metaprogrammer nod in respect.

What Is a Macro? (Text Substitution on Steroids — and Why It’s Different from a Function)

Let’s start with the most fundamental question: what exactly is a macro in assembly language?

A macro is a named block of code that gets textually expanded at the point where it’s invoked. When the assembler encounters a macro invocation, it substitutes the macro’s body into the code at that location — replacing any parameters with their actual values — and then assembles the expanded code.

This is fundamentally different from a function call. Let me illustrate:

Function Call: One Copy of Code, Multiple Call Sites

; Function definition (one copy exists in memory)
print_hex:
    push ebp
    mov ebp, esp
    ; ... implementation ...
    ret

; Function calls (each call site jumps to the same code)
call print_hex    ; Call site 1
; ...
call print_hex    ; Call site 2
; ...
call print_hex    ; Call site 3

With a function:

  • There’s only ONE copy of the print_hex code in memory
  • Each CALL instruction pushes a return address and jumps to that one copy
  • At runtime, the CPU executes a CALL (which involves stack operations) and later a RET (which pops the return address)
  • This has overhead: the CALL and RET instructions take time, and you pay the cost of branch misprediction

Macro: Code Duplication, Zero Runtime Overhead

; Macro definition
%macro PRINT_HEX 1
    push eax
    mov eax, %1
    call print_hex_impl
    pop eax
%endmacro

; Macro invocations (each expands to a copy of the macro body)
PRINT_HEX ebx     ; Expands to: push eax; mov eax, ebx; call print_hex_impl; pop eax
PRINT_HEX ecx     ; Expands to: push eax; mov eax, ecx; call print_hex_impl; pop eax
PRINT_HEX edx     ; Expands to: push eax; mov eax, edx; call print_hex_impl; pop eax

With a macro:

  • The macro body is copied at each invocation site
  • There’s no CALL or RET — the code is right there inline
  • Zero function call overhead at runtime
  • But… the code is larger (you have multiple copies of the same logic)

The Fundamental Tradeoff

AspectFunctionMacro
Code sizeSmall (one copy)Large (copy at each use)
Runtime overheadCALL/RET overheadZero (inline expansion)
Parameter passingRegisters, stackText substitution (any assembly code)
DebuggingEasy (one place to set breakpoint)Harder (code duplicated everywhere)
RecursionSupportedNot supported (expands at compile time)

Macros are essentially a metaprogramming tool. You’re writing code that writes code. The macro itself is processed at assembly time (not at runtime), and the result is injected into your source code before the assembler even sees it.

A Concrete Example: Why Macros Matter

Let’s say you’re writing a bootloader in 16-bit real mode, and you need to print a string using BIOS interrupt 0x10. The code looks like this:

; Print a character using BIOS teletype output
mov ah, 0x0E       ; BIOS teletype function
mov al, 'H'         ; Character to print
mov bh, 0           ; Page number
mov bl, 0x07        ; Text attribute (light gray on black)
int 0x10            ; Call BIOS

If you need to print the entire string “Hello”, you’d write this block five times, once for each character. That’s 20 lines of code for “Hello”.

With a macro:

%macro PRINT_CHAR 1
    mov ah, 0x0E
    mov al, %1
    mov bh, 0
    mov bl, 0x07
    int 0x10
%endmacro

; Now printing "Hello" is easy:
PRINT_CHAR 'H'
PRINT_CHAR 'e'
PRINT_CHAR 'l'
PRINT_CHAR 'l'
PRINT_CHAR 'o'

But we can do even better:

%macro PRINT_STRING 1
    jmp %%start        ; Skip over the string data
    %%str db %1, 0     ; String data (null-terminated)
    %%start:
    lea si, [%%str]    ; Load address of string
    %%loop:
    mov al, [si]       ; Get character
    cmp al, 0          ; Check for null terminator
    je %%done
    mov ah, 0x0E       ; BIOS teletype
    mov bh, 0
    int 0x10
    inc si              ; Next character
    jmp %%loop
    %%done:
%endmacro

; Usage:
PRINT_STRING "Hello"

This macro not only saves typing — it generates an entire loop with string data embedded right there in the code. Try doing that with a function in assembly!

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here

Share your love