Uboot启动流程解析
本文详细解析了嵌入式系统的启动流程和U-Boot源码实现。启动过程分为三个阶段:iROM阶段(加载BL1到SRAM)、SRAM阶段(硬件初始化)和DRAM阶段(操作系统引导)。
仅做为学习记录
理解嵌入式系统的启动流程和U-Boot源码,是深入嵌入式开发的关键一步。这不仅有助于解决实际启动问题,更能提升对系统底层运作的认知。作为BSP底层开发工程师,对于Uboot的学习至关重要,所以在这里以嵌入式系统为例,重点聚焦U-Boot的启动流程及其关键源码实现。
FS4412 SOC的启动过程

解析图中的内容:
Cortax A9 :其是芯片核心,也就是中央处理器——CPU。
Internal Rom :是一个只读存储器,里面存储了代码,总大小为64K。它的功能是用于读写pin脚,其作用是用来告诉系统从何处去读取uboot代码。uboot的代码可以从Nand、SD/MMC、eMMC、USB OTG等地方去启动。
也就说uboot的代码可以存储在外部的存储介质里面,通过拨动Opertaing Mode Pin(拨码开关),来选择从其中某个介质里去读取。而这些读取的驱动代码就集成在iROM之中。
其次iROM还有一个功能是读取外部的代码,将其读入内部的 Internal SRAM 之中,也就是BL1和BL2(未画)。BL代码就是我们自己写的BootLoader代码,然后程序计数器(PC指针)就会从iROM移动到Internal SRAM内部,执行我们的BootLoader代码。
(在这里简单说一下程序计数器(PC指针):PC指针存储当前正在执行指令的下一条指令地址。CPU每执行一条指令后,PC会自动递增(或根据跳转指令修改),指向新的待执行指令地址,驱动程序流程推进。当iROM将BL1/BL2代码加载到SRAM后,需要主动修改PC指针的值,将其指向SRAM中BootLoader代码的起始地址。此后CPU便从SRAM中取指令执行,实现控制权移交。)
Internal SRAM :集成在芯片内部的内存。执行SRAM的代码,也就是开始执行我们的BootLoader代码,它会完成一些硬件的初始化工作,如: 初始化时钟和 我们上面的 DramController控制器 等,这样我们才能够使用DRAM内存。接着将OS加载到DRAM之中。
DRAM :uboot初始化结束之后,会将控制器交给DRAM之中的OS,这时候我们的可访问内存空间的大小就由256K扩展到外部的DRAM大小。
在这里简单说一下Dram和Sram,以及为什么bootloader加载到SRAM而OS加载到DRAM:

简单的说,就是因为SRAM体积小成本高,适合那种不需要初始化马上可以用的小任务,然后把Dram初始化之后,把大体积的操作系统OS整个加载到DRAM中去慢慢运行 ,这就是为什么SRAM加载bootloader而DRAM加载OS
总结一下:
- 芯片的启动过程就是由iROM开始,读取BL到SRAM。
- 然后执行Internal SRAM的BootLoader代码,初始化硬件,加载OS到DRAM之中。
- 接着BootLoader将控制器交给DRAM的OS,至此整个SOC的启动流程的分析完成了。
(传统的)u-boot的启动流程
在进行uboot的链接启动流程分析之前,我们需要先看一下uboot的链接脚本。在C语言中,程序可以分为以下几个部分:
- 代码段(.text)
- 数据段(.data)
- 只读数据段(.rodata)
- 未初始化的数据段(.bss)
- 堆和栈
其中堆和栈属于动态区域,在程序运行时动态分配和释放。而代码段、数据段、只读数据段在链接之后产生。
一个C语言程序分为映象和运行时两种状态:
- 映象只包含代码段、数据段、只读数据段;
- 运行时还将包含动态形成的堆和栈区域;
而链接脚本的作用就是:
- 指定代码段和数据段、只读数据段在内存中的存放地址;
- 指定代码的入口地址;
如:uboot-2012.04.01的uboot的lds文件,其部分代码如下:

