U-Boot分析【学习笔记】(10)

📅 2026/8/25 17:03:25
U-Boot分析【学习笔记】(10)
10. _main分析在U-Boot分析【学习笔记】(9)末尾我们知道 lowlevel_init 函数执行完后会回到 start.S 继续往下执行 bl _main。在 U-Boot 这种支持多架构ARM, MIPS, x86等的系统中我们要在 arch/arm/lib/crt0.S 中寻找 _main原因如下架构关联性它是 ARM 架构的“出厂设置”文件名含义crt0 是 C-Runtime Startup Code 的缩写意为“C 语言运行环境启动代码”。平台特性虽然 start.S 处理的是特定 CPU如 Cortex-A7的寄存器级初始化但搭建 C 语言舞台如对齐栈指针、设置全局变量指针r 9 r9r9对于所有 ARM 架构的芯片来说都是通用的。目录逻辑arch/arm/cpu/armv7/存放的是跟“这颗处理器”相关的代码。arch/arm/lib/存放的是跟“整个 ARM 家族”通用的工具和启动逻辑。结论_main 属于 ARM 家族通用的启动规范所以它被放在了架构级的公共库lib中。职责分离让“芯片开发”与“系统启动”解耦U-Boot 的设计极其注重复用。start.S (位于 cpu 目录)、他负责针对特定的芯片i.MX6ULL把看门狗关了、把基础时钟开了。crt0.S (位于 lib 目录)他不关心你是 NXP 的芯片还是三星的芯片只关心你是不是 ARM。他负责按照 ARM 的标准去分配内存、执行重定位。这种设计的好处如果 NXP 以后出一颗 ARMv8 架构的新芯片开发者只需要重写 start.S 里的硬件逻辑而 crt0.S 里的 _main 启动框架几乎可以原封不动地直接套用。编译系统的“指路牌”链接脚本即便代码分成了两个文件它们最终是如何连在一起的呢在 U-Boot 编译生成的链接脚本通常是 u-boot.lds中明确规定了代码的排布顺序。通常顺序是start.o (包含中断向量表和基础初始化)→ \rightarrow→crt0.o (包含 _main)。当你在 start.S 里写下 bl _main 时链接器会自动去 arch/arm/lib/crt0.S 编译生成的二进制文件里寻找这个入口地址。U-Boot 启动序列_main执行流程本文件处理 U-Boot 启动过程中与目标平台无关的阶段即 ARM 通用阶段这些阶段需要 C 语言运行环境的支持。其入口点为 _main由目标芯片的start.S文件跳转而来。为调用 board_init_f() 搭建初始环境环境描述此环境仅提供一个栈Stack和一块存储 GD全局数据结构体的空间。这两个部分都位于当前可用的内存中如 SRAM、锁定的 L1 Cache 等。重要限制在此阶段变量VARIABLE形式的全局数据无论是否初始化即 BSS 段是不可用的。只有常量CONSTANT类型的初始化数据可用。动作在调用 board_init_f() 之前必须将 GD 空间清零。调用 board_init_f()前台初始化核心职责此函数负责准备硬件以便程序能从系统内存DRAM、DDR 等中运行。逻辑衔接由于此时系统内存DRAM可能尚未就绪board_init_f() 必须使用当前的 GD 来存储需要传递给后续阶段的数据。关键数据这些传递的数据包括重定位目标地址代码搬家到哪、未来的栈地址以及未来的 GD 存储位置。设置中间环境环境描述此时的栈和 GD 已经切换到了 board_init_f() 在系统内存DRAM中分配的新位置。状态但此时 BSS 段和已初始化的非静态变量非 const 数据依然不可用。执行代码重定位仅限 U-Boot Proper非 SPL动作调用 relocate_code()。职责将 U-Boot 从当前位置如 Flash 或 SRAM重定位搬家到由 board_init_f() 计算出的目标 DRAM 地址。SPL 流程二级引导程序动作board_init_f() 直接返回到 crt0。SPL 中通常不涉及代码重定位。为调用 board_init_r() 搭建最终环境环境描述BSS 段已初始化为 0。已初始化非 const 数据已加载为预定值。栈已完全位于系统内存DRAM中。数据继承此时的 GD 保留了之前 board_init_f() 设置的所有关键数值。处理器最后微调仅限 U-Boot Proper动作某些 CPU 在此时仍需对内存进行最后的处理工作因此调用 c_runtime_cpu_setup例如刷新 Cache。跳转至 board_init_r()后台初始化意义正式进入 C 语言的大本营开始初始化各种外设网卡、USB、闪存等并启动内核。10.1 搭建 C 运行环境 并 调用borad_init_f(0)/* * Set up initial C runtime environment and call board_init_f(0). */#ifdefined(CONFIG_SPL_BUILD)defined(CONFIG_SPL_STACK)ldr sp,(CONFIG_SPL_STACK)#elseldr sp,(CONFIG_SYS_INIT_SP_ADDR)#endif#ifdefined(CONFIG_CPU_V7M)/* v7M forbids using SP as BIC destination */mov r3,sp bic r3,r3,#7mov sp,r3#elsebic sp,sp,#7/* 8-byte alignment for ABI compliance */#endifmov r0,sp bl board_init_f_alloc_reserve mov sp,r0/* set up gd here, outside any C code */mov r9,r0 bl board_init_f_init_reserve mov r0,#0bl board_init_f#if!defined(CONFIG_SPL_BUILD)这段代码实现了 _main 的第一个阶段在 SRAM 中搭建最基础的 C 运行环境并调用 board_init_f#ifdefined(CONFIG_SPL_BUILD)defined(CONFIG_SPL_STACK)ldr sp,(CONFIG_SPL_STACK)#elseldr sp,(CONFIG_SYS_INIT_SP_ADDR)#endif如果当前正在编译的是 SPL二级引导程序且定义了专用的 SPL 栈地址那么把SPL栈地址放入 sp 栈指针寄存器中否则从板级配置文件通常是 include/configs/xxx.h中读取预设的初始栈顶地址。#ifdefined(CONFIG_CPU_V7M)/* v7M forbids using SP as BIC destination */mov r3,sp bic r3,r3,#7mov sp,r3#elsebic sp,sp,#7/* 8-byte alignment for ABI compliance */#endif如果当前处理器架构是 ARMv7-M如 Cortex-M 系列由于 v7M 架构的指令集限制禁止直接将 sp 寄存器作为 bic 指令的操作目标。因此代码先将 sp 的值暂存到通用寄存器 r3 中通过 bic 指令对 r3 进行位清除操作低 3 位清零最后再将处理后的地址写回 sp。否则对于 i.MX6ULL 等 ARMv7-A 架构直接对 sp 寄存器执行 bic 操作。什么是“清空低三位”从二进制角度看数字 7 的二进制是 0…0111。BICBit Clear指令的作用是将第一个操作数中对应第二个操作数二进制为 1 的位全部清零。结果无论 sp 原来的地址是多少执行完后其二进制地址的最后三位一定变成了 000。数学意义一个二进制数如果低三位是 000那么这个数一定能被2 3 2^323即 8整除对齐指的是地址必须落在能被 n 整除的那个边界上所以清空低三位就可以实现8字节对启。10.1.1 board_init_f_alloc_reservemov r0,sp bl board_init_f_alloc_reserve mov sp,r0函数的参数传递/返回值关系参数传递并不是 0 到 16在 ARM 32 位架构如 i.MX6ULL中函数调用时的参数传递规则如下前 4 个参数依次使用 r0, r1, r2, r3 这四个寄存器。超过 4 个的部分必须通过 栈Stack 来传递。例如如果你写一个 C 函数 void test(int a, int b, int c, int d, int e)前四个 a, b, c, d 分别占领 r0-r3而第 5 个参数 e 会被压入内存中的栈里。寄存器r 4 r4r4-r 11 r11r11这些被称为“被调用者保存”寄存器。C 函数内部如果要用到它们必须先备份Push用完后再恢复Pop它们不用于直接传递参数。返回值通常只看r 0 r0r0函数执行完后返回值并不是撒在所有寄存器里的32 位返回值如 int, ulong, 指针存储在 r0 中。64 位返回值如 long long存储在 r0低 32 位和 r1高 32 位中。更大或更复杂的结构体通常通过指针在内存中传递。函数目的为全局数据GD预留空间0动作将当前的栈指针sp值传递给r0作为参数调用 C 函数 board_init_f_alloc_reserve。该函数在当前栈顶下方的 SRAM 区域中计算并扣除掉 struct global_data全局数据结构体以及早期分配所需的大小。物理意义函数返回一个新的地址在 r0 中这个地址是扣除 GD 空间后的新栈顶。随后 mov sp, r0 更新栈指针实现了在内存中为 GD “割地”的操作。按照 U-Boot 的源码组织架构board_init_f_alloc_reserve 函数通常位于以下路径common/init/board_init.c为什么在这个位置平台无关性board_init_f_alloc_reserve 的逻辑是纯 C 语言编写的它只是在做简单的地址运算从当前的栈顶减去 gd 和早期堆的空间。这种逻辑对于 ARM、MIPS 或 x86 架构都是通用的。职责划分common/ 目录存放的是 U-Boot 的核心公共代码。由于它是启动流程中“全架构通用”的预留空间操作因此被放在了 common/init/ 下。board_init_f_alloc_reserve 核心注释翻译功能描述从给定的“顶端top”地址开始向下分配用于存放“全局变量globals”的预留空间并返回分配区域的“底端bottom”地址。注意事项真正的“空间预留”动作即修改 SP 指针不能在此函数内完成因为这涉及修改 C 语言的栈指针会导致函数返回时崩溃。因此空间的物理预留由调用者即 _main 汇编代码在函数返回后完成。对齐约束GD全局数据必须向下按 16 字节对齐。Early Malloc早期堆不单独设置对齐因此它遵循当前架构的栈对齐约束。分配顺序GD 必须最后分配。这样函数的返回值既是整个预留区域的底部同时也是 GD 结构体的起始地址方便调用者直接将此地址存入r 9 r9r9。ulongboard_init_f_alloc_reserve(ulong top){/* 1. 预留早期 malloc 内存池空间 */#ifdefined(CONFIG_SYS_MALLOC_F)/* top 指针继续向下移动扣除早期堆的大小 */top-CONFIG_SYS_MALLOC_F_LEN;#endif/* 2. 最后一步预留 GD 空间并向下进行 16 字节对齐 *//* sizeof(struct global_data) 是账本的大小 */toprounddown(top-sizeof(structglobal_data),16);returntop;}为什么 GD 必须最后分配看下面这个内存分布图传入的 top是 _main 传进来的原始栈顶经过 8 字节对齐的 SP。向下减去 Malloc 空间此时 top 指向了早期堆的底部。向下减去 GD 空间此时 top 指向了 global_data 的底部。这个 top 地址现在具有双重身份它是新的栈顶位置用于 mov sp, r0。它也是 GD 结构体的基地址用于 mov r9, r0。由于 GD 就在最底部所以汇编代码只需要执行一次 mov r9, r0就能让r 9 r9r9完美指向 GD 的起始位置。10.1.2 board_init_f_init_reserve这个函数紧跟在空间分配之后通常也位于common/init/board_init.cboard_init_f_alloc_reserve和board_init_f_init_reserve在 U-Boot 启动流程中它们紧密配合共同完成了从“原始汇编环境”向“结构化 C 环境”的物理转型。逻辑上的“因果”关系这两个函数通常在 _main 中按顺序被调用它们共同服务于一个核心目标确立全局数据区GD和早期堆Early Malloc。先圈地 (alloc_reserve)输入当前的栈顶地址SRAM 的顶端。计算根据配置算出 GD 结构体需要多少空间、Early Malloc 需要多少空间。输出返回一个新的地址。这个地址既是新栈顶也是预留空间的起始基地址。汇编配合汇编收到返回值后立刻修改 sp物理搬家和 r9确立管理员。后装修 (init_reserve)输入刚刚圈出来的地皮基地址即 r9 指向的位置。计算把这块内存全部抹零memset并设置内部的各个“房间”如把早期堆的地址填入 GD 账本。输出无返回值但此时 $r9 指向的内存已经从“荒地”变成了“精装房”。空间上的“共生”关系它们共同切分了同一块物理内存。在内存空间中它们的产物是紧挨在一起的alloc_reserve 决定了这块区域的大小和边界。init_reserve 决定了这块区域内部的组织结构GD 放在最下面Early Malloc 堆放在 GD 上面。代码级的协同逻辑我们可以看它们在 _main 汇编里的“接力”过程/* 第一步圈地 */mov r0,sp/* 把旧栈顶传给参数 */bl board_init_f_alloc_reserve/* 计算空间 */mov sp,r0/* 【物理动作】修改 SP正式腾出空间 *//* 第二步确立身份 */mov r9,r0/* 将地皮首地址交给管理员 r9 *//* 第三步装修 */bl board_init_f_init_reserve/* 传入 r0 里的基地址清零并设置内部成员 */r9 寄存器在 U-Boot 运行期间被锁定专门用来存放 gd 结构体的首地址就像是一个永远握在手里的索引无论代码运行到汇编还是 C 语言只要想读写系统配置、频率、内存大小等核心数据CPU 就会直接去读取 r9 寄存器里的地址。voidboard_init_f_init_reserve(ulong base){structglobal_data*gd_ptr;// 定义一个局部指针用于初始操作/* * 1. 彻底清空 GD 空间并进行初始化设置。 * 使用 gd_ptr 进行操作因为此时全局指针 gd 可能还未生效。 */gd_ptr(structglobal_data*)base;// 将传入的基地址强转为 GD 结构体指针/* 将这块内存区域全部填充为 \0确保账本初始状态是干净的 */memset(gd_ptr,\0,sizeof(*gd));/* 2. 设置全局 GD 指针。如果当前架构是 ARM由于汇编中已经 mov r9, r0 所以不需要在此重复调用 arch_setup_gd。*/#if!defined(CONFIG_ARM)arch_setup_gd(gd_ptr);#endif/* 3. 地址指针步进base 现在指向 GD 之后的空间。 roundup(..., 16) 确保了地址按照 16 字节对齐。 */baseroundup(sizeof(structglobal_data),16);/* * 4. 记录早期 malloc 内存池的起始地址。 * 此时可以使用 gd 指针了对于 ARM 来说就是 r9 指针。 */#ifdefined(CONFIG_SYS_MALLOC_F)/* 将早期 malloc 的基地址记录在 gd 账本中方便后续调用 malloc 使用 */gd-malloc_basebase;/* 地址继续步进跳过整个早期 malloc 内存池的大小 base 最终指向这块预留区域的最顶端。 */baseCONFIG_SYS_MALLOC_F_LEN;#endif}由于 board_init_f_init_reserve 只接收一个参数所以这个参数 base 实际上就是 r0 寄存器的值也就是 board_init_f_alloc_reserve 返回的GD 结构体的基地址。函数核心逻辑总结强转指针将传入的 baseSRAM 预留区的首地址强转为 struct global_data 指针。因为 base 只是个无意义的数字强转后它才有了结构。初始化 GD使用 memset 将这块空间全部抹零。这保证了账本里没有之前的随机乱码所有成员初始值都为 0。架设 Early Malloc在 GD 的空间之后跳过对齐间隙将剩余的预留空间交给 Early Malloc 管理。关键动作将跳过 GD 后的新地址写入 gd-malloc_base。结果从此 U-Boot 拥有了“动态存储”的能力。为什么要存放 struct global_data (GD)——解决无处存储的全局变量问题。背景在 C 语言中普通全局变量存放在 .data已初始化或 .bss未初始化段。这些段通常位于 DDR 中。但在 i.MX6ULL 刚上电时DDR 还没初始化直接写全局变量会导致系统崩溃。解决方案我们需要一个“中央账本”。临时存储GD 结构体被强行放在内部 SRAM 里,这是一上电就能用的内存。信息传承U-Boot 在搬家Relocation前会将探测到的硬件信息如CPU 频率、DDR 大小、串口波特率、搬家后的目标地址记录在 GD 里。唯一性通过锁定 r9 寄存器指向 GD确保了无论代码运行到哪都能通过同一个入口找到这些核心数据。为什么要存放 Early Malloc背景传统的 malloc 函数需要管理大片内存堆池这通常是在 DDR 里划出一块几百 MB 的空间。但在 DDR 亮起之前有些驱动程序比如设备树 FDT、串口驱动、电源管理 I2C在初始化时就需要动态申请一丁点儿内存比如几百字节来存放临时数据。解决方案建立“早期堆”。微型内存池在 SRAM 里、紧接着 GD 之后划出一块很小的区域通常只有几 KB。动态支持有了它U-Boot 的早期 C 代码就可以调用 malloc 来申请空间而不需要硬编码内存地址。使命完成一旦 DDR 初始化完成U-Boot 就会建立真正的堆空间这个“早期堆”的使命就完成了。假设没有 Early Malloc如果 U-Boot 的驱动程序想在 DDR 还没亮时存一点数据比如串口驱动要存一下波特率计算结果程序员必须手动指定一个地址。/* 程序员必须自己算SRAM 哪块地儿没被用 *//* 万一算错了就可能把栈或者代码段给覆盖了系统直接挂掉 */int*temp_data(int*)0x0091F000;*temp_data115200;缺点非常危险,不同的芯片、不同的配置SRAM 大小都不一样。手动指定的地址 0x0091F000 在 A 芯片上能跑在 B 芯片上可能就冲突了。这就是“硬编码”的局限性——死板、易错、不可移植。10.1.3 board_init_fmov r0,#0bl board_init_fmov r0, #0根据 ARM 的函数调用约定r0 存放的是函数的第一个参数。在 C 语言中board_init_f 的定义通常是 void board_init_f(ulong boot_flags)。这里把 r0 设为 0意味着传给 boot_flags 的值为 0表示这是一个正常的冷启动。bl board_init_f执行带链接的跳转。此时程序正式离开汇编文件的掌控进入位于 common/board_f.c 的 C 语言世界。board_init_f 是一个通用核心函数它的位置非常固定源码绝对路径common/board_f.cvoidboard_init_f(ulong boot_flags){#ifdefCONFIG_SYS_GENERIC_GLOBAL_DATA/* *对于某些 CPU 架构全局数据gd在调用本函数之前就已经被初始化并投入使用了 *此时应当完好地保留它。而对于另外一些架构则需要定义 CONFIG_SYS_GENERIC_GLOBAL_DATA 宏 *直接在当前的 C 语言栈中临时创建一块空间来托管全局数据直到系统完成代码重定位。 */gd_tdata;gddata;/* *在 initcall_run_list 轮询器内部触发调试打印Debug Print访问全局数据之前 *必须先对其进行清零抹平。否则后续的调试打印可能会因为读到残留的随机乱码 *而对 gd-have_console控制台是否可用标志做出错误的判断。 */zero_global_data();#endifgd-flagsboot_flags;gd-have_console0;if(initcall_run_list(init_sequence_f))hang();#if!defined(CONFIG_ARM)!defined(CONFIG_SANDBOX)\!defined(CONFIG_EFI_APP)!CONFIG_IS_ENABLED(X86_64)/* NOTREACHED - jump_to_copy() does not return */hang();#endif}1.条件编译块#ifdefCONFIG_SYS_GENERIC_GLOBAL_DATAgd_tdata;gddata;zero_global_data();#endif对于 ARM 架构这个条件通常不成立。因为在前几步汇编里已经用mov r9, r0把 r9 寄存器牢牢绑定在 SRAM 的 gd 账本上了。ARM 早就有了合法的 gd 内存不需要在这个函数的 C 语言栈里临时抠一块 gd_t data 出来。如果是其他架构比如某些 MIPS上电后寄存器不够用就会借用这几行代码在栈里临时用一下。2.参数交接与状态初始化gd-flagsboot_flags;gd-have_console0;前一步汇编里的 mov r0, #0 中 r0 清零并且 r0 作为第一个参数传入进来也就是 boot_flags 0。这行代码把 0代表冷启动、正常启动写进标志位里。gd-have_console 0;此时系统还没有初始化串口先把控制台可用标志置为 0。3.核心硬件初始化if(initcall_run_list(init_sequence_f))hang();init_sequence_f 是一个函数指针数组initcall_run_list 会用一个 while 循环把数组里的 serial_init开串口、dram_init点亮内存、reserve_uboot规划新家等函数每个都调用一遍。返回值检查在 C 语言中返回 0 代表成功。如果其中任何一个硬件驱动出了问题initcall_run_list 就会返回非 0 值。hang(); 一旦前面报错立马执行 hang()。这个函数的底层实现就是一个死循环while(1);在早期的裸机上这表现为直接卡死防止带病运行。initcall_run_list物理本质它是一个通用的函数指针轮询器通过接收一个以 NULL 结尾的函数指针数组实现硬件初始化行为的集中化管理。三大核心职能架构解耦将“初始化策略board_init_f”与“具体动作各驱动函数”分离。安全熔断采用“非零即死”的返回机制一旦某个硬件初始化失败触发 hang() 卡死系统。状态诊断为系统提供了调试信息方便捕获上电阶段的硬件故障。4.尾部保护防代码跑飞#if!defined(CONFIG_ARM)!defined(CONFIG_SANDBOX)\!defined(CONFIG_EFI_APP)!CONFIG_IS_ENABLED(X86_64)/* NOTREACHED - jump_to_copy() does not return */hang();#endif注意前面的取反符号 !。因为我们正是 CONFIG_ARM所以这个 if 条件对于 ARM 来说不成立。既然不成立里面的 hang() 就会被编译器直接抹掉。这导致的最终结果是对于 ARM 架构board_init_f 执行完上面的大数组后正常结束函数调用。