当前位置:首页 > > 充电吧
[导读]我们从u-boot启动内核可知道,uboot通过这条命令theKernel (0, bd->bi_arch_number, bd->bi_boot_params);来启动内核。那么我们可以

我们从u-boot启动内核可知道,uboot通过这条命令theKernel (0, bd->bi_arch_number, bd->bi_boot_params);来启动内核。

那么我们可以内核启动第一步肯定是处理u-boot传入的参数(机器ID、启动参数),再通过一系列的步骤达到最终目的:挂接根文件系统来运行应用程序

我们来看一下整体流程图:


1.内核引导阶段 启动文件head.S和head-common.S 

.section ".text.head", "ax"
    .type   stext, %function
ENTRY(stext)
    msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
                        @ and irqs disabled
    mrc p15, 0, r9, c0, c0      @ get processor id
    bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
    movs    r10, r5             @ invalid processor (r5=0)?
    beq __error_p           @ yes, error 'p'
    bl  __lookup_machine_type       @ r5=machinfo
    movs    r8, r5              @ invalid machine (r5=0)?
    beq __error_a           @ yes, error 'a'
    bl  __create_page_tables

    ldr r13, __switch_data      @ address to jump to after
                        @ mmu has been enabled
    adr lr, __enable_mmu        @ return (PIC) address
    add pc, r10, #PROCINFO_INITFUNC

首先看这段汇编代码,它主要是用来做一些内核启动前的检测:
 __lookup_processor_type 检测内核是否支持当前CPU、__lookup_machine_type检测是否支持当前单板,并且__create_page_tables创建页表,__enable_mmu使能MMU。
 这里我们首先打开__lookup_machine_type。

.long   __proc_info_begin
    .long   __proc_info_end
3:  .long   .
    .long   __arch_info_begin
    .long   __arch_info_end
/*
 * Lookup machine architecture in the linker-build list of architectures.
 * Note that we can't use the absolute addresses for the __arch_info
 * lists since we aren't running with the MMU on (and therefore, we are
 * not in the correct address space).  We have to calculate the offset.
 *
 *  r1 = machine architecture number
 * Returns:
 *  r3, r4, r6 corrupted
 *  r5 = mach_info pointer in physical address space
 */
    .type   __lookup_machine_type, %function
__lookup_machine_type:
    adr r3, 3b              @ r3 = address of 3,real address,phy address
    ldmia   r3, {r4, r5, r6}@ r4 = "." virtual address of 3,r5 = __arch_info_begin,r6 =     __arch_info_end
    sub r3, r3, r4          @ get offset between virt&phys
    add r5, r5, r3          @ convert virt addresses to
    add r6, r6, r3          @ physical address space
    /*
    __arch_info_begin = .;
     *(.arch.info.init)
    __arch_info_end = .;
    */

