Tiny virtual machines and a bloatware rant

I just spent yesterday afternoon and this morning “implementing” an LC-3 (Little Computer 3) VM in C. (By the facetious quotes I mean that I was following a tutorial with a lot of training wheels.)

What does this mean?

What’s a virtual machine?

A VM is a program that simulates some CPU architecture with I/O interaction, and can understand programs written in machine language. A VM could simulate a certain video game platform, or a modern OS. A VM could also be some made-up architecture; it doesn’t need to correspond to an existing machine.

Reasons you might use a VM:

How does a container differ from a VM?: Containers (think of Docker or Kubernetes) also provide a certain level of virtual separation, but less than VMs. Containers run on top of the existing operating system, while virtual machines run on top of an entirely simulated architecture.

What’s machine language? How does it differ from Assembly/ASM?

Each microprocessor (or VM) has a hardcoded instruction set it supports. Instructions are very low-level operations corresponding tightly with the underlying architecture, such as “add values in these 2 registers and put the result in another register”, or “jump to this location in memory”. Microprocessors operate entirely in binary (0s and 1s).

Assembly languages are kind of the bare minimum veneer of human readable syntax for describing machine instructions. They let you use niceties such as whitespace, ASCII characters, and even comments, so you don’t have to write pure binary.

For example, in the LC-3 instruction set, 0001001010000011 translates to ADD R1 R2 R3, meaning “add the values in registers 2 and 3, and store the result in register 1”.

Because they are so low-level, every assembly lang must target a particular computer architecture. However, there is nothing stopping multiple different assembly langs from targeting the same architecture.

Many different assembly instruction sets exist out there. One of the most commonly used ones, x86 Assembly, is designed for x86 Intel processors, which are the most widely used processors in the world.

An assembler is a program that converts your human readable assembly code into a binary “object file” so that your machine can actually read it. You can think of an assembler as an extremely specific subtype of compiler that only converts Assembly to binary.

Industrial assemblers often run performance optimizations in the translation step. However, understanding them requires a level of intimacy with the hardware that is eldritch far beyond my current knowledge.

What’s LC-3?

LC-3 is a spec for a small educational toy 16-bit architecture & instruction set that you can build quickly in simulation. The instruction set is a lot smaller and simpler than classic x86 Assembly, which has >80 instructions.

LC-3 has 2^16 = 65,536 memory locations, each of which stores a 16-bit value, for a total of 128KB of memory.

There are 2 special registers (one for the program counter, one for condition flags), and technically 8 general-purpose registers. However, R7 is typically reserved for storing jump locations.

There are 16 opcodes. An opcode is a hardcoded number corresponding to a machine instruction. However, you can have more instructions than opcodes because some opcodes have multiple modes, triggered by flipping a bit flag reserved for that purpose. For example, ADD can either add together the values at two pointers, or add the value at a pointer with a small binary value provided directly in the assembly instruction.

Isn’t it weird to write an Assembly-consuming VM in something as high level as C, which then runs on top of a whole OS?

Not really; the point of VMs is portability.

Most programs have compatibility issues with different computer architectures. If you want to port a program directly to another architecture, you probably need to rewrite a lot of chunks of it at a very low level. This is why it almost never happens for consumer programs with big user interfaces.

Thankfully it is not too rare to see ports of low-level terminal utilities.

LC-3 VM implementation in C

I have always been intimidated by this low level of systems, so I really appreciated the tiny scope of LC-3. It only took about an hour to implement the instruction set, which mostly involved pushing bits around in uint16s. Most of my time was spent reading the documentation and understanding the scaffolding that was necessary before you could start using the instruction set. In total it took a day (of course all thanks to the excellent tutorial writeup).

The tutorial step order made sense from an abstract perspective, but it felt out of order codewise. In order to do intermediate checks to see if I had implemented the instruction sets correctly, I had to skip around a lot in the tutorial. So I skipped around and did the scaffolding first.

I uploaded my implementation here. If you also want to try writing the LC-3 instruction set but you don’t want to understand all the rest of the setup code, I uploaded my scaffolding and toolchain setup instructions here as a jumping-off point.

However, I would really recommend typing everything out manually like I did and taking the time to understand each line, instead of copypasting. I made a typo in one bitwise operator along the way and OH BOY was it an educational experience.

Footnotes: why did I get interested in this rabbithole

Hundred Rabbits uxn platform

I recently read a computery writeup I quite enjoyed, but without VM/OS experience, I had trouble understanding the design considerations. So I went off on the LC-3 tangent in order to better understand this talk.

“Weathering software winter” is a Hundred Rabbits talk about the design considerations that went into the base of their current personal off-grid tech stack, uxn, a super tiny custom VM. It runs programs written in a small assembly instruction set called uxntal, which has 32 opcodes. The smallness makes it easy to port across lots of hardware.

No one can take a paper computer away from you. […] this is a form of computing that can be easily ported.

This is cool because it lets you write utilities that perform well on old, cheap hardware, such as a Nintendo DS that has never touched the internet, or a Raspberry Pi, or a $100 Chromebook. As opposed to the modern method of buying a new thousand dollar device that will last 2 years and break if you don’t feed it 11 gigabyte software updates all the time.

They use uxn to run various custom utility tools they have written to replace their industry bloatware tools. It is obviously not a consumer general-purpose computer, but it works for their purposes – graphics editing, an office toolsuite, livecoding, etc.

For context, Hundred Rabbits is a studio of two creatives (focusing somewhat in video games) who work on computers and live full-time internationally on a sailboat with only 180W of solar power. Open sea crossings mean that they sometimes go months without internet. They have thoroughly clashed with the off-grid limitations and planned obsolescence of modern software and hardware.

A lot of modern Western software & hardware assumes nearly-unlimited internet bandwidth, and crashes in unexpected ways if you remove it from its expected parameters. So you basically cannot develop for MacOS if you live in certain parts of the world, can’t use modern Adobe software at all while offgrid, etc.

Further 100r reading:

modern bloatware rant

I live squarely in a U.S. city with high-speed internet and even so – simply as a person who does not want to throw away a perfectly good computer and buy a new one every few years – I often clash with bloatware and hardware obsolescence. I upgraded from my 2013 computer last year not because anything was inherently wrong with it, but because it could no longer run a modern web browser, videochat, and a team messaging tool simultaneously without seizing up.

Computers get exponentially more powerful and yet the software that runs on them gets slower, buggier, less introspectable, and shorter-lived all the time.

Modern software is simultaneously more and less accessible.

Systems level stuff does not get terribad as quickly – probably because people who directly push bits around understand that magic’s only guarantee is your swift and wretched ruination – but web toolchains get exponentially worse with each passing year. It is pretty standard these days to be forced to setup hundreds of packages and several complicated interlocking layers of frameworks, toolchains, and watchers just to laboriously compile and render the most basic static site functionality you can imagine. As previously described, I avoid modern heavyweight stacks like the plague while in the realm of personal projects.

Last year while contracting for MIT Data+Feminism Lab, I got to overhaul a tool where I was explicitly being hired to make it work better for international users with low network bandwidth, which was really refreshing and restored my faith in humanity a little bit. Most of our non-U.S.-based users were on 2.4GHz networks, and some had <1Mbps download speed. I throttled my browser to 500Kbps for testing, and it was both an upsetting and enlightening experience.

I miss the 00s internet.