uboot如何启动内核

一、uboot和内核到底是什么

1.uboot和内核就是一个裸机程序

(1)uboot的本质就是就是一个复杂点的裸机程序,和ARM部分写的裸机程序没有本质区别。

(2)内核本身也是一个裸机程序,和uboot、裸机程序无本质区别。要说不同的地方,那就是内核运行起来后,在软件上分为内核层和应用层,分层后两层的权限不同,内存访问和设备操作的管理上更加精细(内核可以随便访问各种硬件,而应用程序只能被限制地访问硬件和内存地址)。

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

2.部署在SD卡特定分区内

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

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

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

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

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

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

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

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

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

二、启动内核第一步:加载内核到DDR中

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

1.静态内核镜像在哪里

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

常规启动时,各种镜像都在SD卡中,因此uboot只需要从SD卡的kernel分区去读取内核镜像到DDR中即可。读取要使用uboot的命令来读取(例如x210的iNand版本的movi命令、x210的Nand版本的nand命令)。这种启动方式来加载内核到DDR中,使用命令:movi read kernel 30008000,其中kernel指的是uboot中的kernel分区(就是uboot中规定的SD卡中的一个区域范围,这个区域范围被设计来存放kernel镜像,就是所谓的kernel分区)。

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

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

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

2.镜像要放到DDR的什么地址

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

三、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,这个文件是由arm-linux-objcopy工具进行加工得到的(主要是去掉一些无用的信息)。这个u-boot.bin就叫镜像(Image),镜像是用来烧录到iNand中执行的。

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

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

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

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

3.编译内核得到uImage启动

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

四、zImage启动细节

do_bootm函数从起始一直到397行的after_header_check这个符号处,都是在进行镜像的头部信息校验。校验时就要根据不同类型的Image进行不同的校验,所以do_bootm函数的核心是去分辨传进来的Image到底是什么类型,然后按照这种类型的头信息格式去校验,校验通过则进入下一步准备启动内核,如果校验失败,则表示镜像有问题,不能启动。在这里我们只关注zImage相关的内容:

代码:196 ~ 225行

#ifdef CONFIG_ZIMAGE_BOOT
#define LINUX_ZIMAGE_MAGIC	0x016f2818
	/* find out kernel image address */
	if (argc < 2) {
		addr = load_addr;
		debug ("*  kernel: default image load address = 0x%08lx\n",
				load_addr);
	} else {
		addr = simple_strtoul(argv[1], NULL, 16);
		debug ("*  kernel: cmdline image address = 0x%08lx\n", img_addr);
	}


	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;

		goto after_header_check;
	}
#endif

1.LINUX_ZIMAGE_MAGIC

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

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

(3)zImage头部开始的第37~40字节处存放着zImage标志魔数,从这个位置取出然后对比LINUX_ZIMAGE_MAGIC。

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

五、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启动方式,又为了省事,直接写在了uImage和fdt启动方式之前,于是又多了一对#if_#endif。

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

六、do_bootm_linux函数

do_bootm_linux函数的定义位于lib_arm/bootm.c文件中。

代码:61 ~ 165行(lib_arm/bootm.c)

void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],
		     bootm_headers_t *images)
{
	ulong	initrd_start, initrd_end;
	ulong	ep = 0;
	bd_t	*bd = gd->bd;
	char	*s;
	int	machid = bd->bi_arch_number;
	void	(*theKernel)(int zero, int arch, uint params);
	int	ret;

#ifdef CONFIG_CMDLINE_TAG
	char *commandline = getenv ("bootargs");
#endif

	/* find kernel entry point */
	if (images->legacy_hdr_valid) {
		ep = image_get_ep (&images->legacy_hdr_os_copy);
#if defined(CONFIG_FIT)
	} else if (images->fit_uname_os) {
		ret = fit_image_get_entry (images->fit_hdr_os,
					images->fit_noffset_os, &ep);
		if (ret) {
			puts ("Can't get entry point property!\n");
			goto error;
		}
#endif
	} 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);

	debug ("## Transferring control to Linux (at address %08lx) ...\n",
	       (ulong) theKernel);

#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 (&params);
#endif
#ifdef CONFIG_REVISION_TAG
	setup_revision_tag (&params);
#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");

#ifdef CONFIG_USB_DEVICE
	{
		extern void udc_disconnect (void);
		udc_disconnect ();
	}
#endif

	cleanup_before_linux ();

	theKernel (0, machid, bd->bi_boot_params);
	/* does not return */
	return;

error:
	do_reset (cmdtp, flag, argc, argv);
	return;
}

1.镜像的entrypoint

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

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

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

2.机器码的再次确定

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

3.传参并启动操作

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

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

七、传参详解

1.tag方式传参

(1)struct tag:tag是一个数据结构,在uboot和linux kernel中都有tag数据结构的定义,而且定义都是一样的。

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

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

(4)tag的传参方式是由linux kernel发明的,kernel定义了这种传参的方式,uboot只是实现了这种传参方式,从而可以支持给kernel传参。

tag定义:

struct tag_header {
	u32 size;
	u32 tag;
};

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;
};

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内核的,通过寄存器来实现传参(第1个参数放在r0中,第2个参数放在r1中,第3个参数放在r2中)。第1个参数固定为0,第2个参数是机器码,第3个参数就是tag的首地址。

3.移植时注意项

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

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

八、uboot启动内核总结

(1)启动步骤

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

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

第三步:准备传参

第四步:跳转执行内核

(2)涉及到的主要函数是do_bootm和do_boom_linux

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

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

 

  • 17
    点赞
  • 98
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值