☰
4412平台U-Boot源码结构全解析:从启动装配到链接脚本
2026/10/17 4:17:24 网站建设 项目流程

嵌入式学到uboot这一站,差不多是大部分人开始怀疑人生的地方。前面还在点亮LED、跑流水灯,突然丢给你几十个目录、上万行汇编和C代码,告诉你这就是"引导内核的钥匙"。我手头这块4412开发板(大家习惯叫讯为4412,其实同类的Exynos 4412板子源码结构都一样)配套的uboot,我前前后后啃了三遍才摸清门道。这篇文章不是逐行讲代码,而是给你一张源码结构地图:哪些目录是主战场、哪些文件决定生死、编译配置和链接脚本怎么串起整个流程、阅读顺序怎么安排最省时间。适合正在用4412(或者三星Exynos系列)学习uboot的同学,也适合任何被uboot源码劝退、想搞清楚bootloader结构的人。

1. 为什么非啃uboot源码不可——先搞懂它在系统里的位置

1.1 uboot到底干了什么活

说白了一点:uboot是一个"引导加载程序",负责在开机之后、Linux内核运行之前,把硬件初始化到可用状态,然后把内核镜像从存储介质里读出来,跳转过去执行。用生活类比的话,它像酒店的门童——客人(内核)到门口之前,门童得先把旋转门通电、擦干净玻璃、核对预约信息,然后开门请客人进去。

很多人觉得uboot不就是"烧录一下、敲几个命令"的事吗?对,如果你只做应用开发,确实可以这么想。但只要你需要移植内核、调试驱动、改启动参数、适配新板子,马上就会发现uboot卡在中间:内核起不来,八成是uboot没把DDR初始化好;网络ping不通,先要看uboot里网卡驱动是否正常。这时候不理解uboot源码结构,你就只能对着串口输出瞎猜。

1.2 4412平台的启动链路

Exynos 4412这颗芯片的启动过程比单片机要绕,但理清楚了会非常爽。它的片内固化了一段只读代码,厂商叫iROM(也有人叫BL0)。芯片上电后CPU先执行这段固化代码,它负责初始化时钟和内存控制器的最基础部分,然后从你选择的启动介质(SD卡、eMMC、串口等)读取下一段代码。

4412的完整启动链大致是:iROM(BL0)→ BL1 → BL2 → U-Boot → Linux内核 → 根文件系统。BL1和BL2在SD卡里有固定的存放位置,iROM会按照固定偏移去读。我们做开发时一般不用管BL0,但要明白一件事:uboot不是"从零开始"运行的,它进来之前,CPU和DDR已经被前面的代码做了基础初始化,uboot接手后要做的是更完整的板级初始化,比如时钟树精调、DDR控制器详细配置、串口初始化、网卡/MMC驱动加载等。

1.3 为什么要先看结构、而不是直接看某段代码

我见过很多同学拿到uboot源码第一件事就是grep -r "DM9000",想直接看网卡驱动。结果看完几百行寄存器操作,回头问"这个函数是谁调用的",答不上来。这就是典型的没有全局观的阅读方式。

uboot源码最大的特点是"分散配置 + 集中调用":几乎所有功能都能通过头文件里的宏开关来控制,编译器根据配置裁剪代码,最后通过链接脚本把所有需要的文件串成一个二进制。你直接看某个驱动,等于在不知道菜谱的情况下研究一盘菜的摆盘——得先知道整桌菜是怎么备齐的,再看单独一道菜才有意义。

2. 顶层源码结构总览——先看地图再进森林

2.1 版本与源码获取

4412学习板配套的uboot,一般基于U-Boot 2010.03或2010.12改造,你打开顶层Makefile看VERSION和PATCHLEVEL就能确认。这类老版本虽然年代久远,但目录结构和现代uboot差异不算大,学起来完全不过时。三星官方还有一个叫"origin"的参考板,很多开发板厂商的uboot都是从这个板级代码改出来的,所以下面提到的目录路径里的origin会反复出现。

拿到源码后别急着解压,先用du -sh看看整体大小,用find . -maxdepth 2 -type d | sort扫一遍二级目录,让脑子里先有个轮廓。我再强调一次:这一步不是浪费时间,是让后面的每一次grep都有方向感。

2.2 顶层目录各自管什么

uboot的顶层目录划分非常清晰,一句话总结就是"架构相关代码放在arch下,板级相关代码放在board下,通用逻辑放在common和lib下,驱动放在drivers下,命令放在common的cmd或专门的cmd目录里"。

