多彩编程 多彩编程MZPH · CODE BLOG
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的构建系统跟裸机工程里那种一个Makefile管所有完全不是一回事目录里散落着Kconfig、Makefile、Kbuild、.config、include/configs/xxx.h还有一堆obj-y、obj-$(CONFIG_XXX)的写法。这套东西就是Kbuild。它是Linux内核那套构建体系的同款U-Boot从2014年前后开始大规模迁移到Kbuild把原来手写的、板级强耦合的Makefile逐步替换成了基于Kconfig配置 Kbuild规则驱动的自动化构建。对移植工作来说这意味着两件事第一你新增一个板子、一个驱动、一个命令不再是去改某个巨大的Makefile而是在正确的位置放正确的文件写正确的Kconfig和Makefile片段第二一旦你理解了Kbuild的运作逻辑移植过程中90%的编译类问题都能自己定位而不是靠搜索引擎碰运气。这篇内容面向的是正在做U-Boot移植、被构建系统卡住的嵌入式工程师也适合想搞清楚U-Boot到底怎么把成千上万个文件组织成一个镜像的进阶学习者。我会从Kbuild在U-Boot里的实际角色讲起把配置、编译、链接三个阶段拆开重点讲清楚移植时最常打交道的几个文件该怎么写、为什么这么写最后给出一套我自己排查构建问题的固定套路。全程不堆砌概念尽量用你改哪个文件、改完会发生什么的方式来讲。2. Kbuild在U-Boot里的三层结构配置、编译、链接很多人把Kbuild理解成一套Makefile这个认知会直接导致移植时抓不住重点。准确地说Kbuild是一套由配置驱动的分层构建体系它在U-Boot里清晰地分成三层每层职责不同出问题的表现也不同。2.1 第一层Kconfig负责选什么Kconfig管的是配置项的声明与选择。你在make menuconfig里看到的每一个菜单、每一个[*]或[ ]背后都是某个目录下Kconfig文件里定义的一个config条目。比如CONFIG_CMD_FAT、CONFIG_SYS_TEXT_BASE、CONFIG_DM_GPIO这些符号不是凭空出现的而是有人在对应的Kconfig里写了声明。这一层的产物是.config文件在源码根目录里面是一行行的CONFIG_XXXy或CONFIG_XXX0x...。.config是后续所有编译行为的总开关。移植一块新板子时你首先要做的就是基于一个相近的参考板生成一份能用的.config通常通过make xxx_defconfig完成而xxx_defconfig本身是放在configs/目录下的一个精简配置文件。这里有个容易被忽略的点.config不是最终生效的东西。U-Boot构建时会用scripts/kconfig/conf工具把.config转换成include/generated/autoconf.h给C代码用的宏定义和include/config/auto.conf给Makefile用的变量。所以你在C代码里写的#ifdef CONFIG_XXX实际读的是autoconf.h而不是.config。这个转换链条是理解Kbuild的第一把钥匙。2.2 第二层Makefile/Kbuild负责编什么第二层是目录级的构建规则。U-Boot的每个源码目录下几乎都有一个Makefile里面用obj-y、obj-m、obj-$(CONFIG_XXX)这样的变量告诉构建系统这个目录下哪些文件要编进最终镜像。举个最直观的例子drivers/gpio/Makefile里会有类似这样的行obj-$(CONFIG_DM_GPIO) gpio-uclass.o obj-$(CONFIG_SANDBOX_GPIO) sandbox_gpio.o obj-$(CONFIG_ROCKCHIP_GPIO) rk_gpio.o含义是如果.config里CONFIG_DM_GPIOy那么gpio-uclass.o就会被编译并链接进U-Boot如果CONFIG_ROCKCHIP_GPIOy那么rk_gpio.o参与构建。注意这里没有手动指定编译哪些文件的过程一切都是配置驱动的。这就是为什么移植时你新增一个驱动文件只需要在对应目录的Makefile里加一行obj-$(CONFIG_YOUR_DRIVER) your_driver.o再在Kconfig里声明CONFIG_YOUR_DRIVER剩下的交给构建系统。obj-y和obj-$(CONFIG_XXX)的区别也很关键obj-y是无条件编译obj-$(CONFIG_XXX)是条件编译。移植时如果某个文件死活编不进去第一反应应该是去看它的obj-前缀对应的配置项在当前.config里是不是y。2.3 第三层链接脚本与镜像生成负责放哪里第三层是链接与镜像封装。所有.o文件编译完之后构建系统会按照链接脚本通常是arch/arm/cpu/u-boot.lds或板级定制的.lds把它们拼成一个可执行文件再经过objcopy等工具生成u-boot.bin、u-boot.img、u-boot.srec等最终产物。这一层跟Kbuild的关系在于链接脚本的路径、镜像的格式、是否加SPL、是否加设备树都是由配置项决定的。比如CONFIG_SPL打开后构建系统会额外走一遍SPL的编译链接流程生成spl/u-boot-spl.bin。移植时如果发现主U-Boot编出来了但SPL没生成就要去检查CONFIG_SPL及其相关依赖是否配置正确。把这三层记住后面所有排查都有了坐标系编译报错先看第二层Makefile里的obj规则配置不生效先看第一层Kconfig和.config镜像跑不起来先看第三层链接脚本和镜像格式。3. 移植前必须搞懂的Kconfig语法与依赖关系移植新板子时Kconfig是你打交道最多、也最容易写错的地方。它不像Makefile那样写错了立刻报错Kconfig的很多问题表现为配置项在menuconfig里根本看不到或者选了A却自动把B关掉了非常隐蔽。3.1 一个config条目的完整解剖先看一个典型的Kconfig条目来自U-Boot的驱动配置config DM_GPIO bool Enable Driver Model for GPIO drivers depends on DM default y if DM help Enable the GPIO uclass. This provides a common API for GPIO drivers to use.逐行拆解config DM_GPIO声明一个配置符号最终在.config里表现为CONFIG_DM_GPIO。bool ...类型是布尔型引号里的字符串是menuconfig里显示的菜单名。类型还可以是tristate、int、hex、string。depends on DM依赖项。只有当CONFIG_DMy时DM_GPIO这个选项才会出现在menuconfig里才可能被选中。这是移植时选项找不到的头号原因。default y if DM默认值。当满足DM条件时默认选中。help帮助文本menuconfig里按?能看到。移植时新增一个驱动你至少要写这么一个条目。写完之后如果menuconfig里看不到它按顺序检查三件事depends on的依赖是否满足、这个Kconfig文件是否被上一级Kconfig通过source引入、当前架构是否在arch/xxx/Kconfig里被包含。3.2 depends on 与 select 的取舍Kconfig里有两个控制依赖的关键字depends on和select。它们的区别在移植时非常关键。depends on是我需要别人先满足我才能被选。比如DM_GPIO依赖DM用户必须先开DM才能看到并开启DM_GPIO。这是向上依赖逻辑清晰推荐优先使用。select是我一旦被选就强制把别人也选上。比如某个板级配置select DM_GPIO意思是这块板子要用GPIO驱动模型自动把DM_GPIO打开。这是向下强制用起来方便但滥用会导致依赖关系混乱——你选了A结果B、C、D全被悄悄打开排查问题时非常痛苦。我的经验是板级配置和顶层功能用select驱动内部依赖用depends on。移植新板子时在board/xxx/Kconfig或arch/xxx/Kconfig里用select把这块板子需要的核心功能一次性拉起来是常见做法。但要注意select不会检查被选中的项自身的depends on是否满足如果被选中的项有未满足的依赖Kconfig会给出警告构建可能失败。3.3 defconfig与.config的关系以及移植时的正确姿势configs/xxx_defconfig是移植的起点。它是一份最小配置集只列出跟默认值不同的项。执行make xxx_defconfig时Kconfig工具会以xxx_defconfig为基础结合各Kconfig里的default值生成完整的.config。移植新板子的标准流程是找一块最接近的参考板复制它的defconfig重命名为你的板子名。修改defconfig里的关键项CONFIG_TARGET_XXX、CONFIG_SYS_TEXT_BASE、CONFIG_DEFAULT_DEVICE_TREE、串口相关配置等。执行make xxx_defconfig再make menuconfig微调把参考板有但你的板子不需要的驱动关掉。把最终.config里你手动改动的项回写到defconfig里用make savedefconfig生成精简版再覆盖configs/xxx_defconfig。注意make savedefconfig生成的defconfig只保留与默认值不同的项非常干净。移植完成后一定要执行一次否则你的defconfig会越来越臃肿下次别人用你的配置时会被一堆冗余项干扰。这里有个坑我踩过直接改.config然后make编译能过但下次make xxx_defconfig时改动全丢了。因为.config是生成物defconfig才是源头。移植期间所有持久化的配置改动都必须落到defconfig里。4. Makefile里的obj规则新增驱动和命令的正确写法配置层搞定之后就到了编译层。移植时你新增的每一个.c文件都必须通过某个Makefile里的obj-规则挂到构建树上否则它就是一个被忽略的孤儿文件编都不会编。4.1 obj-y、obj-m、obj-$(CONFIG) 的实际差异U-Boot里主要用obj-y和obj-$(CONFIG_XXX)obj-m用得少U-Boot基本不编内核模块。三者的行为差异如下表写法含义典型使用场景obj-y foo.o无条件编译foo.o核心框架代码如uclass核心obj-$(CONFIG_FOO) foo.o配置为y时编译绝大多数驱动、命令obj-$(CONFIG_FOO) foo/配置为y时进入子目录有多个文件的驱动子系统obj-$(CONFIG_FOO) bar.o baz.o一次挂多个文件同一驱动的多个源文件移植时最常见的操作是给新驱动加一行obj-$(CONFIG_MY_DRIVER) my_driver.o。这里有个细节变量名必须和Kconfig里的符号名严格对应CONFIG_MY_DRIVER对应config MY_DRIVER。大小写、下划线错一个字符编译时不会报错只是这个文件永远不被编译然后你在链接阶段看到undefined reference to my_driver_probe一脸懵。4.2 目录级Makefile的层级传递机制U-Boot的构建是递归的。顶层Makefile通过obj-y drivers/这样的规则进入子目录子目录的Makefile再通过obj-y gpio/进入更深一层。每一层的obj-y汇总起来最终形成完整的编译文件列表。理解这个机制对移植很重要。假设你要在drivers/misc/下新增一个驱动你需要确认drivers/Makefile里有obj-y misc/通常已有。drivers/misc/Makefile里有obj-$(CONFIG_MY_DRIVER) my_driver.o。drivers/misc/Kconfig里有config MY_DRIVER的声明。drivers/misc/Kconfig被drivers/Kconfig通过source drivers/misc/Kconfig引入。这四步缺一不可。我见过有人只做了第2步然后困惑为什么menuconfig里没有我的选项——因为第3、4步没做配置项根本不存在CONFIG_MY_DRIVER永远是空obj-$(CONFIG_MY_DRIVER)展开成obj-文件自然不编。4.3 新增一条U-Boot命令的完整落地路径命令command是U-Boot移植里很典型的一类新增内容它的落地路径能完整展示Kbuild的配置编译联动。假设你要加一条mycmd命令第一步在cmd/Kconfig里声明配置config CMD_MYCMD bool mycmd help Add a custom command for demonstration.第二步在cmd/Makefile里挂载文件obj-$(CONFIG_CMD_MYCMD) mycmd.o第三步写cmd/mycmd.c用U_BOOT_CMD宏注册命令#include common.h #include command.h static int do_mycmd(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]) { printf(mycmd executed\n); return 0; } U_BOOT_CMD( mycmd, 1, 1, do_mycmd, demonstrate a custom command, );第四步在板子的defconfig里加CONFIG_CMD_MYCMDy或者在menuconfig里手动打开。做完这四步重新编译mycmd就会出现在U-Boot的命令列表里。整个过程没有改任何全局Makefile这就是Kbuild的价值——新增内容只影响局部不破坏整体。提示U_BOOT_CMD宏会把命令结构体放到一个特殊的链接段.u_boot_list里U-Boot启动时扫描这个段来注册所有命令。所以命令文件即使编进去了如果链接脚本没有正确保留这个段命令也不会生效。移植时如果命令编进去了但用不了去检查链接脚本里.u_boot_list段是否被正确包含。5. 移植实战从零给一块新板子接入Kbuild前面讲的是原理和局部操作这一节把视角拉到整体走一遍给一块全新板子接入U-Boot Kbuild的完整流程。我以ARM架构、需要SPL、带设备树的板子为例这是目前最主流的场景。5.1 板级目录与Kconfig的创建U-Boot的板级代码放在board/vendor/boardname/下。新建目录后至少要有Makefile定义板级目标文件通常包含obj-y boardname.o。Kconfig声明板级配置项最关键的是config TARGET_BOARDNAME。boardname.c板级初始化代码实现board_init、dram_init等。Kconfig里典型写法if TARGET_MYBOARD config SYS_BOARD default myboard config SYS_VENDOR default myvendor config SYS_CONFIG_NAME default myboard endifSYS_CONFIG_NAME决定了使用include/configs/下哪个头文件这是老式配置和Kbuild配置的衔接点。移植时这个头文件里还保留着大量#define CONFIG_XXX它们和Kconfig的CONFIG_XXX是两套东西容易混淆。新移植的板子应尽量把配置往Kconfig迁移头文件里只留少量无法用Kconfig表达的宏。5.2 defconfig的编写与关键配置项configs/myboard_defconfig是移植的核心文件。一个能跑起来的最小配置大概包含CONFIG_ARMy CONFIG_TARGET_MYBOARDy CONFIG_SYS_TEXT_BASE0x80800000 CONFIG_DEFAULT_DEVICE_TREEmyboard CONFIG_SYS_MALLOC_F_LEN0x2000 CONFIG_SPLy CONFIG_SPL_TEXT_BASE0x80000000几个关键项的含义和取值依据CONFIG_SYS_TEXT_BASEU-Boot主镜像的运行地址。这个值必须和你的DDR布局、启动流程匹配。SPL把U-Boot搬到DDR的哪个位置这里就填哪个位置。填错了表现为串口无输出或跑飞。CONFIG_DEFAULT_DEVICE_TREE默认设备树名对应arch/arm/dts/myboard.dts。设备树文件也要同步创建。CONFIG_SPL_TEXT_BASESPL的运行地址通常在片上SRAM里具体值查芯片手册。CONFIG_SYS_MALLOC_F_LEN早期malloc池大小太小会导致驱动probe时分配失败。这些值没有通用答案必须结合具体芯片的地址映射来定。移植时最可靠的做法是找同芯片或同系列的参考板抄它的地址配置再根据自己板子的实际硬件微调。5.3 设备树与Kbuild的联动设备树在U-Boot里的构建也走Kbuild。arch/arm/dts/Makefile里有类似dtb-$(CONFIG_TARGET_MYBOARD) myboard.dtb这行的作用是当CONFIG_TARGET_MYBOARDy时把myboard.dts编译成myboard.dtb并打包进U-Boot镜像通常是u-boot.dtb再和u-boot.bin拼成u-boot-dtb.bin。移植时如果设备树没生效检查顺序是dts/Makefile里有没有dtb-$(CONFIG_TARGET_MYBOARD)、defconfig里CONFIG_DEFAULT_DEVICE_TREE是否指向正确的dts名、dts文件里的compatible是否和驱动匹配。这三者任何一个不对设备树就是编进去了但没被用。6. 构建问题排查一套可复用的定位套路移植过程中构建问题五花八门但绝大多数可以归到几类。这一节给出我自己用的一套排查套路按从配置到编译到链接的顺序推进基本能覆盖90%的情况。6.1 配置项不生效的三步定位法现象你在menuconfig里开了某个选项但代码里的#ifdef CONFIG_XXX分支没进去或者驱动没被编译。第一步确认.config里有没有这一项。执行grep CONFIG_XXX .config。如果没有说明menuconfig的改动没保存或者这个选项被依赖关系隐藏了。第二步确认include/generated/autoconf.h里有没有#define CONFIG_XXX 1。如果没有说明.config到autoconf.h的转换没跑通常是构建中途被打断执行make clean后重来。第三步确认include/config/auto.conf里有没有CONFIG_XXXy。这个文件是给Makefile用的如果它没有obj-$(CONFIG_XXX)就展开成空文件不编。这三步走完配置问题基本定位。我把它总结成一句话.config管C代码的宏auto.conf管Makefile的变量两个都要有。6.2 undefined reference的常见根因现象链接阶段报undefined reference to xxx。根因通常有三类文件没被编译obj-$(CONFIG_XXX)里的配置项没开或者Makefile里压根没写这行。排查方法是grep -r xxx.o --includeMakefile看有没有地方挂载它。函数签名不匹配头文件里声明和C文件里定义不一致C的name mangling问题U-Boot是C基本不涉及或者static函数被跨文件调用。链接脚本丢弃了段某些函数被放到了自定义段链接脚本没保留。这种情况报错信息里通常带段名。我遇到最多的是第一类。移植时新增文件忘了改Makefile或者配置项名字写错都会导致这个报错。6.3 镜像能编出来但跑不起来的排查方向这是最难受的一类编译链接全过镜像也生成了但板子上电后串口无输出或者输出乱码。按可能性排序CONFIG_SYS_TEXT_BASE或CONFIG_SPL_TEXT_BASE填错。地址不对代码被搬到错误位置第一条指令就飞了。这是头号原因。串口配置错误。时钟频率、波特率、寄存器基地址不对表现为乱码或无输出。检查设备树里的串口节点和CONFIG_BAUDRATE。DDR初始化失败。SPL阶段DDR没配好主U-Boot搬过去后访问内存就崩。检查SPL里的DDR初始化代码和参数。设备树没被正确加载。U-Boot找不到设备树初始化流程中断。排查这类问题最有效的手段是用调试器在SPL入口和U-Boot入口下断点看PC能不能走到走到哪一步飞了。纯靠打印有时候连第一行都出不来调试器是唯一可靠的工具。提示移植新板子时建议先用一个最小可运行配置——只开串口和DDR其他驱动全关。确认能打印出U-Boot版本信息后再逐步加驱动。这样能把问题范围缩到最小避免一上来就被一堆驱动的依赖问题淹没。7. 我在移植中踩过的几个Kbuild坑最后分享几个具体踩过的坑都是文档里不会写、但实际移植时很容易中招的。坑一savedefconfig之后配置变少了以为丢配置。其实savedefconfig只保留与默认值不同的项那些看起来丢了的配置是因为它们的值恰好等于Kconfig里的default。这是正常行为不是bug。判断有没有真丢看make xxx_defconfig make能不能编过就行。坑二改了Kconfig但menuconfig里看不到新选项。九成是因为这个Kconfig文件没被上一级source引入。Kconfig的引入是显式的不像Makefile那样自动递归。新增Kconfig文件后一定要在父级Kconfig里加source path/to/Kconfig。坑三obj-y和obj-$(CONFIG_XXX)混用导致重复编译。同一个文件如果在两个地方被挂载会报重复定义。移植时复制参考板的Makefile容易把参考板的规则一起带过来和自己的规则冲突。新增规则前先grep一下文件名。坑四设备树改了但没重新编译。Kbuild对dts的依赖跟踪有时候不敏感改了dts但make没重新生成dtb。稳妥做法是改完dts执行make dtbs或直接make clean重来。这个坑在调试设备树相关问题时特别浪费时间。坑五并行编译make -jN暴露的依赖缺失。单线程编译能过-j8就报错通常是Makefile里缺少显式的依赖声明。U-Boot的Kbuild体系对依赖处理得比较好但你自己新增的规则如果涉及生成文件要手动加依赖。移植期间建议先用make -j1确认逻辑正确再用并行加速。这套Kbuild体系刚接触时确实有点绕但它的设计逻辑是自洽的配置驱动编译编译产出链接每一层职责清晰。移植时只要记住配置项在Kconfig里声明、文件在Makefile里挂载、地址在defconfig里定大部分问题都能顺着这条线索找到根因。真正花时间的往往不是理解原理而是把板子的具体硬件参数地址、时钟、引脚填对这部分没有捷径只能对着芯片手册一项项核对。
返回列表