打印
[应用相关]

Linux系统从uboot到内核启动流程

[复制链接]
909|23
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
labasi|  楼主 | 2019-7-9 10:18 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
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中实现。


使用特权

评论回复
沙发
labasi|  楼主 | 2019-7-9 10:18 | 只看该作者
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,它的主要功能是显示启动**,加载kernel、dtb、rootfs文件系统,并且启动kernel。一般使用uboot来做。
所以在boot分区,我们要烧写入这三部分的引导代码,mbrc、uboot-spl、uboot。

使用特权

评论回复
板凳
labasi|  楼主 | 2019-7-9 10:19 | 只看该作者
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;

使用特权

评论回复
地板
labasi|  楼主 | 2019-7-9 10:19 | 只看该作者
我们需要关心的是:

    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()函数中:


使用特权

评论回复
5
labasi|  楼主 | 2019-7-9 10:20 | 只看该作者
源文件: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);
                  }
……

}


使用特权

评论回复
6
labasi|  楼主 | 2019-7-9 10:20 | 只看该作者
首先判断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。

使用特权

评论回复
7
labasi|  楼主 | 2019-7-9 10:20 | 只看该作者
有了上面的知识,那么我们如何去设置uboot header中的地址呢,其实这一步是在制作uImage的时候,使用mkimage工具指定的加载地址和运行地址。

mkimage -A arm -O linux -C none -a 0x30008000 -e 0x30008000 -d zImage uImage
-A:CPU类型
-O:操作系统
-C:采用的压缩方式
-a:内核加载地址
-e:内核入口地址

使用特权

评论回复
8
labasi|  楼主 | 2019-7-9 10:21 | 只看该作者
制作镜像头以及下载地址就有两种情况:
第一种:

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,所以将会报错。

使用特权

评论回复
9
labasi|  楼主 | 2019-7-9 10:21 | 只看该作者
第二种:

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进行搬运了,节省了启动时间。

使用特权

评论回复
10
labasi|  楼主 | 2019-7-9 10:21 | 只看该作者
在上面的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 传递给内核参数存放在内存中的首地址。

使用特权

评论回复
11
labasi|  楼主 | 2019-7-9 10:22 | 只看该作者
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文件可以看下具体流程,这一部分不做过多介绍。

使用特权

评论回复
12
labasi|  楼主 | 2019-7-9 10:22 | 只看该作者
内核启动汇编阶段
(这个阶段参考链接文件: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开始执行。


使用特权

评论回复
13
labasi|  楼主 | 2019-7-9 10:22 | 只看该作者
这一阶段涉及到两个重要的结构体:

(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。

使用特权

评论回复
14
labasi|  楼主 | 2019-7-9 10:23 | 只看该作者
在编译时,上面定义的两种结构体变量,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代码中执行。

使用特权

评论回复
15
labasi|  楼主 | 2019-7-9 10:23 | 只看该作者
内核启动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

使用特权

评论回复
16
labasi|  楼主 | 2019-7-9 10:24 | 只看该作者
对于宏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")))表明该结构体在并以后存放的位置。

使用特权

评论回复
17
labasi|  楼主 | 2019-7-9 10:24 | 只看该作者
还记得上面提到的vmlinux.lds中的信息吗?

__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;


所以上面定义的内容将被放到这个__arch_info_begin和__arch_info_end之间的段内。这样就使得在汇编文件中也可以显式查找到这个结构了。

使用特权

评论回复
18
labasi|  楼主 | 2019-7-9 10:24 | 只看该作者
接下来就简单介绍一下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。

使用特权

评论回复
19
labasi|  楼主 | 2019-7-9 10:24 | 只看该作者
__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/-----平台相关代码,移植重点

使用特权

评论回复
20
晓伍| | 2019-8-7 10:04 | 只看该作者
感谢楼主分享

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

51

主题

3372

帖子

2

粉丝