uboot和内核的本质
uboot的本质是一个裸机程序,操作系统内核本质也是裸机程序。区别就是操作系统运行起来后,在软件上分为内核层和应用层,分层后两层的权限不同,内存访问和设备操作的管理更加精细(内核可以随便访问各种硬件,而应用程序只能被限制地访问硬件和内存地址)。
直观来看,uboot的镜像是uboot.bin,Linux系统的镜像是zImage,这两个东西其实都是两个裸机程序镜像,从系统启动的角度来讲,内核其实就是一个大的、复杂的裸机程序。
一个完整的软件+硬件的嵌入式系统,静止时(未上电时)bootloader、kernel、rootfs等必须的软件都以镜像的形式存储在启动介质中(X210中是iNand/SD卡);运行时都是在DDR内存中运行的,与存储介质无关。上面两个状态都是稳定状态,第3个状态是动态过程,也就是启动过程。
动态启动过程就是一个从SD卡逐步搬移到DDR内存,并且运行启动代码进行相关的硬件初始化和软件架构的建立,最终达到运行时稳定状态。
静止时uboot.bin、zImage、rootfs都在SD卡中,它们不可能随意存放在SD卡的任意位置,因此需要对SD卡进行一个分区,然后将各种镜像各自存放在各自的分区,这样在启动过程中uboot、内核等就知道到哪里去找谁。
uboot在第一阶段中进行重定位时将第二阶段(整个uboot镜像)加载到DDR的0xc3e00000地址处,这个地址就是uboot的链接地址。
内核也有类似要求,uboot启动内核时将内存从SD卡读取放到DDR中(其实就是个重定位的过程),不能随意放置,必须放在内核的链接地址处,否则启动不起来。
uboot是无条件启动的,从零开始启动的;内核启动需要帮忙,uboot要帮助内核实现重定位(从SD卡到DDR),uboot还要给内核提供启动参数。
启动内核第一步:加载内核到DDR中
uboot要启动内核,分为两个步骤:第一步是将内核镜像从启动介质中加载到DDR中,第二步是去DDR中启动内核镜像。(内核代码没有考虑重定位,而是直接由uboot加载到DDR中的它的链接地址)。
常规启动时,各种镜像都在SD卡中,因此uboot只需要从SD卡的kernel分区去读取内核镜像到DDR中即可。读取要使用uboot的命令来读取。譬如X210的iNand版本时movi命令,movi read kernel 30008000。其中kernel指的是uboot中的kernel分区(uboot规定的SD卡中的一个区域范围)。
uboot还支持远程启动,也就是内核镜像不用烧录到开发板的SD卡中,而是放在主机的服务器中,然后需要启动时uboot通过网络从服务器中下载镜像到开发板的DDR中。
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,这个东西是由uboot使用arm-linux-objopy工具进行加工得到的。这个uboot.bin就叫镜像(image),镜像是用来烧录到iNand中执行的。
Linux内核经过编译后也会生成一个elf格式的可执行程序,叫vmlinux或vmlinuz,这个就是原始的未经任何处理加工的原版内核elf文件;嵌入式系统部署时烧录的一般不是这个,而是要用objcopy工具去制作成烧录镜像格式(就是uboot.bin格式,但是没有bin后缀),经过制作加工成烧录镜像的文件就叫Image(制作把78M大的精简成了7.5M,因此这个制作烧录镜像的目的就是缩减大小,节省磁盘)。
原则上Image就可以直接被烧录到Flash上进行启动执行(类似于uboot.bin)。实际上Linux得作者们觉得Image还是太大了所以对Image进行了压缩,并且在Image压缩后的文件的前端加了一部分解压缩的代码,构成了一个压缩格式的镜像就叫做zImage。
uboot为了启动内核,还发明了一种内核格式叫uImage,uImage是由zImage加工得到的。(注意,uImage不关Linux内核的事,Linux只管生成zImage)。
zImage启动细节
do_bootm函数中一直到397行的after_header_check这个符号处,都是在进行镜像的头部信息校验。校验时就要根据不同种类的Image类型进行不同的校验。所以do_bootm函数的核心就是去分辨传进来的Image到底是什么类型,然后按照这种类型的头信息格式去校验。校验通过则进入下一步去启动内核;失败则认为内核有问题,不能启动。
- LINUX_ZIMAGE_MAGIC
这个是一个定义的魔数,表示这个镜像是一个zImage,也就是说zImage格式的镜像中在头部的一个固定位置存放了这个数作为格式标记。如果我们拿到一个Image,去他的那个位置去取4字节判断它是否等于对应魔数,就可知道镜像类型。
- image_header_t
这个数据结构是uboot启动内核时使用的一个标准启动数据结构,zImage头信息也是一个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启动的镜像格式,但是后来这种方式被一种新的方式替代,这个新的方式就是设备树方式。
uImage的启动校验主要在boot_get_kernel函数中,主要任务就是校验uImage的头信息,并且得到真正的kernel的起始位置去启动。
上述校验头信息即为启动内核第二步;第三阶段是调用do_bootm_linux函数。
do_bootm_linux函数
函数在uboot/lib_arm/bootm.c中。
ep就是entrypoint的缩写,就是程序入口。一个镜像文件的起始执行部分不是在镜像的开头(开头是n字节的头信息),真正的镜像文件执行时第一句代码在镜像的中部某个字节处,相当于一定的偏移量,这个偏移量记录在头信息中。
一般执行一个镜像都是:第一步先读取头信息,然后在头信息的特定地址去找MAGIC_NUM,由此来确定镜像种类;第二步对镜像进行校验;第三步再次读取头信息,由特定地址知道这个镜像的各种信息(镜像长度,镜像种类,入口地址);第四步就是去entrypoint处执行镜像。
theKernel = (void (*)(int, int, uint))ep;
将ep赋值给theKernel,则这个函数就指向了内存中加载的OS镜像的真正入口地址(就是操作系统的第一句执行代码)。
机器码的再次确定
uboot在启动内核时,机器码要传给内核。uboot传给内核的机器码如何确定?第一顺序是环境变量machid,第二顺序是gd->bd->bi_arch_num(x210_sd.h中硬编码配置的)
Starting kernel ... 这个是uboot中最后一句打印出来的东西。这句如果能出现,说明uboot整个是成功的,也成功加载了内核镜像,也校验通过了,也找到入口地址了,也试图去执行了。如果这句之后串口没有输出,说明内核没有被成功执行,原因一般是:传参(占80%)、内核在DDR中的加载地址。
传参详解
- tag方式传参
struct tag,tag是一个数据结构,在uboot和linux kernel中都有定义tag数据结构,而且定义是一样的。
tag_header和tag_xxx:tag_header中有这个tag的size和类型编码,kernel拿到一个tag后先分析tag_header得到tag的类型和大小,然后将tag中剩余部分当作一个tag_xxx处理。
tag_start与tag_end:kernel接收到的参数是若干个tag组成的,这些tag由tag_start开始,由tag_end结束。
tag传参的方式是由Linux kernel发明的,kernel定义了这种向自身传参的方式,uboot只是实现了这种方式从而可以支持给kernel传参。
- x210_sd.h中配置传参宏
CONFIG_SETUP_MEMORY_TAGS,tag_mem,传参内容是内存配置信息。
CONFIG_CMDLINE_TAG,tag_cmdline,传参内容是启动命令行参数,也就是uboot环境变量的bootargs。
内核如何拿到这些tag?
uboot最终是调用theKernel函数来执行Linux内核的,uboot调用这个函数(其实就是Linux内核)时传递了3个参数。这3个参数就是uboot直接传递给Linux内核的3个参数,通过寄存器来实现传参的。(第一个参数就放在r0中,第二个参数放在r1中,第三个参数放在r2中)第一个参数固定为0,第二个参数时机器码,第三个参数传递的就是大片传参tag的首地址。