RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / src / boot.rs
4526 bytes ยท raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//! Stage 21: boot contract v2 -- which program becomes pid 1.
//!
//! If `fs::mount_disks` found a GPT "Linux root (x86-64)" partition with
//! ext2 and mounted it at "/" ([`fs::root_on_disk`]), the kernel starts the
//! first of [`INIT_PATHS`] that runs, the same order Linux tries, as pid 1:
//!
//! | program      | argv                                   |
//! |--------------|----------------------------------------|
//! | /sbin/init   | `init` (+ `selftest` when headless)    |
//! | /bin/init    | `init` (+ `selftest` when headless)    |
//! | /bin/sh      | `sh` (+ `/scripts/selftest.sh` when headless) |
//!
//! The environment is [`V2_ENV`] (+ `RUSTBELT_HEADLESS=1` when headless).
//! There's no boot loader command line (bootloader 0.9 has none), so the
//! knobs are build time, like the `headless` feature itself:
//!   * `cargo build --features headless`: the headless argv/env above;
//!   * `RUSTBELT_INIT_ARGS="a b"`: extra arguments after those;
//!   * `RUSTBELT_INIT_ENV="K=V K2=V2"`: extra environment variables (one
//!     with a name already there replaces it, e.g. `PATH=...`).
//! (Both are read with `option_env!`, so changing them rebuilds the kernel.)
//!
//! The kernel does not restart pid 1. When it exits, its status ends the
//! QEMU run through isa-debug-exit: exit(n) writes 0x10 + n, so QEMU exits
//! with 33 + 2n (33 = exit 0 = the tests' "pass", 35 = exit 1, ...). A
//! status above 111 (it wouldn't fit in QEMU's 8-bit exit code) or death
//! by a signal writes 0x11 (35).
//!
//! Without a root partition nothing here is used: main.rs runs the old
//! shell supervisor (boot contract v1) exactly as before.

use crate::errno::Errno;
use crate::fs;
use crate::process::{self, ExitStatus, Process, SpawnArgs};
use alloc::{string::String, sync::Arc, vec::Vec};

/// Tried in this order; the first that starts becomes pid 1.
pub const INIT_PATHS: [&str; 3] = ["/sbin/init", "/bin/init", "/bin/sh"];

/// pid 1's environment under contract v2.
pub const V2_ENV: [&str; 3] = ["PATH=/sbin:/bin:/usr/bin:/mnt/disk0/bin", "HOME=/", "TERM=linux"];

/// argv and environment for `path` (one of [`INIT_PATHS`]).
pub fn init_args(path: &str, headless: bool) -> SpawnArgs {
    let sh = path.ends_with("/sh");
    let mut args = SpawnArgs::kernel(if sh { "sh" } else { "init" });
    if headless {
        args.args.push(String::from(if sh { "/scripts/selftest.sh" } else { "selftest" }));
    }
    args.args.extend(option_env!("RUSTBELT_INIT_ARGS").unwrap_or("").split_whitespace().map(String::from));
    args.env = V2_ENV.iter().map(|s| String::from(*s)).collect();
    if headless {
        args.env.push(String::from("RUSTBELT_HEADLESS=1"));
    }
    for var in option_env!("RUSTBELT_INIT_ENV").unwrap_or("").split_whitespace() {
        let key = var.split('=').next().unwrap_or(var);
        args.env.retain(|e| e.split('=').next() != Some(key));
        args.env.push(String::from(var));
    }
    args
}

/// Start pid 1: the first of [`INIT_PATHS`] that starts. Returns its path
/// and process, or the last error if none did. `log` gets a line for every
/// one that was tried and failed.
pub fn start_init(headless: bool, log: &mut Vec<String>) -> Result<(&'static str, Arc<Process>), Errno> {
    let mut last = Errno::ENOENT;
    for path in INIT_PATHS {
        match process::spawn_path(path, init_args(path, headless)) {
            Ok(p) => return Ok((path, p)),
            Err(e) => {
                if e != Errno::ENOENT {
                    log.push(alloc::format!("{}: can't start it: {:?}", path, e));
                }
                last = e;
            }
        }
    }
    Err(last)
}

/// What to write to isa-debug-exit for pid 1's exit status (see above).
pub fn qemu_exit_value(status: ExitStatus) -> u32 {
    match status {
        ExitStatus::Exited(n) if (0..=111).contains(&n) => 0x10 + n as u32,
        _ => 0x11,
    }
}

/// End the QEMU run with pid 1's exit status.
pub fn exit_qemu_with(status: ExitStatus) {
    use x86_64::instructions::port::Port;
    unsafe { Port::new(0xf4).write(qemu_exit_value(status)) };
}

/// Run contract v2's pid 1 and wait for it. Returns its status (Err: no
/// init could be started).
pub fn run_init(headless: bool, log: &mut Vec<String>) -> Result<(&'static str, ExitStatus), Errno> {
    let (path, p) = start_init(headless, log)?;
    let status = process::wait(&p);
    Ok((path, status))
}

/// Is contract v2 in use (a root partition was mounted at "/")?
pub fn v2() -> bool {
    fs::root_on_disk()
}