Assembly: Redefined – Macros to the rescue

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 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.

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 pushes and 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-4].

ebp-4temp
ebp+0/espPreserved ebp value
ebp+4Return address
ebp+8val1
ebp+12val2
Stack

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 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 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.

https://nasm.us/doc/nasmdoc4.html