overview
qemu virt machine drops us into machine mode (m-mode). the first step usually for most kernels is to get into supervisor mode (s-mode).
at a higher level, switching to supervisor mode from here involves:
- delegating necessary traps to s-mode and setup handler
- disabling physical memory protection for s-mode
- setting previous privilege (
MPP) to s-mode - setting return address (
mepcregister) to kernel main function - returning to "previous privilege" (s-mode here) using
mretinstruction
setting up the stack pointer
make sure the linker script has space reserved for the stack pointer and global pointer:
.stack (NOLOAD) : ALIGN(16)
{
__stack_bottom = .;
. += 16K;
__stack_top = .;
. += 4K;
__global_pointer = .;
}16K stack is enough for a small kernel.
la sp, __stack_top
la gp, __global_pointerglobal pointer is used to offset global variables, and i recommend setting it up anyway.
machine trap delegations
all traps handled by m-mode at this stage, but for our kernel we want them to be handled by s-mode. this can be done by redirecting necessary traps using delegation registers.
traps - exceptions
exceptions are synchronous and are caused by the current instruction being executed. delegation of these traps is done by medeleg register.
each bit in the medeleg, if set, delegates a particular exception cause to s-mode. exception causes include:
- load page fault
- store page fault
- illegal instruction
- s-mode ecall
- u-mode ecall
- etc...
li t0, 0xffff
csrw medeleg, t0this delegates all base exceptions to s-mode, you might want to skip a few exceptions like m-mode ecalls, s-mode ecalls later, but this is fine for now.
traps - interrupts
traps are asynchronous, and are caused by hardware. there are 3 main types, with variants for each privilege level:
- MSIP/SSIP: software interrupts, ex: inter-cpu interrupts
- MTIP/STIP: timer interrupts
- MEIP/SEIP: external interrupts, ex: PLIC
li t0, 0x222
csrw mideleg, t0this delegates all interrupt handling to s-mode. u-mode interrupts are extremely rare, so you can safely ignore them unless needed.
setting up trap handler
the stvec csr should contain the s-mode trap handler address:
la t0, stvec_handler
csrw stvec, t0i also like to zero these registers:
csrw sscratch, 0
csrw satp, 0to enable all interrupts in s-mode:
csrw sie, 0x7disabling physical memory protection
i had a lot of trouble figuring out why i couldnt access memory in s-mode, turns out m-mode mommy needs to disable physical memory protection for s-mode:
li t0, -1
csrw pmpaddr0, t0
li t0, 0x0F
csrw pmpcfg0, t0here, -1 enables all bits, and since pmpaddrN stores addr >> 2, thats the highest memory address.
and pmpcfg0 in above code has 0x0F which is RWX on all memory in TOR (top-of-range) mode, so paired together they give s-mode RWX access on all available memory.
preparing to jump to s-mode
set the MPP bits in mstatus register to s-mode (01):
csrr t0, mstatus
li t1, ~(3 << 11) # clear MPP so the first MPP bit is always off
and t0, t0, t1
li t1, (1 << 11) # set MPP bits to s-mode
or t0, t0, t1
csrw mstatus, t0then store the kernel main function address in mepc register:
la t0, kmain
csrw mepc, t0kmain should be a noreturn function if using c or other languages.
jumping to s-mode
mretas simple as that, if all goes well then congrats! you are in s-mode!
testing
im using this linker.ld
SECTIONS
{
. = 0x80000000;
.text
{
*(.text .text.*)
}
.stack (NOLOAD) : ALIGN(16)
{
__stack_bottom = .;
. += 16K;
__stack_top = .;
. += 4K;
__global_pointer = .;
}
}with this Makefile
AS=riscv64-elf-as
LD=riscv64-elf-ld
ASFLAGS=
LDFLAGS=-Tlinker.ld -nostdlib
boot.elf: boot.o
$(LD) $(LDFLAGS) $^ -o $@
boot.o: boot.s
$(AS) $(ASFLAGS) $< -o $@full code
.section .init
_init:
la sp, __stack_top
la gp, __global_pointer
li t0, 0xffff
csrw medeleg, t0
li t0, 0x222
csrw mideleg, t0
la t0, stvec_handler
csrw stvec, t0
csrw sscratch, 0
csrw satp, 0
csrw sie, 0x7
li t0, -1
csrw pmpaddr0, t0
li t0, 0x0F
csrw pmpcfg0, t0
csrr t0, mstatus
li t1, ~(3 << 11) # clear MPP so the first MPP bit is always off
and t0, t0, t1
li t1, (1 << 11) # set MPP bits to s-mode
or t0, t0, t1
csrw mstatus, t0
la t0, kmain
csrw mepc, t0
mret
stvec_handler:
csrr t0, sepc
addi t0, t0, 4
add a1, a1, 1
li a0, 0xdeadbeef
csrw sepc, t0
sret
kmain:
li a0, 0xdeadbeef
.loop:
ecall
j .loopif this works we should see 0xdeadbeef in a0 register and a1 should be ever incrementing.
results
qemu-system-riscv64 -machine virt -bios none -kernel boot.elf -monitor stdio -display noneafter running for like 5 seconds, on info registers, we see:
x10/a0 00000000deadbeef
x11/a1 000000008c6672f0it works!
you might want to revisit this page and make delegations or memory stricter, perhaps, but this is perfect as-is if you ask me.
resources
- https://docs.riscv.org/reference/isa/priv/machine.html
- https://github.com/riscv-software-src/opensbi/blob/master/lib/sbi/sbi_hart.c
TIP: the opensbi implementation i linked was a godsend to me to switch from m-mode to s-mode consider going thru it