uboot如何启动内核

2019-07-13 08:42发布

一、uboot和内核到底是什么

1.uboot和内核就是一个裸机程序

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

2.部署在SD卡特定分区内

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

3.运行时必须先加载到DDR中链接地址处

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

4.内核启动需要必要的参数

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

二、启动内核第一步:加载内核到DDR中

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

1.静态内核镜像在哪里

(1)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分区)。 (2)tftp、nfs等网络下载方式从远端服务器获取镜像 uboot还支持远程启动,也就是内核镜像不烧录到开发板的SD卡中,而是放在主机的服务器中,然后需要启动时,uboot通过网络从服务器中下载镜像到开发板的DDR中。 分析总结:最终结果要的是内核镜像到DDR中特定地址即可,不管内核镜像是怎么到DDR中的。以上两种方式各有优劣。产品出厂时会设置为从SD卡启动(客户不会还要搭建tftp服务器才能用......);tftp下载远程启动这种方式一般用来开发。

2.镜像要放到DDR的什么地址

(1)内核一定要放在链接地址处,链接地址去内核源代码的链接脚本或Makefile中去查找。x210中是0x30008000。

三、zImage和uImage的区别

1.bootm命令对应do_bootm函数

(1)命令名前加do_即可构成这个命令对应的函数,因此当执行bootm命令时,uboot实际执行的函数是do_bootm函数,该函数为cmd_bootm.c文件中。 (2)do_bootm刚开始定义了一些变量,然后使用宏来条件编译执行了secureboot的一些代码(主要进行签名认证),然后又进行了一些细节操作,然后又到了CONFIG_ZIMAGE_BOOT,用这个宏来控制条件编译一些代码,这段代码是用来支持zImage格式的内核启动的。

2.vmlinuz和zImage、uImage

