日期 | 内核版本 | CPU架构 | 作者 |
2019.04.06 | Linux-4.4 | PowerPC | LoneHugo |
系列文章:https://blog.csdn.net/Vince_/article/details/89055979
在介绍系统启动阶段的内容之前先来了解一些基本的知识,方便我们理解相应的准备和操作的原理是什么。
主要有五点:
参考:https://blog.csdn.net/qq_21792169/article/details/50098749
在编译生成内核镜像的时候会进行压缩,并提取关键信息添加内核头部。所以系统启动跳转到内核执行之前需要由u-boot(bootloader)将内核Image解压后放置到对应的内存位置,然后跳转到内核入口处执行。当然也有可能是内核进行自解压之后再跳转执行,跟体系结构有关,比如arm架构下是自解压的方式,具体压缩和解压算法可以在编译内核的时候进行配置。
相对比较简单处理方式是PowerPC,采用gzip压缩,直接由u-boot进行解压,并去掉头部信息,放置到内存特定位置之后进行跳转到内核开始执行。
根据内核镜像头部信息可以获取到entry point和load address信息,接下来跳转到entry point进行执行,其中load address为镜像在内存中的起始地址。如果bootm指定的address与load address不同,则需要进行move操作,将去掉头部之后的Image从其指定的地方转移到load address,然后跳转到内核运行。
在普遍情况下,启动阶段内核运行在实模式,也就是直接访问物理地址。这里面有一个前提就是MMU关闭,地址不做转换。PowerPC e500是个例外,PowerPC Booke架构下MMU是不能关闭的,此时页表也并未建立,因此此时还是在访问虚拟地址。所以在u-boot中还需要针对需要访问的物理地址建立对应的TLB条目实现转换,对应到虚拟地址上。
不同的CPU体系结构处理地址转换的方式不一样。PowerPC采用Effective, Virtual, 和Real三种地址空间,类似x86的Logical, Linear, 和Physical地址空间,同样还需要区分supervisor和guest地址空间访问模式。这部分在早期进行TLB的查找、添加和删除过程中很重要。
参考:https://www.linux-kvm.org/page/PowerPC_Book_E_MMU#PowerPC_Book_E_MMU_architecture
从BootLoader进入内核
head_fsl_booke.S文件开始进入内核
首先是_ENTRY(_stext)段,而最初始部分为_ENTRY(_start),进入执行过程
__HEAD
_ENTRY(_stext);
_ENTRY(_start);
在该段中首先bl get_phys_addr调用汇编函数将device tree address转换为物理地址并存储在r30/31寄存器中,因为未定义CONFIG_RELOCATABLE宏,因此代码直接进入_ENTRY(__early_start)段执行
_ENTRY(__early_start)段由文件包含的形式包含进源代码
#define ENTRY_MAPPING_BOOT_SETUP
#include "fsl_booke_entry_mapping.S"
#undef ENTRY_MAPPING_BOOT_SETUP
而在fsl_booke_entry_mapping.S中完成如下几项功能:
总体来讲,就是建立内核KERNELBASE对应的条目并跳入其中执行,将MMU中的其他条目全部清除
接下来进入set_ivor执行,设置中断向量表,并设定tlb miss时默认加载的tlb条目;
进入main kernel code starts位置开始执行:
early_init的任务
https://blog.csdn.net/juana1/article/details/6908774
https://blog.csdn.net/sailor_8318/article/details/4853319
/*该函数返回(当前运行地址)减去(程序链接地址)的值,用于程序和数据
未映射到KERNELBASE时使用*/
_GLOBAL(reloc_offset)
mflr r0 /*链接寄存器的值*/
bl 1f /*跳转到1所在的地址,这就是当前代码所在的实际地址,这样通过mflr r3就将当前bl 1f的当前运行地址保存在r3中*/
1: mflr r3
PPC_LL r4,(2f-1b)(r3) /* PPC_LL意思为lwz,装载立即数,得到的r4为1f的链接地址*/
subf r3,r4,r3 /*二者相减,获取当前运行地址和链接地址的偏移*/
mtlr r0 /*恢复保存的函数地址*/
blr
.align 3
2: PPC_LONG 1b
清空bss,这里的__bss_start和__bss_stop的值是在Boot/zImage.lds.S中定义的,再确定CPU的类型(identify_cpu),之后根据特定的CPU做相关的fix_up操作。至于identify_cpu里的代码,在之前版本的内核里是定义在misc.s中的汇编代码,现在成C语言的了,理解起来不是很困难,大致的步骤就是先通过PVR&pvr_mask== pvr_value在cpu_specs数组中找到与CPU对应的类型,找到后,将匹配的一组cpu参数当输入值调用setup_cpu_spec函数。这个也是个很简单的函数,注释比代码还懂,相信各位一定能看懂的,它主要实现的功能是将原数组中定义的不足弥补,譬如PMC(Performance Monitor Countor性能监视器)的个数,还有一个OPROFILE,是Linux下的性能分析工具,具体机制没细看,待高手分析吧,以及工作于兼容模式的解决办法。这里就不再赘述。来看一下函数early_init的后面三个fixup函数,这六个函数中的变量__start___**_fixup、__stop___**_fixup都在vmlinux.lds.s文件中定义,这里,linux使用了一个很高级的技巧来复用代码。因为不同的处理器具有不同的特性,如单独的指令、数据Cache;统一的指令数据Cache;动态电源管理特性;硬件或软件TLB查找等,绝大部分CPU之间只是有特性的差异,其它部分如指令集都是一样的,专门为这些处理器提供不同的源文件显得多余,同时又不便于统一维护。因此,Linux在操作有关处理器特性的代码前后加上特殊的宏定义,将此类代码放到单独的一个段中,一旦CPU类型被确定以后,如果CPU具有某特性,则操作该特性的代码不作任何处理,如果CPU不具备该特性,则把操作该特性的代码全部替换成空操作指令(nop),所有的处理都在这个单独的段中完成。这种做法对于操作代码某特性代码量较少的情况下非常有用,这样比使用判断然后跳转的组合指令来得更加有效,上面提的六个变量在vmlinux.lds.S中这个段的定义为:
. = ALIGN(8);
__ftr_fixup : AT(ADDR(__ftr_fixup) - LOAD_OFFSET) {
__start___ftr_fixup = .;
*(__ftr_fixup)
__stop___ftr_fixup = .;
}
此外,在include/asm/Feature-fixup.h中有如下定义:
#define BEGIN_FTR_SECTION_NESTED(label) START_FTR_SECTION(label)
#define BEGIN_FTR_SECTION START_FTR_SECTION(97)
#define END_FTR_SECTION_NESTED(msk, val, label) \
FTR_SECTION_ELSE_NESTED(label) \
MAKE_FTR_SECTION_ENTRY(msk, val, label, __ftr_fixup)
#define END_FTR_SECTION(msk, val) \
END_FTR_SECTION_NESTED(msk, val, 97)
所有特性相关的代码都放在BEGIN_FTR_SECTION和END_FTR_SECTION中。END_FTR_SECTION_IFSET的含义是指当cpu具有某项特性时,包含在中间的代码有效,不用替换;对于END_FTR_SECTION_IFCLR则表示当cpu不具有某项特性时,包含在中间的代码有效,不用替换。包含在在BEGIN_XXX和END_XXX宏之间的代码在链接时存放到__ftr_fixup段中。至于那三个函数也就是实现这样的功能的,通过特定的CPU获取其特性,若需要特殊处理则添加相应功能,否则置空。
machine_init功能