首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
深入解析U-Boot编译流程:从Makefile到可烧写镜像
📅 2026/10/6 19:21:28
✍️ 爱科研究院
👁 阅读 3,247
1. 搞懂编译流程比追着启动代码看更省时间很多刚接触uboot的人都有一个通病拿到一份uboot源码迫不及待打开start.S从第一条汇编开始啃启动流程。我不能说这条路完全不对但如果你连这份uboot是怎么从一堆.c、.S文件变成一个u-boot.bin或u-boot.imx的都不清楚那启动代码里很多条件编译、宏开关、链接地址的意义你其实是看不懂的。我见过太多人卡在一个问题上为什么我改了include/configs/xxx.h里的配置重新make之后u-boot.bin的大小不变为什么我明明加了某个驱动编译却没把它编进去这些问题的答案全都在uboot编译流程里。这篇文章是uboot流程系列的第四章重点只讲一件事从执行make xxx_defconfig到最终拿到可烧写镜像uboot到底经历了什么。我会结合imx6ull和4412这两个常见平台的移植场景来展开因为这两个平台的编译和打包方式有非常典型的差异搞清楚它们你再看其他SoC的编译逻辑会轻松得多。阅读本文不需要你已经读完启动流程那一章但建议你对Makefile有基本概念——至少知道make是干嘛的。如果你完全没接触过Makefile建议先把make的基本语法过一遍不然中间很多细节会有点晕。当然我也会把关键机制用白话讲清楚。2. 顶层Makefile设计Kbuild体系下的三段式编译uboot从2014年左右开始全面采用Linux内核的Kbuild构建体系这套体系把整个编译过程分成三个核心阶段配置config→ 编译build→ 打包package。理解这三段式你就抓住了uboot编译的骨架。2.1 Kbuild体系和老版Makefile的区别早期的uboot比如2010年以前的版本编译方式是执行make xxx_config然后Makefile读取一个config.mk文件再逐层进入子目录编译。那个时代uboot的Makefile几乎是“一棵大杂草”每个人移植一个新板子都要在顶层Makefile里加一大堆东西。Kbuild体系彻底改变了这个局面。它的核心思想是每个目录下的Makefile只负责描述“这个目录里有什么文件要编、编成什么”而“怎么编、用什么参数编”由顶层的规则统一控制。子目录的Makefile不再关心编译器参数、链接脚本这些全局的东西。用一句话概括Kbuild的套路顶层Makefile定义所有规则和变量子目录Makefile只提供目标和源文件清单编译过程由scripts/Makefile.build这个通用引擎驱动。2.2 三段式流程的宏观视图一个完整的uboot编译过程输入是一份源码包输出是若干镜像文件中间跑的是这样一条流水线阶段执行的命令核心产出作用配置make xxx_defconfig.config记录所有开关选项和参数编译make -j8u-boot.bin、u-boot、各.o把源码变二进制打包编译阶段末自动触发u-boot.imx、u-boot-spl.bin等按SoC要求生成可烧写格式注意第三阶段不是独立命令而是在make的最后通过顶层Makefile里的规则自动执行的。这也是很多人编译完发现u-boot.bin有了但u-boot.imx没有的原因——你没有定义相应的打包规则或者CONFIG选项没开。2.3 顶层Makefile的读法uboot的顶层Makefile虽然很长几千行但它不是一个线性执行的文件。你要学会“跳着看”。我建议你打开源码后先搜这几个关键目标%config处理所有xxx_defconfig格式的目标u-boot生成ELF格式的ubootu-boot.bin从ELF转成纯二进制u-boot.imx、u-boot.sb等特定SoC的打包规则搜到这些目标后你会发现整个Makefile的骨架就出来了。其余大部分内容都是在为这几个目标服务——要么生成变量要么定义规则。3. make xxx_defconfig一个配置如何变成.config配置阶段是大多数人“知其然不知其所以然”的部分。很多人只知道要执行make xxx_defconfig但这条命令背后发生了什么完全黑盒。3.1 defconfig文件在哪里先解决一个最常见的问题xxx_defconfig在哪个目录答案是configs/目录。你会在里面看到大量类似imx6ull_14x14_evk_defconfig、smdk4412_defconfig这样的文件。但是注意configs/目录下的这些defconfig文件内容非常精简。以imx6ull的defconfig为例里面可能只有几十行大部分选项都没有出现。那么问题来了没有列出的选项怎么办答案在Kconfig文件里。每个功能模块都在源码树的对应目录下有一个Kconfig文件里面用config关键字定义了所有可能的选项以及它们的默认值。defconfig文件里的选项本质上是对默认值的“覆盖”。3.2 配置过程的四个步骤执行make imx6ull_14x14_evk_defconfig其实发生了这样几件事顶层Makefile匹配到%config规则识别出目标名imx6ull_14x14_evk_defconfig调用scripts/kconfig/conf工具读取arch/../configs/imx6ull_14x14_evk_defconfig文件conf工具根据defconfig里的选项结合各目录Kconfig的默认值并检查依赖关系生成最终的.config同时生成一系列头文件包括include/config.h、include/config/auto.conf等你可以自己试一下执行完make xxx_defconfig后打开根目录的.config里面可能有几千行配置项。再对比configs/下的defconfig你会发现defconfig只是冰山一角。3.3 两个让人困惑的配置体系这里有个历史遗留问题让很多初学者抓狂uboot里居然有两套配置体系同时存在。一套是Kconfig/.config另一套是include/configs/xxx.h里的宏定义。以imx6ull为例你在include/configs/imx6ull_14x14_evk.h里会看到一堆#define CONFIG_SYS_...这些和.config里的CONFIG_...是什么关系两套体系的本质区别.config里的CONFIG_xxx是编译期开关控制某个文件是否参与编译、某个功能是否编进去include/configs/xxx.h里的CONFIG_SYS_xxx是运行期参数更多是内存大小、基地址、环境变量默认值这类但两套体系会互相影响。顶层Makefile里有这样一段逻辑include/config.h: include/configs/xxx.h echo CHK $它会把include/configs/xxx.h里定义的宏和.config里的选项合并生成include/config.h。最终源码里的#include common.h会间接包含这个头文件所以你在C代码里能直接用CONFIG_SYS_xxx。我在实际移植中踩过这个坑改完defconfig加了一个选项发现C代码里依然用不了后来才反应过来defconfig里的选项会进.config但.config里多数CONFIG_xxx只在Kbuild层面生效C代码里能不能用还取决于顶层Makefile是否把它-D传给编译器。所以不要想当然认为.config里有的在C代码里就能用。3.4 配置阶段常见的三个错误配置阶段的报错不多但一旦报错往往很让人摸不着头脑找不到defconfigmake: *** No rule to make target xxx_defconfig。八成是路径写错或者文件不在configs/下。注意有些芯片厂商会把defconfig放在独立目录比如configs/下可能有子目录。Kconfig语法错误这种通常在你自己添加Kconfig选项后出现。Kconfig对缩进极其敏感config和bool/string必须对齐。arm-linux-gnueabihf-gcc: command not found这个其实不是配置阶段报的而是紧接着的编译阶段但经常被误以为是配置失败。因为我用的交叉编译工具链没装或者环境变量CROSS_COMPILE没指定。4. make编译过程从源码到u-boot.bin的完整链路配置完成后执行make -j8才是重头戏。这一节我会把编译过程拆成几个关键环节带你走一遍从源码到u-boot.bin的完整链路。4.1 顶层Makefile的默认目标你可能会以为执行make就是“编译所有东西”。这句话既对也不对。更准确地说make会构建顶层Makefile里定义的第一个目标。uboot顶层Makefile里all是默认目标而all依赖的是u-boot.bin具体依赖关系不同版本略有差异通常是all: u-boot.bin。所以整个编译的终点是u-boot.bin。而u-boot.bin依赖ELF文件u-bootu-boot依赖所有子目录编译出来的.o文件。这就是一个层层递推的依赖链。4.2 递归makeKbuild如何驱动子目录编译顶层Makefile不可能自己处理几千个源文件的编译规则它把任务下发给了scripts/Makefile.build。Makefile.build的工作方式是“一次性处理所有子目录的实际编译”。它维护一个obj-y变量顶层Makefile把需要编译的子目录列表传给它然后它递归进入每个子目录读取子目录的Makefile收集obj-y、lib-y这些变量最终生成该目录下的.o文件和built-in.o。这里有个高效设计值得注意Kbuild采用的是先收集、后编译的机制。它会把所有子目录的obj-y先汇总到一个KBUILD_VMLINUX_OBJS变量里最后统一由一个大的编译规则处理而不是每个目录单独调用一次make -C。这样做的好处是减少make的进程切换开销编译速度快很多。如果你想知道某个文件有没有参与编译可以这样验证编译完成后在源码根目录执行make V1 21 | grep 你要找的文件名V1会输出完整编译命令行比默认的简短输出要详细得多。4.3 编译选项的传递链交叉编译参数是一层一层传下去的。顶层Makefile里会有这样的定义KBUILD_CPPFLAGS : -D__KERNEL__ -D__UBOOT__ -D__ARM__ ... KBUILD_CFLAGS -Os -g -fno-common -ffixed-r8 ...然后Makefile.build会把KBUILD_CFLAGS拼接到每个编译命令后面。如果你想临时加一个全局宏最简单的方法是在顶层Makefile里给KBUILD_CFLAGS追加KBUILD_CFLAGS -DMY_DEBUG但我不建议直接改顶层Makefile更干净的办法是用make KCFLAGS-DMY_DEBUG效果一样。4.4 链接脚本与u-boot.lds所有.o文件编译完成后下一步是链接。uboot的链接脚本通常在arch/arm/cpu/armv7/或arch/arm/cpu/armv8/目录下文件名是u-boot.lds。链接脚本定义了uboot的内存布局代码段放哪、数据段放哪、.rel.dyn重定位表在哪。对uboot这种需要自重定位的程序来说链接脚本直接决定了跳转地址是否正确。我调试过的一个问题特别能说明链接脚本的重要性换了一颗DDR容量更大的芯片后uboot能启动但运行一段时间就挂。最后定位到是链接脚本里BSS段地址没有跟着DDR大小调整导致某些数据被覆盖了。所以做移植时u-boot.lds一定要对着芯片手册确认。u-boot.lds并不是手写的它是由u-boot.lds.S或者arch/arm/cpu/armv7/u-boot.lds经过预处理生成的。顶层Makefile里有规则u-boot.lds: $(LDSCRIPT) FORCE $(CPP) $(CPPFLAGS) -P -D__ASSEMBLY__ -o $ $看到-D__ASSEMBLY__了吗这说明链接脚本是经过C预处理器处理过的所以里面能看到#ifdef CONFIG_...这种条件编译。4.5 u-boot.bin的生成链接完成得到的是ELF格式的u-boot文件带调试信息、带段表体积很大。但芯片不认ELF最终需要的是纯二进制镜像。顶层Makefile里这样转u-boot.bin: u-boot FORCE $(OBJCOPY) -O binary $ $objcopy -O binary会把ELF里的所有可加载段抽出来连续排列生成一个裸的二进制文件。这个文件可以烧录但它里面的代码还不能直接运行——因为uboot的链接地址是CONFIG_SYS_TEXT_BASE比如DDR的某个地址而芯片上电后从固化ROM搬运代码时可能放在其他地址。这就引出了uboot的两阶段机制NOR flash上可以直接XIP执行不用重定位但NAND、SD卡、eMMC这些介质需要SPL或者SoC内置ROM先把uboot拷贝到RAM里然后跳转执行。5. 从u-boot.bin到可烧写镜像IMX6ULL和4412的打包差异不同SoC对uboot镜像的要求完全不同。这一章以imx6ull和4412为例剖析镜像打包背后的逻辑。5.1 IMX6ULL的u-boot.imx是怎么来的正点原子imx6ull的uboot编译完成后你会看到u-boot.imx这个文件。它比u-boot.bin多了前1KB左右的内容。这个多出来的部分是NXP的IVTImage Vector Table头和Boot Data。i.MX6ULL芯片内部的ROM在上电后会读取启动设备SD卡、eMMC等特定偏移处的数据。它先读IVT头IVT里记录了镜像入口地址即CONFIG_SYS_TEXT_BASE可用的DDR初始化参数Boot Data设备配置数据DCD的地址和长度ROM按照IVT的指引先初始化DDR然后把uboot主体拷贝到指定位置最后跳转执行。这个打包动作不是手工做的顶层Makefile里有规则u-boot.imx: u-boot.bin $(MAKE) -f $(srctree)/arch/arm/mach-imx/Makefile ...实际作用是调用tools/mkimage工具配合arch/arm/mach-imx/下的DCD配置通常放在board/freescale/imx6ull_14x14_evk/下的.c文件里生成带IVT的完整镜像。如果你自己做了一个新板子DCD表是你最需要关心的。DCD表里有DDR控制器的初始化参数这部分数据和你的硬件设计强相关照抄开发板的一般没问题但换了DDR颗粒或者改了走线、频率DCD参数也要跟着调。5.2 4412平台的BL1拆分机制三星Exynos 4412的uboot编译流程又是另一番景象。4412的启动流程是iROM固化在芯片里的ROM代码→ BL18KB→ BL2剩余uboot。所以在4412上uboot编译完成后要做的是把u-boot.bin拆成两段。旧版uboot比如2012年左右的版本会用tools/mk4412或类似工具从u-boot.bin里提取前8KB作为BL1剩下的作为BL2。但是很多4412开发板为了简化流程直接用了一个约定BL1直接用编译生成的u-boot-spl.bin代替。早期版本的uboot并没有为4412启用SPL机制所以很多移植者是自己手写SPL的。这也是为什么你搜索“4412移植uboot”时会看到大量关于BL1的讨论。新版本的uboot在4412上支持了标准的SPL框架编译后会生成u-boot-spl.bin配合原来的u-boot.bin才能形成一个完整的启动镜像。5.3 mkimage工具的通用作用不管哪家SoCtools/mkimage都是uboot镜像打包的核心工具。它支持各种格式imximage、s5p、omapimage、zynqimage等。mkimage的原理很直白把uboot的bin文件前面加一段自定义头头里放校验值、入口地址、镜像大小等信息。有些SoC的ROM还要求镜像做某种加密或校验mkimage也会一并处理。你可以用mkimage -l来查看一个镜像的头信息./tools/mkimage -l u-boot.imx输出会显示镜像类型、入口地址、数据大小等。排查启动问题的时候用这个命令确认一下镜像头信息是否和Boot Device配置一致往往能有意外发现。6. 增量编译与版本构建如何避免“改了没生效”编译报错虽然烦人但最好排查。真正让人崩溃的是“改了半天编译过了烧进去发现没变化”。这一章聊聊增量编译和版本构建的坑。6.1 增量编译的依赖判断机制make的增量编译依赖于文件时间戳。顶层Makefile里通过对每个源文件生成对应的.o文件依赖文件.xxx.o.cmd记录了这个.o文件依赖哪些头文件、哪些配置。当你改了某个头文件make会重新编译所有依赖它的.o。这个机制绝大多数时候是准确的但也有失灵的时候。比如你改了include/configs/xxx.h有时候明明改了重新make却没反应。原因是include/configs/xxx.h并不是所有文件都依赖的很多文件只包含common.h而common.h是否依赖你的xxx.h取决于配置系统生成的include/config.h是否及时更新。在旧版uboot中make不会自动检查include/configs/xxx.h的变化需要手动执行make的某种“强制重新配置”。新版uboot在顶层Makefile里加了自动检查逻辑include/config.h: include/configs/xxx.h $(call cmd,autoconf)所以新版一般不会出这个问题。但保险起见改了include/configs/下的文件后我建议手动执行make xxx_defconfig make -j8强制刷新配置确保万无一失。6.2 版本信息没更新的问题很多人在调试时会遇到uboot启动时打印的版本号还是旧的导致不确定自己烧进去的到底是不是新编的。uboot的版本字符串定义在根目录的Makefile里编译时会生成一个include/generated/version_autogenerated.h。如果你只改了源码没改版本号uboot打印的版本字符串不会变。解决这个问题有两种方式在Makefile里改VERSION、PATCHLEVEL这些变量使用EXTRAVERSION字段比如VERSION 2016、EXTRAVERSION -mydebug这样打印出来就是U-Boot 2016.03-mydebug我习惯在调试阶段加上EXTRAVERSION一眼就能认出自己编出来的是哪个版本避免烧错镜像。6.3 多线程编译的隐藏风险make -j8很好用但有一个场景必须谨慎当你修改了某几个文件又在编译过程中按了CtrlC中断再次编译时用-j可能会触发一个bug——make的依赖关系在没完全重建的情况下某些中间文件可能处于“半新不旧”状态导致链接时符号冲突或者奇怪的行为。遇到这种问题不要犹豫先make clean再重新编译。编译uboot一次也就几分钟SPLuboot全量相比排查一个不明不白的链接错误干净重建其实更划算。7. 两个平台移植场景下的编译命令参考为了让你有一个具体的操作对照我把imx6ull和4412的编译命令整理一下。不同厂商的BSP包可能会有改动但基本流程是通用的。7.1 正点原子imx6ull开发板# 1. 清理第一次编译可以跳过 make distclean # 2. 配置 make imx6ull_14x14_evk_defconfig # 3. 编译-j后面的数字根据你CPU核数来一般取核数x2 make -j8编译完成后根目录下会有u-boot.bin、u-boot.imx。烧录用的是u-boot.imx。如果你用的不是正点原子的板子而是自己画的板子那么需要先创建自己的defconfig和板级目录然后重复上述步骤。7.2 友善之臂Smart2104412# 清理 make distclean # 配置不同版本uboot名称可能不同 make smdk4412_config # 老版本 # 或者 make smdk4412_defconfig # 新版本 # 编译 make -j84412编译完成后生成的是u-boot.bin。实际烧录时可能需要自己拆分成BL1和BL2或者结合SPL方案使用u-boot-spl.bin。具体看你的BSP版本和烧写工具要求。7.3 新增板卡的编译验证流程如果你是在做真正的U-Boot移植我建议按这个顺序验证编译先不修改任何代码直接编译默认的厂商板卡defconfig确认环境和工具链OK复制一份厂商板卡的目录和defconfig重命名成你的板子型号修改最小配置内存大小、时钟、串口等编译通过烧录验证串口输出逐步增加功能这个顺序能帮你把“编译问题”和“运行问题”分开不至于两团乱麻一起抓。8. 编译报错排查实录从错误信息反推根因最后这一章我分享一下实际项目里最常见的几类编译错误和排查思路。这些报错信息五花八门但根因往往就那么几个。8.1 链接错误undefined reference toxxx这是最常见的报错类型。比如arm-linux-gnueabihf-ld: board/xxx/xxx.o: in function board_init: undefined reference to foo_bar说明你调用了函数foo_bar但没有找到它的实现。排查步骤全源码搜索这个函数定义在哪个文件确认这个文件有没有被编译搜obj-y或者Makefile确认这个文件编译时对应的宏开关是否打开很多时候是你在C文件里写了#ifdef CONFIG_FOO包着的代码但CONFIG_FOO没开导致函数没编进去。或者你调用了drivers目录下的函数但对应的驱动没有obj-y列入编译。8.2 找不到头文件No such file or directoryfatal error: asm/arch/gpio.h: No such file or directory这类错误多半和arch头文件链接有关系。uboot里有一个机制arch/arm/include/asm/arch是个软链接指向具体的SoC目录。你可以在arch/arm/include/asm/下看到arch - arch-imx6ull 软链接如果这个链接不对或者缺失就会出现上述错误。执行配置命令时通常会重新建立这个链接因此解决办法很简单重新执行make xxx_defconfig后再次编译。8.3 .config:xxx:warning: override: reassigning to symbol XXX这个警告比较常见不一定是错误但值得注意。它表示有两个地方比如defconfig和Kconfig默认值都在设置同一个选项且值不一致。Kbuild采用“后设置的值覆盖前者”的规则。建议打开.config确认最终值是你期望的。8.4 编译到一半突然OOM用-j8在低配电脑上编译uboot偶尔会内存不足。尤其是同时跑着IDE、浏览器的时候。解决办法降到-j4或者临时关闭一些大程序。另外编译uboot时make -j的合理线程数是CPU线程数的1.5~2倍不是越大越好。8.5 头文件被修改后重新编译但行为没变这个场景前面提过再强调一次如果改了头文件觉得“没生效”先查编译命令行里有没有-D传递该宏再看这个宏是否真的被某段代码使用。用grep确认不要靠猜。我在实际把uboot从开发板移植到自己设计的板子上时最深的一个体会是编译流程读懂了很多“玄学”问题都会变成“逻辑问题”。比如镜像打包格式、链接地址这些搞懂了之后看一眼报错和文件大小就能猜个大概。反过来如果你对编译流程不够熟悉出了问题只能反复clean、rebuild、烧录非常浪费时间。建议你找一个熟悉板子把make V1的输出完整过一遍再看看顶层Makefile的几个关键规则花两三个小时把整个过程串起来。这比盲目追着代码看效率高得多后面做移植、排查都会顺很多。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/6 19:21:28
Excel两个表关键词匹配复制:从VLOOKUP到Power Query与Python方案
2026/10/6 19:21:28
AI产品如何判断PMF?一套可落地的验证方法
2026/10/6 19:16:28
AI代理权限失控?OpenClaw安装后信用卡被盗刷的风险解析
2026/10/6 23:11:51
hyperframes 实战:HTML 转 MP4 的 CLI 工具链与 AI 代理集成
2026/10/6 23:11:51
Kettle 9.4企业级ETL部署实战:JDK11适配与UCanAccess驱动集成
2026/10/6 23:11:51
CVBS信号原理与实战:从模拟视频基础到匹配滤波调试
2026/10/6 23:11:51
集成稳压器:从分立电源到高可靠电源设计的工程跃迁
2026/10/6 23:11:51
Notepad++ 8.3.3 升级指南:Python Script 插件配置与避坑实战
2026/10/6 23:06:51
Allegro阵列过孔设计:电流密度建模与热力图优化实战
2026/10/6 1:04:29
搭建无线EEG采集前端:BW16+ESP32-CYD实时波形显示实战
2026/10/6 1:04:29
CH10D功放芯片DIY音箱实战:从选型到调试的完整指南
2026/10/6 1:04:29
视频序列目标跟踪实战:解决ID跳变与遮挡丢失
2026/10/6 15:41:36
Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化
2026/10/6 4:47:52
多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系
2026/10/6 13:15:25
hindsight:面向LLM应用的事后可观测性工程实践
2026/10/6 21:51:29
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/6 22:05:33
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/6 22:06:19
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)