uboot启动流程详解(5)-_main

前言

  _main标号中主要调用的函数有三个,board_init_f,relocate_code,board_init_r,这里先贴出_main的代码并注释,然后对这三个函数的流程及原理进行详细介绍。

1、代码注释

ENTRY(_main)

/*
 * Set up initial C runtime environment and call board_init_f(0).
 */
/*
*这里首先为调用board_init_f准备一个临时堆栈,CONFIG_SYS_INIT_SP_ADDR这个宏
*就是cpu片上内存的高地址(片上内存的大小减去GD_SIZE)。然后将堆栈初始的地址保存在
*r9,所以r9就是gd的起始地址,后面需要靠r9访问gd中的成员。然后将r0赋值成0,r0就是
*要调用的board_init_f函数的第一个参数!
*/
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
    ldr sp, =(CONFIG_SPL_STACK)
#else
    ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
    bic sp, sp, #7  /* 8-byte alignment for ABI compliance */
    sub sp, sp, #GD_SIZE    /* allocate one GD above SP */
    bic sp, sp, #7  /* 8-byte alignment for ABI compliance */
    mov r9, sp      /* GD is above SP */
    mov r0, #0
    bl  board_init_f

#if ! defined(CONFIG_SPL_BUILD)

/*
 * Set up intermediate environment (new sp and gd) and call
 * relocate_code(addr_moni). Trick here is that we'll return
 * 'here' but relocated.
 */

