美文网首页
uboot传递参数至linux内核

uboot传递参数至linux内核

作者: wipping的技术小栈 | 来源:发表于2019-05-03 14:16 被阅读0次

    前言

    之前我们讲过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内核

          本文链接:https://www.haomeiwen.com/subject/emaanqtx.html