__create_page_tables与__enable_mmu(ARM920芯片)说明临时页表的创建
linux内核初始化做完芯片类型检查和CPU启动以后就进入到MMU的启动,这么早启动MMU主要是为了后面建立虚拟映射循序渐进的提前做好部分准备工作。在MMU信息使能初期,首先要建立映射表,这个是这篇文章的两个主题__create_page_tables与__enable_mmu。关于ARM芯片页表概念之前需要了解cache-mmu-sdram内存管理硬件概念,s3c2440芯片裸机文章通过基本实验了解基本概念。
汇编代码head.S文件中创建临时页表,使用的是arm的L1主页表,L1主页表也称为段页表(section page table,说白了就是采用段式管理而不是页式管理),它将4GB的地址空间分成若干个1MB的段(section),因此L1页表包含4096个页表项(section entry);每个页表项是32 bits(4 bytes), 所以L1页表占用 4096*4 = 16k的内存空间;L1页表,实际是给4G的虚拟地址空间中每个1M空间的段,映射其对应的物理内存地址是哪里,通过在对应的页表项写入物理地址值及MMU相关内容实现,在临时页表中,并没有写满所有表项,为这时需考虑的只有内核代码。
1. 初始化跳转页表宏
1.1. 初始化调用接口
- (*1)跳转__create_page_tables完成MMU映射页表创建
- (*2)打开MMU,开启虚拟地址的映射
ENTRY(stext)
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
@ and irqs disabled
mrc p15, 0, r9, c0, c0 @ get processor id @ c0 is source reg, r9 is des reg
bl __lookup_processor_type @ r5=procinfo r9=cpuid
movs r10, r5 @ invalid processor (r5=0)?
beq __error_p @ yes, error 'p'
bl __lookup_machine_type @ r5=machinfo
movs r8, r5 @ invalid machine (r5=0)?
beq __error_a @ yes, error 'a'
bl __create_page_tables -------------------- (*1)
/*
* The following calls CPU specific code in a position independent
* manner. See arch/arm/mm/proc-*.S for details. r10 = base of
* xxx_proc_info structure selected by __lookup_machine_type
* above. On return, the CPU will be ready for the MMU to be
* turned on, and r0 will hold the CPU control register value.
*/
ldr r13, __switch_data @ address to jump to after 将列表__switch_data存到r13中后面会跳到该列表出
@ mmu has been enabled 将程序段__enable_mmu的地址存到lr中。
adr lr, __enable_mmu @ return (PIC) address -------------------- (*2)
// 此命令将导致程序段__arm920_setup的执行,后面会将到。
// r10中存放的基地址是从__lookup_processor_type中得到的,如上面movs r10, r5
add pc, r10, #PROCINFO_INITFUNC @ __arm920_proc_info->__arm920_setup 文件:proc_arm920.S文件中
2. __create_page_tables宏
/*
* Setup the initial page tables. We only setup the barest
* amount which are required to get the kernel running, which
* generally means mapping in the kernel code.
*
* r8 = machinfo
* r9 = cpuid
* r10 = procinfo
*
* Returns:
* r0, r3, r6, r7 corrupted
* r4 = physical page table address
*/
.type __create_page_tables, %function
__create_page_tables:
pgtbl r4 @ page table address ----------------- (1)
/*
* Clear the 16K level 1 swapper page table,4096entry*4byte=16k
*/ ----------------- (2)
mov r0, r4 ---------- (2.1)
mov r3, #0 ---------- (2.2)
add r6, r0, #0x4000 ---------- (2.3)
1: str r3, [r0], #4 ---------- (2.4)
str r3, [r0], #4 ---------- (2.5)
str r3, [r0], #4 ---------- (2.6)
str r3, [r0], #4 ---------- (2.7)
teq r0, r6 ---------- (2.8)
bne 1b ---------- (2.9)
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags ---------- (2.10)
/*
* Create identity mapping for first MB of kernel to
* cater for the MMU enable. This identity mapping
* will be removed by paging_init(). We use our current program
* counter to determine corresponding section base address.
* 下面三行设置了kernel的section的页表项,将起始物理地址为0x30008000
* 的1M内存空间映射到虚拟地址为0x30008000。
*/ ----------------- (3)
mov r6, pc, lsr #20 @ start of kernel section ---------- (3.1)
orr r3, r7, r6, lsl #20 @ flags + kernel base ---------- (3.2)
str r3, [r4, r6, lsl #2] @ identity mapping ---------- (3.3)
/*
* Now setup the pagetables for our kernel direct
* mapped region.
*/
/*
* 因为KERNEL_START是内核的起始虚拟地址(0xC0008000),KERNEL_END为内核
* 的结束虚拟地址,所以下面的代码实际上是将物理地址为kernel的起始地址
* (0x30008000)的一段内存空间(大小为内核映像文件的大小)映射到虚拟地址
* 0xC0008000。
*/ ----------------- (4)
add r0, r4, #(KERNEL_START & 0xff000000) >> 18 ---------- (4.1)
str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! ---------- (4.2)
ldr r6, =(KERNEL_END - 1) ---------- (4.3)
add r0, r0, #4 ---------- (4.4)
add r6, r4, r6, lsr #18 ---------- (4.5)
1: cmp r0, r6 ---------- (4.6)
add r3, r3, #1 << 20 ---------- (4.7)
strls r3, [r0], #4 ---------- (4.8)
bls 1b ---------- (4.9)
/*
* Then map first 1MB of ram in case it contains our boot params.
*/
/*
* 下面的代码用来设置RAM中起始地址为0x30000000、大小为1M虚拟地址的页表,之所以
* 要设置这个页表项的原因是该区域起始地址为0x30000100存储着boot params。因此需
* 要为它建立map,这样开启MMU后就可以访问这些参数了。
*/ ----------------- (5)
add r0, r4, #PAGE_OFFSET >> 18 ---------- (5.1)
orr r6, r7, #(PHYS_OFFSET & 0xff000000) ---------- (5.2)
.if (PHYS_OFFSET & 0x00f00000) ---------- (5.3)
orr r6, r6, #(PHYS_OFFSET & 0x00f00000) ---------- (5.4)
.endif ---------- (5.5)
str r6, [r0] ---------- (5.6)
mov pc, lr ----------------- (6)
.ltorg
页表映射过程包含了四个隔离宏,针对不同类型的宏开关决定是否将对应的代码包含进来,上面代码直接删除需要自己分析。函数调用之前第一步处理结果将R8存储machine_desc的物理地址,R9存储CPU ID,R10存储procinfo。函数返回值r4保存了转换表的在内存中物理地址,打开MMU开关是需要告知其这个参数的值。具体的处理流程说明如下:
- 宏pgtbl将r4设置成页表的物理地址,KERNEL_RAM_PADDR - 0x4000 = 0x4000,用来存放临时页表。head.S文件前面的代码有pgtbl宏声明的地方如下:.macro pgtbl, rd -> ldr \rd, =(KERNEL_RAM_PADDR - 0x4000)。对于s3c2440,KERNEL_RAM_PADDR是内核其实地址,也就是0x30008000,计算以后临时页表防止位置是0x30004000。
- (2.1 - 2.3)主要完成清楚前面变量的初始给定。r0保存表现开始地址0x30004000,r6保存表现结束位置地址0x30008000,r3保存空值0。(2.4 - 2.9)将r3中的0值分别放到0x30004000 - 0x30008000中间的位置,按照每4个4直接的寄存器作为一组进行。(2.10)把R10保存的地址再加PROCINFO_MM_MMUFLAGS后,取该地址的值赋给R7,R10先前已保存了procinfo函数指针数组首地址,再加8就是proc_info_list结构变量的__cpu_mm_mmu_flags成员地址,取该成员的值赋给R7。__cpu_mm_mmu_flags这个位置主要是保存了MMU段描述符中后面对本映射的一个管理相关的内容。
- 是把L1页表中当前运行地址所在的段(当前以汇编代码运行的内核代码段,运行在物理内存起初的部分,取值应在0x0到0x00100000范围内,可以看看KERNEL_RAM_PADDR值为0x00008000,可见从L1页表的分段角度,在第一个段即地址0x0-0x00100000段范围内)的表项写入映射关系,事实上重点是理解为什么要把这里也做映射。之所以要为这个物理地址的段也做映射,是因为现在MMU没开启,过一会就会开启MMU,按说内核代码段的内容在做了映射后,CPU给MMU的PC值是虚拟地址,但由于CPU流水线的预取指功能,还是会有一些PC值还不是虚拟地址而仍然是物理地址,这样的话如果MMU内部没有相应的映射关系,将不知道访问实际物理内存哪部分,导致出现问题,所以这里需要把这部分物理地址在L1页表的相应表项也写入,本质是PA = PA(很多参考文章写PA = VA,其实道理是一样的,但那样表述容易给人以误解,因为开启MMU后,CPU发出的访问按说都是虚拟地址,但确实有一些其实还是物理地址,如果不做映射将让MMU无从知道该访问哪里,所以这里写L1页表的相关物理页表项,其实是做PA = PA的映射给MMU)。(3.1)将当前PC运行地址的低20bit抹去,只保留高12bit。(3.2)将PC高位地址段和__cpu_mm_mmu_flags位置的段控制信息或形成最终的实际的一个entry内容。(3.3)最后将(3.2)步骤的结果放置在从0x30004000开始和当前实际物理地址有关的某个偏移处即可。
- 下面这堆内容,核心目的是为kernel镜像(即内核代码段)做L1页表映射,即KERNL_START到KERNEL_END建立内存映射,这两个宏是编译时产生的,所以它们是虚拟地址,而内核代码段在物理内存的实际位置是从物理内存起初的部分(L1页表角度看就是在第一个段内),后续要开启MMU了,即CPU发给MMU的地址将是虚拟地址了,所以这里要先把内核代码做映射,映射方式方法和前面是一样的,把虚拟的0xc0008000到KERNEL_END(一般内核代码长度为3-4M,所以KERNEL_END值估计为0xc03XXXXX),映射到物理地址的0x0到0x3段内(假定内核代码为3-4M长度大小)。(4.1)R4已保存了页表物理起始地址,对于(KERNEL_START & 0xff000000),这是为了获取高12bit位的段值(为什么不直接以0xfff00000去与?),所谓右移18位,其实就是先右移20位获取到段值,然后左移2位取得所在表项在L1页表的偏移值(因为每个表项占4个字节),最后加上R4即L1页表物理基地址,得出该表项实际的物理地址保存在R0(实际值为0x20004000+0xC00*4 = 0x20007000)(4.2)把R3即内核物理起始地址段值和PROCINFO_MM_MMUFLAGS的或运算结果即映射结果写入内核起始虚拟地址所在的L1表项,对于我们这里就是写入的是0x0与PROCINFO_MM_MMUFLAGS的或运算结果,这里的疑问是:为什么不一开始就用0xfff00000去与运算,而是分两次?(4.3)r6为内核代码的尾部虚拟地址(4.4)R0保存下一个即将要填写的L1页表项的物理地址(4.5)R6本身是内核代码的尾部虚拟地址,右移18位(右移20再左移2,先获取段值然后算出所在L1页表项的偏移),再加上R4值,最后R6值是内核代码结尾的所在L1页表的表项的物理地址。(4.6)-(4.9)循环实现内核区域的一次映射,每一个entry映射时1M(1<<20)
- 通常kernel的启动参数由bootloader放到了物理内存的第1个M上,所以需要为RAM上的第1个M建立映射下面这堆内容,核心目的就是确保RAM的第1个M建立映射; 事实上上面已为PHYS_OFFSET + TEXT_OFFSET建立了映射(如果TEXT_OFFSET小于0x00100000即1MB的话),我们这里TEXT_OFFSET值为0x8000,即也为SDRAM的第一个M建立了映射; 说白了,这里的用意就是说无论上面代码如何,均为SDRAM的第一个M建立映射,事实上是为了考虑TEXT_OFFSET值大于0x00100000即1MB的情况,即内核起始代码超出了第一个段的范围的情况
- mov pc, lr 返回调用
映射结束以后并且映射了如下部分的页表映射: 物理内存第一MB(自己映射自己,因为开启MMU后仍有一些访问是以物理地址发到MMU)、内核代码段(基本是0xc00段到0xc03段,映射至0x0段到0x003段), 后续准备开启MMU。而寄存器结果是:R4: 页表起始处物理地址;R8: machine info,struct machine_desc的基地址;R9: cpu id;R10: procinfo,struct proc_info_list的基地址
2.3. 涉及宏
宏 | 默认值 | 定义 |
KERNEL_RAM_VADDR | 0xC0008000 | 内核在内存中的虚拟地址 |
PAGE_OFFSET | 0xC0000000 | 内核虚拟地址空间的起始地址 |
TEXT_OFFSET | 0x00008000 | 内核起始位置相对于内存起始位置的偏移 |
PHYS_OFFSET | 构架相关 | 物理内存的起始地址 |
3. 开启MMU
MMU开启过程包含两部分代码,其中第一部分代码首先是逐渐MMU的常见公共控制位放在r0寄存器中。然后将r4表现其实地址和r5域控制分别赋值给协寄存器c2和c3。然后将r0寄存器结果赋值给c1寄存器之后可以打开MMU了。
/*
* Setup common bits before finally enabling the MMU. Essentially
* this is just loading the page table pointer and domain access
* registers.
*/
.type __enable_mmu, %function
__enable_mmu:
#ifdef CONFIG_ALIGNMENT_TRAP
orr r0, r0, #CR_A
#else
bic r0, r0, #CR_A
#endif
#ifdef CONFIG_CPU_DCACHE_DISABLE
bic r0, r0, #CR_C
#endif
#ifdef CONFIG_CPU_BPREDICT_DISABLE
bic r0, r0, #CR_Z
#endif
#ifdef CONFIG_CPU_ICACHE_DISABLE
bic r0, r0, #CR_I
#endif
mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
domain_val(DOMAIN_IO, DOMAIN_CLIENT))
mcr p15, 0, r5, c3, c0, 0 @ load domain access register
mcr p15, 0, r4, c2, c0, 0 @ load page table pointer
b __turn_mmu_on
/*
* Enable the MMU. This completely changes the structure of the visible
* memory space. You will not be able to trace execution through this.
* If you have an enquiry about this, *please* check the linux-arm-kernel
* mailing list archives BEFORE sending another post to the list.
*
* r0 = cp#15 control register
* r13 = *virtual* address to jump to upon completion
*
* other registers depend on the function called upon completion
*/
.align 5
.type __turn_mmu_on, %function
__turn_mmu_on:
mov r0, r0
mcr p15, 0, r0, c1, c0, 0 @ write control reg
mrc p15, 0, r3, c0, c0, 0 @ read id reg
mov r3, r3
mov r3, r3
mov pc, r13 @ __switch_data位置开始执行命令
imx6ull ARM-V8内核临时页表
前面几节,我们已经看到了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
1
2
其相应在物理地址上的内存区域是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开启后,内核只能访问虚拟地址空间,无法访问物理地址空间,所以就需要做相应的映射。