使能MMU

① 多核情况使能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.物理地址到虚拟地址的切换巧妙使用进异常进行转化。