u-boot原理分析第六课-------启动内核的过程

    上两节课,我彻底解析了u-boot的命令,也动手实现了一个自己的命令。然而,u-boot的最终目的还是启动内核,所以我们本节课我们就开始探究u-boot启动内核的过程。我们打开common目录下的main.c文件,我们看到main_loop这个函数,当我们运行u-boot的时候,这应该就是我们运行的主函数了。在运行u-boot的时候,它会有一个倒计时,过了倒计时后,就直接启动Linux内核了。所以,我们直接看到他的启动部分:

我们看到,它里面有一句代码:getenv("bootcmd"),获取bootcmd的环境变量的值,并保存在s中。然后下面还有一句:run_command(s,0),也就是把环境变量的值当做命令来执行。我们来看看,这个环境变量的值是什么:

我们看到:bootcmd的值是:nand read.jffs2 0x30007Fc0 kernel; bootm 0x30007Fc0。如果把它当做命令的话,这就是两条命令,第一条则是nand read.jfss2(已jfss2的格式去读取nand 的内容,这样子做的话不需要设定长度的时候不需要你进行页对齐),他是从kernel处读到0x30007FC0处,kernel应该就是内核了,整句话的意思是从nand flash的kernel区中读出内核,然后读到内存中的30007FC0处(内存的地址是0x3000,0000到0x4000,0000)。那么,问题来了,kernel区在哪里呢?对于PC系统,每个硬盘的分区都有一个分区表,而对于嵌入式的系统,则没有这种分区表,所以我们只能在代码中进行写死,规定的它的区域,我们打开100ask24x0.h,看到以下代码:

#define MTDPARTS_DEFAULT "mtdparts=nandflash0:256k@0(bootloader)," \
                            "128k(params)," \
                            "2m(kernel)," \
                            "-(root)"

这里有一个宏定义:MTDPARTS_DEFAULT(MTD全称是memory technology device,内存技术设备,是用于访问linux 储存器(ROM flash)的子系统),就定义好了分区:

        1.0-256K放置bootloader

        2.后面的128K放置内核使用的参数

        3.参数后面的2M放置内核

        4.最后的所有空间用来放置根文件系统。

为了验证,我们也可以使用mtd命令进行查询:



    第一条命令我们已经分析完毕了,接下来我们分析第二条:bootm 0x30007fc0(0x30007fc0,也就是内核在内存里开始的地址)。我们上两节课就了解了bootm这个命令,知道执行这个命令后会调用do_bootm函数,接下来我们就仔细地分析一下这个函数。在次之前,我再讲解一些知识。对于u-boot,它会从flash上去读取内核,而这个内核的格式,则是一个uImage文件。它的构成是一个头部加上一个真正的内核。头部储存有内核的相关信息,能给bootm命令来读取其相关的参数而做出操作。我们先来看一下头部是怎样的:

我们看到,这里定义有一种类型的变量,为:image_header_t,这应该就是我们所需要的找的头部的数据类型。我们找到这个数据类型的定义(在include目录下的image.h头文件里面):

typedef struct image_header {
	uint32_t	ih_magic;	/* Image Header Magic Number	*/
	uint32_t	ih_hcrc;	/* Image Header CRC Checksum	*/
	uint32_t	ih_time;	/* Image Creation Timestamp	*/
	uint32_t	ih_size;	/* Image Data Size		*/
	uint32_t	ih_load;	/* Data	 Load  Address		*/
	uint32_t	ih_ep;		/* Entry Point Address		*/
	uint32_t	ih_dcrc;	/* Image Data CRC Checksum	*/
	uint8_t		ih_os;		/* Operating System		*/
	uint8_t		ih_arch;	/* CPU architecture		*/
	uint8_t		ih_type;	/* Image Type			*/
	uint8_t		ih_comp;	/* Compression Type		*/
	uint8_t		ih_name[IH_NMLEN];	/* Image Name		*/
} image_header_t;

