内核初始映射
改写自 rcore tutorial
之前的内核实现并未使能页表机制,实际上内核是直接在物理地址空间上运行的。这样虽然比较简单,但是为了后续能够支持多个用户进程能够在内核中并发运行,满足隔离等性质,我们要先运用学过的页表知识,把内核的运行环境从物理地址空间转移到虚拟地址空间,为之后的功能打好铺垫。
更具体的,我们现在想将内核代码放在虚拟地址空间中以 0xffffffffc0200000
开头的一段高地址空间中。因此,我们将下面的参数修改一下:
我们修改了链接脚本中的起始地址。但是这样做的话,就能从物理地址空间转移到虚拟地址空间了吗?让我们回顾一下在相当于 bootloader 的 OpenSBI 结束后,我们要面对的是怎样一种局面:
物理内存状态:OpenSBI 代码放在
[0x80000000,0x80200000)
中,内核代码放在以0x80200000
开头的一块连续物理内存中。CPU 状态:处于 S Mode ,寄存器
satp
的 被设置为Bare
,即无论取指还是访存我们都通过物理地址直接访问物理内存。 指向内核的第一条指令。栈顶地址 处在 OpenSBI 代码内。内核代码:由于改动了链接脚本的起始地址,认为自己处在以虚拟地址
0xffffffffc0200000
开头的一段连续虚拟地址空间中,以此为依据确定代码里每个部分的地址(每一段都是从BASE_ADDRESS
往后依次摆开的,所以代码里各段都会认为自己在0xffffffffc0200000
之后的某个地址上,或者说编译器和链接器会把里面的符号/变量地址都对应到0xffffffffc0200000
之后的某个地址上)
接下来,我们在入口点 entry.S
中所要做的事情是:将 寄存器从原先指向OpenSBI 某处的栈空间,改为指向我们自己在内核的内存空间里分配的栈;同时需要跳转到函数 kern_init
中。
在之前的实现中,我们已经在 entry.S
自己分配了一块 的内存用来做启动栈:
符号 bootstacktop
就是我们需要的栈顶地址, 符号 kern_init
代表了我们要跳转到的地址。之前我们直接将 bootstacktop
的值给到 , 再跳转到 rust_main
就行了。看起来原来的代码仍然能用啊?
问题在于,由于我们修改了链接脚本的起始地址,编译器和链接器认为内核开头地址为 0xffffffffc0200000
,因此这两个符号会被翻译成比这个开头地址还要高的某个虚拟地址。而我们的 CPU 目前还处于 Bare
模式,会将地址都当成物理地址处理。这样,我们跳转到 rust_main
就会跳转到比0xffffffffc0200000
还大 的一个物理地址,物理地址都没有这么多位!这显然是会出问题的。
于是,我们需要想办法利用刚学的页表知识,帮内核将需要的虚拟地址空间构造出来。也就是:构建一个合适的页表,让satp
指向这个页表,然后使用地址的时候都要经过这个页表的翻译,使得虚拟地址0xFFFFFFFFC0200000
经过页表的翻译恰好变成0x80200000
,就不会出错了。
我们实现一个最简单的页表,所有的虚拟地址有一个固定的偏移量。比如内核的第一条指令,虚拟地址为 0xffffffffc0200000
,物理地址为 0x80200000
,因此,我们只要将虚拟地址减去 0xffffffff40000000
,就得到了物理地址。
使用上一节页表的知识,我们只需要做到当访问内核里面的一个虚拟地址 时,我们知道 处的代码或数据放在物理地址为 pa = va - 0xffffffff40000000
处的物理内存中,我们真正所要做的是要让 CPU 去访问 。因此,我们要通过恰当构造页表,来对于内核所属的虚拟地址,实现这种 的映射。
还记得上一节中所讲的大页吗?那时我们提到,将一个三级页表项的标志位 不设为全 ,可以将它变为一个叶子,从而获得大小为 的一个大页。
我们假定内核大小不超过 ,通过一个大页将虚拟地址区间[0xffffffffc0000000,0xffffffffffffffff]
映射到物理地址区间 [0x80000000,0xc0000000)
,而我们只需要分配一页内存用来存放三级页表,并将其最后一个页表项(也就是对应我们使用的虚拟地址区间的页表项)进行适当设置即可。
总结一下,要进入虚拟内存访问方式,需要如下步骤:
分配页表所在内存空间并初始化页表;
设置好页基址寄存器(指向页表起始地址);
刷新 TLB。
到现在为止我们终于理解了自己是如何做起白日梦——进入那看似虚无缥缈的虚拟内存空间的。
最后更新于