前面几节,我们已经看到了x86的分段和分页硬件单元把逻辑地址转换为线性地址,再由线性地址转换到物理地址的基本原理,那么这几章我们来主要是内核是怎么实现页表的创建,本章基于imx6ull和qemu来学习整个过程,其主要内容如下:
- imx6ull的映射机制
- 内核启动主要概述
- 内核临时页表
1. imx6ull的映射机制
在之前页式存储管理中,我们主要是针对x86处理器来描述虚拟地址到物理地址的转换,对于ARM来说原理基本类似,下面是我们使用armv7架构图,这张图展示了ARM使用不同方式映射的地址查找过程
描述符的分类为:
- Section: 20位,只支持一级页表,Linux中在建立临时页表的时候采用这种方式
- Large pages:16位,64KB的页表大小,支持二级页表
- Small pages:12位,页表大小为4Kb,Linux中在建立永久页表采用这种方式
- SuperSection:24位,可选,主要是支持大物理地址扩展必须支持
对于4K的转换过程跟X86的转换过程基本一样,只是对于页全局目录表变成了其他的寄存器,其转换过程如下
- 根据TTBRCR寄存器和虚拟地址使用判断使用那个页表及地址寄存器(TTBR0或TTBR1),防止一级页表的基地址
- 处理器根据虚拟地址的bit[31:20]作为索引,在一级页表中查找页表项,一级页表一共又4096个页表项(4K个entry)
- 一级页表的表项中存放了二级页表的基地址,处理器根据虚拟地址bit[19:12]作为索引值,在二级页表中找到对应的表,二级页表一共256个表项
- 二级页表的页表项里面存放了4KB页的物理基地址,加上最后的偏移量bit[11:0],最终寻找到物理内存
在4KB的映射的一级页表和二级页表的表项其实跟x86基本类似,页包含了很多的其他会议
一级页表项:
二级页表项
2. 内核启动主要概述
当U-boot启动后,通过r1,r2来将启动参数传递给内核,arm的启动(arch/arm/kernel/head.S)中kernel热人口地址对应stext,其主要做了以下几件事情
- 设置svc模式,关闭所有中断
- 获取CPU ID,提取相应的proc info
- 验证tags或者dtb
- 创建临时内核页表的页表项
- 配置r13寄存器,也就是设置打开MMU之后要跳转到的函数
- 使能MMU
- 跳转到start_kernel,也就是跳转到第二阶段
kernel里面的所有符号在链接时,都使用了虚拟地址值。在完成基本的初始化后,kernel代码将跳到第一个C语言函数start_kernl来 执行,这些虚拟地址必须能够对它所存放在真正内存位置,否则运行将为出错。为此,CPU必须开启MMU,但在开启MMU前,必须为虚拟地址到 物理地址的映射建立相应的面表。对应的各个宏的解释如下,为后面分析做好判断
宏 | 默认值 | 定义 |
---|---|---|
KERNEL_RAM_VADDR | 0xc0008000 | 内核在内存的虚拟地址 |
PAGE_OFFSET | 0xc0000000 | 内核虚拟地址空间的起始地址 |
TEXT_OFFSET | 0x00008000 | 内核起始位置相对于内存起始位置的偏移 |
PHYS_OFFSET | 0x80000000 | 物理内存的起始地址 |
3. 内核临时页表项
内核通过__create_page_tables创建临时页表项,我们来看以一下其处理流程
__create_page_tables:
pgtbl r4, r8 @ page table address
首先就使用pgtbl,而这个是一个宏,定义如下:
.macro pgtbl, rd, phys
add \rd, \phys, #TEXT_OFFSET
sub \rd, \rd, #PG_DIR_SIZE
.endm
其实际上是这样
add r4, r8, #TEXT_OFFSET
sub r4, r4, #PG_DIR_SIZE
由r8为PHYS_OFFSET,那么r4的值为PHYS_OFFSET+TEXT_OFFSET-PG_DIR_SIZE=0x80000000+0x00008000-0x4000=0x80004000,将r4设置成页表的基地址,页表将4G的地址空间分成若干个1M的段,因此页表包含4096个页表项。每个页表项是4字节,那么页表就占用4096*4=16K的内存空间。之后就将这16K的页表项清0
mov r0, r4
mov r3, #0
add r6, r0, #PG_DIR_SIZE
1: str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
由于IMX不支持CONFIG_ARM_LPAE((Large Physical Address Extensions)大型物理地址扩展,那么就直接运行下面的
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
/*
* Create identity mapping to cater for __enable_mmu.
* This identity mapping will be removed by paging_init().
*/
adr r0, __turn_mmu_on_loc
ldmia r0, {r3, r5, r6}
sub r0, r0, r3 @ virt->phys offset
add r5, r5, r0 @ phys __turn_mmu_on
add r6, r6, r0 @ phys __turn_mmu_on_end
mov r5, r5, lsr #SECTION_SHIFT
mov r6, r6, lsr #SECTION_SHIFT
首先从proc_info_list结构体获取__cpu_mm_mmu_flags,该字段包含了存储空间访问权限等,并存储在r7中,然后取__turn_mmu_on_loc处的地址保存在r0,然后从这块内存中读取3个word到r3,r5,r6中,那么这3个word里面放的什么呢?
__turn_mmu_on_loc:
.long .
.long __turn_mmu_on
.long __turn_mmu_on_end
那么sub r0, r0, r3意义就很明确了,就是求出__turn_mmu_on_loc这个标号的物理地址和虚拟地址之间的偏移量。然后根据这个偏移量求出__turn_mmu_on的物理地址r5和__turn_mmu_on_end的物理地址r6,后面就是最关键的,mov r5, r5, lsr #SECTION_SHIFT通过r5的高12位,通过右移20位得到,最终得到kernel的section机制,r5存放起始地址的段序号,r6存放末地址的段序号。
1: orr r3, r7, r5, lsl #SECTION_SHIFT @ flags + kernel base
str r3, [r4, r5, lsl #PMD_ORDER] @ identity mapping
cmp r5, r6
addlo r5, r5, #1 @ next section
blo 1b
该过程是将r7(也就是段的flag)和r5左移20位,也就是段页表项的内容,然后将段页表项的值写到对应的段页表项中,段页表项的地址=段页表起始地址(r4)+段序号r5*段页表项的size,最后通过判断是否写到__turn_mmu_on_end地址,如果没有写入,继续写入下一段,该过程主要是完成__turn_mmu_on代码的映射。
/*
* Map our RAM from the start to the end of the kernel .bss section.
*/
add r0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER) ---------------- (1)
ldr r6, =(_end - 1)
orr r3, r8, r7 ------------------(2)
add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER) ------------------(3)
1: str r3, [r0], #1 << PMD_ORDER ------------------(4)
add r3, r3, #1 << SECTION_SHIFT
cmp r0, r6
bls 1b
- PAGE_OFFSET表示内核空间的偏移,这里是0xc0000000,也就是内核映射区的起始段的起始地址。将PAGE_OFFSET左移动(SECTION_SHIFT - PMD_ORDER)后得到该地址所在段的段页表项的地址偏移,最后将段页表项的地址偏移+临时内核页表地址得到0xc0000000所在段的段页表项的物理地址,并放到r0中,而r6中存放内核映射区的末尾地址。
- 将DDR起始物理地址(r8)或上MMU的表示(r7),得到0xc0000000所在段的段页表项内容,存放到r3中。
- 将内核映射区的末尾地址(r6)左移(SECTION_SHIFT - PMD_ORDER)后得到其所在段的段页表项的物理地址
- 将r3存入当前段页表项中([r0]),然后将r0加上4,得到下一个段页表项的地址,更新r3中的页表项值为下一个段的页表项值,也就是直接加上,判断是否已经到达内核映射区的末尾,如果不是就进入下一个循环。
从上面可以看出,这段主要是完成对kernel内核空间进行映射,我们可以通过内核的System.map文件可以看出内核的起始和结束地址为
c0008000 T _text
c10e8eec B _end
其相应在物理地址上的内存区域是0x80008000到0x810e8eec区域,因此就完成了创建物理区[0x80008000-0x810e8eec]到内核映射区[0xc0008000-0xc10e8eec]的内存映射。
接下来代码就完成了DTB的映射,其代码如下
mov r0, r2, lsr #SECTION_SHIFT ---------------(1)
movs r0, r0, lsl #SECTION_SHIFT
subne r3, r0, r8
addne r3, r3, #PAGE_OFFSET
addne r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER) ---------------(2)
orrne r6, r7, r0
strne r6, [r3], #1 << PMD_ORDER
addne r6, r6, #1 << SECTION_SHIFT
strne r6, [r3]
- 首先将dtb起始物理地址(r2)左移SECTION_SHIFT,存放在r0中,再将r0右移SECTION_SHIFT得到这个物理内存段的地址(和上一步简单理解就是把低20位清零),计算dtb物理内存段(r0)对应DRAM起始地址(r8)的偏移,存放在r3中,将偏移(r3)加上,内核空间起始地址PAGE_OFFSET,得到要映射到的虚拟地址
- 取要映射的虚拟地址的段的页表项的地址,存放在r3中,将物理内存段地址(r0)或上mmu标识(r7),得到对应页表项值,存放到r6中。将页表项值(r6)写入到页表项中([r3]),然后r3+4,获取到下一个页表项的地址,页表项值+0x100000,得到下一个页表项应该写入的页表项值,将页表项值(r6)写入到页表项中([r3])
总结,create_page_table完成了3种地址映射的页表空间:
- turn_mmu_on所映射的1M空间的屏映射:那么为什么要做映射呢?在执行开启MMU指令之前,CPU取指是在0x80008000附件,如果只做kernel_image的映射,开启MMU后,CPU所看到的地址就全变了,那么就可能无法执行。完成平映射后,就可以完美解决从0x8xxxxxxx到0xcxxxxxxx的过渡。
- kernel_image的线性映射:kernel编译链接的入口地址在0xc0008000,但其物理地址不等于链接的虚拟地址,需要将物理地址映射到对应的虚拟地址空间。
- atags(DTB)所在的1M空间的线性映射:当MMU开启后,内核只能访问虚拟地址空间,无法访问物理地址空间,所以就需要做相应的映射。