uboot如何启动内核
7、1、uboot和内核到底是什么?
1、uboot是一个裸机程序
(1)uboot的本质就是一个复杂点的裸机程序,和我们arm裸机中写的程序没有什么本质上的区别。
(2)uboot最像我们在arm裸机中的最后写的那个shell,它其实就是一个迷你型的uboot。
2、linux内核本身也是一个"裸机程序"
(1)操作系统内核本身就是一个裸机程序,和uboot并没有本质区别。
(2)区别在于,操作系统运行起来后在软件层次上可以分为内核层和应用层,分层后两层的权限不同,内存访问和设备操作管理上更加精细(内核可以随便访问各种硬件,而应用程序只能被限制的访问硬件和内存地址)。
(3)直观来看,uboot的镜像是u-boot.bin,linux系统的镜像是zImage,这两个东西其实都是两个裸机程序镜像。从系统的启动角度来讲,内核就是一个大的复杂点的裸机程序。
3、部署在SD卡的特定分区内
(1)一个完成的软件和硬件的嵌入式系统,静止时(未上电时)bootloader、kernel、rootfs等必须的软件都以镜像的形式存储在启动介质中(x210中是iNand,或者是SD卡)。运行时都是在DDR内存中运行的,与存储介质无关。上面两个状态都是稳定状态的。第三个状态是动态过程,从静止态到运行态的过程,也就是启动过程。
(2)动态过程就是一个从SD卡逐步搬移到内存DDR中,并且运行启动代码进行相关硬件软件架构的建立,最终达到一个稳定的运行状态。
(3)静止时,u-boot.bin、zImage rootfs都在SD卡中,他们不可能随意存在SD卡的任意位置(因为我的代码要将在SD卡我们规定放这些内容的扇区,把这些内容加载到DDR中运行),因此我们需要对SD卡进行一个分区,然后将各种镜像各自存储在各自的分区中,然后在启动过程中,uboot、内核等就知道到哪里去找谁。(uboot和kernel中的分区表要一致,并且要和我们SD卡中实际使用的分区一致。)
4、运行时必须先加载到DDR链接地址处
(1)uboot第一阶段中进行重定位将uboot的第二阶段(整个uboot镜像)加载到DDR中的0x33e00000地址去,这个地址就是uboot的链接地址。
(2)内核也有类似的要求,在uboot启动内核的过程中,将内核从SD卡中读取到DDR中(其实就是重定位的过程),不能随意放在DDR中,必须放在内核的链接地址处,否则是启动不起来的。比如我们用的内核,链接地址是0x30008000.
5、内核启动的时候需要必要的启动参数
(1)uboot是无条件启动,完全从零开始启动的,内核是不能开机自动完全从零开始启动的,内核启动需要别人帮忙,uboot要帮助内核进行重定位(从SD卡重定位到DDR)。所以内核自己本身不需要考虑自己是怎么从SD卡到内存中的,内核刚开始运行的时候就是DDR中,是uboot帮内核从SD卡搬移到DDR中的,就好像uboot是富一代,内核是富二代。
(2)uboot还要给内核提供一个启动参数。
7、2、启动内核的第一步:加载内核到DDR中
(1)uboot启动内核分为两个步骤:第一步将我们的内核从启动介质中加载到DDR中。第二步去DDR中启动内核镜像
内核代码根本就没考虑重定位,因为内核知道会有uboot之类的帮忙的,把自己加载到DDR中链接地址处。所以内核直接就是从连接地址处开始运行的。
7、3、静态内核镜像在哪里?(一般是fastboot命令在uboot命令行下,将内核等镜像烧录到iNand中)
(1)SD卡/iNand/nand/norflash等:raw分区(原始分区)
常规启动时,我们的各种镜像烧录存放在iNand中,因此uboot只需要从iNand中的存放kernel分区去读取内核镜像到DDR中即可。
(2)读取的时候要使用uboot的命令来读取。(如x210iNand版本的movi命令,x210nand版本的nand命令)
movi read kernel 0x30008000 从iNand的kernel分区将内核镜像读取到DDR的0x30008000地址去(内核的链接地址)。
bootm 0x30008000 意思是去DDR的0x30008000处启动内核。
(3)其中movi read kernel 0x30008000 中的kernel指的是uboot中的kernel分区(就是uboot中规定的iNand中的一个区域范围,这区域范围被设计来存放kernel镜像,就是所谓的kernel分区)
(2)tftp、nfs等网络下载方式从远端服务器获取镜像
uboot还支持远程启动,也就内核镜像不直接烧录到开发板的启动介质中,而是放在主机的服务器中底下,然后需要启动时,我们uboot通过网络从服务器中下载镜像到开发板的DDR中。
tftp 0x30008000 zImage-qt 下载到DDR中的0X30008000 前提是主机Ubuntu和uboot ping通
然后bootm 0x30008000去DDR的这个地址去启动内核,因为内核在这个地址了
分析总结:目的是让内核镜像到DDR中,不管用什么方式,只要让内核镜像到DDR中即可。一般的情况下,我们的产品出厂时是从启动介质中启动,所以我们要将镜像烧录到iNand中,如果我们是开发的话,最好是用tftp的方式去将内核的镜像下载到DDR中运行,如果能运行的话,我们在将镜像烧录到开发板中。
7、4、镜像放到DDR中的什么地址?
(1)内核一定要放在内核的链接地址处,链接地址去内核源代码的链接脚本或者makefile中去查找。在x210中内核在DDR的链接地址是0x30008000
7、5、zImage和uImage的区别联系
1、boot命令对应do_bootm函数
(1)命令名前加do_即可构成对应这个命令的函数,因此当我们bootm这个命令执行时,直接执行的是do_bootm这个函数,在cmd_bootm.c这个文件中了。
(2)do_bootm定义了一些变量,用宏来条件编译执行了SECURE_BOOT的一些代码,就是安全启动,但是我们的这个宏没有定义。主要是进行一些签名的认证,我们可以不管
(3)然后进行了一些细节的操作,我们也不用管他,然后到了这个宏CONFIG_ZIMAGE_BOOT,用这个宏来控制进行条件编译一段代码。这段代码是用来支持zImage格式的内核启动的。
2、vmlinuz和zImage和uImage
(1)uboot 经过编译链接生成的elf格式的可执行程序u-boot。这个程序类似于Windows下的exe格式。在操作系统下是可以直接执行的,但是这种格式不能用来烧录下载。我们用来烧录下载的是u-boot.bin,这个东西是由u-boot(elf格式),经过一个工具arm-linux-objcopy,由ELF格式加工处理(去掉一些无用的东西)成可烧录下载的镜像的。这个u-boot.bin就叫镜像(image),镜像就是用来烧录到iNand中执行的。
(2)linux内核经过编译后也会生成一个ELF格式的可执行程序,叫vmlinuz或vmlinux,这个就是原始的未经任何加工处理的原版的内核elf文件,嵌入式系统部署时一般烧录的不是这个vimlinuz,而是要用objcopy工具去制作成的镜像格式(u-boot.bin这种),但是内核镜像没有这个.bin的后缀,有没有后缀没有什么区别只是名字不一样。
由vimlinuz格式的,也就是elf格式的内核可执行程序,经过objcopy工具加工成可以烧录的镜像就叫做Image(制作过程,把78M打的vimlinuz可执行程序,精简成了7.5M,因此这个工具制作烧录镜像的主要的目的就是缩减大小,节省磁盘)
(3)原则上,Image就可以直接烧录到flash上直接启动执行(类似于u-boot.bin)。但是实际上并不是这么简单的,实际上linux的作者们,觉得Image还是太大了,所以对Image进行了压缩,并且在Image压缩后的文件的前段附加了一部分解压缩的代码。构成了一个压缩格式的镜像,就叫zImage(这个压缩文件的前段附加了解压缩的代码),因为操作系统还没启动起来,是不可能让操作系统去解压缩的。而且也没有用uboot去代码实现zImage的解压缩,而是zImage本身的前面一部分有解压缩代码,叫做自解压的方法。
(4)uboot为了启动linux内核,还发明了一种内核格式叫uImage,uImage是由zImage加工得到的,uboot中有一个工具可以将zImage加工生成uImage。注意:uImage不关linux内核的事,linux内核只管生成zImage即可。uboot中的mkimage工具将zImage加工生成uImage来给uboot启动,这个加工过程就是在zImage前面加上64字节的头信息,形成了uImage。专门给从uboot启动内核镜像使用的内核镜像。
(5)但是实际上uboot也可以支持zImage的镜像,是否支持就要看x210_sd.h中是否定义了LINUX_ZIMAGE_MAGIC这个宏
(6)有的uboot支持启动zImage格式的镜像,有的uboot不支持,但是所有的uboot肯定支持uImage格式的内核镜像
3、编译内核得到uImage镜像
(1)我们在kernel的文件夹下,直接make uImage 是通不过的,会报错,因为少了这个mkimage这个工具,这个工具就是用来讲zImage的镜像加工成(在zImage的前面加了64KB的头)uImage格式的镜像的。
(2)这个mkimage工具,在我们uboot编译会得到的,是有mkimage.c和mkimage.h这两个文件生成得到的。在uboot的根目录下的/tools中。所以我们要将这个在uboot/tools/下的mkimage工具复制到我们Ubuntu默认的用户工具的目录下/usr/local/bin/下。
(3)这时我们在kernel下进行编译时就OK了,因为用到了mkimage工具,但是却没有找到,只要将这个工具复制到系统默认寻找工具的位置下就行。 在uboot/tools目录下 cp mkimage /usr/local/bin
7、4、zImage启动的细节
1、
(1)、do_bootm中一直到397行after_header_check这个符号处都是在做镜像的头部信息校验,校验时就要根据不同种类的image进行不同的校验。zImage和uImage本身都是有头信息的,uImage又在zImage的基础之上又加了头信息,在正式的文件中,我们的开始的部分都会有头信息,我们在用时,要进行头部信息的校验,头部之后的信息才是正式的内容。
(2)所以do_bootm函数的核心内容就是分辨出传进来的image是什么类型的,然后按照这种类型的头信息格式进行头检验。
如果检验通过了,则下一步准备启动内核,如果检验失败,则认为镜像是有问题的,所以不能进行启动。
(3)do_bootm函数中的第197行到223行是zImage镜像的头校验。
2、LINUX_ZIMAGE_MAGIC
(1)这是定义了一个魔数(这个数表示一种特定含义),定义的数是0x016f2818,这个数表示这个镜像是zImage,也就是说,在zImage镜像的头部的一个固定位置存放了这个数作为格式标志。
(2)如果我们拿到了一个image,去他的那个放魔数的固定位置读取四个字节的内容,看是否等于这个LINUX_ZIMAGE_MAGIC数,则可以知道这个镜像是不是一个zImage
3、do_bootm函数的参数中有int argc 和char *argv[]两个参数,当我们bootm 0x30008000时, argc = 2. argv[0] = boom argv[1] = 0x30008000这个,注意穿进来的值都是字符串格式的
(1)从do_bootm函数中可以看出来,zImage头部的第37字节开始的连续四个字节中放的是代表是什么类型镜像的魔数信息。可以用阅读软件打开zImage这个文件,来看着个zImage文件的这个位置放的是不是这个魔数来证实。很多软件可以打开二进制文件,如:winhex
4、image_header_t
(1)这个结构体就是zImage的标准头,就是这个镜像的实际地址,将这个地址转换成了这个结构体类型的指针,赋值给了同类型的结构体指针变量,也就是说zImage前面有一段头信息,这段头信息就是按照这个结构体来封装的,其中LINUX_ZIMAGE_MAGIC这个魔数就包含在这个结构体中,在实际运行启动之前,要进行一些改造,hdr->ih_os = IH_OS_LINUX;
hdr->ih_ep = ntohl(addr);这两句代码就是在进行改造
(2)images是一个全局变量,在do_bootm函数中使用,用来完成启动过程的,zImage的校验过程就是先校验是不是zImage,通过这个魔数,校验好后在将相关的信息进行改造,最后初始化到这个images全局变量里
7、5、uImage启动
1、uImage启动
(1)do_bootm函数的227行到397行是
(2)IMAGE_FORMAT_LEGACY中的legacy意思是遗留的,在do_bootm函数这种方式指的就是uImage的方式
(3)uImage本身是uboot发明用来启动linux内核镜像的格式,但是后来这种方式设备树这种新的方式替代了。在do_bootm中叫FIT,所以有CONFIG_FIT这个宏,但是这些老师不讲这种新的设备树的方式。
(4)uImage的启动校验主要在这个boot_get_kernel函数中,主要任务就校验uImage的头信息,并且得到kernel的真正的起始位置去启动。
(5)剩下的源码自己去分析。
总结:uboot开始时只是支持uImage格式的镜像启动,但是后来有了新的方式fdt设备树的方式进行启动。所以在uboot源码中,这个do_bootm函数中添加了一对#if #endif ,但是有些人又想uboot支持启动zImage镜像的内核,所以又添加了一对#if #endif
uboot启动内核的第一步是将内核镜像从我们烧录到的设备中,iNand中搬移到DDR中。第二步是进行镜像的头信息和CRC校验来判断是哪种格式的镜像。第三步就是去启动内核了。我们启动的是linux内核,所以调用的是这个do_bootm_linux函数来完成启动linux内核。
7、6、do_bootm_linux函数
1、找到do_bootm_linux函数
(1)在uboot/lib_atm/bootm.c中,
2、镜像的entrypoint
(1)ep就是entrypoint的缩写,就是程序的入口。一个镜像文件起始执行部分不是镜像的开头(镜像的开头部分是N个字节的头信息),一个镜像的文件真正执行的时候,第一句代码是在镜像文件的中部某个字节处,相当于镜像的头是有一定的偏移量的,然而这个偏移量是记录在头信息中的,
(2)一般执行一个镜像的时候,是先从镜像的头信息的特定位置中找MAGIC_NUM,由此来确定镜像的种类;第二步对镜像进行CRC校验,目的是看镜像是否是完整的。第三步再一次的读取镜像的头信息,在头信息的特定位置知道这个镜像的各种信息(镜像的长度,镜像种类,镜像的入口地址);第四步就是去entrypoint处执行镜像。所以ep中记录的是镜像的入口执行代码地址,镜像的头加上了一个偏移量。
(3)bootm.c的92行
theKernel = (void (*)(int, int, uint))ep;
//将ep这个镜像在内存中真正的代码执行的内存地址,并将这个地址强制类型转换成了函数指针类型,进行赋值,thekernel 在上面定义的,是一个函数指针类型的变量
ep将镜像在内存中的第一句真正的代码执行地址,赋值给了thekernel这个函数指针,这个函数指针就指向了在内存中真正镜像OS操作系统内核的起始代码地址。
总结:所以uboot最终是通过一个函数指针的方式,实现的推动我们内核的启动。这个函数指针执行时,带来三个参数,
theKernel (0, machid, bd->bi_boot_params); 机器码、uboot在内存中放的为内核准备的启动参数的内存的地址。
3、再次确定机器码
(1)uboot在启动内核的时候,要将机器码传递给内核,那么uboot传递给内核的机器码是怎么确定的呢?
第一顺序的选择,是从uboot中环境变量macid中获取,第二种顺序选择是从gd->bi->bi_arch_num(x210_sd.h中用一个宏配置的,赋值给了这个结构体成员)中的记录的机器码。
4、传参并启动概述
(1)110行到145行就是uboot给linux内核准备传参参数的处理。
(2)之后,uboot在最后最后打印出来的东西就是starting kernel...这句信息如果出来了,说明uboot整个是成功的,内核镜像是成功加载到内存中的,因为头信息的校验,CRC校验都通过了,说明镜像是完成的,也找到了内核镜像的真正的第一句代码的执行地址,也试图去执行了,如果这句信息后,后面就没有别的东西了,说明我们的内核并没有被成功的执行。原因:一般是传参的原因(能占百分之80的可能),内核在DDR中被加载到的地址(因为可能你将镜像下载到DDR中的地址是这个地址,你bootm时,也是bootm的这个地址,在uboot阶段一切都顺利,但是可能因为uImage的头校验信息和其他zImage开始的解压缩代码的原因,ep保存的相对于镜像头的偏移量不同,导致真正的内核镜像第一句起始代码的地址不同,所以启动不起来内核,这里只是猜测)。
7、7、传参详解
1、110行到145行就是uboot给linux内核准备传参参数的处理。
2、tag传参方式(tag是一个数据结构)
(1)struct tag,tag是一个数据结构,这个数据结构在uboot中和在linux kernel中都有定义,而且定义都是一样的。
tag的结构体类型的封装在uboot/include/asm-arm/setup.h中。
(2)struct tag_header hdr;
定义了一个tag的头tag_header,这个头中有一个成员叫做tag和size的类型编码,这个tag将来会被赋予一个值,这个值是一个魔数,来区分当前使用的是哪个tag的,linux内核读取这个tag中的魔数值和大小,来知道当前传过来的tag是哪一个,这个struct tag封装中,下面的union共用体是用的哪个类型的union,也就是把剩余的部分当做一个tag_xxx来处理。
tag_xxx是什么东西,就取决于linux读取到的tag是一个什么类型的,linux内核是按照这种方式来处理的,那么我们的uboot也就要按照这种方式来放。
(3)tag_start和tag_end,kernel接收到的参数是由若干个tag构成的。这个tag由tag_start开始到tag_end结束。所以uboot在给linux内核传参后,linux内核会首先到那个地方找tag_start,如果找到了,linux内核就知道下一个tag就是要传递的参数了,完了一直接收,知道找到了tag_end了,linux内核就知道,所传递的参数没有了。
(4)tag的传参方式,不是由uboot发明的,而是由linux kernel发明的,kernel中定义了向我传递参数时的方式,uboot只是实现了这种传参方式,从而支持给kernel进行传参。
7、8、x210_sd.h中配置传参宏
1、CONFIG_SETUP_MEMORY_TAGS对应struct tag中的tag_mem,传参传的内容是内存的配置信息。(uboot掌握了我开发板上有几片内存,没片有多大,起始地址分别是多少,但是linux内核本身是不知道这些信息的,linux内核是不知道内存本身是怎么分布的,但是uboot把这些信息维护到那个gd->bd当中的数据结构里了,所以uboot需要让linux内核知道这些事情,所以uboot就会使用tag_mem这个tag将这些信息传递给linux内核)
2、CONFIG_CMDLINE_TAG对应struct tag这个封装中的tag_cmdline,传参的内容是启动命令行参数,就是bootargs,也就是uboot的环境变量bootargs,目的是告诉linux中的命令行应该怎么启动。
3、CONFIG_INITRD_TAG跟什么磁盘相关,老师没讲。。。。
4、CONFIG_MTDPARTITION,传参内容是iNand的分区表
总结:分析代码知道,起始tag是ATAG_CORE.结束tag是ATAG_NONE,其他的tag种类,ATAG_XXX都是有效信息的tag。
思考:内核是如何拿到这些tag的?
uboot最终调用了thekernel这个函数来执行linux内核,这个函数是一个函数指针,指向的那个内存地址就是linux内核第一句代码的起始地址,这个函数在执行linux内核的时候,给linux内核传递了三个参数,这三个参数很重要,第一个参数放在了r0中,第二个参数放在了r1中,第三个参数放在了r2中。
其中第一个参数固定为0。第二个参数是机器码,第三个参数就是uboot大片传给linux内核的参数的首地址,就是uboot放在内存中的那一片参数,给linux启动的参数的首地址,linux内核知道这个参数,就可以去那个地址去处找tag了,也就是找uboot给传过来的一系列的参数。
总结:uboot在启动内核的时候,uboot是使用了一个tag传参的方式,将将来要告诉给linux内核传参的内存地址赋值给了这个tag,所以tag就指向了linux内核去那个内存找寻参数的地址,所以uboot使用tag指向的内存地址,用这种数据结构将一些要传给linux内核的参数进行打包。打包完成后,uboot就调用thekernel这函数去Linux内核的第一句代码处执行,并且传了三个参数,其中的放在r2寄存器中的参数,就是放给内核传参的内存地址。
uboot移植时的注意:在移植一般情况下,只需要改x210_sd.h中的宏配置就可以了,如果你的内核启动不起来,在uboot本身其他地方没有问题的情况下,那么就要去看,uboot在启动内核时,传递的参数对不对,看bootargs这个环境变量的值设置的对不对,这个环境变量是uboot来告诉Linux内核命令行改怎么启动的,如果参数什么的都没有问题,那就要看宏配置是否配置的正确。主要是这样。
uboot启动内核的总结:
第一步:将内核搬移到DDR中,从内核在flash部署的地方,我们的可是在iNand中,从iNand中属于内核的分区搬移DDR中((0x30008000),也可以从主机Ubuntu中的tftp服务器直接下载到DDR中,属于内核的链接地址的放,也就是内核在DDR中的属于内核的分区,这个是实现uboot和linux内核约定好的。
第二部:对内核镜像格式的校验,CRC校验,看该内核属于哪种的内核格式,是zImage,uImage,fdt的哪一种格式,在进行CRC校验,看内核镜像是否是正确的。
第三步:uboot给内核进行传参的准备,uboot用tag的方式进行给内核传参,事先uboot将要给linux内核传参的所放参数的地址中,一个tag的数据结构,将uboot要给linux内核传参的参数,打包到这个数据结构中,tag的头正好指向了这个给内核传参的内存地址,uboot也只需要在执行thekernel这个函数的时候,给linux内核传一个参数所在的地址(0x30000100)告诉linux内核就行。
第四步:uboot执行thekernel函数指针,所指向的linux kernel实际代码第一句所在的位置去运行。
涉及到的函数是do_bootm和do_bootm_linux;do_bootm函数对应在我们命令下那个bootm 0x30008000,主要对应的是前两个步骤。