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,对每个ttag进行比较,如果是同个类型的,则调用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的代码

你可能感兴趣的:(uboot传递参数至linux内核)