☰
U-Boot移植第一步:搞懂Kbuild构建系统,少走编译弯路
2026/10/8 6:30:22 网站建设 项目流程

搞嵌入式Linux的,早晚都得碰U-Boot。第一次听说“移植U-Boot”这五个字时,我第一反应是拿源码到到处乱改,一头扎进某个board目录的C文件里,翻半天都不知道哪行代码需要动。后来被链接错误、编译报错反复教育过几轮,才慢慢想明白一个道理:移植效率的瓶颈,从来不在最底层的驱动代码,而在头顶上那套构建系统——Kbuild。这篇文章想分享的,就是U-Boot移植路上最容易被新手跳过的第一步:先把Kbuild怎么运转搞清楚,再动手改板级内容。无论你是第一次接触U-Boot、刚接手一块新板子,还是被莫名奇妙的编译错误折磨到想摔键盘,希望这篇实操笔记能让你少走几段弯路。

1. 移植U-Boot,为什么第一步是搞懂Kbuild

1.1 移植的本质:同一套源码,怎么装进一千种板子

U-Boot是一套极其庞大的“多板卡共用源码”。你把官方仓库拉下来之后数一下configs/目录,里面躺着大几百个defconfig文件,每个都对应一块具体的开发板或量产板。从ARM到RISC-V,从老的PowerPC到新的RISC-V,几乎每个主流架构都能在这里找到身影。

这意味着什么?意味着同一份源码,在编译时必须能根据不同的目标板卡,裁剪出完全不同的固件。同是一个serial驱动目录,你的板子可能只需要8250串口驱动,而另一块板子可能要用PL011;同一份net目录,有的板子编入AX88179网卡驱动,有的板子只需要内置MAC。这些差异不能靠写死代码解决,必须依赖一套机制,在编译前就决定“哪些目录参与编译、哪些文件进镜像、哪些宏定义被展开”。

这套机制就是Kbuild。

大学时候装过电脑系统的读者可以这样类比:U-Boot源码像一张Windows安装镜像,功能齐全但不可能全部装到一台机器上;defconfig像是安装时选择的“组件清单”,决定这台机器装什么驱动、开什么服务;而Kbuild就是那个后台安装程序,按清单干活,最终产出一个能在这台硬件上跑起来的固件。

移植U-Boot,本质上就是三件事:写一份符合新板卡硬件情况的清单(defconfig与设备树),修好清单对应的配置依赖(Kconfig),再用Kbuild这套流水线把它编译成可启动镜像。大部分教程上来就让你改代码,这其实把人引偏了。先理解清单怎么生效,比改一百行驱动代码都重要。

1.2 Kbuild不是“一个Makefile”,而是一整套构建体系

很多人刚接触Kbuild时,会习惯性地把它理解成“U-Boot的那个Makefile”。实际上,Kbuild是一整套构建系统的总称,它包含的东西远远超过顶层那个孤零零的Makefile。

最小集合里至少包括这几部分:

  • 顶层Makefile:负责接收make xxx_defconfig、make、make menuconfig这类总入口命令,统一管理ARCH、CROSS_COMPILE等全局变量;
  • scripts/Makefile.build:编译子目录节拍的真正执行者,它处理每个子目录里的obj-y、obj-m等变量;
  • scripts/kconfig/目录:管理Kconfig语法解析、defconfig生成、menuconfig界面的一套独立工具;
  • 各级子目录下的Kconfig与Makefile:前者描述“本目录有哪些配置项”,后者描述“这些配置项选择后,哪些目标参与编译”;
  • include/config/和include/generated/里自动生成的一系列配置派生文件:这是整个体系的最终产品之一。

换句话说,Kbuild不是一个文件,它是一个有输入、有输出、有中间产物的管道系统。你今天敲下make menuconfig,它在跑Kconfig那套交互界面;你敲下make,它在跑递归目录构建;你甚至可以在编译过程中随时用make V=1让它把每一条gcc命令都打印出来,这时候你看到的是Kbuild最赤裸的施工过程。

理解这层结构,对后面排查问题和做定制编译特别有用。比如某个驱动没编进镜像,直觉反应是去改源码,但真正的原因可能是configs/xxx_defconfig里没有开启对应宏,或者Makefile的obj-y没有被正确挂上。不懂Kbuild的人会在这里卡很久,懂的人五分钟就能定位。

