跳到主要内容

1.7 建立我们的自定义调试内核

上一节我们打造了一个「生产内核」。它像个身手敏捷的特工,精简、高效,随时准备投入战场。但作为开发人员,光有特工是不够的——有时候,我们需要的是一个话痨。一个会在每一步操作前都大声嚷嚷自己在干什么、甚至会把自己内心独白打印出来的家伙。

这就是调试内核存在的意义。

在这个内核里,我们不在乎性能损耗,也不在乎安全性折衷——我们在乎的是:当事情出错时,它能留下多少线索

搭建调试内核的过程和生产内核非常相似。为了避免重复那些机械的步骤,我们重点来看看两者的差异。这就像是同一个模具倒出来的两个模型,只是填充的配料不同。

准备工作:生产内核是起点

首先,确保你当前正运行在刚才编译好的生产内核上。这一点很重要,因为接下来的调试内核配置会以当前运行系统的状态为基准。

$ uname -r
5.10.60-prod01

看到了吗?那个 -prod01 的后缀说明我们正坐在正确的基石上。

划定地盘:独立的工作空间

虽然很诱人,但不要在生产内核的源码目录上直接改配置。

一定要建立一个干净的工作目录。是的,这会多占用几个 GB 的磁盘空间,但这笔账绝对划算——想象一下,当你的生产内核和调试内核的配置文件混在一起,互相覆盖时的那种绝望。保持它们互不干扰,是维护理智的第一步。

mkdir -p ~/lkd_kernels/debugk

接下来,像之前那样,把内核源码包解压到这个新目录里。我们复用之前下载好的 linux-5.10.60.tar.xz

cd ~/lkd_kernels
tar xf linux-5.10.60.tar.xz --directory=debugk/

配置策略:继承与变异

进入新目录,我们再次使用 localmodconfig 策略来生成初始配置。这会生成一个仅包含当前运行硬件所需模块的精简配置。

但这一次,「当前运行的内核」是我们自定义的那个生产内核。这意味着,调试内核会继承生产内核的硬件适配特性,同时在此基础上叠加调试功能。

cd ~/lkd_kernels/debugk/linux-5.10.60
lsmod > /tmp/lsmod.now
make LSMOD=/tmp/lsmod.now localmodconfig

配置核心:开启 Kernel hacking

配置界面的入口还是老样子:

make menuconfig

如果你觉得菜单太多令人眼花,没关系,有一个捷径:按 / 键(就像在 vi 里一样),然后输入你要找的配置项名称,可以直接跳过去。

内核的调试设施大部分都藏在一个菜单里——它就在主菜单的最下面,名字听起来有点像黑客帝国:Kernel hacking

在这个菜单里,我们可以看到琳琅满目的调试开关。它们多得有点吓人,而且大部分现在看起来都很晦涩。别担心,你不需要现在就搞懂每一个细节,我们会在后续的章节里遇到它们时逐一拆解。

现在的任务是:把那些最关键的开关打开。

为了方便操作,我整理了一张表(表 1.1),列出了生产内核和调试内核在配置上的典型差异。这绝对不是一份详尽的清单,但它足以作为你的起点。

💡 表 1.1:内核配置变量对比

(注:此处保留原文的表格对比信息,体现生产内核与调试内核在 CONFIG_DEBUG_INFO, CONFIG_KASAN, CONFIG_LOCKDEP 等关键选项上的区别。表格内容较长,涵盖了 General setup, Kernel hacking 等多个子系统的配置建议。)

  • 代表「留给架构师决定」(Depends),这取决于你的产品对高可用性(HA)和安全性的要求。
  • [1] 标记处提醒:如果你在 Ubuntu 20.04 上开启 CONFIG_DEBUG_INFO_BTF,编译可能会报错,因为系统自带的 pahole 版本太旧(需要 v1.16+)。我在代码仓库里放了个 v1.17 的包,救急用:
    sudo dpkg -i dwarves_1.17-1_amd64.deb

保存配置:防患于未然

当你像在自助餐厅一样选完了一大堆调试选项后,记得把这份「菜单」保存下来。这一步千万别省,否则下次你忘了自己选了什么,或者配置被意外覆盖时,你会哭出来的。

