A little adventure into Assembly

A few days ago a thought popped into my head. “Hey, let’s learn how to write assembly!”

While I know the basics of how Assembly works, and even though I’ve read some Assembly code over the years while debugging or analyzing applications, I’ve never written an actual program in any Assembly language. That obviously had to change.

How hard could it be, right?

Assemblers and Linkers

As it turns out, getting started really isn’t all that bad. You only need two things: An assembler, to turn your code into machine code, and a linker, which turns it into an executable. This part isn’t so different from other languages, like C.

The first question then is which assembler and linker to use. While pretty much any linker will do, as they should generally be able to work with whatever the assemblers generate, which assembler to use borders on the question of what programming language to use. They have different styles and come with different built-in functions. Some focus on Windows development, others are more general-purpose.

The two big names you see a lot are MASM (Microsoft Assembler) and NASM (Netwide Assembler). Since I usually focus on Windows development, I first looked at MASM, but the Hello Worlds I found really weren’t what I was expecting.

.386
.model flat,stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

.data
	output db "Hello World!", 0ah, 0h;

.code
start:

	invoke GetStdHandle, STD_OUTPUT_HANDLE
	invoke WriteConsole, eax, addr output, sizeof output, ebx, NULL

	invoke ExitProcess, 0

end start

If you have only ever seen Assembly code in debuggers this sight might be a little surprising, no pushes, no moves… this is no Assembly! It seems closer to C if anything, with its includes and normal looking function calls. The reason for this is that between what I knew as Assembly, and more high level languages, there is some overlap, because Assembly programmers obviously want some comfort as well.

The invoke instruction in this code isn’t actually an instruction. Instead, it might get transformed into the push/mov/call pattern I had expected to see.

push NULL
push ebx
push sizeof output
push addr output
push eax
call WriteConsole

Especially if you come from higher level languages you can see why one might prefer invoke over this. Not only do you have to type more this way, the default Windows calling convention for functions (stdcall) also requires you to push arguments in reverse order, from right to left, which invoke handles automatically.

Similarly, sizeof is a “magical” construct as well, that tells us the length of a string in this example. But it’s just there for convenience. Usually you would have to figure out the length of that string yourself.

MASM has quite a few such creature comforts, and while I’m sure those are great to have when you’re developing seriously, I didn’t want to be tempted by convenient functions like that at the start. I also looked at GoAsm, another assembler with focus on Windows, but it too had quite a few such features, and it’s less popular, meaning you’d find less support for it.

Outside of Windows there’s a few other options, but the most popular choice appears to be NASM. Not only is it less focused on Windows, it also doesn’t come with too many features out of the box, aside from what you might expect from Assembly. Since the syntax was also to my liking, I decided to go with that one.

While looking into GoAsm, however, I tried the linker GoLink from the same author, and I quite liked how simple to use it was, so I went with it. As previously mentioned, switching to a different linker down the line should be fairly simple.

So my tools of choice ended up being NASM and GoLink, both of which can easily be downloaded for free.

Hello, World!

Alright, time for a proper Hello World!

global _start

section .data
	hello:     db 'Hello, World!',10
	helloLen:  equ $-hello

section .text
_start:
	; write hello world
	mov eax, 4
	mov ebx, 1
	mov ecx, hello
	mov edx, helloLen
	int 80h

	; exit with code 0
	mov eax,1
	mov ebx,0
	int 80h

Now that’s more like it! Look at those moves and interrupts! And how the string length is manually calculated! I love it! There’s just one little problem. While you see examples like this a lot, this code won’t run under Windows. In the Linux world, you can use interrupts to call functions in the kernel, such as “function” 0x80, which essentially prints a string. In DOS you could use such interrupts as well, but not on Windows. Microsoft wants you to use the Windows API for such purposes, and that might look something like this.

global _start

extern GetStdHandle
extern WriteConsoleA

section .data
	hello:     db 'Hello, World!',10
	helloLen:  equ $-hello

section .text
_start:
	; get standard output handle
	push -11
	call GetStdHandle
	
	; write hello world
	push 0
	push 0
	push helloLen
	push hello
	push eax
	call WriteConsoleA

	; return 0
	xor eax, eax
	ret

Paste this code into a text file, compile and link it with the following commands, and you’re good to go.

> nasm -f win32 hello_world.asm
> golink /console /entry _start hello_world.obj kernel32.dll

Of note are, for one, the argument to nasm, -f win32, which tells it that we want 32-bit Windows code generated, then the entry argument to golink, telling it the label where the program starts (_start in this case), and finally the argument “kernel32.dll” at the end, which we add to tell the linker to search for external functions (or rather symbols) in that DLL. This is what allows us to call GetStdHandle and WriteConsoleA, which aren’t part of the code, but which we told the Assembler to make addressable, because they exist in some external file.

So far, so good! This isn’t difficult at all! Well…

I miss C

Naturally, you can’t do a lot with a program that does nothing but output some text. The next step for me is usually to extend my Hello Worlds, to make them ask my name and greet me directly. Read the input and print the formatted message Hello, [input]!

