使能MMU
- RISC-V
- 2024-07-07
- 99热度
- 0评论
① 多核情况使能MMU
.Lsecondary_start:
#ifdef CONFIG_SMP
/* Set trap vector to spin forever to help debug */
la a3, .Lsecondary_park
csrw CSR_STVEC, a3
slli a3, a0, LGREG
la a1, __cpu_up_stack_pointer
la a2, __cpu_up_task_pointer
add a1, a3, a1
add a2, a3, a2
/*
* This hart didn\'t win the lottery, so we wait for the winning hart to
* get far enough along the boot process that it should continue.
*/
.Lwait_for_cpu_up:
/* FIXME: We should WFI to save some energy here. */
REG_L sp, (a1)
REG_L tp, (a2)
beqz sp, .Lwait_for_cpu_up
beqz tp, .Lwait_for_cpu_up
fence
/* Enable virtual memory and relocate to virtual address */
la a0, swapper_pg_dir
call relocate
tail smp_callin
#endif
②主hart 使能mmu
_start_kernel
la a0, early_pg_dir
call relocate
relocate:
/* Relocate return address */
li a1, PAGE_OFFSET
la a2, _start
sub a1, a1, a2
add ra, ra, a1
③ PAGE_OFFSET(内核的虚拟地址) - _start(内核加载的物理内存) 得到虚拟地址相对物理地址的偏移,ra = ra + (PAGE_OFFSET - _start),相当于计算了ra的虚拟地址。
/* Point stvec to virtual address of intruction after satp write */
la a2, 1f
add a2, a2, a1
csrw CSR_STVEC, a2
④ 将便签1转化为虚拟地址,a1存储的还是va_pa_offset,再将标签1处的虚拟地址填充到stvec寄存器,设置中断的入口函数。
/* Compute satp for kernel page tables, but don\'t load it yet */
srl a2, a0, PAGE_SHIFT a0 = early_pg_dir, a2 = early_pg_dir >> 12
li a1, SATP_MODE a1 = 8,s atp的模式,3级页表
or a2, a2, a1 a2 = early_pg_dir >> 12 | 8
⑤ 计算satp寄存器的值,mod为8代表3级页表,根目录页表的地址为early_pg_dir,这个页表在set_vm填充的页表,包括内核的镜像的粗粒度映射。这里只是先计算当前的值,但是先不写到satp寄存器。
/*
* Load trampoline page directory, which will cause us to trap to
* stvec if VA != PA, or simply fall through if VA == PA. We need a
* full fence here because setup_vm() just wrote these PTEs and we need
* to ensure the new translations are in use.
*/
la a0, trampoline_pg_dir
srl a0, a0, PAGE_SHIFT trampoline_pg_dir >> 12
or a0, a0, a1 trampoline_pg_dir >> 12 | 8
sfence.vma
csrw CSR_SATP, a0 satp = trampoline_pg_dir >> 12 | 8
⑥ satp设置的页表是trampoline_pg_dir,页表trampoline_pg_di/trampoline_pmd在setup_vm中对PAGE_OFFSET~PAGE_OFFSET+2M的空间做了粗粒度映射。a0=trampoline_pg_dir >> 12 | 8,将a0的值写如到satp的那一刻,MMU就使能了,寻址的地址就需要经过MMU来翻译,因此PC下一条指令的取值地址,需要经过MMU翻译,但是下一条指令依旧是物理地址,MMU没有对应的页表就会陷入异常,具体过程如下:
0x802000f4 <_start+244> addi a0,a0,-240
0x802000f8 <_start+248> srli a0,a0,0xc
0x802000fa <_start+250> or a0,a0,a1
=> 0x80200100 <_start+256> csrw satp,a0
0x80200104 <_start+260> auipc a0,0x0
0x80200108 <_start+264> addi a0,a0,104
0x8020010c <_start+268> csrw stvec,a0
0x80200110 <_start+272> auipc gp,0x880
0x80200114 <_start+276> addi gp,gp,1976
0x80200118 <_start+280> csrw satp,a2
如上的指令,PC运行到0x80200100,这句的指令就是向satp写入a0的值,执行完这条指令后就使能了MMU,后续的取指令地址都需要经过MMU翻译。当继续运行到下一条时PC=0x80200104地址,0x80200104需要经过MMU翻译,但是MMU查询不到页表就会进入异常,而跳转到异常的入口函数由上述④过程填充的1f(虚拟地址),因此下面的1f是异常处理的入口。
这里使用trampoline_pg_dir页表看起来是多余了,其实可以直接使用early_pg_dir,也做过实验把csrw CSR_SATP, a0 直接改为csrw CSR_SATP, a2,并把set_vm中关于trampoline_pg_dir的映射注释掉,系统也能正常起来,因此暂不清楚linux上游不把这个删除掉。
.align 2
1:
⑦这段代码是异常进入的入口,依旧使用虚拟地址取指运行。
/* Set trap vector to spin forever to help debug */
la a0, .Lsecondary_park
csrw CSR_STVEC, a0
⑧重新更新异常的入口为.Lsecondary_park
/* Reload the global pointer */
.option push
.option norelax
la gp, __global_pointer$
.option pop
/*
* Switch to kernel page tables. A full fence is necessary in order to
* avoid using the trampoline translations, which are only correct for
* the first superpage. Fetching the fence is guarnteed to work
* because that first superpage is translated the same way.
*/
csrw CSR_SATP, a2
sfence.vma
⑨ 重新更新satp=early_pg_dir >> 12 | 8
ret
当使能MMU时,下一条指令的取指就需要经过MMU进行翻译,但是下一条指令的地址依旧是物理地址,MMU查询不到对应的页表就会进入异常,而异常的入口实现使用了虚拟地址进行填充,虚拟地址的页表也在set_vm中进行了填充,因此虚拟地址是可以通过MMU查表找到物理地址,这样就从物理地址过渡到了虚拟地址的运行。
针对前面做个小结:
引导linux系统启动,需要解决的是物理地址切换到虚拟地址运行,而分界点就是写satp寄存器,使能MMU的那一刻。虚拟地址转化为物理地址,而关键点就是为MMU分配页表并填充好页表,而此时内存管理未初始化,又分配不了,所以总结下,就遇到了如下的问题。
6.内存管理没准备好。
7.需要分配页表。
8.开了MMU后,分配的页表能够用虚拟地址访问。
9.开了MMU后,要能够用虚拟地址访问内核。
10.开了MMU后,能够用虚拟地址访问设备树。
针对上面的问题对应的解决办法:
1.提前定义全局的静态数组用于做页表。
2.对内核vmlinux做粗粒度映射。
3.固定一段虚拟地址fixmap用于访问设备树、新分配的页表。
4.物理地址到虚拟地址的切换巧妙使用进异常进行转化。