The make qemu builds two main things:
kernel/kernel = xv6 kernel binary
fs.img = disk image containing /init, /sh, /ls, etc.Then QEMU starts a fake RISC-V machine:
64-bit RISC-V
128 MB RAM
3 CPUs by default
UART console
virtio disk backed by fs.imgQEMU loads the kernel at:
0x80000000and jumps to the first kernel instruction.
Boot path:
kernel/entry.SSets up a stack, then calls:
kernel/start.cstart.c prepares the RISC-V CPU: privilege mode, interrupts, timer, trap delegation. Then it jumps into:
kernel/main.cmain.c initializes the kernel:
console
printf
physical memory allocator
kernel page table
process table
trap handling
interrupt controller
buffer cache
inode table
file table
virtio disk
first user processThen xv6 starts the scheduler.
The first process is not /init yet. It is tiny built-in code called initcode.
That tiny code runs in user mode and does:
exec("/init")Now the kernel loads /init from fs.img.
That /init file exists because mkfs put user/_init into the disk image before boot.
Then:
user/init.cruns. It opens the console and starts the shell:
exec("sh")Then:
user/sh.cruns, and you see the xv6 shell.
Compact flow:
make qemu
↓
build kernel/kernel and fs.img
↓
QEMU loads kernel at 0x80000000
↓
entry.S sets stack
↓
start.c prepares RISC-V supervisor mode
↓
main.c initializes kernel subsystems
↓
proc.c creates first process
↓
scheduler runs initcode
↓
initcode execs /init from fs.img
↓
init starts sh
↓
shell runs commandsThat’s the boot sequence.
entry.S
- Qemu puts this code into the kernel’s executable code section
- The linker scripts puts this code at the beginning of the kernel image, at
0x80000000 - the
_entrycode gives each CPU/hart its own stack, then call start() - Qemu does not know the name
_entry. QEMU jumps to address0x80000000; the linker made sure_entrylives there
.section .text
.global _entry
_entry:
la sp, stack0
li a0, 1024*4
csrr a1, mhartid
addi a1, a1, 1
mul a0, a0, a1
add sp, sp, a0
call start
spin:
j spin
start.c
start is the bridge between machine mode (M-mode) and supervisor model (S-mode). When entry.S calls it, the CPU is in M-mode - the highest privilege on RISC-V, with unrestricted access to everything. The job of start() is to configure the hardware so that the kernel can run safely in S-model, then drop down to it.
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "defs.h"
void main();
void timerinit();
__attribute__ ((aligned (16))) char stack0[4096 * NCPU];
void start()
{
// set M Previous Privilege mode to Supervisor, for mret.
unsigned long x = r_mstatus();
x &= ~MSTATUS_MPP_MASK;
x |= MSTATUS_MPP_S;
w_mstatus(x);
// set M Exception Program Counter to main, for mret.
// requires gcc -mcmodel=medany
w_mepc((uint64)main);
// disable paging for now.
w_satp(0);
// delegate all interrupts and exceptions to supervisor mode.
w_medeleg(0xffff);
w_mideleg(0xffff);
w_sie(r_sie() | SIE_SEIE | SIE_STIE);
// configure Physical Memory Protection to give supervisor mode
// access to all of physical memory.
w_pmpaddr0(0x3fffffffffffffull);
w_pmpcfg0(0xf);
// ask for clock interrupts.
timerinit();
// keep each CPU's hartid in its tp register, for cpuid().
int id = r_mhartid();
w_tp(id);
// switch to supervisor mode and jump to main().
asm volatile("mret");
}
// ask each hart to generate timer interrupts.
void
timerinit()
{
// enable supervisor-mode timer interrupts.
w_mie(r_mie() | MIE_STIE);
// enable the sstc extension (i.e. stimecmp).
w_menvcfg(r_menvcfg() | (1L << 63));
// allow supervisor to use stimecmp and time.
w_mcounteren(r_mcounteren() | 2);
// ask for the very first timer interrupt.
w_stimecmp(r_time() + 1000000);
}