Unikernel的基本启动流程

本文基于ArceOS简化版的代码(依靠riscv64_qemu架构),介绍一个简单的组件化的Unikernel的启动流程。
仓库链接:https://github.com/arceos-org/oscamp.git

Unikernel

应用与内核:

  1. 处于相同的特权级-内核态
  2. 共享同一个地址空间
  3. 编译形成一个Image,一体运行

Unikernel可以看作是操作系统内核的基础,基于此,可以发展出更常见的宏内核

组件化

将内核按功能划分为多个模块,最基本内核只包含基础模块,其余模块按需配置:
alt text

启动流程

BIOS启动

硬件会把BIOS固件程序放到物理地址为0x0的地方,并开始执行,对硬件进行一些初始化,然后加载BootLoader(操作系统引导程序的加载者)到0x8000_0000物理地址,并开始执行。此处用的是OpenSBI v1.0 。

其他架构会有其他的BootLoader。

SBI启动

SBI即为 (RISC-V Supervisor Binary Interface),SBI直接运行在系统M模式,可以作为一个bootloader也可以是一个M模式下运行的后台程序,SBI程序拥有最高的权限,可以访问所有的硬件资源,向S-MODE的OS提供了统一的硬件功能调用接口(在此处,就是给axhal提供一些打印、时钟等的支持)。
在本处代码中,SBI作为bootloader时,最后加载对应的内核执行文件到起始物理地址为0x8020_0000的内存中。

