uboot的使命是启动内核,命令是 bootm,那么 uboot 在完成硬件和软件初始化之后是如何启动内核的呢?本节就来分析一下
uboot和内核到底是什么
uboot是一个裸机程序
-
uboot的本质就是一个复杂点的裸机程序。和我们在ARM裸机全集中学习的每一个裸机程序并没有本质区别。
-
ARM裸机第十六部分写了个简单的shell,这东西其实就是个mini型的uboot。
内核本身也是一个”裸机程序”
-
操作系统内核本身就是一个裸机程序,和uboot、和其他裸机程序并没有本质区别。
-
区别就是操作系统运行起来后在软件上分为内核层和应用层,分层后两层的权限不同,内存访问和设备操作的管理上更加精细(内核可以随便访问各种硬件,而应用程序只能被限制的访问硬件和内存地址)。
直观来看:uboot的镜像是u-boot.bin,linux系统的镜像是zImage,这两个东西其实都是两个裸机程序镜像。从系统的启动角度来讲,内核其实就是一个大的复杂点裸机程序。
部署在SD卡中特定分区内
-
一个完整的软件+硬件的嵌入式系统,
-
静止时(未上电时)bootloader、kernel、rootfs等必须的软件都以镜像(zImage,uboot.bin)的形式存储在启动介质中(X210中是iNand/SD卡);
-
运行时都是在DDR内存中运行的,与存储介质无关。比如网吧的无盘工作站。
-
上面2个状态都是稳定状态,第3个状态是动态过程,即从静止态到运行态的过程,也就是启动过程。
-
-
动态启动过程就是一个从SD卡逐步搬移到DDR内存,并且运行启动代码进行相关的硬件初始化和软件架构的建立,最终达到运行时稳定状态。
-
静止时u-boot.bin zImage rootfs都在SD卡中,他们不可能随意存在SD卡的任意位置,因此需要对SD卡进行一个分区,然后将各种镜像各自存在各自的分区中,这样在启动过程中uboot、内核等就知道到哪里去找谁。(uboot和kernel中的分区表必须一致,同时和SD卡的实际使用的分区要一致)
运行时必须先加载到DDR中链接地址处
-
uboot在第一阶段中进行重定位时将第二阶段(整个uboot镜像)加载到DDR的0xc3e00000地址处,这个地址就是uboot的链接地址。
-
内核也有类似要求,uboot启动内核时将内存从SD卡读取放到DDR中(其实就是个重定位的过程),而且必须放在内核的链接地址处,否则启动不起来。譬如我们使用的内核链接地址是0x30008000。
内核启动需要必要的启动参数
-
uboot是无条件启动的,从零开始启动的。
-
内核是不能开机自动完全从零开始启动的,内核启动要别人帮忙。uboot要帮助内核实现重定位(从SD卡到DDR),uboot还要给内核提供启动参数。
启动内核第一步:加载内核到DDR中
-
uboot要启动内核,分为2个步骤:
-
第一步是将内核镜像从启动介质中加载到DDR中
-
第二步是去DDR中启动内核镜像。(内核代码根本就没考虑重定位,因为内核知道会有uboot之类的把自己加载到DDR中链接地址处的,所以内核直接就是从链接地址处开始运行的)
-
静态内核镜像在哪里?
-
SD卡/iNand/Nand/NorFlash等:raw分区
- 常规启动时各种镜像都在SD卡中,因此uboot只需要从SD卡的kernel分区去读取内核镜像到DDR中即可。读取要使用uboot的命令来读取,放到链接地质处。(譬如X210的iNand版本是movi命令,X210的Nand版本就是Nand命令)
-
这种启动方式来加载ddr,使用命令:
movi read kernel 30008000
。其中:- kernel指的是uboot中的kernel分区(就是uboot中规定的SD卡中的一个区域范围,这个区域范围被设计来存放kernel镜像,就是所谓的kernel分区)
- 操作系统起来之后会有先进的分区方法,这中在代码中写死的分区地址就成为原始分区方法。
-
tftp、nfs等网络下载方式从远端服务器获取镜像
-
uboot还支持远程启动,也就是内核镜像不烧录到开发板的SD卡中,而是放在主机的服务器中,然后需要启动时uboot通过网络从服务器中下载镜像到开发板的DDR中。
-
tftp 0x30008000 zImage-qt
-
从PC服务器中下载
zImage-qt
-
下载到DDR的
0x30008000
地址 -
最后用
bootm 0x30008000
启动内核
-
-
-
分析总结:
- 最终结果要的是内核镜像到DDR中特定地址即可,不管内核镜像是怎么到DDR中的。以上2种方式各有优劣。产品出厂时会设置为从SD卡中启动(客户不会还要搭建tftp服务器才能用···);tftp下载远程启动这种方式一般用来开发。
镜像要放在DDR的什么地址?
内核一定要放在链接地址处,链接地址去内核源代码的链接脚本或者Makefile中去查找。X210中是0x30008000。
zImage和uImage的区别联系
bootm命令对应do_bootm函数
-
命令名前加
do_
即可构成这个命令对应的函数,因此当我们bootm命令执行时,uboot实际执行的函数叫do_bootm
函数,在cmd_bootm.c
。 -
do_bootm
刚开始定义了一些变量,然后用宏来条件编译执行了secureboot的一些代码(主要进行签名认证);然后进行了一些一些细节部分操作。然后到了CONFIG_ZIMAGE_BOOT
,用这个宏来控制进行条件编译一段代码,这段代码是用来支持zImage格式的内核启动的。
vmlinuz和zImage和uImage
-
uboot经过编译直接生成的elf格式的可执行程序是u-boot,这个程序类似于windows下的exe格式,在操作系统下是可以直接执行的。但是这种格式不能用来烧录下载。我们用来烧录下载的是u-boot.bin,这个东西是由u-boot使用
arm-linux-objcopy
工具进行加工(主要目的是去掉一些无用的)得到的。这个u-boot.bin就叫镜像(image),镜像就是用来烧录到iNand中执行的。objcopy 就是去掉一些没用的内容,得到一个可以直接执行的程序。 -
linux内核经过编译后也会生成一个elf格式的可执行程序,叫vmlinux或vmlinuz,这个就是原始的未经任何处理加工的原版内核elf文件;嵌入式系统部署时烧录的一般不是这个
vmlinuz/vmlinux
,而是要用objcopy工具去制作成烧录镜像格式(就是u-boot.bin这种,但是内核没有.bin后缀),经过制作加工成烧录镜像的文件就叫Image(制作把78M大的精简成了7.5M,因此这个制作烧录镜像主要目的就是缩减大小,节省磁盘)。 -
原则上Image就可以直接被烧录到Flash上进行启动执行(类似于u-boot.bin),但是实际上并不是这么简单。实际上linux的作者们觉得Image还是太大了所以对Image进行了压缩,并且在image压缩后的文件的前端附加了一部分解压缩代码。构成了一个压缩格式的镜像就叫zImage。(因为当年Image大小刚好比一张软盘(软盘有2种,1.2M的和1.44MB两种)大,为了节省1张软盘的钱于是乎设计了这种压缩Image成zImage的技术)。
-
uboot为了启动linux内核,还发明了一种内核格式叫uImage。uImage是由zImage加工得到的,uboot中有一个工具,可以将zImage加工生成uImage。注意:uImage不关linux内核的事,linux内核只管生成zImage即可,然后uboot中的mkimage工具再去由zImage加工生成uImage来给uboot启动。这个加工过程其实就是在zImage前面加上64字节的uImage的头信息即可。uImage可以通过64字节的头信息知道有关zImage启动所需的信息,这样才可以成功启动zImage
-
原则上uboot启动时应该给他uImage格式的内核镜像,但是实际上uboot中也可以支持zImage,是否支持就看
x210_sd
.h中是否定义了LINUX_ZIMAGE_MAGIC
这个宏。所以大家可以看出:有些uboot是支持zImage启动的,有些则不支持。但是所有的uboot肯定都支持uImage启动。
编译内核得到uImage去启动
如果直接在kernel底下去make uImage
会提示mkimage command not found
。解决方案是去uboot/tools
下cp mkimage /usr/local/bin/
(用户自己的程序),复制mkimage工具到系统目录下。再去make uImage
即可。
zImage启动细节
do_bootm
函数中一直到397行的after_header_check
这个符号处,都是在进行镜像的头部信息校验,证明他是一个有效可执行的镜像。校验时就要根据不同种类的image类型进行不同的校验。所以do_bootm
函数的核心就是去分辨传进来的image到底是什么类型,然后按照这种类型的头信息格式去校验。校验通过则进入下一步准备启动内核;如果校验失败则认为镜像有问题,所以不能启动。
LINUX_ZIMAGE_MAGIC
-
这个是一个定义的魔数,这个数等于0x016f2818,表示这个镜像是一个zImage。也就是说zImage格式的镜像中在头部的一个固定位置存放了这个数作为格式标记。如果我们拿到了一个image,去他的那个位置去取4字节判断它是否等于
LINUX_ZIMAGE_MAGIC
,则可以知道这个镜像是不是一个zImage。 -
命令
bootm 0x30008000
,所以do_boom
的argc=2,argv[0]=bootm argv[1]=0x30008000
。但是实际bootm命令还可以不带参数执行。如果不带参数直接bootm,则会从CFG_LOAD_ADDR
地址去执行(定义在x210_sd.h中)。if (argc < 2) { addr = load_addr; } ulong load_addr = CFG_LOAD_ADDR; /* Default Load Address */ #define CFG_LOAD_ADDR MEMORY_BASE_ADDRESS /* default load address */ #define MEMORY_BASE_ADDRESS 0x30000000
-
zImage头部开始的第37-40字节处存放着zImage标志魔数,从这个位置取出然后对比
LINUX_ZIMAGE_MAGIC
。可以用二进制阅读软件来打开zImage查看,就可以证明。很多软件都可以打开二进制文件,如winhex、UltraEditor。if (*(ulong *)(addr + 9*4) == LINUX_ZIMAGE_MAGIC) { printf("Boot with zImage\n"); // 省略 }
用 vim 打开一个文件,然后执行
%!xxd
即可查看文件的16进制表示
image_header_t
-
这个数据结构是我们uboot启动内核使用的一个标准启动数据结构,zImage头信息也是一个
image_header_t
,但是在实际启动之前需要进行一些改造。// 构建 image_header_t hdr->ih_os = IH_OS_LINUX; hdr->ih_ep = ntohl(addr);
-
images全局变量是
do_bootm
函数中使用,用来完成启动过程的。zImage的校验过程其实就是先确认是不是zImage,确认后再修改zImage的头信息到合适,修改后用头信息去初始化images这个全局变量,然后就完成了校验。
uImage启动
-
LEGACY(遗留的),在
do_bootm
函数中,这种方式指的就是uImage的方式。 -
uImage方式是uboot本身发明的支持linux启动的镜像格式,但是后来这种方式被一种新的方式替代,这个新的方式就是设备树方式(在
do_bootm
方式中叫FIT) -
uImage的启动校验主要在
boot_get_kernel
函数中,主要任务就是校验uImage的头信息,并且得到真正的kernel的起始位置去启动。
总结
-
uboot本身设计时只支持uImage启动,原来uboot的代码也是这样写的。后来有了fdt方式之后,就把uImage方式命令为LEGACY方式,fdt方式命令为FIT方式,于是乎多了写
#if #endif
添加的代码。后来移植的人又为了省事添加了zImage启动的方式,又为了省事把zImage启动方式直接写在了uImage和fdt启动方式之前,于是乎又有了一对#if #endif
。于是乎整天的代码看起来很恶心。 -
第二阶段校验头信息结束,下面进入第三阶段,第三阶段主要任务是启动linux内核,调用
do_bootm_linux
函数来完成。
do_bootm_linux函数
镜像的entrypoint
-
ep就是entrypoint的缩写,就是程序入口。一个镜像文件的起始执行部分不是在镜像的开头(镜像开头有n个字节的头信息),真正的镜像文件执行时第一句代码在镜像的中部某个字节处,相当于头是有一定的偏移量的。这个偏移量记录在头信息中。
-
一般执行一个镜像是:
-
第一步先读取头信息,然后在头信息的特定地址找
MAGIC_NUM
,由此来确定镜像种类; -
第二步对镜像进行校验;
-
第三步再次读取头信息,由特定地址知道这个镜像的各种信息(镜像长度、镜像种类、入口地址);
-
第四步就去entrypoint处开始执行镜像。
-
-
theKernel = (void (*)(int, int, uint))ep;
将ep赋值给theKernel,则这个函数指向就指向了内存中加载的OS镜像的真正入口地址(就是操作系统的第一句执行的代码)。
机器码的再次确定
-
uboot在启动内核时,机器码要传给内核。uboot传给内核的机器码是怎么确定的?
s = getenv ("machid"); if (s) { machid = simple_strtoul (s, NULL, 16); printf ("Using machid 0x%x from environment\n", machid); }
-
第一顺序备选是环境变量machid
-
第二顺序备选是
gd->bd->bi_arch_num
(x210_sd.h中硬编码配置的)
-
传参并启动概述
-
从110行到144行就是uboot在给linux内核准备传递的参数处理。
-
Starting kernel ...
是uboot中最后一句打印出来的东西。这句如果能出现,说明uboot整个是成功的,也成功的加载了内核镜像,也校验通过了,也找到入口地址了,也试图去执行了。如果这句后串口就没输出了,说明内核并没有被成功执行。原因一般是:传参(80%)、内核在DDR中的加载地址·······
传参详解
#if defined (CONFIG_SETUP_MEMORY_TAGS) || \
defined (CONFIG_CMDLINE_TAG) || \
defined (CONFIG_INITRD_TAG) || \
defined (CONFIG_SERIAL_TAG) || \
defined (CONFIG_REVISION_TAG) || \
defined (CONFIG_LCD) || \
defined (CONFIG_VFD) || \
defined (CONFIG_MTDPARTITION)
setup_start_tag (bd);
#ifdef CONFIG_SERIAL_TAG
setup_serial_tag (¶ms);
#endif
#ifdef CONFIG_REVISION_TAG
setup_revision_tag (¶ms);
#endif
#ifdef CONFIG_SETUP_MEMORY_TAGS
setup_memory_tags (bd);
#endif
#ifdef CONFIG_CMDLINE_TAG
setup_commandline_tag (bd, commandline);
#endif
#ifdef CONFIG_INITRD_TAG
if (initrd_start && initrd_end)
setup_initrd_tag (bd, initrd_start, initrd_end);
#endif
#if defined (CONFIG_VFD) || defined (CONFIG_LCD)
setup_videolfb_tag ((gd_t *) gd);
#endif
#ifdef CONFIG_MTDPARTITION
setup_mtdpartition_tag();
#endif
setup_end_tag (bd);
#endif
tag方式传参
-
struct tag
,tag是一个数据结构,在uboot和linux kernel中都有定义tag数据机构,而且定义是一样的。uboot把内核需要的参数准备好,并放在一个特定的地方,之后在告诉内核到指定的地址取参数。 -
tag_header
和tag_xxx
。// tag 的类型 #define ATAG_NONE 0x00000000 #define ATAG_CORE 0x54410001 struct tag_header { u32 size; // tag 的大小 u32 tag; // tag 的类型 }; struct tag { struct tag_header hdr; union { struct tag_core core; struct tag_mem32 mem; struct tag_videotext videotext; struct tag_ramdisk ramdisk; struct tag_initrd initrd; struct tag_serialnr serialnr; struct tag_revision revision; struct tag_videolfb videolfb; struct tag_cmdline cmdline; struct tag_acorn acorn; struct tag_memclk memclk; struct tag_mtdpart mtdpart_info; } u; };
-
可以支持12种不同的 tag,每一个tag代表着不同的信息
-
tag_header
中有这个tag的size和类型编码,kernel拿到一个tag后先分析tag_header
得到tag的类型和大小,然后将tag中剩余部分当作一个tag_xxx
来处理。
-
-
tag_start
与tag_end
。-
kernel接收到的传参是若干个tag构成的,这些tag由
tag_start
起始,到tag_end
结束。 -
内核从
tag_start
开始读取tag
-
-
tag传参的方式是由linux kernel发明的,kernel定义了这种向我传参的方式,uboot只是实现了这种传参方式从而可以支持给kernel传参。
x210_sd.h中配置传参宏
#if defined (CONFIG_SETUP_MEMORY_TAGS) || \
defined (CONFIG_CMDLINE_TAG) || \
defined (CONFIG_INITRD_TAG) || \
defined (CONFIG_SERIAL_TAG) || \
defined (CONFIG_REVISION_TAG) || \
defined (CONFIG_LCD) || \
defined (CONFIG_VFD) || \
defined (CONFIG_MTDPARTITION)
setup_start_tag (bd);
#ifdef CONFIG_SERIAL_TAG
setup_serial_tag (¶ms);
#endif
-
CONFIG_SETUP_MEMORY_TAGS
表示tag_mem
,这个传参内容是内存配置信息。static void setup_memory_tags (bd_t *bd) { int i; for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) { params->hdr.tag = ATAG_MEM; params->hdr.size = tag_size (tag_mem32); params->u.mem.start = bd->bi_dram[i].start; params->u.mem.size = bd->bi_dram[i].size; params = tag_next (params); } }
-
CONFIG_CMDLINE_TAG,tag_cmdline
,传参内容是启动命令行参数,也就是uboot环境变量的bootargs.如果没有定义这个宏,uboot就不会把tag_cmdline
内容传递给内核,导致内核启动过程中出错 -
CONFIG_INITRD_TAG
表示 ramdisk 的初始化参数 -
CONFIG_MTDPARTITION
,传参内容是iNand/SD卡的分区表。void setup_mtdpartition_tag() { char *p, *temp; int i = 0; p = getenv("mtdpart"); params->hdr.tag = ATAG_MTDPART; params->hdr.size = tag_size (tag_mtdpart); for(temp = p; *temp != '\0'; temp++) { if(*temp == ' ') { *temp = '\0'; params->u.mtdpart_info.mtd_part_size[i] = simple_strtoul(p, NULL, 16); p = ++temp; i++; } } params->u.mtdpart_info.mtd_part_size[i] = simple_strtoul(p, NULL, 16); params = tag_next (params); }
-
起始tag是
ATAG_CORE
、结束tag是ATAG_NONE
,其他的ATAG_XXX
都是有效信息tag。static struct tag *params; static void setup_start_tag (bd_t *bd) { params = (struct tag *) bd->bi_boot_params; params->hdr.tag = ATAG_CORE; params->hdr.size = tag_size (tag_core); params->u.core.flags = 0; params->u.core.pagesize = 0; params->u.core.rootdev = 0; params = tag_next (params); }
先给 params 实例化,然后填充
内核如何拿到这些tag?
theKernel (0, machid, bd->bi_boot_params);
uboot最终是调用theKernel函数来执行linux内核的,uboot调用这个函数(其实就是linux内核)时传递了3个参数。这3个参数就是uboot直接传递给linux内核的3个参数,通过寄存器来实现传参的。(第1个参数就放在r0中,第二个参数放在r1中,第3个参数放在r2中)第1个参数固定为0,第2个参数是机器码,第3个参数传递的就是大片传参tag的首地址。
移植时注意事项
-
uboot移植时一般只需要配置相应的宏即可
-
kernel启动不成功,注意传参是否成功。传参不成功首先看uboot中bootargs设置是否正确,其次看uboot是否开启了相应宏以支持传参。比如第一次用的是nand版本的uboot启动,然后uboot会把nand版本的环境变量刷到SD卡中的环境变量分区。下一次用iNand版本的uboot,它会从SD卡中加载nand版本的环境变量,这个不对的boot args就导致iNand版本的uboot无法启动内核
uboot启动内核的总结
-
启动4步骤
-
第一步:将内核搬移到DDR中
-
第二步:校验内核格式、CRC等
-
第三步:准备传参,uboot把要给内核的参数准备好
-
第四步:远跳转执行内核,同时使用r0, r1, r2传参
-
-
涉及到的主要函数是:do_boom和do_bootm_linux
-
uboot能启动的内核格式:
-
zImage
-
uImage
-
fdt 方式
-
-
跳转与函数指针的方式运行内核