十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

老内核编译实战:利用容器解决GCC、binutils与Make工具链兼容性

老内核编译实战:利用容器解决GCC、binutils与Make工具链兼容性 前阵子翻出一台老机器系统里还留着当年编译 Linux 2.6 留下的内核源码包。出于怀旧加实用的目的我想在现在的 Ubuntu 24.04 上把它重新编一遍结果一上来就撞了一鼻子灰——老内核和今天的 GCC、binutils、make 已经不是一代人了。“Compiling Old Kernels Under Today’s GNU/Linux”这个话题其实很多人迟早会遇到你不是专职内核开发者但手里有老内核源码、老硬件驱动或者只是想看看十几年前的内核到底是怎么编出来的。这篇文章就把我从环境准备到最终开机完整的踩坑过程整理出来读完你至少能知道三条可行的路线以及每条路线上最容易卡住你的那几个坑。1. 老内核编译问题的根源不是代码错了是时代变了1.1 核心矛盾新工具链 vs 老内核源码老内核本身并不是不能编译真正的问题是它当年写的时候针对的是当年那套编译器、汇编器、链接器和构建工具。GNU/Linux 生态这二十年变化非常大GCC 默认语言标准变了。老内核大多事实上依赖gnu89这个非标准模式现在的 GCC 13/14 默认按gnu17甚至更新标准编译于是一堆历史上“合法”的隐式函数声明、类型推断、旧式函数定义全部变成 warning甚至会变成 error。binutils 的链接规则变了。老内核的汇编代码和链接脚本里有些符号重定位、段属性写法只被旧版 ld 接受。新版 binutils 对 ELF 文件格式检查更严格链接阶段经常抛出一堆莫名其妙的relocation truncated、bad value报错。make 的行为变了。Linux 内核 3.x 时代的 Makefile 依赖了 make 4.0 之前的一些宽松语法。make 4.3 之后老内核的某些目录会直接报mixed implicit and normal rules让你连编译都进不去。内核源码自身的版本检查。一些老内核会在compiler-gcc.h里写死#if GCC_VERSION xxxx或者干脆找不到对应的compiler-gcc4.h、compiler-gcc5.h。编译器一换新头文件加载直接就断了。可以打个比方老内核是一本用十年前的方言写的小说如今编译器是修订过好几版的大词典你拿着新词典去逐字对照会发现大量词条写着“已废弃”“不再收录”。这不是小说写得烂而是语言环境变了。1.2 三条编译路线怎么选在今天的发行版上编译老内核我试过三种路线各自的代价和收益完全不同。路线 A直接在宿主机上用当代工具链硬编优点是命令最直接源码在哪就在哪编译装完直接mkinitramfs就能引导。但缺点也很明显你大概率要跟 GCC、binutils、make 一个个搏斗。修改 CFLAGS、打补丁、绕过版本检查这些操作需要你对内核构建系统有足够的了解。对只是想拿老内核来跑一跑的人这条路能把热情消磨光。路线 B用容器拉一个旧发行版在容器里编译这是我最推荐的做法。容器里装一个 Debian 8、Ubuntu 16.04 这类老系统它自带的 GCC 4.9、binutils 2.25、make 3.81 和 3.x 内核几乎是同一时代产物编译老内核不需要任何特殊补丁。内核源码放在宿主机通过挂载目录共享构建产物拷出来就行。这条路最大的好处是省心。老工具链编译老代码本质上回归了“正确的时间做正确的事”。代价是容器镜像里缺少一部分内核头文件和构建脚本的依赖需要手动补装但这些都是简单命令不算障碍。路线 C虚拟机里跑完整旧系统这算是最“重”的方案适合需要完整调试环境、需要反复编译启动、或者在老内核上做内核调试的场景。用 QEMU 或 VirtualBox 装一个完整旧发行版在虚拟机里做一切操作。缺点是资源占用高、文件交换麻烦但如果你的目标是从零开始理解内核开发流程这条路线反而最有“仪式感”。综合来看我的建议是先用容器方案把内核编出来遇到实在绕不开的问题再考虑改造工具链。本篇实操主要基于容器路线同时会告诉你在宿主机上直接编译时哪些错误要重点盯防。1.3 整体操作流程预览不管走哪条路流程其实都一样准备构建环境工具链、必要依赖获取老内核源码并解压生成一个合理的.config编译bzImage和内核模块安装模块到宿主机生成 initramfs 并更新引导重启验证下面每一节都会围绕这七个步骤展开把关键选择和操作理由讲透。2. 核心细节解析必须先知道的几个兼容性雷区2.1 GCC 版本带来的“隐式函数声明”和默认标准问题现代 GCC 默认标准是gnu17但老内核的很多代码是按gnu89的标准写的。最典型的表现就是编译时刷屏的警告warning: implicit declaration of function ‘foo’ [-Wimplicit-function-declaration]到了 GCC 14这个 warning 直接升级成 error编译直接停止。内核自身的scripts/Makefile.build也不干净很多老代码里确实存在“依赖旧编译器宽容行为”的写法。如果你坚持在宿主机用新 GCC 编译最常见的临时解决方案是给KCFLAGS加上-stdgnu89export KCFLAGS-stdgnu89如果还有-Werror相关的错误再追加一个-Wno-error。但注意这个方案并不能解决所有问题。老内核的include/linux/compiler-gcc.h中会有一堆针对 GCC 版本的预编译判断如果它发现 GCC 版本“太新”可能直接报#error Sorry, your compiler is too recent for this kernel.遇到这种显式版本检查简单加 CFLAGS 已经没用要么改源码要么换旧工具链。这也就是为什么我强烈建议容器方案让报错根本不会发生。2.2 binutils 链接阶段最玄学的坑编译过程里真正让人头痛的往往不是 C 代码编译而是最后的链接阶段。老内核的arch/x86/entry/entry_64.S、arch/x86/vdso等汇编文件对符号表、重定位、段属性的处理方式是针对十几年前的 ELF 规范写的。新版 binutils 对.gnu.hash、--no-undefined、-z separate-code等新特性是默认开启的老内核的链接脚本没有为这些特性做适配于是你可能看到ld: arch/x86/boot/compressed/head_64.o: in function startup_64: (.text0x0): relocation truncated to fit: R_X86_64_32 against .text或者ld: final link failed: bad value这种错误很难通过给内核源码打补丁解决因为它本质上是“新版 ld 对老汇编的宽容度下降”。如果你用的是宿主机工具链唯一比较现实的方案是装一个旧版 binutils比如 Ubuntu 上的binutils-2.30然后用make LDld-2.30指定旧链接器。但多个 binutils 版本共存本身又是另一场折腾。而在 Debian 8 容器里binutils 2.25 处理 3.10 内核完全没有问题。这段对比你应该能看出工具链的“时代匹配”比任何技术修补都重要。2.3 kconfig 和 bison/flex 的问题内核配置阶段也会遇到一个新旧冲突。运行make menuconfig的时候系统会调用scripts/kconfig里的zconf.tab.c和lex.zconf.c。这些 C 文件在源码包中已经生成好了但当你用新版的 flex、bison 重新生成它们时会因为旧语法规则而失败。尤其 bison 3.8 以后一些老yacc语法被移除会出现类似scripts/kconfig/parser.tab.c: error: conflicting types for ‘yylval’如果你遇到这类错误第一个检查项就是scripts/kconfig目录下有没有*.c文件。有的话说明你不需要重新生成直接跳过 flex/bison 就行没有或者你在改 Kconfig 后必须重新生成那就要考虑降级工具或使用容器。另外老内核的kernel/timeconst.bc生成timeconst.h时依赖bc工具新版 bc 对某些旧语法也会报错。解决方法是装一个bc如果仍报错可以直接从同版本内核源码包里拷贝一个生成好的timeconst.h或者手动指定HOSTCC和BC工具变量。2.4 make 4.3 和内核 Makefile 的恩怨make 4.3 开始官方收紧了“隐式规则 普通规则混合”的检查。老内核的某些 Makefile 中写的是$(obj)/%.o: $(src)/%.o $(call if_changed,objcopy)这种写法在新 make 下直接变成Makefile:450: *** mixed implicit and normal rules. Stop.很遗憾3.10 内核中确实存在这种问题。如果你在宿主机上编译且 make 版本太新建议先安装 make 4.2 或者旧版本。但如果用 Debian 8 容器它自带的 make 3.81 完全不会触发这个错误。这也提醒我们老内核的构建系统对工具链的“容忍阈值”是很敏感的。有时你以为是自己代码配置错了实际上只是构建工具版本太新。2.5 新版 initramfs 和老内核的兼容编译通过只是第一步启动阶段还有坑。现代发行版的 initramfs 生成工具mkinitramfs/dracut默认可能使用 zstd 压缩而老内核的CONFIG_RD_ZSTD选项在 3.10 时代压根不存在。内核解压不了 initramfs启动时会直接卡住或者报RAMDISK: incomplete write (0 ! 32768)解决办法是在生成 initramfs 时指定压缩格式为 gzipsudo update-initramfs -c -k 3.10.108 # 如果默认是 zstd在 /etc/initramfs-tools/initramfs.conf 里加一行 COMPRESSgzip另外还要注意老内核不一定支持新版发行版的根文件系统特性。比如新版 XFS 文件系统带reflink特性老内核不认识Btrfs 的某些新校验算法同样如此。如果老内核要引导一个全新的根文件系统极大概率会因为“不能识别的文件系统特性”而拒绝挂载。务必要先确认根分区格式必要时需要重新格式化或调整分区特性。3. 实操过程用 Debian 8 容器编译 Linux 3.10.1083.1 准备容器和编译环境我选了 Debian 8 作为容器镜像因为它的默认 GCC 是 4.9binutils 是 2.25make 是 3.81内核源码选择 3.10.1083.10 系列最后一个维护版本官方维护到 2017 年。这套组合几乎不需要打补丁。先启动容器docker run -it --rm -v $(pwd):/src debian:8 bash注意这里我把当前宿主机目录挂载到了容器的/src。内核源码包最好放在宿主机上这样编译产物和源码都能留在本地。进入容器后安装编译依赖apt-get update apt-get install -y build-essential libncurses5-dev flex bison bc如果后续编译时发现缺openssl头文件还要补apt-get install -y libssl-dev这个依赖主要用于内核的 module signing 和某些文件系统特性。3.10 内核不一定需要但装了总没错。3.2 获取源码与初始配置从 kernel.org 下载 3.10.108 源码cd /src wget https://cdn.kernel.org/pub/linux/kernel/v3.x/linux-3.10.108.tar.xz tar -xf linux-3.10.108.tar.xz cd linux-3.10.108然后是生成.config。我的建议是先用发行版默认配置打底再按需裁剪make defconfigdefconfig会生成一个基于当前架构x86_64的基础配置虽然未必包含你硬件所需的所有驱动但作为起跑线是干净的。如果你希望更贴近宿主机硬件的配置可以把宿主机/boot/config-$(uname -r)复制成.config然后执行cp /boot/config-$(uname -r) .config make olddefconfig但这里有个坑宿主机内核版本很新.config里会包含大量 3.10 内核不认识的选项。make oldconfig会对每个未知选项逐一提问而make olddefconfig会把它们全部设置成默认值。表面上看省事但这些默认值不一定是你要的尤其是一些新硬件的驱动开关默认关闭之后老内核可能完全找不到硬盘或网卡。所以我的习惯是先用defconfig把基础配置跑通确认能编译、能启动再回过头来加驱动。否则第一遍就把配置调得过于复杂你分不清是配置问题还是工具链问题。3.3 配置内核选项用make menuconfig打开图形化配置界面make menuconfig编译老内核的重点配置项有几个Device Drivers Generic Driver Options下开启CONFIG_DEVTMPFS和CONFIG_DEVTMPFS_MOUNT。现代 udev 依赖这个机制在启动早期挂载/dev不开的话系统起来后设备节点是空的极其痛苦。File systems下确认开启了你要挂载的根文件系统对应的驱动。比如根分区是 ext4就检查CONFIG_EXT4_FS。General setup Initramfs source file(s)留空因为我们用独立 initramfs。Processor type and features下尽量选择与你 CPU 匹配的CONFIG_MCORE2或CONFIG_GENERIC_CPU不要选CONFIG_M586这种太老的型号否则性能会很难看。这里有个小技巧配置完成后先执行一次make savedefconfig它会生成一个精简的defconfig文件。之后每次改配置可以用diff对比变化方便排查哪一项导致启动失败。3.4 编译内核配置满意后开始编译make -j$(nproc) bzImage make -j$(nproc) modulesbzImage是压缩后的内核映像位于arch/x86/boot/bzImagemodules编译所有启用为M的内核模块。如果是在宿主机上直接编译且 GCC 版本较新需要先加上export KCFLAGS-stdgnu89 -Wno-error在 Debian 8 容器里不需要这个操作GCC 4.9 本身就是老内核的年代。编译过程碰到错误时不要一股脑重跑。先记录第一个错误看看是不是某个驱动、某个目录的汇编错误。多数情况下是单个文件、单个目录的问题可以有针对性地查。3.5 安装内核模块到宿主机容器内编译出来的模块默认会安装到容器自己的/lib/modules/里这和宿主机是隔离的。正确的做法是手动指定安装路径make modules_install INSTALL_MOD_PATH/src/modules执行后模块会出现在/src/modules/lib/modules/3.10.108/下。退出容器后把这些模块拷贝到宿主机根目录sudo cp -a modules/lib/modules/3.10.108 /lib/modules/3.6 安装内核映像与生成 initramfs先把内核映像放到宿主机/bootsudo cp arch/x86/boot/bzImage /boot/vmlinuz-3.10.108 sudo cp System.map /boot/System.map-3.10.108 sudo cp .config /boot/config-3.10.108然后生成 initramfs。这里特别要指定 gzip 压缩避免老内核解不开 zstdsudo sh -c echo COMPRESSgzip /etc/initramfs-tools/initramfs.conf sudo update-initramfs -c -k 3.10.108生成的 initramfs 文件名通常是/boot/initrd.img-3.10.108。确认一下ls -lh /boot/initrd.img-3.10.108最后更新 Grubsudo update-grub重启后在 Grub 菜单里选择 3.10.108 这个内核条目。3.7 启动验证启动成功后第一件事是确认内核版本uname -a然后看 dmesg 有没有明显的错误dmesg | grep -i error | head -20 dmesg | grep -i failed | head -20重点检查网络、显卡、声卡驱动是否加载。老内核对新硬件的支持肯定是不完整的但只要你编译时开对了驱动一般基础功能都能跑起来。4. 常见问题与排查技巧实录4.1 编译阶段典型报错速查下面这个表是我实际操作中最常遇到、也最容易被搜索引擎检索到但答案分散的几类问题。报错信息根本原因快速对策error: #error Sorry, your compiler is too recent for this kernel.内核显式版本检查拦截改include/linux/compiler-gcc.h的版本判断或换旧 GCCfatal error: include/linux/compiler-gcc.h: No such file or directory老内核找不到对应 GCC 版本的compiler-gcc*.h创建一个软链如compiler-gcc.h - compiler-gcc4.herror: implicit declaration of function ...默认编译标准太新追加export KCFLAGS-stdgnu89 -Wno-errorMakefile:450: *** mixed implicit and normal rules. Stop.make 4.3 旧语法检查严格安装 make 4.2 及以下版本或换容器scripts/kconfig/parser.tab.c: error: conflicting types for ‘yylval’bison 版本过新使用仓库自带的*.c不重新生成或装旧 bisonld: final link failed: bad valuebinutils 过新老汇编/链接脚本不兼容使用旧 binutils或换旧发行版容器bc: command not found或kernel/timeconst.bc报错缺少 bc 或 bc 太新apt-get install bc或从同版本源码拷贝timeconst.hKernel: arch/x86/boot/bzImage is ready前一步卡死单个驱动文件编译错误逐项查先裁剪掉非必要驱动4.2 启动阶段黑屏、卡死的排查顺序编译成功不等于能启动。常见启动故障建议按这个顺序排查。先确认内核命令行参数是否合理。老内核的显示输出默认走 VGA 文本模式如果显卡驱动没起来屏幕可能一直停在 Grub 之后。尝试在 Grub 启动项里按e在linux行末尾加上consoletty0 consolettyS0,115200这样串口输出能帮你判断内核到底卡在哪一步。然后查dmesg里的 ACPI 报错。老内核和主板 ACPI 表之间常常有兼容性问题启动卡住时可以加acpioff或者pcinoacpi试试。这两个参数会牺牲一部分电源管理和设备枚举能力但至少能让系统先跑起来。还要复查 initramfs 的压缩格式。使用file /boot/initrd.img-3.10.108查看如果显示zstd compressed data老内核直接解不开必须换成 gzip。4.3 我的几个避坑经验第一别裸用宿主机新工具链硬编老内核。如果你不是专门想研究工具链差异这个时间成本完全不值得。容器方案五分钟就能把环境做好准备后面你会轻松很多。第二老内核的启动失败多问一句“这个 initramfs 内核认识吗”。很多看似卡死的问题根源就是压缩格式、文件系统格式不匹配。先把最简单的 gzip 和 ext4 组合跑通再逐步升级。第三尽量选该系列的最终维护版本。比如 3.10 系列不要选 3.10.0 而是 3.10.108Linux 4.4 系列选 4.4.302等等。维护分支会包含大量针对新工具链、新硬件的兼容修复虽然对老内核来说不彻底但会减少很多低级的编译错误。第四内核模块一定要安装到宿主机对应的/lib/modules/$(uname -r)目录。如果只装内核不装模块启动后文件系统相关的模块缺失根分区可能直接挂不上。很多人编译完/boot和 initramfs 都搞定了最后卡在模块没装上。最后再分享一个小技巧如果你只是想体验老内核而不是真要在物理机上跑完全可以跳过安装步骤直接用 QEMU 加载编译好的bzImage和一个最小根文件系统镜像测试。命令大概是qemu-system-x86_64 \ -kernel arch/x86/boot/bzImage \ -initrd /path/to/initramfs.img \ -append root/dev/ram0 consolettyS0 \ -nographic这种方式启动快、方便调试也不影响宿主机环境。我后来很多老内核测试都在 QEMU 里完成省去了反复重启物理机的麻烦。编译老内核最大的收获其实是逼着你把工具链的演进脉络梳理了一遍——那些报错本身就是一段活生生的系统软件史。
返回列表