在古老的 CentOS 7.9-2009 里编译 Linux 7.0.9:一场与工具链和现代 spec 语法的肉搏战
最近由于业务需要,全新安装了生产环境同款的 CentOS 7.9 (2009) ,其实也就是升级一下 OpenSSH ,顺手把 OpenSSL 升级了。看着系统里那陈旧的 3.10.0 残血内核,我突发奇想:既然工具链都顺手配好了,索性把 Linux 内核也编译升级一下? 查了一下,Linux内核最新版本是 7.0.9 ,很新!(CentOS7.9(2009)和7.0.9内核看上去是不是很搭?)
试试就逝世。编译开始才发现这不是一次简单的 make,而是一场新老时代编译机制断层、Glibc 缺失、以及后端口 RPM spec 语法大代沟的高烈度肉搏战。在这篇博客中,我将全量复盘这场内核填坑记录。我们不玩那种只能单机自嗨的“快装短路包”,而是正面硬撼官方打包流,直接轰鸣出正规的官方标准 RPM 三件套,并打通多机分发、真机无缝部署的最后一步。
0x01 前置阻碍:GNU Make 的跨代升级
升级gcc(可选)
由于我在前一步编译 OpenSSH 10.3p1 时尝试使用 OpenSSL 4.0.0 编译,已经提前把本地的 GCC 喂到了 GCC 8。如果编译内核出现了报错,请务必先通过 SCL 仓库把编译器强行拉上现代主线(别忘了切入对应环境):
# 安装 CentOS 官方的 Software Collections 源 (SCL)
sudo yum install -y centos-release-scl
# 安装 devtoolset-8 核心工具链 (内含 GCC 8.3.1 / binutils)
sudo yum install -y devtoolset-8-gcc devtoolset-8-gcc-c++ devtoolset-8-binutils
# 强行切换当前 Shell 环境至 GCC 8(使其成为默认编译器)
scl enable devtoolset-8 bash
gcc --version # 确保输出为 8.x.x
编译升级 make
在把最新的 linux-7.0.9.tar.xz 解压到 /usr/src/ 目录后,我直接 cp 了老内核的配置文件:
cp /boot/config-$(uname -r) .config
然而,刚敲下 make menuconfig,内核就给了我一记当头棒喝:
Makefile:15: *** GNU Make >= 4.0 is required. Your Make version is 3.82。 停止。
最新的 Linux 7.x 内核重构了底层编译构建流,强制要求 GNU Make 必须 $\ge$ 4.0。而 CentOS 7 依然停留在 3.82。
本来想试试 yum 更新一下,避免大动干戈,可惜国内源没有新版本可用。没办法,只能就地源码编译一个最新稳定版的 GNU Make 4.4.1:
cd /usr/src
wget [https://ftp.gnu.org/gnu/make/make-4.4.1.tar.gz](https://ftp.gnu.org/gnu/make/make-4.4.1.tar.gz)
tar -xzf make-4.4.1.tar.gz && cd make-4.4.1
./configure --prefix=/usr/local
make -j$(nproc) && sudo make install
# 强行软链接覆写系统默认 Make(如缺少 gcc,请先 yum install gcc)
sudo ln -sf /usr/local/bin/make /usr/bin/make
由于拿的是 3.10 的老配置丢给 7.0.9,直接编译会触发内核 oldconfig 机制对成百上千个新驱动选项的疯狂刷屏提问(确认了很久,我实在不耐烦了,一直按着回车不放,结果也是没完没了,受不了啦!)直接按下 Ctrl + C 终止。直接使用下面的命令让系统自动应用最稳妥的默认关闭值即可:
make olddefconfig
0x02 内核源码大肉搏:深挖三大报错血案
配置理顺后,我直接启动了全核心编译,但很快,高版本内核与陈旧 CentOS 7 底层生态的激烈冲突全面爆发。
血案一:modpost 阶段 ARM 架构宏缺失
编译推到中后期,系统突然在 scripts/mod/ 停下,抛出了一堆 ARM 重定位符号未定义的报错:
scripts/mod/modpost.c: error: ‘R_ARM_MOVW_ABS_NC’ undeclared
- 原因: 虽然我们是在 x86_64 架构上编译,但新内核的
modpost工具在链接时会全量校检所有已知架构的 ELF 符号。CentOS 7 自带的/usr/include/elf.h头文件太古老,根本没有收录新版 ARM 架构的重定位宏。 - 解决方案: 直接对内核源码的
scripts/mod/modpost.h尾部进行硬编码宏注入,强行给编译器打一剂退烧针:
/* Fix CentOS 7 old elf.h compatibility */
#ifndef R_ARM_MOVW_ABS_NC
#define R_ARM_MOVW_ABS_NC 43
#endif
#ifndef R_ARM_MOVT_ABS
#define R_ARM_MOVT_ABS 44
#endif
#ifndef R_ARM_THM_MOVW_ABS_NC
#define R_ARM_THM_MOVW_ABS_NC 47
#endif
#ifndef R_ARM_THM_MOVT_ABS
#define R_ARM_THM_MOVT_ABS 48
#endif
血案二:objtool 模块的反汇编套娃与链接死结
跨过 modpost 后,工具链死死卡在了 tools/objtool/disas.c 编译上。
由于新内核的 objtool 引入了极其严格的编译参数 -Werror=missing-prototypes(要求所有全局函数必须有前置声明,且类型必须与头文件完美对齐),而 CentOS 7 本地的反汇编标准库 dis-asm.h 与新内核 API 严重断层,导致引回本地头文件后疯狂报错,不引又会触发缺失原型的红线。
- 无赖伪装法: 既然反汇编输出对内核本体运行毫无实质影响,为了彻底避开不兼容的系统头文件和严格的原型校对,我选择直接删掉所有官方头文件引用,手写一排
void *前置声明,用通用的空指针和零返回值进行全局符号伪装。直接用以下内容暴力覆盖tools/objtool/disas.c:
#include <stdio.h>
#include <stddef.h>
/* 自备前置声明,彻底堵住 -Werror=missing-prototypes 的嘴 */
int disas_info_init(void *dinfo, void *fprintf_func);
void *disas_context_create(void *file);
size_t disas_insn(void *dctx, void *insn);
void disas_print_info(FILE *stream, void *insn, int depth, void *fprintf_func);
const char *disas_alt_type_name(void *insn);
int disas_print_insn(void *dctx, void *insn);
const char *disas_alt_name(void *dctx, void *insn);
void disas_context_destroy(void *dctx);
/* 补齐 check.c 和 trace.c 链接期需要的全局符号 */
void *disas_result = NULL;
void *disas_funcs = NULL;
void *disas_warned_funcs = NULL;
int disas_info_init(void *dinfo, void *fprintf_func) { return 0; }
void *disas_context_create(void *file) { return NULL; }
size_t disas_insn(void *dctx, void *insn) { return 0; }
void disas_print_info(FILE *stream, void *insn, int depth, void *fprintf_func) {}
const char *disas_alt_type_name(void *insn) { return NULL; }
int disas_print_insn(void *dctx, void *insn) { return 0; }
const char *disas_alt_name(void *dctx, void *insn) { return NULL; }
void disas_context_destroy(void *dctx) {}
单模块清理 make -C tools/objtool clean 后重新拉起,objtool 顺利实现零报错编译链接!
血案三:系统调用跨代断层:copy_file_range
本以为扫清了工具链,结果在编译内核本体并试图链接主机辅助工具 usr/gen_init_cpio 时,编译器再次砸出致命未定义引用:
usr/gen_init_cpio.c:(.text+0x275):对‘copy_file_range’未定义的引用
collect2: error: ld returned 1 exit status
- 原因: 这是最硬核的系统调用断层。新内核的打包工具调用了具有内核零拷贝特性的
copy_file_range函数,该函数要求系统的基石 C 库必须达到Glibc 2.27+。而 CentOS 7 铁打不动地绑死在Glibc 2.17上,底层根本没有这个系统调用符号。 - 降维补丁: 既然是在编译机上跑的打包程序,我们用
sed对其执行回滚,强行用最传统的普通文件缓冲区读写循环(read/write)来等价替换它:
sed -i '/this_read = copy_file_range/c\
\t\tchar buf[BUFSIZ];\n\t\tssize_t nread;\n\t\tthis_read = 0;\n\t\twhile (size > 0 && (nread = read(file, buf, size > BUFSIZ ? BUFSIZ : size)) > 0) {\n\t\t\tif (write(outfd, buf, nread) != nread) { this_read = -1; break; }\n\t\t\tthis_read += nread;\n\t\t\tsize -= nread;\n\t\t}' usr/gen_init_cpio.c
单目录清理 make -C usr clean。由于这台虚拟机原本只是为了编译 openssh 配的,系统只给分配了 4G内存/4核心。在内存实际识别只有 1.9G 可用的及格线上,我以为 4 核全开进行大链接时会把内存写爆触发 OOM Killer 导致死机,结果并没有 OOM ,只有CPU占用撑爆了。
历时约莫50分钟,几千行 .c 源码轰鸣而过,内核本体(bzImage)和全套模块驱动(.ko)彻底就地编译成功!
0x03 包装冲锋:正面硬撼官方标准三件套 RPM
编译完成了,如果是就地 make install 安装,只能爽到当前这一台机器。真正的挑战在于如何将它打包成正规的 RPM 交付件。
如果我们走内核自带的 make binrpm-pkg,高版本内核在老旧的 rpmbuild 4.11 环境下会发生毁灭性的语法冲突,比如老系统根本不认识带有小括号的现代布尔条件依赖(Boolean Rich Dependencies),开局就会丢出一句:错误:行 25: Dependency tokens must begin with alpha-numeric, '_' or '/': BuildRequires: (elfutils-devel or libdw-devel)。
为了打出官方标准的 kernel、kernel-devel、kernel-headers 三件套大包,并让它能够无缝分发到任何一台物理实机上跑,只能放弃死板的内建宏,直接采用原地源码重包+内建模版清洗:
1. 物理机分发防死锁:强制解开 hostonly 封印
dracut 默认会开启 hostonly 模式,即只抓编译机当前的网卡和磁盘驱动。一旦分发到真机或不同虚拟化平台就会因“缺驱动”导致开机卡死在 dracut:/# 抛锚。
我们直接在系统底层注入全局配置,强制打包万能全量驱动:
sudo mkdir -p /etc/dracut.conf.d
sudo tee /etc/dracut.conf.d/00-generic.conf << 'EOF'
hostonly="no"
EOF
2. 准备正规打包沙盒与全量源码伪装
既然老系统的 rpmbuild 执着于寻找 Source0 源码包,那就原地给它打一个真家伙:
cd /usr/src/linux-7.0.9
mkdir -p rpmbuild/SOURCES rpmbuild/BUILD
# 将当前已编译好的全量内核源码真正打包进去,耗时约4分钟
tar --exclude='./rpmbuild' -zcf rpmbuild/SOURCES/linux.tar.gz .
# 将源码平铺一份到 BUILD 沙盒,为绕过后面的解压检测做准备
rsync -a --exclude='rpmbuild' . rpmbuild/BUILD/
3. 精准解剖官方内建模版,切除降维代差依赖
高版本内核生成的 spec 模版躺在 rpmbuild/SPECS/kernel.spec,里面混杂了老系统没有的 dwarves、python3 以及括号依赖。我们用 sed 实施精准刮骨:
# 清洗掉混在行内的古代环境不认识的依赖
sed -i 's/ dwarves//g' rpmbuild/SPECS/kernel.spec
sed -i 's/ python3//g' rpmbuild/SPECS/kernel.spec
# 强行将整个 %prep 解压和目录清理章节连根拔起,直接注入 exit 0
# 因为源码已经平铺在 BUILD 里了,这样可以完美避开 rm -rf . 导致的退出状态不好报错
sed -i '/%prep/,/%build/{/%prep/!{/%build/!d}}' rpmbuild/SPECS/kernel.spec
sed -i '/%prep/a \exit 0' rpmbuild/SPECS/kernel.spec
4. 调用原生核心,出包!
rpmbuild 内部自带了多核并发编译宏 %{_smp_mflags},它会自动吃满多核性能。直接调用系统底层的打包核心:
rpmbuild -bb rpmbuild/SPECS/kernel.spec --define="_topdir /usr/src/linux-7.0.9/rpmbuild" --target x86_64
这一次编译我把CPU核心数调整为14核心,编译很快,约莫6分钟就以 + exit 0输出结束,在 rpmbuild/RPMS/x86_64/ 目录下,成功完整找到官方标准的三件套:
kernel-7.0.9+-12.x86_64.rpm(内核本体与全量物理/虚拟驱动)kernel-devel-7.0.9+-12.x86_64.rpm(完备的内核开发包)kernel-headers-7.0.9+-12.x86_64.rpm(标准调用接口头文件)
0x04 交付落地:多机分发与 GRUB 终极刷新
这三个标准的 RPM 包已经是完全解开硬件封印的成品,可以分发到任意一台同为 x86_64 架构的全新 CentOS 7 虚拟机或物理实机上。
1. 多机部署命令
如果目标机器上之前有自己折腾过的不完整旧内核包导致“文件冲突”,可以用 --replacefiles 强行覆写顶替安装:
# 强行覆盖安装官方标准三件套
sudo rpm -ivh --replacefiles --nodeps kernel-*.rpm
2. 核心补全:刷新与提纯 GRUB 引导
安装完成后,由于是强推安装,系统可能没有正确建立对应的万能驱动引导项,或者会在 GRUB 菜单里留下多个内核项(我最早就信了哈基米的话,没有重新编译三大套件,直接打包就起不来,引导菜单还多出来一个选项)。
要先进系统(或者用 3.10 老内核保底进去)执行一次全局重构,强制把万能驱动灌入并刷新 GRUB:
# 1. 强行手动为新内核重新打一个包含全量万能驱动的 initramfs 镜像(对应你真正的模块目录 7.0.9+)
sudo dracut -f -N /boot/initramfs-7.0.9+.img 7.0.9+
# 2. 强行卸载掉导致开机死锁、不带加号的伪内核包痕迹(如果有的话)
sudo rpm -e --nodeps kernel-7.0.9-1.x86_64 2>/dev/null || true
# 3. 彻底刷新 GRUB2 全局引导文件,将新镜像与菜单死死绑定,并自动剔除废弃项
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
执行 sudo reboot 重启系统,在 GRUB 开机菜单中,直接回车启动最纯净的第一项 CentOS Linux (7.0.9+) 7 (Core)。你会发现由于万能物理驱动就位,开机大跨步滑过,执行 uname -r,成功看到了 7.0.9+ 的完美谢幕!
0x05 终章:关于“混血状态”的哲学思考
目前,整个系统处于一种非常奇妙且硬核的 “混血状态”:
它的应用层生态依然是极度陈旧的(CentOS 7 / Glibc 2.17),所以我们无法在它上面直接安装任何来自 Rocky Linux 9 的高版本原生 RPM 二进制包。
但是,它的胸腔里顶着一颗完美支持现代硬件特性、完美支持最新 eBPF 特性和全套现代容器控制流的顶级内核跑车引擎(Linux 7.0.9)。
有了这颗心脏,接下来我们可以通过部署现代容器,利用高版本内核对 Cgroups 的极致支持,在里面直接跑 Rocky 9 的现代容器舱;或者通过 Nix 独立接管用户态现代高版本 C 库。
在新老代差的缝隙中强行撕开一条生路的自由度,也许才是玩 Linux 能够带给我们的最大快乐吧。
当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »