Cross-architecture test suite
This commit is contained in:
@@ -8,10 +8,22 @@ const gdt = @import("gdt.zig");
|
||||
const tss = @import("tss.zig");
|
||||
const idt = @import("idt.zig");
|
||||
const paging = @import("paging.zig");
|
||||
const serial = @import("serial.zig");
|
||||
|
||||
/// The saved register/trap frame passed to a fault handler.
|
||||
pub const CpuState = idt.CpuState;
|
||||
|
||||
/// Bring up the serial port (the kernel's machine-readable log). No dependencies,
|
||||
/// so it can be the very first thing called.
|
||||
pub fn serialInit() void {
|
||||
serial.init();
|
||||
}
|
||||
|
||||
/// Write bytes to the serial port.
|
||||
pub fn serialWrite(bytes: []const u8) void {
|
||||
serial.write(bytes);
|
||||
}
|
||||
|
||||
/// Set up the CPU's descriptor tables: our own GDT, the TSS (with an interrupt
|
||||
/// stack for double faults), then the IDT with exception handlers. After this a
|
||||
/// CPU fault is reported instead of triple-faulting. Install the fault handler
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
//! COM1 serial port (16550 UART) — the kernel's machine-readable output channel.
|
||||
//! Unlike the framebuffer console, serial text can be captured to a file by QEMU
|
||||
//! (`-serial file:...`), which is what the test harness asserts on. Each
|
||||
//! architecture has its own UART; this is the x86 one, driven by port I/O.
|
||||
|
||||
const port = 0x3F8; // COM1 base
|
||||
|
||||
fn outb(p: u16, value: u8) void {
|
||||
asm volatile ("outb %[value], %[p]"
|
||||
:
|
||||
: [value] "{al}" (value),
|
||||
[p] "{dx}" (p),
|
||||
);
|
||||
}
|
||||
|
||||
fn inb(p: u16) u8 {
|
||||
return asm volatile ("inb %[p], %[value]"
|
||||
: [value] "={al}" (-> u8),
|
||||
: [p] "{dx}" (p),
|
||||
);
|
||||
}
|
||||
|
||||
/// Configure the UART: 38400 baud, 8N1, FIFO on. Safe to call before anything
|
||||
/// else; it has no dependencies.
|
||||
pub fn init() void {
|
||||
outb(port + 1, 0x00); // disable interrupts
|
||||
outb(port + 3, 0x80); // enable DLAB (set baud divisor)
|
||||
outb(port + 0, 0x03); // divisor low: 38400 baud
|
||||
outb(port + 1, 0x00); // divisor high
|
||||
outb(port + 3, 0x03); // 8 bits, no parity, one stop bit; DLAB off
|
||||
outb(port + 2, 0xC7); // enable + clear FIFO, 14-byte threshold
|
||||
outb(port + 4, 0x0B); // RTS/DSR set
|
||||
}
|
||||
|
||||
fn writeByte(c: u8) void {
|
||||
while (inb(port + 5) & 0x20 == 0) {} // wait until the transmit holding register is empty
|
||||
outb(port, c);
|
||||
}
|
||||
|
||||
/// Write bytes, translating LF to CRLF so terminals and logs line up.
|
||||
pub fn write(bytes: []const u8) void {
|
||||
for (bytes) |c| {
|
||||
if (c == '\n') writeByte('\r');
|
||||
writeByte(c);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
const danos = @import("danos");
|
||||
const arch = @import("arch");
|
||||
|
||||
/// The console font, embedded at compile time. cp850-8x16, PSF2 format:
|
||||
/// a 32-byte header, then 256 glyphs of 16 bytes each (one byte per 8-pixel
|
||||
@@ -40,6 +41,8 @@ pub const Console = struct {
|
||||
}
|
||||
|
||||
pub fn write(self: *Console, bytes: []const u8) void {
|
||||
// Mirror everything to the serial port so it's captured in logs / tests.
|
||||
arch.serialWrite(bytes);
|
||||
for (bytes) |c| self.putChar(c);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ const danos = @import("danos");
|
||||
const arch = @import("arch");
|
||||
const console = @import("console.zig");
|
||||
const pmm = @import("pmm.zig");
|
||||
const tests = @import("tests.zig");
|
||||
const build_options = @import("build_options");
|
||||
const BootInfo = danos.BootInfo;
|
||||
|
||||
/// The calling convention used to enter the kernel. Pinned to SysV explicitly:
|
||||
@@ -25,6 +27,8 @@ export fn _start(boot_info: *const BootInfo) callconv(kernel_abi) noreturn {
|
||||
}
|
||||
|
||||
fn kmain(boot_info: *const BootInfo) noreturn {
|
||||
arch.serialInit(); // machine-readable log; console mirrors to it
|
||||
|
||||
const fb = boot_info.framebuffer;
|
||||
con = console.Console.init(fb);
|
||||
con.clear();
|
||||
@@ -87,6 +91,13 @@ fn kmain(boot_info: *const BootInfo) noreturn {
|
||||
con.print("\ndanos: paging enabled\n", .{});
|
||||
con.print(" page tables: CR3 = 0x{x:0>16}\n", .{arch.readCr3()});
|
||||
|
||||
// In a test build (`zig build -Dtest-case=<name>`), run that case and stop.
|
||||
// Normal builds fall through to the idle halt.
|
||||
if (build_options.test_case) |case| {
|
||||
tests.run(case, boot_info);
|
||||
arch.halt();
|
||||
}
|
||||
|
||||
con.write("\nkernel initialised; nothing left to do, halting.\n");
|
||||
|
||||
arch.halt();
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
//! In-kernel test cases, run at the end of bring-up when the kernel is built with
|
||||
//! `-Dtest-case=<name>`. Each case writes structured markers to the serial port
|
||||
//! that the QEMU harness (test/qemu_test.py) asserts on:
|
||||
//!
|
||||
//! [PASS]/[FAIL] <check> per assertion
|
||||
//! DANOS-TEST-RESULT: PASS|FAIL overall, for non-faulting cases
|
||||
//!
|
||||
//! Faulting cases (fault-ud, fault-pf, fault-df) deliberately don't return a
|
||||
//! result line — they trigger a CPU exception, and the harness asserts on the
|
||||
//! exception report the handler prints (which also reaches serial).
|
||||
|
||||
const std = @import("std");
|
||||
const danos = @import("danos");
|
||||
const arch = @import("arch");
|
||||
const pmm = @import("pmm.zig");
|
||||
|
||||
/// Formatted write straight to serial, independent of the framebuffer console.
|
||||
fn log(comptime fmt: []const u8, args: anytype) void {
|
||||
var buf: [128]u8 = undefined;
|
||||
arch.serialWrite(std.fmt.bufPrint(&buf, fmt, args) catch return);
|
||||
}
|
||||
|
||||
var passed: u32 = 0;
|
||||
var failed: u32 = 0;
|
||||
|
||||
fn check(name: []const u8, ok: bool) void {
|
||||
if (ok) {
|
||||
passed += 1;
|
||||
log("[PASS] {s}\n", .{name});
|
||||
} else {
|
||||
failed += 1;
|
||||
log("[FAIL] {s}\n", .{name});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(case: []const u8, boot_info: *const BootInfo) void {
|
||||
if (eql(case, "smoke")) {
|
||||
smoke(boot_info);
|
||||
} else if (eql(case, "fault-ud")) {
|
||||
faultInvalidOpcode();
|
||||
} else if (eql(case, "fault-pf")) {
|
||||
faultPageFault();
|
||||
} else if (eql(case, "fault-df")) {
|
||||
faultDoubleFault();
|
||||
} else {
|
||||
log("DANOS-TEST-RESULT: FAIL (unknown case '{s}')\n", .{case});
|
||||
}
|
||||
}
|
||||
|
||||
const BootInfo = danos.BootInfo;
|
||||
|
||||
fn eql(a: []const u8, b: []const u8) bool {
|
||||
return std.mem.eql(u8, a, b);
|
||||
}
|
||||
|
||||
/// Non-destructive checks of the memory map and frame allocator.
|
||||
fn smoke(boot_info: *const BootInfo) void {
|
||||
log("DANOS-TEST-BEGIN: smoke\n", .{});
|
||||
|
||||
// The memory map has some usable RAM.
|
||||
const mm = boot_info.memory_map;
|
||||
const regions = @as([*]const danos.MemoryRegion, @ptrFromInt(mm.regions))[0..mm.len];
|
||||
var usable: u64 = 0;
|
||||
for (regions) |r| {
|
||||
if (r.kind == .usable) usable += r.pages;
|
||||
}
|
||||
check("memory map reports usable RAM", usable > 0);
|
||||
|
||||
// The frame allocator hands out distinct, page-aligned frames.
|
||||
const a = pmm.alloc();
|
||||
const b = pmm.alloc();
|
||||
check("alloc returns a frame", a != null);
|
||||
check("alloc returns distinct frames", a != null and b != null and a.? != b.?);
|
||||
check("frames are page-aligned", (a orelse 1) % danos.page_size == 0);
|
||||
|
||||
// Freeing restores the count.
|
||||
const before = pmm.stats().free_frames;
|
||||
if (a) |p| pmm.free(p);
|
||||
if (b) |p| pmm.free(p);
|
||||
check("free returns frames to the pool", pmm.stats().free_frames == before + 2);
|
||||
|
||||
// Paging is active on our own tables (CR3 is non-zero and page-aligned).
|
||||
const cr3 = arch.readCr3();
|
||||
check("paging active (CR3 set)", cr3 != 0 and cr3 % danos.page_size == 0);
|
||||
|
||||
log("DANOS-TEST-RESULT: {s} ({d} passed, {d} failed)\n", .{
|
||||
if (failed == 0) "PASS" else "FAIL",
|
||||
passed,
|
||||
failed,
|
||||
});
|
||||
log("DANOS-TEST-DONE\n", .{});
|
||||
}
|
||||
|
||||
fn faultInvalidOpcode() void {
|
||||
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
|
||||
asm volatile ("ud2");
|
||||
}
|
||||
|
||||
fn faultPageFault() void {
|
||||
log("DANOS-TEST-BEGIN: fault-pf\n", .{});
|
||||
// Runtime address so the backend emits a register store (not a `mov moffs`,
|
||||
// which the self-hosted x86_64 backend can't encode).
|
||||
var addr: u64 = 0xdeadbeef000; // above our identity-mapped 4 GiB
|
||||
const p: *volatile u64 = @ptrFromInt(addr);
|
||||
p.* = 1;
|
||||
addr += 0;
|
||||
}
|
||||
|
||||
fn faultDoubleFault() void {
|
||||
log("DANOS-TEST-BEGIN: fault-df\n", .{});
|
||||
// Point RSP at unmapped memory, then fault: the CPU can't push the fault
|
||||
// frame, which escalates to #DF — survivable only because #DF runs on IST1.
|
||||
var bad_sp: u64 = 0x5000000000;
|
||||
asm volatile (
|
||||
\\mov %[sp], %%rsp
|
||||
\\ud2
|
||||
:
|
||||
: [sp] "r" (bad_sp),
|
||||
: .{ .memory = true }
|
||||
);
|
||||
bad_sp += 0;
|
||||
}
|
||||
Reference in New Issue
Block a user