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/-----平台相关代码,移植重点

Linux系统从uboot到内核启动流程相关推荐

  1. 【嵌入式】构建嵌入式Linux系统(uboot、内核、文件系统)

    知识架构及层次 Linux内核由三部分构成: Bootloader:启动引导系统(可执行文件) Kernel:内核(可执行文件) Root File System:根文件系统 嵌入式Linux系统构成 ...

  2. RK3399-SD卡linux系统制作(uboot,kernel内核,根文件)

    从sd卡启动:原文链接:https://blog.csdn.net/weixin_45746588/article/details/107952681 1.VM虚拟机安装和PC端ubuntu系统安装 ...

  3. Linux内核启动流程(简介)

    1. vmlinux.lds 首先分析 Linux 内核的连接脚本文件 arch/arm/kernel/vmlinux.lds,通过链接脚本可以找到 Linux 内核的第一行程序是从哪里执行的: 第 ...

  4. Linux内核启动流程(待完善)

    文章目录 一.Linux内核自解压过程 二.Linux内核启动第二阶段stage1 2.1.linux系统启动入口函数(stext) 2.2.内核初始化阶段(start_kernel) 2.3.2 r ...

  5. linux内核启动第一个进程,linux内核启动流程

    描述 Linux的启动代码真的挺大,从汇编到C,从Makefile到LDS文件,需要理解的东西很多.毕竟Linux内核是由很多人,花费了巨大的时间和精力写出来的.而且直到现在,这个世界上仍然有成千上万 ...

  6. 【内核】linux内核启动流程详细分析【转】

    转自:http://www.cnblogs.com/lcw/p/3337937.html Linux内核启动流程 arch/arm/kernel/head-armv.S 该文件是内核最先执行的一个文件 ...

  7. 【内核】linux内核启动流程详细分析

    Linux内核启动流程 arch/arm/kernel/head-armv.S 该文件是内核最先执行的一个文件,包括内核入口ENTRY(stext)到start_kernel间的初始化代码, 主要作用 ...

  8. linux内核启动流程(文章最后流程图)

    原文:linux内核启动流程 本文以Linux3.14版本源码为例分析其启动流程.各版本启动代码略有不同,但核心流程与思想万变不离其宗. 内核映像被加载到内存并获得控制权之后,内核启动流程开始.通常, ...

  9. Linux内核启动流程(vmlinux)

    vmlinux的启动过程,之所以是vmlinux,是因为其他格式的内核在进行与vmlinux相同的流程之前会有一些特有的操作.         比如对于压缩格式的内核zImage,它首先会进行自解压得 ...

最新文章

  1. js跟随鼠标移动的写法
  2. 你真的了解 i++, ++i 和 i+++++i 以及 i+++i++ 吗?
  3. 两个字符串的删除操作
  4. java 回调函数很好懂
  5. [深度学习TF2][RNN-NPL数据预处理] -Tokenizer函数
  6. textedit实时显示位置_加什么地形就看什么等高线!等高线实时预览就是这么爽...
  7. python中select模块_基于python select.select模块通信的实例讲解 如何用python写个串口通信的程序...
  8. 手把手带你深入解析静态分派 动态分派原理 | 原力计划
  9. CSS3转换之移位translate(CSS3)
  10. ECshop sina
  11. c# 注册了Ctrl+空格为热键,捕获后发送Ctrl+Shift
  12. 【转载】C#反射 获取程序集信息和通过类名创建类实例(转载)
  13. SVN源码泄露漏洞总结
  14. LaTex(2021)安装教程
  15. Windows界面UI自绘编程(上)之下部
  16. OpenGL着色器语言
  17. 你心有喜欢的明星吗??
  18. JAVA代码实现人物照片的人像分割 | 百度AI
  19. S3C2440 I2C总线控制
  20. 药明生基扩建美国费城细胞及基因疗法生产基地

热门文章

  1. keydown、keypress与keyup用法及区别详解
  2. VB.NET-Textbox中的KeyPress事件
  3. 自动化框架之logbook
  4. 是phalcon快还是java快_PHP 框架:Yaf 和 Phalcon 谁更快?
  5. Docker安装失败No package docker-ce available.
  6. xshell强制更新解决办法(针对xshell6)
  7. matplot设置xy轴范围
  8. java 导出如何合并列_Java导出excel时合并同一列中相同内容的行思路详解
  9. 凯文·凯最新演讲:预测未来非常困难 但未来技术可以预测
  10. Springboot过滤器禁止ip频繁访问功能实现