九.linux开发之uboot移植(九)——uboot源码分析3-uboot启动内核机制

版权声明:本文为博主原创文章,允许转载请注明。谢谢! https://blog.csdn.net/wangweijundeqq/article/details/81435399

借鉴资料:http://blog.51cto.com/9291927/1792467

有道云笔记分享地址:http://note.youdao.com/noteshare?id=b63e0101293984d08ea7a015ab202893&sub=2C6EFA378FC4444DBC8D24BBFF8074EC

一.uboot和内核到底是什么

1、uboot是一个裸机程序

(1)uboot的本质就是一个复杂点的裸机程序。和我们在ARM裸机全集中学习的每一个裸机程序并没有本质区别。

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

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

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

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

3、嵌入式系统的分区

**嵌入式系统部署在Flash设备上时,对于不同SoC和Flash设备,bootloader、kernel、rootfs的分区是不同的。三星S5PV210规定启动设备的分区方案如下:**

SD/MMC设备的分区方案:

A_UY7gwCc236.png

NandFlash设备的分区方案:

A5S8md_34444.png

嵌入式系统在启动时,uboot、kernel、rootfs不能随意存放,必须存放在规划好的相应分区,在启动过程中uboot、kernel会到相应分区加载相应内容,确保正常启动,因此嵌入式系统中,uboot和kernel规划的分区和启动设备中uoot、kernel、rootfs的实际存储分区是一致的。

(1)在嵌入式系统的启动过程中,开机时uboot运行在SoC内部的SRAM中,*uboot会在BL1阶段将整个uboot拷贝到SDRAM中0xC3E00000并远跳转到SDRAM中的BL2运行。Uboot启动kernel时,同样会将kernel从启动设备拷贝到SDRAM中的指定kernel链接位置,最终跳转到kernel运行。*

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

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

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

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

二.uboot之使用两种方式获取镜像启动内核

使用两种方式获取镜像

1.SD卡/iNand/Nand/NorFlash等:raw分区方式

常规启动时各种镜像都在SD卡中,因此uboot只需要从SD卡的kernel分区去读取内核镜像到DDR中即可。读取要使用uboot的命令来读取(譬如X210的iNand版本是movi命令,X210的Nand版本就是Nand命令)

(2)这种启动方式来加载ddr,使用命令:movi read kernel
30008000。0x30008000为内核链接地址,其中kernel指的是uboot中的kernel分区(就是uboot中规定的SD卡中的一个区域范围,这个区域范围被设计来存放kernel镜像,就是所谓的kernel分区)

clipboard.png

clipboard.png

使用bootm命令只能用来启动已经放置在DDR中的内核,打印如下,开发板看见内核已经启动成功

clipboard.png

(2)tftp、nfs等网络下载方式从远端服务器获取镜像

uboot还支持远程启动,也就是内核镜像不烧录到开发板的SD卡中,而是放在主机的服务器中,然后需要启动时uboot通过网络从服务器中下载镜像到开发板的DDR中。

tftp 0x30008000 zImage-qt 命令, 其中zImage-qt
名字必须与ubuntu中的文件名一致,再次重申一遍,0x30008000为内核链接地址

(tftp服务器环境搭建可参考
二.网络命令ping开发搭建使用&tftp服务器的安装&nfs网络服务器的安装)

clipboard.png

clipboard.png

clipboard.png

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

三.uboot之编译内核生成zImage和uImage

1、bootm命令对应do_bootm函数

clipboard.png

(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)原则上Image就可以直接被烧录到Flash上进行启动执行(类似于u-boot.bin),但是实际上并不是这么简单。实际上linux的作者们觉得Image还是太大了所以对Image进行了压缩,并且在image压缩后的文件的前端附加了一部分解压缩代码。构成了一个压缩格式的镜像就叫zImage。

(3)uboot为了启动linux内核,还发明了一种内核格式叫uImage。uImage是由zImage加工得到的,uboot中有一个工具,可以将zImage加工生成uImage。

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

clipboard.png

