Linux-uboot-学习笔记(8):uboot启动内核

Linux-uboot-学习笔记(8):uboot启动内核

uboot启动时,在执行到theKernel指针时即跳转到内核执行,也就标志着uboot的结束。在此之前uboot要进行内核启动的最后准备:从内核镜像的部署位置将内核搬移到DDR中,校验内核格式,准备内核传的参数,跳转到theKernel去执行内核。

一、uboot内核是什么

内核实质上是一个大的、复杂的“裸机程序”,和uboot、和其他裸机程序并没有本质区别。
区别就是操作系统运行起来后在软件上分为内核层和应用层,分层后两层的权限不同,内存访问和设备操作的管理上更加精细(内核可以随便访问各种硬件,而应用程序只能被限制的访问硬件和内存地址)。
直观来看:uboot的镜像是u-boot.binlinux系统的镜像是zImage,这两个东西其实都是两个裸机程序镜像。从系统的启动角度来讲,内核其实就是一个大的复杂点裸机程序。

uboot是无条件启动的,从零开始启动的。而内核是不能开机自动完全从零开始启动的,内核启动要别人帮忙。uboot要帮助内核实现重定位(从SD卡到DDR),uboot还要给内核提供启动参数。

二、特定的分区部署

一个完整的软件+硬件的嵌入式系统,静止时(未上电时)bootloader、kernel、rootfs等必须的软件都以镜像的形式存储在启动介质中(X210中是iNand/SD卡);运行时都是在DDR内存中运行的,与存储介质无关。上面2个状态都是稳定状态,第3个状态是动态过程,即从静止态到运行态的过程,也就是启动过程
动态启动过程就是一个从SD卡逐步搬移到DDR内存,并且运行启动代码进行相关的硬件初始化和软件架构的建立,最终达到运行时稳定状态。
静止时u-boot.bin zImage rootfs都在SD卡中,他们不可能随意存在SD卡的任意位置,因此需要对SD卡进行一个分区,然后将各种镜像各自存在各自的分区中,这样在启动过程中uboot、内核等就知道到哪里去找谁。(uboot和kernel中的分区表必须一致,同时和SD卡的实际使用的分区要一致)

uboot在第一阶段中进行重定位时将第二阶段(整个uboot镜像)加载到DDR的0xc3e00000地址处,这个地址就是uboot的链接地址。
内核也有类似要求,uboot启动内核时将内存从SD卡读取放到DDR中(其实就是个重定位的过程),不能随意放置,必须放在内核的链接地址处,否则启动不起来。譬如我们使用的内核链接地址是0x30008000。

三、uboot内核启动过程

1. 启动内核第一步:加载内核到DDR中

