uboot和linux的启动参数,uboot传递参数至linux内核

前言

之前我们讲过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的代码

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值