天天看点

uboot之内核启动

uboot的最终目的就是用来启动内核,对于我们Inand版本的X210开发板来说,在uboot命令行执行bootm命令时,就去启动内核。那么uboot背后的机制是怎么样的呢?当我们输入bootm命令的时候,uboot会执行do_bootm函数。启动内核我们需要做的几件事情:

step1:将内核从外部介质(SD卡、iNand等)中重定位到内存的相应位置

step2:进行内核镜像种类的识别(zImage、uImage、设备树),并且进行内核镜像消息头的校验。

step3:进行传参的准备,内核需要uboot给它传递相应的参数,传递的参数的个数以及种类可以通过相应的宏来实现。

step4:启动内核,跳转到内存中内核代码部分开始执行内核。

一、uboot和内核到底是什么?

  uboot和内核本质上来说都是裸机程序,不同的是这两个裸机程序的目的是不同,uboot就是用来将内核从启动介质中加载到DDR中,然后跳转到DDR中执行内核程序,内核程序启动之后,也就是说我们的操作系统启动了,在软件上分为内核层和应用层,两个层次的权限是不同的,内存访问和设备操作的管理上更加精细化(内核层可以随意访问各种硬件,而应用层只能被限制访问硬件和内存地址)。

二、内核镜像在哪里?

1、SD卡/iN安定/NorFlash等:raw分区

常规启动时,各种镜像都在SD卡中,因此uboot只需要从sd卡的kernel分区把内核加载到DDR对应的链接地址处,在uboot中我们可以通过命令movi,来从SD卡中加载我们的内核到DDR中。

命令格式:movi read kernel 0x30008000;将SD卡的kernel分区,加载DDR的3000800地址处。

然后使用bootm 0x3000800启动内核即可。

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

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

命令:tftp 0x30008000 zImage-qt   把内核加载到DDR中的特定地址处。

三、zImage、uImage和vmlinuz镜像的区别

   uboot经过编译链接生成elf格式的u-boot程序,这个程序类似于Windows中的exe格式的可执行程序,在Linux系统可以直接执行的。但是这种格式的程序不能够烧录下载,所以我们需要通过工具arm-linux-objcopy,将elf格式的可执行文件转换成可以用来烧录下载的u-boot.bin镜像文件(去掉多余的符号信息,而且大小也变小了很多),可以烧录到iNand中去执行。

  内核经过编译链接之后,生成的elf格式的可执行程序,叫做vmlinux或者vmlinuz,这个就是原始的未经加工的内核elf格式文件。在我们桌面版本的Linux操作系统中,就直接只用该格式的内核。但是在我们部署嵌入式操作系统的时候,这样格式的内核太大,而且不能够烧录下载,所以我们使用和uboot相同的方法,使用arm-linux-objcopy工具将elf格式的内核文件转换成镜像文件Image,不过没有.bin后缀,不过格式都是一样。

  原则上Image就可以直接烧录下载,然后执行,但是我们的开发人员还是嫌弃Image太大,所以对Image进行了压缩,并且在压缩后的头部添加没有经过压缩的解压代码,这样生成的文件叫做zImage。

  但是到这里还没有完,uboot本身有发明了一种镜像文件,就是在zImage的基础上,uboot使用mkimage工具,在zImage的头部添加上一定字节的头信息,生成的文件叫做uImage。给uboot启动的内核镜像文件。

提示:怎么生成uImage镜像文件,我们在内核编译的时候,使用命令make uImage即可。

step1:因为生成uImage镜像需要mkimage工具,所以我们需要从uboot/tools下将mkimage工具copy到我们可以找到的路径下,比如/usr/local/bin文件下。

step2:然后配置内核,使用命令:make x210ii_qt_defconfig

step3:使用make命令。编译内核,即可在相应文件夹下得到内核镜像uImage。

