深入解读Linux内存管理系列(1)——系统启动阶段的操作

日期 内核版本 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中完成如下几项功能:

  1. 找到当前运行环境内存虚拟地址对应的tlb条目,并将其保护位置位,从而不能通过invalid指令将其清除/* Insure IPROT set */
  2. 将除当前运行环境所在的地址对应tlb条目之外的所有条目全部清除;
  3. 建立临时tlb条目并进入该条目映射环境;
  4. 清除前面建立保护的条目;
  5. 建立内核KERNELBASE对应的tlb条目并跳转到该条目;
  6. 清除临时条目;

总体来讲,就是建立内核KERNELBASE对应的条目并跳入其中执行,将MMU中的其他条目全部清除

接下来进入set_ivor执行,设置中断向量表,并设定tlb miss时默认加载的tlb条目;

进入main kernel code starts位置开始执行:

  1. init_task对应的stack(内核stack)初始化
  2. early_init
  3. 如果内核按照动态地址加载,则此处会处理动态加载地址的计算和处理
  4. machine_init
  5. MMU_init
  6. start_kernel开始正式的内核初始化过程

early_init的任务

https://blog.csdn.net/juana1/article/details/6908774

https://blog.csdn.net/sailor_8318/article/details/4853319

  1. 首先清空bss段,这里使用的是memset_io,因为暂时没有cache可用
  2. 确定cpu类型,mfspr(SPRN_PVR)指令获取CPU的版本号
  3. 依据CPU的功能将不需要的启动代码写成NOP
  4. 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_SECTIONEND_FTR_SECTION中。END_FTR_SECTION_IFSET的含义是指当cpu具有某项特性时,包含在中间的代码有效,不用替换;对于END_FTR_SECTION_IFCLR则表示当cpu不具有某项特性时,包含在中间的代码有效,不用替换。包含在在BEGIN_XXXEND_XXX宏之间的代码在链接时存放到__ftr_fixup段中。至于那三个函数也就是实现这样的功能的,通过特定的CPU获取其特性,若需要特殊处理则添加相应功能,否则置空。

 

machine_init功能

  1. lockdep_init初始化hash
  2. udbg_early_init实现打印
  3. patch_instruction对代码进行patch
  4. early_init_devtree
  5. epapr_paravirt_early_init进行early paravirtualization相关的初始化
  6. early_init_mmu
  7. probe_machine匹配对应的machine类型
  8. setup_kdump_trampoline
  9. ppc_md相关的初始化

你可能感兴趣的:(Linux内存管理)