uboot要启动内核,分为2个步骤:第一步是将内核镜像从启动介质中加载到DDR中,第二步是去DDR中启动内核镜像。(内核代码根本就没考虑重定位,因为内核知道会有uboot之类的把自己加载到DDR中链接地址处的,所以内核直接就是从链接地址处开始运行的

内核镜像最初在SD卡的kernel分区,因此在内核启动前需要从SD卡的kernel分区读取内和镜像(zImage-qt)并加载到DDR中。
通过uboot指令读取:movi read kernel 30008000
通过tftp加载:tftp 0x30008000 zImage-qt(加载前要将zImage-qt放到tftpboot文件夹下)
加载完成后,通过指令启动内核:bootm 0x30008000

最终结果要的是内核镜像到DDR中特定地址即可,不管内核镜像是怎么到DDR中的。以上2种方式各有优劣。产品出厂时会设置为从SD卡中启动(客户不会还要搭建tftp服务器才能用···);tftp下载远程启动这种方式一般用来开发

2. 启动内核第二步:校验内核格式

首先说明内核镜像大概有三种版本:Image、zImage和uImage

linux内核经过编译后也会生成一个elf格式的可执行程序,叫vmlinux或vmlinuz,这个就是原始的未经任何处理加工的原版内核elf文件;嵌入式系统部署时烧录的一般不是这个vmlinuz/vmlinux,而是要用objcopy工具去制作成烧录镜像格式(就是u-boot.bin这种,但是内核没有.bin后缀),经过制作加工成烧录镜像的文件就叫Image(制作把78M大的精简成了7.5M,因此这个制作烧录镜像主要目的就是缩减大小,节省磁盘)。
实际上linux的作者们觉得Image还是太大了所以对Image进行了压缩,并且在image压缩后的文件的前端附加了一部分解压缩代码(这部分未经压缩)。构成了一个压缩格式的镜像就叫zImage
uboot为了启动linux内核,还发明了一种内核格式叫uImage。uImage是由zImage加工得到的,uboot中有一个工具,可以将zImage加工生成uImage。注意:uImage不关linux内核的事,linux内核只管生成zImage即可,uboot中的mkimage工具再去由zImage加工生成uImage来给uboot启动这个加工过程其实就是在zImage前面加上64字节的uImage的头信息即可。

do_bootm函数中一直到397行的after_header_check这个符号处,都是在进行镜像的头部信息校验。校验时就要根据不同种类的image类型进行不同的校验。所以do_bootm函数的核心就是去分辨传进来的image到底是什么类型,然后按照这种类型的头信息格式去校验。校验通过则进入下一步准备启动内核;如果校验失败则认为镜像有问题,所以不能启动。

(1)zImage启动

在这里插入图片描述
对于zImage的魔数,如果校验成功了,则通过zImage的格式启动。因此我们可以查看zImage的内容,看看魔数所在的位置与检测时的方法是否对应。
在这里插入图片描述
在这里插入图片描述
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这个全局变量,然后就完成了校验。

(2)uImage启动

在这里插入图片描述
常规情况下uboot支持3种情况:
IMAGE_FORMAT_INVALID:镜像格式不合法。
IMAGE_FORMAT_LEGACY:uImage格式。
IMAGE_FORMAT_FIT:设备树格式。

uImage的启动校验主要在boot_get_kernel函数中,主要任务就是校验uImage的头信息,并且得到真正的kernel的起始位置去启动。
在这里插入图片描述
这里传参的方式与zImage相同,支持两种情况,参数小于2时使用默认地址;参数大于等于2时,将argv[1]传进去。
在这里插入图片描述
这里是通过魔数码判断uImage镜像格式,并打印信息。
在这里插入图片描述这里是判断完成之后,打印镜像内部一些信息。

在这里插入图片描述
这里是uImage的解压缩方式。
在这里插入图片描述
通过分析代码可得,uImage与zImage大致相同,都是先通过魔数码判断镜像格式,之后再通过构建images将参数保存下来。images中包含了该镜像的头信息,通过这些头信息能够得到该镜像的基本内容(魔数,镜像长度、镜像种类、入口地址等)。之后的传参也需要用到这里面的信息。

3. 启动内核第三步:准备内核传参

(1)entrypoint介绍
在这里插入图片描述
ep就是entrypoint的缩写,就是程序入口。一个镜像文件的起始执行部分不是在镜像的开头(镜像开头有n个字节的头信息),真正的镜像文件执行时第一句代码在镜像的中部某个字节处,相当于头是有一定的偏移量的。这个偏移量记录在头信息中。
一般执行一个镜像都是:第一步先读取头信息,然后在头信息的特定地址找MAGIC_NUM,由此来确定镜像种类;第二步对镜像进行校验;第三步再次读取头信息,由特定地址知道这个镜像的各种信息(镜像长度、镜像种类、入口地址);第四步就去entrypoint处开始执行镜像。

(2)机器码对比
在这里插入图片描述

uboot在启动内核时,机器码要传给内核。uboot传给内核的机器码是怎么确定的?第一顺序备选是环境变量machid,第二顺序备选是gd->bd->bi_arch_num(x210_sd.h中硬编码配置的)。

(3)tag传参
uboot给kernel传参是通过struct tagtag是一个数据结构,在uboot和linux kernel中都有定义tag数据机构,而且定义是一样的。
因为uboot和kernel不能同时运行,uboot结束后通过thekernel指针开始执行kernel。所以uboot以一种特定格式(tag)放在特定的位置,然后kernel启动后去对应的位置用对应的方法取。将来传递的参数是一个一个的tag。在这里插入图片描述
tag_header和tag_xxx。tag_header中有这个tag的size和类型编码(0x54410001),kernel拿到一个tag后先分析tag_header得到tag的类型(core)和大小,然后将tag中剩余部分当作一个tag_xxx(tag_core)来处理。
在这里插入图片描述
tag_start与tag_end。kernel接收到的传参是若干个tag构成的,这些tag由tag_start起始,到tag_end结束。

起始tag是ATAG_CORE、结束tag是ATAG_NONE,其他的ATAG_XXX都是有效信息tag。
CONFIG_SETUP_MEMORY_TAGS,tag_mem,传参内容是内存配置信息
CONFIG_CMDLINE_TAG,tag_cmdline,传参内容是启动命令行参数,也就是uboot环境变量的bootargs.
CONFIG_INITRD_TAG,linux的INITRD机制。
CONFIG_MTDPARTITION,传参内容是iNand/SD卡的分区表。

(4)theKernel函数
通过上面tag传参我们可以知道,当内核启动后,通过相应的tag能够进行各种信息参数的传递,那么内核怎么样能够拿到我们这些tag呢?
答:uboot最终是调用theKernel函数来执行linux内核的,uboot调用这个函数(其实就是linux内核)时传递了3个参数这3个参数就是uboot直接传递给linux内核的3个参数,通过寄存器来实现传参的。(第1个参数就放在r0中,第二个参数放在r1中,第3个参数放在r2中)第1个参数固定为0,第2个参数是机器码,第3个参数传递的就是大片传参tag的首地址。
在这里插入图片描述
theKernel = (void (*)(int, int, uint))ep;将ep赋值给theKernel,则这个函数指向就指向了内存中加载的OS镜像的真正入口地址(就是操作系统的第一句执行的代码)。Thekernel是uboot的最后一句,通过函数指针将参数传进去。

发布了72 篇原创文章 · 获赞 98 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/qq_42826337/article/details/105130631