2023-06-11
原文作者:奇小葩 原文地址:https://blog.csdn.net/u012489236/category_9614673.html

前面几节,我们已经看到了x86的分段和分页硬件单元把逻辑地址转换为线性地址,再由线性地址转换到物理地址的基本原理,那么这几章我们来主要是内核是怎么实现页表的创建,本章基于imx6ull和qemu来学习整个过程,其主要内容如下:

  1. imx6ull的映射机制
  2. 内核启动主要概述
  3. 内核临时页表

1. imx6ull的映射机制

在之前页式存储管理中,我们主要是针对x86处理器来描述虚拟地址到物理地址的转换,对于ARM来说原理基本类似,下面是我们使用armv7架构图,这张图展示了ARM使用不同方式映射的地址查找过程

202306111239572751.png

描述符的分类为:

  1. Section: 20位,只支持一级页表,Linux中在建立临时页表的时候采用这种方式
  2. Large pages:16位,64KB的页表大小,支持二级页表
  3. Small pages:12位,页表大小为4Kb,Linux中在建立永久页表采用这种方式
  4. SuperSection:24位,可选,主要是支持大物理地址扩展必须支持

对于4K的转换过程跟X86的转换过程基本一样,只是对于页全局目录表变成了其他的寄存器,其转换过程如下

202306111239579812.png

  1. 根据TTBRCR寄存器和虚拟地址使用判断使用那个页表及地址寄存器(TTBR0或TTBR1),防止一级页表的基地址
  2. 处理器根据虚拟地址的bit[31:20]作为索引,在一级页表中查找页表项,一级页表一共又4096个页表项(4K个entry)
  3. 一级页表的表项中存放了二级页表的基地址,处理器根据虚拟地址bit[19:12]作为索引值,在二级页表中找到对应的表,二级页表一共256个表项
  4. 二级页表的页表项里面存放了4KB页的物理基地址,加上最后的偏移量bit[11:0],最终寻找到物理内存

在4KB的映射的一级页表和二级页表的表项其实跟x86基本类似,页包含了很多的其他会议

一级页表项:

202306111239587903.png
二级页表项

202306111239598534.png

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
  1. PAGE_OFFSET表示内核空间的偏移,这里是0xc0000000,也就是内核映射区的起始段的起始地址。将PAGE_OFFSET左移动(SECTION_SHIFT - PMD_ORDER)后得到该地址所在段的段页表项的地址偏移,最后将段页表项的地址偏移+临时内核页表地址得到0xc0000000所在段的段页表项的物理地址,并放到r0中,而r6中存放内核映射区的末尾地址。
  2. 将DDR起始物理地址(r8)或上MMU的表示(r7),得到0xc0000000所在段的段页表项内容,存放到r3中。
  3. 将内核映射区的末尾地址(r6)左移(SECTION_SHIFT - PMD_ORDER)后得到其所在段的段页表项的物理地址
  4. 将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]
  1. 首先将dtb起始物理地址(r2)左移SECTION_SHIFT,存放在r0中,再将r0右移SECTION_SHIFT得到这个物理内存段的地址(和上一步简单理解就是把低20位清零),计算dtb物理内存段(r0)对应DRAM起始地址(r8)的偏移,存放在r3中,将偏移(r3)加上,内核空间起始地址PAGE_OFFSET,得到要映射到的虚拟地址
  2. 取要映射的虚拟地址的段的页表项的地址,存放在r3中,将物理内存段地址(r0)或上mmu标识(r7),得到对应页表项值,存放到r6中。将页表项值(r6)写入到页表项中([r3]),然后r3+4,获取到下一个页表项的地址,页表项值+0x100000,得到下一个页表项应该写入的页表项值,将页表项值(r6)写入到页表项中([r3])

总结,create_page_table完成了3种地址映射的页表空间:

  1. turn_mmu_on所映射的1M空间的屏映射:那么为什么要做映射呢?在执行开启MMU指令之前,CPU取指是在0x80008000附件,如果只做kernel_image的映射,开启MMU后,CPU所看到的地址就全变了,那么就可能无法执行。完成平映射后,就可以完美解决从0x8xxxxxxx到0xcxxxxxxx的过渡。
  2. kernel_image的线性映射:kernel编译链接的入口地址在0xc0008000,但其物理地址不等于链接的虚拟地址,需要将物理地址映射到对应的虚拟地址空间。
  3. atags(DTB)所在的1M空间的线性映射:当MMU开启后,内核只能访问虚拟地址空间,无法访问物理地址空间,所以就需要做相应的映射。
阅读全文