新闻详情

新闻详情

首页 / 资讯中心 / 详情

U-Boot启动流程详解:从_start汇编到main_loop的完整链路

发布时间:2026/10/1 9:15:53来源:尧图网络
U-Boot启动流程详解:从_start汇编到main_loop的完整链路
从事嵌入式开发这些年我发现一个很有意思的现象很多工程师能把uboot编译出来、烧进板子、看到串口打印的版本号但你要是追问一句“CPU上电之后到main_loop之间前后到底跑了哪些代码、经历了哪些阶段”能完整说清楚的人真不多。这不怪大家现在工具链太完善了make一条命令、烧录一个按钮uboot就自己跑起来了它像一个封装完好的黑盒你只需要知道它能干活就行。可一旦板子启动失败、需要裁剪启动流程、或者要在裸机阶段加一段自定义硬件初始化时这个黑盒就会毫不留情地反噬你。我一直认为理解uboot启动流程最靠谱的路径不是去背那些流程框图而是从源码里那段_start汇编开始顺着CPU真正执行的顺序一步步跟进去。你会看到一段纯汇编代码怎么一点点把手里的“指挥权”交给C语言——这个过程就像一场交接仪式每个环节都有它的必然性和巧妙设计。这篇文章我就带你从头梳理这条链路读完之后你会对“从汇编到C世界”这件事有画面感以后不管是排查启动故障还是想要二次定制心里都有底。1. 上电那一刻CPU先看到的是_start不是main先说清楚一个最核心的事实CPU上电后硬件逻辑决定它会从某个固定的复位地址取第一条指令而不是像PC机那样有BIOS帮你铺路。在ARM平台上这个复位地址一般由芯片厂商的设计决定可能是0x00000000也可能是芯片内部BootROM的一段固化代码。现代大部分SoC的做法是芯片内部有一段只读的BootROM上电后先运行它BootROM再把外部存储介质SD卡、eMMC、NAND、NOR Flash里特定位置的uboot镜像装进SRAM最后跳转到uboot的入口——这个入口就是_start。1.1 为什么非要从汇编啃起可能有人会说“我知道入口是_start可我看它跳来跳去就头晕直接看C代码不好吗”这里我必须泼盆冷水从_start到进入第一个C函数之间所有代码几乎都是汇编而且这段汇编恰恰是整个移植和调试中最容易出问题的地段。它的工作有点像机场的塔台调度——所有飞机后续复杂的C代码能不能安全落地取决于塔台有没有把跑道时钟、油料电源、航道DDR内存都准备好。你要是跳过了这段后面排查启动问题时会发现自己根本没有抓手。1.2 链接脚本如何决定“入口”uboot编译时u-boot.lds链接脚本里有一行ENTRY(_start)。它告诉链接器整个镜像的入口地址是_start这个符号。而_start定义在arch/arm/lib/vectors.S老版本在arch/arm/cpu/armv7/start.S中。所以无论别人怎么跟你说启动流程最终落到源码上第一行真正的代码就是_start标签下的那几条指令。2. 一张异常向量表藏着启动的“第一地基”为什么_start要放在整个镜像的最开头因为ARM处理器的异常向量表要求放在这个位置。你看到的那几行跳转指令表面上是“入口代码”本质上是一张异常向量表。2.1 _start的“表格式”结构以ARMv7架构为例_start标签下的代码长这样.section .vectors, ax .globl _start _start: b reset 0x00 复位异常 ldr pc, _undefined_instruction 0x04 未定义指令异常 ldr pc, _software_interrupt 0x08 软中断异常 ldr pc, _prefetch_abort 0x0C 预取指中止异常 ldr pc, _data_abort 0x10 数据中止异常 nop 0x14 保留 ldr pc, _irq 0x18 外部中断IRQ ldr pc, _fiq 0x1C 快速中断FIQ注意向量表的起始地址就是_start所以整个uboot镜像的入口和向量表位置是同一个点。ARM处理器设计上规定异常向量表中每个入口占4字节32位模式8个异常一共32字节。CPU遇到复位、中断、未定义指令等异常时硬件会自动跳转到这张表里对应的偏移位置去执行。2.2 复位向量为什么用“b reset”而不用“ldr pc”这里有一个细节很多人没注意第一条复位异常用的是b reset其他异常全部用ldr pc, xxx。这里面是有讲究的。b指令是相对跳转跳转范围是当前PC地址往前/往后32MBARM模式不管代码被加载到哪里只要跳转目标在32MB范围内这条指令都能正确工作。而ldr pc, xxx是从某个内存地址把一个32位绝对地址加载到PC里它依赖那个地址处的数据是绝对链接地址。在uboot启动的极早期CPU可能刚从BootROM跳转到SRAM代码运行地址和链接地址很可能是不一致的这个我们后面专门讲。这时候如果第一条指令就使用ldr pc, reset它会拿到一个链接期的绝对地址极有可能指向还没初始化的DDR导致CPU直接跳飞。而b reset是相对于当前PC的偏移跳转代码在哪个物理地址运行它就跳到哪里天然不怕“位置无关”。打个不太严谨但很好懂的比方b是“你往左走三步就到了”ldr pc是“你按GPS定位的经纬度走”。在早期环境里GPS根本还没开机你只能靠相对位置走。2.3 异常向量表的“地基”意义这张表不仅是入口更是整个系统的异常分发中枢。在uboot启动完成后中断处理、FIQ处理仍然通过它来工作。理解它的结构你以后排查“系统莫名其妙进了中断”或者“跳飞了”这类问题时至少能知道去哪里看。后期代码搬移完成后uboot还会调用relocate_vectors把整张向量表搬到新的内存地址并配置好VBAR寄存器向量基址寄存器让CPU能找到新表。3. reset函数进入C语言前的“环境整备”b reset跳转到的地方就是真正的复位处理函数。在arch/arm/cpu/armv7/start.S里reset标签下的代码干的第一批事情件件都是“初始化环境”的关键动作。3.1 为什么非要先把CPU切到SVC模式ARM处理器有多种工作模式用户模式USR、系统模式SYS、监管模式SVC、中断模式IRQ/FIQ、中止模式ABT、未定义模式UND。CPU上电后默认处于SVC模式但这个状态并不保证后续代码执行环境的纯粹性所以uboot会在reset里显式地设一遍CPSR寄存器reset: /* 把CPU设为SVC模式同时屏蔽IRQ和FIQ */ mrs r0, cpsr bic r0, r0, #0x1f 清除模式位M[4:0] orr r0, r0, #0xd3 设置SVC模式(0x13)屏蔽IRQ(0x80)和FIQ(0x40) msr cpsr, r00xd3这个值拆开看低5位0x13是SVC模式的编号0x80是I位屏蔽IRQ0x40是F位屏蔽FIQ。这一步的意图很明确在后续所有外设、中断控制器、栈都还没初始化之前任何中断信号进来都会导致不可预测的跳转所以必须先把中断全部“静音”。3.2 关闭MMU和缓存给“实地址访问”扫清障碍紧接着reset里通常还会操作CP15协处理器关闭MMU和缓存mrc p15, 0, r0, c1, c0, 0 读SCTLR寄存器 bic r0, r0, #0x0005 关闭MMU(bit0)和D-Cache(bit2) bic r0, r0, #0x1000 关闭I-Cache(bit12) mcr p15, 0, r0, c1, c0, 0 写回为什么要关因为早期代码访问的全部是物理地址此时页表还没建立MMU如果还开着地址翻译会把你带到沟里。缓存同理——DDR可能还没初始化缓存命中的数据也可能来自一块尚未存在的内存。这一套动作之后CPU才处在一个“地址即实地址访问即物理访问”的干净状态。3.3 极早期初始化调用lowlevel_init的“第一个C函数”严格来说lowlevel_init不是第一个C函数——很多平台它仍然是用汇编写的但它已经具备“C函数”的形态有明确的入口、执行一段逻辑、然后返回。我见过不少板子lowlevel_init用了C语言实现放在board厂商目录下。这个函数干的事情每个平台各不相同但基本离不开三件套关看门狗看门狗是一种硬件定时器超时未“喂狗”就会强制复位系统。启动早期代码还没跑顺根本来不及定期喂狗不关掉它就是在赌命。设置系统时钟/PLLCPU上电默认跑的可能是芯片最低速的参考时钟lowlevel_init根据板级配置把PLL锁定到目标频率CPU才算进入“正常工作状态”。初始化DDR控制器这是最关键的一步因为uboot后续代码和数据要搬到DDR里运行。如果DDR没初始化后面的一切都无从谈起。在我调试过的板子上经常出现这样的情况串口一开机完全没输出重查发现DDR初始化时序配错了一个参数CPU彻底“脑死亡”。你说它是硬件问题还是软件问题都不纯粹——本质是启动早期汇编那几步的环境整备没做对。4. 设置栈指针没有栈C语言就是空中楼阁从reset出来uboot马上要做一件事设置栈指针SP。这件事看似简单却是从汇编迈进C世界的“天堑关口”。4.1 C语言函数调用到底依赖了什么C语言里的一切高级功能——局部变量、函数参数传递、函数返回地址、递归调用——统统建立在“栈”的基础上。ARM架构中sp寄存器指向栈顶push/pop指令或stmdb/ldmia通过SP完成压栈出栈。如果SP指向的是一块无效内存或者压根是随机值那么第一个C函数调用bl xxx时返回地址会被写到不知道什么地方去执行结果只能用“灾难”来形容。uboot在调用任何C函数之前都会先执行ldr sp, CONFIG_SYS_INIT_SP_ADDRCONFIG_SYS_INIT_SP_ADDR这个宏在头文件里定义不同平台值不一样。它的设计逻辑通常是指向当前运行环境SRAM的高地址区域。为什么是高地址因为ARM栈是向下生长的从高地址往低地址方向增长。你把SP设置到SRAM的最高可用地址栈向下扩展时才能获得最大空间不容易越界。4.2 一个常见误区栈指针要8字节对齐很多人设置SP时只关注“指向SRAM高地址”忽略了ARM嵌入式调用约定里非常重要的一个要求在调用C函数前SP必须保持8字节对齐也就是SP的值必须是8的倍数。这是为了支持64位类型如double、long long在栈上的对齐访问。如果你SP设成了0x1004这样的地址调用C函数后某些编译选项下会直接触发异常。所以uboot里CONFIG_SYS_INIT_SP_ADDR的最终表达式往往是这样的形态#define CONFIG_SYS_INIT_SP_ADDR \ (CONFIG_SYS_SRAM_BASE CONFIG_SYS_SRAM_SIZE - 4)-4这个尾巴就是为了让最终SP值落在8字节对齐边界上——先减到末尾再做对齐修正同时预留一点儿安全余量。这属于那种“文档里看不到、但实际调试时能救命”的细节。4.3 临时栈与最终栈是两回事启动早期这个栈只是一个“临时栈”它的目的就是支撑到relocate完成、board_init_r阶段。UBoot在搬移完代码之后会把SP重新设置到DDR高端地址那才是“终局栈”。你现在看到的CONFIG_SYS_INIT_SP_ADDR名字里的“INIT”已经暗示了这只是Initial阶段的SP后面要换的。理解这个区别你日后调栈溢出问题时就不会一头雾水。5. relocate_codeuboot为什么要“自己搬自己”这是uboot启动流程里最反直觉、也最能体现设计智慧的环节。很多人第一次看到这一步直接懵了代码已经在跑了为什么还要把自己拷贝到另一个地址然后再跳过去继续执行5.1 链接地址与运行地址的撕裂编译uboot时链接脚本会把所有代码段、数据段按一定的地址布局链接。这个链接阶段的地址叫“链接地址”——代码“以为”自己在的地方。但uboot镜像实际被加载执行的地址很可能和链接地址不同。拿一个典型场景来说链接脚本把uboot链接在DDR地址0x80000000但实际上电后uboot被BootROM加载到了SRAM的0x20000000。此时CPU在0x20000000运行代码——指令能跑通是因为早期代码恰好是位置无关的比如我们用b跳转避开绝对地址依赖。但代码里一旦出现真正的绝对地址访问比如全局变量的访问指令ldr r0, some_global_variable这条指令生成的是0x80000000这个链接地址。如果代码还停在SRAM的0x20000000r0就会指向一个尚未初始化、甚至物理上还不存在内容的DDR地址访问结果必然崩溃。解决办法只有一个把整个uboot镜像从当前运行地址整体搬到链接地址让运行地址和链接地址重新对齐。这就是relocate_code存在的意义。5.2 搬移动作之后的“地址修补”光把数据复制过去还不行还有更麻烦的一件事ELF编译产物里存了大量的绝对地址引用这些地址全是链接地址。镜像被搬到正确位置后这些引用恰好对得上号所以无需修改但如果搬移目标和链接地址不完全一致比如你想把uboot运行在一个非链接地址的位置那就需要额外修正所有带重定位信息的地址项。老版本uboot用.rel.dyn段来处理这个问题新版本也保留了类似机制。大约的搬移流程可以用下面的伪代码理解relocate_code: ldr r0, _start 当前代码起始地址运行地址 ldr r1, __image_copy_end 镜像拷贝结束位置 ldr r2, _relocate_target 目标地址一般就是链接地址 copy_loop: ldmia r0!, {r3-r10} stmia r2!, {r3-r10} cmp r0, r1 blo copy_loop /* 搬运完成后还需要做BSS清零、地址修正等 */这个循环一次搬8个32位寄存器效率很高。搬完之后还有几步把BSS段清零、修正全局偏移表GOT、最后把异常向量表也搬到新位置——这一步对应relocate_vectors。5.3 BSS段的“清零仪式”BSS段是C语言中未初始化全局变量和静态变量的存放区它们默认值必须是0。这个“清零”谁来做在uboot里就是relocate流程中紧跟搬运动作之后的一段汇编循环ldr r0, __bss_start ldr r1, __bss_end mov r2, #0 bss_loop: cmp r0, r1 bhs bss_done str r2, [r0], #4 b bss_loop bss_done:这段代码的作用一目了然把从__bss_start到__bss_end这段内存全部写成0。如果这一步漏了你全局变量初始值可能是内存残留的随机值导致后面C代码里的各种状态判断全部失灵——这是极其隐蔽又致命的bug。6. 跳进C世界board_init_f和board_init_r的“双阶段接力”汇编把环境铺好之后真正的大戏开场了调用C语言函数。但uboot这里的安排很特殊——它没有直接跳到main_loop或board_init_r而是先跑了一个board_init_f再回头执行重定位最后才进入board_init_r。为什么要分两个阶段我一开始也觉得多此一举直到自己移植时遇到问题才真正理解。6.1 从汇编最后一跳到C函数第一行在start.S的后续部分可以看到类似下面的代码ldr r0, _board_init_f blx r0这条blx指令执行之后CPU正式踏进C语言世界。blx会先把下一条指令的地址也就是返回地址保存到LR寄存器然后跳转到board_init_f函数。从这一步开始后面的代码就可以用C语言的思维方式组织逻辑了。但值得注意的是此刻CPU仍然运行在SRAM里或NOR Flash里DDR可能刚初始化好但栈还设在临时位置。也就是说这是一个“半成品环境”下的C语言执行。能做的事情有限但足够完成关键决策。board_init_f里做的事情主要是遍历并初始化DDR内存控制器如果lowlevel_init没做完确定relocation后的新栈地址、堆地址、全局数据gd的地址规划整个内存布局把这些地址信息写入gd结构体做一些非常基础的、不依赖复杂外设的初始化这个阶段的名字里有个字母“F”官方全称是board_init_f“F”代表First即重定位前。它执行完成后流程会返回汇编代码触发此前讲到的relocate_code把整个镜像搬过去。等搬移和BSS清理都完成再从汇编跳转到board_init_r——“R”代表Relocated即重定位后。6.2 为什么非得分两段如果只保留一个board_init_r行不行我的回答是不是不行但会非常别扭。原因是C代码运行需要一个像样的内存环境——栈、BSS、堆都要就位。可DDR本身的初始化又必须由C代码或极早期代码来完成。这就形成了一个“鸡生蛋、蛋生鸡”的问题没有DDRC环境不完整没有C环境DDR初始化又写不痛快。UBoot的解法很巧妙先在一个简陋的临时SRAM环境里跑一小段C把DDR调通规划好未来的内存布局然后搬过去在完整环境里跑真正的初始化。这个设计思路在我后来自己写小型bootloader时也借鉴过——先小步快跑再全量展开比一上来就追求“一步到位”要稳妥得多。6.3 启动流程的“终点标尺”main_loopboard_init_r执行完毕后会经历一系列设备驱动初始化网卡、Flash、串口控制台、设备树解析等最终进入main_loop。到了main_loopuboot的控制台就活了能接收你的命令、能自动启动内核。对大多数工程师而言日常接触到的“uboot启动完成”就是这个状态。但真正的全流程是从_start那几行跳转开始一路克服向量表、CPU模式、看门狗、时钟、DDR、栈、重定位最终才在C世界站稳脚跟。每一个环节缺一不可少了任何一环你看到的就不是控制台而是一块沉默的砖。7. 实测排查uboot跑不进C语言时我这样一步步定位学了这么多总归要落到实践上。分享几个我在实际调试中总结的排查经验对照启动流程来用能省下大把瞎试的时间。7.1 串口完全无输出先判断“代码跑没跑”如果uboot烧进去后串口一点反应都没有别急着怀疑串口配置。先问自己三个问题BootROM有没有找到ubootuboot有没有进入_startCPU有没有死在看门狗或DDR初始化里检查启动介质选择引脚或BootROM的启动设备配置这是硬件层面但很多人第一次调板子在这里翻车用示波器或逻辑分析仪看uboot镜像所在存储介质的片选信号有没有被读——BootROM读固件时CS引脚会有动作一眼就能看出来固件有没有被加载如果固件确实被加载了大概率问题出在DDR初始化因为此时串口还没初始化你什么输出都看不到我自己的习惯是在reset的第一条指令处用JTAG调试器打断点单步跟几步观察PC的值是否符合预期。如果在bl lowlevel_init进去之后PC飞了基本就是该函数里DDR时序或PLL配置的问题。7.2 有输出但终止在某个阶段用“打印定位法”uboot自身的调试打印其实遍布各阶段打开CONFIG_DEBUG等宏定义后能输出大量阶段信息。如果你能看到uboot启动早期的调试输出却看不到board_init_r之后的内容那问题基本锁定在重定位阶段地址修正错误、搬移后代码跳飞、BSS清零遗漏这几类是重灾区。我踩过一个很典型的坑某次修改中我在board_init_f里额外申请了一块很大的临时缓冲区结果这个缓冲区的栈位置覆盖了紧邻的镜像区域。搬移时源数据已经被污染搬到DDR后的代码运行起来疯疯癫癫表现就是“明明前面的打印都正常后面的初始化却随机失败”。后来对照启动流程才意识到是早期栈和镜像布局重叠的问题。从那以后我特别关注CONFIG_SYS_INIT_SP_ADDR与镜像尺寸之间的安全余量——跳到C语言之前临时栈不仅要“能用”还要“不越界”。7.3 学完这些你手里多了什么武器理解整套启动流程最大的收获不是背会了几个函数名而是获得了一种“按图索骥”的能力。拿到任何一块新板子不管它用的是ARMv7、ARMv8还是RISC-V你都知道去源码里找什么入口向量在哪里、CPU模式怎么切、栈空间在哪、DDR在哪个阶段被调通、镜像如何搬移。这套思路是跨芯片、跨厂家的因为它本质上是“处理器如何从裸金属运行到操作系统引导”的通用逻辑。我自己后来做bootloader、做系统固件裁剪遇到问题脑子里浮现的始终是这条链路_start→reset→lowlevel_init→ 栈设置 →board_init_f→relocate_code→board_init_r→main_loop。按照这个链条逐段排查几乎没有定位不了的问题。最后一个经验之谈看启动流程源码不要只盯着“这段代码做了什么”更要问“如果这段没做对会发生什么”“为什么它必须放在这个时机做”。带着这两个问题读源码你会发现uboot里每一段汇编都不是白写的每一个跳转都有它存在的硬道理。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

