[置顶] arm-linux内核start_kernel之前启动分析(1)-接过bootloader的衣钵

时间:2021-01-27 03:48:33
前段时间移植uboot仔细研究过uboot启动过程,最近耐不住寂寞,想对kernel下手。

Uboot启动过程分析博文连接如下:


http://blog.csdn.net/skyflying2012/article/details/25804209


移植内核时kernel启动过程需要我们修改的地方比较少,研究这个对于编写driver也没有多大帮助,但对了解整个linux架构,各种机制还是非常有用。


只有知道kernel如何启动,我们才能真正的去理解kernel

作为一个嵌入式工作者,我想不能仅仅局限于某个module driver,而应深入到kernel的汪洋大海中去傲游!


学习启动过程,我本着打破沙锅问到底的原则,希望能研究的明明白白,但也鉴于水平有限,还是有很多纰漏之处

共享博文,希望大家多多交流指正,辛苦整理,如需转载,还请注明出处。

对于arm linux,start_kernel之前都是汇编代码,区区上百行汇编,但是却蕴含着很多精髓。

这部分代码分3篇来分析,另外两篇链接地址如下:

http://blog.csdn.net/skyflying2012/article/details/41447843

http://blog.csdn.net/skyflying2012/article/details/48054417


今天先来学习前几十行!


Kernel版本号:3.4.55

在arch/arm/kernel/head.S中,如下:

.arm
__HEAD
ENTRY(stext)

THUMB( adr r9, BSYM(1f) ) @ Kernel is always entered in ARM.
THUMB( bx r9 ) @ If this is a Thumb-2 kernel,
THUMB( .thumb ) @ switch to Thumb now.
THUMB(1: )

//处理器进入svc模式,关闭中断
setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode
@ and irqs disabled
//获取处理器ID
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
//将proc_type_list pointer存在r10中,如果为NULL,则error_p
movs r10, r5 @ invalid processor (r5=0)?
THUMB( it eq ) @ force fixup-able long branch encoding
beq __error_p @ yes, error 'p'

//CONFIG_ARM_LPAE不太明白含义,我使用处理器配置文件没有选择该项,感兴趣朋友可以研究下
#ifdef CONFIG_ARM_LPAE
mrc p15, 0, r3, c0, c1, 4 @ read ID_MMFR0
and r3, r3, #0xf @ extract VMSA support
cmp r3, #5 @ long-descriptor translation table format?
THUMB( it lo ) @ force fixup-able long branch encoding
blo __error_p @ only classic page table format
#endif
#ifndef CONFIG_XIP_KERNEL
//获取物理地址与虚拟地址的offset,存在r8中
adr r3, 2f
ldmia r3, {r4, r8}
sub r4, r3, r4 @ (PHYS_OFFSET - PAGE_OFFSET)
add r8, r8, r4 @ PHYS_OFFSET
#else
//定义CONFIG_XIP_KERNEL,offset为PHYS_OFFSET
ldr r8, =PHYS_OFFSET @ always constant in this case
#endif

/*
* r1 = machine no, r2 = atags or dtb,
* r8 = phys_offset, r9 = cpuid, r10 = procinfo
*/
//对bootloader传来的tags参数进行检查
bl __vet_atags
Kernel的入口函数是哪个,入口地址在哪,需要根据连接脚本来确定。
在arch/arm/kernel/vmlinux.lds.S,如下:

OUTPUT_ARCH(arm)ENTRY(stext)#ifndef __ARMEB__jiffies = jiffies_64;#elsejiffies = jiffies_64 + 4;#endifSECTIONS{........#ifdef CONFIG_XIP_KERNEL    . = XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR);#else    . = PAGE_OFFSET + TEXT_OFFSET;#endif}
入口函数是head.S中的stext,不采用XIP技术,入口地址是PAGE_OFFSET+TEXT_OFFSET。
./arch/arm/include/asm/memory.h中:

#define PAGE_OFFSET     UL(CONFIG_PAGE_OFFSET)Menuconfig中CONFIG_PAGE_OFFSET = 0xc0000000./arch/arm/Makefile中:textofs-y   := 0x00008000textofs-$(CONFIG_ARCH_CLPS711X) := 0x00028000# We don't want the htc bootloader to corrupt kernel during resumetextofs-$(CONFIG_PM_H1940)      := 0x00108000# SA1111 DMA bug: we don't want the kernel to live in precious DMA-able memoryifeq ($(CONFIG_ARCH_SA1100),y)textofs-$(CONFIG_SA1111) := 0x00208000endiftextofs-$(CONFIG_ARCH_MSM7X30) := 0x00208000textofs-$(CONFIG_ARCH_MSM8X60) := 0x00208000textofs-$(CONFIG_ARCH_MSM8960) := 0x00208000......# The byte offset of the kernel image in RAM from the start of RAM.TEXT_OFFSET := $(textofs-y)
入口地址是0xc0008000.
但是实际操作中,kernel是加载到0x80008000地址运行的。
(我使用处理器sdram物理起始地址是0x80000000)


为什么链接地址和运行地址不一致?

学习完start_kernel之前的汇编,就会明白原因了。

在stext中,首先调用到__lookup_processor_type,Kernel代码将所有CPU信息的定义都放到.proc.info.init段中,因此可以认为.proc.info.init段就是一个数组,每个元素都定义了一个或一种CPU的信息。

目前__lookup_processor_type使用该元素的前两个字段cpuid和mask来匹配当前CPUID,如果满足CPUID & mask == cpuid,则找到当前cpu的定义并返回。 

