Uboot1.3.4启动内核代码剖析(do_bootm_linux)

2020-6-26星期五 北京 阴雨天  仅作为记录,方便以后用到时查看

 

uboot启动内核主要步骤

2.7.8.1、启动4步骤

第一步:将内核搬移到DDR中

第二步:校验内核格式、CRC等

第三步:准备传参

第四步:跳转执行内核

2.7.8.2、涉及到的主要函数是:do_boom和do_bootm_linux

2.7.8.3、uboot能启动的内核格式:zImage uImage fdt方式

2.7.8.4、跳转与函数指针的方式运行内核


*********************uboot启动内核基础*********************************

2.7.1.uboot和内核到底是什么

1、uboot是一个裸机程序

(1)uboot的本质就是一个复杂点的裸机程序

2、内核本身也是一个"裸机程序"

(1)操作系统内核本身就是一个裸机程序,和uboot、和其他裸机程序并没有本质区别。

(2)区别就是操作系统运行起来后在软件上分为内核层和应用层,分层后两层的权限不同,内存访问和设备操作的管理上更加精细(内核可以随便访问各种硬件,而应用程序只能被限制的访问硬件和内存地址)。

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

3、部署在SD卡中特定分区内

(1)一个完整的软件+硬件的嵌入式系统,静止时(未上电时)bootloader、kernel、rootfs等必须的软件都以镜像的形式存储在启动介质中(X210中是iNand/SD卡);运行时都是在DDR内存中运行的,与存储介质无关。上面2个状态都是稳定状态,第3个状态是动态过程,即从静止态到运行态的过程,也就是启动过程。

(2)动态启动过程就是一个从SD卡逐步搬移到DDR内存,并且运行启动代码进行相关的硬件初始化和软件架构的建立,最终达到运行时稳定状态。

(3)静止时u-boot.bin zImage rootfs都在SD卡中,他们不可能随意存在SD卡的任意位置,因此需要对SD卡进行一个分区,然后将各种镜像各自存在各自的分区中,这样在启动过程中uboot、内核等就知道到哪里去找谁。(uboot和kernel中的分区表必须一致,同时和SD卡的实际使用的分区要一致)

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

(1)uboot在第一阶段中进行重定位时将第二阶段(整个uboot镜像)加载到DDR的0xc3e00000地址处(make x21_sd_config时配置的),这个地址就是uboot的链接地址。

(2)内核也有类似要求,uboot启动内核时将内存从SD卡读取放到DDR中(其实就是个重定位的过程),不能随意放置,必须放在内核的链接地址处,否则启动不起来。譬如我们使用的内核链接地址是0x30008000(bootcmd=movi read kernel 30008000; bootm 30008000)。

5、内核启动需要必要的启动参数

(1)uboot是无条件启动的,从零开始启动的。

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

 

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

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

1、静态内核镜像在哪里?

(1)SD卡/iNand/Nand/NorFlash等:raw分区

