ASM Macros
Table of contents
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:
It clearly identifies the parameters as macro parameters, not normal variable names. This avoids confusion.
It allows the parameters to be used generically, without hardcoding specific names. The macro can then work with any number and type of parameters.
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 variablesThe 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 macroparameter1, parameter2, ...
are the names of the macro parameters, separated by commasThe 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:
- 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
- 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
- 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
Loop unrolling: Macros can be used to unroll loops by defining a macro that represents a single iteration. This avoids loop overhead.
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:
- 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.
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.
Macros support complex expressions as parameters: Procedure parameters are typically variables, while macro parameters can be complex expressions.
Fewer instructions: Since macro instructions are inlined, there are no procedure call/return instructions, making the code more compact.
Avoiding stack usage: Macros do not use the stack for parameter passing, avoiding stack usage overhead. This can matter for small, performance-critical code.
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:
- 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
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.
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.
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.
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:
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.
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.
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.
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.
- 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.
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.