uboot 启动内核代码分析
0、uboot和内核区别
uboot的本质就是一个复杂点的裸机程序。内核本身也是一个"裸机程序“,和uboot、和其他裸机程序并没有本质区别。
区别就是操作系统运行起来后在软件上分为内核层和应用层,分层后两层的权限不同,在内存访问和设备操作的管理上更加精细(内核可以随便访问各种硬件,而应用程序只能被限制的访问硬件和内存地址)。
直观来看:uboot的镜像是u-boot.bin,linux系统的镜像是zImage,这两个东西其实都是两个裸机程序镜像。从系统的启动角度来讲,内核其实就是一个大的复杂点裸机程序。
1、嵌入式系统部署在SD卡中特定分区内
(1)一个完整的嵌入式系统,静止时(未上电时)bootloader、kernel、rootfs等软件都以镜像的形式存储在启动介质中(X210中是iNand/SD卡);运行时都是在DDR内存中运行的,与存储介质无关。从静止态到运行态的过程,就是uboot启动内核的过程。
(2)启动过程就是一个将内核镜像从SD卡搬移到DDR内存去运行,最终达到稳定状态的过程。
(3)静止时u-boot.bin zImage rootfs都在SD卡中,他们不可以在SD卡中胡乱存放,而是存放在各自的分区中,这样在启动过程中uboot、内核等就知道到哪里去找。在uboot和kernel中都设置了一张分区表,这张分区表必须保持一致,并且要和实际烧写在SD卡中的位置一致。
譬如uboot的BL1从SD卡49扇区开始重定位整个uboot,那么事先就应该将uboot.bin烧写在49扇区。
2、内核运行必须加载到DDR中的链接地址处
(1)uboot在第一阶段中进行重定位时将第二阶段(整个uboot镜像)加载到DDR的0xc3e00000地址处,这个地址就是uboot的链接地址。(0xc3e00000是个虚拟地址,真实的物理地址是0x33e00000)
(2)内核也有类似要求,uboot启动内核时将内核从SD卡读取放到DDR中(其实就是个重定位的过程),不能随意放置,必须放在内核的链接地址处才能运行,否则内核启动不起来。我们使用的内核链接地址是0x30008000。
3、内核启动需要uboot传递必要的启动参数
(1)uboot是无条件自启动的。
(2)内核是不能开机自动启动的,uboot不仅要重定位内核到DDR内存,还要给内核提供必要启动参数才能运行(譬如uboot对内存的配置信息(维护在gd->bd的数据结构中)以及 uboot的环境变量bootargs 就是将来要传递给内核去使用的)。
4、启动内核第一步:加载内核到DDR中
(1)uboot要启动内核,首先就是要将内核加载到DDR中。uboot实现了重定位自己,但内核没有,内核代码从没考虑重定位自己,因为内核知道会有uboot之类的bootloader将自己加载到DDR中的链接地址处,所以内核直接就是从链接地址处开始运行的。
5、内核镜像在哪里?
(1)SD卡/iNand/Nand/NorFlash等启动介质:raw分区
启动时各种镜像都在SD卡中,内核镜像在SD卡的kernel分区,uboot使用movi命令将内核镜像从这个分区读到DDR中(譬如X210的iNand版本是movi命令,X210的Nand版本就是Nand命令)
使用命令:movi read kernel 30008000 将内核从iNand读取到DDR。其中kernel指的是uboot中的kernel分区(uboot在SD卡中划分了一个区域范围,这个区域范围专门用来存放kernel镜像)
(2)tftp、nfs等网络下载方式从远端服务器获取镜像
uboot还支持远程启动,也就是内核镜像不烧录到开发板的SD卡中,而是放在主机的服务器中,然后uboot通过网络从服务器中下载镜像到开发板的DDR中。
分析总结:
最终目的是要将内核镜像放到DDR中的特定地址,不管内核镜像是怎么到DDR中的。
以上2种方式各有优劣。
产品出厂时会设置为从SD卡中启动内核(客户不会还要搭建tftp服务器才能使用···);
tftp下载远程启动这种方式一般用来开发。
6、镜像要放在DDR的什么地址?
(1)内核一定要放在链接地址处,链接地址去内核源代码的链接脚本或者Makefile中去查找。X210中是0x30008000。
7、zImage和uImage的区别联系
7.1、bootm命令对应do_bootm函数
(1)命令名前加do_即可构成这个命令对应的函数,因此当我们bootm命令执行时,uboot实际执行的函数叫do_bootm函数,在cmd_bootm.c。
(2)do_bootm函数刚开始定义了一些变量,然后用宏来条件编译执行了secureboot的一些代码(主要进行签名认证),x210的uboot中并没有使用安全启动,先不管他;然后进行了一些数据结构的赋值操作,也不管他。然后到了CONFIG_ZIMAGE_BOOT,用这个宏来条件编译一段代码,这段代码是用来支持zImage格式的内核启动的。
7.2、vmlinuz和zImage和uImage
(1)uboot经过编译生成了elf格式的可执行程序u-boot,这个程序类似于windows下的exe格式,在Ubutun下是可以直接运行的。但是这种格式不能用来烧录下载。我们用来烧录下载使用的是u-boot.bin,它是对elf格式的u-boot使用arm-linux-objcopy工具进行加工处理(主要目的是去掉一些无用的)得到的。这个u-boot.bin就是镜像(image),镜像就是用来烧录到iNand中执行的。
(2)linux内核经过编译后也会生成一个elf格式的可执行程序,叫vmlinux或vmlinuz,这个就是原始的未经任何处理加工的内核文件;嵌入式系统部署时烧录的一般不是这个vmlinuz/vmlinux,而是要用objcopy工具去制作成烧录镜像格式(就是u-boot.bin这种,但是内核没有.bin后缀),内核经过objcopy工具加工得到的烧录镜像的文件就叫Image(把78M大的精简成了7.5M,因此这个制作烧录镜像主要目的就是缩减大小,节省磁盘)。
(3)原则上Image就可以直接被烧录到Flash上进行启动执行(类似于u-boot.bin),但是实际上并不是这么简单。实际上linux的作者们觉得Image还是太大了所以对Image进行了压缩,并且在image压缩后的文件的前端附加了一部分解压缩代码,这段代码是未经压缩的。两者一起构成的镜像就叫zImage。(因为当年Image大小刚好比一张软盘(软盘有2种,1.2M的和1.44MB两种)大,为了节省1张软盘的钱于是乎设计了这种压缩Image成zImage的技术)。
zImage = zImage信息头 + 解压缩代码 + image压缩后的文件
(4)uboot为了启动linux内核,还发明了一种内核格式叫uImage。uImage是由zImage加工得到的,uboot中有一个工具mkimage,可以将zImage加工生成uImage。
注意:uImage不关linux内核的事,linux内核只管生成zImage即可,所以在linux内核源码目录中是没有这个mkimage工具的,因此需要将这个工具从uboot目录中复制给linux内核去使用,然后使用mkimage工具将zImage加工生成uImage来给uboot启动。这个加工过程其实就是在zImage前面加上64字节的uImage的头信息。
uImage = uImage信息头 + zImage信息头 + 解压缩代码 + image压缩后的文件
(5)原则上uboot启动时应该给他uImage格式的内核镜像,但是实际上uboot中也可以支持zImage,是否支持就看x210_sd.h中是否定义了CONFIG_ZIMAGE_BOOT这个宏。
因此,有些uboot是支持zImage启动的,有些则不支持。但是所有的uboot肯定都支持uImage启动。
7.3、将内核编译成uImage格式去启动
(1)如果直接在kernel底下去make uImage会提供mkimage command not found错误。解决方案是去uboot/tools下cp mkimage /usr/local/bin/,复制mkimage工具到系统环境变量包含的目录下。再去make uImage即可。
8、zImage启动细节
(1)do_bootm函数中一直到397行的after_header_check这个符号处,都是在进行镜像的头部信息校验。校验时就要根据不同种类的image类型进行不同的校验。所以do_bootm函数的核心就是去分辨传进来的image到底是什么类型,然后按照这种类型的头信息格式去校验。校验通过则进入下一步准备启动内核;如果校验失败则认为镜像有问题,所以不能启动。
zImage = zImage头信息+解压缩代码+压缩后的image
uImage = uImage头信息+zImage头信息+解压缩代码+压缩后的image
8.1、LINUX_ZIMAGE_MAGIC
//cmd_bootm.c下的do_bootm函数第196~225行
#ifdef CONFIG_ZIMAGE_BOOT
#define LINUX_ZIMAGE_MAGIC 0x016f2818
/* find out kernel image address */
if (argc < 2) {
addr = load_addr; //最终值为0x30000000
debug ("* kernel: default image load address = 0x%08lx\n",
load_addr);
} else {
addr = simple_strtoul(argv[1], NULL, 16); //argv[1] = 0x30008000
debug ("* kernel: cmdline image address = 0x%08lx\n", img_addr);
}
//以上这段代码告诉我们镜像放在内存的什么位置
if (*(ulong *)(addr + 9*4) == LINUX_ZIMAGE_MAGIC) { //zImage头部开始的第37-40字节处存放着zImage的标志位,这里是判断这个镜像是不是zImage
printf("Boot with zImage\n");
addr = virt_to_phys(addr); //将虚拟地址转化为物理地址 c3e00000或33e00000 -> 33e00000
hdr = (image_header_t *)addr;
/*image_header_t是一个结构体,zImage头信息也是image_header_t这种类型的数据结构,
*所以这里是要将镜像文件addr中的zImage头信息单独取出,用hdr这个指针指向zImage头
。
*/
hdr->ih_os = IH_OS_LINUX;
//改造zImage头信息中的ih_os和ih_ep
hdr->ih_ep = ntohl(addr);
memmove (&images.legacy_hdr_os_copy, hdr, sizeof(image_header_t));
//用zImage头信息构建images
/* save pointer to image header */
images.legacy_hdr_os = hdr;
images.legacy_hdr_valid = 1;
goto after_header_check;
}
#endif
(1)LINUX_ZIMAGE_MAGIC这是一个定义的魔数,这个数等于0x016f2818,代表这个镜像是一个zImage。在zImage的头信息格式中某个位置定义了一个标志,这个标志用来表示镜像类型,如果这个标志等于0x016f2818,则说明这个镜像是zImage格式的。这里定义这个宏是为了之后和zImage的头信息中的标志进行比对。
(2)uboot命令行下使用命令 bootm 0x30008000 启动内核,所以do_bootm函数的argc=2,argv[0]=bootm argv[1]=0x30008000。但是实际bootm命令还可以不带参数执行。直接bootm,则会从load_addr这个默认地址去执行,load_addr = CFG_LOAD_ADDR = MEMORY_BASE_ADDRESS = 0x30000000(定义在x210_sd.h中)。
(3)zImage头部开始的第37-40字节处存放着标志类型的魔数,从这个位置取出后对比LINUX_ZIMAGE_MAGIC,相等则说明这个镜像文件是zImage格式,不相等则为其它格式。可以用二进制阅读软件来打开zImage查看,就可以证明,如winhex、UltraEditor。
8.2、image_header_t 212行
//do_bootm函数(209~224行)
if (*(ulong *)(addr + 9*4) == LINUX_ZIMAGE_MAGIC) { //zImage头部开始的第37-40字节处存放着zImage的标志位,这里是判断这个镜像是不是zImage
printf("Boot with zImage\n");
addr = virt_to_phys(addr); //将虚拟地址转化为物理地址 c3e00000或33e00000 -> 33e00000
hdr = (image_header_t *)addr;
/*image_header_t是一个结构体,zImage头信息也是image_header_t这种类型的数据结构,
*所以这里是要将镜像文件addr中的zImage头信息单独取出,用hdr这个指针指向zImage头
。
*/
hdr->ih_os = IH_OS_LINUX;
//改造zImage头信息中的ih_os和ih_ep
hdr->ih_ep = ntohl(addr);
memmove (&images.legacy_hdr_os_copy, hdr, sizeof(image_header_t));
//用zImage头信息构建images
/* save pointer to image header */
images.legacy_hdr_os = hdr;
images.legacy_hdr_valid = 1;
goto after_header_check;
}
(1)image_header_t是一个结构体,zImage头信息也是image_header_t这种类型的数据结构,所以这里是要将镜像文件addr中的zImage头信息单独取出,用hdr这个指针指向zImage头
。
后续还进行了一些改造。hdr->ih_os = IH_OS_LINUX; hdr->ih_ep = ntohl(addr);这两句就是在进行改造zImage头信息。
然后再将Image头信息构建成images这个变量,后续会用到。
(2)images全局变量仅仅只是在do_bootm函数中使用。zImage的校验过程其实就是先确认是不是zImage格式的镜像文件,确认后再修改zImage的头信息到合适,修改后用头信息去初始化images这个全局变量,然后就完成了校验,images这个变量再后面会被使用。
9、uImage启动细节
9.1、uImage启动
//cmd_bootm.c下的do_bootm函数第227~244行
/* get kernel image header, start address and length */
os_hdr = boot_get_kernel (cmdtp, flag, argc, argv,
&images, &os_data, &os_len);
if (os_len == 0) {
puts ("ERROR: can't get kernel image!\n");
return 1;
}
/* get image parameters */
switch (genimg_get_format (os_hdr)) {
case IMAGE_FORMAT_LEGACY:
type = image_get_type (os_hdr);
comp = image_get_comp (os_hdr);
os = image_get_os (os_hdr);
image_end = image_get_image_end (os_hdr);
load_start = image_get_load (os_hdr);
break;
(1)do_bootm函数237行中的IMAGE_FORMAT_LEGACY宏,LEGACY(表示遗留的),在do_bootm函数中,这种方式指的就是uImage的方式。uImage方式是uboot本身最早使用的支持linux启动的镜像格式,但是后来这种方式由于一些缺陷被另一种新的方式替代,这个新的方式就是设备树方式(在do_bootm函数中叫FIT,从do_bootm函数245行开始)
(2)uImage的启动校验主要在boot_get_kernel函数中,主要任务就是校验uImage的头信息,以下为boot_get_kernel函数内容:
//在cmd_bootm.c下的do_bootm函数第228行调用,定义在该文件下的562~715行。
/* find out kernel image address */
if (argc < 2) { //和zImage处相同
img_addr = load_addr;
debug ("* kernel: default image load address = 0x%08lx\n",
load_addr);
#if defined(CONFIG_FIT) //这个是设备树传参方式才有的
} else if (fit_parse_conf (argv[1], load_addr, &img_addr,
&fit_uname_config)) {
debug ("* kernel: config '%s' from image at 0x%08lx\n",
fit_uname_config, img_addr);
} else if (fit_parse_subimage (argv[1], load_addr, &img_addr,
&fit_uname_kernel)) {
debug ("* kernel: subimage '%s' from image at 0x%08lx\n",
fit_uname_kernel, img_addr);
#endif
} else { //这个是uImage启动 镜像放在内存中的位置,可以看到和zImage启动相同,都是0x30008000
img_addr = simple_strtoul(argv[1], NULL, 16);
debug ("* kernel: cmdline image address = 0x%08lx\n", img_addr);
}
show_boot_progress (1); //打印一个调试信息,告诉我们启动了百分之多少
/* copy from dataflash if needed */ //实际没有使用,这个是对镜像的重定位,把镜像在30008000复制到30000000
img_addr = genimg_get_image (img_addr);
/* check image type, for FIT images get FIT kernel node */ //检验镜像的类型
*os_data = *os_len = 0;
switch (genimg_get_format ((void *)img_addr)) {
case IMAGE_FORMAT_LEGACY:
printf ("## Booting kernel from Legacy Image at %08lx ...\n",
img_addr);
hdr = image_get_kernel (img_addr, images->verify);//校验镜像的一系列标志位,并且打印image的相关信息(最终是可以看到的)
if (!hdr) //hdr = img_addr = 0x30008000
return NULL;
show_boot_progress (5);
/* get os_data and os_len */
switch (image_get_type (hdr)) {
case IH_TYPE_KERNEL: //说明这是一个内核镜像
*os_data = image_get_data (hdr);
*os_len = image_get_data_size (hdr);
break;
case IH_TYPE_MULTI:
image_multi_getimg (hdr, 0, os_data, os_len);
break;
default:
printf ("Wrong Image Type for %s command\n", cmdtp->name);
show_boot_progress (-5);
return NULL;
}
}
/*
* copy image header to allow for image overwrites during kernel
* decompression.
*/
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; //跟zImage的处理一样
show_boot_progress (6);
break;
}
return (void *)img_addr;
以下为genimg_get_image函数内容,这个函数实际并没有使用,这个是对镜像的重定位,把镜像在30008000复制到30000000:
ulong genimg_get_image (ulong img_addr)
{
ulong ram_addr = img_addr; //0x30008000
#ifdef CONFIG_HAS_DATAFLASH //实际不使用
ulong h_size, d_size;
if (addr_dataflash (img_addr)){
/* ger RAM address */
ram_addr = CFG_LOAD_ADDR; //ram_addr = 0x30000000
/* get header size */
h_size = image_get_header_size (); //uImage方式的校验头大小为sizeof(fdt_header)=64B
#if defined(CONFIG_FIT) //设备树方式使用
if (sizeof(struct fdt_header) > h_size)
h_size = sizeof(struct fdt_header); //设备树方式的校验头大小为sizeof(fdt_header)=40B
#endif
/* read in header */
debug (" Reading image header from dataflash address "
"%08lx to RAM address %08lx\n", img_addr, ram_addr);
read_dataflash (img_addr, h_size, (char *)ram_addr);//复制uImage的校验头到ram_addr缓存
/* get data size */
switch (genimg_get_format ((void *)ram_addr)) { //校验是uImage方式还是设备树传参方式
case IMAGE_FORMAT_LEGACY: //uImage方式
d_size = image_get_data_size ((image_header_t *)ram_addr); //获取镜像的大小
debug (" Legacy format image found at 0x%08lx, size 0x%08lx\n",
ram_addr, d_size);
break;
#if defined(CONFIG_FIT) //设备树传参方式
case IMAGE_FORMAT_FIT: //设备树传参方式
d_size = fit_get_size ((const void *)ram_addr) - h_size;
debug (" FIT/FDT format image found at 0x%08lx, size 0x%08lx\n",
ram_addr, d_size);
break;
#endif
default:
printf (" No valid image found at 0x%08lx\n", img_addr);
return ram_addr;
}
/* read in image data */
debug (" Reading image remaining data from dataflash address "
"%08lx to RAM address %08lx\n", img_addr + h_size,
ram_addr + h_size);
read_dataflash (img_addr + h_size, d_size,
(char *)(ram_addr + h_size));
}
#endif /* CONFIG_HAS_DATAFLASH */
return ram_addr;
}
总结1:uboot本身设计时只支持uImage启动,原来uboot的代码也是这样写的。后来有了fdt(设备树)方式之后,就把uImage方式命令为LEGACY方式,fdt方式命令为FIT方式,于是乎多了些#if #endif添加的代码。后来移植的人又为了省事添加了zImage启动的方式,又为了省事把zImage启动方式直接写在了uImage和fdt启动方式之前,于是乎又有了一对#if #endif。于是乎整个的代码看起来很恶心。
总结2:第二阶段校验头信息结束,下面进入第三阶段,第三阶段主要任务是启动linux内核,调用do_bootm_linux函数来完成。
10、do_bootm_linux函数(do_bootm函数407处调用)
(1)函数定义在uboot/lib_arm/bootm.c中(61~165行)。
void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],
bootm_headers_t *images)
{
int machid = bd->bi_arch_number; //机器码
#ifdef CONFIG_CMDLINE_TAG
char *commandline = getenv ("bootargs"); //获取环境变量bootargs的值,后面会作为参数传给内核
#endif
/* find kernel entry point */
if (images->legacy_hdr_valid) { //第二步时就已经赋值为1,说明当前我们的镜像是有效的
ep = image_get_ep (&images->legacy_hdr_os_copy);//ep为操作系统程序入口,从头信息中获取程序入口
}
theKernel = (void (*)(int, int, uint))ep; //ep本身只是一个数字,强制类型转换成一个函数去访问
s = getenv ("machid"); //获取环境变量中的机器码
if (s) { //如果环境变量中有机器码
machid = simple_strtoul (s, NULL, 16);
//替代第一次bd->bi_arch_number的赋值,可以看出环境变量的优先级更高
printf ("Using machid 0x%x from environment\n", machid);
}
//以下开始传参,uboot传递参数以tag格式的数据结构,一tag一tag的发送到指定地址处(设定为0x30001000)
//那内核从指定地址处取参,从哪里开始读取参数,从哪里结束取参?以start_tag开始,以end_tag结束的参数空间
#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) //以上这下宏,哪个定义了,哪个将来就会以xxx_tag的参数形式发送给内核
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
/* we assume that the kernel is in place */
printf ("\nStarting kernel ...\n\n");
theKernel (0, machid, bd->bi_boot_params);
//启动内核,不再返回,uboot生命周期结束
//参数 0, machid, bd->bi_boot_params 意思分别是:0, 机器码, 参数在内存中存放的首地址(0x30001000)
//那么寄存器r0 = 0 , r1 = machid , r2 = bd->bi_boot_params
/* does not return */
return;
}
(2)SI找不到(是黑色的)不代表就没有,要搜索一下才能确定;搜索不到也不能代表就没有,因为我们在向SI工程中添加文件时,SI只会添加它能识别的文件格式的文件,有一些像Makefile、xx.conf等Makefile不识别的文件是没有被添加的。所以如果要搜索的关键字在makefile中或者脚本中,可能就是搜索不到的。(譬如TEXT_BASE)
10.1、镜像的entrypoint
(1)ep就是entrypoint的缩写,就是程序入口。这个程序入口地址由(内核镜像地址 + 偏移量)得到。
内核镜像地址 = 0x30008000,而偏移量则记录在头信息中。
(2)一般执行一个内核镜像都是:
第一步先读取头信息,然后在头信息的特定地址找MAGIC_NUM,由此来确定镜像种类;
第二步对镜像进行校验,判断内核是不是一个完整有效的内核。
第三步再次读取头信息,在特定地址处知道这个镜像的各种信息(镜像种类、镜像长度、程序入口地址);
第四步就去程序入口entrypoint处开始执行镜像。
至此,OS内核启动。
(3)theKernel = (void (*)(int, int, uint))ep;
将ep强制转化成函数地址并赋值给theKernel,则theKernel这个函数指针就指向了内存中加载的OS镜像的真正入口地址(就是操作系统的第一句执行的代码)。
10.2、机器码的再次确定
s = getenv ("machid"); //获取环境变量中的机器码
if (s) { //如果环境变量中有机器码
machid = simple_strtoul (s, NULL, 16);
//替代第一次bd->bi_arch_number的赋值,可以看出环境变量的优先级更高
printf ("Using machid 0x%x from environment\n", machid);
}
(1)uboot在启动内核时,机器码要传给内核。uboot传给内核的机器码是怎么确定的?第一备选是环境变量machid,第二备选是uboot的一个数据结构gd->bd->bi_arch_num(x210_sd.h中硬编码配置的)
10.3、传参并启动概述
(1)从110行到144行就是uboot将要传递给内核的参数放到“以某个地址为开始的内存区域”(uboot代码中设置为0x30001000)。之后再将首地址告诉内核,内核就会从这里取走参数。
(2)Starting kernel … 这个是uboot中最后一句打印出来的东西。这句如果能出现,说明uboot整个是成功的,也成功的加载了内核镜像,也校验通过了,也找到入口地址了,也试图去执行了。如果这句后串口就没输出了,说明内核并没有被成功执行。原因一般是:传参(80%)、内核在DDR中的加载地址·······
11、传参详解
11.1、tag方式传参
struct tag_header {
u32 size; //指定tag的大小
u32 tag; //指定tag的类型, core、mem、videotext......之类的
};
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem; //内存配置
struct tag_videotext videotext; //视频相关
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;
/*
* Acorn specific
*/
struct tag_acorn acorn;
/*
* DC21285 specific
*/
struct tag_memclk memclk;
struct tag_mtdpart mtdpart_info;
} u;
};
(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来处理,tar_xxx取决于hdrr->tag的类型。
(3)tag_start与tag_end。kernel接收到的传参是若干个tag构成的,这些tag以tag_start起始,到tag_end结束。
(4)tag传参的方式是由linux kernel发明的,kernel定义了bootloder向我传参的方式,uboot只是实现了这种传参方式给内核传参而已。
11.2、x210_sd.h中配置传参宏
(1)CONFIG_SETUP_MEMORY_TAGS,tag_mem,传参内容是内存配置信息。uboot将内存的配置信息维护在gd->bd这个数据结构中,tag_men就是传递的这个
(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的首地址。
11.3、移植时注意事项
(1)uboot移植时一般只需要配置相应的宏即可
(2)kernel启动不成功,注意传参是否成功。传参不成功首先看uboot中bootargs设置是否正确,其次看uboot是否开启了相应宏以支持传参。
12、uboot启动内核的总结
12.1、启动4步骤
第一步:将内核搬移到DDR中
第二步:校验内核格式、CRC(判断内核是否完整)等
第三步:准备参数
第四步:跳转执行内核