Linux内核编译实战:从menuconfig配置到zImage生成全解析
Linux内核编译很多人一听到就头大觉得这是内核维护者或者驱动开发工程师才需要碰的东西。实际上我做了这么多年嵌入式Linux开发和系统移植内核编译几乎是我接触最频繁的一项操作。不管是给开发板适配BSP还是给服务器裁剪无用驱动甚至只是为了开启某一个硬件模块的内核支持整个过程绕不开的就是从menuconfig配置到最终生成zImage这一条主线。这篇文章我想把整套流程完整梳理一遍。你不需要有很深的内核功底只要会基本的Linux命令能看懂终端输出就能跟着走下来。我会把每一步为什么要这么做、背后是什么原理、哪些地方容易踩坑讲清楚。这篇内容既适合刚接触嵌入式开发的新手也适合那些以前只是照抄文档、没搞明白编译流程的同行。咱们直接从最核心的问题开始。1. 编译内核前先搞懂zImage是什么1.1 内核镜像家族的演变路径在真正执行编译之前我建议你先弄清楚一个基本问题我们费那么大功夫编译出来的zImage到底是什么东西其实内核编译产生的是一系列不同格式的镜像文件它们之间存在明确的递进关系。编译过程首先生成的是未压缩的内核可执行文件叫vmlinux一般也有叫vmlinux.out的它就是我们常说的ELF格式的内核镜像里面有完整的符号表信息主要用于内核调试。但实际部署的时候不会直接用vmlinux因为体积太大动辄几十MB甚至上百MB。接下来内核构建系统会调用objcopy工具把vmlinux中的调试信息和注释去掉生成一个纯粹的二进制内核镜像这个文件通常叫Image。Image已经可以引导了但依然比较大所以在ARM等架构上进一步用gzip或lz4等压缩算法把Image压缩一下得到的才是zImage。所以在ARM平台的内核源码目录下编译完成后你会看到arch/arm/boot/这个路径下有几个关键产物Image、zImage还有可能生成dtb设备树文件。zImage本质上是自带解压代码的压缩内核它在启动时会将自己解压到内存的特定位置然后跳转执行真正的内核代码。这也是为什么zImage需要额外的解压头这也是它相比Image最核心的区别。x86架构的情况略有不同因为BIOS/UEFI的引导流程和ARM的BootROM不一样但如果你在x86平台编译内核同样会看到类似的东西只不过x86的默认压缩镜像叫bzImage本质和zImage是同一个思路。你只需要知道当我们说生成zImage是指生成一个可以引导、自带自解压逻辑的压缩内核镜像。1.2 为什么menuconfig决定一切的开始我在带新人的时候经常说一句话内核编译的成功与否七成取决于配置阶段三成才取决于编译工具链。很多人不信觉得编译不顺利应该是编译器的问题。实际上内核源码本身是经过海量测试的你只要用对工具链和配置它基本不会出大问题。真正的学问在于你怎么配置这个内核。menuconfig是内核提供的一套基于ncurses库的图形化配置界面。它读取内核源码目录下的Kconfig文件把这些分散在各子目录里的配置项组织成树状菜单让你可以交互式地选择要编译什么、不编译什么。每个配置项背后对应的就是一个CONFIG_XXX变量这些变量最终会被写入到源码根目录下的.config文件里。你可以把menuconfig理解成一套菜品选择系统。内核源码就像一个大厨房各种驱动、文件系统、网络协议都已经写好了但是不知道该做哪些菜。menuconfig就是菜单你勾选什么厨房就给你做什么。如果你不小心选了一个没有被依赖的配置项或者漏选了一个关键配置项后面就算编译成功系统启动时也会莫名其妙地出问题。我记得以前调试一块ARM开发板eMMC设备怎么都检测不到后来发现是配置内核时漏掉了对应的MMC驱动支持重新选上后问题立刻消失。还有一个小知识点menuconfig操作界面的快捷键很多网上教程讲得不多。我习惯用/键搜索配置项这个功能在配置成千上万个选项时非常实用。空格键可以在三种状态之间切换编译进内核星号、编译成模块M、不编译空。虽然看起来只是简单按几下空格但每个选择背后都得有清晰的考量。2. 环境准备工具链、依赖和源码2.1 从零开始安装编译环境在任何发行版上编译内核第一步都是装好编译工具和依赖库。这部分很多人不重视等到报错了一堆找不到头文件的问题才开始一个个装其实完全没必要。我比较推荐一次性把常用依赖装齐省得来回折腾。如果你用的是Debian或Ubuntu系执行以下命令就可以把基础环境拉起来。sudo apt update sudo apt install -y build-essential flex bison libncurses-dev libssl-dev libelf-dev bc kmod cpio这里每个包都有明确的用途。build-essential提供了gcc、gmake这些最基础的编译工具flex和bison是两个生成词法和语法分析器的工具内核编译时解析设备树和配置文件的某些步骤会用到它们libncurses-dev是menuconfig图形界面运行所需的ncurses库libssl-dev用来支持内核打开CONFIG_SYSTEM_TRUSTED_KEYRING这类安全功能时需要的证书生成libelf-dev是处理ELF文件的库内核里很多生成脚本依赖它bc是一个命令行计算器部分内核版本的编译脚本在计算地址时要用到它。如果用的Fedora或CentOS系区别主要是包管理器不同对应关系大概是sudo dnf install -y gcc make flex bison ncurses-devel openssl-devel elfutils-libelf-devel bc如果编译时提示缺少其他工具比如bison版本太旧也不用慌报错信息里一般都会说清楚缺什么照着安装即可。实在找不到对应的包名就用apt search或dnf search去搜一把。2.2 源码获取与版本选择内核源码的获取方式有几种。最推荐的是从kernel.org官网下载稳定版tar.xz压缩包也可以从内核git仓库克隆。具体用哪个取决于你的用途。如果你只是在自己的PC上编译一个内核来练手用稳定版源码即可如果你在做嵌入式产品的开发板移植建议直接用芯片厂商提供的BSP内核源码因为厂商会在官方内核基础上打上大量的硬件适配补丁直接用官方内核单独编译往往缺少板级支持。我个人的习惯是创建一个专门的内核工作目录比如~/kernel然后下载源码到该目录源码解压后进入目录内操作。mkdir -p ~/kernel cd ~/kernel wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz tar -xf linux-6.6.tar.xz cd linux-6.6解压之后先别急着编译第一个动作是确认当前目录里的Makefile顶部记录的版本信息看看是不是你预期的版本。还有一个很多新手会忽略的操作查看源码目录下的README文件。内核的README非常简洁但会告诉你编译的正确姿势包括不能用root权限直接编译、需要在源码目录下执行命令等基础要求。2.3 关于工具链和交叉编译的一个建议如果你是在PC上编译PC平台的内核那直接用系统自带的gcc就行不需要额外配置。但如果你是在x86的PC上给ARM开发板编译内核就必须用交叉编译工具链并且在编译命令中显式指定架构和工具链前缀。这里我多说一句很多人在初学阶段容易混淆交叉编译不是在目标板上编译而是在性能更强的主机上编译出目标平台能运行的代码。给ARM板子编译内核时如果你直接执行make系统会尝试用x86的gcc去编译最后得到的image在ARM板上根本无法运行或者编译过程中直接报错因为头文件和目标格式根本不匹配。最简单的交叉编译工具链安装方式如下。sudo apt install -y gcc-arm-linux-gnueabihf然后编译时指定make ARCHarm CROSS_COMPILEarm-linux-gnueabihf-ARCH指定目标架构CROSS_COMPILE指定交叉编译工具链的前缀。必须要记得CROSS_COMPILE后面的短横线不能丢因为它只是前缀实际执行的命令是arm-linux-gnueabihf-gcc、arm-linux-gnueabihf-ld等工具名的共同前缀。3. menuconfig配置实战3.1 进入menuconfig的正确方式配置工作要在源码根目录下进行。最常用的三个命令分别是make menuconfig如果当前.config文件不存在menuconfig会基于内核自带的默认配置defconfig生成一个初始配置。这里有个差异你要明白不同平台的defconfig内容完全不一样arm平台对应的是arch/arm/configs/下某个文件而x86用的是x86_64_defconfig。所以如果你在给开发板编译内核正确的做法是先加载板卡厂商提供的defconfig再运行menuconfig做细调而不是直接运行menuconfig。用ARCH和CROSS_COMPILE参数生成默认配置的方式是make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- your_board_defconfig这样会直接生成一份该开发板的基础.config文件后续运行make menuconfig就是在这份配置基础上修改。3.2 menuconfig界面操作逐步拆解menuconfig界面虽然是个文本界面但本质和Windows下的驱动安装向导没有太大区别。进入界面后你会看到顶层的菜单项包括General setup、Enable loadable module support、Device Drivers、File systems、Kernel hacking等。移动光标用上下方向键进入子菜单按回车返回上一级按ESC两次。每个配置项左侧有个方括号或尖括号表示状态方括号对应的配置项只能编译进内核或关闭尖括号对应的配置项则多了一个编译成模块的状态。选中的标记是星号编译成模块的标记是M未选中的是空。按空格键按顺序切换状态是一种方式更高效的是按Y键直接编译进内核按M键编译成模块按N键不编译。这两个快捷键我在实际操作中用得非常频繁因为配置量大一个个按空格效率太低。还有一个超实用的技巧是使用/键搜索。我曾经帮人排查一块板卡的内核需要确认某个USB网卡的驱动配置项是否打开几百个配置项逐条翻不现实直接按/输入关键字系统会列出所有匹配的配置项及其位置路径按下数字键就能跳转到对应位置。这一步如果你能熟练使用配置效率可以提升一倍以上。3.3 关键配置项的经验之谈在做嵌入式系统移植时有几个配置项我几乎每次都要检查。第一个是CONFIG_MODULES也就是可加载模块支持。打开这个选项后内核才能按需加载.ko驱动模块。如果这个关闭了你就只能把所有驱动都编译进内核灵活性大打折扣。开发初期建议打开。第二个是CONFIG_DEVTMPFS和CONFIG_DEVTMPFS_MOUNT这两个决定系统启动时设备节点如何自动生成。如果没有设备管理器udev或mdev同时DEVTMPS又没有开启动后/dev目录可能是空的系统会卡在等待根文件系统那里。第三个是文件系统相关比如CONFIG_EXT4_FS这是x86日常使用的基础嵌入式设备上则需要根据根文件系统的实际格式选择比如CONFIG_SQUASHFS或CONFIG_UBIFS。我遇到过一个很典型的坑根文件系统是ext4但内核里没有把ext4编译进去启动时内核报VFS: Unable to mount root fs看起来很奇怪其实原因非常简单。3.4 保存配置与生成.config配置完成后选择Exit退出menuconfig会询问你是否保存新配置选择Yes后配置会写入源码根目录的.config文件。这个文件本质上是一个纯文本记录了所有配置项的最终状态。你完全可以手动编辑它但不建议这么做因为Kconfig之间的依赖关系非常复杂手动改动容易导致依赖不一致。一个比较好的习惯是保存配置后先复制一份备份比如cp .config config.bak。我开发板调优时经常会在不同配置之间反复切换没有备份就得靠git或者重新手动选择效率极低。4. 编译生成zImage的完整步骤4.1 编译前的最后检查配置完成后不要急着直接执行make先花一分钟检查两件事。第一件事是确认源码目录是干净的。如果你之前编译过最好先执行make mrproper这个命令会清除之前所有的编译产物和配置文件比make clean更彻底。如果不清理残留的.o文件可能会影响新配置下的编译结果特别是当你改了架构之后。第二件事是确认磁盘空间。编译内核的中间文件非常多完整编译一个x86内核可能需要占用10GB以上的空间ARM平台小一些但也别少于5GB。老前辈说编译内核至少要有20倍源码大小的临时空间这些年工具链变换了但是多给一点空间永远没坏处。在确认没问题后正式编译分两步make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j$(nproc)这条命令执行后内核会先读取.config检查依赖关系然后开始逐个子目录编译。对于ARM平台默认的编译目标就是zImage所以你不需要额外指定目标名。编译过程输出的信息非常庞杂很多是从编译子目录里穿插进来的。第一次编译时通常有几百甚至上千行的输出看到这些不要慌只要最终没有出现Error就说明编译顺利。编译结束后检查输出文件位置ls -lh arch/arm/boot/正常情况下你会看到vmlinux和zImage并列其中zImage就是我们要的最终内核压缩镜像。4.2 make -j参数和编译进度的理解-j参数指定并行编译的进程数一般推荐等于CPU核心数的两倍以内。很多人图快直接写-j128如果机器实际只有4个核心反而会出现调度开销过大、编译更慢的现象严重的还会因为内存不足导致OOM。我自己的习惯是先用nproc查看核心数然后-j后面填核心数乘以1.5到2之间的整数。比如8核机器就用-j1632核服务器就用-j48。编译进度方面内核使用Kbuild系统它不是按文件逐个显示的而是铺开编译各个子目录。所以你在终端里看到的是不断滚动的子目标日志没有一个固定进度条可以看。想确认当前进度可以看看arch/arm/boot/下是否有zImage生成如果还没有就让它继续跑。4.3 从vmlinux到zImage经历了什么编译完成后从源码树的结构能清晰地看出整个链接和压缩的过程。首先内核源码先编译所有子目录里的.o目标文件通过链接脚本把这些目标文件链接成一个大的ELF文件vmlinux这个文件带完整符号信息和段结构是调试内核的必备文件。vmlinux生成之后Kbuild会继续处理生成arch/arm/boot/Image这一步调用的是objcopy工具作用是去掉vmlinux中那些运行不需要的内容比如注释、符号表、重定位信息等最终产出一个连续的二进制镜像。Image文件可以理解为裸内核实际上也可以被引导但没有经过压缩体积大。最后一步才是生成zImage这一步会使用压缩工具压缩Image并在头部附加一小段自解压代码。内核启动时这段代码先把压缩的内核拷贝到合适的内存位置然后执行解压解压完成后跳转到真正的内核入口。ARM平台还有一个非常规的压缩目标叫uImage是在zImage的基础上再加一个64字节的U-Boot头这是老版本U-Boot用的格式。现在U-Boot基本都支持直接引导zImage了所以新板卡上uImage已经少见但很多老项目还在用。需要特别注意的是如果你改了配置后重新编译一定要留意make是否能正确地做增量编译。大多数情况下Kbuild会追踪依赖自动重编受影响的文件。但如果你改了.config里的某个驱动类型从内建改为模块并发现zImage没变、却多出了一些.ko文件这其实是因为驱动变成模块后不再编进内核镜像而是生成独立的.ko文件这是正确的行为不是错误。4.4 设备树编译一块容易被忽略的内容现代ARM Linux内核里设备树是比内核镜像本身更值得关注的东西。设备树文件设备树文件以dts源码形式描述板卡上的硬件资源比如内存地址、时钟频率、外设寄存器地址等编译后生成dtb二进制文件。arm64架构上内核已经不强制要求设备树但几乎所有的实际产品都离不开它。在menuconfig里设备树相关配置项会出现在Boot options的某个角落里但大多数情况下我们不需要通过menuconfig去管理它。你只需要在编译内核时确保生成对应的dtb文件位置在arch/arm/boot/dts/目录下。比如make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- dtbs单独编译设备树的命令就是这样。如果你的开发板设备树文件是myboard.dts那编译完成后会生成myboard.dtb确认它存在即可。很多新人在U-Boot启动时遇到设备树加载失败的问题就是因为内核编译好了但dtb漏编译了。5. 常见问题与排查技巧实录5.1 编译报错速查表我在实战中见过太多次编译报错最典型的是下面这几类整理成表格方便大家对照排查。报错特征根本原因解决方案/bin/sh: 1: bison: not found缺少语法分析器安装bisonDebian系使用apt install bisonscripts/extract-cert.c: 致命错误: openssl/bio.h缺少OpenSSL开发头文件安装libssl-devyylloc undeclared系统自带bison版本太旧升级bison或重新安装flexLD vmlinux 报 undefined reference配置选项之间的依赖不满足重新检查config打开对应依赖项无法找到交叉编译器CROSS_COMPILE指定错误或工具链未安装检查工具链是否在PATH里命令前缀是否正确recipe for target xxx failed最常见的一类原因多样往上翻日志找到第一个error行才是真正原因磁盘空间不足中间文件占用过大清理临时文件扩大磁盘或调整目录这里面最坑的是最后一行。编译输出的最后一个Error才是关键一定要往上面翻日志。有时候几百行前面就有一个error后面全是因为那个error导致的连锁失败。不看第一个error只盯最后一行很容易被带偏。5.2 编译速度慢的优化思路我最早编译内核时用的是机械硬盘加4核CPU全量编译一次要一个多小时。后来换到NVMe固态加16核同样的配置只要十分钟。硬盘速度对编译影响非常大因为编译过程会大量读写临时文件。如果你有条件建议把内核源码放在SSD上这比单纯升级CPU更有效。还有一个容易被忽略的因素是CCache。ccache会把编译过的目标文件缓存起来下次编译如果不影响就直接从缓存里拿结果。尤其是在反复调整配置、增量编译的场景下ccache能把时间缩短一半以上。安装方式非常简单sudo apt install ccache export CCACHE_DIR~/.ccache然后在编译前设置工具链前缀加上ccache对ARM交叉编译来说是这样的make ARCHarm CROSS_COMPILEccache arm-linux-gnueabihf- -j$(nproc)个别时候ccache缓存会让调试信息和链接地址产生偏移但只影响调试信息可靠性不影响最终zImage的正确性。调配置文件时也要留意避免全局重编。如果你只是改了.config里的某几个驱动Kbuild通常只重编受影响的子目录但是如果你改了内核命令行、CPU family这类的顶层配置几乎整个内核都会重编。遇到这种情况与其干等不如去泡杯咖啡这是正常的。5.3 编译通过但启动失败怎么办这是最让人头疼的场景编译全程零报错zImage也生成了可上板启动就黑屏或者卡住。这类问题十个里有八个出在配置阶段而不是编译阶段排查方向应该是这几点。第一确认内核命令行是否正确。U-Boot的bootargs里指定了consolettyS0,115200这类参数如果串口控制台没对应上内核早就成功启动了但你看不到输出误以为它死了。第二确认根文件系统挂载参数。root/dev/mmcblk0p2或者rootUUIDxxx这种参数如果根文件系统所在设备写错了内核会卡在VFS篇。第三确认你编译进内核的设备驱动都齐全特别是块设备驱动、文件系统驱动、串口驱动。这些没有编进去的话后面所有的调试都无从谈起。调启动问题的时候串口终端是你唯一的眼睛。绝对不要嫌麻烦去省掉串口调试这一步很多人为了省一条串口线代码烧进去后发现起不来只能盲猜。5.4 动手做一个最小可用配置的实践建议这里分享一个我日常调试的固定套路非常推荐大家试试。我不会每次都从defconfig开始慢慢调而是准备一个自己总结的mini配置模板把必要的配置项提前做好取舍。具体做法是这样的。先执行make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- myboard_defconfig生成基础配置然后运行menuconfig手动关闭掉和当前板卡无关的大类。比如局域网没用到可以把Networking里的大部分子项关掉没有USB摄像头就不用选V4L相关驱动没有触摸屏当然也不用留input touchscreen的驱动。每关掉一个不用的驱动编译速度都能快一点、内核映像也能小一点。最终把裁剪好的配置保存成自己的defconfig文件放到arch/arm/configs/目录下下次直接make my_own_defconfig即可载入。这个过程多做几轮之后你对内核里各个子系统之间的依赖关系就有直觉了看到某个配置项能大概猜到它背后依赖哪些东西这就是内核开发经验在积累的体现。6. 从zImage到可启动系统最后一步衔接6.1 把zImage和dtb打包成boot镜像拿到编译好的zImage和dtb之后距离真正能在设备上启动还差最后一步。不同的引导方案对这两者的组织方式有不同的要求这里以最常见的U-Boot加SD卡启动方案为例讲一下打包过程。最简单的做法是把zImage和设备树dtb分别放在FAT格式的启动分区里U-Boot通过命令读取fatload mmc 0:1 0x42000000 zImage fatload mmc 0:1 0x43000000 myboard.dtb bootz 0x42000000 - 0x43000000这条bootz命令的意思是用地址0x42000000上的zImage启动没有initrd用短横线占位设备树在0x43000000。很多板卡实际上是把zImage和dtb拼接成一个映像文件再烧写的Android设备常见的就是把内核、dtb和ramdisk打包成一个boot.img。打包工具可以在源码树中找到比如mkbootimg。还有一点需要注意zImage解压时需要一段内存作为解压缓冲区如果你的内核是在DDR高地址加载的可能需要在U-Boot里给内核传递一个合理的加载地址和内存布局参数。这个属于平台相关的知识不同SoC差异较大我在这里不过度展开了。6.2 内核版本和模块树一定要对应如果内核配置了CONFIG_MODULES编译过程中除了zImage以外还会生成大量的.ko模块文件。这些模块必须和内核版本严格匹配否则insmod时会出现version magic不匹配的报错。最常见的错误信息类似version magic 6.6.0 SMP mod_unload ARMv7 should be 6.6.0 SMP mod_unload ARMv7 。解决办法非常简单模块文件是由和内核同一套配置编译出来的所以使用make modules_install INSTALL_MOD_PATH目标根文件系统路径把模块安装到根文件系统里。千万不要手动从别的内核树里复制.ko文件过来用除非你知道自己在做什么。另外模块对应的符号版本信息存放在源码目录的Module.symvers文件里如果你在外部单独编译某个内核模块需要在编译命令中指定KBUILD_EXTRA_SYMBOLS指向这个文件否则模块可能引用到未导出的符号。6.3 实测启动后如何验证内核配置生效在板卡上启动新内核后第一个验证命令看内核版本和编译时间确认你加载的确实是最新编译出来的镜像uname -a cat /proc/version第二个验证是用zcat看一下内核里的配置zcat /proc/config.gz但前提是你开启了CONFIG_IKCONFIG_PROC这也是一个经常被忽略的调试选项。我建议在开发阶段一定打开这个选项打包并烧写后可以直接从运行中的内核读出它的完整配置再做一次对照排查配置项有没有生效。第三个验证则是检查模块加载情况和内核日志lsmod dmesg | grep -i error如果驱动的初始化代码在probe阶段报了错误dmesg里都会有明确的打印这些都是排查问题的第一手资料。7. 多说几句掏心窝的经验我在这个行业干了十多年从最早的2.6内核一路编译到现在的6.x说实话Linux内核编译的门槛很大程度是被各种不求甚解的教程抬高的。真正理解了Kconfig、Kbuild、vmlinux、zImage这一条线索之后你会发现内核编译本质上就是一个普通的构建系统再加一堆配置选项并没有那么神秘。如果你现在正好卡在某个内核编译报错上我的建议是先冷静下来看第一个error再检查.config里的依赖关系。如果还是解决不了就去内核源码的Documentation目录里翻一翻然后是基于报错信息搜一搜最后再尝试向社区求助。求助时一定要把编译环境、编译命令、完整日志贴出来这样别人才有可能帮你定位而不是丢一句编译失败就消失。最后分享一个小习惯我每一次成功配置并编译通过后都会把这个配置、对应的内核版本、以及遇到的所有坑记录在一个本地笔记里。时间久了这就是一笔非常宝贵的工具知识库。很多问题以前踩坑解决过但三个月后又碰到了翻到当时的记录三分钟就解决根本没有必要重新花半天时间摸索。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →