ASM Macros

Elucian MoiseElucian Moise
11 min read

Macros are a very useful feature of assembly language that allows you to define shortcuts or nicknames for repetitive sequences of assembly instructions.

Some key points about macros:

  • Macros are defined using the .MACRO and .ENDM directives. Everything between these two directives is the macro definition.

  • Macros are invoked by their name, just like a function call. The macro name is replaced by its defined sequence of instructions during assembly.

  • Macros can accept parameters that are passed during invocation. The parameters are represented using symbols like %1, %2, etc inside the macro definition.

Here is the explanation of Macros in Assembly with code in code blocks:

.MACRO ADD %1,%2     
    ADD R0,%1   
    ADD R0,%2    
.ENDM

This macro is defined to add two numbers by adding them to the R0 register.

It accepts two parameters %1 and %2 representing the two numbers.

The macro is invoked as:

ADD R1,R2

Which will get assembled into the actual addition instructions:

ADD R0,R1   
ADD R0,R2

Adding two numbers 5 and 10 would be done as:

ADD R1,R2   ;R1 has 5 and R2 has 10

;Which expands to:
ADD R0,R1  
ADD R0,R2

;Now R0 will have 15 (5 + 10)

Macros make the code modular by allowing you to define reusable code snippets. They reduce repetition and improve readability.

Some other points:

  • Macros can have loops and conditional statements inside their definition.

  • Macro parameters can have default values.

  • Macros nesting is also possible.

Notes: R0 is not a real register on x86 processors. R0 was used as a generic register name in the macro examples for illustration purposes.

The x86 architecture uses different register names:

  • EAX - Accumulator register

  • EBX - Base register

  • ECX - Counter register

  • EDX - Data register

  • ESI - Source index register

  • EDI - Destination index register

  • ESP - Stack pointer register

  • EBP - Base pointer register

Some other general purpose registers in x86 are:

  • AX

  • BX

  • CX

  • DX

So in the macro examples, R0 was used as a generic accumulator register name. If porting those examples to x86 assembly, EAX should be used instead of R0.

For example, the ADD macro for x86 would be:

.MACRO ADD %1,%2 
    ADD EAX,%1 
    ADD EAX,%2 
.ENDM

Parameter Convention:

When declaring macros with parameters in assembly, it is a convention to use % signs before the parameter names for a few reasons:

  1. It clearly identifies the parameters as macro parameters, not normal variable names. This avoids confusion.

  2. It allows the parameters to be used generically, without hardcoding specific names. The macro can then work with any number and type of parameters.

  3. It makes the macro definition reusable. The same macro definition can be called with different actual parameters each time.

For example:

macro1 MACRO %param1, %param2
    ; Macro body using %param1 and %param2
ENDM

macro1 5, 10   ; Call with actual parameters 5 and 10
macro1 20, 30  ; Call again with different parameters

Here the macro macro1 can be reused with different actual parameters (%param1 and %param2 represent the formal parameters).

The % signs before the parameter names indicate that:

  • %param1 and %param2 are macro parameters, not normal variables

  • The macro body can use them generically, without assuming specific names

  • The macro definition is reusable with different actual parameters

So in summary, using % signs before macro parameters in assembly:

  • Clearly identifies them as macro parameters

  • Allows the macro to work generically with any number of parameters

  • Makes the macro reusable by calling it with different actual parameters

The % convention is just that - a convention, not a requirement. But it helps clarify the macro's definition and makes the parameter names distinct from normal variable names.

Example: Defined a macro 'expr' that takes 5 parameters

MACRO expr 5
    mov rax, %1 ; a
    add rax, %2 ; a + b 
    sub rax, %3 ; a + b - c

    mov rbx, %4 ; d
    add rbx, %5 ; d + e

    imul rbx ; (a + b - c) * (d + e)
ENDM 

section .text
    expr 10, 20, 5, 30, 40 ; Evaluate (10 + 20 - 5) * (30 + 40)

So in summary there are 2 conventions I have found so far. You have to check specific compiler documentation to be sure which one you can use. Once convention is to use a number after the macro name and %n to count parameters from 1 to n.

Another convention is to enumerate parameters after the macro name, separated by a comma. I'm not yet sure what convention is used by every compile. Here is the general syntax:

macro_name MACRO parameter1, parameter2, ...
     ; Macro body
ENDM

Where:

  • macro_name is the name of the macro

  • parameter1, parameter2, ... are the names of the macro parameters, separated by commas

  • The parameters do not need to be enclosed in brackets [ ]

For example, a valid macro definition would be:

addMACRO MACRO reg1, reg2
    ADD reg1, reg2  
ENDM

Use-Cases

Here are some common use cases where macros are preferred over procedures in assembly language:

  1. Frequently used short code snippets: Macros allow you to define short code patterns that you use repeatedly. Since macros perform text substitution, they avoid the overhead of procedure calls for short snippets.

For example, a macro to load a register from memory:

LOADMACRO MACRO reg, address
     MOV reg, [address]
ENDM

This can be used as:

LOADMACRO eax, myVar
LOADMACRO ebx, someAddress
  1. Conditional compilation: Macros can be defined conditionally based on preprocessor #ifdefs, allowing you to selectively include or exclude code at compile time.

For example:

#ifdef DEBUG
     DEBUGMACRO MACRO 
         ; debug code
     ENDM
#endif
  1. Interfacing with hardware: Macros are useful for defining hardware registers and accessing hardware ports. Since macros are pasted literally, they map cleanly to hardware.

For example:

PORTMACRO MACRO port
     IN port, al
     OUT port, al
ENDM
  1. Loop unrolling: Macros can be used to unroll loops by defining a macro that represents a single iteration. This avoids loop overhead.

  2. Short inline functions: For functions that are just a few lines of code, macros avoid the overhead of function calls.

So in summary, macros in assembly are commonly used for:

  • Short, frequently used code snippets

  • Conditional compilation

  • Hardware interfacing

  • Loop unrolling

  • Inline functions

Procedures are still useful for more complex, reusable subroutines. But for the above use cases, macros provide a lightweight, optimized alternative in assembly language.

To clarify, macros are useful when they:

  • Take parameters and perform reusable logic

  • Avoid repetitive inline code

  • Allow for easy future expansion

But for very simple one-line snippets without any parameters or reusable logic, inline code is preferable to unnecessary macros.


Here are some useful use cases where macros are preferable to procedures in assembly language:

  1. Inlining: When a procedure is called, the CPU has to perform a jump to the procedure, execute the code, and then jump back. With a macro, the macro instructions are inlined at the call site. This can improve performance for very small and simple procedures.

For example:

PROCEDURE Swap 
    push ax
    push bx
    mov ax, [bx]
    mov bx, [ax] 
    mov [ax], bx
    pop bx
    pop ax
    ret

Swap:   ; Procedure call
    call Swap

SWAP_MACRO 
    push ax
    push bx
    mov ax, [bx]
    mov bx, [ax]
    mov [ax], bx
    pop bx
    pop ax

SWAP_MACRO ; Macro call

The macro call has the instructions inlined, avoiding the procedure call overhead.

  1. Parameters passed in registers: Procedures typically pass parameters on the stack, while macros can pass parameters in registers. Passing parameters in registers can be faster.

  2. Macros support complex expressions as parameters: Procedure parameters are typically variables, while macro parameters can be complex expressions.

  3. Fewer instructions: Since macro instructions are inlined, there are no procedure call/return instructions, making the code more compact.

  4. Avoiding stack usage: Macros do not use the stack for parameter passing, avoiding stack usage overhead. This can matter for small, performance-critical code.

  5. More control over optimization: Since macro instructions are inlined, the assembler has more opportunities for optimization (e.g. common subexpression elimination).

So in summary, macros are preferable to procedures in assembly when:

  • Instruction inlining provides a performance benefit

  • Passing parameters in registers is faster

  • You need to pass complex expressions as parameters

  • Fewer instructions and less stack usage matter

  • You want more control over optimization opportunities

Macros are generally a good fit for small, performance-critical code where the benefits of inlining and avoiding procedure call overhead outweigh the benefits of modularity that procedures provide.


Here are some examples where macro overhead can provide a significant benefit over function calls:

  1. Tight loops: Consider a tight loop that calls a small function on each iteration:
loop:
    call func
    jmp loop

The function call overhead on each iteration can add up, especially in tight loops that iterate millions of times. Replacing the function with an inlined macro can avoid this overhead:

