拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

U-Boot移植必知:Kbuild构建系统原理与实战避坑指南

U-Boot移植必知:Kbuild构建系统原理与实战避坑指南

1. 从一份编译报错说起:为什么U-Boot移植绕不开Kbuild

第一次给一块新板子做U-Boot移植的人,十有八九会在编译阶段卡住。现象往往很朴素:make xxx_defconfig跑完看着挺正常,接着make一敲,报错信息里冒出一堆No rule to make target、undefined reference,或者更隐蔽的——编译过了,但生成的u-boot.bin烧进去板子根本不启动。这时候很多人第一反应是去翻board/目录下的板级文件,改config.mk、改链接脚本,改到怀疑人生,最后发现根子其实在构建系统这一层。

U-Boot 从 2014 年底开始全面转向Kbuild构建体系,也就是和 Linux 内核同源的那套Kconfig+Makefile+Kbuild组合。这个转变对移植工作的影响是根本性的:以前你改一个include/configs/xxx.h里的宏就能控制编译行为,现在很多开关被收进了Kconfig,编译哪些文件、链接哪些目标,由Makefile和Kbuild文件里的obj-y、obj-$(CONFIG_XXX)决定。不理解这套机制,移植就变成了盲人摸象——你知道要改,但不知道改哪里、为什么改、改完谁生效。

这篇内容面向的是正在做 U-Boot 移植、或者准备接手一块新板子 bring-up 的嵌入式工程师。我会把 Kbuild 在 U-Boot 里的角色拆开讲清楚:它和内核 Kbuild 的异同、目录结构怎么组织、defconfig和.config的关系、obj-y的展开逻辑、板级 Makefile 该怎么写,以及移植过程中最容易踩的几个坑。目标很明确——让你下次看到编译报错时,能顺着 Kbuild 的链路自己定位,而不是靠搜索引擎碰运气。

需要提前说明的是,U-Boot 版本差异很大,本文的讨论以 2018 年之后的主流版本(比如 v2020.04 到 v2024.x)为基准。更老的版本(v2013 之前)还是旧的 Makefile 体系,思路不一样,不要混着看。

2. Kbuild 在 U-Boot 里到底管什么:和内核那套的异同

2.1 Kbuild 的三件套:Kconfig、Makefile、Kbuild

先把概念理清楚。很多人把 Kbuild 当成一个工具,其实它是一套约定俗成的构建框架,由三个部分协同工作:

  • Kconfig:配置系统的描述文件,定义有哪些配置项(config XXX)、它们的类型(bool、tristate、string、hex)、依赖关系(depends on)、默认值(default)和帮助信息。make menuconfig读的就是它。
  • Makefile:顶层和各级目录的构建入口,负责定义目标、变量、编译规则,以及递归进入子目录。
  • Kbuild:各级目录里名为Kbuild的文件(注意没有后缀),专门描述"这个目录下哪些文件要被编译进最终镜像"。它和 Makefile 分工明确——Makefile 管"怎么编",Kbuild 管"编什么"。

在 U-Boot 里,这三者的关系可以这样理解:Kconfig决定配置项的值,这些值最终落到.config文件里,变成一堆CONFIG_XXX=y或CONFIG_XXX=n;Makefile和Kbuild通过读取这些CONFIG_XXX变量,决定把哪些.o文件塞进built-in.o,最后链接成u-boot。

2.2 和 Linux 内核 Kbuild 的关键差异

如果你有内核开发经验,会觉得 U-Boot 的 Kbuild 很眼熟,但千万别直接套用。两者有几个实打实的区别:

对比项Linux 内核U-Boot
配置工具make menuconfig为主make menuconfig和make xxx_defconfig并重
defconfig 位置arch/xxx/configs/configs/顶层目录
模块支持有obj-m,支持可加载模块基本不用obj-m,全部静态链接
多阶段构建单一内核镜像SPL、TPL、U-Boot proper 多阶段
配置头文件include/generated/autoconf.h同样有,但还有include/configs/xxx.h板级头

最后一条差异特别关键。U-Boot 保留了传统的板级配置头文件include/configs/<board>.h,很多宏定义(比如CONFIG_SYS_TEXT_BASE、CONFIG_SYS_LOAD_ADDR)还是写在这里,而不是走 Kconfig。这就造成了一个"双轨制":一部分配置在.config里,一部分在板级头文件里,移植时必须两边都看。

2.3 为什么 U-Boot 要迁到 Kbuild

这个问题的答案直接关系到移植思路。旧体系下,每个板子一个include/configs/xxx.h,里面几百个宏,板子和板子之间大量复制粘贴,改一个公共特性要动几十个文件。迁到 Kbuild 之后,公共配置项收敛到Kconfig,板级差异通过defconfig表达,维护成本大幅下降。

