2026正点原子移植教程——从0开始编译Linux内核(2):进入正题
2026正点原子移植教程——从0开始编译Linux内核(2)进入正题为什么要写这篇编译教程你可能会想Linux内核编译不是有现成的教程吗随便一搜就是一堆。但我敢打赌你第一次尝试的时候至少会遇到以下问题之一编译到一半报错提示缺少某个头文件或工具我当时就是我头文件呢编译完成了但产物是x86的板子上跑不起来孩子们记得ARCHarm少一下就完蛋想改配置结果发现.config和defconfig的关系搞不清楚编译完了一堆文件不知道哪个是真正要用的我当年踩的坑比这还多。第一次编译内核我直接用系统的gcc结果编出来个x86_64的vmlinux还奇怪怎么i.MX6ULL板子认不出来一想到芯片对着X64的Instructions发懵我就想笑。第二次好不容易用对了工具链结果少了bcmake menuconfig直接报错。第三次装齐了依赖编完了不知道怎么验证直接把vmlinux往板子上拷当然不行。所以这篇文章的目标很明确带你走一遍完整的内核编译流程理解每一步在做什么、为什么这么做、可能会遇到什么坑。到了最后你会明白这些步骤可以自动化我会给你一个完整的build脚本——但那时候你已经理解了脚本的每一行在做什么。我们的工作环境先说明一下本文的环境避免踩不必要的坑平台Ubuntu 24.04 LTS 目标板i.MX6ULL 14x14 EVK (512MB DDR) 工具链arm-none-linux-gnueabihf-gcc 内核版本NXP linux-imx (lf-6.12.3)哦对了我上机测试是6.12.49看来打了一些patch环境不完全一样也没关系。Ubuntu 20.04/22.04 都可以工具链只要是ARM硬浮点ABI的就行。内核版本主要影响配置选项编译流程基本一致。准备工作那些看似无关的包为什么必须装在我们开始编译之前先要把依赖装齐。这一步看起来简单但缺了任何一个包你都会在不同阶段遇到莫名其妙的报错。sudoaptinstall\build-essential\bc\bison\flex\libssl-dev\libgnutls28-dev\libncurses-dev\device-tree-compiler\python3我来逐项解释这些包都是干什么的。build-essential是基础构建工具包包含了gcc、make、libc-dev这些编译必备的东西。没有它你连最简单的C程序都编不过。bc是命令行计算器。你可能觉得奇怪编译内核要计算器干嘛答案在于Kconfig配置系统。内核的配置脚本会用到bc进行数值计算比如计算内存大小、时钟分频比。没有bc某些配置选项计算会报错。bison和flex是语法分析器生成工具。内核需要解析Kconfig配置文件还需要生成某些驱动代码。这两者由flex词法分析和bison语法分析处理。你可能会在编译错误信息看到missing bison或missing flex这就是缺这两个包的表现。libssl-dev和libgnutls28-dev是加密库开发文件。内核支持签名验证、加密的模块加载、安全启动等功能。这些功能需要OpenSSL或GnuTLS库。虽然不是严格必需但为了完整性建议装上。libncurses-dev是ncurses库的开发文件。ncurses是一个终端图形库make menuconfig这种文本配置界面就是用它做的。没有它你就没法用图形界面配置内核。device-tree-compiler也就是dtc是设备树编译器。内核需要把.dts设备树源文件编译成.dtb二进制文件。虽然内核源码里自带了一个dtc但系统安装一个版本更稳定而且可以用于验证编译产物。python3是Python解释器。内核的某些构建脚本和工具是用Python写的没有Python编译可能会失败。IMX-Forge项目的构建脚本scripts/build_helper/build-linux.sh会自动检查这些依赖。你运行脚本时它会告诉你哪些包缺失并给出安装命令。理解交叉编译为什么不能直接用gcc现在我们来到第一个核心概念交叉编译。很多新手在这里卡住不明白为什么不能用系统的gcc直接编译。问题很简单你的开发机是x86_64架构的而内核要跑在ARM架构的板子上。x86的CPU跑不了ARM指令反之亦然。所以我们需要一个能运行在x86上、但生成ARM代码的编译器——这就是交叉编译器。交叉编译器的命名规则是有规律的。以arm-none-linux-gnueabihf-gcc为例arm是目标架构none表示没有厂商非嵌入式工具链linux是目标操作系统gnueabihf是GNU EABI硬浮点ABI这里重点解释一下gnueabihf。ARM有两种浮点ABI软浮点gnueabi和硬浮点gnueabihf。软浮点模式下浮点运算用软件模拟函数调用时整数和浮点参数都通过通用寄存器传递。硬浮点模式下浮点运算用硬件FPU执行浮点参数通过浮点寄存器传递。i.MX6ULL有硬件FPU所以我们要用硬浮点工具链性能更好。获取交叉编译工具有几种方式。一种是直接从ARM官网下载预构建的工具链另一种是用Ubuntu的包管理器安装比如gcc-arm-linux-gnueabihf还有一种是自己用crosstool-NG编译。对于初学者推荐用前两种省时省力。安装好后你可以用这个命令验证arm-none-linux-gnueabihf-gcc--version如果输出了版本信息说明工具链在PATH里可以正常使用。第一步设置输出目录——为什么要分离源码和产物开始编译前建议先设置一个独立的输出目录。这样可以保持源码目录干净也方便清理。exportO/path/to/output/dir然后在make时使用O输出目录参数makeO/path/to/output/dirARCHarmCROSS_COMPILEarm-none-linux-gnueabihf- xxx_defconfigmakeO/path/to/output/dirARCHarmCROSS_COMPILEarm-none-linux-gnueabihf- -j$(nproc)IMX-Forge项目的构建脚本使用固定的输出目录PROJECT_ROOT/out/linux。这样所有的编译产物都在一个地方管理起来很方便。第二步defconfig——配置的魔法清理完成后我们需要配置内核makeARCHarmCROSS_COMPILEarm-none-linux-gnueabihf-Oout/linux imx_aes_defconfig这里解释一下这三个变量的作用。ARCHarm告诉内核目标架构是ARM它会在arch/arm/目录下找架构相关代码。CROSS_COMPILEarm-none-linux-gnueabihf-指定交叉编译器前缀。Oout/linux指定输出目录。imx_aes_defconfig是NXP为i.MX6ULL准备的默认配置。defconfig文件位于arch/arm/configs/目录下arch/arm/configs/ ├── imx_aes_defconfig ├── imx_v6_v7_defconfig ├── imx_v7_defconfig └──...defconfig不是.config的完整复制它只存储与默认值不同的配置选项。举个例子如果某个配置项默认是n板子需要它设为ydefconfig里就只会记录CONFIG_XXXy。当你运行make xxx_defconfig时内核会做这几件事加载指定的defconfig处理Kconfig文件评估所有配置符号、依赖和默认值生成完整的.config文件所以.config是defconfig Kconfig系统共同作用的结果不是简单的复制粘贴。配置完成后.config文件会出现在输出目录out/linux/.config。这个文件是编译时实际使用的配置包含了完整的配置信息默认值 板级特定设置。第三步make——并行编译的威力配置完成后终于可以编译了makeARCHarmCROSS_COMPILEarm-none-linux-gnueabihf-Oout/linux -j$(nproc)-j$(nproc)这个参数很重要。nproc命令会输出CPU核心数-j告诉make可以并行运行这么多任务。现代CPU都是多核的不利用并行编译就太浪费了。我电脑是8核make -j8基本上几分钟就编完了。编译过程做了这些事情编译C源文件生成.o目标文件链接生成vmlinux ELF文件解析vmlinux生成System.map符号表用objcopy转换格式生成Image纯二进制压缩Image生成zImage打包zImage设备树生成最终镜像编译过程可能需要几分钟到十几分钟取决于你的CPU性能和配置。编译产物说明一堆文件都是干什么用的编译完成后你会在输出目录看到这些文件out/linux/ ├── vmlinux # ELF格式的内核镜像 ├── System.map # 符号地址表 ├── .config # 内核配置 ├── arch/arm/boot/ │ ├── Image # 未压缩的内核镜像 │ └── zImage # 压缩的内核镜像 └── modules/ # 内核模块如果编译了模块vmlinuxELF格式的完整内核vmlinux是ELF格式的可执行文件带调试信息通常有几十MB。这个文件包含了完整的内核代码和数据但太大且是ELF格式不能直接烧录到板子上。它主要用于调试。vmlinux的名字有点意思vm virtual memory虚拟内存linux Linux内核。早期的Linux内核需要虚拟内存支持所以叫vmlinux这个名字一直沿用到现在。Image纯二进制格式arch/arm/boot/Image是vmlinux去掉ELF头和调试信息后的纯二进制格式大约5-10MB。这个可以直接加载到内存运行但因为没有压缩占用空间较大。zImage自解压的压缩镜像arch/arm/boot/zImage是Image经过gzip压缩后加上自解压代码的镜像大约2-5MB。这是最常用的格式——体积小加载到内存后会自动解压。zImage的名字z gzip压缩。类似的还有bzImagebig zImage用于x86的大内核。对于嵌入式系统zImage通常是最终烧录的文件。System.map符号地址表System.map是内核符号及其地址的映射表。它的格式是c0008000 T _text c0008000 A stext c0008000 t _head ...每一行表示一个符号的地址、类型、名称。当内核出现Oops崩溃时会打印出错的地址你可以用System.map找到对应的函数名帮助定位问题。.config配置文件.config是编译时使用的完整配置。它非常重要因为不同的配置会产生不同的内核。建议把.config保存好下次编译时直接用这样可以保证配置一致。.dtb设备树Blob如果你编译了设备树还会看到.dtb文件。设备树编译后的二进制格式包含了硬件描述。U-Boot加载内核时会把dtb地址传给内核内核根据dtb初始化硬件。产物验证如何确认编译没白忙活编译完成了但我们还不能高兴得太早。你需要验证产物是否正确不然烧到板子上发现起不来排查起来更麻烦。架构检查用readelf看清真相首先检查架构是否正确arm-none-linux-gnueabihf-readelf-hout/linux/vmlinux|grepMachine你应该看到类似这样的输出Machine: ARM如果不是ARM说明你用错了工具链白忙活了。我见过有人用aarch64工具链编译armv7代码产物架构不对板子上当然跑不起来。除了架构还可以看入口地址arm-none-linux-gnueabihf-readelf-hout/linux/vmlinux|grepEntry point输出类似Entry point address: 0xc0008000这个地址是内核在虚拟内存中的入口点。对于ARM0xc0008000是经典的内核加载地址物理地址0x80000000的虚拟映射。大小检查合理范围的验证检查zImage的大小ls-lhout/linux/arch/arm/boot/zImage输出类似-rwxr-xr-x 1 user user 3.2M Mar 15 12:34 out/linux/arch/arm/boot/zImagei.MX6ULL的内核zImage一般在2-5MB之间。如果小于1MB可能编译不完整如果大于10MB可能配置了太多调试选项或不必要的驱动。符号检查System.map是否正确检查System.map是否包含预期的符号head-20out/linux/System.map你应该看到类似这样的输出c0008000 T _text c0008000 A stext c0008000 t _head c0008000 t _start ...如果System.map是空的或只有几行说明编译出了问题。设备树验证dtc反编译如果你编译了设备树可以验证一下dtc-Idtb-Odts arch/arm/boot/dts/imx6ull-14x14-evk.dtb|grepfsl,imx6ull你应该能看到类似这样的输出compatible fsl,imx6ull;如果看不到imx6ull的字样说明设备树可能选错了。常见编译错误及解决编译内核时常见错误有这几类。我整理了一下方便你快速排查。错误1缺少依赖包scripts/kconfig/conf--syncconfig.config /bin/sh:1: bc: not found make: ***[Makefile:xxx: syncconfig]Error127这是缺少bc包。安装方法sudoaptinstallbc类似的错误还可能出现在bison、flex、openssl等包上。错误2架构错误如果你看到类似的警告WARNING: vmlinux.o (.text0x...): unexpected relocation可能是ARCH设错了或者工具链不匹配。检查一下echo$ARCHarm-none-linux-gnueabihf-gcc--version确保ARCHarm工具链是ARM的。错误3配置冲突error: attempt to assign twice to CONFIG_XXX这通常是.config里有冲突的配置。解决方法makeARCHarmCROSS_COMPILEarm-none-linux-gnueabihf-Oout/linux distcleanmakeARCHarmCROSS_COMPILEarm-none-linux-gnueabihf-Oout/linux xxx_defconfig先清理再重新配置。错误4空间不足No space left on device内核编译需要不少临时文件空间确保你的磁盘有足够空间至少2GB。可以用df -h检查。总结成脚本方便起见我们把它自动化到这里你应该已经掌握了内核编译的完整流程。但每次都要敲这么多命令确实有点累。而且容易出错比如忘了distclean导致配置不生效或者ARCH和CROSS_COMPILE写错了。所以我们把这些步骤总结成一个脚本。IMX-Forge项目的scripts/build_helper/build-linux.sh就是这么一个脚本它做了几件事检查主机依赖build-essential、bc、bison等检查交叉编译工具链检查defconfig文件是否存在执行distclean/configure/build三阶段编译验证编译产物使用方法很简单cd/path/to/imx-forge ./scripts/build_helper/build-linux.sh脚本会自动处理所有细节你只需要坐等编译完成。快速编译技巧节省时间的实用方法当你频繁修改和编译时全量编译太浪费时间。这里有几个加速技巧。只编译修改的部分如果你只修改了某个驱动可以只编译这个驱动makeARCHarmCROSS_COMPILEarm-none-linux-gnueabihf-Oout/linux drivers/gpio/gpio-mxc.o跳过模块编译如果你不需要内核模块可以禁用它makeARCHarmCROSS_COMPILEarm-none-linux-gnueabihf-Oout/linux -j$(nproc)CONFIG_MODULESn使用ccacheccache是编译器缓存第二次编译相同代码时直接用缓存大幅提速sudoaptinstallccacheexportCROSS_COMPILEccache arm-none-linux-gnueabihf-IMX-Forge的构建脚本支持--fast-build参数跳过distclean节省时间。写在最后到这里Linux内核编译的完整流程你就掌握了。从手动敲命令到理解每个步骤的含义从排查错误到自动化脚本我们走完了整个旅程。编译不是黑魔法每一步都有它的原因。distclean是为了避免缓存毒药defconfig是通过Kconfig生成配置make -j$(nproc)是利用多核加速产物验证是确保没白忙活。当你理解了这些你就不是在机械地复制命令而是在掌控整个构建过程。但编译只是第一步。下一篇文章我们将深入内核配置的世界。你会看到defconfig和.config到底有什么区别menuconfig怎么用哪些配置项是必须了解的如何创建自己的defconfig准备好了吗我们来配置内核。延伸阅读Linux Kernel Build Documentation - 内核构建系统文档Cross-Compilation with gcc - 交叉编译指南