2.16.3.内核启动的汇编阶段
Posted ocean-star
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2.16.3.内核启动的汇编阶段相关的知识,希望对你有一定的参考价值。
参考https://blog.csdn.net/skyflying2012/article/details/41344377
本节是内核启动的汇编阶段剩余内容,主要是cpu的校验、机器码的校验、传参tag的校验、页表的创建、各种段的处理等。
2.16.3.1、__lookup_processor_type
(1)我们从cp15协处理器的c0寄存器中读取出硬件的CPU ID号,然后调用这个函数来进行合法性检验。如果合法则继续启动,如果不合法则停止启动,转向__error_p启动失败。
(2)该函数检验cpu id的合法性方法是:内核会维护一个本内核支持的CPU ID号码的数组,然后该函数所做的就是将从硬件中读取的cpu id号码和数组中存储的各个id号码依次对比,如果没有一个相等则不合法,如果有一个相等的则合法。
(3)内核启动时设计这个校验,也是为了内核启动的安全性着想。
总结:校验cpu id为什么链接地址和运行地址不一致?
在stext中,首先调用到__loopup_processor_type,kernel代码将所有CPU信息的定义放到.proc.info.init段中,因此可以认为.proc.info.init就是一个数组,每个元素定义了一个/一种CPU的信息。目前__loopup_processor_type使用该元素的前两个字段cpuid和mask来匹配当前CPUID,如果满足CPUID&mask == cpuid,则找到当前CPU的定义并返回。
因为kernel要开启MMU,所以kernel编译链接地址是虚拟地址(物理地址经过MMU转换后CPU看到的地址),并不是物理地址, 链接确定了变量的绝对地址(虚拟地址),但在现阶段,没开启MMU,CPU看到的sdram地址就是其物理地址(0x80000000起始)。 如果直接运行,对于变量的寻址则会出现问题(函数寻址没问题,因为arm函数寻址使用相对跳转指令b bl) 比如,kernel image中全局变量i链接地址在0xc0009000,但现阶段i物理地址是在0x80009000,对于CPU来说,只能在0x80009000上才能找到i。 去0xc0009000寻址,程序运行就出错了。 这就是为什么我们所理解的,链接地址 加载地址 运行地址必须一致的原因。 kernel现阶段给出的解决方法,就是lookup_processor_type前3行汇编程序的链接地址与运行地址为什么要一致?我的理解,链接确定程序运行绝对地址,也确定了其中变量及函数的绝对地址,加载运行地址不是其链接地址,变量实际存储的地址就变了。这时如果对变量进行寻址,就会有不可知的结果,这是我能想到的原因。平时我们编译链接都是一些C语言编写程序,难免会定义一些全局变量,如果链接和运行地址不一致,就不能正常寻址。如果想运行和链接地址不一致,我能想到的办法,只能是汇编中尽量不去涉及一些绝对地址,使用PIC位置无关代码。uboot在relocation之后,kernel在开启MMU之前,都实现了链接地址和运行地址不一致
* uboot在relocation时修改rel.dyn段[存储所有变量地址],实现将所有变量地址重定位到新运行地址
* kernel在开启MMU之前,计算运行地址[物理地址]和链接地址[虚拟地址]的偏移,对变量寻址时进行物理转,从而正常找到变量。开启MMU之后,利用硬件机制,来实现链接和运行地址统一
所以说,链接地址一定要等于运行地址吗?不一定,嵌入式最著名的uboot kernel就是例子!
2.16.3.2、__lookup_machine_type
(1)该函数的设计理念和思路和上面校验cpu id的函数一样的。不同之处是本函数校验的是机器码。uboot传递机器码,这个函数校验总结:校验机器码
2.16.3.3、__vet_atags
(1)该函数的设计理念和思路和上面2个一样,不同之处是用来校验uboot给内核的传参ATAGS格式是否正确。这里说的传参指的是uboot通过tag给内核传的参数(主要是板子的内存分布memtag、uboot的bootargs)
(2)内核认为如果uboot给我的传参格式不正确,那么我就不启动。
(3)uboot给内核传参的部分如果不对,是会导致内核不启动的。譬如uboot的bootargs设置不正确内核可能就会不启动。
2.16.3.4、__create_page_tables
(1)顾名思义,这个函数用来建立页表。
(2)linux内核本身被连接在虚拟地址处,因此kernel希望尽快建立页表并且启动MMU进入虚拟地址工作状态。但是kernel本身工作起来后页表体系是非常复杂的,建立起来也不是那么容易的。kernel想了一个好办法(3)kernel建立页表其实分为2步。第一步,kernel先建立了一个段式页表(和uboot中之前建立的页表一样,页表以1MB为单位来区分的),这里的函数就是建立段式页表的。段式页表本身比较好建立(段式页表1MB一个映射,4GB空间需要4096个页表项,每个页表项4字节,因此一共需要16KB内存来做页表),坏处是比较粗不能精细管理内存;第二步,再去建立一个细页表(4kb为单位的细页表),然后启用新的细页表废除第一步建立的段式映射页表。
(4)内核启动的早期建立段式页表,并在内核启动前期使用;内核启动后期就会再次建立细页表并启用。等内核工作起来之后就只有细页表了。create_page_table完成了3种地址映射的页表空间填写:(1)turn_mmu_on所在1M空间的平映射
(2)kernel image的线性映射
(2)atags所在1M空间的线性映射物理地址空间和虚拟地址空间映射关系图如下:
(1)为什么turn_mmu_on要做平映射?
turn_mmu_on我会在下一篇博文中分析,主要是完成开启MMU的操作。
那为什么将turn_mmu_on处做一个平映射?可以想象,执行开启MMU指令之前,CPU取指是在0x80008000附近turn_mmu_on中。如果只是做kernel image的线性映射,执行开启MMU指令后,CPU所看到的地址就全变啦。turn_mmu_on对于CPU来说在0xc0008000附近,0x80008000附近对于CPU来说已经不可预知了。但是CPU不知道这些,它只管按照地址一条条取指令,执行指令。所以不做turn_mmu_on的平映射(virt addr = phy addr),turn_mmu_on在开启MMU后的运行是完全不可知。完成turn_mmu_on的平映射,我们可以在turn_mmu_on末尾MMU已经开启稳定后,修改PC到0xc0008000附近,就可以解决从0x8xxxxxxx到0xcxxxxxxx的跳转。(
2)kernel image加载地址为什么会在0x****8000?
分析了kernel image线性映射部分,这个就好理解了:kernel编译链接时的入口地址在0xc0008000(PAGE_OFFSET + TEXT_OFFSET),但其物理地址不等于其链接的虚拟地址,image的线性映射实现其运行地址等于链接地址。kernel的每一页表映射1M,所以入口处在(0x80000000-->0xc0000000)映射页表中完成映射。物理地址和虚拟地址的1M内偏移必须一致呀。kernel定义的TEXT_OFFSET = 0x8000.所以加载的物理地址必须为0x****8000.这样,开启MMU后,访问0xc0008000附近指令,MMU根据TLB才能正确映射找到0x****8000附近的指令。
(3)atags跟kernel入口是在同一1M空间内,bootparams的线性映射操作是否多余?根据第二个问题的分析,kernel image可以加载到任何sdram地址空间的0x****8000即可。atags地址是有bootloader中指定,然后告诉kernel。那就有这样一种情况,加入sdram起始地址为0x80000000,atags起始地址为0x80000100。但kernel image我加载到0x81008000,可以看出,这时atags跟kernel image就在不同一1M空间啦atags单独的线性映射操作还是很有必要的。之前分析到__create_page_tables在内核代码区TEXT_OFF下部的16KB区域内进行页表的配置,完成turn_mmu_on的平映射以及kernel image的线性映射。接下来就需要开启MMU,让整个CPU进入虚拟地址运行的新阶段。head.S中stext最后一段代码如下:
看注释也可以明白接下来要完成的2件工作:执行CPU特定处理代码,开启MMU。
r10中存储着本cpu的proc_info_list首地址。在kernel image中定义有一个.proc.info.init的段。在arch/arm/kernel/vmlinux.lds.S中。具体见:[kernel 启动流程] (第三章)第一阶段之——proc info的获取
2.6.3.5、__switch_data
(1)建立了段式页表后进入了__switch_data部分,这东西是个函数指针数组。
(2)分析得知下一步要执行__mmap_switched函数
(3)复制数据段、清除bss段(目的是构建C语言运行环境)
(4)保存起来cpu id号、机器码、tag传参的首地址。
(5)b start_kernel跳转到C语言运行阶段。
总结:汇编阶段其实也没干啥,主要原因是uboot干了大部分活。汇编阶段主要就是校验启动合法性、建立段式映射的页表并开启MMU以方便使用内存、跳入C阶段。
以上是关于2.16.3.内核启动的汇编阶段的主要内容,如果未能解决你的问题,请参考以下文章
v87.01 鸿蒙内核源码分析 (内核启动篇) | 从汇编到 main () | 百篇博客分析 OpenHarmony 源码
v87.01 鸿蒙内核源码分析 (内核启动篇) | 从汇编到 main () | 百篇博客分析 OpenHarmony 源码
v87.01 鸿蒙内核源码分析 (内核启动篇) | 从汇编到 main () | 百篇博客分析 OpenHarmony 源码