第一百九十三章:内核调试
大乘期涉及内核源码:
一
林小源走进一间昏暗的工坊。墙上挂满了各种工具——有些他认识,有些完全陌生。工坊中央是一张巨大的工作台,台上散落着内核的碎片,每一块碎片上都隐约可见代码的痕迹。
"又一个来学调试的。"一个沙哑的声音从角落里传来。
林小源转头,看到一个佝偻的老者正蹲在工作台旁,手里拿着放大镜,仔细端详一块碎片。老者的白发乱糟糟的,衣袍上沾满了灰尘和油渍,但那双眼睛却异常明亮。
"调试比修炼更难。"老者放下放大镜,"在用户空间,程序崩溃了重启就行。在内核里,一个 bug 就能让整个系统变成砖头。"
他从墙上取下几件工具,依次摆在台面上:"printk 是最基本的——内核日志。八个级别,从 KERN_EMERG 到 KERN_DEBUG。但光靠 printk 找 bug,就像在大海里捞针。"
"那还有什么?"
老者从工具架上取下一块刻着 "KASAN" 铭文的铜牌:"内核地址消毒器。再看这个——"他又取下三块,"KMSAN 检测未初始化内存,KCSAN 检测数据竞争,UBSAN 检测未定义行为。还有 lockdep,专门抓死锁。"
/*
* 内核调试工具:
*
* 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");#include <stdio.h>
#include <string.h>
/*
* 内核调试工具:
*
* 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);
}
int main() {
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");
return 0;
}二
老者领着林小源走到工坊深处的一面墙前。墙上挂着一幅奇特的地图——不是地形图,而是内存的影子。每一块内存区域旁边都有一小块灰色的影子,记录着那块内存的状态。
"这是 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 无处遁形。
内核调试之试
内核调试一章里,用来跟踪锁依赖并发现潜在死锁的工具是什么?