第 1 章 调试环境搭建与准备
本章共 10 节,点击下方链接阅读:
第 1 章 在底层失序的边缘
1.1 技术准备:搭建你的手术台
.10 延伸阅读
写到这,第一章其实已经讲完了。
.2 软件调试——本质、溯源与迷思
如果上一节是在搭建实验室的物理空间,那么这一节,我们需要先对齐一个认知:在这个空间里,我们要对付的到底是什么?
.3 软件缺陷——几个真实的惨痛案例
上一节我们还在轻松地聊飞蛾和“Debug”的词源,这一节,画风得变一变了。
.4 搭建工作台
上一节那些血淋淋的案例应该已经给你留下了足够的心理阴影。现在,带着这种「如履薄冰」的敬畏感,我们终于可以动手了。
.7 两种内核:精打细算的生产者,与拿着显微镜的侦探
工具箱备好了,还没开工,我想先让你看一张内核开发者的桌子。
.7 打造你的定制生产内核
到了这一步,我必须假设你对从源码构建 Linux 内核的这套「基本操作」已经不算陌生了:拉取源码树、配置、编译。如果你觉得有点生疏,或者想复习一下细节,强烈推荐去翻翻《Linux Kernel Programming》这本书,或者参考本章末尾的「延伸阅读」部分。
.7 建立我们的自定义调试内核
上一节我们打造了一个「生产内核」。它像个身手敏捷的特工,精简、高效,随时准备投入战场。但作为开发人员,光有特工是不够的——有时候,我们需要的是一个话痨。一个会在每一步操作前都大声嚷嚷自己在干什么、甚至会把自己内心独白打印出来的家伙。
.8 看见差异——生产内核与调试内核的配置对决
上一节我们把调试内核里能开的开关都打开了——这很好,但这也引来了一个问题:我们到底改了什么?或者说,这一堆 y 和 n 的跳动,到底让这颗内核和它的生产版兄弟产生了多大的分歧?
.9 调试的禅意与直觉
上一节我们花了不少时间在「磨刀」上——编译生产内核和调试内核。现在,你的工具箱里已经有了两把不同的刀。
第 2 章 Bug 分类与调试方法论
本章共 3 节,点击下方链接阅读:
第 2 章 黑暗森林的火把
本章建立的核心认知是:调试内核和调试用户态程序完全是两种物种。在用户态你拥有整个 libc 库、隔离的进程空间和随时可以 attach 的 GDB;到了内核态,你的一个指针错误就能让整个机器瘫痪,而且一旦停机,所有的上下文——除了那一堆十六进制数——都会随断电而烟消云散。
.2 Bug 类型分类
就像医生治病前得先搞清楚是病毒感染还是物理创伤一样,在挥舞调试大锤之前,我们得先搞清楚我们要面对的到底是什么类型的 Bug。
.3 内核调试方法全景图——什么时候该用什么招
好了,我们已经把 Bug 分了类,也搞清楚了它们为什么会让系统表现异常。
第 3 章 内核打印与日志调试
本章共 5 节,点击下方链接阅读:
.0 准备工作与本章动机
这一章,我们要深入内核的「黑盒」内部。
.1 无处不在的内核 printk
K&R 那本著名的《C 程序设计语言》里,第一个示例程序为什么总是 Hello, world?
.3 利用 printk 进行调试
你可能觉得,向内核日志输出一条调试消息很简单——不就是发一条 KERN_DEBUG 级别的 printk 嘛。直觉没错,但现实比这复杂得多。
.4 使用内核的动态调试大杀器
上一节我们讲到,trace_printk() 是高频路径下的救生圈。但这就带来一个两难的局面:为了调试,我们需要打印;但为了性能,我们又不敢多打——尤其是在生产环境。
.4 剩余的 printk 杂项与终极调试手段
走到这里,我们其实已经覆盖了 90% 的日常调试场景。从最基础的 printk,到分级的 pr_debug,再到用来防止刷屏的 ratelimit,以及那个像上帝视角一样的 Dynamic Debug。你的工具箱里现在应该有一套相当趁手的家伙了。
第 4 章 Kprobes 动态追踪
本章共 9 节,点击下方链接阅读:
第 4 章 透过针眼看大象——内核探针与动态追踪
我们面对着一团高速运转的乱麻。
第 4 章 透视黑盒:Kprobes 与内核 instrumentation
4.2 传统艺能:静态 Kprobes 的硬核玩法
.3 使用静态 kprobes——演示 3 与演示 4
在上一节里,我们通过手动查阅处理器寄存器,弄清楚了参数是如何在内核深处悄无声息地传递的。这就像是你学会了一门方言的语法——现在,你终于能听懂他们在说什么了。
.4 kretprobes 入门
我们在上一节见识了 kprobes 的威力——就像在内核函数里强行插入了一个断点。但这只能让你看到函数「进去」时的样子,如果我想看它「出来」时带回了什么结果呢?
.5 基于 Kprobe 的事件跟踪——内部细节
还记得上一节结尾留下的那个念想吗?有没有一种办法,不用写一行 C 代码,不用编译内核模块,就能给内核里的任意函数「装个窃听器」?
.6 设置动态 kprobe(通过 kprobe events)——在任意函数上插眼
上一节我们提到过,现实往往比演示要残酷。当你需要监控的那个函数——也许是你自己写的内核模块里的某个不起眼的内部函数,或者是某个冷门的系统调用——根本没有在 /sys/kernel/tracing/events 下露面,该怎么办?
.7 在内核模块上使用动态 kprobe 事件跟踪
上一节我们还在看内核的堆栈回溯,现在换个场景。
.8 追踪进程的上帝视角——通过 perf 和 eBPF 工具探秘 execve
在上一节里,我们证明了动态 kprobe 在追踪内核模块函数时几乎无所不能。但那是针对“我们自己写的代码”。
.9 延伸阅读与探索路径
这一路走来,我们把内核探针的机制拆了个底朝天——从最底层的汇编指令替换,到 pt_regs 的寄存器解析,再到 ftrace 的动态插入,最后站在了 eBPF 的肩膀上。
第 5 章 内存调试工具:KASAN 与 UBSAN
本章共 9 节,点击下方链接阅读:
第 5 章 窥视深渊 —— 内存破坏与未定义行为的动态分析
有一类 Bug,比逻辑错误更让人头疼,也更隐蔽。
.2 内存到底出了什么问题?
上一节我们准备好了一个用来「搞破坏」的内核源码树,甚至把编译器都换成了更锋利的 Clang。这一切准备工作都是为了对付那个在 C 语言世界里最古老、最狡猾,也最致命的敌人:内存问题。
.3 理解 KASAN 的基本原理
上一节我们列了一张长长的「愿望清单」,把内核里可能出现的各种内存错误都扒拉出来晒了一遍。现在的问题是:KASAN 到底是怎么把这张清单上的坏蛋一个个抓出来的?
.4 配置 Generic KASAN 模式
既然决定了先拿 Generic KASAN 开刀,那我们就得先把内核「武装」起来。
.5 使用 KASAN 捕获 Bug
假设你现在已经按照上一节的详细步骤,把开启了 KASAN 的调试内核配置好、编译好,并成功启动进去了。在我的环境里——一台 x86_64 的 Ubuntu 20.04 LTS 虚拟机——这一切已经就绪。
.6 使用 UBSAN 内核检查器捕获未定义行为
上一节我们谈到,KASAN 是内存错误世界的「重炮手」,但它显然不是万能的。有些极其狡猾的 Bug——比如我们在前面看到的那些——能完美避开 KASAN 的雷达。那么,我们还有什么武器可以用?
.7 使用 Clang 构建内核与模块
现在,让我们进入 Clang 的世界。
.8 捕获内核中的内存缺陷——对比与笔记(Part 1)
现在到了做复盘的时候了。
.9 延伸阅读:Rust、安全深渊与工具链的尽头
我们花了五章的时间,像考古学家一样一层层剥开了 C 语言内存管理的秘密花园——只不过花园里埋的不是宝藏,是地雷。从 KASAN 到 UBSAN,从 KFENCE 到古老的 Valgrind,我们手里的探测器越来越精密,但你也必须承认一个事实:我们依然在修补一条以人类易错性为根基的底层道路。
第 6 章 SLUB 调试与内存泄漏检测
本章共 7 节,点击下方链接阅读:
第 6 章 谁动了我的内存?(上)—— 捕捉内核堆栈里的幽灵
6.1 准备工作与 SLUB 调试基础
.2 精准打击:使用 slub_debug 参数埋雷
上一节我们讲,SLUB 调试机制这把枪已经架好了(CONFIGSLUBDEBUG),但默认情况下保险没开。
ch06_3
现在我们到了验收环节——把那些有问题的测试模块扔进内核里,看看刚才配置的那套安检机制能不能把漏洞抓出来。
.4 解读内核 SLUB 调试错误报告
好了,现在我们在内核的引导参数里加了 slub_debug=FZPU,也成功触发了一些 bug。就像我们在上一节末尾看到的,SLUB 调试机制确实抓到了坏人,并且吐出了一堆看起来很吓人的错误日志。
.5 使用 slabinfo 及其配套工具
工具箱里的新家伙
使用 kmemleak
回想一下我们在上一节结尾看到的场景:成百上千个 vmareastruct 对象被分配出来,堆在内存里,没人去管。当时我们说,这是一种合理的系统行为——直到它不再合理为止。
.7 给开发者的几条实战建议
说实话,调试工具再多,也不如写代码时少埋雷。
第 7 章 内核 Oops 与崩溃分析
本章共 7 节,点击下方链接阅读:
第 7 章 当内核开始咆哮
有一类问题,能瞬间让系统工程师的后背发凉。
.2 生成一个简单的内核 Bug 和 Oops
既然工具都就位了,是时候动手干坏事了。
.3 细节里的魔鬼——解剖现场
上一节我们亲手制造了一起内核惨案,看着 Oops 日志刷屏确实挺爽,但痛快完之后,问题来了:这些满屏的十六进制代码到底在说什么?
.4 精准打击:用 objdump 和 GDB 定位罪魁祸首
上一节我们把尸检报告读完了。我们知道了 dothework 函数挂了,也知道 RIP 寄存器停在了偏移量 0x124 的地方。
.5 解读内核 Bug 诊断信息(下)
7.5 善用内核脚本 —— 别重复造轮子
.6 利用控制台设备捕获中断上下文中的崩溃日志
上一节我们给手里的工具箱又加了几件利器——从栈空间检查到源码定位脚本。你可能会觉得,有了这些,哪怕内核崩溃我也能把它按在地上摩擦。
.7 异构战场:ARM Linux 上的 Oops 与 netconsole 实战
在上一节的 x86 虚拟机环境里,我们用一根虚拟串口线就把内核日志「钓」了出来。但在真实的嵌入式战场——比如一台树莓派——上,情况往往没那么优雅。
第 8 章 锁调试与并发问题
本章共 5 节,点击下方链接阅读:
.1 锁调试概览
这里有一类 Bug,它们最擅长的把戏就是「隐身」。
.2 锁机制——关键知识点速查
之前我啰里啰唆地说了一大堆前置知识,但为了确保我们在同一个频道上,还是有几条关于「锁」的核心原则需要再强调一下。你可以把这当成一张作弊条——但记住,在并发编程里,背下来规则和真正理解规则是两码事,后者需要你付出一点调试时的痛苦作为学费。
.3 用 KCSAN 抓住并发 Bug
既然靠人脑硬抗 LKMM 这种级别的复杂度已经有些吃力了,我们最好是找个工具来帮忙。这节要出场的,就是内核并发领域的「安检神器」——KCSAN。
.4 实战中的锁缺陷案例
上一节我们聊完了 KCSAN,它像一个不知疲倦的守夜人,帮我们盯着那些稍纵即逝的数据竞争。
.5 延伸阅读
这一章的信息密度极高。我们讲了锁,讲了海森堡 Bug,讲了 LKMM,还讲了 KCSAN 这种编译器级别的魔法。
第 9 章 Ftrace 跟踪技术
本章共 12 节,点击下方链接阅读:
第 9 章 显微镜下的内核:跟踪、剖析与黑盒的终结
这一章,我们要解决一个尴尬的问题。
.10 使用 trace-cmd、KernelShark 和 perf-tools 前端工具
在进入正题之前,先插一句题外话。当你花大量时间盯着 ftrace 报告研究时,你可能会发现一个现象——安全相关的接口调用频率高得惊人。
.11 LTTng 与 Trace Compass:高层视角的上帝模式
进场:换一种视角看内核
.12 进阶阅读与技术地图
本章的内容到此结束,但你的探索才刚刚开始。
.2 内核跟踪技术全景图
先别急着敲命令。
.3 配置内核以支持 ftrace
大多数现代 Linux 发行版都会预置 ftrace 支持到手。
.4 使用 ftrace 追踪内核流
上一节我们说到,虽然 tracingon 是 1,但只要 currenttracer 还是 nop,系统就是零损耗的。这就像一个虽然通电但没装灯泡的灯座。
.5 实用的 ftrace 过滤选项:从开火瞄准到精确制导
上一节我们解决了「怎么看」的问题——用 trace_pipe 配合各种格式化选项,把内核的行为变成可读的日志。
.6 实战:用原生 ftrace 追踪单次 Ping 请求
好了,工具箱已经打开了。我们现在有了配置内核的能力,有了简单的跟踪手段,也有了这一大堆高级的过滤技巧——Glob、索引、黑名单、命令。
.7 实战:使用 set_event 接口跟踪单次 Ping 请求
上一节我们用 availablefilterfunctions 这种「大撒网」的方式,把整个网络栈的函数调用图给扒了出来。虽然直观,但这就好比为了听清楚两个人在说什么,你把整栋楼人的麦克风都打开了——噪音太大,信息量太溢出。
.8 Ftrace 杂项与遗留问题(FAQ)
关于 ftrace,还有一些碎屑但非常重要的问题。与其把它们像扔垃圾一样列出来,不如用一种更符合我们直觉的方式——FAQ(常见问题解答)——来把这块拼图的最后几块补上。
.9 Ftrace 实战:从栈溢出监控到 Android 排毒
好了,Instances 的话题先告一段落。
第 10 章 内核 Panic 与死锁检测
本章共 5 节,点击下方链接阅读:
第 10 章 永不消逝的回响:当内核决定放弃
10.1 技术准备与场地检查
0.2 当内核决定放弃治疗——Panic 机制全解
要征服这只野兽,你得先懂它。
0.3 编写自定义内核恐慌处理程序
上一节,我们像拆弹专家一样,拆解了内核恐慌发生时的标准流程——从打印最后的遗言到决定是否重启。我们甚至还学会了通过 panic_print 这个调音旋钮,控制内核在临死前吐出多少信息。
0.4 检测内核中的死锁与 CPU 停顿
上一节结尾我们聊到了 Panic 处理器里的那条「红线」:别太复杂,否则连「临终遗言」都留不下。但有时候,内核的死法更阴险——它没有立刻崩溃,也没有大喊大叫,只是突然不说话了。
0.5 利用内核的 Hung Task 与 Workqueue 停顿检测器
接上一节,我们刚刚提到了系统可能会出现任务卡死的情况——也就是所谓的「Hung Task」。
第 11 章 KGDB 内核调试器
本章共 7 节,点击下方链接阅读:
第 11 章 深入内核:当调试器成为内核的一部分
有一类问题,普通调试器是碰不到的。
1.2 概念上理解 KGDB 的工作原理
上一节我们搭建好了 SEALS 项目,拿到了「硬件」(虽然是虚拟的)和根文件系统。现在,我们要把真正的武器——调试器——搬到内核战场。
1.3 搭建 ARM 目标系统与内核
先别急着烧录。
1.4 用 KGDB 狠狠地调试内核
上一节我们已经把这只名为「ARM VExpress」的虚拟羊养活了。它能在 QEMU 里欢快地跑起来,吐出一堆启动日志,最后乖乖地给你一个 shell 提示符。
1.5 调试内核模块:当符号表藏进内存里
上一节我们算是「黑」进了一个正在跑着的内核,看着它在 start_kernel 里醒来。但老实说,那种感觉更多像是在看舞台剧——我们只是观众,看导演安排好的剧情。
1.6 [K]GDB 进阶技巧与杂项
上节我们用 hbreak 咬住了 doinitmodule,算是彻底解决了「模块加载瞬间消失」的问题。但你一旦真的用 KGDB 跑起来,就会发现它像个深不见底的工具箱——大部分时候你只用螺丝刀,但当你真正需要那个弯头钳子时,最好知道它躺在哪个角落。
1.7 进一步阅读
正文到上一段就结束了。
第 12 章 总结与进阶阅读
本章共 2 节,点击下方链接阅读:
第 12 章 调试的武器库:没有银弹
有一类问题,表面上看是工具问题,实际上是哲学问题。
2.2 进一步阅读
书本终有尽头,但内核的深渊没有。