1.出现的问题,没有配置编译生成内核镜像,无法实验编译内核得到uImage

clipboard.png

解决方法:配置生成内核镜像文件

  • make distclean

  • * make x210ii_qt_defconfig *

Y__}}%UA{\$(K\$`ZTGIH

  • make menuconfig 显示一下错误

9Q9282[{OAA)70[E6\$(JQ(O.png

错误为 没有没有nurses库,不支持图像界面出不来

需要**

ubuntu安装nurses库**

参考:https://blog.csdn.net/mingtianwendy/article/details/51328562

2ZK1[67QT%V{XGWM3ZR4\@AQ.png

输入命令:sudo apt-get install libncurses5-dev

即等待安装好nurses库,安装完成后会显示如下:这是配置内核的,选择默认就行(即Exit推出即可)

[5E`VR]QJ3XQ\~\$WJ(T_}[A1.jpg

  • make uImage命令

clipboard.png

2.编译内核得到zImage再生成uImage镜像去启动

  • 如图,上面的操作已经生成了zImage

clipboard.png

  • 再由zImage生成uImage

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

clipboard.png

clipboard.png

  • 最后在串口控制台上分别输入一下命令,使用TFTP下载uImage到开发板

clipboard.png

clipboard.png

到这里,即将uImage镜像通过TFTP下载到了开发板,下节即分析上图中打印的这些信息。

四.uboot之zImage启动源码细节分析

clipboard.png

**\#define LINUX_ZIMAGE_MAGIC0x016f2818**

**if (argc \< 2) {**

**addr = load_addr;**//如果不指定内核地址,则使用CFG_LOAD_ADDR

**} else {**

**addr = simple_strtoul(argv[1], NULL, 16);**//使用指定内核地址

**}**

**if (\*(ulong \*)(addr + 9\*4) == LINUX_ZIMAGE_MAGIC)
{**//如果当前kernel是zImage

**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));**

//将内核的信息赋值给当前uboot启动内核的images变量

**images.legacy_hdr_os = hdr;**

**images.legacy_hdr_valid = 1;**

**goto after_header_check;**

**}**

**通过读取zImage的头部的第36字节开始的四个字节与LINUX_ZIMAGE_MAGIC(0x016f2818)标志位对比,如果相等则当前的kernel是zImage。如果当前kernel是zImage,则打印出”Boot
with
zImage”,并对当前uboot启动内核镜像的变量images进行赋值,指定内核版本和内核入口地址。**

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

1、LINUX_ZIMAGE_MAGIC

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

clipboard.png

(2)命令 bootm 0x30008000,所以do_boom的argc=2,argv[0]=bootm
argv[1]=0x30008000。但是实际bootm命令还可以不带参数执行。如果不带参数直接bootm,则会从CFG_LOAD_ADDR地址去执行(定义在x210_sd.h中)。

clipboard.png

clipboard.png

(3)zImage头部开始的第36-40字节处存放着zImage标志魔数,从这个位置取出然后对比LINUX_ZIMAGE_MAGIC。可以用二进制阅读软件来打开zImage查看,就可以证明。很多软件都可以打开二进制文件,如winhex、UltraEditor。

这里可以看出,**36到40字节就等于**LINUX_ZIMAGE_MAGIC0x016f2818。

clipboard.png

2、image_header_t

clipboard.png

