Hand-written assembly is full of repetition. The same short run of instructions appears again and again, with little variation, and there are few abstractions to reduce boilerplate. Not only that, coding in assembly requires managing values with no obvious semantics. There are no named parameters and most details of the underlying machinery must be handled manually by the programmer.
NASM, the assembler used by the track, offers some tools which can help the programmer with both issues. These tools run at assemble time, before any instruction reaches the processor. They guide the assembler in how to rewrite the source text before it is even translated into machine code.
The most important of these tools are macros, which can be thought of as a recipe for the assembler. They are expanded at the location where used, as if the code had been written there by hand.
Macros can take a number of literal tokens as parameters, making them reusable in different contexts.
However, they are not functions: there is no call, no ret, no transfer of control to another point in the code.
The simplest definition gives a name to a value or a short expression with %define:
%define WIDTH 8
%define BUFFER_SIZE (WIDTH * 16)
Every later occurrence of WIDTH is replaced by 8 before assembly.
The replacement is textual, so the name is substituted anywhere it appears, whether in an operand or inside another definition.
For numeric constants, prefer equ instead of %define.
A single-line macro declared with %define only expands when used, and may be reassigned or undefined.
An equ constant, on the other hand, is evaluated at the point of definition and is never modified.
A %define can also take named parameters, behaving like a small inline function:
%define double(x) ((x) + (x))
mov eax, double(WIDTH) ; assembled as mov eax, ((8) + (8))
Wrap both the whole body and each parameter in parentheses.
With %define square(x) x*x, the call square(1+1) expands to 1+1*1+1, which is 3.
Written as %define square(x) ((x)*(x)), the same call expands to ((1+1)*(1+1)), which is 4.
Names defined with %define are case sensitive.
The variant %idefine makes the name case insensitive, should that be wanted.
Single-line macros are useful to give meaning to various parts of the code. If used with care, this improves readability and reduces the chance of typos. For example, a register that has a dedicated purpose inside a function may be defined by its purpose:
%define COUNTER rcx
...
.loop:
...
dec COUNTER
jnz .loop
...
A whole sequence of code, with any number of lines, is captured with %macro, closed by %endmacro.
A number after the name indicates how many arguments the macro takes, which can be zero.
When arguments are passed, they are separated by a comma (,):
%macro example_macro 0
...
%endmacro
Arguments in multi-line macros are not named.
They are referred to as %1, %2, and so on, in the order they are passed:
; Multiplies two values, returning the result in the first argument
%macro mult_two 2 ; %1 = accumulator, %2 = value
imul %1, %2
%endmacro
mult_two eax, ecx ; multiplies eax and ecx and returns the result into eax
mult_two r8, r9 ; multiplies r8 and r9 and returns the result into r8
Note that the arguments must be valid syntax for the operations performed in the body of the macro.
Here mult_two eax, ecx and mult_two r8, r9 both assemble, but mult_two eax, cl would not, since imul eax, cl is not defined.
A macro that defines an ordinary label breaks the moment it is used twice, because the label would then also be defined twice.
To use labels inside a macro that may be called more than once, it is necessary to prefix such a label with %%.
This instructs the assembler to give this label a different, unique name on each expansion:
; clobbers rcx
%macro clear_n 2 ; %1 = base address, %2 = count of dwords to clear
xor ecx, ecx
%%loop:
mov dword [%1 + rcx*4], 0
inc ecx
cmp ecx, %2 ; %2 is compared with ecx: it is an immediate or a 32-bit operand
jb %%loop
%endmacro
clear_n rdi, 4 ; gets its own %%loop
clear_n rsi, 8 ; a different %%loop, no clash
It is possible to repeat a one-line block of code using times.
This is particularly useful when declaring repeated values in memory:
section .data
negative_block: times 16 db -1
; this assembles to 16 db -1 in sequence, i.e., an array of 16 bytes, all equal to -1
For multi-line code repetition, we may use %rep instead:
%rep 2 ; number of repetitions: 2
add rcx, 32
mov dword [rsi + rcx], 0
%endrep
Note that the number of repetitions is specified just after %rep and there is an %endrep to close the repeated block.
It is common to use an assemble-time counter inside a %rep block, much as a run-time loop uses a counter.
While an assemble-time integer constant may be declared with equ, for assemble-time integer variables we use %assign instead.
Unlike %define, %assign is evaluated immediately to a number and may be reassigned:
xor eax, eax
%assign i 0
%rep 4
add eax, i ; add 0, then 1, then 2, then 3
%assign i i + 1
%endrep
After the preprocessor runs, the loop is gone and four add instructions remain, with the immediates 0, 1, 2, and 3 baked in.
This is loop unrolling done by the assembler: no counter register and no branch, just straight-line code.
There are various conditionals available to the preprocessor. They select between a number of different paths, expanding their body in-place, but only when their condition holds true.
%if ... %elif ... %else ... %endif select a branch from a numeric test:
%if WIDTH > 4
mov eax, 2
%elif WIDTH > 2
mov eax, 1
%else
mov eax, 0
%endif
Macros are often kept in a file of their own and pulled into each source that needs them with %include:
%include "patterns.mac"
The named file is inserted verbatim at that point, before assembly, exactly as if its contents had been typed in place. This keeps the definitions in one location and lets several source files share them.
If the macro file may be assembled as a standalone, it should have an .asm file extension.
Note that macros have no linkage.
Even if the macro file is assembled, its macros are only visible to another file that %includes it.
Otherwise, an %include-only may have any file extension.
The use of either .asm or .mac is a common convention.
You maintain the macro library that a magazine's composition engine is built on. The engine itself, the functions that lay out each page, is fixed and you do not write it. What you write is the assemble-time machinery it is built from.
This exercise does not require you to write any function, only macros. These macros are then included and called in functions already predefined in another file.
You have four tasks.
Each macro is called by the exact name and argument order its task gives, so write them exactly. Otherwise the file will not assemble.
The magazine's format is a handful of fixed numbers, and the rest of the layout refers to them only by name. You should give each a readable name at assemble time:
COLUMN_COUNT is 4
COLUMN_STRIDE is 18
SPACE is 0x20, the blank byteRULE is 0x3D, the byte =
RULE_WIDTH is 4
PLAIN is 0x2E, the byte .
RULED is 0x2D, the byte -
BOXED is 0x23, the byte #
The engine reports these in the order above.
out = {4, 18, 32, 61, 4, 46, 45, 35} // the eight named values, in order
fill_run macroThe engine blanks a galley and lays masthead rules by stamping a run of a single byte. Capture that step as a multi-line macro the engine can call wherever a run needs filling.
First, define a readable name COUNTER for the register rcx.
Then, define the fill_run macro that should fill an array with N copies of a byte.
It takes three arguments in this order:
fill_run rdi, rax, SPACE
fill_run rdx, RULE_WIDTH, RULE
The fill_run macro may clobber COUNTER freely, but no other register.
Note that the count of bytes may be passed in a 64-bit general-purpose register, such as rax or rsi, or as an immediate, such as 10 or 4.
You may consider that this count is never negative.
This macro is called in three times, in two different functions.
length = 8
page = {32, 32, 32, 32, 32, 32, 32, 32}
width = 8
page = {61, 61, 61, 61, 32, 32, 32, 32} // '====' then spaces
lay_columns macroThe grid is fixed for the format, so the column starts can be computed at assemble time.
Define the lay_columns macro, that writes COLUMN_COUNT offsets, each a 64-bit integer.
It takes two arguments in this order:
lay_columns rsi, rax
The number of offsets to be written to the destination address is fixed and equal to COLUMN_COUNT.
Each offset is a 64-bit value calculated with offset[i] = margin + i * COLUMN_STRIDE, where i is the index of the offset starting at 0, and margin is the second argument.
This macro may clobber its two register arguments, but no other. It is called once.
margin = 3
out = {3, 21, 39, 57}
set_header macroEach of the three standing sections has a header style fixed by the format, so the byte is known at assemble time.
Define set_header, taking two arguments in this order:
0 is plain, 1 is ruled, and 2 is boxed.The destination address is the expression of an effective memory address.
It takes a base register that may be summed with an index register scaled by 1, 2, 4 or 8, and an immediate:
set_header rdx+8*rcx+2, 1
Select the header byte (PLAIN, RULED, or BOXED) with conditional assembly (%if / %elif / %else) rather than a runtime branch.
This macro should not clobber any register. It is called three times in the same function.
pitch = 4
out = {46, 0, 0, 0, 45, 0, 0, 0, 35} // '.', '-', '#'
Sign up to Exercism to learn and master x86-64 Assembly with 22 concepts130 exercises, and real human mentoring, all for free.