本文是基于arm平台。例子都是以tiny210(s5pv210 armv7)为基础的。
[kernel 启动流程]系列:
- [kernel 启动流程] 前篇——vmlinux.lds分析
- [kernel 启动流程] (第一章)概述
- [kernel 启动流程] (第二章)第一阶段之——设置SVC、关闭中断
- [kernel 启动流程] (第三章)第一阶段之——proc info的获取
- [kernel 启动流程] (第四章)第一阶段之——dtb的验证
- [kernel 启动流程] (第五章)第一阶段之——临时内核页表的创建
- [kernel 启动流程] (第六章)第一阶段之——打开MMU
- [kernel 启动流程] (第七章)第一阶段之——跳转到start_kernel
建议参考文档:
================================================
零、说明
本文是《[kernel 启动流程] (第一章)概述》的延伸,
阅读本文前建议先阅读《[kernel 启动流程] (第一章)概述》
1、kernel启动流程第一阶段简单说明
arch/arm/kernel/head.S
- kernel入口地址对应stext
- 1
-
第一阶段要做的事情,也就是stext的实现内容
- 设置为SVC模式,关闭所有中断
- 获取CPU ID,提取相应的proc info
- 验证tags或者dtb
- 创建临时内核页表的页表项
- 配置r13寄存器,也就是设置打开MMU之后要跳转到的函数。
- 使能MMU
- 跳转到start_kernel,也就是跳转到第二阶段
本文要介绍的是“创建临时内核页表的页表项”的部分。
2、疑问
主要带着以下几个问题去理解
- 什么是MMU?以及其和页表之间的关系?
- 页表内容?页表格式?如何进行创建?
3、对应代码实现
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
__create_page_tables的工作就是创建临时内核页表。而创建临时内核页表则是为了打开MMU功能。
一、MMU和页表简单介绍
以下只是为了理解很简单地进行介绍。
并且主要介绍段式管理的工作原理。
1、MMU简单介绍
MMU是Memory Management Unit的缩写,中文名是内存管理单元,它是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路。
其主要功能如下:
-
将线性地址映射为物理地址
所谓线性地址就是指虚拟地址,而物理地址就是指实际在RAM上的地址 -
提供硬件机制的内存访问授权
2、页表介绍
MMU工作的核心是页表,也就是其根据页表来找到映射关系以及权限。
页表由页表项构成,每一个虚拟地址映射区都会有一个对应的页表项。
arm的页表有如下分类(在本章中使用的是段式管理,所以这里只说明段式管理):
- 段式管理页表
在arm打开MMU初期,使用的是临时内核页表,其类型就是段式页表。
段式页表将4GB的地址空间(32bit系统)划分成4096个1MB的段,因此段式页表有4096个页表项,每个页表项有32bit(4 byte),故段式页表需要16KB的空间
页表项格式如下:
位号 | 功能 |
---|---|
31:20 bit | 段序号 |
20: 0 bit | MMU flag |
3、arm上MMU的工作原理
arm将页表基地址存放在协处理器cp15的c2寄存器上,具体参考《ARM的CP15协处理器的寄存器》。
如下说明:
CP15 中的寄存器 C2 保存的是页表的基地址,即一级映射描述符表的基地址。
arm的MMU会根据虚拟地址计算出其相应页表项的偏移,从cp15的c2寄存器中获取页表基址之后,加上偏移得到对应的页表项地址。后续操作就是根据页表结构来做的。这些动作都是MMU硬件处理!
如果是段式页表的话,再根据段内偏移以及页表项中的物理段基址最终得到对应的物理地址。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
4、临时内核页表及其内容
为了打开MMU,内核需要创建一个临时内核页表,用于kenrel启动过程中的打开MMU的过渡阶段。
并且,使用的是段式管理的方法。
需要建立如下区域的映射
-
打开MMU的函数的代码区域的恒等映射。
恒等映射是指虚拟地址和物理地址一致的映射。
在打开MMU的过程中,CPU还是按照地址顺序一条接着一条去获取指令,也就是说此时PC指针还是指向这段代码区域的物理地址。当MMU打开之后,如果没有恒等映射的话,PC指针此时还是指向这段代码区域的物理地址,但实际上CPU会把PC指针的地址当作虚拟地址进行操作,而造成找不到对应的物理地址。因此,如果做了恒等映射,虚拟地址和物理地址一致,及时CPU会把PC指针的地址当作虚拟地址进行操作,最终仍会映射到相同的物理地址上。 -
kernel镜像的映射
在《[kernel 启动流程] 前篇——vmlinux.lds分析》中,我们已经知道了kernel的链接地址是0xc0008000。
而我们把kernel加载到0x20008000的位置上。kernel镜像的连接区域映射到实际的物理地址的区域。 -
dtb区域的映射
在kernel启动初期需要使用到dtb的东西,因此,在临时内核页表中需要做这些区域的映射。
二、s5pv210映射说明
内存地址和对应段页表项的地址如下:
(s5p210的物理RAM地址偏移是0x20000000,所以段页表项的基地址是0x20004000)
虚拟段 | 物理段 | 对应页表项地址 | 计算方式 | 临时页表映射的值 |
---|---|---|---|---|
0x00000000-0x000FFFFF | - | 0x20004000 | (0x20004000+0x000*4) | - |
0x00100000-0x002FFFFF | - | 0x20004004 | (0x20004000+0x001*4) | - |
0x00200000-0x003FFFFF | - | 0x20004008 | (0x20004000+0x002*4) | - |
… | ||||
0x20100000-0x201FFFFF | 0x20100000-0x201FFFFF | 0x20004804 | (0x20004000+0x201*4) | (0x201<<20) | mmuflags |
… | ||||
0xC0000000-0xC00FFFFF | 0x20000000-0x200FFFFF | 0x20007000 | (0x20004000+0xC00*4) | (0x200<<20) | mmuflags |
0xC0100000-0xC01FFFFF | 0x20100000-0x201FFFFF | 0X20007004 | (0x20004000+0xC01*4) | (0x201<<20) | mmuflags |
… | ||||
0xC0500000-0xC05FFFFF | 0x20500000-0x205FFFFF | 0X20007014 | (0x20004000+0xC05*4) | (0x205<<20) | mmuflags |
… | ||||
0xDFC00000-0xDFCFFFFF | 0x3FC00000-3FCFFFFF | 0x200077F0 | (0x20004000+0xDFC*4) | (0x205<<20) | mmuflags |
0xDFD00000-0xDFDFFFFF | 0x3FD00000-3FDFFFFF | 0x200077F4 | (0x20004000+0xDFC*4) | (0x205<<20) | mmuflags |
… | ||||
0xFFF00000-0xFFFFFFFF | - | 0x20007FFC | (0x20004000+0xFFF*4) | - |
其中(markdown搞个图片不方便,只能用表格和文字描述了。。。。)
- [0x20100000-0x201FFFFF]段是打开MMU的函数的代码区域的恒等映射。
- [0xC0000000-0xC00FFFFF]段到[0xC0500000-0xC05FFFFF]段是kernel镜像的映射
- [0xDFC00000-0xDFCFFFFF]段到[0xDFD00000-0xDFDFFFFF]段是dtb内存区域的映射
后续代码会进行说明。
三、代码分析
1、代码入口
(1)分配物理地址给r8
- 1
- 2
- 3
- 4
物理地址PHYS_OFFSET的定义如下:
arch/arm/Kconfig
- 1
- 2
- 3
- 4
所以config是0x20000000,和s5pv210的ddr起始ram一致。
(2)调用 bl __create_page_tables
- 1
- 2
- 3
__create_page_tables主要用于创建临时内核页表。
__create_page_tables代码总览,后续小节会详细分析:
arch/arm/kernel/head.S
移除掉CONFIG_ARM_LPAE和CONFIG_DEBUG_LL的无关部分
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
2、详解1
- 1
pgtbl 宏用于通过DRAM物理地址来获取页表的物理地址。
前面我们已经知道r8用于存放DRAM的起始物理地址,r4则是要存放计算得到的页表物理地址。
pgtbl 宏如下:
- 1
- 2
- 3
- 4
- 5
通过《[kernel 启动流程] 前篇——vmlinux.lds分析》我们已经知道kernel在放在DRAM上偏移TEXT_OFFSET的位置上。
而linux规定将TEXT_OFFSET之前的PG_DIR_SIZE大小的空间用作临时页表。
所以计算方式如下:
kernel起始地址=DRAM起始物理地址+TEXT_OFFSET=0x20008000
内核页表地址=kernel起始地址-PG_DIR_SIZE=0x20004000
所以代码换算成如下计算:
\rd(r4) = phys(r8) + TEXT_OFFSET
\rd(r4) = \rd(r4) - PG_DIR_SIZE
* TEXT_OFFSET如下:
./arch/arm/Makefile
- 1
- 2
- 3
- 4
- 5
32K的偏移
* PG_DIR_SIZE如下:
arch/arm/kernel/head.S
- 1
- 2
前面也说过页表的大小是16K,刚好和这里是符合的。
最终获得页表物理地址(s5pv210是0x20004000)并且存放在r4寄存器中。
3、详解2
为临时内核页表分配空间之后,接下来的任务就是清空临时内核页表分配空间。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
4、详解3
设置MMU的标识并存放到r7寄存器中,后续需要写入到临时内核页表的页表项中
- 1
具体需要参考前面的文章《[kernel 启动流程] (第三章)第一阶段之——proc info的获取》。
通过这边文章我们已经知道已经将和对应CPU配置的proc info存放到了r10寄存器中。
- 1
- 2
PROCINFO_MM_MMUFLAGS对应proc_info_list中的__cpu_mm_mmu_flags,这个成员用于表示临时页表映射的内核空间的MMU标识。
s5pv210的这个值对应为PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
PMD_SECT_AF | PMD_FLAGS_UP。
最终,将这个成员对应的值写入到r7寄存器中。
5、详解4
开始进行映射表的创建,首先是创建恒等映射。
代码总览如下
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
也就是对打开mmu的函数,也就是__turn_mmu_on进行恒等映射。
所谓恒等映射,就是将物理地址相应到相同的虚拟地址上。
__turn_mmu_on代码如下
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
__turn_mmu_on和__turn_mmu_on_end标识了其起始代码地址和末端代码地址。
通过System.map查看其连接地址如下:
- 1
- 2
kernel将这些链接地址存放到了__turn_mmu_on_loc中。
- 1
- 2
- 3
- 4
也就是说临时内核映射表需要为__turn_mmu_on添加如下页表
虚拟段 | 物理段 | 对应页表位置 | 计算方式 | 临时页表映射的值 |
---|---|---|---|---|
0x20100000-0x201FFFFF | 0x20100000-0x201FFFFF | 0x20004804 | (0x20004000+0x201*4) | (0x201<<20) | mmuflags |
代码分析如下
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
通过上述,就完成了__turn_mmu_on代码部分的恒等映射。
6、详解5
以下对kernel的内核空间进行映射。
通过System.map中可以看出kernel的连接区域如下:
- 1
- 2
其相应在物理地址上的内存区域是
0x20008000到0x20547d74的区域。
因此后续要创建物理区[0x20008000-0x20547d74]到内核映射区[0xc0008000到0xc0547d74]的内存映射。
对应如下:
虚拟段 | 物理段 | 对应页表位置 | 计算方式 | 临时页表映射的值 |
---|---|---|---|---|
0xC0000000-0xC00FFFFF | 0x20000000-0x200FFFFF | 0x20007000 | (0x20004000+0xC00*4) | (0x200<<20) | mmuflags |
0xC0100000-0xC01FFFFF | 0x20100000-0x201FFFFF | 0X20007004 | (0x20004000+0xC01*4) | (0x201<<20) | mmuflags |
… | ||||
0xC0500000-0xC05FFFFF | 0x20500000-0x205FFFFF | 0X20007014 | (0x20004000+0xC05*4) | (0x205<<20) | mmuflags |
代码分析如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
7、详解6
后续就是创建DTB的映射区。
方法和上述类似,主要是从r2中提取dtb的物理内存地址,计算出对应虚拟地址之后,进行映射表创建。
并且其固定映射了两个段。所以DTB的大小不能超过1M。
例如:tiny210中ubootlog
- 1
所以应该创建 DTB arm区[0x3fc00000-0x3fe00000] 到 DTB映射区[0xdfc00000-0xdfe00000]的映射表。
对应如下:
虚拟段 | 物理段 | 对应页表位置 | 计算方式 | 临时页表映射的值 |
---|---|---|---|---|
0xDFC00000-0xDFCFFFFF | 0x3FC00000-3FCFFFFF | 0x200077F0 | (0x20004000+0xDFC*4) | (0x205<<20) | mmuflags |
0xDFD00000-0xDFDFFFFF | 0x3FD00000-3FDFFFFF | 0x200077F4 | (0x20004000+0xDFC*4) | (0x205<<20) | mmuflags |
代码分析如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
上述就完成了dtb所在的两个段的映射。
综上,临时内核页表就创建完成,并且放在了0x20004000的物理地址上。