ARM64的启动过程之(四):打开MMU
一、前言
经过漫长的前戏,我们终于迎来了打开MMU的时刻,本文主要描述打开MMU以及跳转到start_kernel之前的代码逻辑。这一节完成之后,我们就会离开痛苦的汇编,进入人民群众喜闻乐见的c代码了。
二、打开MMU前后的概述
对CPU以及其执行的程序而言,打开MMU是一件很有意思的事情,好象从现实世界一下子走进了奇妙的虚幻世界,本节,我们一起来看看内核是如何“穿越”的。下面这张图描述了两个不同的世界:
当没有打开MMU的时候,cpu在进行取指以及数据访问的时候是直接访问物理内存或者IO memory。虽然64bit的CPU理论上拥有非常大的address space,但是实际上用于存储kernel image的物理main memory并没有那么大,一般而言,系统的main memory在低端的一小段物理地址空间中,如上图右侧的图片所示。当打开MMU的时候,cpu对memory系统的访问不能直接触及物理空间,而是需要通过一系列的Translation table进行翻译。虚拟地址空间分成三段,低端是0x00000000_00000000~0x0000FFFF_FFFFFFFF,用于user space。高端是0xFFFF0000_00000000~0xFFFFFFFF_FFFFFFFF,用于kernel space。中间的一段地址是无效地址,对其访问会产生MMU fault。虚拟地址空间如上图右侧的图片所示。
Linker感知的是虚拟地址,在将内核的各个object文件链接成一个kernel image的时候,kernel image binary code中访问的都是虚拟地址,也就是说kernel image应该运行在Linker指定的虚拟地址空间上。问题来了,kernel image运行在那个地址上呢?实际上,将kernel image放到kernel space的首地址运行是一个最直观的想法,不过由于各种原因,具体的arch在编译内核的时候,可以指定一个offset(TEXT_OFFSET),对于ARM64而言是512KB(0x00080000),因此,编译后的内核运行在0xFFFF8000_00080000的地址上。系统启动后,bootloader会将kernel image copy到main memory,当然,和虚拟地址空间类似,kernel image并没有copy到main memory的首地址,也保持了一个同样size的offset。现在,问题又来了:在kernel的开始运行阶段,MMU是OFF的,也就是说kernel image是直接运行在物理地址上的,但是实际上kernel是被linker链接到了虚拟地址上去的,在这种情况下,在没有turn on MMU之前,kernel能正常运行吗?可以的,如果kernel在turn on MMU之前的代码都是PIC的,那么代码实际上是可以在任意地址上运行的。你可以仔细观察turn on MMU之前的代码,都是位置无关的代码。
OK,解决了MMU turn on之前的问题,现在我们可以准备“穿越”了。真正打开MMU就是一条指令而已,就是将某个system register的某个bit设定为1之类的操作。这样我们可以把相关指令分成两组,turn on mmu之前的绿色指令和之后的橘色指令,如下图所示:
由于现代CPU的设计引入了pipe, super scalar,out-of-order execution,分支预测等等特性,实际上在turn on MMU的指令执行的那个时刻,该指令附近的指令的具体状态有些混乱,可能绿色指令执行的数据加载在实际在总线上发起bus transaction的时候已经启动了MMU,本来它是应该访问physical address space的。而也有可能橘色的指令提前执行,导致其发起的memory操作在MMU turn on之前就完成。为了解决这些混乱,可以采取一种投机取巧的办法,就是建立一致性映射:假设kernel image对应的物理地址段是A~B这一段,那么在建立页表的时候就把A~B这段虚拟地址段映射到A~B这一段的物理地址。这样,在turn on MMU附近的指令是毫无压力的,无论你是通过虚拟地址还是物理地址,访问的都是同样的物理memory。
还有一种方法,就是清楚的隔离turn on MMU前后的指令,那就是使用指令同步工具,如下:
指令屏障可以清清楚楚的把指令的执行划分成三段,第一段是绿色指令,在执行turn on mmu指令执行之前全部完成,随后启动turn on MMU的指令,随后的指令屏障可以确保turn on MMU的指令完全执行完毕(整个计算机系统的视图切换到了虚拟世界),这时候才启动橘色指令的取指、译码、执行等操作。
三、打开MMU的代码
具体打开MMU的代码在__enable_mmu函数中如下:
__enable_mmu:
ldr x5, =vectors
msr vbar_el1, x5 ---------------------------(1)
msr ttbr0_el1, x25 // load TTBR0 -----------------(2)
msr ttbr1_el1, x26 // load TTBR1
isb
msr sctlr_el1, x0 ---------------------------(3)
isb
br x27 -------------跳转到__mmap_switched执行,不设定lr寄存器
ENDPROC(__enable_mmu)
传入该函数的参数有四个,一个是x0寄存器,该寄存器中保存了打开MMU时候要设定的SCTLR_EL1的值(在__cpu_setup函数中设定),第二个是个是x25寄存器,保存了idmap_pg_dir的值。第三个参数是x26寄存器,保存了swapper_pg_dir的值。最后一个参数是x27,是执行完毕该函数之后,跳转到哪里去执行(__mmap_switched)。
(1)VBAR_EL1, Vector Base Address Register (EL1),该寄存器保存了EL1状态的异常向量表。在ARMv8中,发生了一个exception,首先需要确定的是该异常将送达哪一个exception level。如果一个exception最终送达EL1,那么cpu会跳转到这里向量表来执行。具体异常的处理过程由其他文档描述,这里就不说了。
(2)idmap_pg_dir是为turn on MMU准备的一致性映射,物理地址的高16bit都是0,因此identity mapping必定是选择TTBR0_EL1指向的各级地址翻译表。后续当系统运行之后,在进程切换的时候,会修改TTBR0的值,切换到真实的进程地址空间上去。TTBR1用于kernel space,所有的内核线程都是共享一个空间就是swapper_pg_dir。
(3)打开MMU。实际上在这条指令的上下都有isb指令,理论上已经可以turn on MMU之前之后的代码执行顺序严格的定义下来,其实我感觉不必要再启用idmap_pg_dir的那些页表了,当然,这只是猜测。
四、通向start_kernel
我痛恨汇编,如果能不使用汇编那绝对不要使用汇编,还好我们马上就要投奔start_kernel:
__mmap_switched:
adr_l x6, __bss_start
adr_l x7, __bss_stop1: cmp x6, x7
b.hs 2f
str xzr, [x6], #8 ---------------clear BSS
b 1b
2:
adr_l sp, initial_sp, x4 -----------建立和swapper进程的链接
str_l x21, __fdt_pointer, x5 // Save FDT pointer
str_l x24, memstart_addr, x6 // Save PHYS_OFFSET
mov x29, #0
b start_kernel
ENDPROC(__mmap_switched)
这段代码分成两个部分,一部分是清BSS,另外一部分是为进入c代码做准备(主要是stack)。clear BSS段就是把未初始化的全局变量设定为0的初值,没有什么可说的。要进入start_kernel这样的c代码,没有stack可不行,那么如何设定stack呢?熟悉kernel的人都知道,用户空间的进程当陷入内核态的时候,stack切换到内核栈,实际上就是该进程的thread info内存段(4K或者8K)的顶部。对于swapper进程,原理是类似的:
.set initial_sp, init_thread_union + THREAD_START_SP
如果说之前的代码执行都处于一个孤魂野鬼的状态,“adr_l sp, initial_sp, x4”指令执行之后,初始化代码终于找到了归宿,初始化代码有了自己的thread info,有了自己的task struct,有了自己的pid,有了进程(内核线程)应该拥有的一切,从此之后的代码归属idle进程,pid等于0的那个进程。
为了方便后面的代码的访问,这里还初始化了两个变量,分别是__fdt_pointer(设备树信息,物理地址)和memstart_addr(kernel image所在的物理地址,一般而言是main memory的首地址)。 memstart_addr主要用于main memory中物理地址和虚拟地址的转换,具体可以参考__virt_to_phys和__phys_to_virt的实现。
五、参考文献
1、ARM Architecture Reference Manual
change log:
1、2015-11-30,强调了初始化代码和idle进程的连接
2、2015-12-2,修改了物理空间和虚拟空间的视图
3、2016-9-21,修改对一致性映射的描述
原创文章,转发请注明出处。蜗窝科技
继续讨论
新讨论请用 GitHub 账号在下方发言——评论会保存在本仓库的 Discussions 里,与站点同寿; 再下方的「历史评论」是原站的只读存档。
评论(52 条,只读存档)
以下为原站的历史评论(只读存档);继续讨论请用上方的评论区。




passerby
linuxer
passerby
linuxer
donkey
linuxer
donkey
donkey
passerby
linuxer
passerby
linuxer
archer
linuxer
ericzhou
linuxer
mobz 主页
linuxer
mobz 主页
linuxer
mobz 主页
linuxer
mobz 主页
linuxer
mobz 主页
linuxer
kitty 主页
linuxer
kitty 主页
amusion
linuxer
tgn
linuxer
archer
linuxer
ericzhou
macooma
linuxer
macooma
thatman
breeze
wowo
leba
linuxer
2freeman 主页
rikeyone
rikeyone
see
smcdef
daiyinger
daiyinger
llp