代码如下:

   __CPUINIT__lookup_processor_type:    //3行汇编,计算出物理地址与虚拟地址之间的offset,存在r3中    adr r3, __lookup_processor_type_data    ldmia   r3, {r4 - r6}    sub r3, r3, r4          @ get offset between virt&phys    //获取__proc_info_begin的物理地址    add r5, r5, r3          @ convert virt addresses to    //获取__proc_info_end的物理地址    add r6, r6, r3          @ physical address space    //mask cp15读出的cpuid,与proc_type_list中value对比1:  ldmia   r5, {r3, r4}            @ value, mask    and r4, r4, r9          @ mask wanted bits    teq r3, r4    //一致则返回,不一致则跳到下一个proc_type_list,继续对比    beq 2f    add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)    cmp r5, r6    blo 1b    //匹配成功,r5存该proc_type_list指针,匹配失败,r5置0    mov r5, #0              @ unknown processor2:  mov pc, lrENDPROC(__lookup_processor_type)/* * Look in <asm/procinfo.h> for information about the __proc_info structure. */    .align  2    .type   __lookup_processor_type_data, %object__lookup_processor_type_data:    .long   .    .long   __proc_info_begin    .long   __proc_info_end    .size   __lookup_processor_type_data, . - __lookup_processor_type_data</span>


因为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行汇编:

adr r3, __lookup_processor_type_data 加载__lookup_processor_type_data地址(实际运行地址,这里就是物理地址)到r3

ldmia r3, {r4 - r6} 获取以r3 r3+4 r3+8为地址的变量到r4,r5,r6.
地址变量值是在链接时确定的,所以r4中存的是__lookup_processor_type_data的链接地址(虚拟地址)。

sub r3 ,r3 ,r4     r3中存储的是物理地址与虚拟地址的偏移。


这是多么genius的操作啊!

_proc_info_begin _proc_info_end在链接脚本中定义,是.proc.info.init段的首尾。
该段中是proc_info_list struct,表示处理器相关信息,定义如下:

struct proc_info_list {    unsigned int        cpu_val;    unsigned int        cpu_mask;    unsigned long       __cpu_mm_mmu_flags; /* used by head.S */    unsigned long       __cpu_io_mmu_flags; /* used by head.S */    unsigned long       __cpu_flush;        /* used by head.S */    const char      *arch_name;    const char      *elf_name;    unsigned int        elf_hwcap;    const char      *cpu_name;    struct processor    *proc;    struct cpu_tlb_fns  *tlb;    struct cpu_user_fns *user;    struct cpu_cache_fns    *cache;};


该段是在arch/arm/mm/proc-xxx.S中填充,定义了对应arm指令集的处理器特性和初始化函数,在第三篇文章中我们还会详细来理解proc info的作用,这里先按下不表。

lookup_processor_type_data返回stext中。
接下来同样用上面的方法获取phy&virt offset,存在r8.


根据我之前分析uboot传参kernel的博文(链接如下:http://blog.csdn.net/skyflying2012/article/details/35787971)

r1存储machine id,r2存储atags。

stext中__vet_atags会对atags做一个基本的检查,代码如下:


__vet_atags:    tst r2, #0x3            @ aligned?    bne 1f    ldr r5, [r2, #0]    //判断是否是dtb类型#ifdef CONFIG_OF_FLATTREE    ldr r6, =OF_DT_MAGIC        @ is it a DTB?    cmp r5, r6    beq 2f#endif    cmp r5, #ATAG_CORE_SIZE     @ is first tag ATAG_CORE?    cmpne   r5, #ATAG_CORE_SIZE_EMPTY    bne 1f    ldr r5, [r2, #4]    ldr r6, =ATAG_CORE    cmp r5, r6    bne 1f    //正确tags,返回2:  mov pc, lr              @ atag/dtb pointer is ok    //错误tags,清空r2,返回1:  mov r2, #0    mov pc, lrENDPROC(__vet_atags)


检查tag头4 byte(tag_core的size)和第二个4 byte(tag_core的type)是否正确。


对于stext中前几十行汇编,已经分析完成,总结下做了哪些工作:
(1)设置CPU模式
(2)检查CPUID是否匹配
(3)获取phy&virt offset
(4)检查atags参数


这段代码就分析到这,不过引起了我对于链接地址 运行地址的思考。
教科书上是这样教的,我也一直这么认为,链接地址 运行地址(加载地址)必须是一致,但是却没有真正去思考过为什么。

程序的链接地址与运行地址为什么要一致?

我的理解,链接确定程序运行绝对地址,也确定了其中变量及函数的绝对地址,加载运行地址不是其链接地址,变量实际存储的地址就变了。
这时如果对变量进行寻址,就会有不可知的结果,这是我能想到的原因。
平时我们编译链接都是一些C语言编写程序,难免会定义一些全局变量,如果链接和运行地址不一致,就不能正常寻址。

如果想运行和链接地址不一致,我能想到的办法,只能是汇编中尽量不去涉及一些绝对地址,使用PIC位置无关代码。

联想之前分析的uboot relocation原理(博文链接:http://blog.csdn.net/skyflying2012/article/details/37660265),

uboot在relocation之后,kernel在开启MMU之前,都实现了链接地址和运行地址不一致,看看它们用的什么方法?


(1)uboot在relocation时修改rel.dyn段(存储所有变量地址),实现将所有变量地址重定位到新运行地址


(2)kernel在开启MMU之前,计算运行地址(物理地址)与链接地址(虚拟地址)的偏移,对变量寻址时都进行地址转换,从而正常找到变量。开启MMU之后,利用硬件机制,来实现链接和运行地址的统一


所以说,链接地址一定要等于运行地址吗?不一定,嵌入式最著名的uboot  kernel就是例子!