M6: user runtime library rt + C-convention heap

Add lib/ — the shared user-space runtime every user binary links against
(init now, servers/drivers later): syscall wrappers, the heap, IPC stub,
and the process start shim.

- lib/heap.zig: the kernel first-fit free-list ported to user space, grown
  via the mmap syscall instead of pmm+mapPage. Dual API over one global free
  list: extern "C" malloc/free/calloc/realloc (C ABI for future C code) and a
  std.mem.Allocator adapter (with in-place resize) for Zig std containers.
- lib/syscall.zig + sys.zig: raw syscall0..5 (arg3 in r10) and typed
  yield/write/sleep/exit/mmap/munmap over danos.Syscall.
- lib/start.zig: naked _start -> rt_start -> root.main() (SysV realign via
  call), panic -> exit(127).
- lib/user.ld: the user link script, moved from sbin/linker.ld (shared by all
  user binaries).
- build.zig: register the `rt` module; add an addUserBinary() helper that is
  the one recipe for every user binary (freestanding, .large, use_lld,
  user.ld, image_base), replacing the bespoke init block.
- sbin/init.zig: migrated onto rt; drops its hand-rolled syscall2/shims. Now
  proves the heap (alloc -> write from a heap pointer -> free) before the
  heartbeat loop. Serial shows "init: heap ok". Suite 28/28.
This commit is contained in:
Daniel Samson
2026-07-09 07:08:32 +01:00
parent 9316f9f1c3
commit 0a8c81b17a
9 changed files with 448 additions and 82 deletions
+53 -27
View File
@@ -48,6 +48,43 @@ 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
/// 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,
name: []const u8,
root: []const u8,
) *std.Build.Step.Compile {
const exe = b.addExecutable(.{
.name = name,
.root_module = b.createModule(.{
.root_source_file = b.path(root),
.target = target,
.optimize = .ReleaseSmall,
.code_model = .large,
.single_threaded = true,
.sanitize_c = .off,
.stack_check = false,
.stack_protector = false,
.imports = &.{
.{ .name = "rt", .module = rt_mod },
},
}),
});
exe.setLinkerScript(b.path("lib/user.ld"));
exe.entry = .{ .symbol_name = "_start" };
exe.image_base = 0x7000_0000_0000;
exe.use_llvm = true;
exe.use_lld = true;
return exe;
}
pub fn build(b: *std.Build) void {
ensureZigVersion();
@@ -98,6 +135,18 @@ pub fn build(b: *std.Build) void {
},
});
// The user-space runtime library (a nascent libc): syscall 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"),
.imports = &.{
.{ .name = "danos", .module = mod },
},
});
// Compile-time config 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)");
@@ -151,33 +200,10 @@ pub fn build(b: *std.Build) void {
b.installArtifact(exe);
// --- /sbin/init: the first user-space program ---
// Its own tiny freestanding binary, linked at a fixed address inside the
// kernel's user region (process.zig) and started in ring 3 by the kernel's
// user-ELF loader. `.large` because the image base is above 4 GiB — small/
// medium code models emit 32-bit absolute relocations that can't reach.
// Pinned to ReleaseSmall: the user region gives it a 2 MiB budget and its
// size has no reason to track the kernel's optimize mode.
const init_exe = b.addExecutable(.{
.name = "init",
.root_module = b.createModule(.{
.root_source_file = b.path("sbin/init.zig"),
.target = kernel_target,
.optimize = .ReleaseSmall,
.code_model = .large,
.single_threaded = true,
.sanitize_c = .off,
.stack_check = false,
.stack_protector = false,
}),
});
init_exe.setLinkerScript(b.path("sbin/linker.ld"));
init_exe.entry = .{ .symbol_name = "_start" };
init_exe.image_base = 0x7000_0000_0000;
// The self-hosted linker ignores the script's PHDRS (segment permissions),
// which the kernel's W^X user-ELF loader requires (code must be R+X). Pin to
// LLVM + LLD so the script is authoritative.
init_exe.use_llvm = true;
init_exe.use_lld = true;
// Built by the shared user-binary recipe (see addUserBinary): freestanding,
// linked into the kernel's user region against the `rt` 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");
b.installArtifact(init_exe);
// Boot methods live in src/boot/, one per way of getting the kernel running.