然后再给出一个他的详细注释版:
/* --- 全局设置:定义程序的基本属性 --- */
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
/* 目标文件格式:ARM架构的32位小端ELF格式 */
/* 三个相同参数表示无论默认/大端/小端模式都使用此格式 */
OUTPUT_ARCH(arm)
/* 目标处理器架构:ARM系列芯片 */
ENTRY(_start)
/* 程序入口点:_start符号(在start.S中定义)*/
/* CPU上电后第一条执行的指令地址 */
/* --- 内存布局定义开始 --- */
SECTIONS
{
/* 当前位置计数器置零(虚拟地址基准)*/
/* 实际运行时会被加载到物理地址(如SRAM的0x02020000)*/
. = 0x00000000;
/* 4字节对齐(ARM体系结构要求)*/
. = ALIGN(4); // 确保后续代码从4倍数地址开始
/* --- .text段:存放所有可执行代码 --- */
.text :
{
/* 关键符号:镜像复制起始地址 */
__image_copy_start = .;
/* 在重定位时计算源地址偏移: */
/* 实际地址 = __image_copy_start + (物理加载地址 - 0x0) */
/* 强制特定文件顺序(启动依赖)*/
CPUDIR/start.o (.text) // 1. CPU核心初始化代码
board/samsung/smdk2440/libsmdk2440.o (.text) // 2. 开发板特定初始化
*(.text) // 3. 其他所有目标文件的代码段
}
/* --- 段间分隔符 --- */
. = ALIGN(4); // 4字节对齐分界
/* --- .rodata段:存放只读数据 --- */
.rodata : {
/* 优化指令:双重排序保证内存紧凑 */
*(SORT_BY_ALIGNMENT( // 先按对齐要求排序(避免内存空洞)
SORT_BY_NAME(.rodata*) // 再按名称排序(提高缓存命中率)
))
/* 包含:字符串常量、只读全局变量等 */
}
/* --- 段间分隔符 --- */
. = ALIGN(4); // 4字节对齐分界
/* --- .data段:已初始化的全局变量 --- */
.data : {
*(.data) // 如:int debug_enabled = 1;
}
/* --- 段间分隔符 --- */
. = ALIGN(4); // 4字节对齐分界
. = .; // 显式声明当前位置(增强可读性)
/* --- U-Boot命令表专用段 --- */
/* 符号标记:命令表起始位置 */
__u_boot_cmd_start = .;
.u_boot_cmd : {
*(.u_boot_cmd) // 收集所有U_BOOT_CMD宏定义的命令结构体
}
/* 符号标记:命令表结束位置 */
__u_boot_cmd_end = .;
/* 命令表在U-Boot中的应用:*/
/* for(cmd = __u_boot_cmd_start; cmd < __u_boot_cmd_end; cmd++) */
}
这里简要说明一下:
从上面的代码我们可以看到有**.text段,其指定了所有uboot文件的.text段的存放位置**。往下是**.rodata段,其指定只读数据的存放位置**,紧跟着.text段,并且进行了4字节的对齐操作。再往下是数据段.data的存放位置。其中有一个__u_boot_cmd_start,它充当一个标号的作用,其指定了boot_cmd的起始地址,cmd也就是我们在boot的shell中使用的那些指令,而__u_boot_cmd_end指定的是结束地址。
uboot程序的入口地址,在第3行的 ENTRY(_start) 处指定了 。
uboot代码追踪
我们可以使用 arm-linux-nm u-boot 显示我们生成的uboot文件的符号信息。节选部分如下:

有了这个命令后,我们就可以很方便的查找到我们的uboot启动代码的地址, 通过 arm-linux-nm u-boot | grep _start,我们找到我们的起始地址 ,其输出信息如下面:

知道了地址,我们怎么定位文件的位置呢?
可以通过 arm-linux-addr2line 来定位,其可以将地址转成行号输出显示出来
我们 可以通过 arm-linux-addr2line -e u-boot 33f80000 找到我们起始地址所在的文件,以及其在文件中的位置 ,输出如下:
![]()

上图为uboot的启动流程代码:
- 首先会先执行start.S,其主要完成的工作将CPU设置SVC模式(保护模式)、关闭中断看门狗等;
- 接着会执行lowlevel_init.S的代码,主要完成一些列初始化工作;
- 接着开始进入C语言程序,在进行C语言之前需要先开辟一个栈空间,由ctr0.S来完成;
- 进入C语言程序后,执行board_init_f()函数,进行自搬移操作前期的内存分配;
- 自搬移由crt0.S来完成,然后第二次初始化C语言的运行环境,接着程序就完全进入C语言环境执行;
- board_init_r()会进行MMC和网络初始化,然后进入自启动模式,如果在倒计时之前按下回车就进入main loop循环,也就是我们看到的uboot的shell界面。
启动流程解析
用通俗易懂的语言对上面的图和文字做更加详细的解释就是:

