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
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
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.
invoke instruction in this code isn’t actually an instruction, instead, it might get transformed into the
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.
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, 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.
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
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
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
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
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.
wsprintfA isn’t exactly ideal, because:
- Microsoft strongly recommends not to use it.
- It limits the output string to 1024 characters.
- 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!
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 😏