(1)uboot经过编译直接生成的elf格式的可执行程序是u-boot,这个程序类似于windows下的exe格式,在操作系统下是可以直接执行的,但是这种格式不能用来烧录下载,用来烧录下载的是u-boot.bin,这个文件是由arm-linux-objcopy工具进行加工得到的(主要是去掉一些无用的信息)。这个u-boot.bin就叫镜像(Image),镜像是用来烧录到iNand中执行的。 (2)linux内核经过编译后也会生成一个elf格式的可执行程序,叫vmlinux或vmlinuz,这个就是原始的未经任何处理加工的原版内核elf文件。嵌入式系统部署时烧录的一般不是这个vmlinuz/vmlinux,而是要用objcopy工具去制作成烧录镜像格式(就是u-boo.bin这种,但是内核没有.bin后缀),经过制作加工成烧录镜像的文件就叫做Image(制作把78M大的精简成了7.5M,因此这个制作烧录镜像的主要目的就是缩减大小、节省磁盘)。 (3)原则上Image就可以直接烧录到Flash上进行启动执行(类似于u-boot.bin),但是实际上并不是这么简单,实际上linux的作者们觉得Image还是太大了,所以对Image进行了压缩,并且在Image压缩后的文件的前端附加了一部分解压缩代码,构成了一个压缩格式的镜像就叫zImage。(因为当前Image大小刚好比一张软盘大(软盘有两种:1.2MB和1.44MB),为了节省一张软盘的前,于是就设计了这种压缩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_ZIMAGE_MAGIC这个宏。所以大家可以看出,有些uboot是支持zImage的,有些则不支持。但是所有的uboot肯定都支持uImage启动。

3.编译内核得到uImage启动

(1)如果直接在kernel下去make uImage会出现mkimage command not。解决方案是去uboot/tools下cp mkimage /usr/local/bin,复制mkimage工具到系统目录下,再去make uImage。

四、zImage启动细节

do_bootm函数从起始一直到397行的after_header_check这个符号处,都是在进行镜像的头部信息校验。校验时就要根据不同类型的Image进行不同的校验,所以do_bootm函数的核心是去分辨传进来的Image到底是什么类型,然后按照这种类型的头信息格式去校验,校验通过则进入下一步准备启动内核,如果校验失败,则表示镜像有问题,不能启动。在这里我们只关注zImage相关的内容: 代码:196 ~ 225行 #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 ", load_addr); } else { addr = simple_strtoul(argv[1], NULL, 16); debug ("* kernel: cmdline image address = 0x%08lx ", img_addr); } if (*(ulong *)(addr + 9*4) == LINUX_ZIMAGE_MAGIC) { printf("Boot with zImage "); 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

1.LINUX_ZIMAGE_MAGIC

(1)这个是定义的一个魔数,这个数等于0x016F2818,表示这个镜像是一个zImage。也就是说zImage格式的镜像中,在头部的一个固定位置存放了这个数作为格式标记。如果拿到了一个Image,去那个位置读取4个字节判断它是否等于LINUX_ZIMAGE_MAGIC,则可以知道这个镜像是不是一个zImage。 (2)命令bootm 30008000,所以do_bootm的argc=2、argv[0]=boom、argv[1]=0x30008000,但是实际bootm命令还可以不带参数执行。如果不带参数直接bootm,则会从CFG_LOAD_ADDR地址去执行(定义位于x210_sd.h中)。 (3)zImage头部开始的第37~40字节处存放着zImage标志魔数,从这个位置取出然后对比LINUX_ZIMAGE_MAGIC。

2.image_header_t

(1)这个数据结构是我们uboot启动内核使用的一个标准启动数据结构,zImage头信息也是一个image_header_t,但是在实际启动之前需要进行一些改造。下面这两句就是在进行改造:         hdr->ih_os = IH_OS_LINUX;
        hdr->ih_ep = ntohl(addr); (2)images全局变量是do_bootm函数中使用的,用来完成启动过程的。zImage的校验过程其实就是先确认是不是zImage,确认后再修改zImage的头信息到合适,修改后用头信息去初始化images这个全局变量,然后就完成了校验。

五、uImage启动

1.uImage启动

(1)LEGACY(遗留的),在do_bootm函数中,这种方式指的就是uImage的方式。 (2)uImage方式是uboot本身发明的支持linux启动的镜像格式,但是后来这种方式被一种新的方式替代,这个新的方式就是设备树方式(在do_bootm中叫FIT)。 (3)uImage的启动校验主要在boot_get_kernel函数中,主要任务就是校验uImage的头信息,并且得到真正的kernel的起始位置去启动。

2.设备树方式启动

(1)设备树方式在这里暂时不讲。 总结1:uboot本身设计时只支持uImage启动,后来的uboot代码也是这样写的。后来有了fdt方式之后,就把uImage方式命名为LEGACY方式,fdt方式命名为FIT方式,于是又多了#if_#endif添加的代码。后来移植时为了省事,添加了zImage启动方式,又为了省事,直接写在了uImage和fdt启动方式之前,于是又多了一对#if_#endif。 总结2:第二阶段校验头信息结束,下面进入第三阶段,第三阶段的主要任务是启动linux内核,调用do_bootm_linux函数来完成。

六、do_bootm_linux函数

do_bootm_linux函数的定义位于lib_arm/bootm.c文件中。 代码:61 ~ 165行(lib_arm/bootm.c) void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[], bootm_headers_t *images) { ulong initrd_start, initrd_end; ulong ep = 0; bd_t *bd = gd->bd; char *s; int machid = bd->bi_arch_number; void (*theKernel)(int zero, int arch, uint params); int ret; #ifdef CONFIG_CMDLINE_TAG char *commandline = getenv ("bootargs"); #endif /* find kernel entry point */ if (images->legacy_hdr_valid) { ep = image_get_ep (&images->legacy_hdr_os_copy); #if defined(CONFIG_FIT) } else if (images->fit_uname_os) { ret = fit_image_get_entry (images->fit_hdr_os, images->fit_noffset_os, &ep); if (ret) { puts ("Can't get entry point property! "); goto error; } #endif } else { puts ("Could not find kernel entry point! "); goto error; } theKernel = (void (*)(int, int, uint))ep; s = getenv ("machid"); if (s) { machid = simple_strtoul (s, NULL, 16); printf ("Using machid 0x%x from environment ", machid); } ret = boot_get_ramdisk (argc, argv, images, IH_ARCH_ARM, &initrd_start, &initrd_end); if (ret) goto error; show_boot_progress (15); debug ("## Transferring control to Linux (at address %08lx) ... ", (ulong) theKernel); #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 /* we assume that the kernel is in place */ printf (" Starting kernel ... "); #ifdef CONFIG_USB_DEVICE { extern void udc_disconnect (void); udc_disconnect (); } #endif cleanup_before_linux (); theKernel (0, machid, bd->bi_boot_params); /* does not return */ return; error: do_reset (cmdtp, flag, argc, argv); return; }

1.镜像的entrypoint

(1)ep就是entrypoint的缩写,就是程序入口。一个镜像文件的起始执行部分不是在镜像的开头(镜像开头有n个字节的头信息),真正的镜像文件执行时,第一句代码在镜像的中部某个字节处,相当于头是有一定的偏移量的。这个偏移量记录在头信息中。 (2)一般执行一个镜像文件的过程是:第一步,先读取头信息,然后在头信息的特定地址找MAGIC_NUM,由此来确定镜像种类;第二步,对镜像进行校验;第三步,再次读取头信息,由头信息的特定地址知道这个镜像的各种信息(镜像长度、镜像种类、入口地址);第四步,去entrypoint处执行镜像。 (3)theKernel = (void (*)(int, int, uint))ep;  ——  将ep赋值给theKernel,则这个函数指针就指向了内存中加载的OS镜像的真正入口地址(就是操作系统的第一句执行的代码)。

2.机器码的再次确定

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

3.传参并启动操作

(1)从110行到144行就是uboot在给linux内核准备传递的参数处理。 (2)Starting kernel...:这个是uboot中最后一句打印出来的东西。这句如果能出现,说明uboot整个是成功的,成功地加载了内核镜像,成功通过了校验,找到了入口地址,也试图去执行了。如果在这句后串口没有输出了,说明内核并没有被成功执行,原因一般是传参(80%)、内核在DDR中的地址出错......

七、传参详解

1.tag方式传参

(1)struct tag:tag是一个数据结构,在uboot和linux kernel中都有tag数据结构的定义,而且定义都是一样的。 (2)tag_header和tag_xxx:tag_header中有这个tag的size和类型编码,kernel拿到了一个tag后,先分析tag的类型和大小,然后将tag中剩余部分当做一个tag_xxx来处理。 (3)tag_start和tag_end:kernel接收到的传参时若干个tag构成的,这些tag由tag_start起始,到tar_end结束。 (4)tag的传参方式是由linux kernel发明的,kernel定义了这种传参的方式,uboot只是实现了这种传参方式,从而可以支持给kernel传参。 tag定义: struct tag_header { u32 size; u32 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; /* * Acorn specific */ struct tag_acorn acorn; /* * DC21285 specific */ struct tag_memclk memclk; struct tag_mtdpart mtdpart_info; } u; };

2.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内核的,通过寄存器来实现传参(第1个参数放在r0中,第2个参数放在r1中,第3个参数放在r2中)。第1个参数固定为0,第2个参数是机器码,第3个参数就是tag的首地址。

3.移植时注意项

(1)uboot移植时一般只需要配置相应宏即可。 (2)kernel启动不成功,注意传参是否成功。传参不成功,首先看uboot中bootargs设置是否正确,其次看uboot是否开启了相应宏以支持传参。

八、uboot启动内核总结

(1)启动步骤 第一步:将内核搬移到DDR中 第二步:校验内核格式、CRC等 第三步:准备传参 第四步:跳转执行内核 (2)涉及到的主要函数是do_bootm和do_boom_linux (3)uboot能启动的内核格式:zImage、uImage、fdt (4)跳转与函数指针的方式运行内核