ARTICLE DETAIL

资讯详情

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

U-Boot移植实战:从顶层Makefile到链接脚本的构建体系全解

U-Boot移植实战:从顶层Makefile到链接脚本的构建体系全解 直接上手改U-Boot的时候十个人里有八个是先打开顶层Makefile看两眼然后被那一堆ifdef、obj-y、lib-y绕晕最后选择在板级目录里复制粘贴。我刚开始移植U-Boot时也这样对着include/configs/下的头文件猛改烧进去发现串口只有回车没有输出折腾了三天最后发现是链接脚本里TEXT_BASE和Makefile传参没对上。这篇文章不聊具体某颗芯片的寄存器配置而是把U-Boot移植过程中和Makefile相关的这条线彻底捋一遍从顶层构建流程到板级Makefile写法再到链接脚本生成和常见报错排查。无论是你在做单片机裸机迁移到U-Boot、手里攥着一块新板子想跑起官方代码还是只是想把一个已有的BSP从旧版本升级到新版本这份经验都适用。U-Boot的构建体系是整个移植工作的骨架。很多人把注意力放在board_init_r、dram_init这些C函数上认为改好它们就能启动却忽略了Makefile决定了一个文件编不编译、以什么方式编译、链接进哪个段。移植工作里碰到的所谓“怪毛病”一大半的根源在构建系统而不是C代码。1. 移植U-Boot先搞清Makefile在“管”什么1.1 从一次失败的烧录说起我最早接触U-Boot移植是在一块国产ARM开发板上。按照网上的教程先make xxx_defconfig然后make -j8顺利得到了u-boot.bin。烧进去之后板子完全没有反应连串口都没有任何输出。反复检查硬件、拨码开关、烧录地址最后发现我用的defconfig里面CONFIG_SYS_TEXT_BASE定义的值和板子实际的内存映射差了0x10000000。而更隐蔽的问题在于我改完这个宏之后并没有重新执行配置流程导致Makefile依然按照旧的参数去生成链接脚本。U-Boot的构建系统和Linux内核一样是由嵌套的Makefile体系构成的。直接执行make的时候顶层的Makefile负责调度整个流程但它本身并不直接编译多少代码。真正干活的是各个子目录下的Makefile以及被include进来的config.mk、scripts/Makefile.build这些辅助文件。理解这个分层结构是移植工作的第一课。1.2 U-Boot构建的三个阶段U-Boot的构建过程大致可以拆成三个阶段配置阶段、编译阶段、链接阶段。这三个阶段在Makefile里对应三套不同的机制。配置阶段做的事情是把defconfig里的CONFIG_XXXy这样的配置项转换成编译时真正用到的include/config.h和include/config/auto.conf。前者供C代码使用#include config.h之后就能直接用CONFIG_XXX宏后者供Makefile使用通过CONFIG_XXX : y的形式让Makefile能够做条件判断。这一步由顶层的sinclude机制和scripts/Makefile.autoconf完成。在旧版本U-Boot里这一步靠的是mkconfig脚本现在则完全由Makefile体系接管。这也是很多移植教程让人困惑的地方——你看老教程说执行make xxx_config注意是_config不是defconfig新代码里早就没有这个目标了。编译阶段比较好理解就是遍历各子目录按需编译.c文件生成.o再打包成built-in.o。但这里有个移植时容易忽略的点obj-y和lib-y的写法决定了文件是直接链接进镜像还是先归档成库。比如板级目录下放一个led.o写obj-y led.o它就会被链进U-Boot主体如果某些代码你希望按需链接、避免被垃圾回收掉就要仔细考虑是放在lib-y还是obj-y。链接阶段是Makefile里最玄乎也最关键的部分。链接脚本u-boot.lds不是手写的而是由u-boot.lds.S经过预处理器生成的。预处理器会读取CONFIG_SYS_TEXT_BASE、CONFIG_SYS_UBOOT_BASE这些宏决定_start入口放在哪个地址。如果你改了内存布局相关的配置项却不执行配置阶段的命令那么生成的链接脚本还是旧的编译出来的镜像烧进去自然跑不起来。1.3 顶层Makefile、config.mk、scripts/Makefile.build各自“管”什么把这几层的关系搞明白移植的时候你就知道该去哪里改东西。顶层Makefile是总入口负责解析命令行参数、加载config.mk、处理defconfig目标、设置全局变量比如CROSS_COMPILE、ARCH、CPU然后调用子目录的Makefile。移植时最常改的就是CROSS_COMPILE如果你用的交叉编译器叫arm-none-eabi-就在执行make时通过参数传入make ARCHarm CROSS_COMPILEarm-none-eabi-。当然现在新版U-Boot更推荐放到环境变量或者defconfig里免得每次敲一堆参数。config.mk是顶层Makefile在配置阶段include进来的关键文件它会根据当前的ARCH、CPU、SOC、BOARD加载对应的arch/arm/config.mk、arch/arm/cpu/armv7/config.mk这些层级配置文件。这里定义了大量针对特定SoC的编译选项、地址参数比如CONFIG_SYS_TEXT_BASE的默认值往往就散落在这里。有个细节当你在defconfig里加了CONFIG_SYS_TEXT_BASE它的优先级会覆盖config.mk里的默认值但机制上两者是“合并”进Makefile变量的而不是简单的“后者覆盖前者”不同版本U-Boot对这个宏的处理方式还发生过变化这也是移植时一定要留意版本差异的地方。scripts/Makefile.build更像是一个“通用编译模板”。它定义了单个C文件如何编译成.o、.o如何打包成built-in.o、如何生成.d依赖文件等规则。子目录下的Makefile其实只是声明“我要编译哪些文件”真正干活的规则都在这份模板里。所以你在板级目录的Makefile里看到obj-y foo.o不要觉得奇怪——它只是给模板提供输入。2. 移植时真正要动的Makefile与配置入口2.1 板级目录下的Makefile为什么必须改做移植时第一步通常是复制别人的板级目录。比如你的板子用的SoC和某款开发板接近就复制那款板子的整个目录改名然后开始改。复制完之后板级目录下的Makefile决定了哪些文件参与编译。我见过不少人把新的板级目录加进来之后执行make xxx_defconfig报错提示找不到这个配置。原因往往是Kconfig里没有注册这个新板子——这其实也是Makefile体系的一部分Kconfig被配置系统引用你没有在arch/.../Kconfig里加source路径配置目标就生成不了。板级目录下的Makefile通常写成这样obj-y board.o obj-y ddr.o obj-y clock.o移植时你可能需要在里面加自己的flash.o、eth.o。注意obj-y里的文件名不要带路径如果你的源码放在子目录里要写成obj-y ddr/ obj-y ddr/ddr.o第一个写法把整个子目录编进来第二个写法单独指定一个文件。对应地子目录里还要有它自己的Makefile。我见过有人把源文件放在子目录里但子目录没有Makefile结果编译时报No rule to make target一脸懵。这个报错在热词里也出现了我后面会专门讲。2.2 defconfig与Kconfig从“配置”到Makefile变量的链路新版U-Boot全面引入了Kconfig体系make xxx_defconfig会读取configs/xxx_defconfig把里面的CONFIG_FOOy写入include/config/auto.conf。Makefile在读取这个文件之后才能判断ifdef CONFIG_SPL_BUILD obj-y spl.o endif这条链路是移植时排查问题的关键。假如你在defconfig里写了一个CONFIG_MY_BOARD_SUPPORT然后想在这一句的#ifdef CONFIG_MY_BOARD_SUPPORT里添加代码但编译后一点效果都没有。大概率是你改了defconfig之后没有重新执行make xxx_defconfig或者执行了make但Makefile认为配置没有变化没有重新生成auto.conf。养成习惯每次改defconfig先执行make xxx_defconfig再编译不要偷懒。这里还要提一下include/configs/下的头文件。老版U-Boot大量使用#define CONFIG_SYS_XXX新版虽然迁移到Kconfig但板级头文件仍然保留了大量板级参数。C代码里#include configs/xxx.h能读到这些宏但Makefile的ifdef CONFIG_XXX读不到——Makefile只能读auto.conf头文件里的宏对它无效。这个坑我踩过不止一次在头文件里加了#define CONFIG_MY_FEATURE 1然后在Makefile里写ifdef CONFIG_MY_FEATURE结果死活不生效。正确的做法是Makefile能感知的配置必须放到defconfig或Kconfig里。2.3 从热词“makefile 头文件路径”说起热搜词里有“makefile 头文件路径 rv1106”这说明很多人碰到了编译时找不到头文件的问题。U-Boot里的头文件搜索路径主要靠-I参数传入。scripts/Makefile.build里会计算出一大堆-I路径包括include、arch/arm/include、board/xxx/include等。当你自己写的.c文件在板级目录里想要#include my_common.h而这个头文件放在同一个板级目录下光靠默认路径往往找不到。正确的做法是在板级目录的Makefile里加ccflags-y -I$(srctree)/board/$(BOARD)/include或者更规范一点把头文件放到include/configs/目录下因为U-Boot默认就会搜索这个目录。但这种做法会让板级头文件越来越臃肿不符合现在的代码风格。我个人更喜欢把板级独有的头文件放在板级目录的一个include子目录里然后通过ccflags-y指过去。关于RV1106这类带NPU的SoC移植时往往还涉及DDR初始化、电源管理这些二进制固件的加载这时候Makefile里可能会涉及CONFIG_ROCKCHIP_*之类的配置项和对应二进制文件的复制规则。这些二进制文件放哪个目录、Makefile怎么把它们打包进最终镜像和头文件路径问题一起构成了瑞芯微平台移植时最常翻车的两个点。遇到这类情况最好的办法是找官方BSP里同系列板卡的Makefile作为参照别自己瞎写路径。3. 链接脚本与镜像生成Makefile里最容易被忽略的环节3.1 u-boot.lds不是手写的是算出来的很多人以为u-boot.lds是像单片机工程里那样手写出来的链接脚本其实U-Boot的链接脚本是.S文件经过C预处理器生成的。顶层Makefile里有一条规则大意是把arch/arm/cpu/armv7/u-boot.lds.S不同架构路径不同通过$(CPP)处理生成u-boot.lds。这个过程会把代码里的宏全部展开比如#include config.h SECTIONS { . CONFIG_SYS_TEXT_BASE; ... }最终生成的u-boot.lds里CONFIG_SYS_TEXT_BASE会被替换成具体的数字。这意味着如果你修改了CONFIG_SYS_TEXT_BASE它不只是影响C代码里的宏还影响整个镜像的链接地址。而链接地址错了烧进去之后的表现五花八门有完全没反应的、有串口正常但go命令起不了内核的、有能起来但跑着跑着就死了的。排查这类问题有一个实用技巧编译之后在源码根目录下查看生成的u-boot.lds确认里面的地址和你设想的是否一致。不要只看defconfig里写了什么要看Makefile真正生成了什么。我在实际调试中经常执行make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- u-boot.lds单独重新生成链接脚本然后打开看。这样可以快速确认地址相关问题。3.2 镜像格式生成那几行命令make之后你会得到u-bootELF文件、u-boot.bin裸二进制、u-boot.img带U-Boot头等文件。这些格式转换的命令都写在顶层Makefile里。主要有几个关键步骤链接得到u-bootELF用objcopy -O binary抠出u-boot.bin用mkimage工具加上校验头生成u-boot.img。如果你做的是SPLSecondary Program Loader还会有u-boot-spl.bin它的生成规则单独一套。移植过程中最常用到的操作是改mkimage的参数。比如有些平台要求镜像头的加载地址和入口地址分开设置需要在MKIMAGE相关变量里调整-T、-A、-C、-a、-e这些参数。这里有个常见混淆-a是加载地址-e是入口地址两者可以不同。很多人在SPL跳转U-Boot时卡住检查一下u-boot.img的头信息发现-a和-e写反了或者都写成了DDR地址而SPL直接把U-Boot加载到了SRAM那当然跳不过去。查看镜像头信息用mkimage -l u-boot.img你会看到类似Load Address和Entry Point两行。移植时多看一眼这个输出能省掉很多瞎猜的时间。3.3 调试时常用的Makefile变量与技巧U-Boot的Makefile体系里有一些变量调试移植问题时很有用。首先是V1这是我最常用的。执行make V1Makefile会把每条实际执行的命令完整打印出来。之前编译报错你想知道某个.c文件到底用的什么头文件搜索路径执行make V1然后复制出错的那条编译命令自己手动加-E展开预处理看宏定义排查效率极高。其次是O指定编译输出目录。如果你同时维护几个不同配置的板子强烈建议用make Obuild/boardA xxx_defconfig make Obuild/boardA -j8这样不同板子的中间文件不会互相污染切换配置时也不用make distclean。这个习惯在我同时弄两块板子的时候帮了大忙。还有一个容易被忽略的变量是KBUILD_VERBOSE效果和V1类似。另外CROSS_COMPILE设置里可以带绝对路径比如make CROSS_COMPILE/opt/toolchains/arm-2014.05/bin/arm-none-eabi-确保没有配错工具链。这里要注意的是U-Boot对工具链版本敏感性不低太老或太新的编译器都可能引发奇怪的链接问题。我之前遇到过用GCC 12编译旧版U-Boot链接阶段报lzma相关的错换用GCC 8就没事。所以移植时记录一下验证过的工具链版本能少很多折腾。4. 常见报错与排查技巧实录4.1 make: *** No rule to make target找不到makefile怎么办热搜词里“make没有指明目标并且找不到makefile”是很多人第一次接触U-Boot编译时的拦路虎。这个报错的原因通常有两个一是你根本不在U-Boot源码根目录下执行make二是当前目录没有Makefile文件。很多初学者把u-boot.bin当成一个独立文件下载源码压缩包后只解压了部分文件或者直接在一个空的build目录里执行make。U-Boot的构建体系要求你在源码根目录下操作除非你用了O参数指定输出目录但即便指定了输出目录Makefile本身还是从源码根目录读取的。如果是“编译途中出现No rule to make target”那问题往往出在某个子目录的Makefile引用了不存在的文件。比如你加了一个obj-y foo.o但foo.c并不存在或者foo.c被放在了别的目录。Makefile在解析依赖时找不到规则就会报这个错。我的排查步骤是确认当前目录有没有Makefile没有就进源码根目录。执行make V1看完整输出找到报错前最后处理的目录是哪个。进到那个目录检查它下面的Makefile确认obj-y或lib-y里写的文件名是否真实存在。如果文件名存在但还报错检查是否有大小写差异。Linux文件系统区分大小写FOO.c和foo.c是两个文件。4.2 头文件路径不对与配置项搜不到编译时报fatal error: xxx.h: No such file or directory这个基本就是上面说的头文件搜索路径问题。我需要区分两种情况第一种是你的源码里引用了U-Boot本身带的头文件但名字写错了或者路径不对第二种是你引用了自己添加的头文件但没有把它的目录加进ccflags-y。第一种情况优先检查#include的写法。U-Boot的公共头文件在include/目录可以用#include common.h这种尖括号写法。板级头文件在include/configs/目录通常写作#include configs/xxx.h。如果你加了一个自定义头文件到include/下直接用#include myheader.h是可以找到的因为-Iinclude在默认搜索路径里。但如果你把它放在板级目录下就一定要加-I。第二种情况我给一个可以直接抄的板级Makefile示例ccflags-y -I$(srctree)/board/$(BOARD)/include obj-y board.o obj-y my_driver.o这里$(srctree)是源码根目录$(BOARD)是板级目录名。加上这一行board/xxx/include/myheader.h就能被找到了。还有一个和“配置项搜不到”相关的坑你明明在defconfig里写了CONFIG_FOOy但在C代码里用#ifdef CONFIG_FOO却始终不生效。原因很可能是你用了CONFIG_FOO而Kconfig里定义的是CONFIG_FOO_SUPPORT或者auto.conf还没有重新生成。执行一下grep CONFIG_FOO include/config/auto.conf看这个文件中到底有没有你想要的宏。这是我和Makefile打交道时最高频的排查命令。4.3 编译通过但链接失败编译一行不报错最后链接的时候报undefined reference to xxx这在移植时也极常见。通常原因有两个。第一个是obj-y里没有包含提供这个符号的文件。比如你在board.c里调用了ddr_init()但ddr.o不在obj-y里链接时自然找不到。第二个是函数声明和定义不匹配常见的是inline函数在某个编译单元里被优化掉了导致没有生成符号。这时可以看链接器的报错信息它会指出哪个文件引用了哪个符号顺着去找对应文件是否被编译、是否被链接进来。另一个链接阶段的隐蔽问题是u-boot.lds要求某些段必须存在。如果你裁剪了很多功能某些段因为内容为空被链接器丢弃但链接脚本里还有对应的. ALIGN(4);语句有时会引发奇怪的address溢出问题。解法是打开u-boot.lds看具体是哪个段不对劲再回到Makefile里决定哪个文件必须保留比如在板级Makefile里把关键对象放obj-y确保它不会被丢弃。下面把上面提到过的常见排查点整理成一个速查表现象可能原因排查命令/手段找不到makefile不在源码根目录执行makels Makefile确认有文件再makeNo rule to make targetMakefile里引用文件不存在或路径错make V1定位目录检查obj-y头文件找不到缺少-I路径或文件名错误查ccflags-y手动预处理看搜索路径配置宏不生效auto.conf未更新或宏写在头文件里grep CONFIG_FOO include/config/auto.conf链接undefined reference目标文件没编进obj-ymake V1看链接命令查built-in.o烧录无输出TEXT_BASE错误或链接脚本旧检查u-boot.lds实际地址镜像头信息不对mkimage参数错误mkimage -l u-boot.img查看-a/-e4.4 和“新旧U-Boot版本差异”有关的坑U-Boot版本迭代很快不同版本的构建系统差异大到像两个项目。如果你拿着旧版移植笔记硬套新版会踩很多坑。旧版U-Boot2014年之前那种用make xxx_config生成配置新版用make xxx_defconfig。旧版大量使用include/configs/下的头文件定义宏新版本逐渐把这些宏迁移到Kconfig。旧版顶层的config.mk写了很多逻辑新版把这些逻辑拆分到了arch/arm/mach-xxx/config.mk或者scripts/Makefile.spl里。个人建议移植之前先确定主线版本然后只参考该版本对应的官方文档和同版本的其他板级移植案例。不要拿2013年的教程去套2020年的代码也不要把新版代码的方式硬塞回旧版框架。我在多个项目中深有体会版本不同Makefile细节不同但最顶层的那一套obj-y、lib-y、ccflags-y思想是始终沿用的。5. 一些关于Makefile的“移植心法”这里说的“心法”不是玄学而是踩过足够多的坑之后沉淀下来的做事习惯。第一改任何配置或Makefile之后先执行make xxx_defconfig再编译。不要觉得多余。Makefile的依赖判断并不总是可靠的尤其当你改了Kconfig相关文件之后有时不会自动触发重配置。强制重新生成一次include/config/auto.conf能从源头上消除一大类“改了没生效”的问题。第二编译输出目录隔离。同时维护多个板型时make Obuild/boardA和make Obuild/boardB分开切换板型不需要反复distclean也不用担心中间文件串味。这个习惯能省下大量等候编译的时间。第三出现诡异问题先看V1输出和实际生成的文件而不是猜。make V1会打印完整的命令行u-boot.lds、include/config/auto.conf这些产物都在源码树里躺着。打开看真相往往就在那里——是地址没对上还是宏没展开一眼就能看出来。第四移植时按“最小改动”原则。本来能编译通过的代码不要因为风格问题顺手大改Makefile。一次只动一个变量编译验证一次。我见过有人为了“优化”把板级Makefile从obj-y改成lib-y结果因为符号链接顺序问题引入了一堆undefined reference最后花了两天排查。构建系统这东西稳定运行的就是好的不要做无谓的“优化”。第五留意工具链版本。U-Boot对编译器的隐式依赖比想象中多尤其涉及内嵌汇编、链接脚本预处理时。一个已知可用的工具链版本比一个最新的工具链更值得用于移植调试。新版工具链如果出现问题先别急着怀疑自己的代码试着切换到官方验证过的工具链版本跑一遍能迅速缩小排查范围。我在实际项目中还有一个习惯每次执行编译都把命令行吹出来的完整log保存下来命名带上版本和时间。比如build_20250112_v3.log。出了问题翻log比重新编译更快还能对比两个版本之间的编译差异。这个习惯帮我定位过好几次“之前能编过现在编不过”的回归问题。说到底U-Boot移植的本质不是把代码“改”到能跑而是把Makefile背后隐藏的构建逻辑理顺。你只要搞懂配置怎么传递、文件怎么编进来、链接地址怎么确定那些看起来神乎其神的“移植技巧”其实都是水到渠成的事情。希望这篇围绕Makefile的拆解能让你下次面对一块新板子时少走几步弯路。
返回列表