16S rRNA扩增子数据提交NCBI:SRA与BioProject全流程 2026/10/1 9:56:32

16S rRNA扩增子数据提交NCBI:SRA与BioProject全流程

做微生物组的人迟早会撞上这一步:文章投出去,编辑或审稿人在返修意见里加一句,请把 16S rRNA 测序数据存到公共数据库,并在文中给出登录号。第一次碰到的时候我整个人是懵的——原始 fastq 在硬盘里躺了半年,文件名七零…

阅读更多 →
大模型服务器部署全攻略:选型、云资源与内网穿透实践 2026/10/1 9:56:31

大模型服务器部署全攻略:选型、云资源与内网穿透实践

1. 部署前最重要的不是选框架,而是先定位场景我接触过的不少团队,拿到"部署大模型"这个任务后第一反应就是搜框架排名:vLLM 还是 SGLang?群里的朋友推荐了哪个?然后照着最热门的方案拉一个镜像,模…

阅读更多 →
C与Lua混合开发实战:目标平台选型、嵌入流程与性能优化 2026/10/1 9:56:25

C与Lua混合开发实战:目标平台选型、嵌入流程与性能优化

1. 目标平台与技术栈的选型逻辑1.1 为什么“目标平台”决定了整个项目的走向做任何一款游戏或者工具类项目,第一件事不是写代码,而是把“跑在哪儿”这件事想清楚。目标平台这四个字听起来像是立项文档里的一句废话,但实际上它直接决定了你后面…

