ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

嵌入式Linux交叉编译内核全流程详解:工具链、环境配置与排错指南

嵌入式Linux交叉编译内核全流程详解:工具链、环境配置与排错指南 做嵌入式Linux开发的朋友应该都有同感交叉编译Linux内核这件事表面上看就是几条make命令的事真正上手才发现坑一个接一个。就拿最常见的ARM开发板来说不管是树莓派、orangepi这类现成板子还是自己画的带SoC的底板只要目标平台不是x86架构基本都绕不开交叉编译这四个字。这篇文章把我这几年在x86主机上为ARM平台编译内核的完整流程、关键参数、环境变量设置和踩坑记录一次性整理出来从工具链安装到内核镜像和模块产出再到烧录后启动失败的排查思路全流程捋一遍。想自己定制内核的初学者可以照着做做系统集成的人也能拿走当一份checklist。1. 交叉编译的整体思路先搞清楚你在给谁编译1.1 为什么不在开发板上直接编译内核很多人一开始会有疑问开发板跑的就是Linux理论上当然可以在板子上直接编译内核。但实际项目里几乎没人这么干。首先开发板的CPU算力、内存和存储通常都很有限一块常见ARM板子核心数少、内存1G到2G直接在上面编译一次内核要等几个小时甚至更久中途断电或者内存不足直接被OOM杀掉非常折磨人。其次开发板上通常没有完整编译工具链补装工具链又依赖网络和包管理器而很多商用量产板不会预装这些东西。第三量产环境里同一套内核源码往往要输出多个目标平台的镜像在一台性能强悍的x86服务器上统一做交叉编译才是工程上最合理的做法。从效率角度来说交叉编译的本质就是“把重活留在高性能主机上干把结果拿给资源受限的设备用”。这和用远程服务器编译再拉回产物的思路一脉相承只不过这里连二进制格式都换了不只是异地编译那么简单。1.2 build / host / target 三个平台概念别搞混交叉编译环境里经常出现三个词build是编译动作发生的平台也就是你的电脑host是最终运行编译产物比如编译器、工具的平台target是编译出来的程序或系统镜像要运行的目标平台。对编译内核来说简单理解就是主机负责编译目标板负责运行。工具链名称里的三元组非常直观拿arm-linux-gnueabihf来说arm表示目标架构linux表示运行操作系统是Linuxgnueabihf表示使用glibc工具库hf表示硬件浮点hard float。如果你用的是64位ARM平台工具链名称通常是aarch64-linux-gnu含义类似。可以打个比方build是“你在哪做饭”target是“这顿饭做给谁吃”。如果搞反了你用x86的gcc编出来的内核还是x86格式拿到ARM板子上压根没法执行。1.3 交叉编译内核的典型流程完整流程大概是准备交叉编译工具链 → 准备对应版本的内核源码 → 确定目标平台默认配置 → 执行make配置 → 交叉编译出内核镜像和模块 → 连同设备树device tree一起烧录到目标平台。这里面最容易被忽略的一环是设备树。很多新手以为编译出zImage就万事大吉结果板子起不来问题往往就出在设备树和内核版本不匹配或者设备树里描述的外设资源和实际硬件对不上。设备树的作用通俗讲就是给内核递一份“硬件清单”告诉它GPIO在哪、串口在哪、内存范围是多少。这个文件如果编译错了或者烧错了位置内核要么直接panic要么起来了也缺胳膊少腿。后面我会专门把设备树这件事讲透。2. 搭建交叉编译环境工具链是地基2.1 工具链选型的几个主流方案工具链的选择直接决定后面编译会不会报令人头大的错误。主流方案有三类。第一类是发行版自带的交叉编译工具链比如Ubuntu上的gcc-arm-linux-gnueabihf32位ARM和gcc-aarch64-linux-gnu64位ARM优点是安装方便、依赖简单。第二类是芯片厂商提供的官方工具链部分厂商为了适配自家芯片的特殊指令集或商业库会维护独立工具链比如某些SoC的SDK里就带了一整套编译工具。第三类是通过Buildroot、Yocto这类构建系统自己编译出的工具链这种工具链和内核源码的兼容性最好因为整个工具链的参数和内核构建要求是配套的但上手成本最高。我的建议是你只是给常见开发板编译普通内核直接用发行版自带工具链就够了。我自己一开始也迷信厂商工具链但后来发现内核编译只需要交叉GCC、交叉binutils、交叉glibc头文件这些标准部件发行版工具链完全满足需求反而版本更新及时、bug修得快。2.2 安装并验证工具链以Ubuntu 20.04为例安装64位ARM工具链只需要两条命令sudo apt update sudo apt install gcc-aarch64-linux-gnu32位ARM平台则对应安装sudo apt install gcc-arm-linux-gnueabihf安装完成后不要急着编译内核先跑一下版本验证确认工具链本身可用aarch64-linux-gnu-gcc --version能看到正常的版本号输出工具链就算就绪。这里有个经验工具链的GCC版本不要太新也不要太旧。太新的GCC可能默认启用一些新语法检查和上游内核源码的构建脚本冲突太旧又会缺失对新标准或新特性的支持。我在实际项目中遇到过GCC 12编译某个旧版本内核时爆出一堆warning甚至error的情况换成GCC 9的旧工具链立刻好很多。如果你的项目对稳定性要求高建议锁定一个经过验证的工具链版本并把版本号写进项目文档除非确有必要不要手欠随意升级。2.3 环境变量与统一编译入口交叉编译内核时每次make都要告诉构建系统三个信息目标架构、交叉编译工具链前缀以及其他特性开关。与其每次敲一长串不如统一设置环境变量export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu-如果目标平台是32位ARM就改成export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf-注意CROSS_COMPILE必须带末尾的短横线因为内核构建系统会在其后自动拼接gcc、ld、objcopy、dtc这些工具名称。例如设置成aarch64-linux-gnu-时实际调用的编译器就是aarch64-linux-gnu-gcc。漏掉短横线的后果是系统找不到编译器直接报错。我一般会为每个项目建一个buildenv.sh脚本把export环境变量的语句放进去#!/bin/bash export ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu-每次打开新终端时source buildenv.sh这样可以避免环境变量污染其他项目也方便记录当前项目对应的工具链架构和前缀。换项目时切换环境是这个工程习惯里最大的好处。3. 内核源码准备与配置配置错全盘错3.1 怎么选内核源码版本这一步决定后面所有工作的稳定性。最推荐的做法是去开发板厂商的GitHub或官方下载页找它们维护并验证过的内核源码分支。这类源码通常包含厂商对底层驱动和板级配置的修改比如显示、WiFi、电源管理直接用Linus原版内核反而会缺驱动。泛用场景需要自己裁剪内核则从kernel.org拉取LTS版本比较稳妥比如Linux 5.10、6.1这些长期维护版本社区反馈多、坑相对少。前阵子我用orangepi CM5做项目时就是从厂商仓库拉到的5.10内核源码配合官方手册里的默认配置文件直接编译底板启动一次通过。反过来如果你拿一个通用LTS内核去编某个冷门开发板十有八九会因为缺板级设备树或驱动而失败查起来非常费劲。3.2 找到目标板的默认配置内核源码里的arch目录下会按架构存放默认配置文件。比如ls arch/arm64/configs/ ls arch/arm/configs/里面常见的是defconfig文件比如rockchip_linux_defconfig、bcm2711_defconfig。如果你用的芯片平台在官方文档里明确写了配置文件直接以它为准。正规操作是先把厂商默认配置加载进来再在其基础上调整make rockchip_linux_defconfig这会生成一个.config文件记录当前内核编译的所有选项。千万不要手写.config因为内核Kconfig配置系统有大量依赖关系手写漏掉一个依赖编译阶段就会崩得很惨。3.3 menuconfig 图形化配置的使用心得执行make menuconfig之后界面会列出几乎所有内核功能选项按空格切换三种状态M代表编译成模块*代表编译进内核空代表不编译。经验法则是常驻功能能编进内核就编进内核不需要热插拔的设备也编进内核宁可镜像大一点也不要启动后缺模块。模块方式适合文件系统驱动、网络协议这类可能需要替换硬件环境的场景。原因很直白内核启动早期挂载根文件系统之前很多驱动必须已经在内核里如果它们只是模块内核根本来不及加载。menuconfig里还有一个隐藏技巧输入/可以全局搜索选项名比如想搜某个文件系统配置直接输入文件系统名称就能定位到具体位置比一层层翻菜单快得多。配置完成后保存退出会生成新的.config。我每次配置完都会顺手备份一份cp .config config-myboard这个备份非常重要。内核配置项动辄几千个一旦改乱又没法回退有一份清晰标注的备份在后续排查问题时能让你多一条退路。4. 交叉编译内核的完整实操从源码到镜像4.1 编译前的最后检查编译之前建议花一分钟做三件事。第一确认磁盘空间充足。内核编译的临时文件非常多我见过最少也需要10GB以上建议预留20GB。第二确认源码目录路径不含中文和特殊字符构建脚本解析路径时碰到奇怪字符容易出错。第三执行echo $ARCH和echo $CROSS_COMPILE确认两个环境变量已经生效。如果环境变量在shell会话里丢失后面编译会变成在x86上编x86的内核即使编出来镜像也没法在开发板上运行。这一步看似简单却是我见过翻车率最高的地方。很多人换了终端窗口没有重新source环境脚本直接make等编译完才发现产物格式不对白白浪费几小时。4.2 执行编译从 Image 到 zImage检查完环境变量后直接执行make -j$(nproc)$(nproc)会自动取当前CPU的逻辑核心数比如8核主机就是-j8。并发参数不要贪心交叉编译的内存占用比本机编译更高。我在4核16G内存的机器上开-j8编内核内存占用经常飙到10G以上交换空间不够就直接OOM。如果你的机器内存8G左右建议保守一点用-j4。那样想只编译内核镜像不编译全部模块可以执行make zImage make dtbs不过第一次做整包编译更推荐直接make全量编因为模块依赖关系复杂只编部分目标容易后续缺文件。等编译流程跑熟了再按需拆目标。编译结束时屏幕上会显示生成文件的路径和大小。如果你编译的是arm64内核镜像在arch/arm64/boot/Image如果是32位ARM通常生成arch/arm/boot/zImage。同时设备树编译产物在arch/arm64/boot/dts/或arch/arm/boot/dts/目录下后缀是.dtb。4.3 确认产物格式别把x86镜像当ARM用编译完成后强烈建议用file命令检查一下产物格式确认它就是目标平台的镜像file arch/arm64/boot/Image如果输出显示对应的是ARM aarch64说明编译正确。如果显示x86-64恭喜你你的环境变量肯定没设置对等于用本机GCC编译了一个本机内核。这个时候不要说“反正都是Linux先烧进去再说”板子是绝对启动不起来的必须回头检查ARCH和CROSS_COMPILE。设备树的检查也一样file arch/arm64/boot/dts/rockchip/rkxxxx-xxxx.dtb确认是设备树二进制格式再进入烧录环节。4.4 编译内核模块并安装到目标根文件系统内核镜像并不包含全部模块模块需要单独编译和安装。在目标板根文件系统挂载到开发机目录的情况下可以这样操作make modules make modules_install INSTALL_MOD_PATH/home/user/rootfsINSTALL_MOD_PATH是模块安装的根路径模块会被放到该路径下的lib/modules/内核版本号/目录中。内核版本号可以通过make kernelrelease查看。模块目录必须和内核镜像版本严格一致差一点点insmod时就会报版本不匹配。做系统集成时建议把内核版本号记在项目笔记里因为烧录系统后想再回头确认这个版本号往往要翻一堆日志才能找到。如果你用的是initramfs方式启动还需要把必要模块打进initramfs否则启动早期阶段模块可能加载不上。这一步很多人直接跳过结果启动到一半发现文件系统挂不上排查半天才发现是模块没进去。5. 常见问题与排查实录踩坑是技术的一部分5.1 工具链版本不匹配的典型报错最典型的报错长这样arch/arm64/Makefile: ... 请使用 aarch64-linux-gnu-或者编译过程中大量出现error: unknown type name u64这种问题九成是CROSS_COMPILE没设对或者工具链版本不在源码支持范围内。解决时先执行env | grep CROSS确认前缀是否带了短横线再确认工具链GCC版本是否在源码要求范围内。有些厂商源码会在Makefile里写死工具链前缀你会看到报错里带一个奇怪的路径比如arm-linux-gnueabi-这种情况直接按厂商要求安装对应的工具链包别试图强行用gnueabihf替代。即使在指令集上兼容链接时也可能因为浮点ABI不一致而失败。5.2 编译中断与磁盘、内存问题编译过程中容易遇到“No space left on device”或者日志断在一半不输出。先检查磁盘、内存和交换分区三件事。内核源码编译会在同目录下生成大量.o文件和临时文件磁盘不够时建议清理旧的临时目录或者把编译目录放到另一块大分区。内存不足时Linux会启动OOM killer可能把gcc进程杀掉日志里能看到Killed字样。这时不要盲目调大-j并发数反而应该降并发或临时扩大swap分区sudo fallocate -l 8G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile另外还有个坑是inode耗尽。df -h看着还有空余但df -i显示inode已满大工程编译同样会失败。养成在大工程编译前执行df -h和df -i的习惯能省不少事。5.3 烧录后开发板无法启动的排查顺序内核编译出来烧到板子里启动失败是另一个世界。我的排查顺序固定一套先看开发板串口日志确认启动阶段停在哪里。如果卡在内核解压后优先怀疑设备树和内核不匹配重新按前文方法检查dtb路径和内核版本。如果提示根文件系统挂载失败检查root参数和initramfs是否正确。如果看到Kernel panic - not syncing: VFS: Unable to mount root fs最常见原因是文件系统驱动没编进内核或者模块没装到根文件系统里。这种时候把ext4等必要驱动直接设成*编进内核会比调半天initramfs来的快。串口日志是排查的王道比什么工具都好使前提是搞对串口波特率和接线我见过太多人排查半天最后发现串口根本没通。5.4 常见问题速查表现象可能原因处理方式提示找不到编译器CROSS_COMPILE遗漏短横线或环境变量未生效source环境脚本后重新export并检查env编译出来的镜像是x86格式ARCH设置错误确认export ARCHarm或者arm64编译报unknown type name工具链版本过旧或过新换成源码官方指定GCC版本启动卡在跳转内核设备树缺失或不匹配确认dtb路径和内核源码版本对应无法挂载根文件系统ext4等驱动未编入内核menuconfig里将这些驱动设为*而不是M并发编译被OOM杀掉内存不足降低-j值或扩展swap结尾几个值得长期坚持的习惯最后分享几个我自己这些年养成的习惯。交叉编译这件事表面上是一堆make命令核心其实是版本对齐工具链版本、内核源码版本、设备树版本、目标板根文件系统任何一环不匹配最后都会以奇怪的方式报错或者起不来。所以每次开工前我会先建一个目录记录内核源码的commit号、工具链GCC版本、menuconfig后的配置备份相当于给项目建一份“编译台账”后面排查问题时能省很多时间。另一点是不要把交叉编译看成一次性的操作。定制内核是个反复迭代的过程改配置、重新编译、烧录、看日志、再改循环往复。我自己刚开始做的几次几乎每次都要在设备和主机之间来回折腾大半天后来把编译脚本、烧录脚本、串口工具都整理成一套固定流程效率提升非常明显。能把这条流程熟练到像呼吸一样自然嵌入式Linux开发的基本功就算真正打牢了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进