1️⃣ 临时工棚阶段(汇编裸奔)
相当于在工地旁搭临时帐篷干活
- 做什么:
- start.S:给CPU保安戴工牌(设保护模式)→ 关大门避免干扰(关中断)
- lowlevel_init.S:指挥水电工布线(时钟)、挖地基(内存)、接水管(串口)
- 为什么:
刚通电时系统像毛坯房,连桌椅都没有(内存未初始化),只能在狭小的工棚(SRAM)里做最基础的准备工作。
2️⃣ 搭临时办公点(C语言初体验)
在工棚里搭建临时办公室
- 做什么:
- crt0.S:搬几个塑料凳当座位(栈空间)→ 勉强能开小组会
- board_init_f():规划新房图纸(计算搬运路线)
- 关键限制:
工棚又小又挤(SRAM只有256KB),无法展开大型家具(完整U-Boot),必须先为新家(DRAM)做规划。
3️⃣ 新房装修(惊天大搬运)
把整个公司搬到新大楼
- 核心操作:自搬移 (crt0.S流程)
- 把公司所有文件(U-Boot代码)整箱打包复制到新地址
- 修正所有文件标签(重定位:更新函数地址)
- 在新大楼重建办公室(
第二次初始化C环境)→ 这次有真皮沙发(完整栈) - 大扫除清空地下室(
.bss段清零,全局变量归零)
4️⃣ 正式营业(迎接用户)
新公司开张迎客
- 做什么:
- board_init_r():布置各部门
💾 安装文件柜(MMC存储)
🌐 开通网络专线(网卡驱动)
🔌 调试电力系统(设备树解析) - 最终状态:
倒计时5秒... # 自启动模式 按回车进入→ [U-Boot] # 您看到的命令行
- board_init_r():布置各部门
为什么需要两次C语言初始化?
| 第一次(临时工棚) | 第二次(新房大楼) |
|---|---|
| ❗空间限制:SRAM仅256KB | ✅ 解放空间:DRAM有512MB+ |
| ⚠️ 只能放折叠椅(基本栈空间) | 🛋️ 全套办公家具(完整栈+堆) |
| 🧹 没时间打扫(不处理BSS段) | 🧼 全面保洁(BSS段清零) |
相当于:工棚会议只能站着开会(第一次初始化),搬进写字楼才能开正式董事会(第二次初始化)
自搬移:嵌入式系统的魔术
- 为什么必须搬?
😱 工棚就在新房地基上(SRAM地址被内核占用),不搬就会被推土机压扁! - 如何无伤搬运:
1️⃣ 工头记住所有文件位置(__image_copy_start标记起点)
2️⃣ 克隆文件到新地址(memcpy整段复制)
3️⃣ 更新所有名片地址(重定位:旧地址+偏移量=新地址)
最终视图:U-Boot启动全貌
我用自己的话再说一遍Uboot 的启动流程:芯片上电的时候,CPU从start.C开始执行,首先把CPU设置成SVC模式,并且关闭中断,看门狗等功能,是因为防止在启动期间CPU被打扰。然后,会执行lowlevel_init.S代码,对一些基本的外设进行初始化(时钟,内存,串口等)。然后运行crt0.S代码,分配一个小小的栈空间,为的是能开辟一个小小的空间给C代码运行。然后运行board_init_f(),运行board_init_f()是为了自搬移做一些前期的准备(具体工作是定义在DRAM中所需要的大小,起始的地址)。接下来就是继续运行crt0.S代码,对uboot程序进行自搬移,从SRAM转移到DRAM中去执行。然后crt0.S在DRAM中会开辟一个完整的堆栈,给C程序(board_init_r())进行完整的运行(对设备完整进行初始化,加载驱动,加载设备树),并且清除掉.bss文件,到这里uboot启动程序就大致完成了,进入自启动或命令行状态
uboot源码分析
了解了整个uboot大致的启动流程后,我们就可以开始着手进行uboot的源代码分析了。我们从start.S文件开始分析,也就是uboot的入口地址_start,其节选代码如下:

在第3行有一条跳转指令 b reset 跳转到reset标号处去执行,在reset下面的那些 ldr xxxx 指令是用于后面设置中断向量表使用的。reset标号处的代码如下:

从注释中我们就可以看到,其作用就是关闭中断,将CPU设置为SVC32模式。(reset)
接着_start的代码往下走:
从注释中我们可以看到,行2~14大致完成内容是设置SCTRL寄存器,也就是系统控制器 以及设置中断向量表 ,这个中断向量表的内容就是我们前面在reset下面看到的那些 ldr xxxx 指令,这个需要借助CPU的协处理器CP15进行设置,具体细节不深究。
/* 相当于给120/110设置快速拨号 */
mrc ... // 解锁CPU的隐藏控制面板
bic r0, #CR_V // 关掉原厂默认的急救电话
mcr ... // 确认操作
ldr r0, =_start // 把我们的急救电话簿(_start位置)
mcr p15,0,r0... // 刻到CPU的快速拨号键(VBAR寄存器)
完成上面操作后, 会跳转到cpu_init_cp15和cpu_init_crit标号执行,最后转到_main函数去执行 。
bl cpu_init_cp15 // 医生检查CPU核心状态
bl cpu_init_crit // 检测心跳(时钟)、呼吸(内存)

我们一步一步进行分析,先看cpu_init_cp15标号处的代码:

从代码的注释我们可以看出, 该标号处的代码主要完成关闭缓存、关闭虚拟内存MMU的作用 ,以及设置了diagnostic寄存器。