loop:
    MACRO_FUNC  
    jmp loop
  1. Interrupt handlers: Interrupt handlers need to execute as fast as possible to minimize latency. Function calls within an interrupt handler can add significant overhead. Using inlined macros can speed up interrupt handlers.

  2. Hardware interactions: Code that interacts directly with hardware registers often needs to be executed very quickly. Function calls within this critical code can slow it down. Macros can be inlined to avoid the overhead.

  3. Small functions: Function call overhead is more significant for very small functions that contain only a few instructions. For these cases, inlining the macro can provide a more noticeable performance benefit.

  4. Performance-critical code: In general, for any performance-critical code where microseconds or nanoseconds matter, function call overhead can add up. Macros that are inlined at the call site can avoid this overhead and provide a performance benefit.

In summary, macro overhead can provide a significant benefit over function calls in situations where:

  • Function calls occur in tight loops

  • Latency needs to be minimized (interrupt handlers)

  • Performance is critical (hardware interactions, small functions, etc.)

In these cases, the benefits of inlining the macro and avoiding function call overhead - including stack manipulation, register saving/restoring, and jump instructions - can outweigh the benefits of modularity that functions provide.


Call overhead

Here is an in-depth explanation of function overhead in assembly language:

When a function is called, the CPU has to perform several tasks:

  1. Push arguments to the stack: For each argument that is passed to the function, the CPU pushes the argument value to the stack. This takes time and uses stack space.

  2. Save registers: The CPU saves any registers that the function may modify on the stack. This is done to preserve the register values for the calling function. This also takes time and uses stack space.

  3. Jump to function: The CPU performs a jump instruction to the start of the function code. This jump takes a few CPU cycles to execute.

  4. Function epilogue: At the end of the function, the CPU has to:

  • Pop saved registers from the stack to restore their values

  • Pop function arguments from the stack

  • Perform a return jump instruction to return to the caller

All of this takes additional CPU cycles and stack space.

  1. Register allocation: The CPU may need to save more registers than necessary if the compiler cannot determine which registers are actually used by the function. This wastes CPU cycles and stack space.

In summary, function overhead in assembly comes from:

  • Pushing arguments to the stack

  • Saving registers that may be modified

  • The function call and return jump instructions

  • Popping arguments and saved registers from the stack

  • Potentially unnecessary register saves due to limited optimization

All of these factors contribute to the performance impact of function calls, especially for small functions where the overhead is more noticeable relative to the amount of actual function code.

The overall impact is an increase in CPU cycles, memory accesses (to stack), and stack space usage - all of which can matter for performance-critical or embedded code where resources are limited.


Power Macro

Here is a macro in 8086 assembly language that calculates the power of two numbers:

POWER MACRO num, power  
    LOCAL power_loop
    PUSH    BP
    MOV     BP,SP
    MOV     AX,[BP + 4] ;num in AX
    MOV     BX,[BP + 6] ;power in BX       
    MOV     CX,1        ;Result  
    power_loop:
        CMP BX,0
        JE done   
        IMUL CX,AX     ;Multiply result by num    
        DEC BX        
        JMP power_loop      
    done:    
    POP BP       
    ENDM

How it works:

  • The macro is defined as POWER MACRO num, power and takes two arguments: the base number and the power.

  • The arguments are accessed using BP + 4 and BP + 6 since the first two stack locations are used by PUSH BP and MOV BP,SP.

  • AX is used to store the base number and BX stores the power.

  • CX is used to store the result, initially set to 1.

  • The power_loop iterates:

    • It checks if the power is 0 using CMP BX,0

    • If so, it jumps to done

    • Otherwise, it multiplies the result CX by the base number AX using IMUL CX, AX

    • It decreases the power BX

    • And jumps back to the loop

  • At the end, the result is stored in CX.

  • The macro uses LOCAL to define power_loop as a local label.

  • It ends with ENDM to mark the end of the macro definition.

So in summary, this macro defines a POWER macro that calculates the power of two passed arguments (num and power) by multiplying the base number num power number of times.


Disclaim: The information provided in this article is for educational purposes only. While AI tools can provide useful information and code examples, they are not a substitute for learning from authoritative resources and human experts.

0
Subscribe to my newsletter

Read articles from Elucian Moise directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Elucian Moise
Elucian Moise

Software engineer instructor, software developer and community leader. Computer enthusiast and experienced programmer. Born in Romania, living in US.