| 日期 | 内核版本 | CPU架构 | 作者 |
| 2019.04.06 | Linux-4.4 | PowerPC | LoneHugo |
系列文章:https://blog.csdn.net/Vince_/article/details/89055979
1. 基本概念
在介绍系统启动阶段的内容之前先来了解一些基本的知识,方便我们理解相应的准备和操作的原理是什么。
主要有五点:
- elf format
- load address:内核镜像加载地址
- entry point:内核启动执行的入口地址
- bootm address:bootm指令从改地址启动,需要判断address与load address的异同,不同的话需要进行move操作,将镜像拷贝到对应的地址之后才能跳转运行
- kernel运行地址
参考:https://blog.csdn.net/qq_21792169/article/details/50098749
2. 启动准备
2.1 压缩和解压
在编译生成内核镜像的时候会进行压缩,并提取关键信息添加内核头部。所以系统启动跳转到内核执行之前需要由u-boot(bootloader)将内核Image解压后放置到对应的内存位置,然后跳转到内核入口处执行。当然也有可能是内核进行自解压之后再跳转执行,跟体系结构有关,比如arm架构下是自解压的方式,具体压缩和解压算法可以在编译内核的时候进行配置。
相对比较简单处理方式是PowerPC,采用gzip压缩,直接由u-boot进行解压,并去掉头部信息,放置到内存特定位置之后进行跳转到内核开始执行。
2.2 内存相关
根据内核镜像头部信息可以获取到entry point和load address信息,接下来跳转到entry point进行执行,其中load address为镜像在内存中的起始地址。如果bootm指定的address与load address不同,则需要进行move操作,将去掉头部之后的Image从其指定的地方转移到load address,然后跳转到内核运行。
2.3 MMU和TLB
在普遍情况下,启动阶段内核运行在实模式,也就是直接访问物理地址。这里面有一个前提就是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中完成如下几项功能:
- 找到当前运行环境内存虚拟地址对应的tlb条目,并将其保护位置位,从而不能通过invalid指令将其清除/* Insure IPROT set */
- 将除当前运行环境所在的地址对应tlb条目之外的所有条目全部清除;
- 建立临时tlb条目并进入该条目映射环境;
- 清除前面建立保护的条目;
- 建立内核KERNELBASE对应的tlb条目并跳转到该条目;
- 清除临时条目;
总体来讲,就是建立内核KERNELBASE对应的条目并跳入其中执行,将MMU中的其他条目全部清除
接下来进入set_ivor执行,设置中断向量表,并设定tlb miss时默认加载的tlb条目;
进入main kernel code starts位置开始执行:
- init_task对应的stack(内核stack)初始化
- early_init
- 如果内核按照动态地址加载,则此处会处理动态加载地址的计算和处理
- machine_init
- MMU_init
- start_kernel开始正式的内核初始化过程
early_init的任务
https://blog.csdn.net/juana1/article/details/6908774
https://blog.csdn.net/sailor_8318/article/details/4853319
- 首先清空bss段,这里使用的是memset_io,因为暂时没有cache可用
- 确定cpu类型,mfspr(SPRN_PVR)指令获取CPU的版本号
- 依据CPU的功能将不需要的启动代码写成NOP
- reloc_offset函数:
/*该函数返回(当前运行地址)减去(程序链接地址)的值,用于程序和数据
未映射到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功能
- lockdep_init初始化hash
- udbg_early_init实现打印
- patch_instruction对代码进行patch
- early_init_devtree
- epapr_paravirt_early_init进行early paravirtualization相关的初始化
- early_init_mmu
- probe_machine匹配对应的machine类型
- setup_kdump_trampoline
- ppc_md相关的初始化
本文深入探讨Linux内存管理,从系统启动阶段开始,包括压缩和解压内核镜像、内存管理、MMU和TLB的作用。讲解了PowerPC架构下的启动流程,如何处理地址转换,并在启动过程中建立TLB条目,确保内核正确运行。
——系统启动阶段的操作&spm=1001.2101.3001.5002&articleId=89056235&d=1&t=3&u=f4c2fb4047184303b623e596d3a7c543)
2939

被折叠的 条评论
为什么被折叠?



