Use nasm preprocessor to write clean x86 asm
Why should you use the nasm preprocessor ?
Writing assembly is a tedious work.
Some of my greatest issues with assembly language programming are
- the lack of names for local variables
- the need to write the same assembly code repetitively (for instance function’s prologue an epilogue)
- and the lack of data structures.
These are three good reasons to learn a preprocessor language when you learn to write assembly and there are a lot of others like the need to adapt the code to the cpu features, or the need to generate large portions of code (like declaring an array of 1024 bytes filled with ones), etc.
The goal of this post is to introduce you to the nasm preprocessor which is a way more powerful preprocessor than the C one.
Disclaimer: This post intends to be a reminder of useful nasm preprocessor functionalities. It is mainly for my personal use and reflects what i judge “useful”. If you want an exhaustive view of the nasm preprocessor you can directly read the nasm documentation here Moreover some of the examples in this post are shamelessly copied from the nasm documentation. Note that they all were translated to linux-x86_64 asm.
Including other files
Include a source file
Like with the C preprocessor, you can include a source file at the current line by using the %include directive.
You can use it to create a file containing all the macros we will define and include their definition in your future asm source files.
%include "macros.asm"This will insert the content of the file at the current line.
Like with the C preprocessor you can use header guards in included files:
%ifndef MACROS_ASM
%define MACROS_ASM
; You can put here the
; content of your macros.asm
; file
%endifThe included files are found in the include path which can be specified in the compile command with the -I flag.
Include a binary file
The nasm language contains the incbin directive to include the binary content of an external file in our object file. If you use incbin in your program it will actually use the macro wrapped around the incbin directive to search the filename in the path and add it to the dependency list.
incbin "file.dat" ; include the whole file
incbin "file.dat",1024 ; skip the first 1024 bytes
incbin "file.dat",1024,512 ; skip the first 1024, and actually include at most 512Defining single-line macros
You had a first glimpse at nasm macros with the header guard we used for macros.asm.
We used %ifndef, %define and %endif to conditionally include the file only once. Let’s see what else we can do with the %define directive.
Defining macros with %define
The simpler way to define a macro with nasm as with the C-preprocessor is to use the directive %define. It should be very familiar to you if you already used C.
You can define object macros or function macros using this directive.
; A macro with no parameters
%define ctrl 0x1F &
; A two parameter macro
%define param(a,b) ((a)+(a)*(b))
mov byte [param(2,ebx)], ctrl 'D'Which will expand to:
mov byte [(2)+(2)*(ebx)], 0x1F & 'D'It can be used to define constants as well as the equ directive, but is way more-powerful.
A notable difference between %define and equ is that %define constants are expanded when invoked, while equ constants are expanded when defined.
For instance:
; addr will expand to the location of the assembly position
; - of this line:
addr equ $
; of the line in which the macro is invoked:
%define addr $If you want %define content to be expanded on definition you can use the alternative directive: %xdefine.
The main difference between %define and #define from the C-processor is that with nasm, macro functions can be overloaded.
; macro function can be overloaded
%define foo(x) 1+x
%define foo(x, y) 1+x+yHowever a macro function can’t coexist with a macro object of the same name.
; nasm will produce 'error: macro `foo' defined both with and without parameters [-w+error=pp-macro-def-param-single]'
%define foo bar A macro argument can be omitted if it is never used. Which means an overload macro function cannot have zero arguments (A macro defined with empty parenthesis is a sub-case of functions with one unused argument).
%define overload(x) db 15 ; Define a single-line macro with one argument
%define overload() db 10 ; Redifine overload as a macro with a single unused argument
overload(aaa) ; will expand to db 10
overload() ; will expand to db 10 tooAs in C, a single-line macro can be undefined using the %undef directive.
Macro objects types
Macro objects can have one of the four types: numeric, string, tokens and aliases. Let’s examine the two more useful ones: numerics and strings.
We can define macro numerics using the %assign directive.
The assign directive take an argument which has to be a critical expression and evaluate to a pure number (it can’t be a symbol). The nasm documentation refers to a critical expression as:
an expression whose value is required to be computable in the first pass, and which must therefore depend only on symbols defined before it.
As with %define, a numeric macro can be redefined, which allow us to increment our macro:
%assign i 1
; here the macro will expand to 1
%assign i i+1
; from now the macro will expand to 2A numeric macro can also be defined using the %strlen directive which will define a macro equals to the length of the expanded parameter if its a string.
We also have a directive to define macro strings: %defstr
; Both lines are equivalent
%defstr test TEST
%define test 'TEST'
; The following line assign 4 to the length macro
%strlen length testThe %defstr directive can be useful with another directive: %!, which allows to read environment variables.
For instance to define a string containing the value of the $PATH variable (at compile time !!):
%defstr PATH %!PATHNumeric arguments
You can ensure an argument of a macro is a valid numeric expression by prefixing the name of the parameter with an equal sign. The argument will be evaluated after its expansion.
%define raxset(expr) mov rax, expr
raxset(1 + 5) ; will expand to mov rax, 1 + 5
raxset(rdx) ; will expand to mov rax, rdx
%define raxset(=expr) mov rax, expr
raxset(1 + 5) ; will expand to mov rax, 6
raxset(rdx) ; will produce error: 'non-constant expression in parameter 0 of macro'Quoted arguments
You can turn an argument into a quoted string by prefixing it with an ampersand (’&’). It will surround the argument with double-quote, even if it is already a quoted string. If you want to avoid this behavior you can use the double-ampersand prefix.
%define add_lf(&str) db str, 10, 0
add_lf(1 + 5) ; will expand to db '1 + 5', 10, 0
add_lf("1 + 5") ; will expand to db '"1 + 5"', 10, 0
add_lf('1 + 5') ; will expand to db "'1 + 5'", 10, 0
%define add_lf(&&str) db str, 10, 0
add_lf(1 + 5) ; will expand to db '1 + 5', 10, 0
add_lf("1 + 5") ; will expand to db "1 + 5", 10, 0
add_lf('1 + 5') ; will expand to db '1 + 5', 10, 0Multi-line macros
Our lovely %macro and %endmacro couple
Multi-line macro are defined using the %macro directive an have to be closed using a %endmacro directive.
You have to indicate the amount of parameters the macro can take after its name.
Multi-line macros can be use to reduce aggressively the amount of logic repetition in your code.
A useful example, is the definition of the function prologue:
%macro prolog 1 ; The number of parameter is 1
push rbp
mov rbp, rsp
sub rsp, %1 ; %1 refers to the first parameter
%endmacro
myfunc:
prolog 0x40
; will expand to:
; push rbp
; mov rbp, rsp
; sub rsp, 0x40A macro can take any amount of parameters which will be accessed via their 1-based index starting from %1, %2, %3 and so on.
%0 is reserved for the number of arguments passed to the macro because as we’ll see later, multi-line macros can take a variable amount of parameters.
In the same way that single-line macros can be undefined with %undef, multi-line macro can be undefined with the %unmacro directive.
Macro-local labels
Sometimes you need your multi-line macro to contain a label to perform a jump inside the expansion. Using a local label (a label prefixed by a dot) is not enough as we want to be able to call the macro several times in a function.
You can then use a macro local label which is prefixed by ‘%%’ and will expand to a different symbol for each invocation of the macro.
Let’s write a macro which returns when the Z flag is set:
%macro retz 0
jnz %%skip ; jump over the ret instruction if the Z flag is not set
ret ; returns from the function
%%skip: ; macro-local label
%endmacroCondition codes as macro parameters
As you can enforce a single-line macro parameter to be a numeric expression, you can enforce a multi-line macro parameter to be a condition code.
Referring to a multi-line macro parameter (the first in our example) using %+1 will ensure that it will be a condition code. You can also refer to a condition code using %-1 which will replace the condition code by its negation.
Using this syntax we can now generalize our previous retz macro by taking a condition code as parameter.
%macro retc 1
j%-1 %%skip ; jump over the ret instruction if the condition code is false
ret ; returns from the function
%%skip: ; macro-local label to jump if condition code is false
%endmacro
myfunc:
test rdi, rdi ; set the status flags according to the content of the rdi register
retc ne ; returns if rax is nullI wouldn’t recommend to use the retz and retc macro to make early return as according to the Intel Optimization Reference Manual 3.4.1.2 when using static prediction, forward branches are predicted to not be taken. But early returns are often used as a way to manage errors so we want them to be unlikely and the branch to be predicted as taken (skipping the return instruction).
So you can use these macros but remember it will result in branch mispredictions if the return is unlikely.
If you want to make early returns though you should probably jump to a .exit label at the end of the function the same way the C compiler will do.
Varying amount of parameters
The last parameter of a macro can be declared “greedy” adding a ‘+’ after the parameter count declaration. Which means it will expand to the comma-separated list of all the additional arguments.
Let’s define a macro to write to a file on a 64-bit Unix system. It will take at least two parameters:
- The first will be the file descriptor to which we want to write
- The following parameters will be a list of sequence of bytes to write
SYS_write equ 1 ; sys_write is the syscall n°1
%macro sys_write 2+ ; The second parameter will be greedy
jmp %%endstr
%%str: ; we define a macro-local label
db %2 ; and drop all the parameters from the second one as bytes
%%endstr:
mov rax, SYS_write ; we put the syscall number in rax
mov rsi, %%str ; put the adress of the string in rsi
mov rdx, %%endstr - %%str ; put the size of the string in rdx
mov rdi, %1 ; put the filehandle in rdx
syscall
%endmacroThe macro can also have optional parameters for which you can provide a default value.
The syntax to indicate that the macro take at least 3 parameters and at most 5, with the 4th and 5th parameters respectively defaulting to eax and [eax+2] is:
%macro mymacro 3-5 eax, [ebx+2]Using this knowledge, let’s define a macro to exit the program which take an exit code as optional parameter:
SYS_exit equ 60 ; sys_exit is the syscall n°60
EXIT_SUCCESS equ 0 ; the success error code is 0
%macro sys_exit 0-1 EXIT_SUCCESS ; The macro takes one optional parameter
mov rax, SYS_exit ; we put the syscall number in rax
mov rdi, %1 ; we put the error code in rdi
syscall
%endmacroWith these two macros defined, we can write a hello world much more readable than those you will see in most assembly tutorials:
section .text
global _start
_start:
sys_write 1, "Hello World!", 10, 0
sys_exitCondition and loop directives
Conditional directives
Similarly to the C preprocessor, NASM allows sections of a source file to be assembled only if certain conditions are met. The general syntax of this feature looks like this:
%if<condition>
; some code which only appears if <condition> is met
%elif<condition2>
; only appears if <condition> is not met but <condition2> is
%else
; this appears if neither <condition> nor <condition2> was met
%endifThe inverse forms %ifn and %elifn are also supported.
The %else clause is optional, as is the %elif clause. You can have more than one %elif clause as well.
Variants of the %if directive | the code is assembled if… |
|---|---|
%if | the numerical expression evaluate to non-zero |
%ifdef | the given single-line macro is already defined |
%ifmacro | the given multi-line macro is already defined |
%iftoken | the given tokens exapand to a single token |
%ifctx | the given list of parameters contain the name of the context on top of the stack |
%ifidn | the given two single-line macro expand to the same code |
%idid | the first given token is an identifier |
%ifstr | the first given token is a quoted string |
%ifnum | the first given token is a integer numeric constant |
%ifempty | the expanded parameters does not contain any content at all |
%ifenv | the given environment variable exists |
Each variant has its corresponding %elif, %ifn, and %elifn directives; for example, the equivalents for the %ifdef directive are %elifdef, %ifndef, and %elifndef.
Our final write macro
An important mechanism in the nasm preprocessor is that all the text contained inside the definition of a multi-line macro is saved and expanded when the macro is used.
This means that we can use conditional directives to perform tests over the macro parameters and generate code depending on this parameters.
For example, our write macro can be extended to take advantage of %ifstr and %ifid in the following fashion:
SYS_write equ 1 ; sys_write is the syscall n°1
; For this version we need at least 2 parameter because we'll test the type of the second one.
%macro sys_write 2-3+ ; The third parameter will be optional and greedy
mov edi, %1 ; put the filehandle in edx
%ifstr %2 ; if the second parameter is a string
jmp %%endstr
%%str: ; we define a macro-local label
%if %0 == 3 ; If there are three parameters
db %2, %3 ; we drop all the parameters from the second one as bytes
%else
db %2 ; otherwise we drop the second parameter as bytes
%endif
%%endstr:
mov rsi, %%str ; put the adress of the string in esi
mov edx, %%endstr - %%str ; put the size of the string in edx
%elifid %2
mov rsi, %2 ; put the adress of the string in esi
mov edx, %3 ; put the size of the string in edx
%else
%error "The second parameter of write should be a string or an identifier"
%endif
mov eax, SYS_write ; we put the syscall number in eax
syscall
%endmacroThen the write macro can cope with being called in either of the following two ways:
sys_write [file], strpointer, length
sys_write [file], "hello", 13, 10We used the %error directive to generate and error under certain condition.
This is very useful when you want to produce meaningful error messages.
%rep directive and loop unrolling
With nasm you can use the times prefix to repeat an instruction:
array:
times 3 db 0xA
; Will be assembled to:
; array:
; db 0xA
; db 0xA
; db 0xAHowever it can’t be used to repeat multi-line macros since these are expanded before the times prefix. To circumvent this, nasm gives us the %rep and %endrep directive, which repeat the inner code the number of times given as parameter.
We can compute the index of the loop using the %assign directive.
%assign i 0
%rep 64
inc word [table+2*i]
%assign i i+1
%endrepA loop can be exited using a %exitrep directive (as the break keyword in C).
The %rotate directive moves all parameters of a macro to the left and the first parameter become the last one.
The shift count is passed as a parameter. With a negative count the parameters are rotated to the right.
The %rotate directive can be leveraged with %rep to iterate over the arguments of a multi-line macro:
%macro multipush 1-*
%rep %0
push %1
%rotate 1
%endrep
%endmacro
%macro multipop 1-*
%rep %0
%rotate -1
pop %1
%endrep
%endmacroWriting control structures using the context stack
Something we may want to write using macros are control structures such as if…elsif…else, for loops, while loops… We have already been able to write conditional returns using macro-local labels and condition codes as parameters.
However to write control structures, we need to be able to reference some labels defined by other macro calls from our macros. Luckily, the nasm preprocessor has a mechanism to make this possible: the context stack.
The context stack allows to save on a stack some local labels and local single-line macros.
Each local context can be named when it is pushed on the stack using the %push directive
The context on the top of the stack can be restored using the %pop directive which take an optional context name. If a context name is given and it’s not the name of the context on top of the stack, nasm will generate an error.
%push first
; define some context-local
; labels and macros...
%push second
; labels and macro from the first context
; can't be accessed from here
%pop second
; we're back in our first context
%pop firstContext-local labels
Context-local labels can be defined and used with the %$label syntax.
This will allow us to write a simple repeat…until loop:
%macro repeat 0
%push repeat ; Push a context named 'repeat' on top of the context-stack
%$begin: ; Define a context-local label named 'begin'
%endmacro
%macro until 1
j%-1 %$begin ; Jump to the context-local label 'begin'
%pop repeat ; Pop the 'repeat' context from the context stack
%$end
%endmacroAnd use it this way:
iota_byte:
mov rcx, rdi ; rcx = 2nd arg
repeat
dec rcx ; decrement rcx
mov [rsi + rcx], cl ; write cl at rsi + rcx
test rcx, rcx ; set the status register
until z ; repeat until rcx == 0We can (and will later on), define all the common kinds of control structures that exist in higher level languages.
Context-local macros
Context-local single-line macro can be define in just the same way.
%define %$localmac 3Macro and label from a higher context can both be accessed using %$mylabel for the parent, %$$mylabel for the grandparent and so on…
This allow us to retain information other than labels over multiple macro calls. Using this mechanism we can improve our prolog macro and write its companion the epilog macro:
%macro prolog 1
%push func ; push a new context on the stack
%define %$frame_size %1 ; define the context-local macro frame_size
push rbp
mov rbp, rsp
sub rsp, %$frame_size
%endmacro
%macro epilog 0
%ifnctx func ; test if a prolog macro has been issued and if all the inner contexts have been closed
%error "The epilog macro shouldn't be call until all the contexts pushed in the local function have been poped"
%endif
add rsp, %$frame_size ; use the context-local macro to clear the stack
pop rbp
%pop
ret
%endmacroNow we can use our prolog macro to define the frame size and subtract it from rsp. Then our epilog macro will be able to know the frame size and clean it up by adding it to rsp.
Let’s use a simple fibonnacci implementation as example:
; fibo(rdi: u64)
fibo:
; prologue
prolog 0x10
; test for final case
test rdi, (~1)
jz .zero_or_one ; fallthrough is the likely case
mov [rbp-0x8], rdi ; save n on stack
sub rdi, 1 ; rdi = n - 1
call fibo ; rax = fibo(n - 1)
mov [rbp-0x10], rax ; save fibo(n - 1) on stack
mov rdi, [rbp-0x8] ; rdi = n
sub rdi, 2 ; rdi = n - 2
call fibo ; rax = fibo(n - 2)
mov rdx, [rbp-0x10] ; rdx = fibo(n - 1)
add rax, rdx ; rax = fibo(n - 2) + fibo(n - 1)
jmp .end ; jump to epilogue
.zero_or_one:
mov rax, rdi ; return n
.end:
; epilogue
epilogA if..else..endif macro
Using context-local labels, and the context renaming feature we can now define if..else..endif macros to simplify writing of conditions.
To rename the context on top of the context-stack we can use the %repl directive.
%macro if 1
%push if
j%-1 %$ifnot
%endmacro
%macro else 0
%ifctx if
%repl else
jmp %$ifend
%$ifnot:
%else
%error "expected `if' before `else'"
%endif
%endmacro
%macro endif 0
%ifctx if
%$ifnot:
%pop
%elifctx else
%$ifend:
%pop
%else
%error "expected `if' or `else' before `endif'"
%endif
%endmacroA sample usage of these macros might look like:
cmp ax, bx
if ae ; if(ax >= bx)
cmp bx, cx
if ae ; if(bx >= cx)
mov ax, cx
else
mov ax, bx
endif
else
cmp ax, cx
if ae ; if(ax >= cx)
mov ax, cx
endif
endifNote that according to the common rules of branch prediction the if branch is expected to be the likely case and the else branch should be the unlikely one.
This is similar to the code produced by gcc for an if-else pair.
However, when writing a series of if…elsif…elsif… you should have at most one branch misprediction for each case. Therefore, cases should be written in reverse order.
Simplifing references to variables
Defining structures
We already defined a bunch of macros to simplify system calls. But some system calls take pointers to structured data as arguments. It is the case for the stat system call for instance.
C structures are very simple. It is basically a collection of named offsets.
nasm preprocessor contains the standard macros struct and endstruc to define structures and istruc and iend to declare instances of structures.
The stat system call has this prototype
int stat(const char *restrict chemin, struct stat *restrict statbuf);And struct stat is defined in /usr/include/asm/stat.h.
Using nasm preprocessor:
struc stat
st_dev: resq 1
st_ino: resq 1
st_mod: resw 1
st_nlink: resw 1
st_uid: resw 1
st_gid: resw 1
st_rdev: resq 1
st_size: resq 1
st_blksize: resq 1
st_blocks: resq 1
st_atime: resq 1
st_atime_nsec: resq 1
st_mtime: resq 1
st_mtime_nsec: resq 1
st_ctime: resq 1
st_ctim_nsec: resq 1
__unused_stat: resq 2
endstrucUsing C:
struct stat {
unsigned long st_dev;
unsigned long st_ino;
unsigned short st_mode;
unsigned short st_nlink;
unsigned short st_uid;
unsigned short st_gid;
unsigned long st_rdev;
unsigned long st_size;
unsigned long st_blksize;
unsigned long st_blocks;
unsigned long st_atime;
unsigned long st_atime_nsec;
unsigned long st_mtime;
unsigned long st_mtime_nsec;
unsigned long st_ctime;
unsigned long st_ctime_nsec;
unsigned long __unused4;
unsigned long __unused5;
};The struc…endstruc pair of macro create a list of symbols: st_dev, st_ino… And each symbol is located at the relative offset of the member of the structure.
It also define the symbol stat_size, by concatenating the struct name with _size.
An instance of struct stat can be created this way:
section .bss
file_stat:
resb stat_sizeOr initialized this way:
section .data
file_stat:
istruc stat
at st_dev, dq 0x0
at st_ino, dq 0x0
...
iendAnd members can be accessed using the [file_stat+st_dev] syntax.
section .text
global _start
_start:
sys_stat "/var/log/messages", file_stat
mov r8, [file_stat + st_dev]
mov r9, [file_stat + st_ino]
...Note that to avoid cluttering the global namespace you can define the struct fields as local label:
struc stat
.dev: resq 1
.ino: resq 1
.mod: resw 1
...
endstrucAnd access them this way:
section .text
global _start
_start:
sys_stat "/var/log/messages", file_stat
mov r8, [file_stat + stat.dev]
mov r9, [file_stat + stat.ino]
...Referencing stack parameters: %arg
Simplifying references to local variables and parameters requires giving some information on the structure of the stack.
This is done using the %stacksize directive whose parameter must be one of: flat, flat64, large or small.
As long as after the call of the function rip is on the stack and local variables can be referenced using rbp you can use the flat64 parameter.
On Linux 64-bits, the 6 first arguments are passed using the register. But the others are pushed on the stack in the inverse order they are defined.
nasm gives us another directive %arg to name the adress in the stack of this pushed parameters.
You have to give to %arg the list of these stack-parameters and their size in this format: [name]:[byte|word|dword|qword|tword|oword|yword|zword]
section .text
; long func(long a, long b, long c, long d, long e, long f, long g, short h);
global func
func:
%stacksize flat64
%arg g:qword, h:word
prolog 0x0
xor rax, rax
mov rax, rdi ; 1st param
mov rax, rsi ; 2nd param
mov rax, rdx ; 3rd param
mov rax, rcx ; 4th param
mov rax, r8 ; 5th param
mov rax, r9 ; 6th param
mov rax, qword [g] ; 7th param
xor eax, eax
mov ax, word [h] ; 8th param
epilogReferencing local variables: %local
The nasm preprocessor can also relieve us of the burden of managing local variables’ addresses and the frame size.
We can declare local variables using the %local directive in the same way we used the %arg directive.
Before a call to %local, the context-local macro %$localsize has to be defined as a numeric constant. %local will add the summed size of the local parameters to %$localsize.
We can improve our prolog macro so that it can take two optionals arguments:
- The initial value of %$localsize which will account for the “free space” we want in the stack frame, which is the space left above the local variables declarations.
- The list of local variables as the
%localdirective waits for, enclosed in brackets.
Enclosing the parameters in brackets allows to pass as a single parameter comma and colon-separated tokens.
%macro prolog 0-2
%push func ; Push a context for the function
%ifn %0 = 0 ; If there are arguments
%ifnum %1 ; If the first one is a numeric constant
%assign %$localsize %1 ; Initialize the %$localsize with it
%ifn %0 = 1 ; If there is another parameter
%local %2 ; Pass it to the %local directive
%endif
%else
%assign %$localsize 0 ; Otherwise initialize %$localsize to 0
%local %1 ; And pass the first parameter to %local
%endif
%endif
push rbp ; Save the base pointer on the stack
mov rbp, rsp ; Save the stack pointer in the base pointer
%ifdef %$localsize ; If the %localsize macro is defined
sub rsp, %$localsize ; Update the stack pointer with its value
%endif
%endmacro
%macro epilog 0
%ifnctx func ; test if a prolog macro has been issued and if all the inner contexts have been closed
%error "The epilog macro shouldn't be call before all the contexts pushed in the local function have been poped"
%endif
%ifdef %$localsize ; If the %localsize macro is defined
add rsp, %$localsize ; use the context-local macro to clear the stack
%endif
pop rbp ; Restore the base pointer
%pop ; Pop the function context
ret ; Return from the function
%endmacroWe can now name our local variables in the fibonacci function defined earlier:
; fibo(rdi: u64)
fibo:
; prologue
%stacksize flat64
prolog {n:qword, res:qword}
; test for final case
test rdi, (~1)
jz .zero_or_one ; fallthrough is the likely case
mov [n], rdi ; save n on stack
sub rdi, 1
call fibo ; rax = fibo(n - 1)
mov [res], rax ; save fibo(n - 1) on stack
mov rdi, [n] ; rdi = n
sub rdi, 2
call fibo ; rax = fibo(n - 2)
mov rdx, [res] ; rdx = fibo(n - 1)
add rax, rdx ; rax = fibo(n - 2) + fibo(n - 1)
jmp .end ; jump to epilogue
.zero_or_one:
mov rax, rdi ; return n
.end:
; epilogue
epilogSummary
- You can include source files using
%includeand binary files using the incbin macro - You can define single-line macros using
%define,%assign,%defstr… - You can define multi-line macros using
%macro…%endmacro. These can take a varying amount of parameters accessed using their index (starting from 1, %0 being the number of parameters) - You can use
%if..%elif..%else..%endifand their variants to assemble code conditionally - You can use
%rep..%endrepfor loop unrolling and%exitrepto break the loop early - You can use
%pushand%popto manage the context stack and use the %$identifier syntax to declare context-local labels and macros - You can use the
%argdirective to name stack parameters and%localto name local variableo
Moreover you earned yourself a set of handful macros to write cleaner assembly!