2. Kbuild的三级流水线,分别做了什么

2.1 第一级流水线:从defconfig到.config

第一次尝试移植时,教程一般会教你先执行:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- myboard_defconfig

这个命令看起来简单,背后其实是Kbuild第一级流水线的起点。myboard_defconfig是一个目标名称,顶层Makefile捕获到带有_defconfig结尾的目标后,会把它交给scripts/kconfig/conf工具处理。

这里有个容易混淆的概念:configs/myboard_defconfig文件并不是一份完整配置,它只是一份“差分描述”——告诉conf工具,Kconfig选项树里哪些项要设为y、哪些要设为n、哪些保持默认。真正生成完整配置时,conf工具会解析架构相关的Kconfig文件树,结合defconfig里的覆盖项,以及Kconfig里声明的depends on和select依赖关系,最终展开成一份不含任何缺失项的完整配置,也就是根目录下的.config文件。

实际动手时,新手最容易踩的坑是直接手改.config。.config确实是人类可读的,也确实能改,但它是中间产物,改动后必须重新跑一次配置同步,让它重新生成一次依赖关系。正确做法是修改defconfig,或者用make menuconfig这种交互工具改,保存后Kbuild会帮你把变更写回.config。如果你非要在.config里手改,那改完至少执行一次make olddefconfig,让Kconfig把新的依赖关系补齐,否则很容易出现“明明改了,编译时宏却不生效”的灵异现象。

另外解释一下ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-这两个变量。ARCH决定Kbuild读哪套架构的Kconfig和构建规则,CROSS_COMPILE指定交叉编译工具链的前缀。它们可以写在make命令行里,也可以通过环境变量提前export。我习惯写成一行命令传参,这样每条命令都自包含,换板子时也不容易串配置。

2.2 第二级流水线:从.config到autoconf.h

.config生成之后,还远远没到编译阶段。源码里那些#ifndef CONFIG_XXX、#ifdef CONFIG_XXX的预处理指令,依赖的是一批从.config派生的头文件。这条派生流水线,由另一个工具负责,通常是通过顶层Makefile里的syncconfig目标触发。

最终产物有两个关键文件。第一个是include/generated/autoconf.h,它是C语言可以#include的头文件,里面是一堆#define CONFIG_XXX 1这样的宏定义。源码编译时,#ifdef CONFIG_XXX就是从这个头文件拿到判断结果的。第二个是include/config/auto.conf,它是Makefile语法的一个配置文件,里面是CONFIG_XXX := y这类赋值语句,供Kbuild自身判断哪些目录、哪些文件需要编入。

所以,当你怀疑某个配置项没有生效时,最直接的检查姿势就是打开这两个文件搜一下对应宏是否存在。如果不存在,说明你的配置在Kconfig层面就被吞掉了,要么依赖不满足,要么被某个select强行覆盖,要么defconfig的拼写出了岔子。

顺带一提,Kbuild还维护着一堆.cmd依赖文件,比如include/config/auto.conf.cmd,这些文件用来做增量编译的依赖追踪。它记录了配置选项和头文件的对应关系,一旦配置项变化,下次编译就能精确触发那些依赖该配置的源文件重新编译,而不是整包全量重编。这也是Kbuild比普通Makefile工程高级很多的地方。

2.3 第三级流水线:递归编译、链接与镜像生成

配置派生文件就位后,顶层Makefile开始进入真正的递归构建阶段。这个阶段的核心调度逻辑在scripts/Makefile.build里。

你可以把它理解为工地上的总包经理:拿到obj-y清单后,挨个进入子目录,让子目录里的“分包队”编译出对应的.o文件,再把这些.o打包成built-in.o,一层层向上汇总。最后,顶层把全局的built-in.o、libs、以及链接脚本放在一起,由链接器生成最终的U-Boot ELF文件。

这过程中有几个值得留意的点。一是obj-y往往不是直接列文件名,而是通过Makefile里的条件判断动态生成,比如:

obj-$(CONFIG_NET) += net/ obj-$(CONFIG_DM_GPIO) += gpio/