我们看到,这个结构体里面有许多的信息,其中有两个是ih_load和ih_ep,分别是内核的加载地址和内核的入口地址。当执行bootm命令的时候,它会读出内核的头部的加载地址和入口地址,当发现内核此时并不处于加载地址的时候,它就会把内核移到加载地址去,并跳到入口地址去执行。我们在函数中看看是不是这样,我们看到这个函数里的这句代码:

我们看到, 这里它调用了内存拷贝函数,源地址是data,也就是我们内核的地址,目的地址是头部的加载地址。这一步操作也就是把我们的内核拷贝到加载地址处。我们之前是把内核读到地址0x30007fc0处的,而头部的大小是64字节(你们可以去算一下,其中IH_NMLEN的值是32),0x30007fc0+64=0x30008000。对于s3c2440的开发板,其加载地址就是0x30008000,所以此时内核地址就等于加载地址。所以,这时不应该是执行这里的代码,我们往上看一下,看看有没有其它的分支:

我们看到,这里有一条if语句,判断条件是头部的加载地址是否等于内核当前所处的地址,如果不等,才执行后面else的内容,而我们看到的那句memmove是在else内容中的,这也就验证了我们的猜想。总的来说,bootm这里是读出了头部的参数,然后进行判断是否要移动内核到合适的位置。既然移动好了,下一步就应该是启动内核了,我们看到下面的代码:

这个do_bootm_linux应该就是启动内核的函数了。接下来,我们来研究一下这个函数(注意,我们要看的函数在armlinux.c处,不再此文件里),我直接看到最后一句代码:

这里它调用了一个函数theKernel,那么这个是什么函数呢?我们网上找找它的定义:

我们看到,theKernel是一个函数指针,后面就给这个theKernel赋值了,它的地址就是uImage头部的入口地址。而我们最后直接把这个地址当做函数地址去执行,说明u-boot就直接跳到了入口地址处执行了,也就引导了内核的运行。然而,我们看到这个函数好像除了定义thekernel函数指针和给其赋值之外,还做了一大堆的东西,这是为什么呢?其实,do_bootm_linux这个函数在启动内核之前,还进行了设置参数的操作,比如一些外接的硬盘,内存的检测等等。那么,它是如何去设置参数的呢?内核又是如何去读取的参数的?这里,双方肯定有一个约定的一个格式,这个格式就是TAG,我们看到代码:

这里,我们看到它设置了很多参数,其中最重要的应该就是setup_start_tag(当然,还有setup_end_tag),从#if的条件可以看到无论你定义了什么宏定义,都会先执行setup_start_tag。这里我们分析四个参数的设置:setup_start_tag、setup_memory_tags、setup_commandline_tag、setup_end_tag。

    首先看到setup_start_tag:它传入的值是bd。我们看看这个函数做了什么:

static void setup_start_tag (bd_t *bd)
{
	params = (struct tag *) bd->bi_boot_params;

	params->hdr.tag = ATAG_CORE;
	params->hdr.size = tag_size (tag_core);

	params->u.core.flags = 0;
	params->u.core.pagesize = 0;
	params->u.core.rootdev = 0;

	params = tag_next (params);
}

我们看到,这里先给params这个tag变量赋值,其实这个值就是一个地址,为:0x30000100。我们再看一下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;
	} u;
};
这个结构体里,首先有个tag_header数据成员,然后有一个联合体,这个联合体应该是根据你是设置什么参数和设置什么的值的。我们继续看之前的函数:
static void setup_start_tag (bd_t *bd)
{
	params = (struct tag *) bd->bi_boot_params;

	params->hdr.tag = ATAG_CORE;
	params->hdr.size = tag_size (tag_core);

	params->u.core.flags = 0;
	params->u.core.pagesize = 0;
	params->u.core.rootdev = 0;

	params = tag_next (params);
}