接着我们回到reset处的cpu_init_crit继续往下追踪:

cpu_init_crit处的代码很简单,就是调用lowlevel_init进行初始化 。
由流程图和注释我们可以知道, lowlevel_init主要完成板级相关的一些初始化工作,如:时钟、内存、网卡、串口的初始化 。
BSP工程师(Board Support Package工程师)在移植U-Boot时通常需要编写或修改lowlevel_init.S文件,以适配具体硬件的外设初始化需求。

接着我们回到reset处的_main继续往下追踪。_main标号定义在文件arch/arm/lib/crt0.S里,可以通过arm-linux-nm和arm-linux-address2line进行定位。
下面我们对_main进行追踪:

从注释里我们可以看出, _main首先初始化了一下C的运行环境,接着调用board_init_f函数 。除此以外进行sp寄存器的设置,也就是第10行处的代码,还有在栈空间里为 GD 变量分配了内存。

接着我们继续追踪board_init_f:

其中有两个最重要的全局变量 bd_t *bd 和 gd_t *id 它们用于存储一些信息,这两个全局变量的存储空间就是在调用board_init_f之前已经在栈中进行分配了。
board_init_f主要完成两个工作,一是对 gd_t *id 进行初始化,二是它会调用一些列的函数(在第10行处的代码)完成一些列工作。 GD (也就是 gd_t *id )主要是记录一些地址信息,用于后续的自搬移操作使用。

用自己的话总结:所以说bd是计算Uboot需要的内存大小,gd是规划uboot在DRAM中的目标地址,这两个变量都是在第board_init_f中进行定义的,所以说这个函数至关重要,因为只有进行了这个函数才可以进行下一步的uboot自搬移
接着我们回到_main,接着board_init_f继续往下看:

在最后一行有一个 b relocate_code 跳转指令,从名字我们就可以知道,其完成的工作就是对代码进行自搬移操作,接着我们继续往下看:

自搬移结束后,就准备进入C语言环境运行了。
从注释中可以看到设置了bss段,也就是对bss段进行了清零,标号 clbss_l 标号处的循环代码。

之后就会通过board_init_r标号,跳转到C语言函数,我们继续追踪board_init_r:

board_init_r会打印一些列的信息到串口,然后会进入main_loop()循环,main_loop会进行倒计时,如果此时按下回车就会进入uboot的shell交互界面,否则就会自动引导启动OS系统。

总结
uboot的启动如下,假设我们使用SD卡进行启动:
-
iROM会将SD卡内的BL1加载到iRAM中,然后将控制器交给BL1;
-
BL1会将BL2以及填充,一共16K的代码,加载到iRAM中;
uboot由两部分组成,一部分是SPL完成硬件相关操作,另一部分是uboot代码。
-
接着SPL部分会将uboot代码加载DRAM之中,此时uboot位于DRAM的内存地址0x43E00000处;
-
uboot加载需要加载Linux系统的uImage到内存,一般加载到0x4008000处,为了防止把uboot自己给覆盖了,所以uboot还需要进行一次自搬移操作;
-
然后CPU跳转新的uboot地址执行,这时我们就能看到串口打印一些列控制信息了。
一、嵌入式系统启动流程精要
1. 芯片级启动(iROM阶段)
- 硬件基础:
- iROM固化64KB代码(厂商提供)
- 通过拨码开关选择启动介质(SD/eMMC/NAND等)
- 核心任务:
- 加载BL1(SPL/U-Boot头部)到SRAM
- 移交执行权:PC指针跳转至SRAM入口
2. SRAM阶段(BootLoader初始化)
- 关键操作:
- 时钟/内存控制器初始化(使能DRAM)
- 串口初始化(建立调试通道)
- 加载完整U-Boot到DRAM
- 空间限制:
- SRAM仅256KB → 仅存放核心初始化代码
3. DRAM阶段(操作系统引导)
- 两阶段过渡:
- U-Boot自搬移(避免被内核覆盖)
- 设备树/内核加载到DRAM
- 终点:移交控制权给Linux内核
启动流程关键函数
| 函数 | 执行位置 | 核心职责 | 技术亮点 |
|---|---|---|---|
start.S |
SRAM | 关中断/SVC模式/关MMU | 协处理器操作(mcr/mrc) |
lowlevel_init.S |
SRAM | 时钟/内存/串口初始化 | BSP工程师定制点 |
board_init_f() |
SRAM | 规划自搬移地址(gd/br结构体) | 栈空间预分配技术 |
relocate_code() |
SRAM→DRAM | 代码自搬移+地址重定位 | 动态修正函数指针 |
board_init_r() |
DRAM | 初始化网卡/存储/加载内核 | 无限循环容错设计 |
至此uboot的启动流程大致分析就结束了。
更多推荐



所有评论(0)