这里的CONFIG_XXX就是auto.conf里那些变量。如果某个子系统因为配置没开而没有被加入obj-y,那就算源码放在目录里,编译也完全不会碰它。移植新板子时,忘记加入obj-y依赖,是“源码明明存在却编译不过/编不进镜像”的头号原因。

链接阶段同样由Kbuild掌控。U-Boot要生成多个镜像变体,比如普通固件u-boot.bin、带SPL的u-boot-spl.bin,以及给某些平台用的u-boot.img。这些镜像的生成规则分布在顶层Makefile和scripts/Makefile.spl里。链接地址由CONFIG_SYS_TEXT_BASE这类配置项控制,它在arch/arm/cpu/armv7/u-boot.lds这类链接脚本里被引用。地址配错,轻则链接警告,重则生成一个上板就死机的镜像。

3. 新板卡移植实操:需要动的文件与完整命令

3.1 先做减法:克隆一个参考板

真正动手移植时,我强烈建议别从零开始写板级文件。最靠谱的起步动作,是找一块与目标板卡最接近的官方开发板作为参考板。

什么叫“最接近”?优先级大概是这样的:

  1. 使用同一颗SoC,或者同一系列、引脚完全兼容的SoC;
  2. 同样位宽的DDR颗粒,即同为DDR3、DDR4或LPDDR系列;
  3. 存储介质类似,同样从SD卡启动或同样从eMMC启动。

找到参考板后,把它的defconfig复制一份,然后基于它做修改,比对着一个全新板卡从零开始扣Kconfig树要省太多事。U-Boot社区里几乎所有第三方板卡的支持,都是这么“克隆”出来的。

复制defconfig的命令很简单:

cp configs/refboard_defconfig configs/myboard_defconfig

但光复制不够。你需要打开这个defconfig,把CONFIG_DEFAULT_DEVICE_TREE改成你的设备树文件名,把CONFIG_SYS_CONFIG_NAME改成你的板级头文件名,还要检查CONFIG_TARGET_XXX这类由Kconfig生成的选项是否匹配。这些字段是Kbuild在后续配置阶段定位板级文件的索引,任何一个对不上,后面都会以奇怪的报错方式还回来。

克隆完成后,先跑一次配置命令,让Kbuild按新名字生成.config,看看报错信息是不是能把你引导到下一步。我通常会在这一步就开始迭代,而不是把所有文件都改完再一次性编译,因为Kbuild的报错本身就能一步一步指示你缺什么。这轮“跑一下、缺啥补啥”的循环,就是移植早期的主旋律。

3.2 defconfig、设备树、板级头文件到底怎么配合

很多初学者觉得板级移植文件又多又杂,其实基本可以用一张所谓的“铁三角”来理解:defconfig、设备树、板级头文件。

defconfig里排在最前面的往往是目标配置:

CONFIG_TARGET_MYBOARD=y CONFIG_SYS_CPU="armv7" CONFIG_SYS_SOC="mx6ull" CONFIG_SYS_CONFIG_NAME="myboard" CONFIG_DEFAULT_DEVICE_TREE="myboard"

CONFIG_TARGET_MYBOARD来自板级Kconfig,这个选项被选中后,Kbuild才能找到对应的board/vendor/myboard/目录参与构建。CONFIG_SYS_CONFIG_NAME则决定编译时是否要包含include/configs/myboard.h。CONFIG_DEFAULT_DEVICE_TREE用来定位arch/arm/dts/myboard.dts设备树源文件。

设备树这部分,实际移植时很容易漏掉。U-Boot从很早的版本开始就不再只靠C代码描述硬件了,串口地址、GPIO引脚、DDR时序等很多信息都放在设备树里。U-Boot在启动过程中会把自己编译的dtb解析成内存里的设备树结构,再传给内核使用。所以,如果你的dts文件和实际硬件对不上,U-Boot自身可能根本起不来,更谈不上引导Linux。

板级头文件在老版本里承担了很重的角色,DDR配置、时钟频率、环境变量分区这些都得写在include/configs/refboard.h里。最近几年的U-Boot正在逐步把这些配置迁进Kconfig和设备树,所以新版本里这个头文件已经精简很多,甚至有些板卡完全没有。移植时看清你所用版本,别照着老教程在不存在的位置白费力气。

