genrt is an experimental hard real-time operating system project written primarily in Rust.
Current active target:
- AArch64
- Rust target
aarch64-unknown-none-softfloat - QEMU
virt - single-core EL1 kernel threads
- high-half kernel with AArch64 stage-1 MMU enabled
- QEMU-first bring-up and debugging
The current AArch64 path already has:
- low-linked
.boot.*trampoline that runs before MMU enable - high-linked kernel sections loaded at low physical addresses
- AArch64 stage-1 MMU bring-up with temporary TTBR0 identity mappings
- TTBR1 high direct map using
KERNEL_HVA_OFFSET = 0xffff_0000_0000_0000 - post-memory-init switch from boot-owned page tables to allocator-owned runtime TTBR1 tables
- low identity mapping removal after the high kernel is established
VBAR_EL1exception-vector setup at high VA- PL011 UART output through high virtual MMIO aliases
BootInfohandoff into Rust- DTB-seeded physical memory discovery
- QEMU
virt,gic-version=2DTB loaded byxtaskat0x4000_0000 - low
.boot.textDTB parser for RAM, PL011, and GICv2 initial mappings - QEMU
virtemergency platform fallback for early diagnostic reachability - internal physical memory map with reserved-range carving
- page-aligned usable frame ranges
- minimal free-list physical frame allocator
- architecture-agnostic generic frame allocator that continues to return physical frames
- fixed-size bootstrap kernel heap on
linked_list_allocator - heap initialized through a high virtual pointer over a physical frame range
- single-core IRQ-safe heap lock for task-context allocation/free
- working
alloccontainer smoke tests (Vec,VecDeque,BinaryHeap,BTreeMap) - GICv2 initialization through high virtual MMIO aliases
- architected timer in one-shot nearest-deadline mode
- monotonic hardware counter timebase
- full trap-frame save/restore on IRQ
- IRQ-return-based preemptive task switching
- heap-backed task table with stable boxed stacks and saved frames
- preallocated heap-backed ready queue for runnable tasks
- round-robin scheduling for runnable kernel tasks
- scheduler ownership isolated to bootstrap, timed-event dispatch, and frame handoff
kernel::timeowns a preallocated heap-backed deadline queue and one-shot timer rearming- sleep wakeups and scheduler quantum both delivered as typed timed events
- round-robin quantum configured as a duration at scheduler bootstrap
- bounded mailbox IPC for kernel tasks with heap-preallocated buffers and wait queues
- demo producer/consumer tasks exchanging messages through a capacity-bounded mailbox
- timeout-aware mailbox send/receive operations
- bounded
thread_spawn/thread_exit/thread_join - minimal allocation-free formatted logging with log levels
- improved fatal exception diagnostics
xtaskpost-link.boot.textautonomy check usingreadelfandllvm-objdump
In one sentence:
genrt is currently an early single-core high-half preemptive EL1 kernel prototype on AArch64/QEMU.
The AArch64 build currently uses the Rust target aarch64-unknown-none-softfloat.
This is intentional for the current kernel stage: the scheduler/trap path does not
yet own FP/SIMD state, so the build avoids implicit hard-float/AdvSIMD assumptions
in ordinary Rust code.
- EL0 / user mode
- SMP scheduling
- mailbox registry / dynamic mailbox creation
- driver model
- low-overhead buffered tracing
- demand paging / page faults / per-process address spaces
- ASIDs and TTBR0 userspace address spaces
High-level flow:
_start (.boot.text, low physical/identity)
-> park secondary CPUs
-> set low boot stack
-> boot_build_page_tables()
-> parse QEMU-loaded DTB from platform boot slot
-> build TTBR0 identity mappings
-> build TTBR1 high direct-map/MMIO mappings
-> program MAIR_EL1 / TCR_EL1 / TTBR0_EL1 / TTBR1_EL1
-> enable SCTLR_EL1.M/C/I
-> switch SP to high boot-stack alias
-> branch to high rust_entry
rust_entry (high VA)
-> zero high .bss
-> set high VBAR_EL1
-> initialize UART/GIC high MMIO aliases
-> BootInfo + DTB memory discovery through HVA
-> kernel_main()
-> physical memory init
-> switch to allocator-owned runtime kernel page tables
-> clear TTBR0 temporary identity mappings
-> start first task from prepared trap frame
Timer IRQ
-> save full TrapFrame
-> identify timer interrupt
-> kernel::time::on_timer_interrupt(frame)
-> read monotonic counter
-> collect all expired timed events
-> dispatch WakeTask / QuantumExpired
-> scheduler may select next task
-> compute nearest next deadline
-> reprogram one-shot timer
-> active frame may be replaced
-> restore selected TrapFrame
-> eret into selected task
Key milestone already reached:
task switching is performed by replacing the IRQ return frame, not by a normal function-call-style switch
- single-core only
- EL1 kernel threads only
- no EL0/user address spaces yet
- no ASIDs or per-process TTBR0 yet
- VM API currently supports only 2 MiB-aligned TTBR1 kernel mappings
- heap is currently a fixed-size
16 MiBbootstrap region - direct-to-UART logging
- scheduler/time dynamic containers are preallocated at bootstrap and must not grow in IRQ paths
- heap does not grow from arbitrary frames yet
- no SMP TLB shootdown
- platform-specific boot protocol and MMIO discovery live in the AArch64 platform layer
genrt/
├── arch/aarch64/ # AArch64 boot, MMU, traps, timer, GIC, platform discovery
├── kernel/ # architecture-neutral kernel logic
├── crates/bootinfo/ # early boot handoff structures
├── tools/xtask/ # build/run/debug workflow
├── docs/
└── ai-docs/
Available macros:
kprint!,kprintln!error!,warn!,info!,debug!,trace!
Available levels:
ErrorWarnInfoDebugTrace
The logger is allocation-free and intended for kernel bring-up. It is useful for diagnostics, but high-volume UART logging still perturbs timing.
The current AArch64 strategy is:
low-linked trampoline + high-linked kernel loaded low
.boot.* sections have low VMA/LMA and execute before the MMU is enabled. The
main kernel sections are linked at high virtual addresses but loaded at low
physical addresses via linker AT(...); no segment copy is performed.
Address convention:
KERNEL_HVA_OFFSET = 0xffff_0000_0000_0000
HVA = PA + KERNEL_HVA_OFFSET
PA = HVA - KERNEL_HVA_OFFSET
The bootstrap page tables are intentionally small:
- TTBR0 temporarily maps the low identity window needed by the trampoline and DTB access.
- TTBR1 maps the high direct-map RAM window and high Device mappings for UART/GIC.
- After
kernel::memory::init(), the kernel switches to allocator-owned TTBR1 tables and clears TTBR0.
xtask controls the QEMU bare-metal protocol. It generates a compact QEMU
virt,gic-version=2 DTB and loads it at 0x4000_0000 with a loader device. The
kernel image stays at 0x4008_0000. The low .boot.text parser reads that DTB
before UART/GIC are initialized and extracts only the ranges needed for initial
MMU mappings. If that early parse fails, the AArch64 QEMU platform layer has an
emergency fallback for RAM/UART/GIC so early diagnostics can still reach UART.
The generic frame allocator remains MMU-agnostic: it manages physical frames and
returns PhysAddr. PA-to-HVA conversion happens only at explicit dereference
boundaries such as DTB reads, free-list metadata inside frames, heap init,
page-table writes, and MMIO access.
The build command runs a post-link .boot.text autonomy check. It verifies that
the pre-MMU boot code has no relocations, no runtime helper thunks such as
memcpy/memset/panic/formatting, no high-VA instruction operands, and no
direct branch/call out of .boot.*.
The kernel heap is currently initialized from one contiguous 16 MiB region
allocated out of the physical frame allocator during early memory bootstrap.
Initialization order is:
- parse and normalize physical memory regions
- initialize the frame allocator on usable page ranges
- allocate one contiguous heap range via
alloc_contiguous - convert the physical heap range to HVA at the heap boundary
- initialize
linked_list_allocator - run heap-backed smoke tests
This keeps heap ownership unambiguous: once the bootstrap heap region is allocated, it is no longer part of the frame allocator free list.
Allocation policy for the current kernel stage:
- heap allocation/free is allowed during bootstrap and in ordinary task context
- heap allocation/free is protected against local IRQ reentrancy on the current single core
- heap allocation/free remains forbidden in timer IRQ, scheduler handoff, time fast-path dispatch, exception fast paths, and high-frequency tracing
- dynamic containers used by those IRQ-critical paths must be preallocated or otherwise bounded before entering the fast path
The scheduler and time subsystem now follow that rule explicitly:
- the task table, saved frames, task stacks, ready queue, and deadline queue are heap-backed
- all of those containers are allocated and reserved during bootstrap
- timer IRQ and scheduler handoff only perform bounded operations on already allocated storage
The first VM API is deliberately narrow and kernel-only. It supports TTBR1 kernel mappings after runtime page tables are active:
phys_to_virtvirt_to_phys_directtranslate_kernel_vamap_kernel_regionunmap_kernel_regionprotect_kernel_regiondrop_boot_identity_mappingswitch_to_runtime_kernel_tables
Mutation APIs return VmError::NotInitialized until
switch_to_runtime_kernel_tables() has replaced boot-owned tables from
.boot.bss with frame-allocator-owned page tables. This prevents mappings from
being added to tables that will be discarded and prevents reclaiming boot table
storage through the generic frame allocator.
The first IPC primitive is a bounded mailbox for EL1 kernel tasks.
Current mailbox scope:
- client-defined message type (
Mailbox<T>) - heap-preallocated fixed-capacity ring buffer
- non-blocking
try_send/try_recv - blocking
send/recv - timeout-aware
send_until_counter/recv_until_counter - explicit duration wrappers in ticks, microseconds, and milliseconds
- preallocated bounded send and recv wait queues
- one bootstrap-created demo mailbox owned by the demo task module
Mailbox state is protected by the shared IRQ-save lock abstraction. In the current no-SMP build that means local IRQ masking plus contention checks; the same abstraction is the intended upgrade point for a future SMP spinlock. Blocking waits enter the scheduler through a typed synchronous task-call path, which lets the IPC layer recheck the wait condition and join waiter insertion with scheduler blocking. This avoids heap allocation and lost wakeups in the preemption-critical path.
IPC timeouts are represented as typed time events rather than callbacks. The scheduler stores an opaque IPC wait token and timeout event; normal IPC wakeup cancels the event, while timeout dispatch asks IPC to remove the task from the owning wait queue before waking it with a timeout result.
Kernel tasks now have a bounded thread lifecycle API:
kernel::sched::thread_spawn(entry, ThreadArg, attrs)kernel::sched::thread_exit(code)kernel::sched::thread_join(id)
Thread handles are ThreadId { index, generation } values. The index names a
preallocated scheduler slot; the generation changes before a freed slot is
reused, so stale handles fail validation instead of naming a later thread.
Thread slots, stacks, saved frames, and ready queue capacity are prepared during
scheduler bootstrap. Runtime spawn does not grow scheduler containers; it
initializes a free slot, prepares its trap frame, and queues it. Returning from a
spawned thread entry goes through the same controlled SVC path as explicit
thread_exit, which records the exit code, wakes a single joiner if present,
and never resumes the exited thread. Successful join reclaims the slot for reuse.
Bootstrap/static tasks use the same fn(ThreadArg) -> usize entry shape as
runtime-spawned threads; ThreadArg can carry a small integer or an explicit
raw pointer when a caller needs richer Rust-owned context.
The current stack class is fixed at 8 KiB per thread slot. Detached threads are
supported by ThreadAttrs::detached() and are reclaimed on exit; the demo uses
joinable workers to exercise spawn -> exit -> join.
just doctor
just build-aarch64
just run-aarch64
just debug-aarch64
just gdb-aarch64With explicit log level:
just run-aarch64 debug
just run-aarch64 traceOr via xtask:
cargo xtask run-aarch64 --log-level debug
cargo xtask run-aarch64 --log-level traceThe best next steps are:
- refine VM permissions and page-table ownership invariants
- growable heap design on top of frame allocation
- minimal page-fault diagnostics
- userspace/process lifecycle groundwork on top of the MMU
docs/month1-plan.md— month 1 closure and actual outcomedocs/month2-plan.md— roadmap for the next monthai-docs/decision-records/ADR-0001-architecture-strategy.mdai-docs/decision-records/ADR-0002-aarch64-irq-path-gicv2-timer.mdai-docs/decision-records/ADR-0003-aarch64-preemptive-irq-return-switching.mdai-docs/decision-records/ADR-0004-aarch64-boot-exception-separation-and-fatal-path.mdai-docs/decision-records/ADR-0005-one-shot-timer-deadline-engine.mdai-docs/decision-records/ADR-0006-time-owned-timed-events.mdai-docs/decision-records/ADR-0007-dtb-memory-map-and-frame-allocator.mdai-docs/decision-records/ADR-0008-aarch64-softfloat-kernel-target.mdai-docs/decision-records/ADR-0009-bootstrap-kernel-heap-on-frame-allocator.mdai-docs/decision-records/ADR-0010-irq-safe-kernel-heap-lock-and-allocation-policy.mdai-docs/decision-records/ADR-0011-dynamic-preallocated-scheduler-and-time-structures.mdai-docs/decision-records/ADR-0012-bounded-mailbox-ipc.mdai-docs/decision-records/ADR-0013-mailbox-timeout-semantics.mdai-docs/decision-records/ADR-0014-bounded-kernel-thread-lifecycle.mdai-docs/decision-records/ADR-0015-aarch64-high-half-mmu-bring-up.md