ARTICLE DETAIL

资讯详情

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

交叉编译Linux内核:从工具链到ARM板部署的完整实践

交叉编译Linux内核:从工具链到ARM板部署的完整实践 先聊点实际的。Linux内核编译这件事很多人第一反应是“在开发机上make一下就行”但真正做嵌入式开发的人都知道目标板子是ARM架构开发机却是x86这时候就必须引入交叉编译。网上关于交叉编译内核的教程不能说少但大多停留在“照着敲一遍就能过”的层面一旦遇到编译器版本不匹配、内核源码树里有顽固的host工具、或者设备树编译不过去这类问题新手往往直接卡死。这篇文章不打算重复那些烂大街的步骤而是围绕“交叉编译环境下对linux内核编译”这条主线把我在实际项目中踩过的坑、验证过的正确姿势、以及那些文档里不会明说的细节一次性讲清楚。内容会特别覆盖ARM目标平台的场景包括交叉编译工具链的选型与安装、内核配置的两种思路、make参数背后的真实含义、以及最终产物如何烧录到板子上并验证。无论你是刚接触嵌入式Linux的学生还是在公司里被分配了内核裁剪任务的工程师这篇文章都能帮你省下大量试错时间。1. 交叉编译到底在解决什么问题一段前置认知很多人第一次听到“交叉编译”这个术语时都会懵一下因为普通PC上写C语言程序编译完直接就能跑压根不需要绕弯子。要理解交叉编译先得搞清楚一个概念编译器和目标硬件之间存在“架构绑定关系”。打个比方你用中文写了一份说明书这份说明书只有懂中文的人能看懂。x86架构的CPU和ARM架构的CPU它们的机器指令集完全不同就像两种完全不同的语言。普通编译是“用中文写给中国人看”编译出来的二进制只能在同架构CPU上运行交叉编译则是“用翻译器把中文转换成英文”在x86的开发机上生成一份ARM架构CPU能懂的二进制文件再传输给ARM板子去执行。1.1 为什么嵌入式开发必须走交叉编译这条路嵌入式设备本身通常不具备编译能力原因很现实。多数ARM开发板的CPU主频有限、内存只有几百MB到1GB左右、存储空间也紧凑真要在板子上装一套完整的GCC工具链去编译Linux内核耗时可能长达几小时甚至直接内存溢出。而PC开发机拥有强劲的CPU、大内存和高速磁盘同样是编译内核开发机可能只需要十几分钟。另一个原因是工具链本身非常庞大。一套完整的交叉编译工具链包含gcc、binutils、glibc或uClibc、gdb等组件安装后动辄占用几个GB的磁盘空间对于目标板而言这几乎是不可能完成的任务。所以业界标准做法就是在x86开发机上安装交叉编译工具链编译出ARM架构的内核镜像、设备树文件、内核模块然后通过TFTP、SD卡或烧录工具部署到板子上。整条流水线的核心就是找到一套版本匹配、可用的交叉编译工具链。1.2 本机编译和交叉编译的对照关系先给一张对照表理清两者差异维度本机编译交叉编译编译器前缀gccarm-linux-gnueabihf-gcc举例产物架构x86_64ARMarmv7、arm64等运行环境可在本机直接执行必须在目标板上执行配置参数make defconfigmake ARCHarm CROSS_COMPILEarm-linux-gnueabihf-常用场景桌面软件、服务器程序内核、Bootloader、嵌入式应用这里的关键就是CROSS_COMPILE环境变量。它告诉内核的构建系统编译用的工具链前缀是什么。内核构建系统会自动调用$(CROSS_COMPILE)gcc、$(CROSS_COMPILE)ld等命令而不是默认的gcc、ld。2. 交叉编译工具链的选择别在这步走弯路工具链的选型是整个流程的基石。选错了后续所有工作都白费选对了后面一路顺畅。我自己经历过因为工具链版本过老导致内核编译报出一堆莫名奇妙的错误排查了整整两天才发现是编译器无法识别新内核源码中的某些语法特性。2.1 主流工具链有哪些针对ARM平台市面上活跃的交叉编译工具链主要有以下几类Linaro GCC工具链由Linaro组织维护专门针对ARM架构优化版本更新及时社区活跃度高非常适合用来编译Linux内核。ARM官方GNU工具链ARM公司自己发布的工具链可靠性高但部分版本面向的是裸机环境bare-metal用来编译Linux内核时需要确认是否带Linux头文件和C库支持。Buildroot或OpenEmbedded生成的自定义工具链适合对整个根文件系统有定制需求的场景但初始学习和构建成本较高。SoC厂商提供的工具链例如瑞芯微、全志等厂商在其SDK中会附带工具链通常与自家BSP版本匹配度最高但相对封闭。2.2 我实际用的工具链组合我以一块基于瑞芯微方案的ARM64开发板作为目标平台来做交叉编译实践。这里用的是aarch64-linux-gnu-前缀的编译工具链在Ubuntu 20.04系统上直接执行以下命令安装sudo apt update sudo apt install gcc-aarch64-linux-gnu安装完成后确认工具链路径和版本aarch64-linux-gnu-gcc -v输出中会包含gcc version 9.3.0之类的版本信息同时能看到Target: aarch64-linux-gnu字段这代表编译器目标架构是ARM64。提示安装时尽量选择与内核源码发布时期相近的编译器版本。内核5.x版本建议GCC 8及以上老编译器在处理新内核的__attribute__语法、内嵌汇编约束时会出现不兼容问题。2.3 32位ARM和64位ARM的区别如果你的目标平台是32位ARM比如Cortex-A7、Cortex-A9应该选择arm-linux-gnueabihf前缀hf代表硬浮点。安装方式类似sudo apt install gcc-arm-linux-gnueabihf两种工具链的产物不能混用64位平台只能使用aarch64工具链32位平台应根据硬浮点还是软浮点选择arm-linux-gnueabihf或arm-linux-gnueabi。判定目标平台架构最简单的方式是查看开发板SoC的手册或执行uname -m如果板上已有系统。3. 内核源码的准备与配置复杂但有章法有了工具链接下来就是准备Linux内核源码。这部分存在两个常见选择获取官方主线源码还是使用芯片厂商维护的BSP内核源码。这个选择直接决定后面是简单模式还是困难模式。3.1 获取源码时的判断标准如果仅仅是学习内核编译流程或者做一些通用硬件平台上的实验直接用官方主线源码即可。从 kernel.org 获取长期支持版本LTS比如5.10.x、5.15.x、6.1.x。如果你的目标是让某款具体开发板正常工作包括显示屏、Wi-Fi、蓝牙、音频编解码等外设驱动就必须使用SoC厂商提供的BSP内核源码。因为很多外设驱动和设备树补丁只存在于BSP仓库中尚未合入主线或者合入版本较晚。以我用的瑞芯微平台为例官方在GitHub上有长期维护的内核仓库其中包含完整的板级设备树、GPU驱动、VPU编解码驱动等。从官方仓库拉取代码git clone https://github.com/rockchip-linux/kernel.git cd kernel git checkout -b local-6.1 origin/linux-6.1注意BSP内核通常会维护多个分支不同分支对应不同的内核大版本建议优先选择最新且稳定的分支。3.2 内核配置的三个方法编译内核前必须告诉构建系统“目标平台是谁需要哪些功能”这个过程称为配置。配置结果保存在.config文件里所有后续编译工作都围绕它展开。方法一在已有配置基础上修改。最稳妥的方式是直接使用BSP仓库自带的配置文件通常在arch/arm64/configs/目录下文件名为rockchip_linux_defconfig这类格式。生成.configmake ARCHarm64 rockchip_linux_defconfig方法二从头开始自定义配置。对内核的模块和组件非常熟悉或者有强制裁剪需求的场景下可以在生成的默认配置基础之上运行菜单式配置界面make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- menuconfig这个方法会展开终端图形界面可以逐项搜索和开关内核功能。对于新手我建议先做一遍默认配置编译通过后再用menuconfig做减法慢慢裁剪模块。方法三直接复用其他现成配置。例如从开发板的/proc/config.gz导出现有系统的配置在板子上执行zcat /proc/config.gz config.txt然后拷贝到主机源码根目录并重命名.config。不过这个方法依赖对应内核源码版本的一致性如果版本不匹配配置过程中会出现大量缺失符号的警告。3.3 配置阶段容易忽略的依赖关系配置内核时一个需要留意的点是设备树Device Tree的关联。设备树描述了硬件平台的信息比如内存基址、外设地址、中断号等编译内核时设备树源文件.dts会被编译成二进制的 .dtb 文件。大部分时候设备树源文件的编译是自动的无需单独设置。但如果某个外设驱动没有在内核配置中开启那么设备树中对应节点就不会被实际初始化可能表现为设备完全不可见或功能异常。所以在配置时建议同步确认以下命令输出的设备树目标列表ls arch/arm64/boot/dts/rockchip/确认文件中包含你所使用板子的设备树源文件比如rk3588-orangepi-..........dts这类命名。设备树文件名和板子的对应关系通常可以从板卡厂商文档中查到。4. 内核编译的完整实操从零构建镜像配置完成后就是真正的编译环节了。这一步是很多人会翻车的地方因为内核的编译命令很长参数容易被忽略而且编译告警信息非常多新手容易把警告误判为错误导致陷入无谓的焦虑。4.1 编译命令的正确写法进入内核源码根目录依次执行make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- -j$(nproc) Image dtbs拆解一下ARCHarm64告诉构建系统目标架构是ARM64。CROSS_COMPILEaarch64-linux-gnu-指定交叉编译器前缀。-j$(nproc)启用多核并行编译$(nproc)返回CPU逻辑核心数。注意不要图省事写成-j不带数字这样可能把编译速度拖慢。Image生成内核镜像文件ARCH为arm64时默认产出非压缩的Image格式镜像。dtbs编译所有设备树二进制文件。如果想生成压缩格式的zImage可以将目标从Image替换为zImage。不过ARM64平台上一般直接用Image因为后续的Bootloader通常支持加载Image格式。4.2 编译过程中可能出现的报错与应对编译中大概率会碰到几个典型的坑这里按出现频率排序报错一头文件找不到出现openssl/xxx.h: No such file or directory这个错误几乎每个编译内核的人都会遇到。处理方式很直接安装对应依赖sudo apt install libssl-dev其实这是内核构建过程中生成签名证书的环节需要依赖OpenSSL头文件。如果不需要模块签名特性也可以在配置时将CONFIG_MODULE_SIG关闭。报错二/bin/sh: 1: flex: not found内核源码中部分解析器依赖flex和bison生成词法/语法分析代码。安装sudo apt install flex bison报错三fatal error: linux/compiler-gcc9.h: No such file or directory这类问题通常源于源码树不完整。多半是git clone时没有拉取足够完整的代码或压缩包解压不完整。重新确保源码完整性即可。个别情况是make clean清掉了生成头文件此时重新编译即可。**报错四**关于设备树编译时的光秃秃的告警Warning (graph_port): ...这类统统是警告不影响产物生成可以无视。只要没有Error就代表流程没断。4.3 编译输出产物清单编译顺利跑完后在arch/arm64/boot/目录下会生成关键产物Image内核镜像文件约20MB到50MB不等具体视配置而定。dts/或dts/rockchip/编译生成的多个.dtb设备树文件。编译内核模块如果需要make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- modules -j$(nproc)内核模块编译后的.ko文件散落整个源码树中后续需要安装到根文件系统时再执行make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- INSTALL_MOD_PATH/path/to/rootfs modules_installINSTALL_MOD_PATH指向目标板根文件系统的挂载根目录这个命令会将所有.ko文件按目录结构拷贝到指定路径下的/lib/modules/kernel-version/中。4.4 多核编译的取舍并行编译参数-j并非越大越快。如果你的是笔记本散热和电源限制可能导致8核并行编译反而不如4核稳定。实测下来虚拟机环境中6核并行编译比8核稳定文件较多时也不会因为I/O瓶颈导致编译错误。稳妥起见可以使用-j4或-j$(nproc)其中较小者。5. 模块编译与设备树容易踩坑的细节区很多人以为内核编译只要跑完Image就万事大吉其实模块和设备树才是后患最多的地方。模块承载了大部分驱动设备树决定了硬件能否被内核正确识别。这部分我会仔细拆解。5.1 模块的编译与安装流程内核模块可以理解为“可选择加载的驱动部分”它们不直接编译进内核镜像而是以.ko文件存在运行时按需加载。带来的好处是内核镜像体积小、启动快、灵活性高。编译模块和编译镜像一样需要带上架构和工具链参数make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- modules -j$(nproc)然后安装到目标根文件系统make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- INSTALL_MOD_PATH~/rootfs modules_install执行之后检查~/rootfs/lib/modules/目录应该能看到与内核版本号一致的目录比如~/rootfs/lib/modules/6.1.0-rc6/里面是各类.ko模块文件和依赖关系信息modules.dep。提示modules_install时INSTALL_MOD_PATH参数和源码目录的读写权限都要先确认好。一个常见失误是直接指定到rootfs的根目录却忘了用sudo导致模块安装后权限混乱。5.2 设备树的专项检查与编译设备树是描述硬件细节的配置文件它是一份文本化的.dts源文件经编译成.dtb二进制后由Bootloader传给内核解析。设备树写错最直接的现象是外设不工作但内核日志里往往没有任何报错排查非常费时。编译单独设备树源文件可以直接指定文件名make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- dtbs如果只想编译某一个设备的编译树直接指定文件路径例如make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- rockchip/rk3588-evb1-v10.dtb这里有一个容易被忽略的点设备树源文件中的节点状态status属性。很多外设默认在设备树中被标记为disabled需要你根据实际使用的板子手动改为okay。一些BSP厂商默认关闭了某些调试串口、PMIC、音频编解码器等必须打开才能工作。uart0 { status okay; };不熟悉设备树的同学要特别注意这个okay是设备树规范中的关键字符串改成disable、ok等拼写都不会生效且编译期不会报错。5.3 预留的编译时间评估交叉编译内核需要多少时间实测数据如下x86开发机、i5-12400处理器、16GB内存、SSD配置目标操作耗时defconfig配置生成默认配置1-2秒make Image -j6编译内核镜像5-10分钟make modules -j6编译全部模块10-20分钟make dtbs编译设备树1分钟以内这个耗时属于“可以起身倒杯水”的级别不像有些人想象中那么漫长前提是不要用全默认配置开启大量不必要的驱动。BSP的defconfig通常已经做过优化直接用它起步就好。6. 部署与验证如何确定内核真的跑起来了生成镜像和设备树只是第一步部署到目标板并看到系统正常启动整个流程才算闭环。这里介绍最通用的SD卡部署方案适合绝大多数ARM开发板。6.1 SD卡准备与分区准备一张32GB以上的高速SD卡至少Class10推荐U3在Linux虚拟机中插入SD卡读卡器使用lsblk识别设备名。这一步务必看清楚设备名一旦写错可能清空宿主机硬盘。假设SD卡设备名为mmcblk0用parted或fdisk重新分区第一个分区类型为fat16或fat32大小建议 512MB 或 1GB存放Bootloader、内核镜像、设备树等。第二个分区类型为ext4大小占满剩余空间存放根文件系统。也可以直接用一体化的镜像写入方式把厂商提供的根文件系统镜像.img通过dd写入SD卡sudo dd ifrootfs.img of/dev/mmcblk0 bs4M statusprogress convfsync这种方式的好处是免去手动分区不足之处是内核和设备树需要单独处理。6.2 拷贝内核与设备树到Boot分区将编译好的产物拷贝到SD卡的Boot分区sudo mount /dev/mmcblk0p1 /mnt/boot sudo cp arch/arm64/boot/Image /mnt/boot/ sudo cp arch/arm64/boot/dts/rockchip/rk3588-xxxx.dtb /mnt/boot/ sudo umount /mnt/boot如果你的Bootloader约定内核文件名必须是kernel.img或Resource.img部分瑞芯微平台的特性还需要额外处理。部分平台使用的工具是resource_tool可以将多个dtb打包成单一resource.img但新版本的U-Boot已经支持读入独立.dtb文件建议优先采用独立dtb方式排查问题更简单。6.3 上电启动验证手段板子插上SD卡上电后通过串口线连接开发板打开终端串口工具波特率通常是1500000或115200具体看板子设定观察启动日志。如果想知道你自己编译的内核是否真的在运行留一手验证技巧启动后登录系统查看内核版本号uname -a或者直接查看/proc/versioncat /proc/version这个文件会显示编译用的GCC版本和编译时间。如果你看到的编译时间戳与你编译内核的时间一致说明跑的就是你自己编译的内核这是最直观的确认方法。如果启动不了怎么办优先查看串口日志最后几行常见问题如下停在Starting kernel ...大概率是设备树与内核不匹配检查dtb是否拷贝正确或Bootloader启动参数中指定的dtb名与实际文件名不一致。报Kernel panic - not syncing: VFS: Unable to mount root fs说明内核找不到根文件系统。检查rootfs分区是否存在、启动参数中的 root 设备名是否正确。系统使用mmcblk0p2时启动参数可能还要加rootwait参数等待设备就绪。一直循环复位可能原因是DDR初始化失败或Bootloader与内核的DDR配置不一致这类问题基本需要从厂商SDK的对应分支重新编译Bootloader解决。6.4 模块的动态加载验证内核模块编译并部署到rootfs对应位置后在板子上执行modprobe 模块名如果没有报错再用lsmod查看模块是否已加载。在没有实际模块可加载的框架验证场景下也可以创建一个简单内核模块测试工具链是否好用比如编写一个只打印日志的hello world模块交叉编译后加载验证。这部分工作能极大检验工具链和开发环境的一致性。7. 我在多次编译实践中总结的避坑经验到了收尾环节我把自己这些年反复折腾交叉编译内核总结出来的经验一次性分享出来每一条都是真金白银换来的。7.1 先确认目标平台和工具链的匹配最省时间的一步恰恰是最多人忽略的。拿到一块开发板不要马上找内核源码开干先确认三件事SoC型号、内核版本要求、官方BSP仓库地址。你可以用cat /proc/cpuinfo或uname -a查看已有系统信息如果板子没有系统那就查厂商文档。这一步做扎实后面所有环节都不会跑偏。7.2 保存一份有效的构建配置每次编译时务必保留一份编译成功时用的.config。不要随手清理.config文件因为这相当于你这个平台的内核“基因”。下次要加一个功能时基于这份配置修改远比从零开始快得多。cp .config my-board.config后续维护时任何需要重新配置的操作都先从这份配置开始cp my-board.config .config make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- olddefconfigolddefconfig的作用是用当前内核源码的默认值自动补齐配置中缺失项非常实用。7.3 不要把编译产物同不同架构混在一起如果你在同一个源码目录里切换不同架构的编译目标务必先执行make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- distclean否则残留的.o文件和.config会导致极其诡异的编译错误。不同架构混编是新手最容易踩的隐藏雷区我见过有人在同一个目录既编过x86桌面内核又编过ARM内核结果二进制里混入了 x86 的汇编指令跑在ARM板子上直接段错误。7.4 用脚本固化这套流程如果你需要反复多次编译就别每次都手敲长命令建一个简单的构建脚本能省心很多#!/bin/bash export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- make distclean make rockchip_linux_defconfig make -j$(nproc) Image dtbs modules脚本化之后每次迭代代码只需执行一次脚本还能避免手滑漏参数。7.5 善用ccache加速重复编译如果只是修改了一点配置或驱动代码就要重新编译全程重编非常浪费时间。可以用ccache做编译缓存sudo apt install ccache export CCACHE_DIR$HOME/.ccache export KBUILD_BUILD_TIMESTAMP然后在编译命令前使用ccache包装编译器make ARCHarm64 CROSS_COMPILEccache aarch64-linux-gnu- -j$(nproc) Image实测下来二次编译速度能提升数倍尤其适合调试内核驱动的场景。7.6 善用make的单独目标调试一个模块时不需要每次都编整个内核。比如你只改了某个网络驱动可以直接指定该模块的编译目标make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- drivers/net/ethernet/xxx/xxx.ko这样只编译修改过的模块速度飞快符合“越改越短”的调试节奏。最后再统一做一次modules_install部署即可。结语把交叉编译内核当成一次射击训练整个过程做到这里你已经完成了从工具链准备、内核配置、交叉编译、产物部署到目标板验证的完整闭环。往回看这其实就是一套标准化的流程没有太高深的魔法难点在于每一步之间环环相扣任何一个环节出错后面都会连锁反应。我自己在实际操作中的体会是多花时间在前期准备上永远不亏选对BSP仓库、保存好基准配置、固定工具链版本后续的工作量会指数级下降。如果你是从零开始的初学者第一次跑通整条链路之后建议再把每一步背后的原理重新梳理一遍这个项目对你的价值会远超“能编出内核”本身。
返回列表