实际修改过程中,我习惯把参考板的include/configs/xxx.h整体复制成myboard.h,然后先用原样编译,跑通第一条流水线,再逐项改成自己板子的参数。每次只改一类东西,比如先把串口调通,再调DDR,再调网卡。一次改太多,出问题就没法定位是哪一项改坏了。

3.3 从配置到固件的完整命令序列

完整跑一遍的参考命令序列大概是这样的:

export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- make myboard_defconfig make -j$(nproc)

如果一切顺利,当前目录会生成u-boot、u-boot.bin、u-boot.map、System.map等文件。其中u-boot是ELF格式调试镜像,u-boot.bin是烧录用的二进镜像。如果配置了SPL,还会有u-boot-spl.bin。

第一轮编译如果失败,别急着改代码。先看报错信息是来自配置阶段还是编译阶段。配置阶段报错一般是缺某个Kconfig选项或defconfig的语法问题;编译阶段报错才是真的要动源码或调整编译选项。

中间如果想微调配置,用menuconfig比直接改defconfig更直观:

make menuconfig

它把Kconfig选项树渲染成交互界面,按空格选y/n、按M选m,退出时Kbuild会帮你把变更写回.config。但记住,menuconfig直接改的是.config,如果你还想把这些变更固化到defconfig,要执行:

make savedefconfig

这会根据当前.config生成一份精简的最小defconfig,再手动复制回configs/myboard_defconfig。我第一次用这个命令时,发现它生成的defconfig比我手工维护的版本干净太多,后来就再也没手动填过defconfig里的增量选项。

4. Kbuild相关翻车现场与排查手册

4.1 配置改了却没生效:先查autoconf.h

移植中非常常见的一个困惑:“我明明在defconfig里加了CONFIG_XXX,编译出来的镜像里怎么没有?”

这类问题十有八九卡在配置同步上。defconfig对.config的更新只发生在你执行配置目标的瞬间,比如再次make myboard_defconfig或make olddefconfig。如果改完defconfig直接跑make,Kbuild确实会触发syncconfig,但它只同步autoconf.h这些派生文件,不一定把你新加的非标准选项正确展开。稳妥做法是:

make myboard_defconfig make -j$(nproc)

或者更简洁地:

make olddefconfig make -j$(nproc)

然后立刻去include/generated/autoconf.h里搜索对应宏。搜不到,再考虑两种可能:一种是你这个选项的依赖不满足,比如它depends on的另一个配置没有开启;另一种是它被更高优先级的选择逻辑覆盖了。查依赖最直接的办法是看对应目录的Kconfig文件,把depends on和select链跟一遍,基本就能定位。

4.2 一看就头疼的报错信息,到底哪些值得害怕

编译报错五花八门,但Kbuild相关的错误类别其实很有限。我把移植期间最常撞见的几类整理成一张表,方便对照:

报错特征常见原因排查方向
fatal error: configs/myboard.h: No such file or directoryCONFIG_SYS_CONFIG_NAME指向的头文件不存在检查include/configs/下文件是否已创建,路径拼写是否一致
undefined reference to 'xxx_board_yyy'某个函数声明了但没编译进镜像检查对应驱动目录的obj-y依赖是否因配置未开启而被跳过
multiple definition of 'xxx'同一个符号被多个目录重复编译检查Makefile是否重复加了同一目标,或全局配置开关切分不干净
board/myboard/myboard.c:0: error: implicit declaration of function板级文件缺少必要的config宏对照参考板defconfig补全宏,重点看CONFIG_SYS_*相关项
.text section will not fit in regionDDR参数、链接地址或代码体积问题检查CONFIG_SYS_TEXT_BASE与DDR初始化大小是否匹配

这里要特别提醒一下,链接阶段报“region Overflow”这类错误时,很多人会去怀疑链接脚本,然后尝试改lds文件里的布局。我的经验是,绝大多数情况下问题不在lds,而在DDR初始化代码配置的可用内存大小太小,或者CONFIG_SYS_TEXT_BASE指向了错误地址。把链接脚本乱改一通,往往只会让问题更隐蔽。

4.3 增量编译的坑:不用动不动make clean

