Kernel Project Map, Boot Flow, and Machine Contract
LESSON
Kernel Project Map, Boot Flow, and Machine Contract
By the end of this lesson, you will be able to...
Define the machine assumptions a small kernel may rely on before its main code runs.
Trace a boot from machine entry to one observable
kernel_readystate.Review a boot design by checking ownership, evidence, failure boundaries, and portability costs.
Idea in one sentence: A kernel can start reliably only when the code and the machine agree on where execution begins, which state already exists, and who must create the rest.
Core Insight
Suppose you have compiled a small RISC-V kernel named kernel.elf. QEMU accepts the image. The terminal stays blank.
The kernel may have failed before reaching kernel_main. It may also be running correctly while writing to the wrong UART address. The CPU may have entered in a privilege mode your first instruction did not expect. The code may have called a function before creating a valid stack. Even a successful build says nothing about these boundaries.
The tempting plan is simple:
void kernel_main(void) {
console_init();
memory_init();
scheduler_init();
}
But a normal C function already assumes a working execution environment. It expects a stack, a valid program counter, a matching instruction set, usable memory, and rules for returning or trapping. No operating system exists yet to provide those things.
The first design task is therefore not “write the scheduler.” It is “state the machine contract.”
The Promise We Need to Keep
For this lesson, the target is deliberately small:
Boot one RISC-V hart on QEMU's
virtmachine, establish the state required by C code, print one trustworthy readiness message, and stop in a known state.
A hart is a RISC-V hardware thread: one independent instruction-execution context. We will use one hart now. Multi-hart startup would introduce shared initialization and synchronization before we have built the tools to explain them.
Our chosen teaching contract is:
| Boundary | Decision for this kernel |
|---|---|
| Machine | QEMU RISC-V virt platform |
| CPU | One 64-bit RISC-V hart |
| Firmware | No default firmware; the kernel owns the early handoff |
| Entry privilege | Machine mode |
| Kernel load and link address | Physical address 0x80000000 |
| Early memory | A fixed RAM range known to the build and launch command |
| Early device | The virt UART for serial evidence |
| First symbol | _start, written in assembly |
| First C function | boot_main, called only after a stack exists |
| Success evidence | A serial line plus a final known halt loop |
These are not universal RISC-V facts. They are design decisions tied to one launch environment. If firmware enters the kernel in supervisor mode, or another board places RAM elsewhere, the contract changes.
That distinction is central:
Plain meaning:
The machine contract is the list of facts the kernel assumes are already true at entry.
In this scenario:
The kernel assumes one RISC-V hart starts at _start in machine mode, with the image linked where QEMU loaded it.
Technical name:
This is a boot protocol or machine handoff contract. It sits below the later user-kernel ABI. The machine contract gets the kernel running; the syscall ABI will later define how user programs ask the running kernel for work.
The Naive Design
A first project often jumps directly from the linker to C:
kernel.elf -> kernel_main() -> initialize everything
This picture hides every precondition.
Consider just one C call. The compiler may generate instructions that save a return address and local variables on the stack. If the stack pointer contains an arbitrary value, the first store can overwrite code, touch an unmapped address, or disappear into a device region.
Now consider a global variable:
static bool memory_ready;
C expects uninitialized static storage to begin as zero. That storage normally lives in the BSS section. If the boot path does not arrange or verify that zero state, memory_ready can appear true before memory initialization has happened.
The simple diagram also hides placement. A linked address is not a suggestion. If the linker emits references for code at 0x80000000 but the launcher places the image elsewhere, absolute addresses and early control flow can be wrong before the console works.
So the naive design fails because it treats hidden state as if it were guaranteed state.
Design Alternatives
There are several valid ways to divide boot responsibility.
Alternative A: the kernel enters directly in machine mode
The launch environment loads the image, then the kernel configures early privileged state itself.
This makes the teaching path short. The kernel can see the machine boundary directly. It also couples the kernel to more platform and privilege details.
Alternative B: firmware hands control to a supervisor-mode kernel
Firmware such as OpenSBI performs machine-mode setup and passes control to the kernel in supervisor mode. The handoff may include a hart identifier and a pointer to a device tree that describes memory and devices.
This separation is closer to many real RISC-V systems. It reduces what the kernel owns at the highest privilege level. It also means the kernel must obey a larger firmware contract before it can do anything useful.
Alternative C: use a richer boot environment
A bootloader or UEFI-like environment can load files, provide tables, and standardize parts of the handoff. That improves portability across machines that support the same protocol. It adds components, configuration, and failure surfaces.
For this small kernel, we choose Alternative A. The choice exposes the early mechanism with the fewest moving parts. It is a teaching trade-off, not a claim that production kernels should bypass firmware.
A Better Boundary
We can now split the project by responsibility:
linker script
decides addresses and exports section boundaries
entry assembly: _start
establishes state required before C is safe
boot code: boot_main
validates the contract and prepares privileged execution
kernel initialization: kernel_main
constructs kernel subsystems in dependency order
The linker script might express the placement boundary like this:
ENTRY(_start)
SECTIONS {
. = 0x80000000;
.text : { *(.text.entry) *(.text .text.*) }
.rodata : { *(.rodata .rodata.*) }
.data : { *(.data .data.*) }
__bss_start = .;
.bss : { *(.bss .bss.*) *(COMMON) }
__bss_end = .;
}
The exact syntax matters less than the promises it makes:
_startis the entry symbol.- code is linked for the agreed physical address.
- BSS boundaries are visible to boot code.
- later memory setup can distinguish the kernel image from free RAM.
The entry code then creates the minimum safe environment:
_start:
disable interrupts
read hart id
park any hart other than hart 0
set stack pointer to boot_stack_top
clear memory from __bss_start to __bss_end
call boot_main
enter a known halt loop if boot_main returns
Notice the ownership rule. Assembly owns the state needed to call C. C must not quietly assume that assembly completed work that is missing from the contract.
Worked Boot Ledger
A boot ledger makes the handoff inspectable. Each row records who owns a transition, which state it consumes, what it creates, and how we can observe failure.
| Stage | Owner | Input state | State created | Evidence or failure signal |
|---|---|---|---|---|
| 0 | QEMU virt |
launch arguments and kernel.elf |
CPU reset path, RAM, emulated devices, loaded image | QEMU starts; debugger can inspect the reset vector |
| 1 | machine handoff | reset state | control reaches _start at the linked entry |
breakpoint at _start; wrong placement gives no valid entry |
| 2 | _start |
hart id and writable RAM | one selected boot hart and a valid stack pointer | stack address falls inside reserved boot-stack memory |
| 3 | _start |
linker-exported BSS bounds | zeroed static storage | sentinel globals have their specified zero value |
| 4 | boot_main |
safe C environment | early serial output and checked machine assumptions | boot: entry contract accepted appears once |
| 5 | privilege setup | machine-mode control | defined interrupt state and a deliberate next privilege boundary | CSR inspection matches the design |
| 6 | kernel_main |
validated handoff | allocator metadata and later subsystem state | ordered log markers identify the last completed step |
| 7 | final boot check | initialized minimum kernel | kernel_ready plus a known idle or halt state |
output is stable and does not return through an invalid call path |
Here is a compact implementation sketch:
void boot_main(void) {
early_uart_init();
early_puts("boot: entry contract accepted\n");
require(current_hart_id() == 0);
require((uintptr_t)_start == KERNEL_LINK_ADDRESS);
require(bss_is_zero());
prepare_privilege_transition();
kernel_main();
halt_with_reason("kernel_main returned");
}
The require checks do not prove the whole machine is correct. They convert some silent assumptions into visible failures. That is already a major improvement.
Now contrast two runs.
In the good run, the debugger reaches _start, the stack points into reserved memory, the BSS sentinel is zero, and the UART prints each marker once. The ledger reaches stage 7.
In a bad run, the debugger reaches _start, but no serial text appears. That evidence rules out “the CPU never executed the image.” It does not prove boot_main failed. The UART address, clock assumptions, or output code may be wrong. The next probe should inspect the program counter after early_uart_init or write a marker into known RAM.
So far, we have replaced “the screen is blank” with a sequence of owned transitions and testable claims. This matters because early boot has almost no services. The design must create its own evidence.
Check Your Understanding
Check: Why must _start set the stack before calling an ordinary C function?
Think first, then reveal.
Answer: The compiled function may immediately save registers or local data through the stack pointer. Without a valid reserved stack, the first memory access can corrupt another region or fault before any diagnostic path exists.
Check: A debugger stops at _start, but the serial console is blank. Which claim is supported?
Think first, then reveal.
Answer: The image is executing at least as far as _start. The evidence does not show that console initialization is correct or that boot_main was reached. Inspect the next transition instead of restarting the whole diagnosis.
Trade-offs and Limits
An explicit machine contract improves repeatability. It also creates coupling.
- A fixed link address makes the first kernel easier to inspect, but the image will not automatically run on a board with a different RAM map.
- Direct machine-mode entry exposes every early transition, but it gives the kernel responsibility that firmware often owns in a production design.
- Starting one hart avoids early races, but it does not teach multi-hart release, per-hart stacks, or one-time initialization.
- Serial markers provide useful evidence, but a broken UART path can make a healthy CPU look silent.
- Early assertions localize some failures, but they do not validate caches, interrupts, virtual memory, or device behavior that has not been initialized yet.
The boundary is visible when a new platform forces many unrelated boot-code changes. That signal suggests the contract is too implicit or too mixed with platform-specific code. A small platform adapter or a firmware handoff may then be worth the extra layer.
Common Confusions
Confusion: the emulator removes hardware assumptions
Why it is tempting:
QEMU feels like a controlled software process, so the guest machine can seem generic.
Better model:
QEMU emulates a specific board model. CPU features, RAM placement, UART addresses, interrupt controllers, and firmware options still form a machine contract.
Confusion: the linker decides where the image is loaded
Why it is tempting:
The linker script contains an address, so it appears to control placement.
Better model:
The linker assigns addresses inside the image. The launcher or firmware places the image. Both sides must agree.
Confusion: the boot contract is the syscall ABI
Why it is tempting:
Both are low-level interfaces involving registers and control transfer.
Better model:
The boot contract is between the machine or firmware and the kernel. The syscall ABI is between a running user program and a running kernel. Lesson 008 will build that later boundary.
Practice: Change the Handoff
Review this modified design:
- OpenSBI enters the kernel in supervisor mode.
- Register
a0carries the hart identifier. - Register
a1carries a device-tree pointer. - Two harts may enter near the same time.
- The kernel should discover RAM and UART addresses instead of compiling them as constants.
Rewrite stages 1–5 of the boot ledger for this handoff.
A good answer should mention:
- firmware, not the kernel, owns the earlier machine-mode setup
- the entry code must preserve and validate
a0anda1 - one hart must own global initialization while each hart receives a separate stack
- device-tree parsing replaces some fixed platform constants
- evidence must distinguish “waiting for the boot hart” from “failed before entry”
- supervisor-mode code must not execute instructions reserved for machine mode
Resources
- [BOOK] xv6: a simple, Unix-like teaching operating system — Focus: Follow the RISC-V machine model, kernel image, entry code, and early initialization as one inspectable teaching system.
- [DOC] QEMU
virtGeneric Virtual Platform — Focus: Check which CPU, memory-discovery, interrupt, UART, and firmware facilities the emulated board provides. - [REFERENCE] The RISC-V Instruction Set Manual, Volume II: Privileged Architecture — Focus: Use the privilege modes, reset behavior, status registers, and trap-control rules as the architectural boundary.
Key Takeaways
- A kernel's first dependency is a machine contract: entry address, privilege, memory, hart, device, and handoff assumptions must agree.
_startowns the state required before C is safe, especially hart selection, stack creation, and BSS initialization.- A linker address and a loader placement are separate decisions that must match.
- A boot ledger turns silent startup assumptions into owned transitions, evidence, and local failure hypotheses.
- A narrow teaching contract makes boot easier to inspect, but portability requires moving platform assumptions behind a stronger handoff boundary.