接下来,他是设置hdr这个成员的tag,值为ATAG_CORE,这个值为0x5441000,接着是设置它的size,tag_size是一个带参宏:

#define tag_size(type)	((sizeof(struct tag_header) + sizeof(struct type)) >> 2)

我们把值带入进去算一下(tag_core的大小为12字节,可以自己找得到算出来,tag_header的大小为八字节),结果得到:16字节,我们可以用一张图表示一下:

size的值为5,单位为四个字节,最后的一句tag_next表示其指向下一个tag。

    我们再看到下一个我们要研究的参数设置:setup_memory_tags(bd),这个是告诉内核我们的内存的参数,我们 看一下这个函数的定义:

static void setup_memory_tags (bd_t *bd)
{
	int i;

	for (i = 0; i < CONFIG_NR_DRAM_BANKS; i++) {
		params->hdr.tag = ATAG_MEM;
		params->hdr.size = tag_size (tag_mem32);

		params->u.mem.start = bd->bi_dram[i].start;
		params->u.mem.size = bd->bi_dram[i].size;

		params = tag_next (params);
	}
}

我们看到它这里定义了一个循环计数变量,然后下面有一个循环。CONFIG_NR_DRAM_BANKS这个值是1(我们只有一块内存,没有其它的了(这个blank是值有多少块内存,不是内存里面有分成多少个blank)),所以其实只会执行一次。在循环体里,首先先给params的tag赋值,值为:ATAG_MEM(0x54410002),然后又给hdr的size进行赋值。接着是指定内存的开始地址和大小,这个值分别是:0x30000000和0x04000000(大小64M),都是写死的:

最后,再指向下一个参数设置。我们用图表示一下:

    接下来,我们就看setup_commandline_tag这个函数:

我们看到,这个函数的参数比之前多一个,这个commandline是什么呢?


看来,这个值在环境变量里,我们打开查看一下这个环境变量的值:

这是传递给内核的命令行参数,比如根文件系统:位于第四个Flash分区,第一个应用程序,内核打印信息从哪里打印出来。下面我们看一下这个函数的定义:

static void setup_commandline_tag (bd_t *bd, char *commandline)
{
	char *p;

	if (!commandline)
		return;

	/* eat leading white space */
	for (p = commandline; *p == ' '; p++);

	/* skip non-existent command lines so the kernel will still
	 * use its default command line.
	 */
	if (*p == '\0')
		return;

	params->hdr.tag = ATAG_CMDLINE;
	params->hdr.size =
		(sizeof (struct tag_header) + strlen (p) + 1 + 4) >> 2;

	strcpy (params->u.cmdline.cmdline, p);

	params = tag_next (params);
}

这里,首先定义了一个指针p,然后检查一下commandline是不是空指针,如果为空指针的话就返回,下面的一个for循环是去除命令行前面的空格,然后再判断一下p的值是不是空的,如果是的话就直接返回。下面是设置params的参数,最后是将命令行的内容拷贝到params->u.cmdline.cmdline里面去,然后指向下个参数。这里我再用图表示一下:


    接下来,我们看到最后一个命令:setup_end_tag。这个命令比较简单

static void setup_end_tag (bd_t *bd)
{
	params->hdr.tag = ATAG_NONE;
	params->hdr.size = 0;
}

tag设置为0,size为0就结束了。

    我们来总结一下u-boot启动内核的过程:

    1.从Flash中读出内核

    2.启动(这里分两步:1.设置启动参数 2.跳到入口地址)

我们再看到theKernel这个函数:

theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
}

这里有三个参数:第一个是0,第二个是它的机器ID,第三个是我们之前设置的参数的首地址。那么,我们的机器ID是什么呢?我们可以找一下:

可以看到,它是一个宏定义,这里应该是填入机器ID,然后内核检测是否支持你这块开发版。

    以上就是I启动内核的所有过程了!

猜你喜欢

转载自blog.csdn.net/xiaokangdream/article/details/79551296