M11–M12: IRQ-as-IPC and bus drivers; expand names tree-wide

Two driver-model milestones plus a tree-wide naming pass. Suite 35/35
(QEMU) + host tests green.

M11 — IRQ-as-IPC. A ring-3 driver now sleeps until its device interrupts
it. New src/kernel/irq.zig: per-GSI endpoint bindings, comptime per-vector
trampolines, dispatch = mask GSI -> LAPIC EOI -> notifyLocked, all under one
lock region. irq_bind/irq_ack syscalls, gated by the device claim like
mmio_map. interruptDispatch no longer EOIs — each handler owns its EOI,
because a level line must be masked before it is acknowledged (irq_ack is
the unmask). Bindings are keyed on the owning task and released on exit
(a shared endpoint's siblings survive). hpetd rewritten interrupt-driven.
Tests: hpet (rewritten, reads back the I/O APIC routing) and irqfree.

M12 — bus drivers. DeviceDesc gains a parent, making the device table a
tree. dev_register (device_register) lets a process publish children below
a device it claimed; the kernel enforces resource containment (a child's
resources must nest in its parent's), so a descriptor can't fabricate a
window over kernel RAM. Descriptor copied in via copyFromUser (physmap
walk — an unmapped user pointer fails the call instead of faulting the
kernel). Per-parent child cap bounds table exhaustion. sbin/busd.zig is a
worked bus driver. Test: bus.

Naming — per docs/coding-standards.md: non-acronym abbreviations spelled
out (message, descriptor, device_service, scheduler, runtime, physical,
interpreter, ...); acronyms kept (IPC, MMIO, DMA, HCD, ...); files are
kebab-case (ipc-synchronous.zig, device-service.zig, vfs-protocol.zig, ...).
Exceptions: POSIX/C ABI names and Zig idioms (init/len/ptr) kept. Module
collisions resolved by specific naming (config -> parameters, device.zig
alias -> device_model). AML op/Op disambiguated: op = opcode, Op =
operation; per-opcode parse handlers renamed opX -> parseX.

New driver docs: drivers.md, driver-model.md (bus/class/HCD shapes + the
proposed M13–M16 ABI), coding-standards.md.
This commit is contained in:
Daniel Samson
2026-07-10 11:39:56 +01:00
parent 83881641ca
commit 15b70856c9
63 changed files with 4722 additions and 2690 deletions
+49 -46
View File
@@ -51,13 +51,13 @@ fn timestamp(b: *std.Build) []const u8 {
/// Build one user-space binary the same way for every program (init, and later
/// the VFS server + drivers): freestanding, ReleaseSmall, `.large` code model
/// (the image base is above 4 GiB — smaller models emit 32-bit relocations that
/// can't reach), linked against the `rt` runtime library with the shared user
/// can't reach), linked against the `runtime` runtime library with the shared user
/// link script. Pinned to LLVM + LLD so the script's PHDRS (segment permissions)
/// are authoritative — the kernel's W^X user-ELF loader requires exact perms.
fn addUserBinary(
b: *std.Build,
target: std.Build.ResolvedTarget,
rt_mod: *std.Build.Module,
runtime_module: *std.Build.Module,
name: []const u8,
root: []const u8,
) *std.Build.Step.Compile {
@@ -73,7 +73,7 @@ fn addUserBinary(
.stack_check = false,
.stack_protector = false,
.imports = &.{
.{ .name = "rt", .module = rt_mod },
.{ .name = "runtime", .module = runtime_module },
},
}),
});
@@ -91,74 +91,74 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Shared handoff definitions (BootInfo, Framebuffer, ...). No target is set,
// Shared handoff definitions (BootInformation, Framebuffer, ...). No target is set,
// so the module inherits the target of whichever binary imports it — the
// freestanding kernel or the UEFI bootloader.
const mod = b.addModule("danos", .{
const danos_module = b.addModule("danos", .{
.root_source_file = b.path("src/root.zig"),
});
// Kernel tunables (max_cpus, stack sizes, tick rate). A dependency-free module of
// Kernel tunables (maximum_cpus, stack sizes, tick rate). A dependency-free module of
// compile-time constants, imported wherever a knob is read; keeps the trade-offs
// in one place instead of scattered across the tree. See src/config.zig.
const config_mod = b.addModule("config", .{
.root_source_file = b.path("src/config.zig"),
// in one place instead of scattered across the tree. See src/configuration.zig.
const parameters_module = b.addModule("parameters", .{
.root_source_file = b.path("src/parameters.zig"),
});
// Architecture-specific kernel code (CPU ops, entry, later GDT/IDT/paging).
// The generic kernel imports this as "arch" and never names x86_64, so a new
// The generic kernel imports this as "architecture" and never names x86_64, so a new
// architecture is a matter of pointing this module at a different directory.
const arch_mod = b.addModule("arch", .{
const architecture_module = b.addModule("architecture", .{
.root_source_file = b.path("src/kernel/arch/x86_64/cpu.zig"),
.imports = &.{
.{ .name = "danos", .module = mod }, // paging uses the shared BootInfo/memory-map types
.{ .name = "config", .module = config_mod }, // max_cpus, ist_stack_size, timer_hz
.{ .name = "danos", .module = danos_module }, // paging uses the shared BootInformation/memory-map types
.{ .name = "parameters", .module = parameters_module }, // maximum_cpus, ist_stack_size, timer_hz
},
});
// CPU-exception stubs — real assembly, since they need cross-symbol
// jumps/calls that Zig inline asm can't express (see the file's header).
arch_mod.addAssemblyFile(b.path("src/kernel/arch/x86_64/isr.s"));
architecture_module.addAssemblyFile(b.path("src/kernel/arch/x86_64/isr.s"));
// The AP bring-up trampoline: 16-/32-/64-bit mode-switch code that can't be
// inline asm (it runs relocated to a low page, not at its link address).
arch_mod.addAssemblyFile(b.path("src/kernel/arch/x86_64/trampoline.s"));
architecture_module.addAssemblyFile(b.path("src/kernel/arch/x86_64/trampoline.s"));
// Firmware-agnostic device discovery. The generic kernel imports this as
// "platform" and asks it to enumerate hardware into a backend-neutral device
// tree, never naming ACPI (or, later, device-tree) — the same discipline the
// arch module applies to CPU code. The backend is selected at runtime from
// architecture module applies to CPU code. The backend is selected at runtime from
// the boot handoff (see src/device/platform.zig).
const platform_mod = b.addModule("platform", .{
const platform_module = b.addModule("platform", .{
.root_source_file = b.path("src/device/platform.zig"),
.imports = &.{
.{ .name = "danos", .module = mod }, // BootInfo (carries the ACPI RSDP)
.{ .name = "config", .module = config_mod }, // max_cpus (the discovery pool)
.{ .name = "danos", .module = danos_module }, // BootInformation (carries the ACPI RSDP)
.{ .name = "parameters", .module = parameters_module }, // maximum_cpus (the discovery pool)
},
});
// The user-space runtime library (a nascent libc): syscall wrappers, the
// The user-space runtime library (a nascent libc): system_call wrappers, the
// C-convention heap, IPC helpers, the process start shim. Compiled into every
// user binary (see addUserBinary), so it inherits each exe's `.large` code
// model — do NOT set a target/code_model here. It imports `danos` for the
// shared Syscall numbers.
const rt_mod = b.addModule("rt", .{
.root_source_file = b.path("lib/rt.zig"),
// shared SystemCall numbers.
const runtime_module = b.addModule("runtime", .{
.root_source_file = b.path("lib/runtime.zig"),
.imports = &.{
.{ .name = "danos", .module = mod },
.{ .name = "danos", .module = danos_module },
},
});
// The initrd container format, shared by the kernel (unpacks it) and the
// build-time packer tools/mkinitrd.zig (produces it). No dependencies.
const initrd_mod = b.addModule("initrd", .{
.root_source_file = b.path("src/user/proto/initrd.zig"),
const initrd_module = b.addModule("initrd", .{
.root_source_file = b.path("src/user/protocol/initrd.zig"),
});
// Compile-time config the kernel reads as `@import("build_options")`. The
// Compile-time configuration the kernel reads as `@import("build_options")`. The
// QEMU test harness sets -Dtest-case=<name> to run one self-test at boot.
const test_case = b.option([]const u8, "test-case", "Kernel self-test case to run at boot (see src/kernel/tests.zig)");
const build_options = b.addOptions();
build_options.addOption(?[]const u8, "test_case", test_case);
const build_options_mod = build_options.createModule();
const build_options_module = build_options.createModule();
// --- Kernel: freestanding x86_64 ELF, jumped to by the bootloader ---
// SSE2 is part of the x86_64 baseline and UEFI leaves it enabled at handoff,
@@ -177,18 +177,18 @@ pub fn build(b: *std.Build) void {
.target = kernel_target,
.optimize = optimize,
.code_model = .kernel, // kernel runs in the top 2 GiB (higher half)
.red_zone = false, // interrupts would corrupt the SysV red zone
.red_zone = false, // interrupts would corrupt the SystemV red zone
.single_threaded = false, // SMP: the big kernel lock's atomics must be real across cores
.sanitize_c = .off, // the UBSan runtime needs f128/SSE support we don't provide
.stack_check = false, // stack-probe calls have no runtime to land in
.stack_protector = false,
.imports = &.{
.{ .name = "danos", .module = mod },
.{ .name = "arch", .module = arch_mod },
.{ .name = "platform", .module = platform_mod },
.{ .name = "config", .module = config_mod },
.{ .name = "build_options", .module = build_options_mod },
.{ .name = "initrd", .module = initrd_mod },
.{ .name = "danos", .module = danos_module },
.{ .name = "architecture", .module = architecture_module },
.{ .name = "platform", .module = platform_module },
.{ .name = "parameters", .module = parameters_module },
.{ .name = "build_options", .module = build_options_module },
.{ .name = "initrd", .module = initrd_module },
},
}),
});
@@ -208,18 +208,19 @@ pub fn build(b: *std.Build) void {
// --- /sbin/init: the first user-space program ---
// Built by the shared user-binary recipe (see addUserBinary): freestanding,
// linked into the kernel's user region against the `rt` runtime library, and
// linked into the kernel's user region against the `runtime` runtime library, and
// started in ring 3 by the kernel's user-ELF loader.
const init_exe = addUserBinary(b, kernel_target, rt_mod, "init", "sbin/init.zig");
const init_exe = addUserBinary(b, kernel_target, runtime_module, "init", "sbin/init.zig");
b.installArtifact(init_exe);
// --- initrd: a bundle of extra user binaries (VFS server + drivers) ---
// Each is built by the same user-binary recipe, then packed into one image by
// the host-side mkinitrd tool. The bootloader ferries the image to the kernel,
// which unpacks it and spawns each program (src/user/proto/initrd.zig).
const vfs_exe = addUserBinary(b, kernel_target, rt_mod, "vfs", "sbin/vfs.zig");
const vfstest_exe = addUserBinary(b, kernel_target, rt_mod, "vfstest", "sbin/vfstest.zig");
const hpetd_exe = addUserBinary(b, kernel_target, rt_mod, "hpetd", "sbin/hpetd.zig");
// which unpacks it and spawns each program (src/user/protocol/initrd.zig).
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, "vfs", "sbin/vfs.zig");
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, "vfs-test", "sbin/vfs-test.zig");
const hpetd_exe = addUserBinary(b, kernel_target, runtime_module, "hpetd", "sbin/hpetd.zig");
const busd_exe = addUserBinary(b, kernel_target, runtime_module, "busd", "sbin/busd.zig");
// Pack the user binaries into the initrd image with the host-side Python tool
// (the container format is trivial, and Python sidesteps std API churn). Args:
@@ -229,10 +230,12 @@ pub fn build(b: *std.Build) void {
const initrd_img = mk_run.addOutputFileArg("initrd.img");
mk_run.addArg("vfs");
mk_run.addFileArg(vfs_exe.getEmittedBin());
mk_run.addArg("vfstest");
mk_run.addArg("vfs-test");
mk_run.addFileArg(vfstest_exe.getEmittedBin());
mk_run.addArg("hpetd");
mk_run.addFileArg(hpetd_exe.getEmittedBin());
mk_run.addArg("busd");
mk_run.addFileArg(busd_exe.getEmittedBin());
// Install the image to zig-out/bin (so the QEMU test harness picks it up like
// the other binaries). The run-x86-64 ESP install is added below.
@@ -252,7 +255,7 @@ pub fn build(b: *std.Build) void {
}),
.optimize = optimize,
.imports = &.{
.{ .name = "danos", .module = mod },
.{ .name = "danos", .module = danos_module },
},
}),
});
@@ -261,14 +264,14 @@ pub fn build(b: *std.Build) void {
// --- run-x86-64: boot the x86-64 kernel in QEMU via UEFI/OVMF ---
// Firmware lives in different places per OS/distro, so probe the known
// layouts (Arch, Debian/Ubuntu, Fedora, macOS Homebrew) and use the first
// layouts (Architecture, Debian/Ubuntu, Fedora, macOS Homebrew) and use the first
// that exists. Override with -Dovmf-code / -Dovmf-vars if yours is elsewhere.
const ovmf_code = b.option(
[]const u8,
"ovmf-code",
"Path to the OVMF_CODE firmware image",
) orelse firstExisting(b.graph.io, &.{
"/usr/share/edk2/x64/OVMF_CODE.4m.fd", // Arch
"/usr/share/edk2/x64/OVMF_CODE.4m.fd", // Architecture
"/usr/share/OVMF/OVMF_CODE_4M.fd", // Debian/Ubuntu
"/usr/share/OVMF/OVMF_CODE.fd", // older Debian/Ubuntu
"/usr/share/edk2-ovmf/x64/OVMF_CODE.fd", // Fedora
@@ -280,7 +283,7 @@ pub fn build(b: *std.Build) void {
"ovmf-vars",
"Path to the OVMF_VARS firmware image (a writable copy is made)",
) orelse firstExisting(b.graph.io, &.{
"/usr/share/edk2/x64/OVMF_VARS.4m.fd", // Arch
"/usr/share/edk2/x64/OVMF_VARS.4m.fd", // Architecture
"/usr/share/OVMF/OVMF_VARS_4M.fd", // Debian/Ubuntu
"/usr/share/OVMF/OVMF_VARS.fd", // older Debian/Ubuntu
"/usr/share/edk2-ovmf/x64/OVMF_VARS.fd", // Fedora