M4: /sbin/init as PID 1 — a heartbeat process
init is now a real scheduled ring-3 process: it prints a heartbeat and sleeps, forever. The kernel spawns it at boot via spawnProcess (its own address space, preemption on) and the boot context drops to idle — the system's steady state is "kernel idle, init alive in ring 3", beating ~1 Hz. Adds the sleep(ms) syscall. The init/process tests are reshaped around the heartbeat (repeated syscalls, still-alive, two concurrent processes on distinct address spaces). Pins init to LLD and folds the .large code model's .ltext/.lrodata/.ldata into the linker script so its code segment is R+X. Removes the now-dead borrowed-thread ELF loader (runInitElf); spawnProcess is the one path. Docs updated. Suite 28/28. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
37fb3cb0cf
commit
b9d9e1e523
|
|
@ -173,6 +173,11 @@ pub fn build(b: *std.Build) void {
|
|||
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;
|
||||
b.installArtifact(init_exe);
|
||||
|
||||
// Boot methods live in src/boot/, one per way of getting the kernel running.
|
||||
|
|
|
|||
|
|
@ -81,16 +81,20 @@ prerequisites.
|
|||
(with boot-services memory reclaimed), [paging](paging.md) with W^X, [exceptions and
|
||||
interrupts](interrupts.md), a [calibrated timer + ns clock](device-interrupts.md), a
|
||||
[heap](heap.md), a [fixed-priority preemptive scheduler](scheduling.md) with blocking,
|
||||
in-kernel [IPC channels](ipc.md), SMP (all cores scheduling, with affinity), and
|
||||
**ring 3**: user GDT/TSS plumbing, U/S-bit mappings, an `int 0x80` syscall gate, and
|
||||
`/sbin/init` — a real user ELF built from `sbin/`, shipped on the boot volume, loaded
|
||||
by the kernel, run at CPL 3 — plus a [test harness](testing.md).
|
||||
in-kernel [IPC channels](ipc.md), SMP (all cores scheduling, with affinity), a
|
||||
**higher-half kernel** with a physmap, and **user space**: per-process address
|
||||
spaces, `syscall`/`sysret` with the `swapgs` discipline, a user-ELF loader, and
|
||||
`/sbin/init` — a real user ELF built from `sbin/`, running at CPL 3 as PID 1 on its
|
||||
own page tables — plus a [test harness](testing.md).
|
||||
|
||||
- **Isolation track** — **user mode + address-space isolation** (higher-half kernel,
|
||||
ring 3, per-process page tables). The substrate everything else needs. *In
|
||||
progress: ring 3 + a loaded `/sbin/init` work (M1); next the higher-half move (M2),
|
||||
then per-process address spaces + `syscall`/`sysret` + init as a real schedulable
|
||||
process (M3), then ELF/initrd generalisation (M4).*
|
||||
- **Isolation track** — **user mode + address-space isolation**. *Done: a
|
||||
higher-half kernel with a physmap (the low half is user space), per-process
|
||||
address spaces with CR3 switched on context switch, the `swapgs` discipline,
|
||||
`syscall`/`sysret`, a user-ELF loader, and `/sbin/init` running as a real
|
||||
preemptive ring-3 process (PID 1). Remaining polish: an address-space/stack
|
||||
reaper for exited tasks, SMAP + fault-recovering copy-in/out, the real IPC
|
||||
syscalls (IPC_Call/IPC_ReplyWait — they arrive with the second user server),
|
||||
and TLB shootdown once a process has more than one thread.*
|
||||
- **Resilience track** — fault → kill → notify, a supervisor/reincarnation server,
|
||||
resource cleanup on death, then a restartable driver as proof. Needs isolation.
|
||||
See [resilience.md](resilience.md).
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
//! /sbin/init — the first user-space program. Built as its own freestanding
|
||||
//! binary (see build.zig), shipped on the boot volume at sbin/init, loaded by
|
||||
//! the bootloader, and started in ring 3 by the kernel's user-ELF loader
|
||||
//! (src/kernel/usermode.zig). It talks to the kernel only through the
|
||||
//! `int $0x80` syscall gate.
|
||||
//! /sbin/init — the first user-space program, PID 1. Built as its own
|
||||
//! freestanding binary (see build.zig), shipped on the boot volume at sbin/init,
|
||||
//! loaded by the bootloader, and started in ring 3 as a scheduled process by the
|
||||
//! kernel (src/kernel/usermode.zig). It talks to the kernel only through the
|
||||
//! `syscall` instruction.
|
||||
//!
|
||||
//! Today it just proves the path — say hello, exit — and grows into the real
|
||||
//! init (service supervision) once processes are schedulable (M3).
|
||||
//! Today it's a heartbeat: it prints a line and sleeps, forever — enough to show
|
||||
//! the system reaches user space and stays alive with a real process scheduled
|
||||
//! alongside the kernel's idle loop. It grows into the real init (service
|
||||
//! supervision) once there are other user programs to supervise.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// The M1 syscall numbers (usermode.zig): 0 = exit(code), 2 = write(ptr, len).
|
||||
// Syscall numbers (see src/kernel/usermode.zig):
|
||||
const sys_exit = 0;
|
||||
const sys_write = 2;
|
||||
const sys_sleep = 3;
|
||||
|
||||
fn syscall2(n: u64, a: u64, b: u64) u64 {
|
||||
// The `syscall` instruction clobbers RCX (return RIP) and R11 (saved RFLAGS);
|
||||
|
|
@ -28,6 +31,10 @@ fn write(msg: []const u8) void {
|
|||
_ = syscall2(sys_write, @intFromPtr(msg.ptr), msg.len);
|
||||
}
|
||||
|
||||
fn sleep(ms: u64) void {
|
||||
_ = syscall2(sys_sleep, ms, 0);
|
||||
}
|
||||
|
||||
fn exit(code: u64) noreturn {
|
||||
_ = syscall2(sys_exit, code, 0);
|
||||
unreachable; // the kernel never returns from exit
|
||||
|
|
@ -35,7 +42,7 @@ fn exit(code: u64) noreturn {
|
|||
|
||||
/// Entry. Naked: the kernel enters with rsp 16-aligned, but a SysV function
|
||||
/// expects rsp ≡ 8 (mod 16) on entry (as if reached by `call`) — so re-enter
|
||||
/// the ABI with an actual call. The trap after is unreachable.
|
||||
/// the ABI with an actual call. The trap after is a safety net.
|
||||
pub export fn _start() callconv(.naked) noreturn {
|
||||
asm volatile (
|
||||
\\call init_main
|
||||
|
|
@ -44,8 +51,10 @@ pub export fn _start() callconv(.naked) noreturn {
|
|||
}
|
||||
|
||||
export fn init_main() callconv(.c) noreturn {
|
||||
write("init: hello from user space\n");
|
||||
exit(0);
|
||||
while (true) {
|
||||
write("init: heartbeat\n");
|
||||
sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
/// No runtime to unwind into — report the panic as a nonzero exit code.
|
||||
|
|
|
|||
|
|
@ -18,22 +18,29 @@ PHDRS {
|
|||
}
|
||||
|
||||
SECTIONS {
|
||||
/* The `.large` code model (needed for the >4 GiB image base) emits code and
|
||||
* data into .ltext/.lrodata/.ldata/.lbss; fold those into the matching
|
||||
* permission segment alongside the normal names. */
|
||||
.text ALIGN(4K) : {
|
||||
*(.text .text.*)
|
||||
*(.ltext .ltext.*)
|
||||
} :text
|
||||
|
||||
.rodata ALIGN(4K) : {
|
||||
*(.rodata .rodata.*)
|
||||
*(.lrodata .lrodata.*)
|
||||
} :rodata
|
||||
|
||||
.data ALIGN(4K) : {
|
||||
*(.data .data.*)
|
||||
*(.ldata .ldata.*)
|
||||
} :data
|
||||
|
||||
/* .bss occupies memory but not file space; the loader zeroes the
|
||||
* filesz..memsz gap. */
|
||||
.bss ALIGN(4K) : {
|
||||
*(.bss .bss.*)
|
||||
*(.lbss .lbss.*)
|
||||
*(COMMON)
|
||||
} :data
|
||||
|
||||
|
|
|
|||
|
|
@ -256,28 +256,24 @@ fn kmain(boot_info: *const BootInfo) noreturn {
|
|||
log.checkpoint(cp_running);
|
||||
status("kernel initialised.\n");
|
||||
|
||||
// Hand over to user space: run /sbin/init (read off the boot volume by the
|
||||
// loader) in ring 3. Preemption is off for the run — the M1 user-mode path
|
||||
// publishes *this* core's TSS.rsp0 and must not migrate (the flag is
|
||||
// global, so the system goes cooperative meanwhile; the other cores are
|
||||
// idle). init becomes a real schedulable process in M3.
|
||||
// Hand over to user space: load /sbin/init (read off the boot volume by the
|
||||
// loader) and spawn it as a real ring-3 process, PID 1. It runs on its own
|
||||
// address space, preemptively, alongside the kernel — no cooperative
|
||||
// borrowing. This boot context then becomes the BSP's idle loop.
|
||||
if (boot_info.init_len != 0) {
|
||||
status("starting /sbin/init...\n");
|
||||
const image = @as([*]const u8, @ptrFromInt(danos.physToVirt(boot_info.init_base)))[0..boot_info.init_len];
|
||||
scheduler.setPreemption(false);
|
||||
const code = usermode.runInitElf(image);
|
||||
scheduler.setPreemption(true);
|
||||
if (code) |c| {
|
||||
statusPrint("/sbin/init exited with code {d}.\n", .{c});
|
||||
} else |err| {
|
||||
usermode.spawnProcess(image, 4) catch |err| {
|
||||
statusPrint("/sbin/init failed to load: {s}\n", .{@errorName(err)});
|
||||
}
|
||||
};
|
||||
} else {
|
||||
status("no /sbin/init on the boot volume.\n");
|
||||
}
|
||||
|
||||
status("\nnothing left to do; halting CPU.\n");
|
||||
|
||||
// Become the idle task: drop below every real task and halt until an
|
||||
// interrupt. The timer keeps preempting into init and any other work.
|
||||
scheduler.setPriority(0);
|
||||
status("\nkernel idle; /sbin/init is running.\n");
|
||||
arch.halt();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -746,11 +746,12 @@ fn procWorker() void {
|
|||
sched.exit();
|
||||
}
|
||||
|
||||
/// Real processes: load /sbin/init as a scheduled ring-3 process with its own
|
||||
/// address space, twice in succession. The first run proves a process executes
|
||||
/// on its own page tables (write from CPL 3) and coexists preemptively with a
|
||||
/// kernel task; its exit frees the address space. The second run reuses those
|
||||
/// reclaimed frames — succeeding proves create/exit/teardown/recreate is sound.
|
||||
/// Real processes: load /sbin/init as TWO scheduled ring-3 processes, each with
|
||||
/// its own address space at the same virtual addresses, running concurrently
|
||||
/// with a kernel task. Both must make heartbeat syscalls from CPL 3 — which can
|
||||
/// only happen if each runs on its own page tables (CR3 switched correctly per
|
||||
/// process) and preemption interleaves them with the kernel worker. This is the
|
||||
/// strongest cheap proof of address-space isolation.
|
||||
fn processTest(boot_info: *const BootInfo) void {
|
||||
log("DANOS-TEST-BEGIN: process\n", .{});
|
||||
check("bootloader handed over sbin/init", boot_info.init_len != 0);
|
||||
|
|
@ -759,38 +760,30 @@ fn processTest(boot_info: *const BootInfo) void {
|
|||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(danos.physToVirt(boot_info.init_base)))[0..boot_info.init_len];
|
||||
const expected = "init: hello from user space\n";
|
||||
|
||||
usermode.write_count = 0;
|
||||
usermode.write_cs = 0;
|
||||
proc_worker_run = true;
|
||||
proc_worker_ran = false;
|
||||
sched.spawn(procWorker, 4); // kernel task, same priority as the processes
|
||||
sched.spawn(procWorker, 4); // kernel task at the processes' priority
|
||||
|
||||
var runs: u32 = 0;
|
||||
var last_cs: u64 = 0;
|
||||
var spawned: u32 = 0;
|
||||
if (usermode.spawnProcess(image, 4)) spawned += 1 else |_| {}
|
||||
if (usermode.spawnProcess(image, 4)) spawned += 1 else |_| {}
|
||||
|
||||
// Wait (real time) for several heartbeats across the two processes. Each
|
||||
// process sleeps ~1 s between beats, so a few seconds yields several.
|
||||
sched.setPriority(1); // drop below the workers so they get the cores
|
||||
var round: u32 = 0;
|
||||
while (round < 2) : (round += 1) {
|
||||
usermode.write_len = 0;
|
||||
usermode.write_cs = 0;
|
||||
usermode.exit_code = 0xdead;
|
||||
usermode.spawnProcess(image, 4) catch {
|
||||
log("DANOS-PROC: spawn round {d} failed\n", .{round});
|
||||
continue;
|
||||
};
|
||||
var spins: u64 = 0;
|
||||
while (usermode.exit_code == 0xdead and spins < 5_000_000_000) : (spins += 1) sched.yield();
|
||||
log("DANOS-PROC: round {d} write_len={d} exit_code=0x{x}\n", .{ round, usermode.write_len, usermode.exit_code });
|
||||
if (eql(usermode.write_buf[0..usermode.write_len], expected)) {
|
||||
runs += 1;
|
||||
last_cs = usermode.write_cs;
|
||||
}
|
||||
}
|
||||
const deadline = arch.millis() + 8000;
|
||||
while (usermode.write_count < 4 and arch.millis() < deadline) sched.yield();
|
||||
sched.setPriority(4);
|
||||
proc_worker_run = false;
|
||||
|
||||
check("process ran twice on its own address space (create/exit/recreate)", runs == 2);
|
||||
check("process wrote from CPL 3 (CS = user selector | RPL 3)", last_cs == 0x23);
|
||||
check("a kernel task coexisted with the process (preemption)", proc_worker_ran);
|
||||
log("DANOS-PROC: spawned {d} processes, {d} heartbeats\n", .{ spawned, usermode.write_count });
|
||||
check("both init processes spawned on their own address spaces", spawned == 2);
|
||||
check("processes made repeated heartbeat syscalls (>=4)", usermode.write_count >= 4);
|
||||
check("heartbeats came from CPL 3 (CS = user selector | RPL 3)", usermode.write_cs == 0x23);
|
||||
check("a kernel task coexisted with the processes (preemption)", proc_worker_ran);
|
||||
result();
|
||||
}
|
||||
|
||||
|
|
@ -805,9 +798,11 @@ fn userPfTest() void {
|
|||
log("DANOS-TEST-RESULT: FAIL (user read of kernel memory did not fault)\n", .{});
|
||||
}
|
||||
|
||||
/// The full user-binary path: the bootloader read sbin/init off the boot
|
||||
/// volume and handed it over; load it as a user ELF and run it in ring 3. The
|
||||
/// same call the normal boot path makes — here with teeth.
|
||||
/// The full PID-1 path: the bootloader read sbin/init off the boot volume and
|
||||
/// handed it over; load it as a user ELF and spawn it as a real ring-3 process
|
||||
/// — the same call the normal boot path makes — then confirm it beats. init
|
||||
/// heartbeats forever, so this proves it reaches ring 3, makes repeated syscalls
|
||||
/// (write + sleep), and stays alive rather than exiting.
|
||||
fn initTest(boot_info: *const BootInfo) void {
|
||||
log("DANOS-TEST-BEGIN: init\n", .{});
|
||||
check("bootloader handed over sbin/init", boot_info.init_len != 0);
|
||||
|
|
@ -816,19 +811,26 @@ fn initTest(boot_info: *const BootInfo) void {
|
|||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(danos.physToVirt(boot_info.init_base)))[0..boot_info.init_len];
|
||||
sched.setPreemption(false); // see userTest: pins the run to this core's rsp0
|
||||
const code = usermode.runInitElf(image);
|
||||
sched.setPreemption(true);
|
||||
|
||||
if (code) |c| {
|
||||
check("init loaded, ran, and exited (user ELF path)", true);
|
||||
check("init exited cleanly (code 0)", c == 0);
|
||||
} else |err| {
|
||||
usermode.write_count = 0;
|
||||
const spawned = if (usermode.spawnProcess(image, 4)) true else |err| blk: {
|
||||
log("DANOS-INIT-ERR: {s}\n", .{@errorName(err)});
|
||||
check("init loaded, ran, and exited (user ELF path)", false);
|
||||
}
|
||||
check("init's write arrived intact", eql(usermode.write_buf[0..usermode.write_len], "init: hello from user space\n"));
|
||||
check("write came from CPL 3 (CS = user selector | RPL 3)", usermode.write_cs == 0x23);
|
||||
break :blk false;
|
||||
};
|
||||
check("init loaded and spawned as a process", spawned);
|
||||
|
||||
// Wait (real time) for at least two heartbeats — proving it runs, writes,
|
||||
// and sleeps repeatedly (init sleeps ~1 s between beats).
|
||||
sched.setPriority(1);
|
||||
const deadline = arch.millis() + 8000;
|
||||
while (usermode.write_count < 2 and arch.millis() < deadline) sched.yield();
|
||||
sched.setPriority(4);
|
||||
|
||||
const prefix = "init: heartbeat";
|
||||
const beat_ok = usermode.write_len >= prefix.len and eql(usermode.write_buf[0..prefix.len], prefix);
|
||||
check("init produced repeated heartbeats (>=2)", usermode.write_count >= 2);
|
||||
check("heartbeat text arrived intact", beat_ok);
|
||||
check("heartbeats came from CPL 3 (CS = user selector | RPL 3)", usermode.write_cs == 0x23);
|
||||
check("init is still alive (did not exit)", usermode.exit_code == 0);
|
||||
result();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
//! Ring-3 execution — the isolation track's first rung (M1). Two entry points:
|
||||
//! `run` executes a raw code blob (the user/user-pf test programs), and
|
||||
//! `runInitElf` loads and runs a real user ELF (`/sbin/init`, handed over by
|
||||
//! the bootloader). Both run at CPL 3 in the current (global) page tables:
|
||||
//! frames are mapped user-accessible with W^X (code RO+X, data RW+NX), the CPU
|
||||
//! drops privilege via iretq, and the program talks to the kernel only through
|
||||
//! the `int $0x80` syscall gate. No processes or per-task address spaces yet —
|
||||
//! this proves the privilege mechanisms (user descriptors, TSS.rsp0, the U/S
|
||||
//! page bit, ring transitions) that M3 builds on.
|
||||
//! Ring-3 execution. Two entry points:
|
||||
//! - `spawnProcess` loads a user ELF (`/sbin/init`) into a fresh address space
|
||||
//! and schedules it as a real preemptive ring-3 process on its own page
|
||||
//! tables. This is the production path.
|
||||
//! - `run` executes a raw code blob (the user/user-pf test programs) on the
|
||||
//! *current* kernel context via the borrowed-thread path — a minimal probe of
|
||||
//! the ring-transition mechanisms, kept for the tests.
|
||||
//! Both map frames user-accessible with W^X (code RO+X, data RW+NX); the program
|
||||
//! talks to the kernel only through the syscall instruction (or the int 0x80
|
||||
//! gate). The shared handler is installed once by `init`.
|
||||
//!
|
||||
//! Caller contract (M1 limitations):
|
||||
//! - enter/exit publishes TSS.rsp0 on the *current* core only, so the caller
|
||||
//! must prevent migration for the duration — disable preemption around the
|
||||
//! call (rsp0-per-context-switch arrives with real user tasks in M3).
|
||||
//! - The kernel-side saved context (`user_saved_rsp` in isr.s) is a single
|
||||
//! global: at most one core may be inside user mode at a time.
|
||||
//! Borrowed-path caveat (`run` only): it publishes TSS.rsp0 on the *current*
|
||||
//! core and uses a single global unwind slot (`user_saved_rsp` in isr.s), so the
|
||||
//! caller must disable preemption and only one core may be inside it at a time.
|
||||
//! Real processes (`spawnProcess`) have none of these limits — the scheduler
|
||||
//! maintains rsp0/CR3 per switch.
|
||||
|
||||
const std = @import("std");
|
||||
const elf = std.elf;
|
||||
|
|
@ -58,6 +58,7 @@ pub var ping_count: usize = 0;
|
|||
pub var write_buf: [256]u8 = undefined;
|
||||
pub var write_len: usize = 0;
|
||||
pub var write_cs: u64 = 0;
|
||||
pub var write_count: u64 = 0; // total write syscalls served (for the heartbeat tests)
|
||||
pub var exit_code: u64 = 0;
|
||||
|
||||
/// The M3 syscall surface, dispatched on the saved user rax:
|
||||
|
|
@ -108,10 +109,10 @@ fn syscall(state: *arch.CpuState) void {
|
|||
const len = state.rsi;
|
||||
if (len <= write_buf.len and ptr >= code_virt and ptr <= stack_virt + page_size - len) {
|
||||
const src: [*]const u8 = @ptrFromInt(ptr);
|
||||
const n = @min(len, write_buf.len - write_len);
|
||||
@memcpy(write_buf[write_len..][0..n], src[0..n]);
|
||||
write_len += n;
|
||||
@memcpy(write_buf[0..len], src[0..len]); // keep the latest message
|
||||
write_len = len;
|
||||
write_cs = state.cs;
|
||||
write_count += 1;
|
||||
log.write("DANOS-INIT: ");
|
||||
log.write(src[0..len]);
|
||||
state.rax = len;
|
||||
|
|
@ -128,6 +129,7 @@ fn resetRecords() void {
|
|||
ping_count = 0;
|
||||
write_len = 0;
|
||||
write_cs = 0;
|
||||
write_count = 0;
|
||||
exit_code = 0;
|
||||
}
|
||||
|
||||
|
|
@ -191,12 +193,6 @@ const Segment = struct {
|
|||
}
|
||||
};
|
||||
|
||||
/// Pages mapped for the running init image, for rollback if loading fails
|
||||
/// midway. On success the mappings stay for the system's life — there's no
|
||||
/// address-space teardown until real processes exist (M3).
|
||||
var loaded = [_]struct { virt: u64, frame: u64 }{.{ .virt = 0, .frame = 0 }} ** (max_pages + 1);
|
||||
var loaded_count: usize = 0;
|
||||
|
||||
/// Parse and validate every PT_LOAD before touching memory. Bounds are checked
|
||||
/// against the image and the user region; segments must be page-aligned,
|
||||
/// non-overlapping, and W^X (R-only is fine — linkers may emit a headers-only
|
||||
|
|
@ -261,32 +257,6 @@ fn parseSegments(image: []const u8, segs: *[max_segments]Segment) InitError!stru
|
|||
return error.BadEntry;
|
||||
}
|
||||
|
||||
/// Map one page of a segment: fresh frame, zero + copy through the identity
|
||||
/// mapping (the user mapping may be read-only), then the user-visible mapping
|
||||
/// with the segment's W^X permissions. Records the page for rollback.
|
||||
fn loadPage(image: []const u8, seg: Segment, page_index: u64) InitError!void {
|
||||
const frame = pmm.alloc() orelse return error.OutOfMemory;
|
||||
const dst: [*]u8 = @ptrFromInt(danos.physToVirt(frame));
|
||||
@memset(dst[0..page_size], 0);
|
||||
const page_off = page_index * page_size;
|
||||
if (page_off < seg.filesz) {
|
||||
const n = @min(page_size, seg.filesz - page_off);
|
||||
@memcpy(dst[0..n], image[seg.off + page_off ..][0..n]);
|
||||
}
|
||||
const virt = seg.vaddr + page_off;
|
||||
arch.mapUserPage(virt, frame, seg.writable, seg.executable);
|
||||
loaded[loaded_count] = .{ .virt = virt, .frame = frame };
|
||||
loaded_count += 1;
|
||||
}
|
||||
|
||||
fn unloadAll() void {
|
||||
for (loaded[0..loaded_count]) |p| {
|
||||
arch.unmapPage(p.virt);
|
||||
pmm.free(p.frame);
|
||||
}
|
||||
loaded_count = 0;
|
||||
}
|
||||
|
||||
/// Load one page of a segment into address space `pml4`: a fresh frame, zeroed
|
||||
/// and filled through the physmap, mapped user-accessible with the segment's W^X.
|
||||
/// On a later failure the whole address space is torn down, which frees every
|
||||
|
|
@ -304,11 +274,11 @@ fn loadPageInto(pml4: u64, image: []const u8, seg: Segment, page_index: u64) Ini
|
|||
}
|
||||
|
||||
/// Load a user ELF image into a fresh address space and spawn it as a scheduled
|
||||
/// ring-3 process at `priority`. Unlike `runInitElf` (the borrowed-thread test
|
||||
/// path), this returns immediately — the process runs preemptively on its own
|
||||
/// page tables alongside everything else, and its exit is handled by the syscall
|
||||
/// layer. The whole build (address space + ELF load + task) runs under the
|
||||
/// kernel lock so it appears atomically and can't race pmm/heap on another core.
|
||||
/// ring-3 process at `priority`. Returns immediately — the process runs
|
||||
/// preemptively on its own page tables alongside everything else, and its exit
|
||||
/// is handled by the syscall layer. The whole build (address space + ELF load +
|
||||
/// task) runs under the kernel lock so it appears atomically and can't race
|
||||
/// pmm/heap on another core.
|
||||
pub fn spawnProcess(image: []const u8, priority: u3) InitError!void {
|
||||
var segs: [max_segments]Segment = undefined;
|
||||
const parsed = try parseSegments(image, &segs);
|
||||
|
|
@ -328,27 +298,3 @@ pub fn spawnProcess(image: []const u8, priority: u3) InitError!void {
|
|||
if (!sched.spawnUserLocked(pml4, parsed.entry, stack_virt + page_size, priority))
|
||||
return error.OutOfMemory;
|
||||
}
|
||||
|
||||
/// Load a user ELF image, run it in ring 3 from its entry point, and return its
|
||||
/// exit code. Same caller contract as `run` (preemption off, one core). On
|
||||
/// success the user mappings are left in place — teardown comes with real
|
||||
/// processes (M3); on a loading error everything is rolled back.
|
||||
pub fn runInitElf(image: []const u8) InitError!u64 {
|
||||
var segs: [max_segments]Segment = undefined;
|
||||
const parsed = try parseSegments(image, &segs);
|
||||
|
||||
loaded_count = 0;
|
||||
errdefer unloadAll();
|
||||
for (segs[0..parsed.count]) |seg| {
|
||||
for (0..seg.pages()) |i| try loadPage(image, seg, i);
|
||||
}
|
||||
const stack_frame = pmm.alloc() orelse return error.OutOfMemory;
|
||||
arch.mapUserPage(stack_virt, stack_frame, true, false); // RW + NX
|
||||
loaded[loaded_count] = .{ .virt = stack_virt, .frame = stack_frame };
|
||||
loaded_count += 1;
|
||||
|
||||
resetRecords();
|
||||
arch.enterUser(sched.currentCpuIndex(), parsed.entry, stack_virt + page_size);
|
||||
arch.enableInterrupts(); // the exit arrived through an interrupt gate
|
||||
return exit_code;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue