Skip to content

第一百九十三章:内核调试

大乘期

涉及内核源码:

林小源走进一间昏暗的工坊。墙上挂满了各种工具——有些他认识,有些完全陌生。工坊中央是一张巨大的工作台,台上散落着内核的碎片,每一块碎片上都隐约可见代码的痕迹。

"又一个来学调试的。"一个沙哑的声音从角落里传来。

林小源转头,看到一个佝偻的老者正蹲在工作台旁,手里拿着放大镜,仔细端详一块碎片。老者的白发乱糟糟的,衣袍上沾满了灰尘和油渍,但那双眼睛却异常明亮。

"调试比修炼更难。"老者放下放大镜,"在用户空间,程序崩溃了重启就行。在内核里,一个 bug 就能让整个系统变成砖头。"

他从墙上取下几件工具,依次摆在台面上:"printk 是最基本的——内核日志。八个级别,从 KERN_EMERG 到 KERN_DEBUG。但光靠 printk 找 bug,就像在大海里捞针。"

"那还有什么?"

老者从工具架上取下一块刻着 "KASAN" 铭文的铜牌:"内核地址消毒器。再看这个——"他又取下三块,"KMSAN 检测未初始化内存,KCSAN 检测数据竞争,UBSAN 检测未定义行为。还有 lockdep,专门抓死锁。"

c
/*
 * 内核调试工具:
 *
 * 1. printk
 *    内核日志
 *    不同级别: KERN_INFO, KERN_ERR
 *
 * 2. 动态调试
 *    pr_debug/dynamic_pr_debug
 *    运行时启用
 *
 * 3. KASAN
 *    内核地址消毒器
 *    检测内存错误
 *
 * 4. KMSAN
 *    内核内存消毒器
 *    检测未初始化内存
 *
 * 5. KCSAN
 *    内核并发消毒器
 *    检测数据竞争
 *
 * 6. UBSAN
 *    未定义行为消毒器
 *    检测未定义行为
 *
 * 7. lockdep
 *    死锁检测
 *    分析锁依赖
 *
 * 8. kmemleak
 *    内存泄漏检测
 *
 * 调试技巧:
 *   1. 二分法
 *   2. git bisect
 *   3. 条件断点
 *   4. 日志分析
 */

/* 模拟不同级别的日志 */
#define KERN_EMERG   "0"
#define KERN_ALERT   "1"
#define KERN_CRIT    "2"
#define KERN_ERR     "3"
#define KERN_WARNING "4"
#define KERN_NOTICE  "5"
#define KERN_INFO    "6"
#define KERN_DEBUG   "7"

void simulate_printk(const char *level, const char *msg) {
    printf("[%s] %s\n", level, msg);
}

printf("=== 内核调试 — 找到隐藏的 bug ===\n\n");

printf("--- printk ---\n");
simulate_printk(KERN_ERR, "错误: 设备未找到");
simulate_printk(KERN_WARNING, "警告: 内存不足");
simulate_printk(KERN_INFO, "信息: 模块加载");
simulate_printk(KERN_DEBUG, "调试: 函数调用");

printf("\n--- 调试工具 ---\n");
printf("1. KASAN:\n");
printf("   内核地址消毒器\n");
printf("   检测内存错误\n");
printf("   use-after-free\n");
printf("   buffer-overflow\n\n");
printf("2. KMSAN:\n");
printf("   内核内存消毒器\n");
printf("   检测未初始化内存\n\n");
printf("3. KCSAN:\n");
printf("   内核并发消毒器\n");
printf("   检测数据竞争\n\n");
printf("4. UBSAN:\n");
printf("   未定义行为消毒器\n");
printf("   检测未定义行为\n\n");
printf("5. lockdep:\n");
printf("   死锁检测\n");
printf("   分析锁依赖\n\n");
printf("6. kmemleak:\n");
printf("   内存泄漏检测\n\n");

printf("--- 调试技巧 ---\n");
printf("1. 二分法:\n");
printf("   逐步缩小范围\n\n");
printf("2. git bisect:\n");
printf("   找到引入 bug 的提交\n\n");
printf("3. 条件断点:\n");
printf("   gdb 条件断点\n\n");
printf("4. 日志分析:\n");
printf("   dmesg\n");
printf("   journalctl\n\n");

printf("--- KASAN 原理 ---\n");
printf("影子内存:\n");
printf("   每 8 字节映射 1 字节\n");
printf("   记录内存状态\n\n");
printf("检查:\n");
printf("   每次内存访问\n");
printf("   检查影子内存\n\n");

printf("--- 启用调试 ---\n");
printf("CONFIG_KASAN=y\n");
printf("CONFIG_KCSAN=y\n");
printf("CONFIG_LOCKDEP=y\n");
printf("CONFIG_DEBUG_KMEMLEAK=y\n");

老者领着林小源走到工坊深处的一面墙前。墙上挂着一幅奇特的地图——不是地形图,而是内存的影子。每一块内存区域旁边都有一小块灰色的影子,记录着那块内存的状态。

"这是 KASAN 的影子内存。"老者说,"每 8 字节内存对应 1 字节影子。影子记录着:这块内存是可访问的、已被释放的、还是毒化的。"

林小源看着那些影子,发现有些是灰色的——正常;有些是红色的——已被释放;有些是黑色的——从未分配。

"每次你访问内存,KASAN 就会检查对应的影子。"老者用手指点了点一块红色的影子,"如果你访问了一块已释放的内存——use-after-free——影子会立刻发出警报。如果你写入超过分配大小——buffer-overflow——影子也会报警。"

"代价呢?"林小源问。

"性能。"老者坦然道,"每次内存访问都要查影子,开销不小。所以 KASAN 只在调试时启用,生产环境关闭。CONFIG_KASAN=y,编译内核时打开。"

老者从墙上取下一把奇怪的工具——一把两刃剑,剑身上刻着密密麻麻的提交记录。

"git bisect。"老者说,"当你知道某个 bug 是最近引入的,但不知道是哪次提交,就用它。"

他把剑横在工作台上:"假设你知道一个月前没有这个 bug,现在有。git bisect 用二分法——先测试中间的提交,如果 bug 存在,说明是后半段引入的;如果不存在,说明是前半段。不断二分,十几次就能定位到具体的提交。"

"二分法。"林小源眼睛一亮。

"对。"老者点头,"还有 gdb 的条件断点——只在满足条件时断下来。还有 dmesg 和 journalctl 看日志。调试是一门艺术,工具只是手段,关键是你怎么用。"

老者把那些工具一一收回墙上,回头看着林小源:"记住,在内核里调试,最重要的不是工具,是耐心。"


道藏笔记

内核启示

内核调试需要专门的工具和技巧。

调试工具:

  • printk — 内核日志
  • KASAN — 内存错误检测
  • KMSAN — 未初始化内存检测
  • KCSAN — 数据竞争检测
  • UBSAN — 未定义行为检测
  • lockdep — 死锁检测
  • kmemleak — 内存泄漏检测

调试技巧:

  • 二分法
  • git bisect
  • 条件断点
  • 日志分析

调试是发现——让隐藏的 bug 无处遁形。


破关试炼

内核调试之试

内核调试一章里,用来跟踪锁依赖并发现潜在死锁的工具是什么?

答对后才能继续滑动和进入下一章。

以修仙之名,悟内核之道