Linux系统从uboot到内核启动流程
1. BROM引导:
ARM CPU刚上电时,它的PC寄存器指针指向IC内嵌的一片ROM的起始位置处,这片ROM称之为BROM(boot rom),系统就是通过这片BROM引导起来的。BROM的空间比较小,一般是32/64KB,IC上的ShareRAM大小也不尽相同,所以IC引导过程也是会有所不同。
BROM中会存储上电引导程序,这段程序也一般会包括以下几个内容:
1. CPU上电初始化操作。
2. 启动介质的驱动操作。
3. 固件下载操作,用来进入刷机模式,更新时固件使用。
4. BROM引导程序,主要功能是用来从从启动介质中读取和加载第二阶段引导程序。
5. 签名验证操作。
BROM中的引导程序由IC厂商自己定制开发。它会根据硬件上不同,来判断要进入刷机模式还是启动模式,并且还要判断是从哪种介质中引导启动。接下来要从启动介质中读取MBREC上的引导程序到ShareRAM中,并跳转执行。BROM属于read only的,在SOC流片的时候就固定下来了,所以拿到SOC以后基本上就不会去修改了,而可定制化的东西都放在bootloader中实现。
2. bootloader引导(第二阶段引导):
ARM架构下使用的BROM引导,有点类似于X86下的BIOS引导,BROM内的引导固件一般是不会改变的,从第二阶段这里开始就进入了可定制的阶段,第二阶段的引导程序一般会单独放在启动介质的一个分区,我们叫它boot分区或者MBR分区(主引导分区)。
我们可以把第二阶段引导分为多级引导:
比如分为如下所示的三级引导过程:
(1) firstMBRC
第一级引导程序需要符合BROM引导所需要的格式,会调用BROM中的驱动函数把secondMBRC拷贝到shareRAM中校验,并跳转执行,这个都是独立代码,一般使用汇编来做。
(2) secondMBRC(uboot-spl)
第二级引导程序的功能是调用BROM中的驱动函数把mainMBRC拷贝到DDR 中校验,并跳转执行。第二阶段可以使用uboot中的spl来实现,也可以由自己独立代码实现。
(3) mainMBRC(uboot)
第三级是主要的引导程序,前面的两级引导都是为了加载mainMBRC,它的主要功能是显示启动logo,加载kernel、dtb、rootfs文件系统,并且启动kernel。一般使用uboot来做。
所以在boot分区,我们要烧写入这三部分的引导代码,mbrc、uboot-spl、uboot。
3. uboot引导
采用uboot来启动的内核为uImage,这种内核包括两部分,一个是头部,一个是真正的内核,可以这样来表示uImage=uboot header+zImage。
头部的定义为:
typedef struct image_header {
uint32_t ih_magic; /* Image Header Magic Number */
uint32_t ih_hcrc; /* Image Header CRC Checksum */
uint32_t ih_time; /* Image Creation Timestamp */
uint32_t ih_size; /* Image Data Size */
uint32_t ih_load; /* Data Load Address */
uint32_t ih_ep; /* Entry Point Address */
uint32_t ih_dcrc; /* Image Data CRC Checksum */
uint8_t ih_os; /* Operating System */
uint8_t ih_arch; /* CPU architecture */
uint8_t ih_type; /* Image Type */
uint8_t ih_comp; /* Compression Type */
uint8_t ih_name[IH_NMLEN]; /* Image Name */
} image_header_t;
我们需要关心的是:
uint32_t ih_load; /* Data Load Address */
uint32_t ih_ep; /* Entry Point Address */
ih_load是内核加载地址,即内核开始运行前应该位于的地方 ,ih_ep是内核入口地址,入口地址和加载地址可以相同。
Uboot启动内核的过程是通过读取环境变量env中的bootcmd来决定如何启动kernel,比如uboot想从nand flash上读取kernel分区到内存地址的0x30007FC0上并且启动kernel,可以使用如下命令:bootcmd = nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0。启动kernel的关键是bootm命令。
bootm命令的实现在uboot中是do_bootm()函数中:
源文件:cmd_bootm.c
int do_bootm (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
…
if (argc < 2) {
addr = load_addr;
} else {
addr = simple_strtoul(argv[1], NULL, 16);
}
/*从加载地址处读取uboot header并进行解析*/
……
switch (hdr->ih_comp) {
case IH_COMP_NONE:
if(ntohl(hdr->ih_load) == addr) { /
printf (" XIP %s ... ", name);
} else {//
memmove ((void *) ntohl(hdr->ih_load), (uchar *)data, len);
}
……
}
首先判断bootm命令后面是否带了加载地址,若没有加载地址的参数,则将默认加载地址赋值给addr,否则将使用bootm命令后附带的地址作为加载地址。然后的关键是从加载地址处读取uboot header并且解析。从上面的代码逻辑我们可以看到,会判断ubootheader中的ih_load和bootm传入的加载地址是否一致,并由此区分了两种情况:
- (1)如果不同的话会把去掉头部(64Byte)的内核(zImage)复制到ih_load指定的地址中,并从ih_ep处开始启动内核。因此这种情况下,ih_load和ih_ep要相同。
- (2)如果相同的话那就让其原封不同的放在那,并从ih_ep处开始启动内核(zImage)。因此这种情况下,执行入口函数地址要和加载地址之间相差了一个uboot header。因此 ih_ep= ih_load+64Byte。
有了上面的知识,那么我们如何去设置uboot header中的地址呢,其实这一步是在制作uImage的时候,使用mkimage工具指定的加载地址和运行地址。
mkimage -A arm -O linux -C none -a 0x30008000 -e 0x30008000 -d zImage uImage
-A:CPU类型
-O:操作系统
-C:采用的压缩方式
-a:内核加载地址
-e:内核入口地址
制作镜像头以及下载地址就有两种情况:
第一种:
mkimage -A arm -O linux -C none -a 0x30008000 -e 0x30008000 -d zImage uImage
tftp 0x31000000 uImage
bootm 0x31000000
加载地址和入口地址相同,tftp和bootm后面的地址是任意地址(除了-a指定的地址外)。
这种情况下uboot会对内核进行搬运的动作,搬运的是不包括uboot header的zImage,所以加载地址和入口地址要设置为相同。如果tftp和bootm后面的地址也是0x30008000,会出现什么情况呢?从上面的代码可以看出,如果相同,将不执行搬运动作,只打印除了一条信息,然后跳到入口地址进行执行,此时入口地址是一个uboot header,这里并不是可执行的zImage,所以将会报错。
第二种:
mkimage -A arm -O linux -C none -a 0x30008000 -e 0x30008040 -d zImage uImage
tftp 0x30008000 uImage
bootm 0x30008000
入口地址在加载地址后面64个字节,tftp和bootm后面的地址一定要在-a指定的加载地址上。
我们一般习惯于使用第二种方法,并下载到–a所指定的地址上,这样就不用劳烦uboot进行搬运了,节省了启动时间。
在上面的do_bootm 中,我们通过解析uboot header,已经获得了内核镜像相关的信息,其中就包括了内核入口地址,在引导的最后阶段,将跳转到内核中去执行。这一步是在do_bootm_linux()函数中实现的。通过一个函数指针 thekernel()带三个参数跳转到内核( zImage )入口点开始执行,此时, u-boot 的任务已经完成,控制权完全交给内核( zImage )。
do_bootm_linux(),在arch\arm\lib\bootm.c定义,因为我们已经知道入口地址了,所以只需跳到入口地址就可以启动linux内核了。
theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
hdr->ih_ep----Entry Point Address ,uImage 中指定的内核入口点,还记得ih_ep吗?其中第二个参数为机器 ID, 内核所设置的机器码和uboot所设置的机器码必须一致才能启动内核,第三参数为 u-boot 传递给内核参数存放在内存中的首地址。
4. 内核启动
经过uboot引导以后,系统开始进入到zImage中执行。zImage是包括了解压缩代码和vmlinux的镜像,所以它的执行可以分为三部分,分别是zImage解压缩,vmlinux内核启动汇编阶段,vmlinux内核启动C语言阶段。
(1) zImage解压缩
(2) 内核启动汇编阶段
(3) 内核启动c语言阶段(start_kernel到创建第一个进程)
zImage解压缩
这一阶段所涉及的文件也只有三个:
(1)arch/arm/boot/compressed/vmlinux.lds
(2)arch/arm/boot/compressed/head.S
(3)arch/arm/boot/compressed/misc.c
首先跳转到head.S中的start函数开始执行,结合lds文件可以看下具体流程,这一部分不做过多介绍。
内核启动汇编阶段
(这个阶段参考链接文件:arch/arm/kernel/vmlinux.lds)
启动汇编阶段的代码是从arch/arm/kernel/head.S开始的,执行起点是stext函数。入口函数是通过vmlinux.lds中的ENTRY(stext)指定的。需要区分的是,在汇编.S文件中也有ENTRY的宏定义,它需要和ENDPROC成对出现,表示定义的一个函数。另外在.S文件中也要指明当前代码所在的段,比如:
__HEAD
ENTRY(stext)
……
ENDPROC(stext)
__HEAD是声明为.head.text段的宏定义,ENTRY和ENDPROC用来定义一个stext的函数。相对应的lds文件如下所示,其中的_text是lds中定义的常量,不同的段中会存在不同的相关常量,如.head.text段的_text常量,.text段的_stext和_etext常量:
. = 0xC0000000 + 0x00008000;
.head.text : {
_text = .;
*(.head.text)
}
这部分主要完成的工作有cpu ID检查,machine ID检查,创建初始化页表,设置C代码运行环境,跳转到内核第一个真正的C函数start_kernel开始执行。
这一阶段涉及到两个重要的结构体:
- (1) 一个是struct proc_info_list 主要描述CPU相关的信息,结构定义在文件arch/arm/include/asm/procinfo.h中,与其相关的函数及变量在文件arch/arm/mm/proc_xxx.S中被定义和赋值,比如arch/arm/mm/proc-v7.S文件是armv7使用的。
- (2) 另一个结构体是描述开发板或者说机器信息的结构体struct machine_desc,结构定义在arch/arm/include/asm/mach/arch.h文件中,其函数的定义和变量的赋值在板极相关文件arch/arm/mach-s3c2410/mach-smdk2410.c中实现,这也是内核移植非常重要的一个文件。
该阶段一般由前面的解压缩代码调用,进入该阶段要求:
MMU = off, D-cache = off, I-cache = dont care,r0 = 0, r1 = machine id.
所有的机器ID列表保存在arch/arm/tools/mach-types 文件中,在编译时会生成相应的头文件在kernel/include/generated/ mach-types.h中。Kernel会根据传入的machine id查找到匹配的struct machine_desc结构,并使用其中的回调函数来启动kernel。
在编译时,上面定义的两种结构体变量,struct proc_info_list会被链接到内核映像文件vmlinux的__proc_info_begin和__proc_info_end之间的段中。struct machine_desc会被链接到内核映像文件vmlinux的__arch_info_begin和__arch_info_end之间的段中。分别对应*(.proc.info.init)和*(.arch.info.init),可以参考下面的连接脚本vmlinux.lds。
__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
在汇编阶段执行的最后,会跳转到C语言阶段继续启动,在head-common.S中通过指令b start_kernel跳转到C代码中执行。
内核启动C语言阶段
C语言的入口函数定义在kernel/init/main.c中,通过函数start_kernel开始执行,它会调用到很多跟平台相关的函数,这部分函数的定义依然在kernel/arch/arm/mach-XXX目录中。
比如machine的定义,在start_kernel就可以使用匹配的machine所定义的函数:
对于平台smdk2410 来说其对应 machine_desc 结构在文件linux/arch/arm/mach-s3c2410/mach-smdk2410.c中初始化:
MACHINE_START(SMDK2410, "SMDK2410")
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.map_io = smdk2410_map_io,
.init_irq = s3c24xx_init_irq,
.init_machine = smdk2410_init,
.timer = &s3c24xx_timer,
MACHINE_END
对于宏MACHINE_START 在文件 arch/arm/include/asm/mach/arch.h 中定义:
#define MACHINE_START(_type,_name) /
static const struct machine_desc __mach_desc_##_type /
__used /
__attribute__((__section__(".arch.info.init"))) = { /
.nr = MACH_TYPE_##_type, /
.name = _name,
#define MACHINE_END /
};
attribute((section(".arch.info.init")))表明该结构体在并以后存放的位置。
还记得上面提到的vmlinux.lds中的信息吗?
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
所以上面定义的内容将被放到这个__arch_info_begin和__arch_info_end之间的段内。这样就使得在汇编文件中也可以显式查找到这个结构了。
接下来就简单介绍一下CPU启动过程,如下所示:
对于SMP,bootstrap CPU会在系统初始化的时候执行cpu_init函数,进行本CPU的初始化设定,具体调用序列是:start_kernel—>setup_arch—> setup_processor—>cpu_init。对于系统中其他的CPU,bootstrap CPU会在系统初始化的最后,对每一个online的CPU进行初始化,具体的调用序列是:
start_kernel--->rest_init--->kernel_init---> kernel_init_freeable--->
kernel_init_freeable--->smp_init--->cpu_up--->_cpu_up--->__cpu_up。
__cpu_up函数是和CPU architecture相关的。对于ARM,其调用序列是
__cpu_up--->boot_secondary--->smp_ops.smp_boot_secondary(SOC相关代码)--->
secondary_startup--->__secondary_switched--->secondary_start_kernel--->cpu_init。
其中涉及到的关键代码目录有:
- Kernel/init/---------------------------C语言启动入口
- Kernel/arch/arm/kernel/----------arm架构通用代码
- Kernel/arch/arm/mach-xxx/-----平台相关代码,移植重点
更多推荐
所有评论(0)