对移植者的实际影响是:新板子的配置应该尽量往defconfig和Kconfig里放,而不是继续往板级头文件里堆宏。我见过不少从老版本迁移过来的代码,板级头文件里塞了几百行#define,其中一大半在 Kconfig 里已经有对应项了,这种代码维护起来就是灾难。

3. 目录结构与配置流向:一次 make 背后发生了什么

3.1 顶层目录里和 Kbuild 相关的文件

拿到一份 U-Boot 源码,先别急着改代码,把顶层这几个文件认全:

  • Makefile:顶层构建入口,定义all、u-boot、spl/u-boot-spl等目标,负责调用scripts/Kbuild.include里的通用规则。
  • Kconfig:顶层配置入口,通过source语句把各级目录的Kconfig串起来。
  • configs/:存放所有xxx_defconfig文件,每个板子一个。
  • scripts/kconfig/:Kconfig 解析工具的实现,conf、mconf、menuconfig这些可执行文件编译出来后放在这里。
  • scripts/Kbuild.include:Kbuild 的通用规则库,if_changed、cmd、build这些核心宏都在里面。
  • include/configs/:板级配置头文件,传统宏的聚集地。

3.2 从 defconfig 到 .config 的完整链路

执行make xxx_defconfig的时候,背后发生的事比你想的多:

  1. Makefile 检测到目标是%config,调用scripts/kconfig/conf工具。
  2. conf读取configs/xxx_defconfig,这是一份精简的配置清单,只列出和默认值不同的项。
  3. conf再读取顶层Kconfig,递归解析所有source进来的子 Kconfig,构建出完整的配置树。
  4. 把 defconfig 里的值套到配置树上,其余项取默认值,生成.config。
  5. 同时生成include/config/auto.conf(给 Makefile 用的变量形式)和include/generated/autoconf.h(给 C 代码用的宏形式)。

这里有个容易忽略的点:defconfig不是完整配置,只是差异清单。所以你在menuconfig里改了一个项,保存后.config变了,但defconfig没变。下次再make xxx_defconfig,你的修改就丢了。正确做法是改完用make savedefconfig生成精简版,再覆盖到configs/目录。

3.3 auto.conf 和 autoconf.h 的分工

这两个文件经常被搞混,其实分工很清楚:

  • include/config/auto.conf:Makefile 读的,内容是CONFIG_XXX=y这种形式,被include进 Makefile 后变成 Make 变量。
  • include/generated/autoconf.h:C 代码读的,内容是#define CONFIG_XXX 1,被include进源文件后变成预处理宏。

所以当你在 Makefile 里写obj-$(CONFIG_FOO)时,用的是 auto.conf 里的变量;在 C 代码里写#ifdef CONFIG_FOO时,用的是 autoconf.h 里的宏。两者同源,但用途不同。移植时如果发现"配置明明开了,代码却没生效",先检查是不是这两个文件没同步生成——删掉include/config/和include/generated/重新编译通常能解决。

4. obj-y 的展开逻辑:文件是怎么被编进 u-boot.bin 的

4.1 obj-y、obj-$(CONFIG_XXX) 和 obj-$(CONFIG_YYY)

Kbuild 里最核心的语法就是obj-*。看一个典型的板级 Makefile:

# board/myvendor/myboard/Makefile obj-y += myboard.o obj-$(CONFIG_MYBOARD_SDRAM) += sdram_init.o obj-$(CONFIG_SPL_BUILD) += spl.o

展开逻辑是这样的:obj-y里的文件无条件编译;obj-$(CONFIG_MYBOARD_SDRAM)在配置项为y时展开成obj-y,为n时展开成obj-(空),文件就不编。最终所有obj-y里的.o会被打包成当前目录的built-in.o,逐级向上汇总,最后链接进u-boot。

这里有个细节值得注意:obj-$(CONFIG_XXX)里的CONFIG_XXX必须等于y才会生效。如果配置项是m(模块),在 U-Boot 里通常不处理,因为 U-Boot 基本不用可加载模块。所以写 Kconfig 时,板级相关的项一般定义成bool类型。

4.2 built-in.o 的逐级汇总机制

理解built-in.o的汇总过程,对排查链接错误特别有用。假设目录结构是:

board/myvendor/myboard/ ├── Makefile ├── myboard.c └── sdram_init.c

编译后生成myboard.o和sdram_init.o,然后 Makefile 里的规则把它们ld -r成一个built-in.o。这个built-in.o再被上一级board/myvendor/Makefile通过obj-y += myboard/收集,逐级往上,最终到顶层。

所以当你看到undefined reference to 'board_init'这类错误时,排查路径是:先确认board_init所在的.c文件有没有被obj-y收录,再确认它所在的目录有没有被上一级 Makefile 收录。很多时候问题就出在某一级 Makefile 漏写了obj-y += 子目录/。

