linux驱动之uboot启动过程及参数传递
前言
一直以来都在学习和开发嵌入式linux,但对于一些常用的工具和机制却不甚了解,包括今天要说的uboot引导和启动linux内核,最近打算启动技术博客来学习和记录探索这些过程中所获得的知识。从事嵌入式linux开发的人应该都知道uboot,支持多种操作系统,多种硬件平台的uboot在嵌入式linux界可是大名鼎鼎,我们今天就来谈一谈uboot如何启动内核。这里我们不提供代码,仅给出相关的关键结构体,其余的请各位自行查看uboot代码
过程讲解
do_bootm
在uboot引导Linux启动时,使用的是bootm的命令。这个命令执行的函数就是do_bootm
, 这个函数的地址在cmd/bootm.c
中。
代码如下:
int do_bootm(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
{
#ifdef CONFIG_NEEDS_MANUAL_RELOC
static int relocated = 0;
if (!relocated) {
int i;
/* relocate names of sub-command table */
for (i = 0; i < ARRAY_SIZE(cmd_bootm_sub); i++)
cmd_bootm_sub[i].name += gd->reloc_off;
relocated = 1;
}
#endif
/* determine if we have a sub command */
argc--; argv++;
if (argc > 0) {
char *endp;
simple_strtoul(argv[0], &endp, 16);
/* endp pointing to NULL means that argv[0] was just a
* valid number, pass it along to the normal bootm processing
*
* If endp is ':' or '#' assume a FIT identifier so pass
* along for normal processing.
*
* Right now we assume the first arg should never be '-'
*/
if ((*endp != 0) && (*endp != ':') && (*endp != '#'))
return do_bootm_subcommand(cmdtp, flag, argc, argv);
}
return do_bootm_states(cmdtp, flag, argc, argv, BOOTM_STATE_START |
BOOTM_STATE_FINDOS | BOOTM_STATE_FINDOTHER |
BOOTM_STATE_LOADOS |
#ifdef CONFIG_SYS_BOOT_RAMDISK_HIGH
BOOTM_STATE_RAMDISK |
#endif
#if defined(CONFIG_PPC) || defined(CONFIG_MIPS)
BOOTM_STATE_OS_CMDLINE |
#endif
BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |
BOOTM_STATE_OS_GO, &images, 1);
}
参数
我们来说说这个函数的参数
cmd_tbl_t *cmdtp
:目前笔者也不清楚它的来历,从命名方式中可以看出大约是命令表之类结构体
int flag
:该参数笔者跟踪了一下其传入位置,目前并没有发现需要它的地方
int argc
:不用说了,相信大家都知道这个就是bootm传入参数的个数
char * const argv[]
:同上,这个就是传入的参数了
函数讲解
这里先略过前面的CONFIG_NEEDS_MANUAL_RELOC
宏定义的部分,这里笔者也不甚了解
然后如果在命令有传入参数,则使用simple_strtoul
对参数进行字符串到长整型数据类型的转换,这里解析的是传入的第一个参数,并将其赋值给endp
,其实该参数就是在存储介质中内核的地址,但这个变量似乎并没有传入到函数里面去,仅用作判断。
执行函数do_bootm_subcommand
,这个函数中执行了do_bootm_states
,uboot分阶段启动,每一个阶段称之为subcommand
而do_bootm_states
执行的就是不同阶段的subcommand
在这里我们可以见到,如果没有传入do_bootm
参数,也就是参数argc
为0,那么do_bootm_states
的state
参数将会是一大堆的标志宏,这些标志宏就是uboot启动时需要的阶段,每个阶段都有一个宏来表示
do_bootm_states
我们现在假设没有给bootm
命令传入参数,那么我们现在进入do_bootm_states
函数了
代码如下,有点长,节选部分出来
int do_bootm_states(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[],
int states, bootm_headers_t *images, int boot_progress)
{
boot_os_fn *boot_fn;
ulong iflag = 0;
int ret = 0, need_boot_fn;
images->state |= states;
/*
* Work through the states and see how far we get. We stop on
* any error.
*/
if (states & BOOTM_STATE_START)
ret = bootm_start(cmdtp, flag, argc, argv);
if (!ret && (states & BOOTM_STATE_FINDOS))
ret = bootm_find_os(cmdtp, flag, argc, argv);
if (!ret && (states & BOOTM_STATE_FINDOTHER))
ret = bootm_find_other(cmdtp, flag, argc, argv);
/* Load the OS */
if (!ret && (states & BOOTM_STATE_LOADOS)) {
ulong load_end;
iflag = bootm_disable_interrupts();
ret = bootm_load_os(images, &load_end, 0);
if (ret == 0)
lmb_reserve(&images->lmb, images->os.load,
(load_end - images->os.load));
else if (ret && ret != BOOTM_ERR_OVERLAP)
goto err;
else if (ret == BOOTM_ERR_OVERLAP)
ret = 0;
}
........
/* From now on, we need the OS boot function */
if (ret)
return ret;
boot_fn = bootm_os_get_boot_func(images->os.os);
need_boot_fn = states & (BOOTM_STATE_OS_CMDLINE |
BOOTM_STATE_OS_BD_T | BOOTM_STATE_OS_PREP |
BOOTM_STATE_OS_FAKE_GO | BOOTM_STATE_OS_GO);
if (boot_fn == NULL && need_boot_fn) {
if (iflag)
enable_interrupts();
printf("ERROR: booting os '%s' (%d) is not supported\n",
genimg_get_os_name(images->os.os), images->os.os);
bootstage_error(BOOTSTAGE_ID_CHECK_BOOT_OS);
return 1;
}
/* Call various other states that are not generally used */
if (!ret && (states & BOOTM_STATE_OS_CMDLINE))
ret = boot_fn(BOOTM_STATE_OS_CMDLINE, argc, argv, images);
if (!ret && (states & BOOTM_STATE_OS_BD_T))
ret = boot_fn(BOOTM_STATE_OS_BD_T, argc, argv, images);
if (!ret && (states & BOOTM_STATE_OS_PREP)) {
#if defined(CONFIG_SILENT_CONSOLE) && !defined(CONFIG_SILENT_U_BOOT_ONLY)
if (images->os.os == IH_OS_LINUX)
fixup_silent_linux();
#endif
ret = boot_fn(BOOTM_STATE_OS_PREP, argc, argv, images);
}
#ifdef CONFIG_TRACE
/* Pretend to run the OS, then run a user command */
if (!ret && (states & BOOTM_STATE_OS_FAKE_GO)) {
char *cmd_list = getenv("fakegocmd");
ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_FAKE_GO,
images, boot_fn);
if (!ret && cmd_list)
ret = run_command_list(cmd_list, -1, flag);
}
#endif
/* Check for unsupported subcommand. */
if (ret) {
puts("subcommand not supported\n");
return ret;
}
/* Now run the OS! We hope this doesn't return */
if (!ret && (states & BOOTM_STATE_OS_GO))
ret = boot_selected_os(argc, argv, BOOTM_STATE_OS_GO,
images, boot_fn);
/* Deal with any fallout */
}
参数
cmd_tbl_t *cmdtp
:同上
int flag
:同上
int argc
:同上
char * const argv[]
:同上
int states
:这个参数就是那一大堆的标志宏
bootm_headers_t *images
:这个数据结果就重要了,他传入的是一个全局的结构体,这个结构体用于保存从存储介质中读到的linux内核头部信息,同时这个全局结构体的也被命名为images
int boot_progress
:似乎无作用
函数讲解
那么我们最先看到的是images
的成员被赋值为states
往下看是会将参数states
跟宏BOOTM_STATE_START
进行与操作,如果通过则执行
bootm_start
,那么这里就可以知道上面所说的每个阶段都有一个宏来表示
接下来就会进入bootm_start
这个函数了
1、这里我们先设置一个断点,直接跳到下面看bootm_start
,看完我们再回来
好,我们看完bootm_start
后,接下来往下面,接着执行bootm_find_os
,同样,我们先跳到后面去查看它的函数讲解,等下再回来
2、执行完bootm_find_os
,接着执行bootm_find_other
,这里不细讲,主要是查询是否有ramdisk
3、然后关闭中断,执行bootm_load_os
,我们继续跳到后面去看这个函数
4、跳过ramdisk
的代码,我们直接查看bootm_os_get_boot_func
,这个函数很简单,直接查看boot_os
变量,直接获取我们使用的操作系统的启动函数,uboot为每个不同的操作系统都编写了不同的启动函数。将其返回并赋值给变量boot_fn
。
终于要接近尾声了,继续跳过一些可选代码。我们直接看boot_selected_os
,这函数里面就执行do_bootm_linux
跳转到我们的内核去运行了,如无意外,到了这里一般情况下就不返回了。
这里我们使用的是linux,所以它的启动函数是do_bootm_linux
这里会根据不同的阶段去执行boot_fn
,需要执行的阶段有以下这些
BOOTM_STATE_OS_CMDLINE
BOOTM_STATE_OS_BD_T
BOOTM_STATE_OS_PREP
BOOTM_STATE_OS_GO
5、在这里,我们讲解的是ARM
架构,在这种结构中前2个阶段是不用的,我们跳到文章后面查看do_bootm_linux
的BOOTM_STATE_OS_PREP
和 BOOTM_STATE_OS_GO
实现吧
bootm_start
代码如下:
static int bootm_start(cmd_tbl_t *cmdtp, int flag, int argc,
char * const argv[])
{
memset((void *)&images, 0, sizeof(images));
images.verify = getenv_yesno("verify");
boot_start_lmb(&images);
bootstage_mark_name(BOOTSTAGE_ID_BOOTM_START, "bootm_start");
images.state = BOOTM_STATE_START;
return 0;
}
参数
cmdtp
、flag
、argc
、argv[]
这几个参数相信不用我讲大家也都知道他们是什么了
函数讲解
最先看到的是清空images
结构体,然后获取uboot的环境变量verify
,并复制给images
的成员
然后执行boot_start_lmb
,这个函数看起来想是初始化镜像结构体的lmb
成员,
然后获取环境变量中的某些变量并复制到images->lmb
中,具体其作用目前暂不明白
最后执行bootstage_mark_name
,大致就是记录启动阶段的名字和记录此时的一些数据
好,我们回去刚刚的do_bootm_states
bootm_find_os
代码如下:
static int bootm_find_os(cmd_tbl_t *cmdtp, int flag, int argc,
char * const argv[])
{
const void *os_hdr;
bool ep_found = false;
int ret;
/* get kernel image header, start address and length */
os_hdr = boot_get_kernel(cmdtp, flag, argc, argv,
&images, &images.os.image_start, &images.os.image_len);
if (images.os.image_len == 0) {
puts("ERROR: can't get kernel image!\n");
return 1;
}
/* get image parameters */
switch (genimg_get_format(os_hdr)) {
images.os.type = image_get_type(os_hdr);
images.os.comp = image_get_comp(os_hdr);
images.os.os = image_get_os(os_hdr);
images.os.end = image_get_image_end(os_hdr);
images.os.load = image_get_load(os_hdr);
images.os.arch = image_get_arch(os_hdr);
}
......
if (images.os.type == IH_TYPE_KERNEL_NOLOAD) {
images.os.load = images.os.image_start;
images.ep += images.os.load;
}
images.os.start = map_to_sysmem(os_hdr);
}
参数
同bootm_start
函数讲解
这里需要说一下函数,该函数主要参数有images
,os_data
,os_len
boot_get_kernel
函数先执行genimg_get_kernel_addr_fit
来获取内核镜像在存储介质中的位置,如果没有传入命令参数,则默认使用全局变量load_addr
并返回,该变量由每个硬件平台自己定义宏并赋值,可能这里是移植需要做的工作之一。另外它似乎有扫描多个内核镜像并找到启动镜像的功能,但这里暂且不表。
获取到内核的存储地址后,使用genimg_get_image
读取内核到内存中,这个函数将内核头部的64字节信息和内核全部读到指定地址CONFIG_SYS_LOAD_ADDR
,然后返回内核所在的内存地址。
genimg_get_image
获取头部信息指针后,然后根据头部指针来获取内核的大小和内核目前所在的内存地址os_len
和os_data
,这2个指针指向的其实就是在imges
结构体中的成员。到了这里,boot_get_kernel
执行完毕,我们返回内核头部信息指针
接着从内核头部信息指针中获取内核的格式,格式有传统格式,FIT
格式和安卓格式等,这里我们使用传统格式来讲解。我们回到bootm_find_os
。
根据返回的头部信息指针,我们去获取到内核想信息并复制给images.os
的各个成员,包括内核类型type
,内核压缩方式comp
,内核是什么操作系统os
,内核要装载到内存的哪个位置load
,内核是什么体系架构arch
,为以后的工作做准备,这里要说明一下,现在内核所在的内存地址是uboot所指定,而内核启动的内存地址不一定在这里,是在laod
成员所执行的地址,后面需要把整个镜像拷贝到这里。
最后将images.os.load
赋值给images.ep
,其实就是内核的启动地址了
下面内核头部信息结构体,到了这里,我们就可以返回到do_bootm_states
。
typedef struct image_header {
__be32 ih_magic; /* Image Header Magic Number */
__be32 ih_hcrc; /* Image Header CRC Checksum */
__be32 ih_time; /* Image Creation Timestamp */
__be32 ih_size; /* Image Data Size */
__be32 ih_load; /* Data Load Address */
__be32 ih_ep; /* Entry Point Address */
__be32 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;
bootm_load_os
主要参数
bootm_headers_t *images
:就在这一行字的上边
代码如下:
static int bootm_load_os(bootm_headers_t *images, unsigned long *load_end,
int boot_progress)
{
image_info_t os = images->os;
ulong load = os.load;
ulong blob_start = os.start;
ulong blob_end = os.end;
ulong image_start = os.image_start;
ulong image_len = os.image_len;
bool no_overlap;
void *load_buf, *image_buf;
int err;
load_buf = map_sysmem(load, 0);
image_buf = map_sysmem(os.image_start, image_len);
err = bootm_decomp_image(os.comp, load, os.image_start, os.type,
load_buf, image_buf, image_len,
CONFIG_SYS_BOOTM_LEN, load_end);
if (err) {
bootstage_error(BOOTSTAGE_ID_DECOMP_IMAGE);
return err;
}
flush_cache(load, ALIGN(*load_end - load, ARCH_DMA_MINALIGN));
debug(" kernel loaded at 0x%08lx, end = 0x%08lx\n", load, *load_end);
bootstage_mark(BOOTSTAGE_ID_KERNEL_LOADED);
no_overlap = (os.comp == IH_COMP_NONE && load == image_start);
if (!no_overlap && (load < blob_end) && (*load_end > blob_start)) {
debug("images.os.start = 0x%lX, images.os.end = 0x%lx\n",
blob_start, blob_end);
debug("images.os.load = 0x%lx, load_end = 0x%lx\n", load,
*load_end);
/* Check what type of image this is. */
if (images->legacy_hdr_valid) {
if (image_get_type(&images->legacy_hdr_os_copy)
== IH_TYPE_MULTI)
puts("WARNING: legacy format multi component image overwritten\n");
return BOOTM_ERR_OVERLAP;
} else {
puts("ERROR: new format image overwritten - must RESET the board to recover\n");
bootstage_error(BOOTSTAGE_ID_OVERWRITTEN);
return BOOTM_ERR_RESET;
}
}
return 0;
}
函数讲解
首先调用bootm_decomp_image
,来解压内核。查看其传入参数,我们知道都是从上一步中获取得到的各种数据,包括装载地址,解压类型等等。os.image_start
是内核未解压时所在的地址,load_buf
是内核的启动位置也就是解压后内核所在的地址了。
我们继续返回到do_bootm_states
do_bootm_linux
不同的硬件平台有不同的实现,我们这里查看的ARM
架构的实现代码
代码如下
int do_bootm_linux(int flag, int argc, char * const argv[],
bootm_headers_t *images)
{
/* No need for those on ARM */
if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
return -1;
if (flag & BOOTM_STATE_OS_PREP) {
boot_prep_linux(images);
return 0;
}
if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {
boot_jump_linux(images, flag);
return 0;
}
boot_prep_linux(images);
boot_jump_linux(images, flag);
return 0;
}
boot_prep_linux
首先先执行BOOTM_STATE_OS_PREP
的代码,这里调用的是boot_prep_linux
,这个函数跟内核传递参数有关系,uboot向内核传递参数就是在这里做的准备
这里先调用char *commandline = getenv("bootargs");
从uboot的环境变量中获取到我们传入的启动参数,并
使用指针指向了这串字符串,该字符串在后面会用到
再调用setup_start_tag
设置启动要用到的 tag,在这里有一个全局变量params
,bd->bi_boot_params
的值赋给它,params
的结构如下
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
结构。该结构包括hdr
和各种类型的tag_*
hdr
来标志当前的tag是哪种类型。
setup_start_tag
是初始化了第一个tag
,类型为tag_core
。
最后调用tag_next
跳到第一个tag
末尾,为下一个tag
赋值做准备。
接下来调用setup_serial_tag
,代码如下,功能笔者觉得是设置控制台串口号。其中get_board_serial
是各个硬件平台的实现,其功能大概是获取环境变量中的串口号,将该串口作为控制台输出。
static void setup_serial_tag(struct tag **tmp)
{
struct tag *params = *tmp;
struct tag_serialnr serialnr;
get_board_serial(&serialnr);
params->hdr.tag = ATAG_SERIAL;
params->hdr.size = tag_size (tag_serialnr);
params->u.serialnr.low = serialnr.low;
params->u.serialnr.high= serialnr.high;
params = tag_next (params);
*tmp = params;
}
接着,再调用setup_commandline_tag
,代码如下,可以看出,这里调用了strcpy
来赋值字符串,赋值的字符串正是上面提到的,函数开头使用getenv
获取的启动参数字符串
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);
}
接着下面还调用了setup_revision_tag
、setup_memory_tags
,同理都是设置不同的tag
而已。这里比较特殊的是setup_memory_tags
,如果有多片内存ram,会循环为每一片的ram
设置一个tag
继续调用setup_board_tags
,这个是板级实现,如果没有实现则跳过
最后将最末尾的tag
设置为ATAG_NONE
,标志tag
结束。
由此可知我们的启动参数params
是一片连续的内存,这片内存有很多个tag
,我们通过调用不同的程序来设置这些tag
。
这样整个参数的准备就结束了,最后在调用boot_jump_linux
时会将tags
的首地址也就是bi_boot_params
传给内核,让内核解析这些tag。当然我想也有朋友不懂内核传递的参数都是字符串,设置这些tag
跟传递的参数有什么关系呢,笔者也不明白,等到后面我们再来讲解。
总结一下,uboot将参数以tag数组的形式布局在内存的某一个地址,每个tag代表一种类型的参数,首尾tag标志开始和结束,首地址传给kernel供其解析。
我们回到do_bootm_linux
,其实这行到这里是跳出do_bootm_linux
回到我们的do_bootm_states
boot_jump_linux
kernel_entry
变量是个函数指针,我们会讲images->ep
赋值给它作为跳转到内核执行的入口。寄存器r2
会赋值为gd->bd->bi_boot_params
,就是我们之前所有是tag
启动参数
然后传入其余相关参数并执行kernel_entry
启动内核
到了这里,uboot引导内核的启动过程讲解完毕
后记
总结来说,uboot启动内核就是读取内核,加载内核,解析内核头部,解压内核,装载内核到执行启动地址,准备启动参数,启动内核这几个阶段。
写完该片,笔者对uboot的理解深了一层,当然该文所讲的只是uboot引导的主要部分,还有很多细节我们跳过了(但大体上不影响)。以前笔者仅仅只是使用uboot,并没有对它进行一个系统的理解,今天算是对uboot的有了一些更深的理解。当然了,这些都是理论层面,还需要各位去根据uboot的代码进行实践。实践出真知,要了解uboot后的内核启动,我们还需要对操作系统和编译原理有一定的了解,关于我们后面有机会再继续聊吧
本文参考:
uboot向kernel的传参机制——bootm与tags https://blog.csdn.net/skyflying2012/article/details/35787971
什么是FIT uImage? https://blog.csdn.net/rikeyone/article/details/86594196