带你了解uboot如何启动Linux内核

uboot和内核到底是什么?
uboot实质就是一个复杂的裸机程序;uboot可以被配置也可以做移植;

操作系统内核本身就是一个裸机程序,和我们学的uboot和其他裸机程序没有本质的区别;区别就是我们操作系统运行起来后可以分为应用层和内核层,分层后,两层的权限不同,内存访问和设备操作的管理上更加精细(内核可以随便方位各种硬件,而应用程序只能被限制的访问硬件和内存地址)

直观来看:uboot的镜像是u-boot.bin,Linux系统的镜像是zImage,这两个东西其实都是两个裸机程序镜像。从系统启动的角度来讲的。内核和uboot就是裸机程序;

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

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

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

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

静态内核镜像在哪里?
(1)SD卡、inand、nand、norflash等:raw区
常规启动时各种镜像都在SD卡中,因此uboot只需要从启动介质SD卡的内核分区去读取内核镜像到DDR的链接地址处,读取要使用uboot命令来读取(例如inand版本是movi命令,x210的nand版本就是nand命令)
(2)这种启动方式来加载ddr,使用命令:movi read kernel 30008000. 其中kernel指的是uboot中的kernel分区(就是uboot规定SD卡中的一个区域范围,这个区域范围被设计来存放kernel镜像,就是所谓的kernel分区,有时候也叫原始分区,操作系统启动后可以用文件系统来管理分区。)
(3)tftp、nfs等网络下载方式从远端服务器获取镜像
uboot还支持远程启动,也就是内核镜像不烧录到开发板的SD卡中,而是放在主机的服务器中,然后需要启动时uboot通过网络从服务器中下载镜像到开发板的DDR中。

分析总结:最终结果是内核镜像到DDR中的特定地址即可。以上两种方式各有优劣,产品在出厂时会设置从SD卡启动,客户不会还有去搭建tftp服务器才能使用,tftp下载远程启动,适合开发。

镜像要放在DDR的什么地址?
内核一定要放在链接地址处,链接地址去内核源代码的连接脚本或者makefile中去查找。x210中是0x300080000。

zImage和uImage的区别联系
bootm命令对应do_boot函数
(1)命令名前加do_,do_bootm在cmd_bootm.c中
(2)do_bootm刚开始定义了一个变量,然后用宏来条件编译执行了secureboot的一些代码主要是安全认证。然后到了CONFIG_ZIMAGE_BOOT,用这个宏来控制进行条件编译的一段代码,这段代码是用来支持zImage格式的内核启动的。

vmlinuz和zImage和uImage
(1)uboot编译后生成了一个ELF格式的可执行程序u-boot,这个类似于windows下的EXE格式,在操作系统下可以执行,但是不能用来烧录下载,我们用来烧录下载的是u-boot.bin,它是由u-boot使用arm-linux-objcopy(主要目的是去掉一些无用的)得到。u-boot.bin就叫镜像,镜像就是用来烧录的,烧录到inand中执行,或者是放在SD卡中执行,或者dnw下载执行,
(2)Linux kernel经过编译后也会生成一个ELF格式的可执行程序,叫vmlinuz或者vmlinux,这个就是原始的未经任何处理加工的原版内核ELF文件,嵌入式部署烧录的一般不是这个vmlinuz,而是使用objcpy工具去制作成烧录镜像格式(就是u-boot.bin这种,但是内核没有.bin后缀)制作的这个烧录镜像是Image,制作镜像主要目的是缩减大小,节俭磁盘。
(3)原则上Image就可以直接被烧录到Flash,但实际上并不是这么简单,实际上Linux的作者们觉得Image太大了,对其进行了压缩,并且在压缩后的前一段部分,附加了一部分解压缩代码,构成了一个压缩可是的镜像就是zImage(因为当年Image大小刚刚比一张软盘大,(软盘有2中,1.2M和1.4M,Image比1.4M大一点),为了节省一张软盘的钱,于是乎,设计了这种压缩Image成zImage的技术)
(4)uboot为了启动Linux内核,还发明了一种内核格式叫uImage。uImage是由zImage加工得到的,uboot中有一个工具,可以将zImage加工生产uImage。注意:uImage不管Linux内核的事,Linux内核只管生成zImage即可,然后uboot中的mkimage工具再去有zImage加工生成uImage来给uboot启动。这个加工的过程是在zImage前面加上64字节的uImage的头信息即可。
(5)原则上uboot启动时应该给他uImage格式的内核镜像,但是实际上uboot也可以支持zImage,是否支持就看x210_sd.h中是否定义了LINUX_ZIMAG_MAGIC这个宏。所以有些uboot支持zImage启动,有些则不支持。但是所有的uboot肯定都支持uImage启动。