阅读更多 →
MAS 激活脚本:新手 3 步免费快速激活 Windows 11 与 Office 完整指南 2026/10/1 9:56:25

MAS 激活脚本:新手 3 步免费快速激活 Windows 11 与 Office 完整指南

MAS 激活脚本:新手 3 步免费快速激活 Windows 11 与 Office 完整指南 【免费下载链接】Microsoft-Activation-Scripts Open-source Windows and Office activator featuring HWID, Ohook, TSforge, and Online KMS activation methods, along with advanced trouble…

阅读更多 →
EP_无人机机巢的参数和米定位、对比 2026/10/1 9:56:25

EP_无人机机巢的参数和米定位、对比

EP:Engineering and Project 当前无人机的机场的配置存在两个等级:一、高配,全天候,全适应;二、减配,提高出勤条件、降低出勤效率。而当前大疆无人机机场和道通无人机机巢正是这两类的典型代表,…

阅读更多 →
【MATLAB例程】三维RRT(快速扩展随机树)路径规划与TDOA(到达时间差)定位算法。附完整代码的下载链接 2026/10/1 9:56:25

【MATLAB例程】三维RRT(快速扩展随机树)路径规划与TDOA(到达时间差)定位算法。附完整代码的下载链接

原创代码,包运行成功。讲解、定制可联系我 文章目录简介路径规划模型量测模型运行结果MATLAB源代码简介 程序实现三维快速扩展随机树(Rapidly-exploring Random Tree, RRT)避障路径规划与到达时间差(Time Difference of Arrival,…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