What's your name?
: exec
Hello, exec!

And this took more than copying and understanding a Hello World example. See, as far as I can tell, there’s no viable, build-in function to print a formatted string in the Windows API, nor is there a function to read a line from the input that works exactly as I was expecting.

WriteConsoleA writes a string, but doesn’t support formatting. ReadConsoleA reads a line, but it includes the line-break (\r\n) in the result, which you use to end the data input. Now, there’s a very simple solution to these problems: use printf and getline. These are part of the C library, which you can easily link to, and they’re the functions I might use for this Hello World test in other languages. But if I had wanted to use C functions, I could write C. What’s next, link to .NET and use Console.WriteLine? No, that doesn’t seem right. I won’t learn anything by going the easy route.

First things first, printing a formatted string. Without using the C library, you seem to have exactly two options here. One is writing your own formatting function, but I didn’t feel quite ready for that by this point. The other is called wsprintfA and is part of user32.dll.

https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-wsprintfa

Unfortunately, wsprintfA isn’t exactly ideal, because:

  1. Microsoft strongly recommends not to use it.
  2. It limits the output string to 1024 characters.
  3. It has no limit checks outside of those 1024 characters.

They recommend some alternatives, but these are only implemented via header files, so they presumably forgot about all the Assembly programmers out there. These alternatives internally use vsnprintf, which is a C library function… Because, naturally, Microsoft’s C++-based DLLs link against the C library. In lack of a better alternative, I decided to use wsprintfA for the moment, but to implement a better solution later on.

So! Prepare a buffer, write a formatted string to it, and print everything!

hello:     db  'Hello, %s!',0
helloLen:  equ $-hello
input:     db 'exec',0
buffer:    times 1024 db 0

; ...

push input
push hello
push buffer
call wsprintfA ; format string
add esp, 12    ; clean up stack
mov ebx, eax   ; save length of the formatted string

push -11
call GetStdHandle

push 0
push 0
push ebx       ; string length
push buffer    ; buffer
push eax       ; handle
call WriteConsoleA

The buffer is a simple byte array. We call wsprintfA, save the return value, which is the length of the formatted string, and pass everything to WriteConsoleA as before. Just include “user32.dll” in the linking process now, and you’ll get a formatted message.

> hello_world.exe
Hello, exec!

One thing of note in this code, is that after calling wsprintfA, the arguments must be removed from the stack manually, as the function doesn’t clean up after itself. That’s because it’s a variadic function, that takes a variable amount of parameters, and it would be difficult for the callee to determine how many parameters it’s supposed to remove from the stack, so the caller needs to take care of that. And the simplest way to remove 3 arguments from the stack, is to move the stack pointer by 3*4 bytes (3 arguments, times 32-bit per argument on the stack). This is also how the “cdecl” calling convention, which C uses, works, so you always have to clean up after calling functions in the C library. But the 32-bit Windows API uses stdcall, where the function takes care of that.

Now only reading a line is left!

Reading is hard

As I’ve mentioned above, there is a function to read a line from the input, but the result includes the line break. Also, at this point I was still just calling the right functions to get things done, but I did want to get my hands dirty, so instead of relying on ReadConsole I decided to give ReadConsoleInput a try. Hoh boy…

I just wanted to read from the input, character by character, and write those into a buffer, but ReadConsoleInput doesn’t simply read characters, it’s basically an event for everything that happens to the console, from mouse input to focusing the console window. It’s actually much more powerful than a simple getchar, and with that, I certainly had an oppurtunity to get my hands dirty, because not only do you have to read characters in a loop to fill up a buffer, you also have to work with the structs that function returns, pick out the events you’re interested in, output the characters you take from the input, etc. This was the first actual code I wrote that didn’t just call some functions, and it took me a while to get it right.

From misaligning my struct definitions, to access violations due to passing the wrong registers or address offsets, to logic errors because I failed to write basic if checks, I had everything. It was kinda funny to start up a debugger and see my code 1:1 in there though 😊

After a day of fiddling around with ReadConsoleInput, and also testing PeekConsoleInput at some point, I ended up with a simple routine that could do what I wanted, though it wasn’t perfect. It didn’t support backspace yet, nor navigating with the arrow keys, all of which are things you have to implement yourself if you were to go this route, which I hadn’t thought about.

My final, though unfinished and unoptimized code, looked something like this.

struc INPUT_RECORD
	.EventType resw 1
	alignb 4
	
	 ; union
	.Event:
		.KeyEvent:
		.MouseEvent:
		.WindowBufferSizeEvent:
		.MenuEvent:
		.FocusEvent:
			resb KEY_EVENT_RECORD.sizeof

        ; Include an empty label at the end of the struct,
        ; which's offset will be equal to the struct's size.
	.sizeof resb 0
endstruc