四、zImage启动细节

 1、#define LINUX_ZIMAGE_MAGIC    0x016f2818

         LINUX_ZIMAGE_MAGIC  是一个魔数,表示该镜像为zImage,zImage格式的镜像会在头部的一个固定位置存放一个32位的数,这个数用来表示内核镜像的类型,如果这个数等于LINUX_ZIMAGE_MAGIC 就表明,当前的内核镜像为zImage。实际上这个字节的位置在内核镜像zImage所在地址的首地址的第37-40个字节存储着。

2、内核地址的判断

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

根据函数传参的个数,来判断内核在DDR中的地址,如果通过bootm xxxx,函数就是用传递进来的xxx作为内核的地址。如果直接使用命令bootm,则程序会使用默认的内核地址load_addr作为内核的在DDR中的位置。

3、判断内核镜像

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));//对image进行封装,在后面会用到这个image,是一个全局 变量

		/* save pointer to image header */
		images.legacy_hdr_os = hdr;

		images.legacy_hdr_valid = 1;

		goto after_header_check; //直接跳到下一步
	}           

其中image是一个Image_header_t类型的全局变量,这个是uboot启动内核的标准头信息,这个数据结构是uboot启动内核使用的标准。需要对这个头,进行一定的封装,以便后面使用。

五、uImage启动细节

 1、uImage方式是uboot本身发明支持的Linux启动的镜像格式,但是后来这种方式被新的方式替代,这个新的方式就是设备树的方式(在do_bootm函数中叫做FIT)。

uImage的启动校验在boot_get_kernel函数中,主要任务就是校验uImage的头信息。

下面主要分析boot_get_kernel函数

2、判断内核在DDR中的位置

原理和上面判断zImage是相同的,这里就不再介绍了。

3、show_boot_progress (1);

这个就是用来显示,uboot启动内核进行到哪个阶段,就相当于我们使用printf打印输出信息一样的。

下面在do_bootm_linux分析代码

4、ep = image_get_ep (&images->legacy_hdr_os_copy);

ep就是EntryPoint的缩写,就是内核程序的入口。一个镜像的开始执行部分不是在镜像的开头(镜像开头有几个头信息),真正的起始在有一定的偏移量,这个偏移量记录在头信息中,从头信息中可以找到真正的入口地址。

5、theKernel = (void (*)(int, int, uint))ep;

theKernel 这个是一个函数指针,这个函数指针其实就是内核程序,执行该程序就表示启动内核。

6、机器码的确定

机器码的来源有两个,一个是环境变量中的,一个就是在x210_sd.h中硬编码的。

7、传参

这个在下面介绍,uboot如何给内核传递参数的。

8、printf ("\nStarting kernel ...\n\n");

这是uboot打印的最后一句话,这计划之后就会启动内核,uboot执行成功,加载内核镜像,校验头信息,入口地址等都成功了,但是如果没有启动内核,可以的问题:

  • 传参错误,机器码等
  • 内核烧录的问题
  • 内核在DDR中加载的地址

六、uboot传参详解

1、uboot是通过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;
};           

tag是一个结构体,在uboot和Linux中都有定义,且定义是相同的,tag主要是tag_header和具体的不同tag的消息结构体。

struct tag_header {
	u32 size;
	u32 tag;
};           

tag_header中主要包含了tag的类型和tag的大小,当传递不同参数时,就需要不同tag,tag的类型就不同,所传tag的消息也就不同。

2、tag的布局

uboot通过tag的方式想内核传递参数,但是kernel怎么知道uboot传递了哪些tag,以及tag的数量,tag的起始地址。

首先在布局上,tag的头和尾是固定的,tag的头tag是一个类型为ATAG_CORE的tag,他的地址是 bd->bi_boot_params;tag的尾tag是是一个类型为ATAG_NONE的tag,所以kernel通过地址找到第一个start,然后通过最后一个tag判断uboot传递给自己的信息,都结束了。

3、怎么控制传递的tag信息

uboot传递不同的tag都是由x210_sd.h头文件中对应的tag宏实现的,打开不同的tag宏就传递不同的tag。

继续阅读