uboot如何启动内核
本文接着上一阶段对于uboot的两个阶段的分析,继续分析uboot是如何启动内核的。
首先得说说内核的启动与uboot的关系:
uboot无条件的从零开始启动,但是内核的启动需要uboot的帮忙,为其提供启动参数,帮助内核实现重定位。完整的软件+硬件的嵌入式操作系统静止未上电时BootLoader、rootfs等都是以镜像的形式储存在启动介质之中的(例如nand、inand、sd卡),如静止时uboot.bin、zimage、rootfs都在SD卡中,因此需要对SD卡进行分区,再将镜像放在对应的分区之内,然后在启动时uboot就会从这个设定的分区中找到对应的镜像文件;镜像最终运行时都在DDR内存中运行,与储存介质无关;而启动过程(也就是uboot所做的)就是将SD卡中的镜像搬移至DDR内存之中,并且运行代码进行相关硬件的初始化和软件架构的建立,最终达到稳定状态的过程。
重定位:搬移sd卡内的内核镜像zimage至SDRAM中的运行的过程也叫重定位,在重定位之前uboot遵从代码的指令,从sd卡的固定分区读取内核的镜像文件,这个要求我们在烧录时指定的烧录分区和这里uboot读取的分区必须一致,不然就无法进行;其次是重定位地址,也就是内核镜像在SDRAM中运行的地址。 在uboot的第一阶段进行重定位将第二阶段(也就是整个uboot的镜像)加载至0xc3e00000地址处,这个地址就是uboot的链接地址。内核启动时也一样,将内存从SD卡读取到DDR之中,也必须放在内核的链接地址处,(譬如我们使用的内核链接地址是0x30008000)
三种镜像文件:linuxz Image zImage uImage
Image:linuxz内核直接编译生成的二进制elf文件,相当于uboot编译之后生成的elf文件uboot,但是这个镜像对于大,所以通过arm-linux-objcopy工具将其生成为可以烧录的Image文件,这个文件约为linuxz的十分之一。
zImage:对Image进行一次压缩,生成zImage镜像文件。而这个zImage文件的头部有一段未压缩的代码,这段代码实现了zImage的自解压。
uImage:zImage加工生成的:通过mkimage脚本,在zImage前面加上64字节的校验头信息。
uboot中绝对支持的是uImage,而zImage是否支持是否支持就看x210_sd.h中是否定义了LINUX_ZIMAGE_MAGIC这个宏。
说到这里就不得不说说镜像文件的执行顺序:
1.得到镜像问价的头信息;
2.校验头信息的正确性,完整性;
3.根据头信息得到镜像的一些相关参数,例如起始地址和长度等;
4.跳转至真正的镜像文件位置执行镜像程序。
这也是一种很常见,很重要的设计理念。
启动宏观分析:
第一阶段:重定位,从启动介质中加载内核镜像到SDRAM/DDR中。
第二阶段:先校验镜像文件的头信息(boot_get_kernel函数),然后启动linux内核镜像(do_bootm_linux函数)。
下面对两个阶段进行进一步具体的分许:
第一阶段的具体分析:
加载内核到SDRAM中的命令方法:
从启动介质sd卡中:movi read kernel 30008000
利用网络,远程下载镜像: tftp 0x30008000 zImage 注:这里的zimage就是tftf共享文件夹中的内核镜像。
这里重定位的方法类似于uboot的重定位,将内核从规定的位置、分区进行搬移,不做重复的分析。
第二阶段的具体分析:
启动内核的命令方法: bootm 0x30008000 注:0x30008000就是内核在SDRAM中运行的地址。
对bootm命令的分析:
bootm命令的原型是do_bootm函数,这个函数在Cmd_bootm.c中:
int do_bootm (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
。。。。。。。。
}
(1)首先判断传入参数的个数,如果为指定启动内核的位置的话,则从默认的地址读取镜像文件:
if (argc < 2) {
addr = load_addr;
debug ("* kernel: default image load address = 0x%08lx\n",
load_addr);
而这个默认的地址是ox300000000。
(2)进行镜像文件类型的校验:(以zimage为例)
通过固定位置的值与一个已知魔数相比对,来确认是否是zImage镜像:
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);
通过二进制代码比对齐将zImage转化后比对,确实相同。(注:不相同时可能是大小端的原因)
(3)获取镜像的头信息并进行比较:
/* get kernel image header, start address and length */
os_hdr = boot_get_kernel (cmdtp, flag, argc, argv,
&images, &os_data, &os_len);
返回的os_hdr指向镜像的起始地址,同样通过输出型参数传出镜像头信息的长度等信息。
(4)根据校验得到的结果,进行一些调试信息的打印:
printf (" XIP %s ... ", type_name);
printf (" Loading %s ... ", type_name);
printf (" Uncompressing %s ... ", type_name);
(5)调用do_bootm_linux()函数完成具体的内核启动工作:
do_bootm_linux (cmdtp, flag, argc, argv, &images);
索引得知do_bootm_linux()函数在bootm.c中定义。
其中传入的参数images是在前面初始化的一个全局变量,同时在前面、、读取头文件的信息之后,将镜像相关参数赋值给image。
①定义了一个很重要的参数:
ulong ep = 0;
②机器码的再次确认:
int machid = bd->bi_arch_number;
将在BL2阶段赋值的机器码再次赋值给machine。
③定义了一个十分重要的函数指针:
void (*theKernel)(int zero, int arch, uint params);
④进行内核传参的具体处理,关于此部分的详细分析请移步:
⑤执行镜像执行的第三四步:
得到镜像的起始地址:
if (images->legacy_hdr_valid) {
ep = image_get_ep (&images->legacy_hdr_os_copy);
执行镜像文件:
theKernel = (void (*)(int, int, uint))ep;
将镜像的起始地址传给函数指针theKernel。
theKernel (0, machid, bd->bi_boot_params);
最后通过调用theKernel函数指针的方式执行镜像文件。
如果uboot运行正确并成功的加载了内核的镜像会打印出调试信息:
/* we assume that the kernel is in place */
printf ("\nStarting kernel ...\n\n");
这说明镜像的头信息校验通过了,也找到了镜像的入口地址,并执行了镜像文件。
但如果这句后串口就没输出了,说明内核并没有被成功执行,原因一般是:uboot给传参不正确(80%)、内核在DDR中的加载地址不正确。