struc KEY_EVENT_RECORD
	.bKeyDown resb 1
	alignb 4
	.wRepeatCount resw 1
	.wVirtualKeyCode resw 1
	.wVirtualScanCode resw 1
	
	; union
	.uChar:
		.UnicodeChar:
		.AsciiChar:
			resw 1
	
	.dwControlKeyState resd 1
	
	.sizeof resb 0
endstruc

; Buffer for ReadConsoleInput
inputRecord resb INPUT_RECORD.sizeof

; ...

ReadLine:

	push ebp
	mov ebp, esp
	
	mov edi, [esp+4] ; buffer
	mov ecx, [esp+8] ; size

.read:
        ; TODO: Don't call GetStdHandle over and over.
	push STD_INPUT_HANDLE
	call GetStdHandle

	; Read one character from stdin
	push readLineEventsRead
	push 1
	push inputRecord
	push eax
	call ReadConsoleInput
	
	; Cancel if read console failed
	cmp eax, 0
	je .done
	
	; Read next character if event was not a key press
	cmp word [inputRecord+INPUT_RECORD.EventType], KEY_EVENT
	jne .read
	cmp byte [inputRecord+INPUT_RECORD.KeyEvent+KEY_EVENT_RECORD.bKeyDown], 1
	jne .read
	
	; Cancel if the character was a new line
	mov dl, byte [inputRecord+INPUT_RECORD.KeyEvent+KEY_EVENT_RECORD.AsciiChar]
	cmp dl, `\r`
	je .done
	cmp dl, `\n`
	je .done
	
	; Don't accept any more characters if buffer is full
	cmp dword [ebp+4], 0
	je .read
	
	; Write character to buffer and advance it by 1, where the next
	; character or a terminator can be placed.
	mov byte [edi], dl
	inc edi
	
	; Decrement size to keep track of how much space we have left in
	; the buffer.
	dec dword [ebp+4]
	
	; Write character to stdout
	push 1
	push inputRecord+INPUT_RECORD.KeyEvent+KEY_EVENT_RECORD.AsciiChar
	call WriteN
	
	jmp .read
	
.done:
	mov byte [edi], 0
	
	pop ebp
	ret 8

And after I finally had something that kind of did what I wanted… I quickly switched to using ReadConsole instead and just trimmed the new line at the end 😅

ReadLine:
	
	mov ebx, [esp+4] ; buffer
	mov ecx, [esp+8] ; size
	
	push STD_INPUT_HANDLE
	call GetStdHandle
	
	push 0
	push readLineEventsRead
	push ecx
	push ebx
	push eax
	call ReadConsole
	
	; Get length of string
	mov eax, [readLineEventsRead]
	sub eax, 2
	
	; Trim new line by terminating string after the read characters,
	; where the line break would start.
	add ebx, eax
	mov byte [ebx], 0
	
	ret 8

It’s fun to try to reinvent the wheel every once in a while, and it’s always a good lerning experience, but of course you should use whatever gets you to your goal the fastest, and implementing your own input reader is definitely not part of that if you can get around it.

But after finishing my readers and writers, I was done with my extended Hello World program.

extern Write
extern WriteLine
extern WriteLineF
extern ReadLine

section .data
	nameQuery   db `What's your name?\r\n: `,0
	hello       db 'Hello, %s!',0
	inputGiven  db `(Input given: %s)`,0
	noName      db 'Fine, keep your secrets.',0

section .bss
	inputLen equ  64
	input    resb inputLen

section .text
	_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
> hello_world.exe
What's your name?
:
(Input given: )
Fine, keep your secrets.

> hello_world.exe
What's your name?
: exec
(Input given: exec)
Hello, exec!

Conclusion

From start to finish, it took me an entire weekend to read up on Assembler, decide on a toolchain, write print and read functions, get everything to a point where I can compile my code with a simple command, run it, and not get any crashes. Though I once more realized how small a layer C is on top of Assembly.

If you allow it yourself, Assembly isn’t so different from slightly less low level languages, and given a few convenience functions and clever macros, you can actually end up with something that isn’t so far off from base C. You can easily simulate constructs like ifs and loops, make accessing arguments and variables easier, even replace calling constructs, like with invoke, to arrive at something that looks closer to languages like C in some ways.

Assembly takes more time and effort, and making mistakes is much easier (if not guaranteed) for sure, but no matter how you approach it, using the C library, writing everything from scratch, or taking a mixed approach, at some point you will have a base library that you can use to write programs with relative ease, because you end up just using the right functions and macros for the job at hand.

Just like you might not want to use C for a project because you can be more productive in a higher level language, you probably shouldn’t use Assembly either if your goal is to just get things done and you have other options, not to mention that in most cases compilers can produce better Assembly code than you anyway… But I would recommend the experience to people who want to see the real low level, or who’re interested in reverse engineering, because learning a few things about Assembly, common patterns, and why some things are the way they are is eye opening. And it is kinda fun 😁

If I were to continue writing in Assembly, I would definitely start adding macroinstructions like invoke to my code, but I would write them myself, so I still know exactly what they do, and how. Wouldn’t want to lose that learning experience.

Next stop: a server emulator in Assembly 😏