ARM架构内核启动分析-head.S(1.3、stext分析之内存临时页表建立)

2024-05-09 22:38

本文主要是介绍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   

通过宏pgtblr4设置成页表的物理地址,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分析之内存临时页表建立)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/974708

相关文章

mybatis的整体架构

mybatis的整体架构分为三层: 1.基础支持层 该层包括:数据源模块、事务管理模块、缓存模块、Binding模块、反射模块、类型转换模块、日志模块、资源加载模块、解析器模块 2.核心处理层 该层包括:配置解析、参数映射、SQL解析、SQL执行、结果集映射、插件 3.接口层 该层包括:SqlSession 基础支持层 该层保护mybatis的基础模块,它们为核心处理层提供了良好的支撑。

百度/小米/滴滴/京东,中台架构比较

小米中台建设实践 01 小米的三大中台建设:业务+数据+技术 业务中台--从业务说起 在中台建设中,需要规范化的服务接口、一致整合化的数据、容器化的技术组件以及弹性的基础设施。并结合业务情况,判定是否真的需要中台。 小米参考了业界优秀的案例包括移动中台、数据中台、业务中台、技术中台等,再结合其业务发展历程及业务现状,整理了中台架构的核心方法论,一是企业如何共享服务,二是如何为业务提供便利。

NameNode内存生产配置

Hadoop2.x 系列,配置 NameNode 内存 NameNode 内存默认 2000m ,如果服务器内存 4G , NameNode 内存可以配置 3g 。在 hadoop-env.sh 文件中配置如下。 HADOOP_NAMENODE_OPTS=-Xmx3072m Hadoop3.x 系列,配置 Nam

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

MySQL数据库宕机,启动不起来,教你一招搞定!

作者介绍:老苏,10余年DBA工作运维经验,擅长Oracle、MySQL、PG、Mongodb数据库运维(如安装迁移,性能优化、故障应急处理等)公众号:老苏畅谈运维欢迎关注本人公众号,更多精彩与您分享。 MySQL数据库宕机,数据页损坏问题,启动不起来,该如何排查和解决,本文将为你说明具体的排查过程。 查看MySQL error日志 查看 MySQL error日志,排查哪个表(表空间

springboot3打包成war包,用tomcat8启动

1、在pom中,将打包类型改为war <packaging>war</packaging> 2、pom中排除SpringBoot内置的Tomcat容器并添加Tomcat依赖,用于编译和测试,         *依赖时一定设置 scope 为 provided (相当于 tomcat 依赖只在本地运行和测试的时候有效,         打包的时候会排除这个依赖)<scope>provided

内核启动时减少log的方式

内核引导选项 内核引导选项大体上可以分为两类:一类与设备无关、另一类与设备有关。与设备有关的引导选项多如牛毛,需要你自己阅读内核中的相应驱动程序源码以获取其能够接受的引导选项。比如,如果你想知道可以向 AHA1542 SCSI 驱动程序传递哪些引导选项,那么就查看 drivers/scsi/aha1542.c 文件,一般在前面 100 行注释里就可以找到所接受的引导选项说明。大多数选项是通过"_

usaco 1.3 Prime Cryptarithm(简单哈希表暴搜剪枝)

思路: 1. 用一个 hash[ ] 数组存放输入的数字,令 hash[ tmp ]=1 。 2. 一个自定义函数 check( ) ,检查各位是否为输入的数字。 3. 暴搜。第一行数从 100到999,第二行数从 10到99。 4. 剪枝。 代码: /*ID: who jayLANG: C++TASK: crypt1*/#include<stdio.h>bool h

usaco 1.3 Calf Flac(暴搜)

思路是暴搜。 需要注意的地方是输入的方法,以及输出时的换行。 代码: /*ID: who jayLANG: C++TASK: calfflac*/#include<stdio.h>#include<string.h>#include<math.h>int main(){freopen("calfflac.in","r",stdin);freopen("calfflac.ou