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