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 内核同源的那套KconfigMakefileKbuild组合。这个转变对移植工作的影响是根本性的以前你改一个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_XXXy或CONFIG_XXXnMakefile和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.includeKbuild 的通用规则库if_changed、cmd、build这些核心宏都在里面。include/configs/板级配置头文件传统宏的聚集地。3.2 从 defconfig 到 .config 的完整链路执行make xxx_defconfig的时候背后发生的事比你想的多Makefile 检测到目标是%config调用scripts/kconfig/conf工具。conf读取configs/xxx_defconfig这是一份精简的配置清单只列出和默认值不同的项。conf再读取顶层Kconfig递归解析所有source进来的子 Kconfig构建出完整的配置树。把 defconfig 里的值套到配置树上其余项取默认值生成.config。同时生成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.confMakefile 读的内容是CONFIG_XXXy这种形式被include进 Makefile 后变成 Make 变量。include/generated/autoconf.hC 代码读的内容是#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 的多阶段构建是移植里最容易出错的地方。SPLSecondary 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_BUILDy第一行生效编译 U-Boot proper 时CONFIG_SPL_BUILD未定义第二行生效。移植时的常见坑是某个初始化函数在 SPL 和 proper 里都要用但只在一个阶段编了另一个阶段链接时报未定义。解决办法是把公共代码抽到一个独立的.c文件在两个阶段的obj-y里都加上。我一般会在板级目录下建一个common.c专门放这种两阶段共用的代码。5. 板级移植实操从零给一块新板子接上 Kbuild5.1 选一个最接近的参考板移植的第一步不是写代码是找参考。U-Boot 源码里configs/目录下有上千个 defconfigboard/目录下有对应的板级代码。选参考板的原则是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_ARMy CONFIG_ARCH_MYCHIPy CONFIG_SYS_TEXT_BASE0x40000000 CONFIG_DEFAULT_DEVICE_TREEmyboard CONFIG_TARGET_MYBOARDy CONFIG_SYS_MALLOC_LEN0x400000然后执行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.oKconfig 负责定义板级配置项并被上级 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文件里定义了。排查步骤找到定义xxx的.c文件。看它所在目录的Makefile或Kbuild有没有obj-y xxx.o。看它所在目录有没有被上一级 Makefile 的obj-y收录。逐级往上查直到顶层。我一般用grep -rn xxx.o --includeMakefile --includeKbuild快速定位。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 1Kconfig 里又有config FOO两者值不一致时行为不可预测。U-Boot 的规则是 Kconfig 优先但头文件里的宏如果被 C 代码直接引用可能绕过 Kconfig。移植时如果发现配置行为诡异先检查有没有这种冲突。7. 调试 Kbuild 问题的几个实用手段7.1 用 V1 看完整编译命令默认编译输出是精简的看不到实际执行的命令。加V1可以打印完整命令行make V1这样你能看到每个.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_TREEmyboard配了但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 移植就从玄学变成了工程。后面再遇到编译问题顺着链路一层层查基本都能定位到根因。
返回列表