动态调试xv6内核,复习OS知识

TL;DR

很早之前,我就做完了xv6内核的实验,也算是入门了操作系统

xv6小而精的设计思路的确适合初学者入门这样一个复杂的计算机分支

今天经过一定时间的沉淀后,重新来看看当时没有被完全理解,还有一些被一笔带过的知识


固件代码

过去,我们只关注到了内核被加载之后的内容,每次调试都是b _entry之后

但是在这之前,是时候看看硬件ROM中的代码了

make qemu-gdb

在启动qemu的gdb-server后,程序会暂定执行。此时,我们在另一个终端执行

gdb-multiarch kernel/kernel

就可以自动连接gdb-server。由于是远程连接,没有办法starti直接进行调试,只能通过地址打断点

先检查当前的pc,再将当前pc打上断点即可

pc_image

接下来就可以查看指令。如果gdb中反汇编不方便查看,在gdb中执行(gdb) monitor pmemsave 0x1000 0x1000 bootrom.bin就可以将对应的内存dump出来

通过objdump就可以反汇编

# 由于没有元数据,所以必须要显式指定参数
riscv64-unknown-elf-objdump \
  -b binary \
  -m riscv:rv64 \
  --adjust-vma=0x1000 \
  -D bootrom.bin > bootrom.asm

下面就是QEMU riscv virt平台的BootROM代码

0000000000000100 <.data>:
 100:	00000297          	auipc	t0,0x0
 104:	02828613          	addi	a2,t0,40 # 0x128
 108:	f1402573          	csrr	a0,mhartid
 10c:	0202b583          	ld	a1,32(t0)
 110:	0182b283          	ld	t0,24(t0)
 114:	00028067          	jr	t0

virt实际上只是一个虚拟主板,它将CPU,DRAM,DISK等硬件串联在一起

QEMU在启动的时候就会根据virt标准,构建出一个标准的Memory Map的主板布局,并分配好各个硬件的地址空间

所以CPU在刚上电的时候,首先就会执行QEMU virt中的ROM代码。我们看到的反汇编只是QEMU virt的实现而已,但并不影响学习

由于virt主板是虚拟的,所以执行代码也非常简单,省去了很多复杂的流程。例如:配置 PLL 锁相环,给 CPU 和外设倍频/供电等。

在真实的环境中,BootROM要做复杂的物理硬件初始化

此时,CPU处于M-Mode(机器模式)


Boot Protocol

Boot Protocol是指硬件固件在将 CPU 控制权转交给操作系统内核的时候,双方必须共同遵守的规范

所以上面的几行汇编代码完全就是满足riscv的boot protocol

根据文档中的描述

需要加载a0存放当前CPU核心的ID,a1存放设备树的物理地址

在真实的物理硬件中,设备树通常是在事先编译好并写死在非易失性存储器中


虚拟内存 and MMU

虚拟内存这层,xv6 是在物理地址上直接搭起来的。MMU 开启之前,整个系统还处于裸机状态,页表树必须靠普通 C 代码写进物理内存。整个过程可以拆成四步

裸机物理模式准备

_entry 到 start() 的汇编只有几行。先给 C 语言分配运行栈 stack0,在 M-Mode 下设置 mstatus,把降权目标指向 S-Mode,再把 mepc 写成 main() 的地址。w_satp(0) 这一句显式禁用了 MMU。从此刻起,指令取指和内存读写全部直接作用在物理 RAM 上

建立物理内存分配器

进入 main() 之后,第一件事是 kinit()。它在 S-Mode 下初始化物理内存管理,把空闲页串成单链表 freelist

这里藏着一个顺序问题:页表本身也要消耗物理内存页来存放页表项,所以 kalloc() 必须在 MMU 开启之前就准备就绪。先有分配器,后有页表,页表树才有地方落脚

在物理 RAM 中“手绘”页表树

kvminit() 负责画整棵树。第一步通过 kalloc() 拿出一页 4KB 物理内存作为根页表(L2)

接着 kvmmap 建立恒等映射:对外设 MMIO(UART0、PLIC、VIRTIO0)以及内核代码/数据段(KERNBASE 到 PHYSTOP),全部映射成 VA = PA。权限也做了区分,代码段是 PTE_R | PTE_X,只读可执行;数据段和堆内存是 PTE_R | PTE_W,可读写但不可执行

唯一打破恒等映射的是跳板页 TRAMPOLINE。它被映射到内核虚拟地址的最高端,为后续用户态/内核态的安全切换做准备

底层的建树工作由 mappages 和 walk 完成。遍历虚拟地址时,如果发现缺失 L1/L0 子页表,walk() 会自动调用 kalloc() 动态分配新的 4KB 页表页,最后把二进制页号(PPN)和标志位(如 PTE_V)直接按位拼装,写进物理内存

激活 MMU

树画完了,剩下就是挂载

kvminithart() 执行 w_satp(MAKE_SATP(kernel_pagetable)),底层就是一条 csrw satp, a0。指令落下的瞬间,硬件 MMU 被切换成 Sv39 模式。紧随其后的 sfence.vma 清空 TLB,保证 MMU 从物理内存重新加载最新的映射

三个值得记住的点

把整个过程收拢一下:

  • 先有分配器,后有页表。kalloc() 在 MMU 开启前就能直接管理物理内存,页表树的每个节点都从它这里拿存储空间
  • 恒等映射保证平滑过渡。内核代码段采用 VA = PA 的映射,CPU 写入 satp 激活 MMU 的那一瞬间,指令流水线的 PC 不会因地址突变而触发缺页异常
  • kvmmap 是数据填充,不是系统调用。它只是一个普通 C 函数,通过原生指针直接往物理内存里写 pte_t 页表项,为 MMU 预先准备好推导地图
recent-work

数字电路知识点记录

TL;DR 最近太无聊,重新拾起cs61c课程,开始有些痛苦的数字电路学习。 我的仓库 这个博客就用来记录一下各种硬件的引脚和电路设计等知识。 Warm up Bssic 在更高抽象的程序中,一般情况下都是串行的。 但是在硬件视角中,都是并行的。(电路很快,所以要有负反馈/回授等 …

Read more →

动态调试xv6内核,复习OS知识

TL;DR 很早之前,我就做完了xv6内核的实验,也算是入门了操作系统 xv6小而精的设计思路的确适合初学者入门这样一个复杂的计算机分支 今天经过一定时间的沉淀后,重新来看看当时没有被完全理解,还有一些被一笔带过的知识 固件代码 过去,我们只关注到了内核被加载之后的内容,每次调试 …

Read more →