Kernel layout
At the end of the Hello, World! section, our kernel is a compact binary located right at the beginning of physical memory. In order to provide isolation from user programs later, we place the kernel code to somewhere else. One can of course keep kernel here and use another virtual page, plus some tranpoline tricks, to achieve this. But we've chosen to move kernel because of simplicity and our design choice.
Below shows typical memory layout for a running kernel:
PHYSICAL MEMORY VIRTUAL MEMORY VIRTUAL MEMORY
(KERNEL) (USER PROGRAM)
┌────────────────────────┐ ┌────────────────────────┐ 0xFFFFFFFFFFFFFFFF
· · · ·
├────────────────────────┤ ├────────────────────────┤
│ │ │ │
│ KERNEL │ │ KERNEL │
│ │ │ │
├────────────────────────┤ ├────────────────────────┤ 0xFFFFFFC080200000
· · · ·
┌────────────────────────┐ · · │ │
│ │ · · │ │
│ PAGE │ · · │ │
│ │ · · │ USER PROGRAM │
├────────────────────────┤ · · │ │
│ │ · · │ │
│ KERNEL │ · · │ │
│ │ · · │ │
├────────────────────────┤ ├────────────────────────┤ ├────────────────────────┤ 0x80200000
│ OPEN SBI │ │ OPEN SBI │ · ·
├────────────────────────┤ ├────────────────────────┤ · · 0x80000000
· · · · · ·
├────────────────────────┤ ├────────────────────────┤ · ·
│ MMIO │ │ MMIO │ · ·
├────────────────────────┤ ├────────────────────────┤ · · 0x10001000
· · · · · ·
└────────────────────────┘ └────────────────────────┘ └────────────────────────┘ 0x0
The kernel runs at the higher address space 0xFFFFFFC080200000
, shifting from its physical address 0x80200000
by 0xFFFFFFC000000000
. Below we define some utilities:
File: src/mem.rs
#![allow(unused)] fn main() { pub const BASE_LMA: usize = 0x0000000080000000; pub const BASE_VMA: usize = 0xFFFFFFC080000000; pub const OFFSET: usize = BASE_VMA - BASE_LMA; /// Is `va` a kernel virtual address? pub fn is_vaddr<T>(va: *mut T) -> bool { va as usize >= BASE_VMA } /// From virtual to physical address. pub fn vtop<T>(va: *mut T) -> usize { assert!(is_vaddr(va as *mut T)); va as usize - OFFSET } /// From physical to virtual address. pub fn ptov<T>(pa: usize) -> *mut T { assert!(!is_vaddr(pa as *mut T)); (pa + OFFSET) as *mut T } }
Link the kernel
In the previous linker script, anything is linked at its physical address - the linker assumes the code runs at where it was compiled. But since we are running the whole kernel at the higher address space, the pc
is shifted from where it was compiled - when weird things could happen. To resolve this, we use the AT keyword to specify load address that is different from virtual address. In this linker script, every virtual address is shifted by 0xFFFFFFC000000000
, but shifted back for load address:
File: src/linker.ld
BASE_LMA = 0x0000000080000000;
BASE_VMA = 0xFFFFFFC080000000;
OFFSET = BASE_VMA - BASE_LMA;
SECTIONS
{
. = BASE_VMA;
. += 0x200000;
.text : AT(. - OFFSET) {
*(.text.entry)
*(.text*)
}
. = ALIGN(4K);
etext = .;
.data : AT(. - OFFSET) { *(.*data*) }
.bss : AT(. - OFFSET) {
sbss = .;
*(.*bss*)
ebss = .;
}
. = ALIGN(4K);
ekernel = .;
/DISCARD/ : { *(.eh_frame) }
}
Move the kernel
The kernel is moved by creating a kernel pagetable, which has
-
An identity mapping for the whole kernel image at its physical address (starting from
0x80000000
). This is done by registering a 1GB large page to the third page table entry. -
Another large page that maps
0xFFFFFFC080000000
to0x80000000
similiarily.
File: src/main.rs
#![allow(unused)] fn main() { core::arch::global_asm! {r#" .section .data .globl _pgtable .align 12 _pgtable: .zero 4096 .section .text.entry .globl _start _start: la t0, _pgtable addi t1, t0, 1024 li t2, 0x2000002F # maps 0x80000000+1GB sd t2, 16-1024(t1) # to 0x80000000+1GB, and sd t2, 16+1024(t1) # to 0xFFFFFFC080000000+1GB srli t0, t0, 12 # set the PPN field of satp li t1, 0x8 << 60 or t0, t0, t1 # set MODE=8 to use SV39 sfence.vma zero, zero csrw satp, t0 # enable page table sfence.vma zero, zero la sp, _stack ld t0, _main jr t0 "#} }
Note that we are jumping a little bit differently: instead of directly j main
, we load from the symbol _main
and jr
to it. The symbol _main
stores the address of main
- why are we doing this? The answer is in the RISC-V ISA itself. The j
instruction, which is a pseudo-instruction of jal
, works by relative addressing. If we just call j main
, the PC will still be in lower address space. Therefore we have to store the (absolute) address of the symbol main
into memory, and then jump there:
File: src/main.rs
#![allow(unused)] fn main() { core::arch::global_asm! {r#" .section .data .globl _main _main: .dword main "#} }