Bedrock logo

Rust · CPU, assembler, kernel

Watch two tasks
share one CPU.

Bedrock is a from-scratch virtual CPU, an assembler for its instruction set, and a small operating system kernel written in that assembly. This page runs the exact kernel the Rust CLI runs: it assembles the source, boots on the emulated machine, installs an interrupt vector table, and time-slices between two user tasks with a timer interrupt and a round-robin scheduler. Step it one instruction at a time and watch the registers, flags, mode, memory, and console change.

Boot the machine View on GitHub
How to use this playground

This page reimplements Bedrock's instruction set and runs the exact same kernel source the Rust CLI runs. Everything below is computed live in JavaScript: the same ISA, the same two-task round-robin scheduler, the same output.

The controls

What you are looking at

A guided tour (about a minute)

  1. Press Step a few times. Boot runs in kernel mode: it sets the stack, installs the vector table with lidt, then hand-builds a saved stack frame for each task.
  2. Keep stepping to the final iret of boot, the mode badge flips to user and you land in task0_entry. Task 0 prints its first A with trap SYS_PRINT.
  3. Watch the timer bar drain. When it hits zero the timer fires: the vector table's row 0 glows, mode flips to kernel, and timer_handler saves the task's registers and switches to task 1. Now you see B appear.
  4. Press Run and watch the timeline fill with alternating blue and green, the two tasks time-slicing. When each task has printed its letter six times it calls trap SYS_EXIT; when both are gone the scheduler executes halt.
  5. Or just press Run to completion: the console should read ABBABABABABA, matching the Rust engine byte-for-byte.

The machine assembling…

ready cycle 0 KERNEL

Kernel source: kernel/kernel.asm

Memory (hex)

Registers

pc
0x0000
sp
0xfffc

Flags & mode

Timer countdown

Current instruction @ pc

0x0000

Interrupt vector table

Console output

 

Task timeline: who holds the CPU each step

Task 0 (prints A) Task 1 (prints B) Kernel / handlers

Event log: interrupts, traps, faults, context switches

Deterministic, down to the last cycle

This page reimplements the same instruction set, assembler, and two-task round-robin scheduler the Rust engine ships, and runs the exact kernel/kernel.asm source. On load it self-checks its output against the Rust engine's ground truth, the value printed next to "The machine" above.

# cargo run --release -- kernel
ABBABABABABA
verified vs Rust engine: 2082 cycles, 24 timer irqs, 14 traps, 24 context switches

# the same run every time: same config, identical trace and final state.
# task 0 prints A, task 1 prints B, preempted by the timer between them.

Because Bedrock owns the whole machine, its registers, flags, memory, and instruction set, every claim about scheduling, interrupts, privilege levels, and determinism is asserted directly against observed state. A load or store out of the 64 KiB, a bad opcode, a privileged instruction in user mode, and a divide by zero all resolve to a clean guest fault instead of touching the host.

The primitives that matter

Six mechanisms, and you have a kernel.

Every one is a real part of the machine you can single-step above, small enough to read in an afternoon.

isa Fixed 8-byte instructions

One opcode byte, three register fields, and a 32-bit little-endian immediate. Decoding is trivially bounded, so a malformed stream can never run off the end.

interrupts Interrupt vector table

lidt installs the timer, syscall, and fault vectors. The timer preempts the running task, and iret restores the interrupted registers, flags, stack pointer, and program counter exactly.

privilege User and kernel mode

A mode bit in the flags register. Privileged instructions (lidt, sti, cli, iret, halt) run in kernel mode and fault if a user task attempts them.

syscall The trap instruction

A user task enters kernel mode through the syscall vector to print, yield, or exit. r6 carries the trap number and r1 the argument.

scheduler Round-robin, in assembly

A suspended task's whole context lives on its own stack as [flags][pc][r0..r7]. A context switch is just save the stack pointer, pick the next runnable task, load its stack pointer, iret.

safety Clean faults, then double fault

Out-of-bounds memory, a bad opcode, a divide by zero, and a wild stack pointer all raise a clean guest fault. If the fault itself cannot be delivered, the machine raises a double fault and halts instead of recursing.

The readable version of the whole stack

Bedrock owns the machine end to end, so nothing is hand-waved and nothing is out of reach of a test.

Teaching VMs and write-your-own-CPU projects

Usually stop at the CPU and an assembler. Bedrock carries the same idea up through interrupts, privilege levels, and a preemptive scheduler written in the assembly itself.

The real ISAs it borrows from

Fixed-width encoding, a flags register, memory-mapped IO, and an interrupt vector table are taken from real hardware and shrunk to the smallest form that still teaches the mechanism.

QEMU or real hardware

Boot a real kernel and no unit test can assert what happened inside. Because Bedrock owns the emulated machine, every claim about scheduling, privilege, and determinism is checked directly against observed state.

Bedrock

A virtual CPU, an assembler, a disassembler, and a kernel in its own assembly. Small enough to single-step in a browser, strict enough to fuzz in CI, deterministic to the last of its 2082 cycles.

Use it

One binary with four subcommands, a library crate, and the correctness gate.

kernel

Boot the bundled kernel: two tasks time-sliced by the timer. Add --trace for the mode, task, and event on every instruction.

run

Assemble and run your own .asm program, with an optional --trace and a --timer period.

asm / disasm

Assemble a source file to hex, or disassemble it back. assemble(disassemble(assemble(x))) reproduces the exact bytes.

library and tests

The CPU, assembler, and disassembler are a Rust library crate. cargo test runs instruction semantics, interrupts, kernel behavior, and adversarial fuzzing.

# build the emulator, assembler, and bundled kernel
cargo build --release

# boot the bundled kernel: two tasks time-sliced by the timer
cargo run --release -- kernel

# trace it step by step (mode, task, and event per instruction)
cargo run --release -- kernel --trace

# assemble and run your own program, with a trace and a timer
cargo run --release -- run path/to/program.asm --trace --timer 50

# assemble to hex, or disassemble
cargo run --release -- asm    path/to/program.asm
cargo run --release -- disasm path/to/program.asm

# the correctness gate: instruction semantics, interrupts, kernel, fuzzing
cargo test