sOS0x01

2025年11月20日 · 1884 字 · 4 分钟

riscv for moss-kernel,简单记录写每一块时的想法

Setup and prerequisites

配置文件显示了我们构建的目标,也就是 riscv64gc

RISC-V 的 QEMU 启动命令

qemu-system-riscv64 \
    -machine virt \
    -cpu rv64 \
    -smp 4 \
    -m 128M \
    -drive if=none,format=raw,file="$base/hdd.dsk",id=foo \
    -device virtio-blk-device,scsi=off,drive=foo \
    -nographic \
    -serial mon:stdio \
    -bios none \
    -device virtio-rng-device \
    -device virtio-gpu-device \
    -device virtio-net-device \
    -device virtio-tablet-device \
    -device virtio-keyboard-device \
    -kernel "$elf"

链接脚本

需要指定链接脚本,放在了/src/arch/riscv64/boot/link.ld

修改build.rs使得根据 CARGO_CFG_TARGET_ARCH 环境变量自动选择链接脚本

boot

启动汇编代码位于 src/arch/riscv64/boot/start.s

  • 安全初始化

      csrw sie, zero    # 禁用所有中断
      csrw sip, zero    # 清除挂起的中断
    

确保启动过程中不被中断打断

  • 保存启动参数

      mv s0, a0  # 保存 Hart ID(CPU核心编号)
      mv s1, a1  # 保存设备树(DTB)物理地址
    

RISC-V启动时硬件自动传递这些参数

  • 多核处理

      bnez a0, .Lsecondary_park  # 如果不是核心0,则挂起
    

只有Hart 0(主核心)继续执行

其他核心进入WFI(等待中断)循环

  • 内存初始化

      la t0, __bss_start
      la t1, __bss_end
    

    清除BSS段(存储未初始化全局变量的区域)

    循环清零内存

  • 设置临时栈

      la sp, __boot_stack  # 设置启动栈
    

为C/Rust函数调用准备栈空间

  • 调用Rust进行页表初始化

      call paging_bootstrap
    

调用Rust函数初始化内存分页

返回页表根地址(SATP值)

  • 启用MMU和地址空间切换

      csrw satp, a0       # 设置页表
      sfence.vma          # 刷新TLB
    

启用虚拟内存

从物理地址切换到虚拟地址空间

调整栈指针到虚拟地址

  • 进入高地址内存

      jr ra  # 跳转到高地址虚拟内存
    

内核通常映射在高地址

  • Rust初始化

      call arch_init_stage1  # 第一阶段初始化
      call arch_init_stage2  # 第二阶段初始化
    

传递启动参数给Rust内核

设置最终的内核栈

准备陷阱帧(TrapFrame)

initial memory mapping

主要包含三个任务:探测并管理物理内存、建立内核的逻辑映射以及设置内核运行时所需的堆栈

  1. 物理内存探测与分配器初始化
// src/arch/riscv64/boot/memory.rs

pub fn setup_allocator(dtb_ptr: TPA<U8>, image_start: PA, image_end: PA) -> Result<()> {
    // ... 解析 FDT ...
    
    // 1. 添加可用内存到分配器
    dt.memory().try_for_each(|mem| {
        // alloc.add_memory(...)
    })?;

    // 2. 保留内核镜像本身
    // 防止分配器把内核代码当成空闲内存分配出去
    alloc.add_reservation(PhysMemoryRegion::from_start_end_address(image_start, image_end))?;

    // 3. 保留 Initrd (如果存在)
    if let Some(chosen) = dt.find_nodes("/chosen").next() {
        // 解析 linux,initrd-start 和 linux,initrd-end
        alloc.add_reservation(...)
    }
    
    Ok(())
}

内核启动时需要知道物理内存的确切布局,以及哪些区域已经被占用(如Kernel Image、DTB、Initrd等),利用fdt库解析由bootloader传递的设备树信息,获取内存布局并初始化物理内存分配器

  1. 建立内核的逻辑映射
// src/arch/riscv64/boot/logiacl_map.rs

