Capstone: A Tiny Stack Machine
The full brief — you architect and build it
- › Design a bytecode virtual machine: opcodes, an operand stack, and a fetch-decode-execute loop
- › Implement it in C using everything from the spine — arrays, structs, control flow, functions
- › Understand your VM by seeing it mirror the real CPU you studied
- › Produce a working interpreter you can extend, and read its own assembly
The build that ties it all together
Thirty days ago a byte was a mystery. Now you'll build a computer inside a computer: a tiny stack machine — a virtual CPU that executes its own little bytecode. This is the capstone because it uses *everything*: an array (the program and the stack), a struct (the machine state), control flow (the fetch-decode-execute loop), functions, and the exact mental model of a CPU you learned in week 1. When it runs, you'll realize you're not just using the machine — you understand it well enough to *build one*.
What a stack machine is
A stack machine is a CPU whose instructions operate on an operand stack instead of registers.
PUSH 5 puts 5 on the stack; ADD pops the top two, pushes their sum. It's how the JVM, Python's
bytecode, and WebAssembly work — and it's simpler to build than a register machine, so it's the
perfect first VM. Your machine has three pieces, mirroring the real CPU (day 3):
- an operand stack (an array + a stack pointer) — like the CPU's stack (day 11),
- a program (an array of bytecode) and a program counter (index of the next instruction) —
- like memory + rip,
- a fetch-decode-execute loop — exactly the CPU's cycle (day 3).
The instruction set (you can extend it)
Define an opcode enum. A minimal but real set:
1typedef enum {2 OP_PUSH, // next byte is a value -> push it3 OP_ADD, // pop b, pop a, push a+b4 OP_SUB, // pop b, pop a, push a-b5 OP_MUL, // pop b, pop a, push a*b6 OP_PRINT, // pop and print the top7 OP_HALT // stop the machine8} OpCode;9 10typedef struct {11 int stack[256]; // the operand stack12 int sp; // stack pointer: index of the next free slot13 const unsigned char *code; // the bytecode program14 int pc; // program counter: index of the next opcode15 int running; // 1 while executing16} VM;The heart: fetch, decode, execute
The whole machine is one loop. Fetch the opcode at pc, advance pc, decode it (a switch), execute
its effect on the stack. This *is* the CPU cycle from day 3, written in C:
1void run(VM *vm) {2 while (vm->running) {3 unsigned char op = vm->code[vm->pc++]; // FETCH (and advance pc)4 switch (op) { // DECODE5 case OP_PUSH: {6 int v = vm->code[vm->pc++]; // operand follows the opcode7 vm->stack[vm->sp++] = v; // EXECUTE: push8 break;9 }10 case OP_ADD: {11 int b = vm->stack[--vm->sp];12 int a = vm->stack[--vm->sp];13 vm->stack[vm->sp++] = a + b; // pop 2, push sum14 break;15 }16 // OP_SUB, OP_MUL similar...17 case OP_PRINT:18 printf("%d\n", vm->stack[vm->sp - 1]);19 break;20 case OP_HALT:21 vm->running = 0;22 break;23 }24 }25}vm->pc is your rip, vm->code is your instruction memory, vm->sp
and vm->stack are your rsp and stack, the switch is the CPU's instruction decoder, and the
while is the fetch-decode-execute cycle. You are re-implementing, in C, the exact machine you spent
a month learning — which is the surest possible proof that you understand it. The stack-pointer
push/pop logic is day 11; the opcode dispatch is a cousin of day 20's dispatch table.A program for your machine
Bytecode to compute (2 + 3) * 4 and print it — hand-assembled:
1unsigned char prog[] = {2 OP_PUSH, 2, // stack: [2]3 OP_PUSH, 3, // stack: [2, 3]4 OP_ADD, // stack: [5]5 OP_PUSH, 4, // stack: [5, 4]6 OP_MUL, // stack: [20]7 OP_PRINT, // prints 208 OP_HALT9};PUSH 10, PUSH 4, SUB, PUSH 2, MUL, PRINT, HALT. Note
that SUB pops b then a and pushes a−b. What does it print?Build the stack machine
Implement the full VM in vm.c: the OpCode enum, the VM struct, an init function, and the run() fetch-decode-execute loop handling PUSH, ADD, SUB, MUL, PRINT, and HALT. Write at least two hand-assembled bytecode programs (including (2+3)*4 and one of your own) and run them, verifying the output by tracing the stack yourself first. Run under valgrind for a clean report and -fsanitize=address to catch any stack over/underflow. This is YOUR architecture — you may name opcodes and extend as you like, but it must correctly execute the programs. Draw on days 3, 11, 17, 18, 20.
$ gcc -O0 -g -Wall -fsanitize=address vm.c -o vm && ./vm$ valgrind ./vm- › The fetch-decode-execute loop correctly runs (2+3)*4 and prints 20
- › Your own second program produces the output you traced by hand
- › SUB does a−b in the right order (you got the pop order right)
- › Clean valgrind / ASan report — no stack overflow or invalid access