A lightweight, portable stack-based virtual machine implementation in C.
This project implements a simple stack-based virtual machine (VM) that executes bytecode instructions. The VM is designed with a clear separation between components, making it easy to understand, modify, and extend. It includes support for basic arithmetic operations, control flow, function calls, and memory access.
- Stack-based architecture: All operations work with a Last-In-First-Out (LIFO) stack
- Rich instruction set:
- Basic stack operations (PUSH, POP, DUP, SWAP)
- Arithmetic operations (ADD, SUB, MUL, DIV)
- Control flow (JMP, JZ, JNZ)
- Function calls (CALL, RET)
- Memory access (LOAD, STORE)
- System operations (PRINT, STOP)
- 32-bit integer operations
- Function call support with a dedicated call stack
- Memory segment for variable storage
Defines the available instructions for the VM:
typedef enum {
OP_NOP, // No operation
OP_PUSH, // Push value onto stack
OP_POP, // Pop value from stack
OP_DUP, // Duplicate top value
OP_SWAP, // Swap top two values
// ... other operations
} OpCode;
Manages the operand stack:
typedef struct {
int32_t data[STACK_MAX_SIZE];
int32_t top;
} Stack;
Manages function calls and returns:
typedef struct {
CallFrame frames[CALL_STACK_MAX];
int32_t count;
} CallStack;
Stores bytecode to be executed:
typedef struct {
uint8_t* code; // Bytecode array
size_t length; // Length of bytecode
size_t capacity; // Allocated capacity
} CodeSegment;
Main virtual machine structure:
typedef struct {
CodeSegment* code_segment;
Stack stack;
CallStack call_stack;
int32_t memory[MEMORY_SIZE];
bool running;
} VM;
Instructions consist of an opcode byte followed by optional operands:
OP_PUSH
: Followed by a 4-byte (32-bit) integer valueOP_JMP
,OP_JZ
,OP_JNZ
,OP_CALL
: Followed by a 4-byte target address- Most other operations take their operands from the stack
#include <stdio.h>
int main() {
// Initialise code segment
CodeSegment code;
code_segment_init(&code, 256);
// Create a program that calculates (42 + 8)
code_segment_write(&code, OP_PUSH);
code_segment_write_int32(&code, 42);
code_segment_write(&code, OP_PUSH);
code_segment_write_int32(&code, 8);
code_segment_write(&code, OP_ADD);
code_segment_write(&code, OP_PRINT);
code_segment_write(&code, OP_STOP);
// Initialise and run the VM
VM vm;
vm_init(&vm, &code);
vm_run(&vm);
// Clean up
code_segment_free(&code);
return 0;
}
OP_PUSH
: Pushes a value onto the stackOP_POP
: Removes and discards the top valueOP_DUP
: Duplicates the top valueOP_SWAP
: Swaps the top two values
OP_ADD
: Pops two values, adds them, and pushes the resultOP_SUB
: Pops two values, subtracts the second from the first, and pushes the resultOP_MUL
: Pops two values, multiplies them, and pushes the resultOP_DIV
: Pops two values, divides the first by the second, and pushes the result
OP_JMP
: Unconditional jump to a target addressOP_JZ
: Jump to a target address if the top value is zeroOP_JNZ
: Jump to a target address if the top value is not zero
OP_CALL
: Call a function at a target addressOP_RET
: Return from a function call
OP_LOAD
: Load a value from memory at the address specified by the top valueOP_STORE
: Store the second value on the stack to memory at the address specified by the top value
OP_PRINT
: Print the top value of the stackOP_STOP
: Stop the VM execution
The VM includes simple error handling for:
- Stack underflow and overflow
- Division by zero
- Invalid memory access
- Invalid jump targets
- Unknown opcodes
make run
To add new instructions:
- Add a new opcode to the
OpCode
enum - Implement the operation in the
vm_run
function's switch statement - Use the new opcode in the bytecode
This project is available under the MIT License.