内核执行文件链接时有如下链接脚本文件指导,修改默认的链接操作:

  1. 修改入口为_start,默认可能为main
  2. 添加了一些地址符号(在生成的elf的符号表中),如_skernel等,后续内核程序可以通过该符号得知一些特定的地址值。
  3. 为起始的一些数据结构分配对应的物理内存(通过对齐的方式预留),如boot_page_table和boot_stack
  4. 指定各个具体段的布局,如把.text.boot(_start所在地)放在.text段的最前面
    (rust编程中可以通过#[link_section = ".data.boot_page_table"]的方式指定某个item所属的段名称)
  5. 指定输出架构,应该是会根据架构的不同选择之后axhal中不同的内核程序(axhal提供了各种架构,也就是各种BootLoader的适配)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    # oscamp/arceos/modules/axhal/linker.lds.S

    OUTPUT_ARCH(%ARCH%)

    BASE_ADDRESS = %KERNEL_BASE%;

    ENTRY(_start)
    SECTIONS
    {
    . = BASE_ADDRESS;
    _skernel = .;

    .text : ALIGN(4K) {
    _stext = .;
    *(.text.boot)
    *(.text .text.*)
    . = ALIGN(4K);
    _etext = .;
    }

    .rodata : ALIGN(4K) {
    _srodata = .;
    *(.rodata .rodata.*)
    *(.srodata .srodata.*)
    *(.sdata2 .sdata2.*)
    . = ALIGN(4K);
    _erodata = .;
    }

    .data : ALIGN(4K) {
    _sdata = .;
    *(.data.boot_page_table)
    . = ALIGN(4K);
    *(.data .data.*)
    *(.sdata .sdata.*)
    *(.got .got.*)
    }
    ...
    }

执行Boot

作为操作系统的初始化程序,一些与架构有关的操作要在Boot中完成,如MMU的页表格式:

  1. 保存参数

  2. 建立栈
    栈空间已经分配完成(在链接时),把栈顶指针sp设置完成,就算是建立了栈,注意,此处的栈是高地址往低地址增长。函数的调用依赖于栈的有效设置,所以为了尽早可以正常使用函数,而不是像_start这样自己写汇编,就要尽早建立栈。

  3. 建立页表并启用MMU(内存管理单元)
    页表空间也已分配完成,此处先对一个G的物理空间形成了恒等映射和“偏移映射”,偏移映射是给应用和全局内存分配器用的,恒等映射应该是给一些更底层的模块用的(保留一个直接访问物理内存的形式)。这个页表非常的粗粒度,而且根据下面的实际所有物理段的显示,此处显然只形成了非mmio的映射,所以此时并不支持一些外部设备。
    值得注意的是,这里的映射是巨页映射,即直接在根页表中给出了1G的页映射,这是避免在初始化时加载过多的页表,简化系统启动。MMU在解析巨页映射时,直接就会计算出对应虚拟地址的物理地址,无需正常查三级页表。
    巨页映射则这个项就是页表项,RWX至少有一位为1,如果指向的是一级子页表,RWX应该全为0
    而所谓的启动mmu,其实就是给satp寄存器设置值,把页表的模式和页表地址告诉它,此后的地址访问,就会默认为虚拟地址,然后经过MMU映射后再得到物理地址(不设置satp就是默认直接访问)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [PA:0x80200000, PA:0x80206000) .text (READ | EXECUTE | RESERVED)
    [PA:0x80206000, PA:0x80209000) .rodata (READ | RESERVED)
    [PA:0x80209000, PA:0x8020c000) .data .tdata .tbss .percpu (READ | WRITE | RESERVED)
    [PA:0x8020c000, PA:0x8024c000) boot stack (READ | WRITE | RESERVED)
    [PA:0x8024c000, PA:0x80270000) .bss (READ | WRITE | RESERVED)
    [PA:0x80270000, PA:0x88000000) free memory (READ | WRITE | FREE)
    [PA:0x101000, PA:0x102000) mmio (READ | WRITE | DEVICE | RESERVED)
    [PA:0xc000000, PA:0xc210000) mmio (READ | WRITE | DEVICE | RESERVED)
    [PA:0x10000000, PA:0x10001000) mmio (READ | WRITE | DEVICE | RESERVED)[PA:0x10001000, PA:0x10009000) mmio (READ | WRITE | DEVICE | RESERVED)
    [PA:0x22000000, PA:0x24000000) mmio (READ | WRITE | DEVICE | RESERVED)
    [PA:0x30000000, PA:0x40000000) mmio (READ | WRITE | DEVICE | RESERVED)
    [PA:0x40000000, PA:0x80000000) mmio (READ | WRITE | DEVICE | RESERVED)
  4. 修正sp寄存器
    访问sp寄存器的值,应该是虚拟地址,所以要加一个偏移进行修正

  5. 恢复参数,进入axruntime

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    // oscamp/arceos/modules/axhal/src/platform/riscv64_qemu_virt/boot.rs

    #[link_section = ".bss.stack"]
    static mut BOOT_STACK: [u8; TASK_STACK_SIZE] = [0; TASK_STACK_SIZE];

    #[link_section = ".data.boot_page_table"]
    static mut BOOT_PT_SV39: [u64; 512] = [0; 512];

    unsafe fn init_boot_page_table() {
    // 0x8000_0000..0xc000_0000, VRWX_GAD, 1G block
    BOOT_PT_SV39[2] = (0x80000 << 10) | 0xef;
    // 0xffff_ffc0_8000_0000..0xffff_ffc0_c000_0000, VRWX_GAD, 1G block
    BOOT_PT_SV39[0x102] = (0x80000 << 10) | 0xef;
    }

    unsafe fn init_mmu() {
    let page_table_root = BOOT_PT_SV39.as_ptr() as usize;
    satp::set(satp::Mode::Sv39, 0, page_table_root >> 12);
    riscv::asm::sfence_vma_all();
    }

    #[link_section = ".text.boot"]
    unsafe extern "C" fn _start() -> ! {
    // PC = 0x8020_0000

    // OpenSBI传来的两个参数
    // a0 = hartid 用于将来识别CPU
    // a1 = dtb 传入DTB的指针
    core::arch::asm!("
    mv s0, a0 // save hartid
    mv s1, a1 // save DTB pointer
    la sp, {boot_stack}
    li t0, {boot_stack_size}
    add sp, sp, t0 // setup boot stack

    call {init_boot_page_table}
    call {init_mmu} // setup boot page table and enabel MMU

    li s2, {phys_virt_offset} // fix up virtual high address
    add sp, sp, s2

    mv a0, s0
    mv a1, s1
    la a2, {entry}
    add a2, a2, s2
    jalr a2 // call rust_entry(hartid, dtb)
    j .",
    phys_virt_offset = const PHYS_VIRT_OFFSET,
    boot_stack_size = const TASK_STACK_SIZE,
    boot_stack = sym BOOT_STACK,
    init_boot_page_table = sym init_boot_page_table,
    init_mmu = sym init_mmu,
    entry = sym super::rust_entry,
    options(noreturn),
    )
    }

    // oscamp/arceos/modules/axhal/src/platform/riscv64_qemu_virt/mod.rs
    unsafe extern "C" fn rust_entry(cpu_id: usize, dtb: usize) {
    // .bss段 存放未初始化的静态变量和全局变量(.data段是已经初始化了的)此处由操作系统来支持将其初始化为全0
    crate::mem::clear_bss();

    // 把当前CPU设置为主CPU
    crate::cpu::init_primary(cpu_id);

    // 用于设置异常或中断处理的入口地址到stvec寄存器,保证异常发生时能够跳转到trap_handler之类的函数。
    crate::arch::set_trap_vector_base(trap_vector_base as usize);

    // UART (一种串口通信接口) 的初始化
    self::time::init_early();
    rust_main(cpu_id, dtb); // 进入axruntime
    }

架构无关的初始化

不是说执行的代码真的与平台无关,只是已经进入了axruntime,各个架构都运行统一的一个初始化框架,但涉及平台的调用时,会按统一的接口往下调用axhal中绑定的具体架构的代码。
其实到了此处,一个最基本的内核算是初始化完成了,有基本的MMU,print helloworld可以正常执行。
如下面代码所示,基本就是打印一些信息,然后根据feature的不同,加入不同的组件。

然后,正如前面所述,unikernel就一个镜像,所以在编译链接时,通过A参数来指定APP文件,内核就可以直接调用到APP文件中的main函数并执行相应的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
pub extern "C" fn rust_main(cpu_id: usize, dtb: usize) -> ! {
...

axlog::init();
axlog::set_max_level(option_env!("AX_LOG").unwrap_or("")); // no effect if set `log-level-*` features
info!("Logging is enabled.");
info!("Primary CPU {} started, dtb = {:#x}.", cpu_id, dtb);

info!("Found physcial memory regions:");
for r in axhal::mem::memory_regions() {
info!(
" [{:x?}, {:x?}) {} ({:?})",
r.paddr,
r.paddr + r.size,
r.name,
r.flags
);
}

#[cfg(any(feature = "alloc", feature = "alt_alloc"))]
init_allocator();

#[cfg(feature = "paging")]
axmm::init_memory_management();

info!("Initialize platform devices...");
axhal::platform_init();
...
unsafe { main() }
...
}