The linker-qemu.ld script is responsible for arranging the different sections of the kernel into the final executable and specifying their memory locations.
OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80200000;
SECTIONS
{
. = BASE_ADDRESS;
skernel = .;
stext = .;
.text : {
*(.text.entry)
. = ALIGN(4K);
strampoline = .;
*(.text.trampoline);
. = ALIGN(4K);
*(.text .text.*)
}
. = ALIGN(4K);
etext = .;
srodata = .;
.rodata : {
*(.rodata .rodata.*)
*(.srodata .srodata.*)
}
. = ALIGN(4K);
erodata = .;
sdata = .;
.data : {
*(.data .data.*)
*(.sdata .sdata.*)
}
. = ALIGN(4K);
edata = .;
sbss_with_stack = .;
.bss : {
*(.bss.stack)
sbss = .;
*(.bss .bss.*)
*(.sbss .sbss.*)
}
. = ALIGN(4K);
ebss = .;
ekernel = .;
/DISCARD/ : {
*(.eh_frame)
}
}After the hardware is powered on, the bootloader (in our case, RustSBI) is loaded. The bootloader's job is to load the kernel image into memory and prepare the environment for its execution. It loads the kernel to the BASE_ADDRESS (0x80200000), switches the CPU to Supervisor mode, and then jumps to the kernel's entry point, _start, which is defined in entry.asm and specified in the linker script.
The _start entry point performs minimal setup (like initializing the kernel stack) and then transfers control to the main Rust function, rust_main. This function orchestrates the initialization of all kernel subsystems in a specific order:
clear_bss: Clears the.bsssection, which contains uninitialized static variables, ensuring they start with a zero value.mm::init: Initializes the memory management subsystem, including the physical frame allocator and the kernel's page table.task::add_initproc: Creates the very first user process,initproc, and adds it to the process manager's ready queue.trap::init: Initializes the trap handling mechanism, setting up the trap vector to handle interrupts, exceptions, and system calls.trap::enable_timer_interrupt: Enables supervisor-level timer interrupts, which are crucial for preemptive multitasking.timer::set_next_trigger: Sets the first timer interrupt. This kicks off the time-sharing scheduler.task::run_tasks: Starts the scheduler, which switches to the first available process (initproc) and begins its execution.
Once the kernel is initialized, it runs the first user-space program, initproc. This process is responsible for setting up the initial user environment, primarily by launching the user shell. Here is the workflow of initproc:
fn main() -> i32 {
if fork() == 0 {
exec("user_shell\0");
} else {
loop {
let mut exit_code: i32 = 0;
let pid = wait(&mut exit_code);
if pid == -1 {
yield_();
continue;
}
println!(
"[initproc] Released a zombie process, pid={}, exit_code={}",
pid,
exit_code,
);
}
}
0
}- It calls
fork()to create a child process. - The child process immediately calls
exec("user_shell")to replace its own program image with the user shell. - The original
initproc(the parent) enters an infinite loop. In this loop, it continuously callswait()to clean up any terminated (zombie) child processes. This makesinitprocthe ultimate parent process, responsible for reaping any orphaned processes in the system.