前言
之前我们讲过uboot引导了linux内核启动的过程,但对于其中的参数传递我们还没做过多的说明,在这篇文章中,我们将继续上一片文章,继续揭秘uboot传递参数给linux的过程。下面按笔者的理解分为几个阶段向各位阐述
过程讲述
校验阶段
当uboot引导linux启动后,linux将从入口函数进入
入口函数的文件是 arch/arm/kernel/head.S
进入函数,首先是对内核地址进行一个映射,我们先不管,继续往下看。这里挖个坑,以后有机会笔者学习了再来讲解
PS:markdown不支持汇编代码块,这里就先用C语言代码块代替
/*
* swapper_pg_dir is the virtual address of the initial page table.
* We place the page tables 16K below KERNEL_RAM_VADDR. Therefore, we must
* make sure that KERNEL_RAM_VADDR is correctly set. Currently, we expect
* the least significant 16 bits to be 0x8000, but we could probably
* relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0x4000.
*/
#define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
#if (KERNEL_RAM_VADDR & 0xffff) != 0x8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif
#ifdef CONFIG_ARM_LPAE
/* LPAE requires an additional page for the PGD */
#define PG_DIR_SIZE 0x5000
#define PMD_ORDER 3
#else
#define PG_DIR_SIZE 0x4000
#define PMD_ORDER 2
#endif
.globl swapper_pg_dir
.equ swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE
.macro pgtbl, rd, phys
add \rd, \phys, #TEXT_OFFSET
sub \rd, \rd, #PG_DIR_SIZE
.endm
我们直接查看传递参数部分,之前我们讲到参数是通过 tag
来传递的,那么在代码里我们尝试搜索这个关键词,能够找到下面的代码。
PS:参数传递涉及到了ABI规范
,关于ABI规范笔者后面再做一篇文章说明
这里的代码显然个是跳到 __vet_atags
函数
/*
* r1 = machine no, r2 = atags or dtb,
* r8 = phys_offset, r9 = cpuid, r10 = procinfo
*/
bl __vet_atags
函数代码如下,前文提到,r2存放的是参数的地址,所以我们这里主要是对r2寄存器进行处理。我们从注释中尝试理解一下
1、先把r2这个寄存器存放的地址指向的内容的0字节装载到r5寄存器,然后设置r6寄存器为设备树魔数,在比较r5和r6寄存器是否相等,所以这一步是判断参数是否为设备树(如果CONFIG_OF_FLATTREE
宏没打开那么将跳过这一步)
2、接着比较r5和ATAG_CORE_SIZE
,显然这一步就是查看tag
的第一个4字节是否为ATAG_CORE_SIZE
,第二个四字节是否为ATAG_CORE
,这里不清楚tag
结构体的读者请翻阅前文或uboot代码
3、返回到调转的地方
__vet_atags:
tst r2, #0x3 @ aligned?
bne 1f
ldr r5, [r2, #0]
#ifdef CONFIG_OF_FLATTREE
ldr r6, =OF_DT_MAGIC @ is it a DTB?
cmp r5, r6
beq 2f
#endif
cmp r5, #ATAG_CORE_SIZE @ is first tag ATAG_CORE?
cmpne r5, #ATAG_CORE_SIZE_EMPTY
bne 1f
ldr r5, [r2, #4]
ldr r6, =ATAG_CORE
cmp r5, r6
bne 1f
2: ret lr @ atag/dtb pointer is ok
1: mov r2, #0
ret lr
ENDPROC(__vet_atags)
总结一下,校验阶段主要是查看tag
的头部是否正确
赋值阶段
上面的步骤仅仅只是校验tag
是否正确,对于它们的处理是在start_kernel
函数中的setup_arch
setup_arch
的文件是arch/arm/kernel/setup.c
start_kernel
的文件是init/main.c
我们直接上setup_arch
的关键代码,可以从中看出,是在setup_machine_tags
函数进行设置的
const struct machine_desc *mdesc;
setup_processor();
mdesc = setup_machine_fdt(__atags_pointer);
if (!mdesc)
mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
machine_desc = mdesc;
可能有读者异或 __atags_pointer
这个变量的来源,因为它是extern
定义,使用source insight也不能找出它的所在,经过笔者的全局搜索发现它是在
arch/arm/kernel/head-common.S
文件中定义的。代码如下,这段代码有点长,如果不熟悉汇编的读者可以先跳过,跟笔者一起查看关键的代码就行
__mmap_switched:
adr r3, __mmap_switched_data
ldmia r3!, {r4, r5, r6, r7}
cmp r4, r5 @ Copy data segment if needed
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
mov fp, #0 @ Clear BSS (and zero fp)
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ARM( ldmia r3, {r4, r5, r6, r7, sp})
THUMB( ldmia r3, {r4, r5, r6, r7} )
THUMB( ldr sp, [r3, #16] )
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
str r2, [r6] @ Save atags pointer
cmp r7, #0
strne r0, [r7] @ Save control register values
b start_kernel
ENDPROC(__mmap_switched)
.align 2
.type __mmap_switched_data, %object
__mmap_switched_data:
.long __data_loc @ r4
.long _sdata @ r5
.long __bss_start @ r6
.long _end @ r7
.long processor_id @ r4
.long __machine_arch_type @ r5
.long __atags_pointer @ r6
#ifdef CONFIG_CPU_CP15
.long cr_alignment @ r7
#else
.long 0 @ r7
#endif
.long init_thread_union + THREAD_START_SP @ sp
.size __mmap_switched_data, . - __mmap_switched_data
我们把关键的代码抽出来,如下。
首先是用adr
指令将__mmap_switched_data
的相对地址复制个r3寄存器。我们可以看到__mmap_switched_data
标志的内容就是定义了好几个变量,其中就要我们的__atags_pointer
接着使用ldmia
指令,将r3存储地址的内容按顺序手动加到的R4到R7寄存器。这里不细讲汇编指令,有需求的读者自行搜查。那么我们可以看到,随着r3存储的内容递增,我们的__atags_pointer
变量地址会被存储到r6寄存器中。
最后我们就看到了将r2存储的地址放到r6所指向的地址,也就是变量__atags_pointer
到这里我们就完成了__atags_pointer
变量的赋值了
adr r3, __mmap_switched_data
ldmia r3!, {r4, r5, r6, r7}
...
str r1, [r5] @ Save machine type
str r2, [r6] @ Save atags pointer
...
__mmap_switched_data:
.long __data_loc @ r4
.long _sdata @ r5
.long __bss_start @ r6
.long _end @ r7
.long processor_id @ r4
.long __machine_arch_type @ r5
.long __atags_pointer @ r6
总结一下,赋值阶段主要将uboot传递过来的tag
参数的地址赋值给变量__atags_pointer
tag解析阶段
回到之前的 setup_arch
,直接查看函数setup_machine_tags
的关键内容
这里面会将__atags_pointer
从屋里地址转换为虚拟地址,并赋值给tags
变量,然后做一些常规检查。
通过检查后调用save_atags
函数来保存我们的tag
,将我们的tag
保存到全局变量atags_copy
然后调用parse_tags
,这里面就是循环对每个tag
调用parse_tag
,少了个s哈,各位读者注意
在parse_tag
中,判断tag
是否在__tagtable_begin
到__tagtable_end
的地址之间,如果是则调用他们自身的解析函数。我们继续往下翻阅
const struct machine_desc * __init
setup_machine_tags(phys_addr_t __atags_pointer, unsigned int machine_nr)
{
struct tag *tags = (struct tag *)&default_tags;
const struct machine_desc *mdesc = NULL, *p;
char *from = default_command_line;
...
if (__atags_pointer)
tags = phys_to_virt(__atags_pointer);
else if (mdesc->atag_offset)
tags = (void *)(PAGE_OFFSET + mdesc->atag_offset);
...
if (tags->hdr.tag != ATAG_CORE) {
early_print("Warning: Neither atags nor dtb found\n");
tags = (struct tag *)&default_tags;
...
if (tags->hdr.tag == ATAG_CORE) {
...
save_atags(tags);
parse_tags(tags);
}
/* parse_early_param needs a boot_command_line */
strlcpy(boot_command_line, from, COMMAND_LINE_SIZE);
return mdesc;
}
void __init save_atags(const struct tag *tags)
{
memcpy(atags_copy, tags, sizeof(atags_copy));
}
static void __init parse_tags(const struct tag *t)
{
for (; t->hdr.size; t = tag_next(t))
if (!parse_tag(t))
pr_warn("Ignoring unrecognised tag 0x%08x\n",
t->hdr.tag);
}
static int __init parse_tag(const struct tag *tag)
{
extern struct tagtable __tagtable_begin, __tagtable_end;
struct tagtable *t;
for (t = &__tagtable_begin; t < &__tagtable_end; t++)
if (tag->hdr.tag == t->tag) {
t->parse(tag);
break;
}
return t < &__tagtable_end;
}
在parse_tag
函数中,是定义了变量t
,然后将__tagtable_begin
的地址赋值给t,对每个t
和tag
进行比较,如果是同个类型的,则调用t
的解析函数来解析tag
那么这里读者们可能有2个疑惑
1、每个t
都有自己的解析函数parse(tag)
,那么他们是在哪里定义的呢?
2、__tagtable_begin
和__tagtable_end
从哪里来的呢?
我们先从第二个问题入手
这里涉及到一点链接器脚本的知识,笔者全局搜索后发现这2个文件arch/arm/kernel/vmlinux.lds.S:
中定义的,这就是linux的链接器脚本
这里的意思就是将__tagtable_begin
和__tagtable_end
定义为2个地址,这里的地址由链接器根据链接脚本来指定的,而他们2者的其他作用我们在下面会讲到
.init.tagtable : {
__tagtable_begin = .;
*(.taglist.init)
__tagtable_end = .;
}
抱着上面的疑惑我们继续,现在我们看看第一个问题的答案在哪里。
我们查看arch/arm/kernel/atags_parse.c
这个文件,找到一个宏叫
__tagtable
,我们看一下它的定义
很明显,这里用到了__attribute__
的__used__
和__section__
其中__used__
的作用简单来说就是让编译器在编译阶段忽略 unused警告
另外的__attribute__((__section__(".taglist.init")))
才是我们要讲的
,它的作用就是在编译阶段,让带有这个这个属性的变量连接到.taglist.init
指定的地址段,查看我们前面的链接器脚本,这个地址段就是__tagtable_begin
和__tagtable_end
之间。
#define __used __attribute__((__used__))
#define __tag __used __attribute__((__section__(".taglist.init")))
#define __tagtable(tag, fn) \
static const struct tagtable __tagtable_##fn __tag = { tag, fn }
我们可以在这个文件中找到下面这些宏,结合我们上面讲的,它其实就是定义一个struct tagtable
类型的变量,这变量的地址在__tagtable_begin
和__tagtable_end
之间,然后他们的tag
成员由第一个参数指定,而第二个参数fn
就是我们的解析函数了!!
__tagtable(ATAG_SERIAL, parse_tag_serialnr);
...
__tagtable(ATAG_REVISION, parse_tag_revision);
...
__tagtable(ATAG_CMDLINE, parse_tag_cmdline);
总结一下,首先在链接器脚本中定义我们tag
的存储区间,然后在代码文件中定义结构体struct tagtable
并让这些结构体位于该存储区间,最后遍历该区间的结构体,将uboot传进来的tag
和linuxe内核的结构体struct tagtable
进行对比,如果相同则调用对应的解析函数
那么我们直接看下面的解析传递参数的代码,很容易从代码中看出,就是讲uboot传递进来的tag
中的cmdling
成员中的赋值到全局变量default_command_line
static int __init parse_tag_cmdline(const struct tag *tag)
{
#if defined(CONFIG_CMDLINE_EXTEND)
strlcat(default_command_line, " ", COMMAND_LINE_SIZE);
strlcat(default_command_line, tag->u.cmdline.cmdline,
COMMAND_LINE_SIZE);
#elif defined(CONFIG_CMDLINE_FORCE)
pr_warn("Ignoring tag cmdline (using the default kernel command line)\n");
#else
strlcpy(default_command_line, tag->u.cmdline.cmdline,
COMMAND_LINE_SIZE);
#endif
return 0;
}
然后我们返回setup_machine_tags
函数,在这个函数有下面的代码。
结合上述的,其实这里就已经将参数命令行从uboot中拷贝到了boot_command_line
char *from = default_command_line;
...
strlcpy(boot_command_line, from, COMMAND_LINE_SIZE);
那么到了这里,就完成了从uboot到linux内核的传递了
解析阶段
再回到setup_arch
函数,这里有个函数parse_early_param
,就是对uboot传递进来的参数进行解析了,这里就是对参数进行早期的解析了。下面的代码笔者就不细讲,有兴趣的读者可以自行翻阅linux代码。
void __init parse_early_param(void)
{
static int done __initdata;
static char tmp_cmdline[COMMAND_LINE_SIZE] __initdata;
if (done)
return;
/* All fall through to do_early_param. */
strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
parse_early_options(tmp_cmdline);
done = 1;
}
后记
在翻阅Linux的代码过程中,笔者使用了source insgiht,但这款强大的工具依旧无法满足查看linux代码的要求。因为linux内核有些都不能在代码文件中找到。比如这话篇文章中,我们需要去查看链接器脚本,有些需要查看汇编代码等等。所以翻阅linux代码,我们还需要使用全局搜索的工具,这样才有助于我们学习Linux的代码