本文主要是介绍ARM架构内核启动分析-head.S(1.3、stext分析之内存临时页表建立),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
1.2.4、创建临时页表:
对于创建临时页表,使用的是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相关内容实现,在临时页表中,并没有写满所有表项,因为这时需考虑的只有内核代码。
另外注意在此时,R8存储machine_desc的物理地址;R9存储CPU ID,R10存储procinfo。
/*创建临时页表*/
bl __create_page_tables
__create_page_tables:
pgtbl r4 @ page table address
通过宏pgtbl将r4设置成页表的物理地址,KERNEL_RAM_PADDR - 0x4000 = 0x4000,即内核的前面是页表,head.S文件前面的代码有pgtbl宏声明的地方如下:
.macro pgtbl, rd
定义了一个宏pgtbl等于寄存器rd的值
ldr \rd, =(KERNEL_RAM_PADDR - 0x4000)
把(KERNEL_RAM_PADDR - 0x4000)这个地址值赋给rd寄存器,一般这个地址用来存放临时页表,这也就让宏pgtbl的值为(KERNEL_RAM_PADDR - 0x4000 = 0x4000)
/*
* Clear the 16K level 1 swapper page table
*/
/*1、下面是把页表清零,页表是在内核之前16KB位置*/
mov r0, r4
让R0也保存页表起始物理地址
mov r3, #0
置R3为0
add r6, r0, #0x4000
让R6保存页表结尾地址
1: str r3, [r0], #4
把R3的值(0)存储在R0指向地址处,并且R0加4即指向的地址加4
str r3, [r0], #4
不断这样操作,意即不断清零R0到R6地址处即页表,直到R0指向地址和R6相同
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
/*2、把R10保存的地址再加PROCINFO_MM_MMUFLAGS后,取该地址的值赋给R7,R10先前已保存了procinfo的地址,再加8就是proc_info_list结构变量的__cpu_mm_mmu_flags成员地址,取该成员的值赋给R7,这个成员的具体意义与arm MMU相关,需要仔细查看arm手册,这是下一阶段学习的重点!*/
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
/*
* 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.
*/
/*3、下面这堆内容,其实是把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)*/
mov r6, pc
让R6获取到PC值
/*然后获取到kernel的section的物理地址保存在R6,R6 = 0x00008xxx(PC当前值)*/
mov r6, r6, lsr #20 @ start of kernel section
通过pc值的高12位(右移20位),得到kernel的section,并存储到r6中, 因为当前是通过运行时地址得到的kernel的section,因而是物理地址, 所以R6 = 0x00008xxx << 20 = 0
/*然后获取到写页表应该写什么值,并保存在R3,R3 = R7 + 0 = R7 = PROCINFO_MM_MMUFLAGS,R6刚才已保存了当前正在执行的内核代码(PC)所在的段值,这里左移20位得到该段地址起始值(这里即0x00000000,含义即物理地址的kernel base值), 再加上R7即PROCINFO_MM_MMUFLAGS,这个就是要写入L1页表表项的内容(具体为什么填入这个和MMU有关,还需后续仔细研究!)*/
orr r3, r7, r6, lsl #20 @ flags + kernel base
flags + kernel base,得到页表中需要设置的值保存在R3
/*接下来实际写页表,每个L1页表的表项长度为4字节,为了定位这个页表项在L1页表的位置或者说算出其在L1页表的偏移,所以左移2位即乘以4得出偏移值,再加上基地址R4,得出写在哪个L1页表的表项*/
str r3, [r4, r6, lsl #2] @ identity mapping
R4已保存了页表物理起始地址,R6保存了页表物理结束地址,上一步R3也保存了需要设置页表的值,这里把R3的值写入从R4+R6*4 = R4字节空间处,即写页表的第一个条目的值为R3值。
/*
* Now setup the pagetables for our kernel direct
* mapped region.
*/
/*4、下面这堆内容,核心目的是为kernel镜像(即内核代码段)做L1页表映射,即KERNL_START到KERNEL_END建立内存映射,这两个宏是编译时产生的,所以它们是虚拟地址,而内核代码段在物理内存的实际位置是从物理内存起初的部分(L1页表角度看就是在第一个段内),后续要开启MMU了,即CPU发给MMU的地址将是虚拟地址了,所以这里要先把内核代码做映射,映射方式方法和前面是一样的,把虚拟的0xc0008000到KERNEL_END(一般内核代码长度为3-4M,所以KERNEL_END值估计为0xc03XXXXX),映射到物理地址的0x0到0x3段内(假定内核代码为3-4M长度大小)*/
add r0, r4, #(KERNEL_START & 0xff000000) >> 18
R4已保存了页表物理起始地址,对于(KERNEL_START & 0xff000000),这是为了获取高12bit位的段值(为什么不直接以0xfff00000去与?),
所谓右移18位,其实就是先右移20位获取到段值,然后左移2位取得所在表项在L1页表的偏移值(因为每个表项占4个字节),
最后加上R4即L1页表物理基地址,得出该表项实际的物理地址保存在R0(实际值为0x20004000+0xC00*4 = 0x20007000)
str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
把R3即内核物理起始地址段值和PROCINFO_MM_MMUFLAGS的或运算结果即映射结果写入内核起始虚拟地址所在的L1表项,
对于我们这里就是写入的是0x0与PROCINFO_MM_MMUFLAGS的或运算结果,这里的疑问是:为什么不一开始就用0xfff00000去与运算,而是分两次?
ldr r6, =(KERNEL_END - 1)
r6为内核代码的尾部虚拟地址
add r0, r0, #4
R0保存下一个即将要填写的L1页表项的物理地址
add r6, r4, r6, lsr #18
R6本身是内核代码的尾部虚拟地址,右移18位(右移20再左移2,先获取段值然后算出所在L1页表项的偏移),再加上R4值,最后R6值是内核代码结尾的所在L1页表的表项的物理地址
1: cmp r0, r6
add r3, r3, #1 << 20
R0是内核代码起始处所在L1页表项物理地址,R6是内核代码结尾处所在L1页表项物理地址,R3是内核代码起始处对应的物理地址映射值;
strls r3, [r0], #4
之后的内核代码对应的物理地址映射值将是其段值相应加1,即0xc00-0x000、0xc01-0x001、0xc02-0x002、0xc03-0x003.....
bls 1b
对于L1页表项,其实是写第0xc00、0xc01、0xc02、0xc03个条目的表项内容(物理地址在0x20007000、0x20007004、0x20007008、0x2000700c)
接下来的一段由宏CONFIG_XIP_KERNEL定制的代码无需关注;
/ *
* Then map first 1MB of ram in case it contains our boot params.
*/
/*5、通常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的情况,即内核起始代码超出了第一个段的范围的情况*/
add r0, r4, #PAGE_OFFSET >> 18
orr r6, r7, #(PHYS_OFFSET & 0xff000000)
.if (PHYS_OFFSET & 0x00f00000)
orr r6, r6, #(PHYS_OFFSET & 0x00f00000)
.endif
str r6, [r0]
接下来的一堆宏定制的代码无需关注,最后是返回stext,这时临时页表建立成功,但仅仅是建立成功还未真正使用,真正使用还在后面。
/*6、返回函数stext调用__create_page_tables的地方*/
mov pc, lr
临时页表创建后:
R4: 页表起始处物理地址
R8: machine info,struct machine_desc的基地址
R9: cpu id
R10: procinfo,struct proc_info_list的基地址
并且映射了如下部分的页表映射: 物理内存第一MB(自己映射自己,因为开启MMU后仍有一些访问是以物理地址发到MMU)、内核代码段(基本是0xc00段到0xc03段,映射至0x0段到0x003段), 后续准备开启MMU!*/这篇关于ARM架构内核启动分析-head.S(1.3、stext分析之内存临时页表建立)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!