先聊点实际的。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 本机编译和交叉编译的对照关系
先给一张对照表,理清两者差异:
| 维度 | 本机编译 | 交叉编译 |
|---|---|---|
| 编译器前缀 | gcc | arm-linux-gnueabihf-gcc(举例) |
| 产物架构 | x86_64 | ARM(armv7、arm64等) |
| 运行环境 | 可在本机直接执行 | 必须在目标板上执行 |
| 配置参数 | make defconfig | make ARCH=arm CROSS_COMPILE=arm-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这类格式。生成.config:
make ARCH=arm64 rockchip_linux_defconfig方法二:从头开始自定义配置。对内核的模块和组件非常熟悉,或者有强制裁剪需求的场景下,可以在生成的默认配置基础之上运行菜单式配置界面:
make ARCH=arm64 CROSS_COMPILE=aarch64-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 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) Image dtbs拆解一下:
ARCH=arm64:告诉构建系统目标架构是ARM64。CROSS_COMPILE=aarch64-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 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules -j$(nproc)内核模块编译后的.ko文件散落整个源码树中,后续需要安装到根文件系统时,再执行:
make ARCH=arm64 CROSS_COMPILE=aarch64-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 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules -j$(nproc)然后安装到目标根文件系统:
make ARCH=arm64 CROSS_COMPILE=aarch64-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 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs如果只想编译某一个设备的编译树,直接指定文件路径,例如:
make ARCH=arm64 CROSS_COMPILE=aarch64-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 if=rootfs.img of=/dev/mmcblk0 bs=4M status=progress conv=fsync这种方式的好处是免去手动分区,不足之处是内核和设备树需要单独处理。
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/version:
cat /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 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- olddefconfigolddefconfig的作用是用当前内核源码的默认值自动补齐配置中缺失项,非常实用。
7.3 不要把编译产物同不同架构混在一起
如果你在同一个源码目录里切换不同架构的编译目标,务必先执行:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- distclean否则残留的.o文件和.config会导致极其诡异的编译错误。不同架构混编是新手最容易踩的隐藏雷区,我见过有人在同一个目录既编过x86桌面内核又编过ARM内核,结果二进制里混入了 x86 的汇编指令,跑在ARM板子上直接段错误。
7.4 用脚本固化这套流程
如果你需要反复多次编译,就别每次都手敲长命令,建一个简单的构建脚本能省心很多:
#!/bin/bash export ARCH=arm64 export CROSS_COMPILE=aarch64-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 ARCH=arm64 CROSS_COMPILE="ccache aarch64-linux-gnu-" -j$(nproc) Image实测下来,二次编译速度能提升数倍,尤其适合调试内核驱动的场景。
7.6 善用make的单独目标
调试一个模块时,不需要每次都编整个内核。比如你只改了某个网络驱动,可以直接指定该模块的编译目标:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- drivers/net/ethernet/xxx/xxx.ko这样只编译修改过的模块,速度飞快,符合“越改越短”的调试节奏。最后再统一做一次modules_install部署即可。
结语:把交叉编译内核当成一次射击训练
整个过程做到这里,你已经完成了从工具链准备、内核配置、交叉编译、产物部署到目标板验证的完整闭环。往回看,这其实就是一套标准化的流程,没有太高深的魔法,难点在于每一步之间环环相扣,任何一个环节出错,后面都会连锁反应。
我自己在实际操作中的体会是:多花时间在前期准备上永远不亏,选对BSP仓库、保存好基准配置、固定工具链版本,后续的工作量会指数级下降。如果你是从零开始的初学者,第一次跑通整条链路之后,建议再把每一步背后的原理重新梳理一遍,这个项目对你的价值会远超“能编出内核”本身。