目录职责4412平台重点关注程度
arch/CPU架构相关,ARM平台下包括start.S、cpu相关代码高
board/板级代码,每个开发板一个子目录极高
common/核心逻辑和命令行实现高
drivers/各类外设驱动(串口、网卡、MMC、I2C等)高
include/头文件,特别是include/configs下存放板级配置头文件极高
lib/通用库函数(字符串、显示、重定位等)中
net/网络协议栈(tftp、nfs等)中
fs/文件系统支持(ext4、fat等)低(看uboot阶段需要)
tools/主机工具,如mkimage,用于打包内核镜像低
Documentation/说明文档,老版本内容很少低

以4412配套源码为例,你真正高频进出的大概只有三个地方:board/samsung/origin/、arch/arm/cpu/armv7/、include/configs/。其他目录主要起"被调用"的作用——配置头文件里开了一个宏,编译系统就会去对应目录里把相关文件拉进来。

2.3 4412相关源码具体埋在哪

展开源码后,建议先找这几个位置并记住它们,后面所有分析都从这里展开:

  • board/samsung/origin/:板级初始化、DDR配置、网卡配置、低层汇编初始化,都在这里。
  • arch/arm/cpu/armv7/:CPU启动汇编、cache操作、中断控制等架构相关代码。
  • arch/arm/include/asm/arch-exynos/:Exynos芯片寄存器地址定义,看驱动时会频繁引用。
  • include/configs/origin.h:整个uboot的"总开关",所有宏配置都集中于此。
  • include/config.mk:编译配置,由顶层Makefile自动生成,记录ARCH/CPU/BOARD等变量。

先找齐这五个位置,再来谈深入阅读。

3. 关键目录与核心文件逐个拆解

3.1 board/samsung/origin——板级初始化的主战场

这个目录是学习板uboot里最应该先看透的。里面通常有origin.c、lowlevel_init.S、mem_setup.S、config.mk等多个文件。

origin.c里定义了一堆带board_前缀的函数:board_init、board_early_init_f、board_eth_init、board_mmc_init等等。这些函数是板级硬件和uboot核心逻辑之间的桥:核心逻辑说"我要初始化串口",板级代码告诉他"我的串口基地址是0x13800000,波特率分频值这样算"。也就是说,核心里写的是"做什么",板级代码写的是"具体怎么做"。

lowlevel_init.S和mem_setup.S是用汇编写的,前者在CPU刚跳转到SRAM/DDR时执行,负责设置栈指针、关闭看门狗、初始化关键时钟;后者专门配置DDR初始化序列。这是uboot里最"硬核"的部分,不建议初学者死磕每一个寄存器的值,先看懂流程即可。你需要记住的是:lowlevel_init之前的代码运行环境很简陋,连栈都不一定可靠;lowlevel_init之后才开始调用C函数。

3.2 arch/arm/cpu/armv7——CPU启动汇编的接力棒

从arch/arm/cpu/armv7/start.S开始,是所有ARM平台uboot的入口。这段汇编做的事按顺序排列:设置CPU为SVC模式(超级管理员模式)、关闭中断、关MMU和cache、设置栈指针、跳转到lowlevel_init、然后进入C代码。

4412的BSP在这段流程里还会做一件事:重定位(relocate)。简单说,uboot可能一开始运行在SRAM里,等DDR初始化完成后,需要把自身代码从SRAM拷贝到DDR高位地址,再跳过去继续跑。这个"拷贝+跳转"的点,在源码里是relocate_code。你看源码时会发现TEXT_BASE这个宏反复出现,它的含义就是uboot最终在内存中运行的链接基地址,我后面会专门讲。

初学建议:start.S不要试图一遍读懂所有指令,你只需要在代码里画出"当前在哪一步、下一步去哪"的箭头。看懂跳转关系,比看懂每条指令值多少。

3.3 include/configs/origin.h——整个uboot的灵魂

如果只能选一个文件精读,我建议选这个。6410、4412、树莓派、任何开发板的uboot移植工作,七成时间都在改这个头文件。

它里面是清一色的#define宏,分几类:

  • CONFIG_SYS_*:系统级配置,包括内存地址(CONFIG_SYS_SDRAM_BASE)、uboot链接地址(CONFIG_SYS_TEXT_BASE)、uboot在内存中的大小、堆栈大小等。
  • CONFIG_CMD_*:命令开关,比如CONFIG_CMD_PING决定编译时是否包含ping命令。
  • CONFIG_*_BASE:外设基地址,如DM9000网卡的寄存器地址。
  • 使能宏:CONFIG_DM9000、CONFIG_MMC、CONFIG_CMD_NET这些,决定驱动是否被编译进uboot。