4.3 SPL 和 U-Boot proper 的编译隔离

U-Boot 的多阶段构建是移植里最容易出错的地方。SPL(Secondary Program Loader)和 U-Boot proper 是两套独立的编译产物,用同一个 Makefile 但不同的配置宏区分。关键宏是CONFIG_SPL_BUILD:

# 同一个 Makefile 里区分两个阶段 obj-$(CONFIG_SPL_BUILD) += spl_board_init.o obj-$(CONFIG_!SPL_BUILD) += full_board_init.o

注意CONFIG_!SPL_BUILD这种写法,它表示"非 SPL 阶段"。编译 SPL 时,CONFIG_SPL_BUILD=y,第一行生效;编译 U-Boot proper 时,CONFIG_SPL_BUILD未定义,第二行生效。

移植时的常见坑是:某个初始化函数在 SPL 和 proper 里都要用,但只在一个阶段编了,另一个阶段链接时报未定义。解决办法是把公共代码抽到一个独立的.c文件,在两个阶段的obj-y里都加上。我一般会在板级目录下建一个common.c,专门放这种两阶段共用的代码。

5. 板级移植实操:从零给一块新板子接上 Kbuild

5.1 选一个最接近的参考板

移植的第一步不是写代码,是找参考。U-Boot 源码里configs/目录下有上千个 defconfig,board/目录下有对应的板级代码。选参考板的原则是:SoC 相同或同系列 > 内存布局相近 > 外设配置相近。

比如你要移植一块基于某款 ARM Cortex-A53 的板子,先找同 SoC 的现有板子,把它的configs/xxx_defconfig、board/vendor/xxx/、arch/arm/dts/xxx.dts三处文件复制过来,改名字。这一步不要想着从零写,U-Boot 的板级代码复用度极高,从零写纯属浪费时间。

5.2 创建 defconfig 并跑通编译

复制完参考板后,先改configs/下的 defconfig。一个最小可用的 defconfig 大概长这样:

CONFIG_ARM=y CONFIG_ARCH_MYCHIP=y CONFIG_SYS_TEXT_BASE=0x40000000 CONFIG_DEFAULT_DEVICE_TREE="myboard" CONFIG_TARGET_MYBOARD=y CONFIG_SYS_MALLOC_LEN=0x400000

然后执行:

make myboard_defconfig make -j$(nproc)

第一次编译大概率会报错,这很正常。报错分两类:一类是配置项缺失(undefined reference),一类是文件路径不对(No rule to make target)。前者去 Kconfig 里补配置项,后者去 Makefile 里补obj-y。

5.3 板级 Makefile 和 Kconfig 的写法

板级目录下需要两个文件:Makefile和Kconfig。Makefile 负责收录源文件:

# board/myvendor/myboard/Makefile obj-y += myboard.o obj-$(CONFIG_MYBOARD_DDR) += ddr_init.o

Kconfig 负责定义板级配置项,并被上级 Kconfigsource进来:

# board/myvendor/myboard/Kconfig config TARGET_MYBOARD bool "My Vendor MyBoard" select MYCHIP_COMMON help Support for My Vendor MyBoard.

注意select的用法——它表示"选中本项时自动选中依赖项"。移植时如果发现某个公共驱动没被编进来,检查是不是漏了select。

5.4 板级头文件里该放什么、不该放什么

include/configs/myboard.h是传统宏的地盘,但新代码应该尽量少往里塞东西。我的经验是只放三类:

  • 地址类宏:CONFIG_SYS_TEXT_BASE、CONFIG_SYS_LOAD_ADDR、CONFIG_SYS_SDRAM_BASE,这些和具体硬件绑定,放头文件合理。
  • 环境变量默认值:CONFIG_EXTRA_ENV_SETTINGS,定义bootcmd、bootargs的默认值。
  • 无法用 Kconfig 表达的宏:比如某些需要拼接字符串的宏。

其余能进 Kconfig 的都进 Kconfig。判断标准很简单:如果这个宏的值是y/n或者一个数字,且不涉及字符串拼接,就应该做成 Kconfig 项。

6. 移植过程中最容易踩的五个坑

6.1 改了 defconfig 但没 savedefconfig

前面提过,defconfig是差异清单。很多人习惯直接make menuconfig改配置,改完make编译通过,就以为完事了。结果下次别人make xxx_defconfig重新生成.config,你的修改全没了。

正确流程是:make menuconfig改完 →make savedefconfig→ 把生成的defconfig覆盖到configs/xxx_defconfig。savedefconfig会自动剔除和默认值相同的项,生成最精简的差异清单。

6.2 obj-y 漏写导致链接错误

这是最高频的错误。现象是undefined reference to 'xxx',但xxx明明在某个.c文件里定义了。排查步骤:

  1. 找到定义xxx的.c文件。
  2. 看它所在目录的Makefile或Kbuild有没有obj-y += xxx.o。
  3. 看它所在目录有没有被上一级 Makefile 的obj-y收录。
  4. 逐级往上查,直到顶层。

