引言:最近要改动uboot,实现像微软PC上,u盘一键刷机或手机上安全模式下刷机的操作
专门去好好研究了点uboot的启动过程;以下为总结:
嵌入式系统 微软-PC
————————————————————————
bootloader -引导.启动内核 Bios
内核 引导操作系统Windows
挂载根文件 识别系统盘(C/D盘)
应用程序 应用程序....
————————————————————————
bootloader之uboot的作用:引导、启动内核
从flash中读内核到SDRAM,在这之前,要求先先关开门口内初始化时钟和初始化SDRAM(通常称为硬件相关初始化)
uboot三个步奏:
1)从flash读出内核,
2)将其放到sdram中运行(要求可:写读flash->初始化sdram(初始化时钟、初始化串口)
3)然后启动内核
4)为开发方便,额外加入其他功能--烧写flash(比如从网络,usb或串口传进来的内核)!
分析Makefile,达到分析文件结构;分析配置过程:
1)配置:比如make Linux-menuconfig
2)编译:makez
二.读出内核并启动内核
1.了解系统的上电过程:
board_init_r --> init_sequence_r --> run_main_loop --> main_loop --> bootdelay_process-->autoboot_command 主要就是这么个流程(下图从右向左)
所以:具体怎么决定是进命令行还是启动kernel是在autoboot_command决定的!!!
正常自动启动如下:
左边为终端打印:右方tstc()即为获取键值;此处可以设置特定键值为启动内核开关!!
如果看到:
if (bootdelay >= 0)
{ //此处:键盘按Ctrl+C、enter、或者Space将abort置1,将不再启动内核
if (tstc())
{ /* we got a key press */
key = getc(); /* consume input */
puts("\b\b\b 0");
switch (key)
{
case 0x03: /* ^C - Ctrl+C */
case 0x0d: /* Enter */
case 0x20: /* Space */ //only "enter" key can triger abort
abort = 1; /* don't auto boot */
}
}
如果abortboot_single_key / abortboot_normal返回1,则autoboot_command执行完成,回到main_loop继续跑到cli_loop-->cli_simple_loop进入,并停在命令行模式。
2.上面是命令行模式,如果abortboot_single_key/abortboot_normal返回0.则abortboot返回0,那么autoboot_command里调用run_command_list,run_command_list中执行命令bootcmd启动kernel!
接下来说,上电从flash读取内核存入系统内存并启动kernel的过程:
如上说到run_command_list函数,该函数会调取board_run_command(buff);
#ifdef CONFIG_CMDLINE
rcode = cli_simple_run_command_list(buff, flag);
#else
rcode = board_run_command(buff);
#endif
在调取之前,需要读取环境变量的数据参数;根据参数去读取指定位置flash的内核!
如下,是环境参数:
--------下面涉及到一般执行一个内核镜像的四个步骤-------
第一步先读取头信息,然后在头信息的特定地址找MAGIC_NUM,由此来确定种类,第二步对镜像进行校验,第三步再次读取头信息,由头信息的特定地址知道这个镜像的各种信息,包括长度,种类,入口地址等等,第四步就是去entrypoint处开始执行镜像,
第一步:如果直接在kernel底下去make uImage会提示mkimage没找到,解决方案是在 /uboot/tool下去复制到 /usr/local/bin.下面去,复制它到系统目录下,再去执行make uImage就可以了;
do_bootm函数一直到397行之前都是进行zImage镜像的头部信息校验。校验时就要根据不同种类的image类型进行不同的校验。所以do_bootm函数的核心就是去分辨传进来的image到底是什么类型,然后按照这种类型的头信息格式去校验。校验通过则进入下一步准备启动内核;若果校验失败则认为镜像有问题,所以不能启动
do_bootm调用内核获取和校验
image_header_t结构体
(1)这个数据结构是我们uboot启动内核使用的一个标准启动数据结构,zImage头信息也是一个image_header_t,但是在实际启动之前需要进行一些改造,
if (*(ulong *)(addr + 9*4) == LINUX_ZIMAGE_MAGIC) {
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));
/* save pointer to image header */
images.legacy_hdr_os = hdr;
images.legacy_hdr_valid = 1;
这里就是在进行改造。
(2)image全局变量是在bootm函数中使用,目的是用来指向 os/initrd/fdt images,也就是用来完成启动的,zImage校验过程先确定是不是zImage,然后再修改zImage头信息,到合适,也就是上面的改造,最后再用这个头信息去初始化image,然后完成了校验。
zImage启动方式是后来添加的,而且用了goto的方式跳转了一部分代码,本身对uboot的结构上添加的。
uImage启动
(1)在uboot启动的do_boot中有一个legacy的方法,指的就是uImage这样的方式,为什么是legacy(遗留的),是因为uImage本身是uboot发明的一种启动的方式,后来这种方式是不好的,被废弃,于是被一种新的方式给替代了,新的方式就是设备树的方式,在这里被叫做fit,这个就是设备树的方式。
(2)uImage启动校验函数是在boot_get_kernel这个函数里,主要任务就是校验我们的uImage的头信息,并且得到真正的kernel的起始位置去启动。
总结:uboot本身也只支持uImage方式启动的,后来有了设备树之后,就把uImage方式命名为legacy方式,fdt方式就命名为fit方式,于是乎多了#if #endif添加代码。后来移植的人又为了省事添加了zImage的方式,又为了省事,添加了#if #endif .
第二阶段校验头信息结束!
第三阶段,启动Linux内核,调用do_bootm_linux这个函数
do_bootm_linux:找到do_bootm_linux,这个函数在lib_arm/bootm.c中() ,给内核传参-image结构体
第四步:镜像的entrypoint:
ep就是程序入口,一个镜像的起始部分不是在镜像的开头(镜像的开头有很多字节的头信息),真正开始执行的代码在中间的某个部分,相对于头有一定偏移量的,这个偏移量放在头信息中的。
一般执行一个镜像都是:第一步先读取头信息,然后在头信息的特定地址找MAGIC_NUM,由此来确定种类,第二步对镜像进行校验,第三步再次读取头信息,由头信息的特定地址知道这个镜像的各种信息,包括长度,种类,入口地址等等,第四步就是去entrypoint处开始执行镜像(此处第四步)
theKernel = (void (*)(int, int, uint))ep;将真正的入口地址赋值给thekernel,就是操作系统的第一句代码
Starting kernel ...这里打印是uboot的最后打印信息,如果这句后,串口没输出信息了,说明内核没有成功被执行,原因是传参错误(80%),内核在DDR中的加载地址。。。
如果传参正确:"Starting kernel ..."是内核启动的第0条
接下来查看给内核传参并启动内核的操作
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传参。
配置传参宏
(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的首地址。
移植时注意事项
(1)uboot移植时一般只需要配置相应的宏即可
(2)kernel启动不成功,注意传参是否成功。传参不成功首先看uboot中bootargs设置是否正确,其次看uboot是否开启了相应宏以支持传参。
附:uboot命令的实现-命令为输入的字符串+参数[比较简单,详细可以去看韦东山的uboot视频]
输入字符串---即为命令name--->存在针对于命令的结构体
结构体
{
。name
。fun =
。。。。
}
出现对应的动作--->函数fune-
run_common()的实现先比较,若name比较正确,执行对应的fun