Minimal multi-arch bare-metal kernel (x86 / RISC-V / ARM) — boots on QEMU and real Allwinner D1 & Zynq-7020 silicon
Find a file
auser 8c44d12357 minos M1: minimal multi-arch kernel (x86 + RISC-V + ARM/Zynq)
Boots under QEMU on all three ISAs from one clang build:
- shared C core (kmain, console) calls a small per-arch contract (arch.h)
- per-arch asm entry + trap/vector table + UART driver
- parallel Makefile (make -j), 'make run-<arch>', 'make sizes'
M1 = boot, UART up, trap vector installed, banner from kmain().
2026-09-25 00:41:24 +03:00
arch minos M1: minimal multi-arch kernel (x86 + RISC-V + ARM/Zynq) 2026-09-25 00:41:24 +03:00
common minos M1: minimal multi-arch kernel (x86 + RISC-V + ARM/Zynq) 2026-09-25 00:41:24 +03:00
include minos M1: minimal multi-arch kernel (x86 + RISC-V + ARM/Zynq) 2026-09-25 00:41:24 +03:00
.gitignore minos M1: minimal multi-arch kernel (x86 + RISC-V + ARM/Zynq) 2026-09-25 00:41:24 +03:00
Makefile minos M1: minimal multi-arch kernel (x86 + RISC-V + ARM/Zynq) 2026-09-25 00:41:24 +03:00
README.md minos M1: minimal multi-arch kernel (x86 + RISC-V + ARM/Zynq) 2026-09-25 00:41:24 +03:00

minos — a minimal multi-arch kernel

One tiny OS core, three ISAs: x86 (i386), RISC-V (rv64), ARM (Cortex-A9 / Zynq-7000). Built by a single clang in parallel so you can compare how the same C behaves across architectures.

Milestone M1 (this state): each target boots, brings up its UART, sets its interrupt/trap vector, and prints a banner from shared C (kmain).

Design

The only boundary is a small per-arch contract (include/arch.h):

arch_early_init()      bring up the UART
arch_uart_putc(c)      emit one byte
arch_set_trap_vector() install IDT / mtvec / VBAR
arch_name()            banner string
arch_halt()            hlt / wfi

Everything in common/ is portable C and calls only that contract. Each arch/<name>/ supplies the irreducible assembly (boot.S, trap/vector table) plus a C file implementing the contract and a linker script.

common/    main.c (kmain), console.c (putc/puts/puthex)
include/   arch.h (the contract), console.h
arch/x86/    boot.S (multiboot)   arch.c (COM1 0x3F8, IDT)   link.ld (1 MiB)
arch/riscv/  boot.S (_start)      trap.S (mtvec)  arch.c (UART 0x10000000)
arch/arm/    boot.S (_start)      vectors.S (VBAR) arch.c (Cadence UART 0xE0000000)

Build & run

make -j            # build all three in parallel
make run-x86       # boot under QEMU (Ctrl-A X to quit)
make run-riscv
make run-arm
make sizes         # compare stripped binary size
make clean

The interesting comparison (M1)

Aspect x86 (i386) RISC-V (rv64) ARM (Cortex-A9)
Entry protocol Multiboot1 header _start @ 0x80000000 _start @ DDR
Console port I/O outb 0x3F8 MMIO 16550 0x10000000 MMIO Cadence 0xE0000000
Trap vector IDT via lidt mtvec CSR VBAR (CP15)
Multi-core gate (single by default) mhartid check MPIDR check
Halt hlt wfi wfi
Idle byte size ~890 ~816 ~800

Real hardware (next)

  • RISC-V: the only change is the UART base — 0x10000000 (QEMU virt) → 0x02500000 (Allwinner D1 / Lichee RV). Loadable via xfel.
  • ARM/Zynq: same Cadence UART base as the real PS; boot via U-Boot/JTAG.
  • x86: boot the multiboot ELF from GRUB.

Roadmap

  • M2: real trap handlers + a timer tick per arch.
  • M3: expose a tiny syscall-style interface (the "own interface").