7.5 Macros, Modules, and Program Structure
Section 7.4 introduced procedures as runtime reuse: many CALL sites share one instruction body and cooperate through a calling convention. Assembly tools also provide source-level reuse and separate compilation. These mechanisms solve different problems, so a program should choose them deliberately.
A macro expands before instructions are encoded
A macro is a named source template. When the assembler encounters a macro invocation, it substitutes the template body and its arguments into the source stream, then assembles the expanded result.
CLEAR_WORD MACRO destination
MOV destination,0
ENDM
CLEAR_WORD AX
CLEAR_WORD counterConceptually, the assembler sees:
MOV AX,0
MOV counter,0Macro arguments are normally source tokens, not runtime stack values. The assembler can reject an expansion that produces an illegal instruction, but the macro itself does not create a CALL, return address, or stack frame.
This creates a tradeoff:
- A macro avoids CALL/RET runtime work and can specialize its body for each argument, but every expansion adds code bytes.
- A procedure keeps one body and usually saves code when reused many times, but each call uses an instruction, a return-address stack operation, and a register/parameter contract.
Macros with internal labels need unique names per expansion. In MASM-style syntax, LOCAL again asks the assembler to replace again with a distinct generated symbol every time:
REPEAT_STEP MACRO count
LOCAL again
MOV CX,count
again:
; operation body
LOOP again
ENDMWithout LOCAL, expanding the macro more than once defines again: repeatedly in the same assembled source. That is an assembly-time name collision, not a runtime control-flow failure.
Modules separate ownership and enable independent assembly
A module is a source unit that can be assembled separately into an object file. Its internal labels stay private unless deliberately exported. Its external dependencies are declared rather than assigned guessed addresses.
In MASM-style syntax, PUBLIC exposes a definition to the linker and EXTRN declares that another module will provide a definition:
; main.asm
EXTRN sum_words:NEAR
EXTRN print_hex:NEAR
CALL sum_words
CALL print_hex; math.asm
PUBLIC sum_words
sum_words PROC NEAR
; implementation
RET
sum_words ENDPThe assembler for main.asm records unresolved references instead of inventing final addresses. The linker later builds a global symbol table, assigns module regions, and patches each reference with its bound address.
A valid external reference needs exactly one matching definition:
- If main calls
print_hewhile io exportsprint_hex, the names are different andprint_heis unresolved.
- If math and util both export strong definitions named
sum_words, the linker cannot choose one unique address and reports a duplicate symbol.
- Multiple modules may declare the same name EXTRN because declarations are uses, not competing definitions.
Organize around interfaces and ownership
A maintainable assembly program can separate:
- A startup module that initializes segments, invokes high-level work, and owns program termination.
- Algorithm modules that expose small procedure interfaces.
- Device or operating-environment modules that isolate input/output details.
- Shared include files that hold constants, structure offsets, and macro definitions—but not multiple conflicting storage definitions.
Each procedure interface should document parameter locations, return values, changed flags, clobbered registers, preserved registers, and cleanup ownership. Each module should have one clear owner for every public data object and procedure.
Separate compilation makes these contracts more important, not less. The linker can prove that a name has a unique address; it cannot prove that the caller passed parameters in the format the callee expects. That semantic agreement still belongs to the programmer and tests.
Chapter 7 has moved from individual instructions to complete program construction: source and tools, observable control flow, bounded data traversal, safe procedure contracts, and separately assembled modules. Chapter 8 now looks beneath these programs and follows how a processor datapath and control unit carry out each fetch, decode, and execute step.