恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
U-Boot启动流程深度解析:从_start汇编到C语言世界
首页
资讯中心
/
U-Boot启动流程深度解析:从_start汇编到C语言世界
U-Boot启动流程深度解析:从_start汇编到C语言世界
发布时间:2026/10/6 1:32:04
1. 从一条让人抓狂的启动日志说起如果你曾经在调试一块 ARM64 开发板时盯着串口终端里那几行冷冰冰的启动信息发呆那你一定懂我在说什么。板子上电串口打印出几行字符然后要么卡死要么跳进一个你完全不知道从哪来的地址要么干脆连打印都没有。你手里有源码有编译工具链有调试器但就是不知道 CPU 执行的第一条指令到底在哪、它怎么从汇编一步步走到 C 语言的board_init_f、board_init_r最后把内核拉起来。这个问题的核心就藏在 U-Boot 的启动流程里。而启动流程的起点就是那个几乎所有移植文档都会提到、但很少有人真正讲透的符号——_start。它通常位于arch/arm/lib/vectors.S或者arch/arm/cpu/armv8/start.S这类汇编文件里是链接脚本u-boot.lds中ENTRY(_start)指定的入口。CPU 复位后程序计数器被硬件设置为某个复位向量地址而 U-Boot 的镜像恰好被烧写或加载到那个位置于是_start就成了整个软件世界的第一行代码。我写这篇东西的目的很直接把 U-Boot 从_start汇编到进入 C 世界这段路掰开揉碎讲清楚。不是泛泛地讲“U-Boot 分两个阶段”而是从第一条指令开始逐段分析它在干什么、为什么这么干、异常向量表怎么摆、栈怎么设、重定位怎么发生、_main是怎么被调到的。适合谁看适合那些已经能编译 U-Boot、能烧写、能看串口输出但一遇到启动卡死就无从下手的嵌入式工程师也适合想真正理解“裸机程序如何从汇编过渡到 C”的初学者。关键词里的 uboot、_start、汇编、C、ARM64就是这篇文章要串起来的主线。2. 复位之后的第一现场异常向量表与 _start 的真实位置2.1 为什么入口不是 main而是 _start很多人第一次看 U-Boot 源码时会下意识去找main函数结果发现根本找不到传统意义上的main。原因很简单CPU 复位后没有任何操作系统帮你准备运行环境栈指针是未知的全局变量所在的内存可能还没初始化甚至连内存控制器都没配好。这种状态下C 代码根本无法可靠运行因为 C 代码依赖栈、依赖已初始化的数据段、依赖可预测的内存布局。所以链接器必须把入口点指定为一个汇编符号这个符号就是_start。在 U-Boot 的链接脚本arch/arm/cpu/armv8/u-boot.lds里你能看到类似这样的段落ENTRY(_start) SECTIONS { . 0x00000000; . ALIGN(8); .text : { *(.__image_copy_start) *(.vectors) CPUDIR/start.o (.text*) *(.text*) } ... }注意*(.vectors)被放在很靠前的位置而CPUDIR/start.o (.text*)紧随其后。这意味着最终镜像里异常向量表在最前面然后是start.S编译出来的代码。_start这个符号就定义在start.S里但它和向量表的关系需要仔细看。2.2 ARM64 的向量表布局与 0x0 地址的玄机ARM64 架构下异常向量表有固定的格式要求。它必须放在一个 2KB 对齐的地址上表内包含 16 个条目每个条目 128 字节总共 2048 字节。这 16 个条目对应四类异常同步、IRQ、FIQ、SError在四种状态下的入口。U-Boot 在arch/arm/lib/vectors.S里定义了这张表.globl _start _start: b reset .align 7 b undefined_instruction .align 7 b software_interrupt ...在 ARMv8 的 U-Boot 中向量表的基地址由VBAR_EL3或VBAR_EL2等系统寄存器决定。复位时如果运行在 EL3硬件会从VBAR_EL3指向的地址取第一条指令。但很多 SoC 的 BootROM 会把 U-Boot 的镜像加载到某个地址并跳转过去这时候_start的实际运行地址可能和链接地址不一致这就引出了后面要讲的重定位问题。这里有个容易踩的坑如果你在调试时发现程序跑飞先确认向量表是否真的在预期地址上。我遇到过一块板子BootROM 把镜像加载到 0x40000000但链接脚本里_start链接在 0x0结果所有绝对地址跳转全部错乱。解决办法是在链接脚本里把起始地址改成实际加载地址或者确保重定位代码在位置无关的前提下尽早执行。2.3 reset 标签里到底做了什么向量表第一条是b reset这个reset标签才是真正干活的起点。在 ARMv8 的start.S里reset处的代码大致做这几件事第一保存启动参数。BootROM 或上一级引导程序可能会在寄存器里留下一些信息比如设备树地址、启动介质标识。U-Boot 会把这些值保存到通用寄存器或临时内存里后续 C 代码会用到。第二设置 CPU 状态。包括关闭 MMU、关闭 D-Cache、设置异常级别、配置栈指针。关闭 MMU 和 D-Cache 是必须的因为此时页表还没建立缓存一致性也无法保证。栈指针通常先指向一个临时区域比如CONFIG_SYS_INIT_SP_ADDR定义的地址这个地址必须在 SRAM 或已初始化的内存范围内。第三调用lowlevel_init。这个函数因平台而异通常负责初始化时钟、DDR 控制器、串口等最基础的硬件。没有它后面连打印都出不来。第四设置好 C 运行环境后跳转到_main。注意不是main而是_main这个符号在arch/arm/lib/crt0_64.S或类似文件里定义是汇编到 C 的桥梁。3. 从汇编跳进 C 之前必须搭好的三个台子3.1 栈指针C 语言的命根子C 函数调用依赖栈来保存返回地址、局部变量和寄存器现场。在汇编阶段如果不把栈指针设好第一个 C 函数调用就会直接跑飞。U-Boot 在reset里通常会这样设置ldr x0, CONFIG_SYS_INIT_SP_ADDR mov sp, x0但这里有个细节ARM64 的栈是向下增长的而且要求 16 字节对齐。CONFIG_SYS_INIT_SP_ADDR一般指向一块 SRAM 的高地址端这样栈向下增长时不会覆盖到代码段。如果你在移植时发现 C 函数一进去就异常先检查这个地址是否落在有效的 RAM 范围内以及是否对齐。另外在进入 C 之前U-Boot 还会预留一段空间给全局数据gd。gd是一个指向global_data结构体的指针C 代码通过它访问各种全局状态。在 ARM64 上gd通常保存在x18寄存器里这是平台约定的。所以你在_main里会看到先把x18设置好再调用board_init_f。3.2 BSS 段清零为什么不能省C 语言规定未初始化的全局变量和静态变量初值为 0。但裸机环境下这些变量所在的 BSS 段在内存里可能是随机值。所以必须在进入 C 之前把 BSS 段清零。U-Boot 在crt0_64.S里有一段循环ldr x0, __bss_start ldr x1, __bss_end mov x2, #0 clear_bss: str x2, [x0] add x0, x0, #8 cmp x0, x1 b.lo clear_bss这段代码看起来简单但有两个坑。第一__bss_start和__bss_end必须由链接脚本正确导出如果链接脚本写错可能把代码段也清零了那就直接死机。第二如果 BSS 段很大清零会消耗时间在某些对启动速度敏感的场合可以考虑只清零必要的部分但标准做法还是全清。3.3 重定位U-Boot 最容易被误解的环节重定位是 U-Boot 启动流程里最绕的部分。简单说U-Boot 编译时链接在一个地址比如 0x0 或 0x40000000但实际运行时可能被加载到另一个地址比如 DDR 里的 0x7FF00000。如果代码里有绝对地址访问就会出错。解决办法是在启动早期把整个 U-Boot 镜像从加载地址复制到链接地址或者反过来让代码运行在正确的位置。在 ARM64 的 U-Boot 中重定位通常发生在board_init_f之后、board_init_r之前。board_init_f会计算需要多少内存、最终运行地址在哪然后调用relocate_code完成复制。复制完成后程序会跳转到新的地址继续执行。这个过程涉及__image_copy_start、__image_copy_end、__rel_dyn_start等链接符号以及动态重定位表的处理。我踩过的一个坑是在重定位之前使用了全局变量结果变量地址还是旧的导致判断逻辑出错。所以 U-Boot 在重定位前尽量只用栈上和寄存器里的数据gd也是通过寄存器传递的。如果你在board_init_f里看到某个全局变量值不对先想想是不是重定位还没发生。4. _main 到 board_init_fC 世界的第一段路4.1 _main 的职责清单_main是汇编和 C 的分界线它本身也是汇编写的但它的主要工作是为 C 函数铺路。在 ARM64 的crt0_64.S里_main大致做这些事设置gd指针把它放到x18寄存器。调用board_init_f_alloc_reserve分配早期内存。调用board_init_f_init_reserve初始化gd结构体。清零 BSS。调用board_init_f。注意board_init_f是第一个真正意义上的 C 函数。它接收一个参数就是gd指针。在 ARM64 调用约定里第一个参数放在x0寄存器所以你会看到mov x0, x18然后bl board_init_f。4.2 board_init_f 里到底在规划什么board_init_f的名字容易让人误解以为它是“初始化硬件”。其实它的核心任务是“规划”——规划内存布局、规划外设初始化顺序、规划后续流程。它运行在重定位之前所以不能依赖全局变量所有状态都通过gd传递。它主要做这几件事第一初始化串口。这是你能看到打印的前提。board_init_f会调用serial_init或类似的函数把串口配好然后通过printf输出一些调试信息。如果你在串口里看到第一行 U-Boot 版本信息说明这一步成功了。第二计算内存分配。它会根据CONFIG_SYS_MALLOC_LEN、CONFIG_SYS_STACK_SIZE等配置计算 U-Boot 自身需要多少内存以及最终运行地址应该在哪。这些信息会存到gd-relocaddr、gd-new_gd等字段里。第三初始化定时器、环境变量存储等基础组件。这些组件在后续阶段会用到但此时还不能依赖完整的内存管理。第四调用relocate_code完成重定位。重定位之后程序会跳转到新的地址继续执行board_init_r。4.3 串口打印为什么是调试启动流程的第一抓手在启动流程调试中串口打印的价值无可替代。因为此时没有操作系统、没有文件系统、没有网络你能依赖的输出通道只有串口。U-Boot 在board_init_f里初始化串口后会通过debug或printf输出大量信息。如果你在某个阶段卡死串口最后一行输出就是定位问题的关键线索。我通常会在关键节点手动加打印比如在_start里加一句汇编级别的串口输出需要先配好串口寄存器或者在board_init_f的每个子步骤前后加printf。这样即使没有调试器也能大致判断卡在哪。注意在重定位之前加打印要小心因为串口寄存器地址可能是绝对地址重定位后可能失效。所以重定位前后的打印要分开处理。5. 重定位之后board_init_r 与启动内核的最后一公里5.1 board_init_r 的初始化顺序重定位完成后程序跳转到board_init_r。这个函数运行在新的地址上可以正常使用全局变量和完整的内存管理。它的初始化顺序大致是初始化 malloc 堆让malloc可用。初始化环境变量从存储介质里读取env。初始化设备模型DM扫描设备树绑定驱动。初始化各种外设MMC、NAND、网络、USB 等。进入主循环等待用户输入或执行启动命令。这个顺序不能乱。比如网络初始化依赖 malloc环境变量读取依赖存储驱动设备模型初始化依赖设备树。如果你在移植时发现某个外设不工作先检查它的初始化是否在正确的阶段被调用。5.2 启动内核时 U-Boot 做了什么当用户输入bootm或booti命令时U-Boot 会执行启动内核的流程。以 ARM64 的booti为例第一解析内核镜像。ARM64 的 Image 格式有一个 64 字节的头部里面包含魔数、加载地址、入口地址等信息。U-Boot 会校验魔数如果不对就报错。你看到的“内核段既不是 arm64 image”这类错误通常就是魔数校验失败。第二准备启动参数。包括设备树地址、initrd 地址、命令行参数等。这些信息会通过寄存器或内存传递给内核。第三关闭中断、关闭缓存、设置好 CPU 状态然后跳转到内核入口地址。从这一刻起U-Boot 的生命周期结束内核开始接管。5.3 常见启动卡死场景与排查思路启动卡死是嵌入式调试的家常便饭。我整理了几种典型场景和排查方法现象可能原因排查手段串口无任何输出串口未初始化、时钟未配置、镜像未正确加载检查 BootROM 跳转地址、测量串口引脚波形输出几行后卡死DDR 初始化失败、栈指针错误、BSS 清零越界在卡死前加打印、用调试器查看 PC 值重定位后跑飞链接地址与运行地址不一致、重定位表损坏检查链接脚本、确认__image_copy_start等符号内核启动失败镜像格式错误、设备树地址错误、命令行参数错误校验镜像魔数、打印设备树地址、检查bootargs这张表不是万能的但能覆盖大部分常见问题。关键是要养成“从第一条指令开始排查”的习惯而不是一上来就怀疑内核。6. 几个让我印象深刻的移植翻车现场6.1 栈指针指向了未初始化的 DDR有一次移植一块新板子串口能输出第一行但紧接着就卡死。用调试器连上去看发现 PC 停在一个莫名其妙的地方栈指针指向的地址读出来全是 0xFF。后来查出来是CONFIG_SYS_INIT_SP_ADDR配到了 DDR 区域但 DDR 控制器还没初始化读写 DDR 直接返回错误。把栈指针改到片内 SRAM 后问题解决。这个坑的教训是早期栈必须放在无需初始化就能访问的内存里通常是片内 SRAM 或 TCM。等 DDR 初始化完成后再切换到 DDR 里的栈。6.2 重定位后全局变量值不对另一次是在board_init_f里设置了一个全局变量然后在board_init_r里读它发现值变了。原因就是重定位board_init_f运行时变量在旧地址重定位后变量被复制到新地址但board_init_f里写的还是旧地址的值。解决办法是改用gd结构体传递因为gd指针在重定位时会被更新。6.3 向量表没对齐导致异常处理失效ARM64 要求向量表 2KB 对齐。有一次链接脚本里忘了加.align 11结果向量表没对齐发生异常时 CPU 跳到了错误的地方直接死机。加上对齐后异常处理正常能看到具体的异常类型和地址排查效率大幅提升。7. 给正在啃 U-Boot 启动流程的你几点实在建议第一不要一上来就通读所有汇编。先找到_start然后顺着reset、_main、board_init_f、relocate_code、board_init_r这条主线走每个阶段搞清楚输入、输出和关键操作。支线代码用到再看。第二善用链接脚本和符号表。u-boot.map和u-boot.lds能告诉你每个符号的地址和段布局。当你怀疑某个地址不对时先查 map 文件。第三串口打印和调试器结合使用。串口告诉你“卡在哪”调试器告诉你“为什么卡”。两者缺一不可。第四重定位是分水岭。重定位之前的代码要尽量位置无关重定位之后才能放心用全局变量和完整内存管理。理解这一点很多奇怪现象就解释得通了。第五多动手改配置、加打印、单步调试。看十遍源码不如自己改一次配置、加一行打印、用调试器单步走一遍。启动流程的细节只有在实际操作中才会真正暴露出来。最后分享一个我常用的技巧在_start的最开头加一段汇编直接把某个 GPIO 拉高用示波器或逻辑分析仪看波形。这样即使串口没配好也能知道 CPU 是否执行到了这里。等串口配好后再去掉这段代码。这个方法在调试最早期启动问题时特别管用。