这个文件的工作机制是"预处理期裁剪"。你多定义一个CONFIG_CMD_PING,编译系统就会把common/cmd_net.c里的ping实现编译进来;你去掉它,代码直接消失。所以改配置之前先全局搜索该宏在哪些地方被引用,改完之后一定要重新完整编译,不要只make一下就收工。

3.4 drivers——驱动搜索路径的套路

所有外设驱动都在drivers/下,子目录按类型分:drivers/net、drivers/mmc、drivers/serial、drivers/i2c、drivers/mtd等。4412网卡如果是DM9000,驱动就在drivers/net/dm9000x.c。

想找某个驱动的入口函数,最靠谱的方法是看它对应的结构体。以dm9000为例,文件里会定义一个struct eth_device并填充init、send、recv等函数指针。uboot核心代码通过调用eth_init、eth_send间接跳到这些函数。驱动开发者的工作就是填充这些函数指针。理解了"函数指针表承载驱动接口"这个模式,再看MMC、串口、I2C驱动,完全一通百通。

3.5 common——命令行和核心流程所在

老版本uboot(2010.03/2010.12)启动主流程是start_armboot,定义在lib_arm/board.c里。它会按顺序调用一大串初始化函数,比如board_init、env_init、serial_init、net_init等。初始化完成后进入main_loop,等待用户输入uboot命令。

命令的实现文件分两类:一类是common/cmd_*.c,比如cmd_ping.c、cmd_mmc.c;另一类在common/根目录下,比如cmd_bootm.c。每个命令通过U_BOOT_CMD宏把自己注册到一个特殊的段(.u_boot_cmd)。这个宏会生成一个结构体,包含命令名、帮助信息、参数提示、处理函数指针。链接脚本会收集所有这类结构体,uboot运行时就能遍历命令表、匹配用户输入、调用对应处理函数。

很多初学uboot的同学想添加自定义命令,问"要改哪些文件",答案就是:新建或修改cmd_xxx.c,里面用U_BOOT_CMD定义一条命令,然后在include/configs/xxx.h里加上CONFIG_CMD_XXX配置即可。就这么简单,但背后是"段收集"这个机制在起作用。

4. 从编译配置到链接脚本——结构如何串起来

4.1 配置过程到底发生了什么

拿到uboot源码第一件事通常是make origin_config,这个命令的含义是"生成origin板配置"。它调用顶层Makefile里的规则,核心动作是执行mkconfig脚本,根据boards.cfg里的配置组合,生成两个关键文件:

  • include/config.mk:内容类似ARCH=arm、CPU=armv7、BOARD=origin、VENDOR=samsung、SOC=exynos,供Makefile使用。
  • include/config.h:内容只有一个#include <configs/origin.h>。

这就有意思了:你改了板级配置文件(比如新加一个宏),下次编译时Makefile检测到依赖变化,会重新处理config.h并自动展开宏,但如果你改了include/configs/origin.h之外的某个被间接包含的文件,有可能因为依赖关系不完整而没触发重编。所以改配置后最稳的操作是make clean后再make,或者干脆删掉include/autoconf.mk(这是另一个由config.h自动生成的文件)再编。

这里我补充一个为什么经常有人"改了宏没效果"的底层原因。uboot编译时,真正的自动配置头文件是include/autoconf.mk,它由include/config.h经预处理后生成。也就是说,你直接改origin.h,理论上autoconf.mk会在配置阶段重新生成。但如果你用的是IDE的"增量编译",或者make依赖检测漏掉了某些头文件,就会导致autoconf.mk还是旧内容。明白这个链路,排查配置不生效问题就有方向了。

4.2 链接脚本如何把一切串起来

uboot编译完成后会链接成一个u-boot文件,而告诉链接器"代码放哪、数据放哪、哪个段靠前"的,是链接脚本u-boot.lds。4412配套BSP里它一般在board/samsung/origin/下,也有版本放在顶层目录,你可以用find . -name "u-boot.lds"来找。

链接脚本的核心内容类似这样(实际条目更多):

SECTIONS { . = 0x43e00000; .text : { *(.text.start) *(.text) *(.rodata) } __u_boot_cmd_start = .; .u_boot_cmd : { *(.u_boot_cmd) } __u_boot_cmd_end = .; __bss_start = .; .bss : { *(.bss) } __bss_end = .; }

你看懂三个点,链接脚本就没障碍了:

第一,.表示当前地址,一开始设成0x43e00000,这个值就是CONFIG_SYS_TEXT_BASE,它决定了uboot代码段被链接到内存的什么位置。第二,.u_boot_cmd段专门收集所有U_BOOT_CMD宏生成的结构体,__u_boot_cmd_start和__u_boot_cmd_end这两个符号标出命令表的边界,uboot运行时就是从这个边界之间遍历命令的。第三,.bss段放未初始化的全局变量,链接脚本记录__bss_start和__bss_end,start.S会在早期把这段内存清零。

很多人混淆"链接地址"和"加载地址",这里必须说清楚:链接地址是代码中所有绝对地址计算的基准,uboot里所有函数指针、全局变量地址都是按照TEXT_BASE算出来的;加载地址是uboot二进制实际被放在存储介质中的位置。启动时,SD卡里的uboot可能存放在某个固定偏移处,但它会被拷贝到以TEXT_BASE为起点的RAM区域再运行,只有这样才能保证代码里的绝对跳转不跑飞。如果拷贝地址和TEXT_BASE不一致,最常见的现象就是"运行到一半跳飞"或者"函数能跑、全局变量乱值"。

4.3 编译产物与镜像生成流程

编译完成后,源码根目录会出现u-boot(ELF格式,带符号表,调试用)、u-boot.bin(裸二进制,烧录用)、u-boot.map(符号映射表,查函数在内存中的位置)。学习板配套的uboot还会有一个u-boot-sd.bin或者类似的打包文件,里面有可能把BL1、BL2和uboot拼接在了一起,这是因为4412的iROM要从SD卡固定位置读取BL1,厂商在编译后期用脚本把多段镜像拼成一个文件。

如果你想知道某个函数最终被放到了哪个地址,看u-boot.map最直观。比如你想确认board_init的地址,grep board_init u-boot.map即可。这在你调试跳转问题时是救命工具。

5. 阅读源码的实战路线图——建议的阅读顺序

5.1 主线优先:从开机到命令行的路径

我建议所有初学者按这条主线读第一遍,目标不是记住每个函数,而是画出调用链:

arch/arm/cpu/armv7/start.S(汇编入口)→lowlevel_init.S(时钟、DDR)→start_armboot(C代码主流程,老版本)或board_init_f/board_init_r(新版本拆分)→main_loop(命令行循环)。

这条线走通之后,你对uboot的"骨架"就有感觉了。具体操作是:打开源码,先用grep -rn "start_armboot"找到C入口,用grep -rn "board_init_f\|main_loop"顺藤摸瓜,并在纸上画出函数调用树。不要小看这个"纸上画树"的笨办法,它比任何IDE的关系图都管用,因为你画的过程就是思考的过程。

5.2 分支扩展:命令和驱动的读法

主线读完后,再做三个分支练习。第一个是"加一条命令",比如仿照现有cmd_led.c写一个cmd_hello.c,用U_BOOT_CMD注册,编译进uboot后敲help看命令是否出现。这个练习能帮你彻底理解命令表的注册和收集机制。第二个是"读一个驱动",选dm9000网卡驱动,顺着board_eth_init找到驱动初始化,再把eth_send到实际寄存器操作走一遍。第三个是"改一个环境变量默认值",比如默认的bootcmd(自动启动命令)或bootargs(传给内核的参数),你会接触到env_default函数的实现。

这三个练习做完,你基本已经入门了。剩下的比如uboot引导内核的bootm流程、设备树支持(如果版本够新的话),可以在需要时再针对性研究。

5.3 工具与技巧:怎么查得又快又准

阅读uboot源码的必备工具是grep和ctags,IDE选Source Insight、VSCode都行,但记住一点:IDE只是辅助,不能替代命令行检索。我在实际工作中最常用的命令是:

grep -rn "CONFIG_DM9000" --include="*.h" --include="*.c" .

这条命令能帮我找到某个配置宏影响了哪些文件。另一个高频率操作是生成tags:

ctags -R .

然后无论用Vim还是VSCode,都能一键跳转到函数定义。这里分享一个很实用的习惯:给uboot源码建一个"阅读日志"文件,每搞懂一个函数/流程,就用自己的话记下来,包括函数名、调用者、关键参数、注意事项。很多人觉得记笔记耗时,但uboot这种老代码,你今天看懂了board_init_f,明天不看笔记一定忘。笔记是你的"第二大脑"。