pub fn setup_logical_map(pgtbl_base: TPA<PgTableArray<L0Table>>) -> Result<()> {
    // ...
    // 遍历所有物理内存区域
    for mem_region in mem_list.iter() {
        // 建立映射: VA = PA + Offset
        let map_attrs = MapAttributes {
            phys: mem_region,
            virt: mem_region.map_via::<PageOffsetTranslator>(), // 使用偏移转换
            mem_type: MemoryType::Normal,
            permis: PtePermissions::rw(false), //以此读写权限映射
        };
        // 使用 FixmapMapper 辅助写入页表
        map_range(pgtbl_base, map_attrs, &mut ctx)?;
    }
    Ok(())
}

暂时将所有可用的物理内存映射到内核虚拟地址空间的一个固定偏移处,这样当内核需要访问任意物理地址时只需要加上一个固定offset即可得到虚拟地址,无需频繁修改页表

  1. 堆栈的设置
//src/arch/riscv64/boot/memory.rs
pub fn setup_stack_and_heap(pgtbl_base: TPA<PgTableArray<L0Table>>) -> Result<VA> {
    // 1. 分配 Stack 物理内存
    let stack = alloc.alloc(KERNEL_STACK_SZ, PAGE_SIZE)?;
    
    // 2. 映射 Stack
    // allocate_kstack_region 返回虚拟地址范围,并处理 Guard Page 偏移
    let stack_virt_region = allocate_kstack_region();
    map_range(pgtbl_base, MapAttributes {
        phys: stack_phys_region,
        virt: stack_virt_region,
        // ...
    }, &mut ctx)?;

    // 3. 初始化全局堆
    let heap = alloc.alloc(KERNEL_HEAP_SZ, PAGE_SIZE)?;
    // ... 映射堆区域 ...
    unsafe {
        HEAP_ALLOCATOR.lock().init(
            heap_virt_region.start_address().as_ptr_mut().cast(),
            heap_virt_region.size(),
        )
    };

    // 返回新的栈顶地址 (SP),随后汇编代码会切换到这个新栈
    Ok(stack_virt_region.end_address())
}

为了安全(如防止栈溢出)和动态内存管理,单独为内核栈和堆分配虚拟地址空间

  • Kernel Stack: 从物理内存分配页面,并映射到高地址虚拟空间。为了检测栈溢出,代码中在栈的末尾隐含了一个 Guard Page(通过allocate_kstack_region中的逻辑,留出一个未映射的页面,一旦溢出访问该页将触发缺页异常)
  • Kernel Heap: 分配一大块物理内存(64MiB),映射后初始化全局堆分配器 (HEAP_ALLOCATOR),从而支持 Box, Vec, Arc等Rust特性

RISCV-V内存管理

底层是 RISC-V SV39分页硬件定义,核心是通用的伙伴系统物理页分配器,上层是地址空间抽象

  1. 物理内存分配机制

核心逻辑位于 libkernel/src/memory/page_alloc.rs,采用伙伴系统算法管理物理内存页

每个物理页帧pfn对应一个frame结构,包含state和link字段,framestate则用于标记该页的状态(空闲、已分配、保留)、记录引用计数和阶数,引用计数支持了共享内存(fork时的cow)

空闲链表使用侵入式链表,将链表节点直接嵌入在frame结构中,避免了额外的内存分配,提高了性能;维护了一个数组存储不同大小的空闲块

  1. RISC-V分页硬件抽象

libkernel/src/arch/riscv64/memory/pg_tables.rs和pg_descriptors.rs

  • SV39分页模式,39位虚拟地址空间,三级页表结构,同时PgTableArray保证了页表是4KiB对齐的,包含512个描述符
  • PTE(页表项)结构体封装了RISC-V的页表项格式,提供了读写权限、内存类型等属性的设置方法
  1. 地址空间与MMU接口 libkernel/src/arch/riscv64/memory/mmu.rs
  • 内核地址空间由自旋锁保护,实现了简单的线性映射
  • 用户空间地址空间暂时预留了接口,功能未完全实现