(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这个全局变量,然后就完成了校验。

clipboard.png

五.uboot之uImage镜像启动细节

1、uImage启动

(1)uImage的启动校验主要在boot_get_kernel函数中,主要任务就是校验uImage的头信息,并且得到真正的kernel的起始位置去启动。

clipboard.png

打印出uImage镜像信息的函数如下 :image_print_contents函数

clipboard.png

clipboard.png

2.uImage校验头信息结束后,下一阶段主要任务是启动linux内核,调用do_bootm_linux函数来完成。

clipboard.png

六.uboot之uImage启动之do_bootm_linux函数流程分析

找到do_bootm_linux函数(do_bootm_linux启动内核)

函数的主要功能是获取环境变量中的内核传递参数,获取当前uboot启动kernel的images变量中的kernel入口地址,获取uboot中的机器码,准备向kernel传递的参数,最后跳转到kernel执行,uboot执行完毕。uboot在执行完成前打印了”Starting
kernel …”信息。如果uboot实际启动kernel过程中打印出了”Starting kernel
…”信息,则表明uboot在加载、校验kernel是正确的。如果uboot最终启动kenel失败,则大部分原因是uboot向内核传递参数错误导致的。

clipboard.png

(1)函数在uboot/lib_arm/bootm.c中。

void do_bootm_linux (cmd_tbl_t \*cmdtp, int flag, int argc, char \*argv[],

     bootm_headers_t \*images)

{

ulonginitrd_start, initrd_end;

ulongep = 0;

bd_t\*bd = gd-\>bd;

char\*s;

intmachid = bd-\>bi_arch_number;**//uboot的机器码**

void(\*theKernel)(int zero, int arch, uint params);

intret;

\#ifdef CONFIG_CMDLINE_TAG

char \*commandline = getenv ("bootargs");

**//获取环境变量中的内核传递参数变量**

\#endif

if (images-\>legacy_hdr_valid) {

ep = image_get_ep
(\&images-\>legacy_hdr_os_copy);**//获取images变量中的内核入口地址**

} else {

puts ("Could not find kernel entry point!\\n");

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\\n", machid);

}

ret = boot_get_ramdisk (argc, argv, images, IH_ARCH_ARM,

&initrd_start, \&initrd_end);

if (ret)

goto error;

show_boot_progress (15);

**//uboot准备给kernel传递参数**

\#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

printf ("\\nStarting kernel ...\\n\\n");**//打印信息Starting kernel ...**

\#ifdef CONFIG_USB_DEVICE

{

extern void udc_disconnect (void);

udc_disconnect ();

}

\#endif

cleanup_before_linux ();

theKernel (0, machid, bd-\>bi_boot_params);**//跳转到kernel执行**

return;

error:

do_reset (cmdtp, flag, argc, argv);

return;

}

2、镜像的entrypoint

clipboard.png

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

(2)一般执行一个镜像都是:

  • 第一步先读取头信息,然后在头信息的特定地址找MAGIC_NUM,由此来确定镜像种类;

  • 第二步对镜像进行校验;

  • 第三步再次读取头信息,由特定地址知道这个镜像的各种信息(镜像长
    度、镜像种类、入口地址);

  • 第四步就去entrypoint处开始执行镜像。

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

clipboard.png

3、机器码的再次确定

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

clipboard.png

4、传参并启动概述

(1)从110行到144行就是uboot在给linux内核准备传递的参数处理。下节详细分析传参过程。

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

clipboard.png

clipboard.png

clipboard.png

七.uboot之uImage启动之do_bootm_linux函数传参详解

(1)从110行到144行就是uboot在给linux内核准备传递的参数处理

clipboard.png

1、tag方式传参

clipboard.png

(1)uboot使用tag方式传参,tag是一种数据结构,与linux
kernel中的tag是相同的数据结构。tag结构体包含tag_header和tag_xxxx成员,tag_header结构体包含tag的大小和类型编码,kernel接收到tag参数后根据头信息中类型编码确定tag的类型。

clipboard.png

(2)tag_header和tag_xxx。tag_header中有这个tag的size和类型编码,kernel拿到一个tag后先分析tag_header得到tag的类型和大小,然后将tag中剩余部分当作一个tag_xxx来处理。

clipboard.png

(3)tag_start与tag_end。kernel接收到的传参是若干个tag构成的,这些tag由tag_start起始,到tag_end结束。

2、x210_sd.h中配置传参宏

clipboard.png

clipboard.png

clipboard.png

(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的首地址。

clipboard.png

4、移植时注意事项

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

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

八.uboot启动内核的总结

1、启动4步骤

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

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

第三步:准备传参

第四步:跳转执行内核

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

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

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

猜你喜欢

转载自blog.csdn.net/wangweijundeqq/article/details/81435399