cp -af .config ~/lkd_kernels/kconfig_dbg01

编译与安装:胖子的诞生

现在,开始编译。由于我们开启了大量的调试信息(尤其是符号表),这次编译出来的产物会是个「大胖子」。

make -j8 all

编译完成后,对比一下两个核心文件的大小,你会被震惊到:

$ ls -lh arch/x86/boot/bzImage vmlinux
-rw-r--r-- 1 letsdebug letsdebug 18M Aug 20 12:35 arch/x86/boot/bzImage
-rwxr-x--x 1 letsdebug letsdebug 1.1G Aug 20 12:35 vmlinux

注意到了吗?vmlinux —— 那个未压缩的内核二进制文件 —— 竟然有 1.1GB

这其实是一个反直觉的时刻。平时我们看到的内核镜像(bzImage)只有几十 MB,为什么这个家伙这么大?

答案就在我们刚才打开的调试选项里。CONFIG_DEBUG_INFO 把所有的调试符号都塞进了二进制文件;如果开启了 CONFIG_KASAN(内核地址消毒剂),它还会插入大量的影子内存代码。这就像给一个精瘦的运动员穿上了全套的感应盔甲,体积当然膨胀。

这里 bzImage 依然是 18M 左右(因为它会被压缩),而 vmlinux 的大小才真实反映了「臃肿」的程度。

最后,把模块安装好,更新 initramfs 和引导菜单,流程和生产内核一模一样:

sudo make modules_install && sudo make install

视窗:如何查看当前内核的配置

有时候你会遇到一台正在运行的机器,你需要搞清楚它当初是怎么编译出来的,但又找不到源码目录。内核其实自带了一个「黑匣子」功能,但这取决于一个配置选项:CONFIG_IKCONFIG

在这个配置里,我们通常这样设置:

  • 调试内核CONFIG_IKCONFIG=y(直接内置,总是可见)
  • 生产内核CONFIG_IKCONFIG=m(做成模块,需要时才加载,多一层安全掩体)

如果生产内核里把它设为模块(m),那么默认情况下你是看不到配置文件的,除非你有 root 权限并且手动加载模块。这是一个很聪明的设计:开发者需要时能加载,而普通用户甚至不知道它的存在。

让我们来实际操作一下。在一台生产内核的机器上,你首先尝试查看配置文件:

$ ls -l /proc/config.gz
ls: cannot access '/proc/config.gz': No such file or directory

看不见?对的,因为模块还没加载。

现在,让我们用 sudo 权限把那个「黑匣子」模块插上:

$ sudo modprobe configs
$ ls -l /proc/config.gz
-r--r--r-- 1 root root 34720 Oct 5 19:35 /proc/config.gz

出现了。现在我们可以用 zcat 解压并查询里面的内容,甚至查询它自己的配置项,这简直像是在照镜子:

$ zcat /proc/config.gz | grep IKCONFIG
CONFIG_IKCONFIG=m
CONFIG_IKCONFIG_PROC=y

完美。这确认了当前内核正是以模块形式(m)提供此功能的。


🍔 Food for Thought

在前面的配置表 1.1 里,我把生产内核的 CONFIG_KALLSYMS_ALL 设为了 <D>(由架构师决定)。你可能会问:既然是生产内核,为了安全起见,不应该把查看所有内核符号的能力关掉吗?

直觉确实是这样的。但我想讲一个古老的故事——火星探路者。

1997 年,火星探路者在着陆后不久开始频繁重启。JPL 的软件团队负责人 Glenn Reeves 在事后复盘时说过一句意味深长的话:

"飞在火星上的软件其实包含了很多实验室里用的调试功能。虽然我们在太空中不使用它们(因为产生的数据量太大传不回地球),但这些功能并不是「不小心」留下的,而是故意保留的。我们坚信一种哲学:测试什么就飞什么(Test what you fly and fly what you test)。"

有时候,在生产的险恶环境中,保留哪怕一点点调试能力和日志记录,可能会在关键时刻救你一命。这是在「完美的安全性」和「生存能力」之间的一场博弈。