6. 常见问题与排查技巧实录

6.1 配置阶段报错"Can't find board/origin"

典型的boards.cfg配置不匹配问题。检查顶层boards.cfg或Makefile里新板子配置行的字段顺序和board/samsung/origin/目录是否一致,尤其注意Board名、Vendor、SOC列不能写错。老版本uboot里make xx_config会调用mkconfig脚本,它按行拆分配置并生成include/config.mk,如果boards.cfg里找不到对应行,就会提示找不到板级目录。

6.2 改了配置头文件,编译后没变化

这个问题踩的人最多,根因我已经在前面解释过:配置生效依赖于include/autoconf.mk被重新生成。新手最容易踩的坑是在include/configs/origin.h里改了宏,但Makefile依赖关系没捕捉到这个头文件的变化,直接make还是旧配置。解决办法:

make clean rm -f include/autoconf.mk make

如果还不行,直接搜一下include/autoconf.mk里有没有你新加的宏,避免"以为自己加了其实没加"。

6.3 串口完全没有输出

uboot阶段串口没输出的原因很多,按顺序排查:先确认串口硬件连接和波特率(常见115200);再看CONFIG_SERIAL_BASE或CONFIG_S5P_UART这类宏是否和板子原理图一致;然后确认lowlevel_init里时钟初始化是否完成、PLL配置是否正常;最后确认TEXT_BASE和uboot实际加载地址是否匹配。如果uboot压根没跑起来,通常不会有任何输出;如果跑一半死掉,串口可能会输出几个乱码字符或者半句话。

这里分享一个经验:老版本uboot的串口初始化分成两段,一段在汇编阶段(lowlevel_init前后,通常只做最基础的UART寄存器配置),真正完整初始化在serial_init。如果你发现前一段没输出,优先怀疑时钟配置;如果能看到uboot版本信息但停住,优先怀疑DDR配置或环境变量加载有问题。

6.4 下载代码后跳飞、死机

跳飞最直接的原因往往是TEXT_BASE和实际加载地址不一致。我用4412开发板时就踩过这个坑:SD卡烧写工具把uboot放到了0x43e00000,而我编译时TEXT_BASE写成了0x43f00000,结果代码执行几十秒后随机死机。排查方法就是用调试器(J-Link或OpenOCD)查看PC指针跑到哪去了,再回头检查TEXT_BASE。

6.5 网络不通、ping不通主机

uboot阶段网卡问题一般集中在三处:驱动初始化失败(硬件复位引脚没配好)、MAC地址没设置(ethaddr环境变量为空)、网络协议栈的IP配置不对。建议先用uboot的dhcp命令测试自动获取IP,再ping主机地址。如果ping通但tftp下载老卡住,检查防火墙和tftp服务端配置,这已经是uboot之外的问题了。

一个额外的调试技巧:把uboot的编译日志保存下来,编译报错时从日志尾部往前翻,大部分问题会在第一个报错处暴露。不要只看最后一行错误,往前多翻20行往往才是真正的坑。

6.6 如何验证自己真的看懂了结构

最好的验证方式是关掉源码,花15分钟在白纸上默写整个启动流程:从start.S入口到main_loop,每一阶段大概做了什么、分别调用了哪些关键函数、哪个宏控制了哪段代码的编译。能默写出来,说明结构已经内化;写不出来的部分,就是你下一步要回去补的地方。

7. 最后分享一点实际体会

带过不少人看4412的uboot源码,我发现能不能啃下来,跟基础关系不大,跟"会不会用搜索"和"能不能忍得住不求甚解"关系很大。uboot代码量大,但真正需要精读的核心文件不超过十个。第一遍读切忌逐行死磕寄存器操作,先抓函数调用关系,再回头看细节。我个人的习惯是先把start.S、lowlevel_init、start_armboot、main_loop、origin.h这五个文件读熟,之后再看任何驱动和命令都是降维打击。

还有一个很实用的小技巧:给源码加注释不是浪费时间。你可以复制一份源码,在里面用中文标注你理解的逻辑,比如在board_init函数前写一句"此处初始化网卡和MACH ID"。这份注释版源码就是你以后快速检索的利器。等到你要移植新板子时,直接复制板级目录,改头文件里的宏和寄存器地址,启动链路不用重学,靠的就是当初这份积累。

下一步你可以沿着uboot引导内核这条线继续深入,重点研究bootm命令的处理流程和内核镜像的格式解析。那会是整个嵌入式引导链路最有意思的部分之一。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询