新手拿到编译错误后,习惯性做法是直接make clean重来一遍。这么做倒没错,但很浪费时间,因为Kbuild本身有很好的增量编译能力。真正该做的是理解哪些情况需要clean,哪些不需要。

单纯的配置宏变化,走一遍make olddefconfig让autoconf.h更新,Kbuild会通过.cmd依赖文件自动重编受影响的目标。但如果中间产物本身损坏,比如上次编译被Ctrl+C打断、磁盘满了导致生成一半的.o文件,或者你升级了交叉编译工具链,那最好还是make clean甚至make distclean一次,避免旧产物干扰。

另外提一句,make distclean会把.config也一起删掉。如果之后想回到之前调试到一半的状态,就得重新执行defconfig命令。所以我把distclean理解成“彻底推倒重来”,不怎么常用;大多数时候用make clean就已经够了。

5. 把Kbuild用熟的几个进阶技巧

5.1 用V=1和-n看穿编译的每个动作

Kbuild默认的编译输出很简洁,只显示短的进度信息,比如CC arch/arm/cpu/armv7/start.o。这确实清爽,但排查问题时不方便。

这时候用make V=1,它会打印出每一条完整的gcc命令,包括所有头文件搜索路径、预处理宏定义、优化等级参数。我排查“头文件找不到”问题时,通常第一件事就是重新编译一次并加V=1,这样能直接确认include路径里是否包含include/configs目录。比如编译命令里如果缺少-Iinclude/configs,那#include <configs/myboard.h>找不到就是必然结果。

另一个技巧是make -n,dry-run模式,只打印将要执行的命令而不真正执行。加V=1一起用,可以安全地观察Kbuild整个执行流程而不实际改动任何文件:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -n V=1

这个组合我用来“预习”一段陌生工程的构建过程,非常管用。不用担心风险,它本质上只是多打印信息,不动你的源码和中间产物。

5.2 用O=把编译产物挪出源码树

嵌入式开发时,源码目录和编译产物混在一起会带来一堆麻烦:清理时容易误删、不同板卡配置切换时中间产物互相干扰、备份时各种*.o文件占空间。Kbuild早有标准解法,就是O=指定输出目录:

make O=build ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- myboard_defconfig make O=build ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc)

用了O=build之后,所有的.o、.cmd、.config、autoconf.h乃至最终镜像,都会生成在build/目录里。源码目录保持干净。

这种方式在同时维护两块板卡时尤其好用:一块板卡用build/boardA,另一块用build/boardB,切换编译时各用各的输出目录,互不干扰。我第一次发现这个功能时,感觉自己之前用手动复制源码再分别编译的方式,简直是在和Kbuild对着干。

5.3 编译产物里全是调试线索,别只盯着u-boot.bin

编译结束后的产物里,除了烧录用的u-boot.bin,还有几个文件对移植调试非常有价值。

u-boot.map是完整的内存分布图,链接阶段每个符号被分配到哪个地址,在上面都能查到。我在排查“某个变量/函数跑到错误地址”或“代码段溢出”问题时,几乎都会翻这个文件。比对着CONFIG_SYS_TEXT_BASE看,能非常直观地判断地址布局是否合理。

System.map是符号表,虽然U-Boot运行时不一定需要,但配合调试器时很有用。它可以告诉你某个功能函数的虚拟地址,然后你在反汇编输出里搜索这个地址,就能精准定位实际运行路径。

u-boot.cfg则是极容易被忽略的文件,它记录了编译U-Boot时最终生效的所有配置宏。当你怀疑某个配置项到底有没有编进去时,看这里比看任何Kconfig文件都更让人安心。

这些产物属于那种“你不用时觉得毫无存在感,用起来才惊呼原来一直在那”的东西。移植调试遇到诡异问题时,随手打开u-boot.cfg和u-boot.map各看一眼,往往比漫无目的地改代码效率高得多。

最后再分享一个个人习惯:每次给新板子做第一轮移植,我都会把Kbuild生成的.config另存一份留档,比如存成configs/myboard_full.config。一旦后面menuconfig改乱了,随时能拿这份完整配置回来对比差异,省去反复savedefconfig的折腾。Kbuild这套系统确实有学习门槛,但它一旦上手,就是你移植路上最趁手的工具。

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

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

立即咨询