/*
*这段代码的主要功能就是将uboot搬移到内存的高地址去执行,为kernel腾出低端空间,
*防止kernel解压覆盖uboot。
*adr这个指令非常有意识,可以自己问度娘了解以下。这里这三行代码
*adr    lr, here
*ldr    r0, [r9, #GD_RELOC_OFF]
*add    lr, lr, r0
*的功能就是,将relocate后的here标号的地址保存到lr寄存器,这样等到relocate
*完成后,就可以直接跳到relocate后的here标号去执行了。
*relocate_code函数的原理及流程,是uboot的重要代码,下面详解!
*/
    ldr sp, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
    bic sp, sp, #7  /* 8-byte alignment for ABI compliance */
    ldr r9, [r9, #GD_BD]        /* r9 = gd->bd */
    sub r9, r9, #GD_SIZE        /* new GD is below bd */

    adr lr, here
    ldr r0, [r9, #GD_RELOC_OFF]     /* r0 = gd->reloc_off */
    add lr, lr, r0
    ldr r0, [r9, #GD_RELOCADDR]     /* r0 = gd->relocaddr */
    b   relocate_code
here:

/* Set up final (full) environment */
     /*
     *relocate完成后,uboot的代码被搬到了内存的顶部,所以必须重新设置异常向量表的
     *地址,c_runtime_cpu_setup这个函数的主要功能就是重新设置异常向量表的地址。
     */
    bl  c_runtime_cpu_setup /* we still call old routine here */
     
     /*
     *在relocate的过程中,并没有去搬移bss段。bss段是auto-relocated的!为什么?
     *可以自己思考一下,又或许看完我后面介绍的relocate的原理后你会明白!
     */
    ldr r0, =__bss_start    /* this is auto-relocated! */
    ldr r1, =__bss_end      /* this is auto-relocated! */

    mov r2, #0x00000000     /* prepare zero to clear BSS */
     
     /*
     *清空bss段。
     */
clbss_l:cmp r0, r1          /* while not at end of BSS */
    strlo   r2, [r0]        /* clear 32-bit BSS word */
    addlo   r0, r0, #4      /* move to next */
    blo clbss_l
     
     /*
     *这两行代码无视之,点灯什么的,和这里要讲的uboot的原理及过程没有半毛钱关系。
     */
    bl coloured_LED_init
    bl red_led_on
     /*
     *将relocate后的gd的地址保存到r1,然后调用board_init_r函数,进入uboot的新天地!
     */
    /* call board_init_r(gd_t *id, ulong dest_addr) */
    mov     r0, r9                  /* gd_t */
    ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
    /* call board_init_r */
    ldr pc, =board_init_r   /* this is auto-relocated! */

    /* we should not return here. */

#endif

ENDPROC(_main)

2、board_init_f

  这个函数的主要功能就是初始化一些硬件设备(串口、定时器等)并且设置gd结构体中的成员。执行完后,gd中一些重要成员的指向如下图所示:
  这里写图片描述
  刚开始uboot在编译地址TEXT_BASE处执行,relocate完成后,uboot会被搬移到gd->relocaddr这个地址执行。gd->start_addr_sp是relocate完成后新的堆栈起始地址。接下来对其调用到的主要函数进行介绍。
  
  1、获取整个uboot的大小到gd->mon_len

static int setup_mon_len(void)
{
#ifdef __ARM__
    gd->mon_len = (ulong)&__bss_end - (ulong)_start;
#elif defined(CONFIG_SANDBOX)
    gd->mon_len = (ulong)&_end - (ulong)_init;
#else
    /* TODO: use (ulong)&__bss_end - (ulong)&__text_start; ? */
    gd->mon_len = (ulong)&__bss_end - CONFIG_SYS_MONITOR_BASE;
#endif
    return 0;
}

  __bss_end ,_start这两个标号在链接脚本u-boot.lds中定义。__bss_end -_start uboot就是整个uboot的大小。如果对链接脚本不了解,可以参考《编译及连接过程 》
  
  2、获取环境变量的起始地址到gd->env_addr

int env_init(void)
{
    /* use default */
    gd->env_addr = (ulong)&default_environment[0];
    gd->env_valid = 1;

    return 0;
}

  default_environment是全局初始化变量,保存在uboot的data段中!

  3、设置gd->baudrate
  从环境变量中获取波特率到gd->baudrate成员中。

static int init_baud_rate(void)
{
    gd->baudrate = getenv_ulong("baudrate", 10, CONFIG_BAUDRATE);
    return 0;
}

  
  4、获取内存的实际大小到gd->ram_size

int dram_init(void)
{
    gd->ram_size = get_ram_size((void *)PHYS_SDRAM, PHYS_SDRAM_SIZE);

    return 0;
}

  5、获取gd->relocaddr
  重新设置内存大小,预留出内存顶部的4K不用。
  

static int setup_dest_addr(void)
{
    debug("Monitor len: %08lX\n", gd->mon_len);
    /*
     * Ram is setup, size stored in gd !!
     */
    debug("Ram size: %08lX\n", (ulong)gd->ram_size);
#if defined(CONFIG_SYS_MEM_TOP_HIDE)
    /*
     * Subtract specified amount of memory to hide so that it won't
     * get "touched" at all by U-Boot. By fixing up gd->ram_size
     * the Linux kernel should now get passed the now "corrected"
     * memory size and won't touch it either. This should work
     * for arch/ppc and arch/powerpc. Only Linux board ports in
     * arch/powerpc with bootwrapper support, that recalculate the
     * memory size from the SDRAM controller setup will have to
     * get fixed.
     */
    gd->ram_size -= CONFIG_SYS_MEM_TOP_HIDE;
#endif
#ifdef CONFIG_SYS_SDRAM_BASE
    gd->ram_top = CONFIG_SYS_SDRAM_BASE;
#endif
    gd->ram_top += get_effective_memsize();
    gd->ram_top = board_get_usable_ram_top(gd->mon_len);
    gd->relocaddr = gd->ram_top;
    debug("Ram top: %08lX\n", (ulong)gd->ram_top);
#if defined(CONFIG_MP) && (defined(CONFIG_MPC86xx) || defined(CONFIG_E500))
    /*
     * We need to make sure the location we intend to put secondary core
     * boot code is reserved and not used by any part of u-boot
     */
    if (gd->relocaddr > determine_mp_bootpg(NULL)) {
        gd->relocaddr = determine_mp_bootpg(NULL);
        debug("Reserving MP boot page to %08lx\n", gd->relocaddr);
    }
#endif
    return 0;
}

  6、设置gd->reloc_off
  设置relocate的偏移量,并将旧gd中的内容拷贝到新gd中。

static int setup_reloc(void)
{
    gd->reloc_off = gd->relocaddr - CONFIG_SYS_TEXT_BASE;
    memcpy(gd->new_gd, (char *)gd, sizeof(gd_t));

    debug("Relocation Offset is: %08lx\n", gd->reloc_off);
    debug("Relocating to %08lx, new gd at %08lx, sp at %08lx\n",
          gd->relocaddr, (ulong)map_to_sysmem(gd->new_gd),
          gd->start_addr_sp);

    return 0;
}

3、relocate_code

3.1 原理介绍

  u-boot在启动过程中,会把自己拷贝到RAM的顶端去执行。这一拷贝带来的问题是执行地址的混乱。代码的执行地址通常都是在编译时有链接地址指定的,如何保证拷贝前后都可以执行呢?
  一个办法是使用拷贝到RAM后的地址作为编译时的链接地址,拷贝前所有函数与全局变量的调用都增加偏移量。(如VxWorks的bootloader)尽量减少拷贝前需要执行的代码量。
  另一个办法是把image编译成与地址无关的程序,也就是PIC - Position independent code。编译器无法保证代码的独立性,它需要与加载器配合起来。U-boot自己加载自己,所以它自己就是加载器。PIC依赖于下面两种技术:
1) 使用相对地址
2) 加载器可以自动更新涉及到绝对地址的指令
  对于PowerPC架构,u-boot只是在编译时使用了-fpic,这种方式会生成一个.got段来存储绝对地址符号。对于ARM架构,则是在编译时使用-mword-relocations,生成与位置无关代码,链接时使用-pie生成.rel.dyn段,该段中的每个条目被称为一个LABEL,用来存储绝对地址符号的地址。

3.2 调试过程

  为了理解rel.dyn段的作用,在uboot源代码的common/main.c文件中,加入如下代码,并在main_loop函数中调用rel_dyn_test。

void test_func(void)  
{  
    printf("test func\n");  
}  

static void * test_func_val = test_func;  
static int test_val = 10;   

void rel_dyn_test()  
{  
    test_val = 20; 
    test_func(); 

    printf("test = 0x%x\n", test_func);  
    printf("test_func = 0x%x\n", test_func_val);       
} 

  然后重新编译uboot,并且对新生成的elf文件(u-boot)进行反汇编:
arm-fsl-linux-gnueabi-objdump -D u-boot > u-boot.huibian。然后在反汇编文件中找到对应的代码段、数据段以及rel.dyn段。如下:
代码段:

/*这里是test_func()函数的反汇编*/
87802fe0 <test_func>:
87802fe0:   e59f300c    ldr r3, [pc, #12]   ; 87802ff4 <test_func+0x14>
87802fe4:   e3a01007    mov r1, #7
87802fe8:   e59f0008    ldr r0, [pc, #8]    ; 87802ff8 <test_func+0x18>
87802fec:   e5831000    str r1, [r3]
87802ff0:   ea003a4a    b   87811920 <printf>/*注:以下两行为Lable1*/
87802ff4:   8784004c    strhi   r0, [r4, ip, asr #32]
87802ff8:   8783127f            ; <UNDEFINED> instruction: 0x8783127f


/*这里是rel_dyn_test函数的反汇编*/
878031c0 <rel_dyn_test>:
878031c0:   e59f302c    ldr r3, [pc, #44]   ; 878031f4 <rel_dyn_test+0x34>
878031c4:   e3a02014    mov r2, #20
878031c8:   e92d4010    push    {r4, lr}
878031cc:   e59f4024    ldr r4, [pc, #36]   ; 878031f8 <rel_dyn_test+0x38>
878031d0:   e5832000    str r2, [r3]
878031d4:   ebffff81    bl  87802fe0 <test_func>
878031d8:   e59f001c    ldr r0, [pc, #28]   ; 878031fc <rel_dyn_test+0x3c>
878031dc:   e1a01004    mov r1, r4
878031e0:   eb0039ce    bl  87811920 <printf>
878031e4:   e59f0014    ldr r0, [pc, #20]   ; 87803200 <rel_dyn_test+0x40>
878031e8:   e1a01004    mov r1, r4
878031ec:   e8bd4010    pop {r4, lr}
878031f0:   ea0039ca    b   87811920 <printf> /*注:以下四行为Lable2*/
878031f4:   87838c3c            ; <UNDEFINED> instruction: 0x87838c3c
878031f8:   87802fe0    strhi   r2, [r0, r0, ror #31]
878031fc:   8783129b            ; <UNDEFINED> instruction: 0x8783129b
87803200:   878312a8    strhi   r1, [r3, r8, lsr #5]

  这些函数末尾存储变量地址的内存空间称为Label(编译器自动分配),这里记<test_func>的最后两行为Lable1,<rel_dyn_test>的最后四行为Lable2。等一下可以看到这些Lable的地址是存储在rel.dyn段中的。
数据段

87838c3c <test_val>:
87838c3c:   0000000a    andeq   r0, r0, sl

8784004c <jimmy_val>:
8784004c:   00000000    andeq   r0, r0, r0

rel.dyn段

/*Lable1的rel.dyn段*/
8783b4a0:   87802ff4            ; <UNDEFINED> instruction: 0x87802ff4
8783b4a4:   00000017    andeq   r0, r0, r7, lsl r0
8783b4a8:   87802ff8            ; <UNDEFINED> instruction: 0x87802ff8
8783b4ac:   00000017    andeq   r0, r0, r7, lsl r0

/*Lable2的rel.dyn段*/
8783b4c8:   878031f4            ; <UNDEFINED> instruction: 0x878031f4
8783b4cc:   00000017    andeq   r0, r0, r7, lsl r0
8783b4d0:   878031f8            ; <UNDEFINED> instruction: 0x878031f8
8783b4d4:   00000017    andeq   r0, r0, r7, lsl r0
8783b4d8:   878031fc            ; <UNDEFINED> instruction: 0x878031fc
8783b4dc:   00000017    andeq   r0, r0, r7, lsl r0
8783b4e0:   87803200    strhi   r3, [r0, r0, lsl #4]
8783b4e4:   00000017    andeq   r0, r0, r7, lsl r0

  接下来看一下test_val = 20;这条c语句对应的汇编代码为:

ldr r3, [pc, #44] -----(1)
mov r2, #20       -----(2)
str r2, [r3]      -----(3)

  由于arm的流水线架构,所以执行第(1)条指令时pc是指向第(3)条指令的,再加上44(也就是再往下数11条指令,因为一条指令4个字节),即指向了地址为<878031f4>的语句,然后将其内容加载到r3中(即r3=0x87838c3c,对照上面的数据段,可知这就是变量test_val的地址);接着将立即数20放到r2;最后将r2的内容放到以r3为地址的空间。
  也就是说,代码在取符号(全局变量、函数等)的时候,会使用相对地址的方法去函数末尾的Lable中取到该符号的地址(说明白点,就是这些Lable存储了要查找符号的地址),然后再根据该地址读取符号的内容。所以当data段搬移时,Lable中的内容也要跟着改变!而这些Lable的地址保存在rel.dyn段中,所以代码relocate时要用到rel.dyn段,对Lable中的内容进行修改!

3.3 代码分析

ENTRY(relocate_code)
        /*
        *拷贝__image_copy_start到__image_copy_end之间的内容到新地址,即只拷贝了代码段
        *和data段,没有拷贝bss段和rel.dyn段。
        */
    ldr r1, =__image_copy_start /* r1 <- SRC &__image_copy_start */
    subs    r4, r0, r1      /* r4 <- relocation offset */
    beq relocate_done       /* skip relocation */
    ldr r2, =__image_copy_end   /* r2 <- SRC &__image_copy_end */

copy_loop:
    ldmia   r1!, {r10-r11}      /* copy from source address [r1]    */
    stmia   r0!, {r10-r11}      /* copy to   target address [r0]    */
    cmp r1, r2          /* until source end address [r2]    */
    blo copy_loop

    /*
     * fix .rel.dyn relocations
     */
    /*
    *根据rel.dyn段设置Lable的内容,relocate后运行地址和编译地址不一致还能跑,
    *全靠这里啦。
    */
    ldr r2, =__rel_dyn_start    /* r2 <- SRC &__rel_dyn_start */
    ldr r3, =__rel_dyn_end  /* r3 <- SRC &__rel_dyn_end */
fixloop:
    ldmia   r2!, {r0-r1}        /* (r0,r1) <- (SRC location,fixup) */
    and r1, r1, #0xff
    cmp r1, #23         /* relative fixup? */
    bne fixnext

    /* relative fix: increase location by offset */
    add r0, r0, r4
    ldr r1, [r0]
    add r1, r1, r4
    str r1, [r0]
fixnext:
    cmp r2, r3
    blo fixloop

relocate_done:

#ifdef __XSCALE__
    /*
     * On xscale, icache must be invalidated and write buffers drained,
     * even with cache disabled - 4.2.7 of xscale core developer's manual
     */
    mcr p15, 0, r0, c7, c7, 0   /* invalidate icache */
    mcr p15, 0, r0, c7, c10, 4  /* drain write buffer */
#endif

    /* ARMv4- don't know bx lr but the assembler fails to see that */

#ifdef __ARM_ARCH_4__
    mov        pc, lr
#else
    bx        lr
#endif

ENDPROC(relocate_code)
<think>好的,我现在需要详细解释U-Boot启动流程。首先,我需要回忆一下U-Boot的基本知识。U-Boot是一个开源的引导加载程序,主要用于嵌入式系统。它的启动流程涉及到硬件初始化、环境变量设置、加载操作系统等步骤。用户可能希望了解从加电到加载内核的整个过程,包括各个阶段的具体任务。 首先,我得确认不同架构的启动流程可能有所不同,比如ARM和PowerPC可能有差异。用户没有指定具体架构,所以我需要保持通用性,或者假设以常见的ARM为例。另外,可能需要分阶段描述,比如分为BL1、BL2等阶段,或者是U-Boot自身的启动过程。 接下来,我需要回忆U-Boot启动阶段。通常,启动流程可以分为几个阶段:SoC内部的ROM代码执行,SPL(Secondary Program Loader),然后是U-Boot主体,最后加载操作系统。每个阶段的具体任务是什么?比如,ROM代码负责初始化最基础的硬件,SPL进行内存初始化,然后加载U-Boot到内存中执行。 然后,可能需要详细说明每个阶段的具体步骤。例如,在SPL阶段,会初始化时钟、内存控制器,设置堆栈,然后加载U-Boot到DDR内存中。U-Boot主体启动后,会进行更复杂的外设初始化,读取环境变量,根据配置加载内核映像,传递参数,最后跳转到内核执行。 需要注意关键点,比如环境变量的存储位置(如NOR Flash、EEPROM)、如何读取,以及启动命令的执行流程,比如bootcmd的设置。此外,设备树(DTB)的加载过程也很重要,特别是在ARM架构中,内核需要设备树来识别硬件配置。 还要考虑可能的错误处理,比如U-Boot如何应对启动失败,比如自动恢复或者进入命令行模式。另外,不同板级支持包(BSP)可能有不同的配置,但核心流程应该是一致的。 需要验证信息的准确性,比如SPL的作用是否仅限于初始化内存和加载U-Boot,是否有例外情况。另外,启动延迟、中断处理、多阶段引导的安全性措施(如签名验证)是否需要提及?用户的问题是关于启动流程详解,因此可能需要覆盖这些高级主题,但保持主次分明。 最后,结构安排要清晰,可能按顺序分阶段讲解,每个阶段的职责和关键步骤,结合代码示例或流程图是否有助于理解?但用户要求的是文本解释,所以需要用文字描述清楚。同时,注意使用中文术语,比如“设备树”、“环境变量”等,避免术语混淆。 总结思考步骤:确定架构(假设ARM),分阶段描述(ROM→SPL→U-Boot→内核),各阶段任务,关键初始化步骤,环境变量处理,设备树加载,启动命令执行,错误处理。确保覆盖从硬件初始化到内核跳转的全过程,并指出不同配置的可能差异。</think>### U-Boot启动流程详解(以ARM架构为例) #### 一、硬件启动阶段 1. **SoC ROM代码执行**(BL0) - 上电复位后,CPU从固化在芯片内部的ROM代码开始执行 - 主要任务: - 设置初始时钟频率 - 初始化基本存储接口(如BootROM支持的启动介质:NOR Flash/SD卡/eMMC) - 加载SPL(Secondary Program Loader)到SRAM - 验证SPL的数字签名(安全启动场景) - 典型代码片段: ```armasm ldr sp, =0xFFFF0000 ; 设置临时堆栈 bl lowlevel_init ; 初始化时钟和DDR控制器 ldr r0, =SPL_START_ADDR ; 加载SPL到0x80000000 bl load_image_from_mmc bx r0 ; 跳转到SPL ``` #### 二、SPL阶段(BL1) 2. **SPL初始化流程** - 执行位置:SRAM(通常64-256KB) - 核心任务: - 初始化DDR内存控制器 - 设置完整时钟树 - 加载完整U-Boot到DDR - 可选安全校验(如HASH验证) - 关键数据结构: ```c struct spl_image_info { u8 os; /* 操作系统类型(IH_OS_U_BOOT)*/ u32 load_addr; /* 加载地址(如0x87800000) */ size_t size; }; ``` - 典型执行流程: ```c board_init_f() → 初始化基础外设 timer_init() → 设置系统定时器 spl_mmc_load() → 从存储介质加载U-Boot jump_to_image_no_args() → 跳转到U-Boot ``` #### 三、U-Boot主体阶段(BL2) 3. **U-Boot主要初始化流程** - 执行位置:DDR内存(地址如0x87800000) - 阶段划分: - **板级初始化前段**(`board_init_f`) ```c initf_malloc() // 初始化早期内存分配器 arch_cpu_init() // CPU架构相关初始化 board_early_init() // 板级早期GPIO配置 dram_init() // 检测内存容量 ``` - **重定位阶段** ```armasm adr r0, _start ; 获取当前地址 ldr r1, =CONFIG_SYS_TEXT_BASE cmp r0, r1 beq relocated ; 判断是否需要重定位 bl copy_uboot_to_ram relocated: ``` - **板级初始化后段**(`board_init_r`) ```c env_init() // 初始化环境变量存储 stdio_init() // 初始化控制台 initr_net() // 网络设备初始化 run_main_loop() // 进入主循环 ``` 4. **环境变量处理** - 存储位置检测流程: ``` 1. 检查`CONFIG_ENV_IS_IN_MMC` → 查找FAT分区的uboot.env 2. 检查`CONFIG_ENV_IS_IN_SPI_FLASH` → 直接读取SPI Flash偏移量 3. 使用默认环境(CONFIG_USE_DEFAULT_ENV_FILE) ``` - 变量优先级: ```bash 默认环境 < 存储介质环境 < 运行时修改的环境 ``` #### 四、内核加载阶段 5. **bootcmd执行流程** - 典型启动命令解析: ```bash # 常见bootcmd配置 setenv bootcmd 'mmc dev 0; \ fatload mmc 0:1 ${kernel_addr_r} zImage; \ fatload mmc 0:1 ${fdt_addr_r} dtb; \ bootz ${kernel_addr_r} - ${fdt_addr_r}' ``` - 内核加载关键函数调用链: ``` do_bootz()bootz_start() → image_setup_libfdt() → do_bootm_states() ``` 6. **设备树处理流程** - 设备树修改过程: ```c int ft_system_setup(void *blob, bd_t *bd) { /* 动态添加板级配置 */ fdt_setprop(blob, node, "clock-frequency", &clk, sizeof(clk)); return 0; } ``` - 内存节点更新示例: ```dts / { memory@80000000 { device_type = "memory"; reg = <0x80000000 0x20000000>; // 512MB }; }; ``` #### 五、异常处理机制 7. **启动失败处理** - 自动恢复流程: ``` 1. 检测启动计数器(bootcount) 2. 超过最大失败次数(如3次)后: - 清除环境变量区 - 从备份分区恢复 - 进入紧急下载模式(如USB DFU) ``` - 关键代码: ```c if (bootcount_get() > MAX_BOOTCOUNT) { env_set_default("启动失败,恢复出厂设置", 0); do_reset(NULL, 0, 0, NULL); } ``` #### 六、高级启动配置 8. **多重启动支持** - 典型boot_targets配置: ```bash boot_targets = mmc0 usb pxe dhcp ``` - 优先级判断逻辑: ```c for (i = 0; i < ARRAY_SIZE(boot_targets); i++) { if (device_available(boot_targets[i])) { boot_device = boot_targets[i]; break; } } ``` 9. **安全启动实现** - 签名验证流程: ``` 1. 加载内核镜像和签名(如PKCS7格式) 2. 使用内置公钥验证签名 3. 哈希校验镜像完整性 ``` - 关键配置选项: ```makefile CONFIG_RSA_VERIFY=y CONFIG_FIT_SIGNATURE=y CONFIG_OF_CONTROL=y ``` **启动流程全貌图示**: ``` [ROM Code] → [SPL] → [U-Boot] → [设备树处理] → [内核加载] → [启动OS] │ │ │ │ └─> 基础时钟 环境变量加载 启动参数传递 存储接口初始化 外设驱动初始化 initramfs处理 ```
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值