编译内核得到uImage去启动
(1)第一步:如果直接在kernel底下去make uImage会提示mkimage没找到,解决方案是在 /uboot/tool下去复制到 /usr/local/bin.下面去,复制它到系统目录下,再去执行make uImage就可以了

zImage启动细节
#ifdef CONFIG_ZIMAGE_BOOT
#define LINUX_ZIMAGE_MAGIC 0x016f2818
/* find out kernel image address */
if (argc < 2) {
addr = load_addr;
debug ("* kernel: default image load address = 0x%08lx\n",
load_addr);
} else {
addr = simple_strtoul(argv[1], NULL, 16);
debug ("* kernel: cmdline image address = 0x%08lx\n", img_addr);
}

if (*(ulong *)(addr + 9*4) == LINUX_ZIMAGE_MAGIC) {
printf("Boot with zImage\n");
addr = virt_to_phys(addr);
hdr = (image_header_t *)addr;
hdr->ih_os = IH_OS_LINUX;
hdr->ih_ep = ntohl(addr);

memmove (&images.legacy_hdr_os_copy, hdr, sizeof(image_header_t));

/* save pointer to image header */
images.legacy_hdr_os = hdr;

images.legacy_hdr_valid = 1;

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

#define LINUX_ZIMAGE_MAGIC 0x016f2818
(1)这是一个魔数,0x016f2818表示这个镜像是zImage,也就是说zImage格式的镜像中,在头部的一个固定位置存放了这个数,作为格式标记,如果我们拿到了一个image,去他的那个位置去取4个字节,判断它是否等于这个魔数LINUX_ZIMAGE_MAGIC。则可以知道这个镜像是不是zImage
(2)命令bootm 0x30008000,所以do_bootm的argc=2,argv[0]=bootm argv[1]=0x30008000 但是实际bootm命令还可以不带参数执行,如果不带参数直接bootm则会从,CFG_LOAD_ADDR地址去执行(定义在x210_sd.h中)
(3)zImage从头部开始的第37到40个字节,存的是zImage的标志的魔数,从这个位置取出对比LINUX_ZIMAGE_MAGIC,我们可以用二进制查看工具来查看zImage的镜像,发现真的是存放的这个魔数。

image_header_t
(1)这个数据结构是我们uboot启动内核使用的一个标准启动数据结构,zImage头信息也是一个image_header_t,但是在实际启动之前需要进行一些改造,
if (*(ulong *)(addr + 9*4) == LINUX_ZIMAGE_MAGIC) {
printf("Boot with zImage\n");
addr = virt_to_phys(addr);
hdr = (image_header_t *)addr;
hdr->ih_os = IH_OS_LINUX;
hdr->ih_ep = ntohl(addr);

memmove (&images.legacy_hdr_os_copy, hdr, sizeof(image_header_t));

/* save pointer to image header */
images.legacy_hdr_os = hdr;

images.legacy_hdr_valid = 1;

这里就是在进行改造。
(2)image全局变量是在bootm函数中使用,目的是用来指向 os/initrd/fdt images,也就是用来完成启动的,zImage校验过程先确定是不是zImage,然后再修改zImage头信息,到合适,也就是上面的改造,最后再用这个头信息去初始化image,然后完成了校验。
zImage启动方式是后来添加的,而且用了goto的方式跳转了一部分代码,本身对uboot的结构上添加的。

uImage启动
(1)在uboot启动的do_boot中有一个legacy的方法,指的就是uImage这样的方式,为什么是legacy(遗留的),是因为uImage本身是uboot发明的一种启动的方式,后来这种方式是不好的,被废弃,于是被一种新的方式给替代了,新的方式就是设备树的方式,在这里被叫做fit,这个就是设备树的方式。
(2)uImage启动校验函数是在boot_get_kernel这个函数里,主要任务就是校验我们的uImage的头信息,并且得到真正的kernel的起始位置去启动。

总结:uboot本身也只支持uImage方式启动的,后来有了设备树之后,就把uImage方式命名为legacy方式,fdt方式就命名为fit方式,于是乎多了#if #endif添加代码。后来移植的人又为了省事添加了zImage的方式,又为了省事,添加了#if #endif .

第二阶段校验头信息结束,进行第三阶段,启动Linux内核,调用do_bootm_linux这个函数

do_bootm_linux
(1)找到do_bootm_linux,这个函数在lib_arm/bootm.c中,sourceinsight找不到,要搜索查找,

(2)镜像的entrypoint
ep就是程序入口,一个镜像的起始部分不是在镜像的开头(镜像的开头有很多字节的头信息),真正开始执行的代码在中间的某个部分,相对于头有一定偏移量的,这个偏移量放在头信息中的。
一般执行一个镜像都是:第一步先读取头信息,然后在头信息的特定地址找MAGIC_NUM,由此来确定种类,第二步对镜像进行校验,第三步再次读取头信息,由头信息的特定地址知道这个镜像的各种信息,包括长度,种类,入口地址等等,第四步就是去entrypoint处开始执行镜像,

theKernel = (void (*)(int, int, uint))ep;将真正的入口地址赋值给thekernel,就是操作系统的第一句代码

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

传参并启动
给内核传参(do_bootm_linux这里的110-144行)

Starting kernel ...这里打印是uboot的最后打印信息,如果这句后,串口没输出信息了,说明内核没有成功被执行,原因是传参错误(80%),内核在DDR中的加载地址。。。

一tag方式传参
(1)struct tag,tag是一个数据结构,在uboot和linux kernel中都有定义tag数据机构,而且定义是一样的。
(2)tag_header和tag_xxx。tag_header中有这个tag的size和类型编码,kernel拿到一个tag后先分析tag_header得到tag的类型和大小,然后将tag中剩余部分当作一个tag_xxx来处理。
(3)tag_start与tag_end。kernel接收到的传参是若干个tag构成的,这些tag由tag_start起始,到tag_end结束。
(4)tag传参的方式是由linux kernel发明的,kernel定义了这种向我传参的方式,uboot只是实现了这种传参方式从而可以支持给kernel传参。

x210_sd.h中配置传参宏
(1)CONFIG_SETUP_MEMORY_TAGS,tag_mem,传参内容是内存配置信息。
(2)CONFIG_CMDLINE_TAG,tag_cmdline,传参内容是启动命令行参数,也就是uboot环境变量的bootargs.
(3)CONFIG_INITRD_TAG
(4)CONFIG_MTDPARTITION,传参内容是iNand/SD卡的分区表。
(5)起始tag是ATAG_CORE、结束tag是ATAG_NONE,其他的ATAG_XXX都是有效信息tag。
思考:内核如何拿到这些tag?
uboot最终是调用theKernel函数来执行linux内核的,uboot调用这个函数(其实就是linux内核)时传递了3个参数。这3个参数就是uboot直接传递给linux内核的3个参数,通过寄存器来实现传参的。(第1个参数就放在r0中,第二个参数放在r1中,第3个参数放在r2中)第1个参数固定为0,第2个参数是机器码,第3个参数传递的就是大片传参tag的首地址。

2.7.7.3、移植时注意事项

(1)uboot移植时一般只需要配置相应的宏即可

(2)kernel启动不成功,注意传参是否成功。传参不成功首先看uboot中bootargs设置是否正确,其次看uboot是否开启了相应宏以支持传参。

你可能感兴趣的:(linux,网络,运维)