ARM64的启动过程之(四):打开MMU
作者:linuxer 发布于:2015-10-24 12:35 分类:ARMv8A Arch
一、前言
经过漫长的前戏,我们终于迎来了打开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,修改对一致性映射的描述
原创文章,转发请注明出处。蜗窝科技
标签: 打开MMU

评论:
2019-06-05 10:39
2018-02-28 09:29
我想请教一下,在start kernel之前创建的页表应该只是临时页表,那么在start kernel中的setup arch中会重新创建内核页表的映射,那么已经运行在虚拟空间上了,这个映射是怎么切换的呢?
2017-12-12 09:13
------ 应该还与 pipeline 有关系,所以,那个 idmap 还是需要的。
2018-05-15 11:28
2017-07-26 14:19
本来我以为memblock是在arm64_memblock_init()中初始化的,但我发现kernel在call arm64_memblock_init()之前就已经初始化过memblock一次了. 如:我在arm64_memblock_init()的入口处调用memblock_dump_all(), 输出如下:
MEMBLOCK configuration:
memory size = 0xf0000000 reserved size = 0x0
memory.cnt = 0x1
memory[0x0] [0x00000010000000-0x000000ffffffff], 0xf0000000 bytes flags: 0x0
reserved.cnt = 0x1
reserved[0x0] [0x00000000000000-0xffffffffffffffff], 0x0 bytes flags: 0x0
至少memblock.memory已经初始化过了,region为: [0x00000010000000-0x000000ffffffff].
请问楼主您知道是在哪里初始化的吗?我百思不得其姐.
谢谢.
2016-10-10 14:21
有个问题想请教一下,如下:
idmap_pg_dir是为turn on MMU准备的一致性映射,物理地址的高16bit都是0,因此identity mapping必定是选择TTBR0_EL1指向的各级地址翻译表。
-------------------------------------------------------------------------------------
请问这里的“必定”是因何而来呢? 谢谢!
2016-07-31 14:25
PAGE_OFFSET开始的虚拟地址 会 映射到PHYS_OFFSET的物理地址 (线性区),
PHYS_OFFSET的定义:
#define PHYS_OFFSET ({ VM_BUG_ON(memstart_addr & 1); memstart_addr; })
这个memstart_addr的全局变量是在arm64_memblock_init这个函数中计算确定的。
所以线性区的起始物理地址是在这里才确定的?
我的问题:因为内核的虚拟地址在PAGE_OFFSET的0x80000偏移处,所以Image物理地址也应该在memstart_addr的0x80000偏移处? 但是memstart_addr是在这里才计算确定的,bootloader是怎么知道要把Image加载到这里的呢?
2016-08-01 10:23
所以线性区的起始物理地址是在这里才确定的?
-------------------
我看的是4.4.6内核,memstart_addr是在内核启动的初始阶段被设定的:
ENTRY(stext)
......
adrp x24, __PHYS_OFFSET
......
之后,在__mmap_switched函数中:
str_l x24, memstart_addr, x6 // Save PHYS_OFFSET
mov x29, #0
因为内核的虚拟地址在PAGE_OFFSET的0x80000偏移处,所以Image物理地址也应该在memstart_addr的0x80000偏移处?
--------------
是的
但是memstart_addr是在这里才计算确定的,bootloader是怎么知道要把Image加载到这里的呢?
---------------
这些内容本来就是bootloader和kernel之间接口的一部分,bootloader本来就知道应该copy kernel image到哪里的,如果不知道,系统就没有办法运作了。
2016-05-21 01:09
我是用的4.5的内核
功能
最新评论
- wangjing
写得太好了 - wangjing
写得太好了! - DRAM
圖面都沒辦法顯示出來好像掛點了。 - Simbr
bus至少是不是还有个subsystem? - troy
@testtest:只要ldrex-modify-strex... - gh
Linux 内核在 sparse 内存模型基础上实现了vme...
文章分类
随机文章
文章存档
- 2025年4月(5)
- 2024年2月(1)
- 2023年5月(1)
- 2022年10月(1)
- 2022年8月(1)
- 2022年6月(1)
- 2022年5月(1)
- 2022年4月(2)
- 2022年2月(2)
- 2021年12月(1)
- 2021年11月(5)
- 2021年7月(1)
- 2021年6月(1)
- 2021年5月(3)
- 2020年3月(3)
- 2020年2月(2)
- 2020年1月(3)
- 2019年12月(3)
- 2019年5月(4)
- 2019年3月(1)
- 2019年1月(3)
- 2018年12月(2)
- 2018年11月(1)
- 2018年10月(2)
- 2018年8月(1)
- 2018年6月(1)
- 2018年5月(1)
- 2018年4月(7)
- 2018年2月(4)
- 2018年1月(5)
- 2017年12月(2)
- 2017年11月(2)
- 2017年10月(1)
- 2017年9月(5)
- 2017年8月(4)
- 2017年7月(4)
- 2017年6月(3)
- 2017年5月(3)
- 2017年4月(1)
- 2017年3月(8)
- 2017年2月(6)
- 2017年1月(5)
- 2016年12月(6)
- 2016年11月(11)
- 2016年10月(9)
- 2016年9月(6)
- 2016年8月(9)
- 2016年7月(5)
- 2016年6月(8)
- 2016年5月(8)
- 2016年4月(7)
- 2016年3月(5)
- 2016年2月(5)
- 2016年1月(6)
- 2015年12月(6)
- 2015年11月(9)
- 2015年10月(9)
- 2015年9月(4)
- 2015年8月(3)
- 2015年7月(7)
- 2015年6月(3)
- 2015年5月(6)
- 2015年4月(9)
- 2015年3月(9)
- 2015年2月(6)
- 2015年1月(6)
- 2014年12月(17)
- 2014年11月(8)
- 2014年10月(9)
- 2014年9月(7)
- 2014年8月(12)
- 2014年7月(6)
- 2014年6月(6)
- 2014年5月(9)
- 2014年4月(9)
- 2014年3月(7)
- 2014年2月(3)
- 2014年1月(4)
2020-01-15 10:35
ldr x1, =0xf0000000
str x0,[x1]
br x27
ENDPROC(__enable_mmu)
我是4核A53处理器,我在用其中一个核来引导另一个内核,如上我打算在开启mmu之后往0xf0000000物理地址写特征值表征mmu已开启,发现要等很久(几十秒)才发现0xf0000000的值被改写,如果代码放到开启mmu之前就不会。