常规启动时各种镜像都在SD卡中,因此uboot只需要从SD卡的kernel分区去读取内核镜像到DDR中即可。读取要使用uboot的命令来读取(譬如X210的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中。

在uboot下,以ubuntu的tftp作为服务器,直接下载内核调试

Uboot1.3.4启动内核代码剖析(do_bootm_linux)_第1张图片

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

2、镜像要放在DDR的什么地址?

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

 

2.7.3.3.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,这个东西是由u-boot使用arm-linux-objcopy工具进行加工(主要目的是去掉一些无用的)得到的。这个u-boot.bin就叫镜像(image),镜像就是用来烧录到iNand中执行的。

(2)linux内核经过编译后也会生成一个elf格式的可执行程序,叫vmlinux或vmlinuz,这个就是原始的未经任何处理加工的原版内核elf文件;嵌入式系统部署时烧录的一般不是这个vmlinuz/vmlinux,而是要用objcopy工具去制作成烧录镜像格式(就是u-boot.bin这种,但是内核没有.bin后缀),经过制作加工成烧录镜像的文件就叫Image(制作把78M大的精简成了7.5M,因此这个制作烧录镜像主要目的就是缩减大小,节省磁盘)。

(3)原则上Image就可以直接被烧录到Flash上进行启动执行(类似于u-boot.bin),但是实际上并不是这么简单。实际上linux的作者们觉得Image还是太大了所以对Image进行了压缩,并且在image压缩后的文件的前端附加了一部分解压缩代码。构成了一个压缩格式的镜像就叫zImage。(因为当年Image大小刚好比一张软盘(软盘有2种,1.2M的和1.44MB两种)大,为了节省1张软盘的钱于是乎设计了这种压缩Image成zImage的技术)。

(4)uboot为了启动linux内核,还发明了一种内核格式叫uImage。uImage是由zImage加工得到的,uboot中有一个工具,可以将zImage加工生成uImage。注意:uImage不关linux内核的事,linux内核只管生成zImage即可,然后uboot中的mkimage工具再去由zImage加工生成uImage来给uboot启动。这个加工过程其实就是在zImage前面加上64字节的uImage的头信息即可。

(4)原则上uboot启动时应该给他uImage格式的内核镜像,但是实际上uboot中也可以支持zImage,是否支持就看x210_sd.h中是否定义了LINUX_ZIMAGE_MAGIC这个宏。所以大家可以看出:有些uboot是支持zImage启动的,有些则不支持。但是所有的uboot肯定都支持uImage启动。

uImage又是什么的?它是uboot专用的映像文件,它是在zImage之前加上一个长度为64字节的“头”,说明这个内核的版本、加载位置、生成时间、大小等信息;其0x40之后与zImage没区别。

3、编译内核得到uImage去启动

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

**************************************************

主要代码解析

**************************************************

Uboot启动启动内核前最后一个命令bootm 30008000, 实际调用的函数为do_bootm()函数

关键数据结构

Uboot1.3.4启动内核代码剖析(do_bootm_linux)_第2张图片

Uboot1.3.4启动内核代码剖析(do_bootm_linux)_第3张图片

这两个结构体都是用来存储镜像的头信息的,image_header 仅用于 Legacy 方式启动的镜像(魔数ih_magic,大小,入口地址等),而 bootm_headers 用于 Legacy 或 设备树(FDT)方式启动的镜像(对于Legacy来说只是存放了一个头指针结构体指针和头指针结构体变量和Legacy的是否有效的一个值:legacy_hdr_valid;)。这里只分析 Legacy 方式启动的镜像,在 image_header 中需要注意这几个成员:

uint32_t    ih_magic;   /*镜像的魔数,用来给uboot判断是什么格式的镜像(zImage、uImage等)*/

uint32_t    ih_ep;      /* Entry Point Address 内核启动地址 */

Uboot1.3.4启动内核代码剖析(do_bootm_linux)_第4张图片

2.7.4.、LINUX_ZIMAGE_MAGIC

(1)这个是一个定义的魔数,这个数等于0x016f2818,表示这个镜像是一个zImage。也就是说zImage格式的镜像中在头部的一个固定位置存放了这个数作为格式标记。如果我们拿到了一个image,去他的那个位置去取4字节判断它是否等于LINUX_ZIMAGE_MAGIC,则可以知道这个镜像是不是一个zImage。

(2)命令 bootm 0x30008000,所以do_boom的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查看,就可以证明。

2.7.4.2、image_header_t

(1)这个数据结构是我们uboot启动内核使用的一个标准启动数据结构,zImage头信息也是一个image_header_t,但是在实际启动之前需要进行一些改造。hdr->ih_os = IH_OS_LINUX;

          hdr->ih_ep = ntohl(addr);   //赋值,将执行命令bootm 0x30008000后的第二个参数,也就是uImage或zImage内核的存放地址,传给对应内核(uImage或zImage)image_header_t结构体的指针hdr指向的元素ih_ep。这个元素存放的就是内核(uImage或zImage)启动地址即入口地址:镜像执行时的第一句代码的地址,

 

(2)images全局变量是do_bootm函数中使用,用来完成启动过程的。zImage的校验过程其实就是先确认是不是zImage,确认后再修改zImage的头信息到合适,修改后用头信息去初始化images这个全局变量,然后就完成了校验。

Uboot1.3.4启动内核代码剖析(do_bootm_linux)_第5张图片

2.7.5.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启动的方式,又为了省事把zImage启动方式直接写在了uImage和fdt启动方式之前,于是乎又有了一对#if  #endif。于是乎整天的代码看起来很恶心。

总结2:第二阶段校验头信息结束,下面进入第三阶段,第三阶段主要任务是启动linux内核,调用do_bootm_linux函数来完成

Uboot1.3.4启动内核代码剖析(do_bootm_linux)_第6张图片

Uboot包含各种系统的启动了,我们走的是linux,也就是调用do_bootm_linux()

Uboot1.3.4启动内核代码剖析(do_bootm_linux)_第7张图片

这里定义的几个变量,ep 是镜像入口,最后会赋值给 theKernel ,theKernel 所在的地址就是内核启动的第一句代码,还会接受 uboot 给他传递的几个参数。

即ep = image_get_ep (hdr);将内核头信息结构体image_header_t的指针hdr传入函数 image_get_ep()中,这个函数的作用是用头信息结构体image_header_t的指针hdr,取出结构体中的元素ih_ep的值赋值给ep变量。因为这个元素的值就是对应内核入口地址,函数名 image_get_ep (hdr)中的ep是可变的,也就是说函数名是可变的,一旦函数名确定,譬如说image_get_xxx (hdr),则这个函数的意思就是从指针hdr对应的结构体取出一个元素,这个元素的特点是ih_xxx,这个元素的前面三个字符ih_是固定的,后面的字符xxx是根据变量名的定义时的改变而改变。

uboot/include/image.h的第290~300行, uimage_to_cpu 函数后发现就是 ntohl 函数,效果就是将 hdr->ih_ep 进行关于大小端的转换后返回这个值。

(1)ep就是entrypoint的缩写,就是程序入口。一个镜像文件的起始执行部分不是在镜像的开头(镜像开头有n个字节的头信息),真正的镜像文件执行时第一句代码在镜像的中部某个字节处,相当于头是有一定的偏移量的。这个偏移量记录在头信息中。

(2)一般执行一个镜像都是:第一步先读取头信息,然后在头信息的特定地址找MAGIC_NUM,由此来确定镜像种类;第二步对镜像进行校验;第三步再次读取头信息,由特定地址知道这个镜像的各种信息(镜像长度、镜像种类、入口地址);第四步就去entrypoint处开始执行镜像。

(3)theKernel = (void (*)(int, int, uint))ep;将ep赋值给theKernel,则这个函数指向就指向了内存中加载的OS镜像的真正入口地址(就是操作系统的第一句执行的代码)。

机器码的再次确定

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

uboot在给linux内核准备传递的参数处理。其中包含各种参数的传递,内存信息,串口信息等如下共用体中结构体。

Uboot1.3.4启动内核代码剖析(do_bootm_linux)_第8张图片

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传参。

这个 tag 数据结构中定义了两个成员,一个是 tag_header 结构体,其中的 tag 成员用来表示有效信息(比如 tag为 ATAG_CORE 就是起始,ATAG_NONE 就是结束,其他的 ATAG_XX 就是表示下面的联合体中具体是哪一个结构体)。还有就是这个 tag 结构体没有定义一个具体的变量,在操作时是事先定义的 tag* 类型的指针 params 来操作的

这一段代码中的 setup_xxx_tag 函数实现方式、作用都非常类似,都是首先给 hdr 赋值 tag参数的类型名称和大小,随后给联合体中写入之前存放板子参数信息的bd变量中的值。

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。

我以内存传参为例:

Uboot1.3.4启动内核代码剖析(do_bootm_linux)_第9张图片

我们配置的关于内存相关的信息都是放在x210_sd.h中(编译时确定了内存条数量,起始地址及大小),保存在bd->bi_dram[]数组中。

放入tag* 类型的指针 params所指向的结构体中, 不同类型的参数信息以结构体大小为单位依次顺序排列。这样向内核传参时,只需要传递首地址即可传递各种类型的参数。

然后向Starting kernel ...  这个是uboot中最后一句打印出来的东西。这句如果能出现,说明uboot整个是成功的,也成功的加载了内核镜像,也校验通过了,也找到入口地址了,也试图去执行了。如果这句后串口就没输出了,说明内核并没有被成功执行。原因一般是:传参

清cache

//使用函数指针调用函数theKernel,启动内核函数了,这个函数指针指向的函数并不是我们平时所说的函数名,有函数名的函数是有函数体的函数,譬如int add(int a, int b),它对应的函数指针是int(*p)(int, int),

给指针p 赋值:p = add,调用时使用p(3, 5)即可执行函数add(3, 5)的效果。但是这里的内核函数theKernel的指针指向的是一个内存的地址,即内核的入口地址,并没有所谓的函数体,但是入口地址处以后的代码就可以认为就是内核函数指针theKernel指向的函数体,函数指针调用传参时不是按照定义函数时的参数:int a, int b传入的,因为theKernel没有函数名和函数体,所以也就没有办法定义一个内核函数指针时参考函数名后的参数类型及参数个数,于是内核就自己根据启动内核的需要:机器码machid,机器信息,这里是以tag方式存储的,所以要将tag结构体的首地址当在内核参数传入,类型吗?就是 两个int 和一个uint 类型,于是定义指针时就定义成:void    (*theKernel)(int zero, int arch, uint params);然后给指针赋值: theKernel = (void (*)(intint, uint))ep;然后就使用函数指针的方式调用函数:

跳转到 theKernel,附带三个参数,第一个是0,第二个是机器码,第三个是一系列 tag 结构体的首地址(即params = (struct tag *) bd->bi_boot_params;params->hdr.tag = ATAG_CORE 的结构体的地址(第一个结构体首地址))。在这里跳转到 theKernel 后就正式进入到内核了,所以这里的注释写的 does not return ,不会返回了。

uboot最终是调用theKernel函数来执行linux内核的,uboot调用这个函数(其实就是linux内核)时传递了3个参数。这3个参数就是uboot直接传递给linux内核的3个参数,通过寄存器来实现传参的。(第1个参数就放在r0中,第二个参数放在r1中,第3个参数放在r2中)第1个参数固定为0,第2个参数是机器码,第3个参数传递的就是大片传参tag的首地址。

移植时注意事项

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

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

 

 

 

 

 

 

 

你可能感兴趣的:(uboot和系统移植)