A small 32-bit VM with SMP support, MMIO devices, interrupts, and a custom ISA.
This project is temporarily on hold due to personal reasons.
- VM boots and runs custom binaries (
--bin). - SMP core bring-up works (
STARTAP,CPUID,IPI). - PCIe root complex (ECAM, BAR sizing/relocation, capability list, MSI onto INTC) is
implemented; see
docs/pci.md. The guest enumerates bus 0 and drives the Ethernet, XRGB8888 display, and 48 kHz PCM endpoints through assigned BARs, IOMMU DMA where applicable, and MSI. - Built-in selftests pass in current tree:
startap_cpuidipimmu_percpu_rootiommu_paged_translationpcie_enumeration- ISA/device conformance checks for relative control flow, zero-flag branches,
CALLR, atomics, divide-by-zero interrupts, 16-bit sign extension, indexed read/write widths, extended relative conditions,INTI, PS/2, and framebuffer acceleration, PCI GPU damage/page-flip, PCI audio DMA completion, runtime statistics ABI, and virtual CPU pacing
- Atomic ISA instructions are implemented with C11 atomic semantics on aligned 32-bit RAM words.
Assembler/toolchain project:
- https://github.com/glowingstone124/lampvm-toolchain
- https://github.com/glowingstone124/llvm-project (Custom LLVM backend)
src/: host-side VM implementation, device model, debugger, loader, and selftestskernel/: guest kernel sources and build scriptbios/: guest BIOS/bootloader sourcesuser/: guest userspace runtime and sample appsdocs/: architecture and ABI notes
Networking bring-up and BusyBox wget notes are in docs/networking.md.
Firmware/PCI graphics takeover is described in docs/graphics.md; the shared
DMA-ring and SDL3 PCI audio path is described in docs/audio.md.
The classic, translated-fetch, decoded direct-threaded, and experimental
ARM64 JIT engines are documented in
docs/execution-engine.md.
- CMake >= 3.20
- C11 compiler
- SDL3
- pthreads (via
Threads::Threads)
cmake -S . -B build
cmake --build build -jThis produces the native CLI as build/lampvm. The full graphical build is a
separate, optional application:
cd tools/lampvm-debugger
gradle -g .gradle-user :app:createDistributableOn Apple Silicon this produces
app/build/compose/binaries/main/app/lampvm-debugger.app. It bundles the native
VM core and Kotlin/Native bridge and uses the Compose debugger as the default
LampVM graphical startup experience.
The raw image is intentionally 512 MiB so the guest has room to grow, but most of it is empty and compresses very well. Package it without modifying the source image:
bash tools/package_disk_image.sh --input disk.img --output dist/disk.img.gzFor a smaller package when zstd is available:
bash tools/package_disk_image.sh --input disk.img --output dist/disk.img.zst --format zstdBefore running the VM, restore the raw filename expected by the launcher:
gzip -dc dist/disk.img.gz > disk.img
# or: zstd -d -c dist/disk.img.zst > disk.imgNotes:
- On Apple Silicon, CMake is configured to build
arm64. - Linux links
libmand enables_POSIX_C_SOURCE=200809Ltogether with_DEFAULT_SOURCE.
The static mode requires a Linux toolchain, libSDL3.a, and static archives
for SDL3's enabled platform dependencies. If SDL3 provides its official CMake
package, Lamp VM uses SDL3::SDL3-static; otherwise it uses the dependency
closure reported by pkg-config --static sdl3.
cmake -S . -B build-linux-static \
-DCMAKE_BUILD_TYPE=Release \
-DVM_STATIC=ON
cmake --build build-linux-static -jFor an SDL3 installation in a custom prefix, also pass
-DCMAKE_PREFIX_PATH=/path/to/sdl3 or set PKG_CONFIG_PATH. Verify the
result with:
file build-linux-static/lampvm
ldd build-linux-static/lampvm
# Expected from ldd: not a dynamic executableWhen building SDL3 for a fully static glibc executable, disable its D-Bus and IBus integrations:
cmake -S /path/to/SDL -B build-sdl-static \
-DSDL_SHARED=OFF \
-DSDL_STATIC=ON \
-DSDL_DBUS=OFF \
-DSDL_IBUS=OFFSDL's Linux thread-priority fallback normally reaches RTKit through D-Bus.
That requires dlopen() and is unsafe when a static glibc executable loads a
host D-Bus library built against a different glibc. VM_STATIC therefore uses
Linux setpriority() directly, and the distributable SDL archive must keep
D-Bus disabled so SDL-owned audio threads cannot re-enter that fallback.
GNU ld may still warn about dlopen when linking an official static SDL3
archive. SDL3 keeps its dynapi code enabled in static builds by
design;
the warning does not by itself add ELF DT_NEEDED dependencies. Runtime-loaded
X11, Wayland, and audio backends can still require compatible host shared
libraries when their SDL _SHARED options are enabled. Use the file and
ldd checks above to verify the ELF, and disable those backends or link their
complete static dependency closure for a hermetic binary.
VM_STATIC is intentionally Linux-only. macOS always dynamically links the
system libc and Frameworks even when SDL3 itself is built as a static archive.
The executable supports subcommands for the common workflows:
./build/lampvm run bios/boot.bin --cores 1
./build/lampvm test
./build/lampvm helpArguments:
run [program.bin]: boot a guest program image (default:boot.bin)test: run built-in SMP tests and exithelp: show usage and examples--cores <n>/--smp <n>: CPU worker thread count in[1, 64](default:1)--cpu-mhz <n>: per-vCPU execution cap in[1, 10000]MHz (default:100)--engine <name>: selectclassic,cached,threaded, or experimentaljit(ARM64 Tier 0; unsupported targets/instructions fall back tocached)- serial input/output uses the invoking host terminal; the Kotlin debugger provides the graphical serial terminal and framebuffer window
--net <mode>: ethernet backend:null,nat, orudp:<bind-port>:<peer-port>
Legacy flag-style invocation is still accepted:
./build/lampvm --bin bios/boot.bin --smp 1
./build/lampvm --selftestThe full lampvm-debugger application owns the graphical frontends. Its serial
console is a JediTerm xterm/VT100 terminal with editing, scrollback, clipboard,
and ANSI color support. Its VGA window reads the
framebuffer through the debugger ABI and injects PS/2 keyboard and relative
mouse packets through explicit input APIs. Click VGA to capture the pointer;
use Control+Command+G on macOS or Ctrl+Alt+G elsewhere to release it.
Normal graphical runs do not print host lifecycle logs. Set LAMP_VM_LOG=1 to
show startup, device-registration, and VNC messages; errors remain visible.
The old SDL serial and display frontends have been removed. SDL remains a VM runtime dependency for timing, thread priority, and audio support.
Run selftests:
./build/lampvm test- The configured MHz is a virtual oscillator frequency, not a benchmark of the host interpreter.
- Clock model v1 charges one virtual cycle for every retired guest instruction. Each vCPU is paced against a monotonic host deadline with a roughly 250 us budget interval; a host scheduling stall does not create a long catch-up burst.
- The invariant virtual cycle counter continues with monotonic time. If the host cannot sustain the configured rate, retired instructions and measured MIPS lag behind that counter, representing vCPU stalls.
- This is deterministic instruction-cost accounting, not a cycle-accurate model of a physical pipeline, cache, or branch predictor.
SYSINFO layout v3 exposes the nominal frequency, invariant cycles, retired
instruction count, aggregate execution rate, uptime, and host RSS. The kernel
publishes these through /proc/cpuinfo and /proc/lampvm; scheduler and memory
summaries are available in /proc/stat, /proc/uptime, /proc/loadavg, and
/proc/meminfo.
CPU0is BSP and starts immediately.CPU1..N-1are APs and stay parked untilSTARTAP.- Each core has private architectural state:
regs/ip/flags- call/data/ISR stack pointers and interrupt context
- Cores share RAM/MMIO/IO/device model.
- Normal
LOAD/STORE/LOAD32/STORE32/...go through the VM memory API. - Shared VM state is serialized by a global VM lock.
Atomic instructions operate on aligned 32-bit RAM words using C11 atomics:
CASXADDXCHGLDAR(acquire load)STLR(release store)FENCE(SC fence)
Rules:
- Unaligned atomic addresses panic.
- Atomic ops on MMIO addresses panic.
atomic_thread_fenceis only used with atomic operations; normal memory is not implicitly upgraded to atomic by fences.
- Stacks are stored in VM RAM (not host-side arrays).
--smp 1: legacy fixed bases are used.--smp > 1: per-core stack regions are allocated near top of RAM.- VM checks image/stack pool overlap at startup.
Single binary format:
- 24-byte header (
u32little-endian x6) - Text section (
u64little-endian instructions) - Data section (raw bytes)
Header fields:
TEXT_BASETEXT_SIZE(bytes)DATA_BASEDATA_SIZE(bytes)BSS_BASEBSS_SIZE(bytes)
| Region | Start | End | Size | Purpose |
|---|---|---|---|---|
| IVT | 0x000000 |
0x0007FF |
2048 B | 256 vectors x 8B |
| CALL_STACK | 0x000800 |
0x000FFF |
2048 B | Call stack |
| DATA_STACK | 0x001000 |
0x0017FF |
2048 B | Data stack |
| ISR_STACK | 0x001800 |
0x001FFF |
2048 B | Interrupt stack |
| TIME MMIO | 0x002000 |
0x00201B |
28 B | time registers |
| PROGRAM | 0x00201C |
FB_BASE-1 |
variable | text/data/bss |
| FrameBuffer | FB_BASE |
FB_BASE+FB_SIZE-1 |
variable | video buffer |
Additional fixed MMIO regions:
| Region | Start | End | Size | Purpose |
|---|---|---|---|---|
| Legacy FrameBuffer Alias | 0x00620000 |
0x0074BFFF |
1228800 B | video buffer legacy mapping |
| SYSINFO MMIO | 0x0074C000 |
0x0074C08F |
144 B | firmware and runtime VM metadata |
| INTC MMIO | 0x0074D000 |
0x0074D503 |
1284 B | interrupt controller |
| IOMMU MMIO | 0x0074E000 |
0x0074E0FF |
256 B | IOMMU capability/control |
| MMU MMIO | 0x0074F000 |
0x0074F0FF |
256 B | paging control/fault registers |
| Ether MMIO | 0x00750000 |
0x0075001B |
28 B | NIC registers |
| PCIe ECAM | 0x00900000 |
0x009FFFFF |
1 MiB | PCI Express configuration space, see docs/pci.md |
Note: the native C stack used by BIOS and early kernel execution is rooted at
0x00800000 and grows downward; it is not a fixed-size MMIO region so it does
not appear in this table, but any new fixed physical window must still avoid
it. See the "Why 0x00900000?" section in docs/pci.md before adding new
fixed addresses here.
Enable debug features:
cmake -S . -B build -DVM_DEBUG=ON
cmake --build build -jWhen enabled:
- instruction statistics
- alignment checks
- interactive step/breakpoint debugger
Runtime debug env vars:
VM_DEBUG_STEP=1orVM_STEP=1VM_DEBUG_PAUSE=1VM_BREAKPOINTS=0x201C,0x2024
See:
docs/isa.mddocs/bios.mddocs/kernel.mddocs/posix.mddocs/user-abi.mddocs/bios-build.md
It's time to develop a kernel for this platform.
Recommended start order:
- Bring up single-core kernel first (
--smp 1): boot, trap/interrupt entry, timer tick, syscall ABI. - Add scheduler and memory management in single-core mode.
- Enable SMP bring-up (
STARTAP), then add spinlocks and per-core data. - Use atomic ISA ops (
LDAR/STLR/CAS/XADD) for lock primitives.
Practical note:
- If you want fastest iteration, keep kernel bring-up on
--smp 1until trap path and basic drivers are stable, then turn on SMP.