借鉴资料:http://blog.51cto.com/9291927/1792467
有道云笔记分享地址:http://note.youdao.com/noteshare?id=b63e0101293984d08ea7a015ab202893&sub=2C6EFA378FC4444DBC8D24BBFF8074EC
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设备的分区方案:
NandFlash设备的分区方案:
嵌入式系统在启动时,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还要给内核提供启动参数。
使用两种方式获取镜像
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分区)
使用bootm命令只能用来启动已经放置在DDR中的内核,打印如下,开发板看见内核已经启动成功
(2)tftp、nfs等网络下载方式从远端服务器获取镜像
uboot还支持远程启动,也就是内核镜像不烧录到开发板的SD卡中,而是放在主机的服务器中,然后需要启动时uboot通过网络从服务器中下载镜像到开发板的DDR中。
tftp 0x30008000 zImage-qt 命令, 其中zImage-qt
名字必须与ubuntu中的文件名一致,再次重申一遍,0x30008000为内核链接地址
(tftp服务器环境搭建可参考
二.网络命令ping开发搭建使用&tftp服务器的安装&nfs网络服务器的安装)
分析总结:最终结果要的是内核镜像到DDR中特定地址即可,不管内核镜像是怎么到DDR中的。以上2种方式各有优劣。产品出厂时会设置为从SD卡中启动(客户不会还要搭建tftp服务器才能用···);tftp下载远程启动这种方式一般用来开发。
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)原则上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启动。
解决方法:配置生成内核镜像文件
make distclean
* make x210ii_qt_defconfig *
错误为 没有没有nurses库,不支持图像界面出不来
需要**
参考:https://blog.csdn.net/mingtianwendy/article/details/51328562
输入命令:sudo apt-get install libncurses5-dev
即等待安装好nurses库,安装完成后会显示如下:这是配置内核的,选择默认就行(即Exit推出即可)
2.编译内核得到zImage再生成uImage镜像去启动
(1)如果直接在kernel底下去make uImage会提供mkimage command not
found。解决方案是去uboot/tools下cp mkimage
/usr/local/bin/,复制mkimage工具到系统目录下。再去make uImage即可
到这里,即将uImage镜像通过TFTP下载到了开发板,下节即分析上图中打印的这些信息。
**\#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。
(2)命令 bootm 0x30008000,所以do_boom的argc=2,argv[0]=bootm
argv[1]=0x30008000。但是实际bootm命令还可以不带参数执行。如果不带参数直接bootm,则会从CFG_LOAD_ADDR地址去执行(定义在x210_sd.h中)。
(3)zImage头部开始的第36-40字节处存放着zImage标志魔数,从这个位置取出然后对比LINUX_ZIMAGE_MAGIC。可以用二进制阅读软件来打开zImage查看,就可以证明。很多软件都可以打开二进制文件,如winhex、UltraEditor。
这里可以看出,**36到40字节就等于**LINUX_ZIMAGE_MAGIC0x016f2818。
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这个全局变量,然后就完成了校验。
1、uImage启动
(1)uImage的启动校验主要在boot_get_kernel函数中,主要任务就是校验uImage的头信息,并且得到真正的kernel的起始位置去启动。
打印出uImage镜像信息的函数如下 :image_print_contents函数
2.uImage校验头信息结束后,下一阶段主要任务是启动linux内核,调用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向内核传递参数错误导致的。
(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
(1)ep就是entrypoint的缩写,就是程序入口。一个镜像文件的起始执行部分不是在镜像的开头(镜像开头有n个字节的头信息),真正的镜像文件执行时第一句代码在镜像的中部某个字节处,相当于头是有一定的偏移量的。这个偏移量记录在头信息中。
(2)一般执行一个镜像都是:
第一步先读取头信息,然后在头信息的特定地址找MAGIC_NUM,由此来确定镜像种类;
第二步对镜像进行校验;
第三步再次读取头信息,由特定地址知道这个镜像的各种信息(镜像长
度、镜像种类、入口地址);
第四步就去entrypoint处开始执行镜像。
(3)theKernel = (void (*)(int, int,
uint))ep;将ep赋值给theKernel,则这个函数指向就指向了内存中加载的OS镜像的真正入口地址(就是操作系统的第一句执行的代码)。
3、机器码的再次确定
(1)uboot在启动内核时,机器码要传给内核。uboot传给内核的机器码是怎么确定的?第一顺序备选是环境变量machid,第二顺序备选是gd->bd->bi_arch_num(x210_sd.h中硬编码配置的)
4、传参并启动概述
(1)从110行到144行就是uboot在给linux内核准备传递的参数处理。下节详细分析传参过程。
(2)Starting kernel …
这个是uboot中最后一句打印出来的东西。这句如果能出现,说明uboot整个是成功的,也成功的加载了内核镜像,也校验通过了,也找到入口地址了,也试图去执行了。如果这句后串口就没输出了,说明内核并没有被成功执行。原因一般是:传参(80%)、内核在DDR中的加载地址·······
(1)从110行到144行就是uboot在给linux内核准备传递的参数处理
1、tag方式传参
(1)uboot使用tag方式传参,tag是一种数据结构,与linux
kernel中的tag是相同的数据结构。tag结构体包含tag_header和tag_xxxx成员,tag_header结构体包含tag的大小和类型编码,kernel接收到tag参数后根据头信息中类型编码确定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结束。
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内核的3个参数,通过寄存器来实现传参的。(第1个参数就放在r0中,第二个参数放在r1中,第3个参数放在r2中)第1个参数固定为0,第2个参数是机器码,第3个参数传递的就是大片传参tag的首地址。
4、移植时注意事项
(1)uboot移植时一般只需要配置相应的宏即可
(2)kernel启动不成功,注意传参是否成功。传参不成功首先看uboot中bootargs设置是否正确,其次看uboot是否开启了相应宏以支持传参。
1、启动4步骤
第一步:将内核搬移到DDR中
第二步:校验内核格式、CRC等
第三步:准备传参
第四步:跳转执行内核
2、涉及到的主要函数是:do_boom和do_bootm_linux
3、uboot能启动的内核格式:zImage uImage fdt方式
4、跳转与函数指针的方式运行内核