Skip to content

Latest commit

 

History

History

kernel

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

README.md

kernel demo

A small freestanding "kernel" -- a UEFI application that badc compiles for x86_64 and AArch64 and that boots under QEMU through UEFI firmware (OVMF on x86_64, ArmVirtQemu/AAVMF on AArch64). It runs before any OS, talks only to the firmware console (which the firmware mirrors to the serial line), and is a live end-to-end test of badc's inline assembly: a correct boot means the inline asm assembled to the right bytes and executed on real hardware paths.

What each target exercises:

  • x86_64 -- cpuid reads the processor vendor string; a bswap lowered through the table-driven encoder byte-reverses a known constant (the printed 0x0807060504030201 proves it produced the right bytes); a raw-byte NOP spelled as a .byte directive.
  • AArch64 -- mrs reads a system register (CTR_EL0); a raw-byte NOP spelled both as a .byte directive and as a bare hex-byte run.

Run

cargo build --release --features full      # build badc
python3 demos/kernel/smoke.py               # build + boot both targets
python3 demos/kernel/smoke.py --arch native # boot this host's arch only
python3 demos/kernel/smoke.py --self-test   # check the arch filter, no boot

The smoke compiles kernel.c for windows-x64 and windows-arm64 (PE32+ EFI applications) and, when QEMU and UEFI firmware are present, boots each and checks the serial output for the per-target markers and the final BADC-KERNEL-OK. If QEMU or the firmware is missing the boot is skipped, the demo is build-only for that target, and the closing summary says how many boots ran. Override the badc binary with $BADC.

The emulator is stopped as soon as the expected markers reach the serial line, so a boot costs what the firmware and the kernel take to print them. Neither kernel exits on its own -- preempt.c ends in a halt loop and kernel.c returns to the firmware -- so a boot spends the whole 60 s budget only when its markers do not appear.

--arch <x64|aarch64|native> narrows the run to one architecture's boots; every kernel is still built for both, each build costing a fraction of a second. The local gate runs both architectures on each Linux lane and does not use the filter.

Booting inside badc-built QEMU

By default the smoke uses the system qemu-system-<arch>. To run the kernel inside the badc-built emulator from the qemu demo, point the per-arch override at its output:

QEMU_SYSTEM_X64=demos/qemu/qbuild-x86_64/qemu-system-x86_64 \
QEMU_SYSTEM_AARCH64=demos/qemu/qbuild-aarch64/qemu-system-aarch64 \
    python3 demos/kernel/smoke.py

so the whole stack -- the kernel, and the emulator it runs in -- is badc-built.

Preemptive multitasking (preempt.c)

preempt.c is a second, larger kernel: it installs its own timer interrupt, runs three hardcoded threads, and context-switches between them on every tick, with each thread printing to the serial port under a spin lock. It goes beyond the inline-asm smoke by taking over the interrupt vector table and the timer, so it is a live test of badc emitting a real interrupt service routine -- a __attribute__((naked)) function whose body is the context switch.

Both targets are complete and the smoke boots them: the serial output shows the three threads round-robin under the timer, then efi_main resumes after the scheduler stops and prints BADC-PREEMPT-OK.

  • x86_64 reprograms the 8259 PIC + 8254 PIT and installs an IDT; the naked ISR saves the registers, calls the C scheduler, restores, and iretqs. It exercises naked prologue suppression, explicit-register operands, push/pop, immediate port I/O, and a direct call to a C symbol.
  • AArch64 programs the GICv2 + the virtual generic timer and installs an EL1 vector table. UEFI maps loaded data execute-never, so the table lives in a page of EfiLoaderCode obtained from BootServices->AllocatePages; the naked ISR reaches the scheduler through TPIDR_EL1 and a blr (AArch64 inline asm has no symbol references), saves the full integer context with stp, and returns through eret.

On x86_64 every vector the demo does not install gets a diagnostic gate: a 16-byte stub pushes its vector number and jumps to a common handler that prints the vector, the error code where the CPU pushed one, and the faulting RIP, then halts. Without them an unexpected exception escalates to a triple fault, which resets the guest with nothing on the serial line. Building with -DPREEMPT_FAULT_INJECT raises #GP right after the IDT is installed, which is how the smoke checks the diagnostic.

Both arch_start_scheduler implementations mask interrupts before they touch the vector table. UEFI enters an application with interrupts enabled and the firmware's own timer running -- on x86_64 that is IRQ0 through the 8259, whose initialization sequence clears the master's vector base and mask until ICW2 arrives, so an IRQ0 taken in that six-instruction window arrives at vector 0 and lands on the demo's unhandled-fault gate. Building with -DPREEMPT_PIC_WINDOW_STRESS holds the window open past a firmware timer period, which turns that race into a certainty; the smoke boots it and requires the normal markers.

On AArch64 the mask follows the AllocatePages call for the vector page: the boot service returns with IRQ unmasked, so a mask taken before it does not hold. The firmware's tick is the virtual timer's own interrupt, every 10 ms under QEMU's edk2, and one taken between the VBAR_EL1 and TPIDR_EL1 writes enters the ISR before the scheduler pointer it loads is set; the ISR faults with IRQ masked and the guest prints nothing after its first line. -DPREEMPT_VBAR_WINDOW_STRESS holds that window open for 40 ms, and the smoke boots it the same way.

All addresses and saved stack pointers use the pointer-width UINTN (unsigned long long), not unsigned long, because the EFI targets are LLP64 (long is 32-bit); a 32-bit IDT base or saved SP would fault on the first tick.