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.
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
defines, 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.
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
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
pops 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+0/esp||Preserved ebp value|
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
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
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
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
subs, one per local, and naturally we don’t want our code to become less efficient by use of macros. Instead, let’s add a kind of 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… 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
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
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.
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.