1:  ldr r3, [r5, #MACHINFO_TYPE]    @ get machine type
    teq r3, r1              @ matches loader number?
    beq 2f              @ found
    add r5, r5, #SIZEOF_MACHINE_DESC    @ next machine_desc
    cmp r5, r6
    blo 1b
    mov r5, #0              @ unknown machine
2:  mov pc, lr

我们在archarmkernel找到__lookup_machine_type被定义在head-common.S文件中。
 开始分析代码:
 首先,读出3物理地址给r3。
 然后用ldmia指令将r3对应的3条指令的虚拟地址分别存入r4,r5,r6。
 所以现在r4=. ; r5=__arch_info_begin ; r6=__arch_info_end
 然后用r3-r4求出物理地址和虚拟地址的偏移值,再利用这个偏移值求出r5和r6的实际物理地址。
 其中__arch_info_begin__arch_info_end定义在内核目录archarmkernel下vmlinux.lds文件中,
 经过起始虚拟地址= (0xc0000000) + 0x00008000逐层叠加得到。

SECTIONS
{



 . = (0xc0000000) + 0x00008000;

 .text.head : {
  _stext = .;
  _sinittext = .;
  *(.text.head)
 }

 .init : { /* Init code and data        */
   *(.init.text)
  _einittext = .;
  __proc_info_begin = .;
   *(.proc.info.init)
  __proc_info_end = .;
  __arch_info_begin = .;//
   *(.arch.info.init)
  __arch_info_end = .;//

这里的__arch_info_begin和__arch_info_end中间存放的是段属性为.arch.info.init的结构体。
 这里我们可以直接在linux下查询内核中包含.arch.info.init的文件。

/*************Direction:include/asm-arm/arch.h********************/
#define MACHINE_START(_type,_name)          
static const struct machine_desc __mach_desc_##_type    
 __used                         
 __attribute__((__section__(".arch.info.init"))) = {    
    .nr     = MACH_TYPE_##_type,        
    .name       = _name,

#define MACHINE_END             
};

/*************Direction:arch/arm/mach-s3c2440/mach-smdk2440.c******************/
MACHINE_START(S3C2440, "SMDK2440")
    /* Maintainer: Ben Dooks*/
    .phys_io    = S3C2410_PA_UART,
    .io_pg_offst    = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
    .boot_params    = S3C2410_SDRAM_PA + 0x100,

    .init_irq   = s3c24xx_init_irq,
    .map_io     = smdk2440_map_io,
    .init_machine   = smdk2440_machine_init,
    .timer      = &s3c24xx_timer,
MACHINE_END

如上:在include/asm-arm/arch.h中找到了定义的结构体类型machine_desc,并且在代码中它的段属性被强制定义成了.arch.info.init。
 这样做的目的是在刚刚我们看到的vmlinux.lds链接脚本文件中,可以将具有.arch.info.init段属性的结构体统一放在__arch_info_begin和__arch_info_end之间。
 非常便于处理。那么现在我们将这个结构体展开,看看它的内容。也就是将arch/arm/mach-s3c2440/mach-smdk2440.c中的参数传入。展开后如下:

static const struct machine_desc __mach_desc_S3C2440    
 __used                         
 __attribute__((__section__(".arch.info.init"))) = {    
    .nr     = MACH_TYPE_S3C2440,        
    .name       = "SMDK2440",
    /* Maintainer: Ben Dooks*/
    .phys_io    = S3C2410_PA_UART,
    .io_pg_offst    = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
    .boot_params    = S3C2410_SDRAM_PA + 0x100,    //0x30000100

    .init_irq   = s3c24xx_init_irq,
    .map_io     = smdk2440_map_io,
    .init_machine   = smdk2440_machine_init,
    .timer      = &s3c24xx_timer,
};

现在我们看到,定义的结构体类型machine_desc,内容为.nr到.timer。
 我们可以看出这个结构体大概是存储硬件信息。
 nr存放机器ID,name存放单板名称,phys_io存放输入输出口,io_pg_offst存放IO的偏移地址,boot_params存放uboot传给内核的启动参数(TAG),init_irq存放的是中断初始化信息,map_io为IO的映射表,init_machine存放的是单板的初始化信息,timer存放的是单板的定时器信息。

struct machine_desc {
    /*
     * Note! The first four elements are used
     * by assembler code in head-armv.S
     */
    unsigned int        nr;     /* architecture number  */
    unsigned int        phys_io;    /* start of physical io */
    unsigned int        io_pg_offst;    /* byte offset for io 
                         * page tabe entry  */

    const char      *name;      /* architecture name    */
    unsigned long       boot_params;    /* tagged list      */

    unsigned int        video_start;    /* start of video RAM   */
    unsigned int        video_end;  /* end of video RAM */

    unsigned int        reserve_lp0 :1; /* never has lp0    */
    unsigned int        reserve_lp1 :1; /* never has lp1    */
    unsigned int        reserve_lp2 :1; /* never has lp2    */
    unsigned int        soft_reboot :1; /* soft reboot      */
    void            (*fixup)(struct machine_desc *,
                     struct tag *, char **,
                     struct meminfo *);
    void            (*map_io)(void);/* IO mapping function  */
    void            (*init_irq)(void);
    struct sys_timer    *timer;     /* system tick timer    */
    void            (*init_machine)(void);
};

我们打开arch.h文件,看到对machine_desc结构体的定义确实和我们刚刚所说的一样。
 到此处理u-boot传来的机器ID结束。再回到head-common.S文件,这里对mmap_switch定义:

.type   __switch_data, %object
__switch_data:
    .long   __mmap_switched
    .long   __data_loc          @ r4
    .long   __data_start            @ r5
    .long   __bss_start         @ r6
    .long   _end                @ r7
    .long   processor_id            @ r4
    .long   __machine_arch_type     @ r5
    .long   cr_alignment            @ r6
    .long   init_thread_union + THREAD_START_SP @ sp

/*
 * The following fragment of code is executed with the MMU on in MMU mode,
 * and uses absolute addresses; this is not position independent.
 *
 *  r0  = cp#15 control register
 *  r1  = machine ID
 *  r9  = processor ID
 */
    .type   __mmap_switched, %function
__mmap_switched:
    adr r3, __switch_data + 4

    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

    ldmia   r3, {r4, r5, r6, sp}
    str r9, [r4]            @ Save processor ID
    str r1, [r5]            @ Save machine type
    bic r4, r0, #CR_A           @ Clear 'A' bit
    stmia   r6, {r0, r4}            @ Save control register values
    b   start_kernel

mmap_switch做了很多工作,这里我们看到有复制数据段,清BSS段,保存CPU的ID,保存机器ID,清‘A’位,保存控制寄存器的值,然后就到了C语言段——start_kernel函数。

2.内核启动的第二阶段 C语言段—start_kernel

asmlinkage void __init start_kernel(void)
{
    local_irq_disable();
    early_boot_irqs_off();
    early_init_irq_lock_class();

/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
    lock_kernel();
    tick_init();
    boot_cpu_init();
    page_address_init();
    printk(KERN_NOTICE);
    printk(linux_banner);
    setup_arch(&command_line);
    setup_command_line(command_line);
    printk(KERN_NOTICE "Kernel command line: %sn", boot_command_line);
    parse_early_param();
    parse_args("Booting kernel", static_command_line, __start___param,
           __stop___param - __start___param,
           &unknown_bootoption);
    init_IRQ();
    profile_init();
    if (!irqs_disabled())
        printk("start_kernel(): bug: interrupts were enabled earlyn");
    early_boot_irqs_on();
    local_irq_enable();
    console_init();

    rest_init();
}

接下来进入start_kernel启动内核的C函数。
 上面是start_kernel的部分代码。
 这部分代码的主要作用是处理uboot传递来的参数,设置与体系结构相关的环境,初始化控制台,最后执行应用程序,实现功能。
 start_kernel函数框架如下。

内核启动流程:
arch/arm/kernel/head.S
start_kernel
    setup_arch(&command_line)           //  解析Uboot传入的启动参数
    setup_command_line(command_line)    //  解析Uboot传入的启动参数
    parse_early_param
        do_early_param
            从__setup_start到__setup_end,调用early函数
    unknown_bootoption
        obsolete_checksetup
            从__setup_start到__setup_end,调用非early函数
    rest_init
        kernel_init
            prepare_namespace
                mount_root      //挂接根文件系统
            init_post
                //执行应用程序
这里每一个退格(TAB)都代表此函数被上一个函数调用(例如obsolete_checksetup是unknown_bootoption调用的函数)。  setup_arch(&command_line)和setup_command_line(command_line)就是用来处理uboot传递进来的启动参数的(处理TAG)。  do_early_param从__setup_start到 __setup_end,调用用early标识的函数(但因为__setup_param(str, fn, fn, 0)中early赋值为0,所以不在这里调用),所以我们主要用obsolete_checksetup,后面会提及。  obsolete_checksetup从__setup_start到 __setup_end,调用用非early标识的函数。  mount_root是挂载根文件系统,因为Linux上的应用程序最终要在根文件系统上运行。最后是init_post中运行应用程序。
 那么现在就有一个问题,Linux内核是如何接收uboot传来的根文件系统信息的呢?
bootcmd=nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0
bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0

上面是uboot启动时打印的环境变量。其中我们能够看到根文件系统挂载到第4个分区:root=/dev/mtdblock3 (从0分区开始)。
 上面我们提到过,setup_arch(&command_line)和setup_command_line(command_line)就是用来处理uboot传递进来的启动参数的(处理TAG)。但这个处理只是简单的复制粘贴而已,这两个函数将TAG保存,但并未进行真正的处理。那么真正告诉内核在哪里挂载的函数是什么呢?
 我们通过查看rest_init->kernel_init->prepare_namespace可以看到一个saved_root_name。查找saved_root_name,发现在Do_mounts.c文件中有对它的调用:

static int __init root_dev_setup(char *line)
{
    strlcpy(saved_root_name, line, sizeof(saved_root_name));
    return 1;
}

__setup("root=", root_dev_setup);//传入一个字符串,一个函数

根据我们之前的经验,我们可以猜测这个__setup宏,也是定义了一个结构体。通过查找__setup我们找到了它的宏定义:

Dir:init.h
#define __setup(str, fn)                    
    __setup_param(str, fn, fn, 0)


#define __setup_param(str, unique_id, fn, early)            
    static char __setup_str_##unique_id[] __initdata = str;    
    static struct obs_kernel_param __setup_##unique_id    
        __attribute_used__                
        __attribute__((__section__(".init.setup")))    
        __attribute__((aligned((sizeof(long)))))    
        = { __setup_str_##unique_id, fn, early }

在init.h文件里,定义__setup等于__setup_param。那么在__setup_param的宏定义里,我们可以知道:
 它先定义了一个字符串,然后定义了一个结构体类型obs_kernel_param __setup。
 这个结构体的段属性为.init.setup,内容为一个字符串,一个函数,还有early。
 具备这个属性的结构体被链接脚本文件放到一起,从__setup_start到 __setup_end搜索调用。

在vmlinux.lds中

__setup_start = .;
   *(.init.setup)
  __setup_end = .;

但是在Flash里没有分区,只能和uboot一样,将分区在代码里写死。一般在启动Linux的时候,Linux会自动打印出分区的信息。这里我的分区是这样的:

Creating 4 MTD partitions on "NAND 256MiB 3,3V 8-bit":
0x00000000-0x00040000 : "bootloader"
0x00040000-0x00060000 : "params"
0x00060000-0x00260000 : "kernel"
0x00260000-0x10000000 : "root"

我们搜索这个分区名grep ""bootloader"" * -nR.在arch/arm/plat-s3c24xx/Common-smdk.c中找到分区代码:

static struct mtd_partition smdk_default_nand_part[] = {
    [0] = {
        .name   = "bootloader",
        .size   = 0x00040000,
        .offset    = 0,
    },
    [1] = {
        .name   = "params",
        .offset = MTDPART_OFS_APPEND,
        .size   = 0x00020000,
    },
    [2] = {
        .name   = "kernel",
        .offset = MTDPART_OFS_APPEND,
        .size   = 0x00200000,
    },
    [3] = {
        .name   = "root",
        .offset = MTDPART_OFS_APPEND,
        .size   = MTDPART_SIZ_FULL,
    }
};

至此,处理完uboot传递的参数,进行CPU和单板的校验,挂载根文件系统等一系列操作后,最终内核执行init_post()中的应用程序。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: 驱动电源

在工业自动化蓬勃发展的当下,工业电机作为核心动力设备,其驱动电源的性能直接关系到整个系统的稳定性和可靠性。其中,反电动势抑制与过流保护是驱动电源设计中至关重要的两个环节,集成化方案的设计成为提升电机驱动性能的关键。

关键字: 工业电机 驱动电源

LED 驱动电源作为 LED 照明系统的 “心脏”,其稳定性直接决定了整个照明设备的使用寿命。然而,在实际应用中,LED 驱动电源易损坏的问题却十分常见,不仅增加了维护成本,还影响了用户体验。要解决这一问题,需从设计、生...

关键字: 驱动电源 照明系统 散热

根据LED驱动电源的公式,电感内电流波动大小和电感值成反比,输出纹波和输出电容值成反比。所以加大电感值和输出电容值可以减小纹波。

关键字: LED 设计 驱动电源

电动汽车(EV)作为新能源汽车的重要代表,正逐渐成为全球汽车产业的重要发展方向。电动汽车的核心技术之一是电机驱动控制系统,而绝缘栅双极型晶体管(IGBT)作为电机驱动系统中的关键元件,其性能直接影响到电动汽车的动力性能和...

关键字: 电动汽车 新能源 驱动电源

在现代城市建设中,街道及停车场照明作为基础设施的重要组成部分,其质量和效率直接关系到城市的公共安全、居民生活质量和能源利用效率。随着科技的进步,高亮度白光发光二极管(LED)因其独特的优势逐渐取代传统光源,成为大功率区域...

关键字: 发光二极管 驱动电源 LED

LED通用照明设计工程师会遇到许多挑战,如功率密度、功率因数校正(PFC)、空间受限和可靠性等。

关键字: LED 驱动电源 功率因数校正

在LED照明技术日益普及的今天,LED驱动电源的电磁干扰(EMI)问题成为了一个不可忽视的挑战。电磁干扰不仅会影响LED灯具的正常工作,还可能对周围电子设备造成不利影响,甚至引发系统故障。因此,采取有效的硬件措施来解决L...

关键字: LED照明技术 电磁干扰 驱动电源

开关电源具有效率高的特性,而且开关电源的变压器体积比串联稳压型电源的要小得多,电源电路比较整洁,整机重量也有所下降,所以,现在的LED驱动电源

关键字: LED 驱动电源 开关电源

LED驱动电源是把电源供应转换为特定的电压电流以驱动LED发光的电压转换器,通常情况下:LED驱动电源的输入包括高压工频交流(即市电)、低压直流、高压直流、低压高频交流(如电子变压器的输出)等。

关键字: LED 隧道灯 驱动电源
关闭