尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

imx6ull实战:用BuildRoot集成自定义so文件全流程解析

imx6ull实战:用BuildRoot集成自定义so文件全流程解析 之前写过一篇关于 imx6ull 和 BuildRoot 的分享那会儿还在项目初期的摸索阶段很多理解停留在“能编出来”的层面。最近因为一个量产项目的需要我重新把整个构建流程从零过了一遍又踩了不少坑也把之前没讲透的地方彻底想明白了。这篇重制版就是把这一轮的实际经验重新梳理一遍重点放在“怎么在 BuildRoot 体系里把自己的 so 文件加进去、编出来、跑起来”这条主线上。先说清楚这篇文章能帮你解决什么问题如果你在用 imx6ull 做项目不想用厂商提供的现成 SDK 或者手动交叉编译那一套而是想通过 BuildRoot 一次性生成可烧写的系统镜像并且需要把自研算法库、第三方预编译库这些 so 文件干净地集成进去那这篇内容应该能帮你少走很多弯路。我尽量把关键原理和具体操作都写出来偏实践但不跳过“为什么”。1. 项目整体思路为什么在 imx6ull 上选 BuildRoot1.1 imx6ull 项目里构建系统到底在解决什么问题先说项目背景。imx6ull 这颗芯片是 NXP i.MX 6ULL 系列Cortex-A7 单核/双核主频不高但外设齐全、价格友好、资料丰富很多工业控制、物联网网关、边缘计算小盒子都拿它做主控。我手里的这个项目也是典型的小型边缘设备一个核心板加一块底板跑 Linux需要支持网络通信、串口外设、本地存储最后把整个系统做成一个可烧录的镜像交付给产线。刚开始做的时候我确实动过手动交叉编译的念头。下载工具链、编译 U-Boot、编译 Kernel、用 BusyBox 做根文件系统、再手动把各种库和应用程序拷进去。这一套流程听起来可控但实际上非常容易翻车。最痛苦的是依赖关系A 库依赖 B 库的某个版本B 库又依赖 C 库的编译选项一旦升级了某个组件整个镜像里所有东西都要跟着验证一遍。我试过在一个老项目里给 openssl 升个版本结果因为 glibc 版本不一致整个文件系统里一半的工具都跑不起来了。所以到 imx6ull 这个项目的时候我一开始就决定用构建系统来管理整个软件栈。构建系统做的事情本质上就是把“下载源码、选择配置、交叉编译、安装到根文件系统、打包镜像”这一整套流程自动化并且用一份可重复执行的配置来描述“我的系统里到底有哪些组件”。这样一来换一台机器、换一个版本、重新拉取代码最终做出来的镜像都是一致的。1.2 BuildRoot 和 Yocto、手动编译的对比市面上常见的选择无非是手动编译、Yocto、BuildRoot 这三条路。手动编译不再多说了适合那种“一辈子只做一次镜像”的项目不适合需要持续迭代的产品。Yocto 功能是真的强配方体系成熟社区庞大但学习曲线也真的是陡BitBake 的语法、layer 的组织方式、各种 append 文件的玩法没有一两个月的持续投入很难用得顺手。而且 Yocto 全量编译一次的时间非常感人我见过整整编一天的。BuildRoot 刚好卡在一个很舒服的位置它用类似内核的menuconfig方式选择软件包配置完直接make整个系统包括交叉工具链都会自动构建出来。它的包管理思路比 Yocto 简单直接得多而且对中小型嵌入式 Linux 系统来说BuildRoot 产出的镜像已经足够干净、足够小、足够可控。我整理了一个简单的对比表格可以更直观地看出区别方案上手难度定制灵活性构建速度依赖管理适用场景手动交叉编译中最高快靠自己一次性的老项目维护Yocto高极高很慢自动但复杂大型量产、多板型、长期演进BuildRoot低高中自动简单清晰中小型 Linux 设备、快速迭代BuildRoot 最让我满意的一点是它的“可复现性”。整个项目的软件配置都集中在一个.config文件里配合board/下面的板级文件git 一提交任何人拉下来都能重现同一个构建环境。这一点在团队合作、产线交付、后期维护中价值非常大。2. BuildRoot 的关键细节解析2.1 版本选型和目录结构BuildRoot 本身不是一个固定的软件版本它以 git 仓库的形式维护社区会定期发布稳定版本。做产品的话我建议直接选最近的 LTS 或长期维护版本不要去追 master 分支因为 master 上经常有包版本更新带来的兼容性波动出问题的时候很难判断是配置问题还是工具链问题。我的习惯是把 BuildRoot 作为一个独立仓库拉下来固定在某个 release tag 上然后把自己的板级配置、自定义软件包放到这个仓库里一起管理。完整的目录结构大致是这样的buildroot/ ├── board/ # 板级配置目录放内核配置、根文件系统 overlay、post-build 脚本 ├── configs/ # 现成的 defconfig 文件比如 imx6ull_myboard_defconfig ├── dl/ # 下载缓存目录所有源码包都缓存到这里 ├── output/ │ ├── build/ # 编译中间文件和源码目录 │ ├── host/ # 主机工具比如交叉工具链 │ ├── images/ # 最终生成的镜像文件 │ └── target/ # 目标根文件系统 ├── package/ # 软件包目录每个子目录一个软件包 └── Makefile很多新手容易忽略的是dl/目录的作用。BuildRoot 在构建过程中会去各个上游地址下载源码压缩包每次都去网上下载既慢又不可靠。dl/目录就是下载缓存同一个包只要下载过一次下次构建直接用本地缓存不重新下载。我做项目的第一件事就是搭好这个缓存目录避免后续反复构建时被网络问题卡住。output/target和output/build的区别也要搞清楚build是真正的编译工作目录里面是每个包解压后的源码和编译产物target是最终目标文件系统的目录所有包安装的目标文件都会汇聚到这里images里的根文件系统镜像就是从它生成的。2.2 defconfig 与 menuconfigBuildRoot 的配置入口BuildRoot 的配置方式有两种一种是命令行批量的 defconfig 文件一种是交互式的 menuconfig 界面。defconfig 文件本质上是一个预置的.config配置文件里面记录了所有软件包的启用状态和编译参数。NXP 官方和很多第三方核心板厂商都会提供现成的 defconfig比如imx6ull_defconfig或者厂商自己命名的板级配置。使用流程一般是make imx6ull_myboard_defconfig make menuconfig makemake imx6ull_myboard_defconfig是把预制配置写入当前的.configmake menuconfig是进一步微调。如果项目有特殊需求比如要加某个软件包、改文件系统格式、调整工具链选项这一步就能完成。menuconfig 里的关键区域我重点说几个Target options选择目标架构imx6ull 是 ARM Cortex-A7这里要选对工具链的编译参数会跟架构选项直接关联。Toolchain工具链配置。可以用 BuildRoot 自己构建的工具链也可以使用外部工具链。自己构建的好处是版本完全可控缺点是第一次编译工具链比较耗时。我一般选择自建工具链因为换机器重编的时候版本一致性有保障。Filesystem images选择根文件系统的镜像格式。imx6ull 的 NAND Flash 版本常用 ubifseMMC 或 SD 卡版本常用 ext4 或 squashfs需要根据实际启动方式选择。Kernel如果让 BuildRoot 顺便编译内核这里要配置内核源码路径和内核 defconfig。我的做法是让 BuildRoot 统一管理 U-Boot 和 Kernel这样每次构建一套镜像bootloader、内核、文件系统版本完全对应。Target packages选择需要集成的软件包这是最常用的区域。这一轮“重制版”里我最想补上的内容是如何不局限于菜单里已有的包把项目自己的 so 文件集成进去。这块才是真正定制化的关键下面重点展开。3. 实操过程在 BuildRoot 中添并编译 so 文件3.1 项目里为什么需要自己加 so先说说我遇到的实际场景。项目里有一块 FPGA 协同处理逻辑FPGA 厂商给了一个预编译的静态接口库只提供了.so文件和一个头文件另外我们自己还写了一个图像预处理算法库这个是有完整源码的。所以我在 BuildRoot 里同时需要支持两类 so 的集成一类是源码编译生成的动态库一类是厂商提供的预编译二进制库。这两类集成方式差别很大下面分开讲。在动手之前先理解 BuildRoot 安装软件包的核心机制。每个软件包最终都要通过make install把文件安装到两个地方一个是output/staging用于给其他编译中的软件包提供头文件和库文件一个是output/target最终目标文件系统。对于源码包BuildRoot 在$(TARGET_MAKE_ENV)环境背景下执行 make再用$(TARGET_INSTALL_TARGET_CMDS)把产物拷贝进 target。这也是我后面写自定义包时要遵循的基本框架。3.2 源码形式的库自定义 package 的标准写法如果 so 文件是我们自己写的源码库最干净的做法是在package/目录下新建一个自定义包让 BuildRoot 走标准编译流程。下面以一个名为mylib的算法库为例演示完整步骤。第一步创建包目录和文件package/mylib/ ├── Config.in ├── mylib.mk └── mylib.hash (可选建议有)第二步写Config.in。这个文件用于向 menuconfig 注册这个包config BR2_PACKAGE_MYLIB bool mylib help My custom algorithm library.bool后面的字符串就是菜单里显示的名称help里的内容可以不写。这个符号最终决定了BR2_PACKAGE_MYLIB这个宏是否被定义其他包可以通过select BR2_PACKAGE_MYLIB来建立依赖。第三步写mylib.mk。BuildRoot 的包命名规则是每个包一个.mk文件变量前缀是包名的大写形式后缀统一是_VERSION、_SITE、_DEPENDENCIES、_BUILD_CMDS、_INSTALL_TARGET_CMDS等。下面是一个完整的示例################################################################################ # # mylib # ################################################################################ MYLIB_VERSION 1.0.0 MYLIB_SITE $(BR2_EXTERNAL_MYLIB_PATH)/mylib MYLIB_SITE_METHOD local MYLIB_LICENSE Proprietary MYLIB_LICENSE_FILES LICENSE MYLIB_DEPENDENCIES openssl define MYLIB_BUILD_CMDS $(MAKE) -C $(D) \ CC$(TARGET_CC) \ CFLAGS$(TARGET_CFLAGS) \ LDFLAGS$(TARGET_LDFLAGS) endef define MYLIB_INSTALL_TARGET_CMDS $(INSTALL) -D -m 0755 $(D)/libmylib.so.1.0.0 \ $(TARGET_DIR)/usr/lib/libmylib.so.1.0.0 ln -sf libmylib.so.1.0.0 $(TARGET_DIR)/usr/lib/libmylib.so $(INSTALL) -D -m 0644 $(D)/mylib.h \ $(STAGING_DIR)/usr/include/mylib.h endef $(eval $(generic-package))重点解释一下这个文件里的几个关键点MYLIB_SITE_METHOD local表示源码直接来自本地目录不是去网上下载。MYLIB_SITE $(BR2_EXTERNAL_MYLIB_PATH)/mylib则指向外部目录路径由外部配置定义。$(MAKE) -C $(D)是进入包源码目录执行 make$(D)就是该包在output/build/mylib-1.0.0下的绝对路径。交叉编译的关键是把CC、CFLAGS和LDFLAGS传递给源码里的 Makefile这里为什么不直接写死arm-linux-gnueabihf-gcc而是用$(TARGET_CC)因为 BuildRoot 为每个包都准备了统一的交叉编译环境变量直接引用即可换了工具链配置也不用改这里。安装到$(TARGET_DIR)/usr/lib的原理是TARGET_DIR就是目标文件系统目录最终打包时会把它变成根文件系统镜像。同时把头文件安装到$(STAGING_DIR)/usr/includeSTAGING_DIR是编译期的 staging 目录如果后续还有别的软件包要链接这个库就需要从这里找到头文件和库文件。第四步在package/Config.in的主菜单文件中加入一行source package/mylib/Config.in这样make menuconfig里就能看到mylib这个选项了。第四步之后执行make menuconfig找到mylib并勾选然后makeBuildRoot 就会自动编译并安装这个 so 文件。3.3 预编译二进制 so不需要源码怎么集入再来看更棘手的场景厂商只提供libvendor.so没有源码没有 Makefile。这种库同样可以用自定义 package 来集成只是不需要BUILD_CMDS只需要INSTALL_TARGET_CMDS。我的做法是把这个预编译库放在一个固定目录里比如外部扩展目录下的vendorlib/子目录然后在.mk文件里写拷贝安装命令。核心文件长这样VENDORLIB_VERSION 1.0.0 VENDORLIB_SITE $(BR2_EXTERNAL_MYLIB_PATH)/vendorlib VENDORLIB_SITE_METHOD local define VENDORLIB_INSTALL_TARGET_CMDS $(INSTALL) -D -m 0755 $(D)/libvendor.so \ $(TARGET_DIR)/usr/lib/libvendor.so $(INSTALL) -D -m 0644 $(D)/vendor_api.h \ $(STAGING_DIR)/usr/include/vendor_api.h endef $(eval $(generic-package))注意这里没有定义MYLIB_BUILD_CMDS因为压根不需要编译。$(eval $(generic-package))会自动识别哪些宏有定义、哪些没有没有定义BUILD步骤就只做安装。对于这类预编译库有几点需要特别留意架构必须匹配拿到 so 文件之前先确认它是 ARM 版本file libvendor.so看一下输出如果是 x86 的那后面所有工作都白做。strip 处理BuildRoot 默认会在打包时对 target 里的文件进行 strip这通常没问题。但如果这个 so 文件本身有比较特殊的段信息或者厂商明确说不能 strip需要在包定义里加VENDORLIB_STRIP no来禁止对该包做 strip。路径统一如果库放的位置不在标准搜索路径后续应用运行时容易找不到建议统一放到/usr/lib省事很多。3.4 在应用里链接 so并让 BuildRoot 感知依赖库加进去之后项目的应用程序要能链接它、运行时要能找到它这要求我们在应用包的.mk文件里加上依赖和链接参数。假设应用的包叫myapp需要在myapp.mk中做三件事。第一声明依赖确保mylib和vendorlib先编译安装MYAPP_DEPENDENCIES mylib vendorlib第二在编译命令里加入头文件路径和库链接参数define MYAPP_BUILD_CMDS $(MAKE) -C $(D) \ CC$(TARGET_CC) \ CFLAGS$(TARGET_CFLAGS) -I$(STAGING_DIR)/usr/include \ LDFLAGS$(TARGET_LDFLAGS) -L$(STAGING_DIR)/usr/lib -lmylib -lvendor endef$(STAGING_DIR)目录里放着前一步通过INSTALL_TARGET_CMDS安装进去的头文件和库用-I和-L指向它就是标准的交叉编译链接方式。第三处理运行期动态链接问题。链接成功只代表编译链接期没问题运行时要让系统能找到 so 文件。在 BuildRoot 生成的根文件系统里通常有两个手段把 so 文件直接放到/usr/lib并且不对它们做多余处理。因为系统器/usr/lib是动态链接器默认搜索路径。用ldconfig更新动态链接缓存。前提是你启用了BR2_PACKAGE_GLIBC或者BR2_PACKAGE_UCLIBC的 ldconfig 支持并且镜像里包含了ldconfig工具。如果 so 文件不在默认路径也可以在应用启动脚本里通过export LD_LIBRARY_PATH/custom/lib/path:$LD_LIBRARY_PATH临时指定搜索路径。但我实测下来的建议是能放/usr/lib就别搞特殊路径产线上少一个变量就少一类问题。3.5 另一种更快的方案post-build 脚本直接拷文件除了标准 package 方式BuildRoot 还有一个非常实用的机制叫做 post-build 脚本。它是在整个根文件系统组装完成之后、镜像打包之前执行的脚本适合做一些临时的、不太适合写成完整 package 的操作。如果项目里只有一两个第三方 so 文件需要集成不想为它们单独写包定义post-build 脚本是效率最高的方案。具体使用方法是在board/目录下放一个脚本然后通过 menuconfig 指定# board/mycompany/post-build.sh #!/bin/sh set -e VENDOR_DIR${BR2_EXTERNAL_MYLIB_PATH}/vendorlib TARGET_LIB_DIR${TARGET_DIR}/usr/lib cp -f ${VENDOR_DIR}/libvendor.so ${TARGET_LIB_DIR}/libvendor.so cp -f ${VENDOR_DIR}/libfpga_engine.so ${TARGET_LIB_DIR}/libfpga_engine.so chmod 0755 ${TARGET_LIB_DIR}/libvendor.so ${TARGET_LIB_DIR}/libfpga_engine.so然后在配置中启用BR2_ROOTFS_POST_BUILD_SCRIPTboard/mycompany/post-build.sh脚本里可以直接使用TARGET_DIR环境变量指向当前构建的根文件系统目录。我再强调一次post-build 脚本适合的是“少量拷贝”这类轻量操作如果这个库将来还要升级、还要有依赖关系、还要参与编译链接那还是走 package 方式更标准团队协作时也更透明。4. 常见问题与排查技巧实录4.1 dl 下载失败或校验失败这是 BuildRoot 新手上手最容易遇见的坑。BuildRoot 在构建时会从网上下载各个包的源码压缩包如果网络不稳定或者上游源失效就会报 download failed 之类的错误。解决方法分两种。一种是手动把源码包放进dl/目录。BuildRoot 检查缓存时会按包名和版本找对应文件只要文件名和版本号一致它就会直接使用本地文件不会再尝试下载。另一种是清理.hash校验文件的问题。BuildRoot 的包目录下通常有一个.hash文件里面记录了源码包的 SHA256 校验值。如果你手动替换了源码包比如打了补丁的压缩包校验会不通过。这时要么更新.hash文件要么在配置里跳过校验。我的建议是除非你是故意用修改过的源码包否则不要跳过校验这个机制能帮你挡住很多“明明下载了错误版本”的问题。4.2 提示包未启用或“No rule to make target”很多时候你明明把自定义包的文件写好了make的时候却报No rule to make target ...或者在其他包依赖这个库的时找不到它。这类问题九成是因为menuconfig 里的对应符号没有勾选。BuildRoot 是通过 Kconfig 系统来驱动构建的。Config.in里的符号只有被勾选对应的.mk文件才会被 include 进构建流程。如果你在.mk里写了MYAPP_DEPENDENCIES mylib但 menuconfig 里BR2_PACKAGE_MYLIB没勾选构建系统就不会生成mylib的规则报错自然就出现了。排查思路很简单先确认 menuconfig 里包是否已 enable再执行make mylib-rebuildmylib-rebuild是 BuildRoot 为每个包自动生成的调试 target作用是强制重新编译该包。类似的还有mylib-reinstall重新安装和mylib-dirclean删除编译目录重新开始。这三个 target 是我日常调试使用频率最高的命令。4.3 镜像里找不到自己添加的 so 文件集成完成并成功编译后启动系统找不到 so 文件这个问题的排查路径基本固定。第一步检查output/target/usr/lib下是否有对应文件。如果 target 下有问题出在镜像打包或启动配置如果 target 下没有问题出在安装步骤本身。第二步看文件格式和权限file output/target/usr/lib/libmylib.so ls -l output/target/usr/lib/libmylib.so动态库要有可执行权限位这一点很多人会忽略。-m 0755是我在安装命令里固定加上的就是防止漏掉权限问题。第三步如果在 target 下有文件但系统起来还是报cannot open shared object file优先用ldconfig -p查看动态库缓存列表或者在目标板上执行export LD_DEBUGlibs后再运行应用看详细的库搜索过程。这个过程能直接告诉你它找过哪些路径、为什么没找到。4.4 “重制版”必须说的坑增量编译缓存不一致这是我这轮项目里吃过最大的亏。BuildRoot 的增量编译机制做得不错但它的增量是有前提的源码目录、配置选项、依赖关系都不能发生脏变化。有一次我直接改了mylib源码里的某个接口然后只重新make mylib-rebuild编译和安装都成功了。但应用链接的时候怎么都报找不到符号折腾了半天才发现 BuildRoot 的output/build/myapp-*目录和output/staging里的旧文件残留导致链接时用了旧版本的库。从那以后我定了个规矩如果改了某个库的对外接口或者改了编译选项最安全的做法是把这个包和依赖它的包一起清理再重建不要贪图增量编译的几秒钟make mylib-dirclean myapp-dirclean make另外手动改动output/target目录里的文件是没有任何意义的。每次重新构建只要触发某个包的重装BuildRoot 会刷新 target 里的对应文件。如果想要永久修改唯一的正确路径是修改包定义或者 post-build 脚本。4.5 快速问题速查表问题现象可能原因排查/解决方法下载失败网络问题或源失效手动下载后放入dl/或更换上游源hash 校验失败源码包内容被替换更新.hash或确认源码包来源No rule to make target包未在 menuconfig 中启用勾选对应包确认依赖链完整target 中没有 so安装步骤未执行或路径错误检查.mk中的INSTALL_TARGET_CMDS运行时报找不到 so搜索路径不对或权限问题确认放在/usr/lib、加执行权限、必要时用LD_DEBUGlibs链接时符号找不到依赖包缓存不一致dirclean后重建避免脏的增量编译5. 进阶玩法与最终经验体会5.1 让生成的镜像更小更干净BuildRoot 默认配置会打包一些可能用不到的东西让最终 rootfs 偏大。对 imx6ull 这种存储空间有限的设备控制镜像体积是很有意义的。几个实用技巧在 menuconfig 里勾选BR2_STRIP_stripBuildRoot 会统一 strip 掉 target 中的调试符号。这个选项默认通常开着但如果你发现镜像特别大可以先检查是不是被关掉了。不用的内核模块不要编译。在 Kernel 配置里尽量把驱动编成模块而不是全编进去文件系统里只放需要的模块文件。考虑用 squashfs 文件系统。squashfs 是只读压缩文件系统体积比 ext4 raw 小很多适合大多数不常改动文件的嵌入式设备。借助make size-stats生成体积报告哪些包占了多少空间一目了然。这个命令在优化阶段非常有用可以快速定位“是什么把 rootfs 撑大的”。5.2 把 BuildRoot 工程纳入版本管理的正确姿势项目走到量产阶段工程的可维护性就变得极其重要。我一般用 git 管理整个 BuildRoot 工程但有几个目录必须排除dl/ # 下载缓存体积大不需要进版本库 output/ # 构建产物可以随时重新生成需要提交的是configs/ # 板级 defconfig board/ # 板级文件、post-build 脚本、rootfs overlay package/ # 自定义包的源码目录和配置每次修改配置后用make savedefconfig将当前.config中的有效配置导出为最小化的 defconfig然后提交这个 defconfig 文件。这样版本管理里保存的是“增量”而不是“全量”可读性会好很多。5.3 把构建系统当成配置管理工具踩了这么多坑之后我最大的体会是BuildRoot 的本质不是“编译工具”而是一个“系统配置管理工具”。它的价值不在于帮你敲几条编译命令而在于把“系统的某个组成成分是什么版本、从哪里来、和谁有依赖”这个信息显式化、可复现化。用这个思路去看待 BuildRoot很多设计上的选择就容易理解了为什么过程要围绕Config.in展开为什么强调.hash校验为什么要区分 staging 和 target为什么每个包都有独立的 rebuild target。这些机制本质上都是为了让“软件配置”这件事可控、可追踪。回到 imx6ull 这个平台来说它的性能虽然不强但作为一款量级适中的嵌入式主控配合 BuildRoot 这套构建体系非常适合中小团队做产品。你需要的 U-Boot、内核、根文件系统、第三方库、自研应用都可以在同一套流程中完成集成和构建最终产出一个确定性的、可烧录的镜像。最后再分享一个个人习惯每次构建完成后我都会把output/images里的镜像文件立刻备份一份并记下当时的 git commit 号和 defconfig 状态。就是这个小习惯在我后续做版本回溯对比时帮我省了无数时间。如果你也准备用 BuildRoot 做产品强烈建议也把这个习惯纳入你的日常工作流。
返回列表