我一般用grep -rn "xxx.o" --include=Makefile --include=Kbuild快速定位。

6.3 SPL 和 proper 共用代码的重复定义

如果一段代码在 SPL 和 proper 里都编了,但里面有全局变量或函数,链接时可能报multiple definition。解决办法是用CONFIG_SPL_BUILD做条件编译,或者把公共代码抽出来只编一次。

6.4 Kconfig 依赖关系写错导致配置项不显示

make menuconfig里找不到某个配置项,通常是depends on写错了。比如:

config MYBOARD_DDR bool "DDR init" depends on TARGET_MYBOARD

如果TARGET_MYBOARD没选中,MYBOARD_DDR就不会显示。排查时用make menuconfig的搜索功能(按/),输入配置项名字,它会告诉你依赖哪些项、当前值是什么。

6.5 板级头文件宏和 Kconfig 项冲突

同一个配置,板级头文件里#define CONFIG_FOO 1,Kconfig 里又有config FOO,两者值不一致时行为不可预测。U-Boot 的规则是 Kconfig 优先,但头文件里的宏如果被 C 代码直接引用,可能绕过 Kconfig。移植时如果发现配置行为诡异,先检查有没有这种冲突。

7. 调试 Kbuild 问题的几个实用手段

7.1 用 V=1 看完整编译命令

默认编译输出是精简的,看不到实际执行的命令。加V=1可以打印完整命令行:

make V=1

这样你能看到每个.o是怎么编出来的、用了哪些-D宏、链接顺序是什么。排查宏定义问题和链接顺序问题时特别有用。

7.2 用 make -n 干跑看目标

make -n只打印不执行,可以看某个目标会触发哪些动作:

make -n u-boot

输出里能看到所有依赖的built-in.o和最终的链接命令。如果某个文件没出现在输出里,说明它没被obj-y收录。

7.3 检查 auto.conf 和 autoconf.h 的实际内容

配置问题最终都落到这两个文件上。直接看:

grep CONFIG_MYBOARD include/config/auto.conf grep CONFIG_MYBOARD include/generated/autoconf.h

如果 auto.conf 里有但 autoconf.h 里没有,说明配置项类型定义有问题(比如定义成了string但 C 代码当bool用)。

7.4 用 scripts/dtc 检查设备树

设备树问题经常伪装成 Kbuild 问题。比如CONFIG_DEFAULT_DEVICE_TREE="myboard"配了,但arch/arm/dts/myboard.dts不存在,编译时报的是 Makefile 找不到目标。用ls arch/arm/dts/ | grep myboard确认文件在不在,再用make dtbs单独编设备树看报错。

8. 我在这块踩过的几个真实教训

第一个教训是关于savedefconfig的。早期我改配置全靠menuconfig,改完直接提交,结果团队里其他人拉代码后make xxx_defconfig编译出来的东西和我本地不一样。查了半天才发现是 defconfig 没同步。从那以后我养成了习惯:任何配置改动,最后一步必须是make savedefconfig并覆盖configs/下的文件。

第二个教训是关于 SPL 的。有次移植一块新板子,SPL 阶段死活起不来,串口没输出。查了两天,最后发现是 SPL 的obj-y里漏了一个时钟初始化的文件。因为 U-Boot proper 阶段这个文件被另一个配置项收录了,所以 proper 能跑,SPL 不行。这个坑让我意识到:SPL 和 proper 的 Makefile 要分开审查,不能想当然认为配置项会同时生效。

第三个教训是关于 Kconfig 的select和depends on的区别。select是强制选中,depends on是条件依赖。有次我写了个配置项用select去选一个驱动,结果那个驱动又depends on另一个没选的项,导致配置树出现循环依赖,menuconfig直接报错。后来改成depends on加default y才解决。这两个关键字的语义差异,建议在写 Kconfig 前先翻一遍Documentation/kbuild/kconfig-language.rst。

最后一个经验是关于版本差异的。U-Boot 每季度一个版本,Kbuild 相关的细节经常变。比如某个配置项在 v2022.01 里叫CONFIG_FOO,到 v2023.01 改名成CONFIG_BAR了。移植时如果参考的是老版本代码,一定要对照当前版本的Kconfig确认配置项名字。我一般会git log --oneline -- configs/看一下目标板 defconfig 的变更历史,能省不少事。

这套 Kbuild 机制刚上手确实有点绕,但一旦理解了defconfig → .config → auto.conf/autoconf.h → obj-y → built-in.o这条链路,U-Boot 移植就从"玄学"变成了"工程"。后面再遇到编译问题,顺着链路一层层查,基本都能定位到根因。

返回列表