I've used a lot of languages over the years, but my main language of choice for native applications is C++. Early September I had the idea to really make an effort to learn assembly. It turned out to be a good decision for a number of reasons.
Learning assembly had been in the back of my mind for a while, partly out of curiosity and partly because I thought it might help me reason about performance, to micro-optimise code and interpret disassemblies - and partly because I might want to move more toward embedded development in the future, or at least have the option to. Perhaps the main reason though was that it just seemed like a fun thing to do. I already had a bit of experience with it, but had never used it to build anything really substantial.
I have a bit of an obsession with Boulder Dash style games ever since playing Rocks'n'Gems on the original PlayStation as a ten year old. I've always referred to these games as Boulder Dash clones, though I'm not sure if Boulder Dash was actually the first. I've never even played Boulder Dash. Anyway, this type of game is my go-to project idea whenever the question "What shall I make with this language?" comes up. I usually call the game Gems'n'Rocks. I've made it at least five times now.
So I decided to make Gems'n'Rocks in x86_64 assembly on linux. For some reason I also decided that I wasn't going to use any libraries - not even the C standard library. I would just use the linux system call interface. Why make life easy?
Here's a video of the game running on my laptop. The code is on GitHub.
Despite not having much prior experience with assembly, years of C++ development had given me a reasonable grasp of what goes on under the hood during a program's execution (the process address space, the stack, etc.), so whilst there were no major epiphanies, I do have a few thoughts I'd like to summarise here.
First off, conceptually, there's nothing particularly hard about it. There weren't really any new concepts to grasp, it was just extremely fiddly.
I imagined that writing assembly would be a very painstaking and laborious process with a lot of time spent debugging and struggling to make progress. This was only true initially. Once I got the hang of it, it came together very quickly. On September 23rd I could draw a section of an image to the screen; on September 27th I could display an animation by drawing a different section of the image on each iteration of the game loop; three days later I had finished the game.
Whilst working on this game, complexity was the enemy. The need to keep things simple tempered any urge I might've had to generalise prematurely with needless abstraction. For example, when I last made Gems'n'Rocks in C++, animations could have any number of frames from any section of the sprite sheet, and they were defined in an external text file. I also defined transformations and animations as distinct concepts, where a transformation moved the entity at a certain delta per frame and an animation changed the section of the sprite sheet that it rendered. This was too complicated to do in assembly. I decided all animations will be 8 frames in duration, all frames will be 64 by 64 pixels, and all animations will be on a separate row of the sprite sheet. There was no distinct notion of transformations - just a bit flag to indicate if the entity is moved but not animated.
In 2024, I had the benefit of the internet. I could query general questions and get an answer immediately. The tools, the instruction set, and the platform are all well documented. I didn't have to worry about performance; I could just do the simplest thing and it would still be fast enough. In x84_64 there are loads of registers available, so I didn't need to use the stack very often when passing arguments to functions. What I did here is nothing compared to the awesome things people did back in the day.
It can actually be quite straight-forward and readable. However, it does require comments - lots of them. It's not self-documenting in the way most high level languages are.
This probably can't be said of assembly or embedded development in general, but at least for this project, my cycle of write code -> build -> run -> debug -> fix code was extremely rapid. Building the executable was instant. I'd then switch to TTY mode with ctrl + alt + f1 and run the app. It would usually segfault or just not work, so I'd run it with gdb, set a break point, and step through the code while inspecting the registers. I'd usually find the problem quite quickly. There were a couple of upshots to this - the first being surprisingly rapid progress, as I've already mentioned, the second being that it became extremely addictive. It was all too easy to say "Let me just try this one last thing before calling it a day", and before I knew it it was two o'clock in the morning. It might just be how my brain works, but, for me, a fast development loop is essential for productivity. I can't get into the zone otherwise.
I don't want to turn this into a rant about how bloated modern software has become and how much I hate it. I'll save that for another time.
There used to be a TV show here in Britain called Supersize vs Superskinny in which an obese and an underweight person were made to swap diets for five days. Both had lifestyles that were dysfunctional, but in opposite ways. Not only was it entertaining, but there was a kind of logic to it. Sometimes, to gain a sense of perspective, you need to experience the extreme opposite of what you're currently doing. The optimum is then somewhere in-between. If every developer whose first instinct when, say, building a simple website is to npm install 3 frameworks and 10 libraries was made to write some non-trivial application from scratch in assembly, it might go some way toward dealing with the bloat problem.
A software engineer in the 80s or 90s might have imagined that in 2024 computer hardware and software will have grown so complex that every application would be a gargantuan, compiler generated blob of impenetrable logic that no human could ever comprehend the low level workings of, and that this would be the only way to program the machine. I have no idea if anyone actually thought that, or if it was even reasonable to think that, but in any case they needn't have worried. Whilst it's true that bloat is totally out of control, you can still hand-write a tiny program in assembly, that compiles to a few kilobytes, and run it just fine on a modern system. It may not be sensible to do so, but I do like that it's still possible.