After taking my first steps in the world of Assembly, and getting my little extended Hello World running, I had learned that writing error-free Assembly code is by no means impossible, but you have to take great care to not make mistakes. I basically had to turn on my debugger after every bigger modification because I had accidentally broken something.
But just like C and other languages protect you from simple mistakes, like not unwinding the stack after calling a cdecl-convention function or addressing variables on the stack incorrectly, you can easily help yourself write cleaner, more robust code with the help of macros.
Preprocessing
NASM, the assembler I decided to use, has a pretty powerful preprocessor, that is similar to C’s preprocessor in many ways. You have your define
s, you can create macro “functions”, and with a bit of fiddling around, you can turn something like this:
_start:
push nameQuery
call Write
push inputLen
push input
call ReadLine
push eax
push input
push inputGiven
call WriteLineF
add esp,8
pop eax
cmp eax, 0
jne .sayHello
push noName
call WriteLine
jmp .end
.sayHello:
push input
push hello
call WriteLineF
add esp, 8
.end:
xor eax, eax
ret
into the visually much more appealing
sproc _start
invoke Write, nameQuery
invoke ReadLine, input, inputLen
cinvoke WriteLineF, inputGiven, input
if eax,'==',0
invoke WriteLine, noName
else
cinvoke WriteLineF, hello, input
endif
xor eax, eax
endp
I’m always amazed how simple it is to “redefine” a language this way, and aside from being easier on the eyes, it can also help with making the code less error prone.
Functions
The first macro I tackled was proc
(for procedure), which is what most Assembly coders seem to name their function macros. Technically, setting up a simple function isn’t all that difficult or error prone, since it’s essentially just a label that you jump to, but it can become very useful in combination with other macros, like ones for defining arguments or local variables, as the proc
macro can help with preparing everything, and the endp
macro with clean up.
For demonstration purposes, let’s take a simple function that multiplies two values with each other and also uses a local variable.
; int multiply(int val1, int val2)
multiply:
push ebp
mov ebp, esp
sub esp, 4 ; make space on stack for temp variable
mov eax, [ebp+8] ; val1
mul dword [ebp+12] ; multiply by val2
mov dword [ebp-4], eax ; save eax in temp
mov eax, 1234 ; use eax for something else
mov eax, [ebp-4] ; write temp back to eax for return
leave
ret 8
A rather pointless function, but it will work for demonstrating the proc
pattern that I came up with. First though, a quick word about what’s going on here.
The function starts with preserving ebp
on the stack and setting it to esp
, while calling leave
before ret
, which basically reverses this, restoring ebp
. This is a common pattern to make addressing arguments and local variables easier, because ebp
is typically a preserved register, that you can expect not to change when calling other functions. Additionally, you don’t have to factor in potential push
es and pop
s in your code, which would make addressing local variables using [esp+X]
rather difficult.
With the preserved ebp value sitting at [ebp+0]
, and the return address at [ebp+4]
, the first argument can be found at [ebp+8]
. In the other direction, since we push local variables after setting ebp
, you can find the first local variable at [ebp-4]
.
ebp-4 | temp |
ebp+0/esp | Preserved ebp value |
ebp+4 | Return address |
ebp+8 | val1 |
ebp+12 | val2 |
Arguments and locals are then accessed with [ebp+-X]
, and at the end ret
is called with the size of the arguments (2 integers = 8 byte) to unwind the stack and effectively remove them from it. And by resetting esp
to ebp
using leave at the end you remove the local variables automatically. The function is very simple, but one can imagine how easy it is to make mistakes in just these few lines.
If you add or remove an argument you have to remember to also update the return, otherwise your application will likely crash, due to the state the stack is in. If you forget leave
, your function will not restore ebp
and your locals will not be removed, also most likely causing a crash. And if you get the offset to one of your arguments or locals wrong, in the best case you will get wrong behavior, and in the worst, you guessed it, a crash.
The first step for me then was to define the proc
macro, as a wrapper around my function body.
%imacro proc 1
%ifctx proc
%error "proc can't be nested"
%endif
%1:
push ebp
mov ebp, esp
%push proc
%endmacro
%imacro endp 0
%ifnctx proc
%error "unexpected endp without proc"
%endif
leave
ret
%pop proc
%endmacro
These are multi-line macros, with which you can do a lot. You define them with a name and a number of arguments, which you can then use inside them. The one parameter proc
gets is the name of the function. It then ensures that you’re not using the macro multiple times without ending the previous one, sets up a label that can be used with call
, and sets up ebp for use with arguments and locals.
By combining the instructions proc foobar
and endp
, you quickly get a simple wrapper for a function.
proc foobar
; code
endp
; evolves to~
foobar:
push ebp
mov ebp, esp
; code
leave
ret
Arguments
A good start, but just hiding how ebp
is modified and making ret
static, without stack unwind, is probably not such a good idea, unless the function doesn’t have any arguments. With the addition of an arg
macro and some changes to endp
, however, everything comes together.
%imacro arg 2
%ifnctx proc
%error "arg must be contained in a proc"
%endif
%ifndef %$argsOffset
%assign %$argsOffset 8
%assign %$returnSize 0
%endif
%xdefine .%1 ebp+%$argsOffset
%assign %$argsOffset %$argsOffset+%2
%assign %$returnSize %$returnSize+%2
%endmacro
%imacro endp 0
%ifnctx proc
%error "unexpected endp without proc"
%endif
leave
%ifdef %$returnSize
ret %$returnSize
%else
ret
%endif
%undef %$argsOffset
%undef %$returnSize
%pop proc
%endmacro
The arg
macro essentially does just one thing, it sets up a local define that you can use to address your arguments with by name. That’s the only effect it has on your code, though it doesn’t even produce any code, it all happens on the preprocessor. It also saves the total size of the arguments in a context-scope variable though, which can then be used from endp
to unwind the stack correctly. What it needs for these two things are a name for the variable and its size, because you naturally don’t have to only push simple 4 byte integers to the stack, it could also be arrays, structs, etc.
Setting up local variables is almost the same, just with a different offset for ebp
, so I’ll not paste that here, but at the end of this post I’ll include all the macros I wrote, including comments, so you can take a closer look at them.
With these changes in place, the function already becomes quite a bit more structured.
proc multiply
arg val1, 4
arg val2, 4
local temp1, 4
sub esp, 4
mov eax, [.val1]
mul dword [.val2]
mov dword [.temp1], eax
mov eax, 1234
mov eax, [.temp1]
endp
However, the local variables still need to be set up, and with everything else hidden behind macros, that lone sub esp
seems out of place. No big deal, we can get rid of that as well. One option would be to change esp
inside the local
macro, but with a lot of local variables, that would be a lot of unnecessary sub
s, one per local, and naturally we don’t want our code to become less efficient by use of macros. Instead, let’s add a convention to place arguments and locals in a kind of “header” part of the function, and initialize the “body” of the function with another macro, beginp
.
(beginp… I serioulsy didn’t notice that until just now.)
All this macro will do is modify esp
for the total size of the locals we declared.
%imacro beginp 0
%ifdef %$localsSize
sub esp, %$localsSize
%endif
%endmacro
I also added a few alternatives for arg
and local
, so you don’t always have to define the size in bytes, and that gives us our final multiply function. No “magic numbers” to access the stack, no risk of using the wrong register, automatic clean up with implicit leaves and returns at the end.
proc multiply
argd val1
argd val2
locald temp1
beginp
mov eax, [.val1]
mul dword [.val2]
mov dword [.temp1], eax
mov eax, 1234
mov eax, [.temp1]
endp
Plagiarism
It’s funny how sometimes you learn what certain design decisions of other people were about, that didn’t make sense to you at the time. For example, when I tried to write my first 2D top-down game I was determined to not rely on tile movement, because I thought free pixel movement would be so much better, and easier, because you can just move the character pixel by pixel as long as the move key is pressed. That worked great, until I realized that in a tile-based map design you have to be able to walk through narrow paths, and with strict pixel-based movement it’s rather difficult to get around corners without much additional work… and that’s how I decided that tile-movement was definitely superior!
What does that have to do with Assembly though? Well, I felt pretty good about this pattern I had come up with, but somehow… it felt familiar. This header section… and how arguments and locals are defined before the actual code of the function… That’s when it dawned on me, that I had accidentally re-invented Pascal/Delphi functions. There, this function might look something like this:
function multiply(val1: Integer, val2: Integer): Integer
temp1: Integer;
begin
val1 := val1 * val2;
temp1 := val1;
val1 := 1234;
Result := temp1
end;
Quite similar, isn’t it? A few years ago I briefly had to work with Delphi and I absolutely hated this design. What was that “header section” about? Coming from other languages it seemed so weird. That’s definitely not how I would design a language! Well, if you approach it from the other direction, you have to consider that the compiler has to turn that code into Assembly and/or machine code somehow, and since those compilers are written by humans, the code they produce will be similar to what a human might write. I’m guessing that the design the Pascal developers created stemmed from a similar structure as the one I came up with, something that could be easily converted to Assembly.
Conclusion
I ended up refining these macros some more, adding a proc
alternative that doesn’t modify ebp, for functions that don’t need it, adding support for variadic functions (variable number of arguments with no ret
value), adding invoke macros to make calling functions easier, and even adding if/elseif/else macros, to make branching easier, eventually arriving at the code example at the start of this post.
sproc _start
invoke Write, nameQuery
invoke ReadLine, input, inputLen
cinvoke WriteLineF, inputGiven, input
if eax,'==',0
invoke WriteLine, noName
else
cinvoke WriteLineF, hello, input
endif
endp
I like where this is going, and I could see myself actually developing applications this way, but as I went along, I realized more and more how silly it would be to actually do it as long as I have other options, like C. Even if you took the C library away, it already has all the function, struct, loop, and variable patterns you could want, which you’re just “patching in” when you’re using Assembly. Why would you do that?
For me personally, a web, desktop, and server developer, there’s little to no practical reason. Having started to work with Assembly, I have a natural urge to do more with it, and familiarizing myself more with it will come in handy when analyzing and debugging programs, but for that it might even be better to write raw Assembly, tedious as it might be, since that’s also what I’m seeing in debuggers. Writing actual applications in my “redefined” Assembly though? I don’t know.
That being said, Assembly does have a certain mystic about it, and it would be kind of fun to have an actual application written in it. I might just do it 😅
In case you’re curious about my full util.inc file, with all my current macros and more detailed comments, you can find it here:
For further reference, I recommend reading the official documention for the NASM preprocessor, which contains all information necessary to write these and other macros. It can be found at the following URL.