diff --git a/.gitignore b/.gitignore
index 09da07364..0551cdc74 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
# AI agent infra — 公开(AGENTS.md / CLAUDE.md / .claude 资产库),仅以下保持私有
.claude/settings.local.json
.claude/cache/
+.claude/drafts/
.claude/issues/
.claude/prompts/produce-vol5-labs.md
.claude/prompts/vol5-code-verification.md
diff --git a/README.md b/README.md
index a38d959ad..9f3b7da59 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@
---
- 492/492 docs translated
+ 491/494 docs translated
## 这是什么项目
diff --git a/documents/en/vol8-domains/index.md b/documents/en/vol8-domains/index.md
index cf506f59b..a51d4276f 100644
--- a/documents/en/vol8-domains/index.md
+++ b/documents/en/vol8-domains/index.md
@@ -35,7 +35,6 @@ An estimated 80 to 100 articles.
Embedded Development
- Network Programming — Planned
GUI and Graphics — Planned
Data Storage — Planned
Algorithms and Data Structures — Planned
diff --git a/documents/en/vol8-domains/networking/index.md b/documents/en/vol8-domains/networking/index.md
deleted file mode 100644
index 4df8303ec..000000000
--- a/documents/en/vol8-domains/networking/index.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-title: Network Programming
-description: Socket, HTTP, asynchronous I/O, WebSocket, RPC
-platform: host
-tags:
-- cpp-modern
-- host
-- intermediate
-translation:
- source: documents/vol8-domains/networking/index.md
- source_hash: 061eff1fa2f15c315ac1026800957c3f310ee80500e97f3297e0f59c2dde6a1d
- translated_at: '2026-05-26T12:23:40.447955+00:00'
- engine: anthropic
- token_count: 62
----
-# Network Programming
-
-> Status: Planned
-
-## Overview
-
-This subdomain covers C++ network programming, including Socket fundamentals, HTTP, asynchronous I/O, WebSocket, and RPC.
-
-Thirteen articles are expected.
-
-## Chapter Navigation
-
-> Content is being written, stay tuned.
diff --git a/documents/vol6-performance/10-asan-family-and-memory-safety.md b/documents/vol6-performance/10-asan-family-and-memory-safety.md
new file mode 100644
index 000000000..9c149752c
--- /dev/null
+++ b/documents/vol6-performance/10-asan-family-and-memory-safety.md
@@ -0,0 +1,398 @@
+---
+title: "ASan 工具家族与内存安全:shadow memory、Heartbleed 与 sanitizer 选型"
+description: "从 Heartbleed 说起,拆解 AddressSanitizer 的 shadow memory 三件套,实测 OOB/UAF/全局越界与 UBSan,理清 ASan/LSan/MSan/TSan/UBSan 五兄弟的职责与互斥关系"
+chapter: 6
+order: 10
+platform: host
+difficulty: advanced
+cpp_standard: [11, 14, 17, 20]
+reading_time_minutes: 22
+prerequisites:
+ - "动态内存管理(new/delete 与智能指针)"
+ - "并发程序调试技巧(ThreadSanitizer)"
+related:
+ - "动态内存管理(new/delete 与智能指针)"
+ - "并发程序调试技巧(ThreadSanitizer)"
+ - "C 语言动态内存管理(malloc/free 与 valgrind)"
+tags:
+ - host
+ - cpp-modern
+ - advanced
+ - 内存安全
+ - 调试
+ - 内存管理
+---
+
+# ASan 工具家族与内存安全:shadow memory、Heartbleed 与 sanitizer 选型
+
+> PS: 这部分内容是笔者读大学期间迁移的笔记,仅经过有限的搜索验证,如果发现存在技术断言不严谨的问题甚至时错误,欢迎报告 Issue! 或者是直接的修复PR!
+
+写过一段时间 C/C++,你大概率被这几类问题反复折磨过:数组多读了一位、释放完的指针被别人又用了一次、同一个 `delete` 调了两遍。这些错误有个共同的恶心之处——它们是**未定义行为**(大名鼎鼎的Undefined Behavior),不是"一定会崩",而是"在 Debug 构建里跑得好好的,到了 Release 或者换了台机器就随机爆炸"。更糟的是,崩的地方往往离真正出错的代码十万八千里,堆栈指向的可能是某个无辜的库函数。
+
+为什么会这样?因为这类 bug 破坏的是内存管理器自己的元数据,等到下一次 `malloc`/`free` 走到那个被踩坏的位置才发作。本卷前面讲性能,这一篇我们换到另一个维度:怎么在 bug 还没酿成线上事故之前,就用工具把它揪出来。主角是 AddressSanitizer(ASan)和它背后那一整套 sanitizer 工具家族。
+
+先别急着把 ASan 当成一个"加个 flag 就完事"的小工具。它背后的设计——shadow memory、编译期插桩——其实是过去十几年 C/C++ 内存安全领域最重要的工程进展之一,而且它最初被发明出来,就是为了堵住一个让整个互联网心惊肉跳的洞。我们从那个洞讲起。
+
+## 一切的起点:Heartbleed 与 buffer over-read
+
+2014 年 4 月,CVE-2014-0160 被披露,代号 Heartbleed。这是 OpenSSL 实现里一个听起来人畜无害的特性——TLS 心跳扩展(heartbeat)——里藏的洞。协议很简单:客户端发来一段任意数据,告诉服务器"这段数据有 N 字节,请原样读回来",用来验证连接还活着。
+
+漏洞在于,服务器**信任了客户端报上来的长度 N,但没有校验 N 是否真的不超过自己手里那段数据的实际长度**。于是攻击者只要把 N 报得很大(比如 64KB),服务器就会从自己进程内存里"读回" 64KB 给攻击者。读到的可能是别的会话的 TLS 私钥、用户密码、session token——进程内存里挨着那块缓冲区的一切,统统泄漏。
+
+这个 bug 的本质不是越界**写**,而是越界**读**(buffer over-read / over-read)。写越界好歹会破坏数据、容易暴露;读越界安静得多,进程自己不会崩,数据就这么悄无声息地流出去了。ASan 当年被反复拎出来讲,正是因为它属于少数能**稳定检测 over-read**的工具——只要那段越界内存碰到了 ASan 埋下的 redzone,读一下就立刻报错。
+
+我们用几十行 Modern C++ 复刻一个 Heartbleed 形状的 bug,然后让 ASan 抓现行。这是本篇的王牌演示,后面要反复用到。
+
+```cpp
+// oob_read.cpp —— 复刻 Heartbleed 形状的越界读
+// Platform: host Standard: C++20
+// 编译: g++ -std=c++20 -O1 -fsanitize=address -g oob_read.cpp -o oob_read
+#include
+#include
+#include
+
+// 心跳回显:客户端说"还给我 n 字节"。服务器照办,但不校验 n 上界。
+std::string read_back(const std::array& buf, int n)
+{
+ return std::string(buf.data(), n); // n 可能远大于 8
+}
+
+int main()
+{
+ std::array buf{'H', 'i', '!', 0, 0, 0, 0, 0};
+ // 只授权了 8 字节,却要求"读回" 64 字节 —— 经典 over-read
+ auto leaked = read_back(buf, 64);
+ std::printf("读到 %zu 字节: %.8s...\n", leaked.size(), leaked.c_str());
+}
+```
+
+不带 ASan 编译运行,这段代码大概率"看起来正常":`std::string` 的构造函数老老实实按你给的长度从 `buf.data()` 起拷贝 64 字节,把栈上后面那些不相干的字节全读走了,程序不会崩。这正是 over-read 可怕的地方。
+
+加 `-fsanitize=address` 再跑,画风突变。用本机 GCC 16.1.1 跑出来:
+
+```text
+=================================================================
+==37023==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x72175e1f0028 at pc 0x761760d29ac2 ...
+READ of size 64 at 0x72175e1f0028 thread T0
+ #0 0x... in memcpy (/usr/lib/libasan.so.8+0x129ac1)
+ ...
+ #6 0x... in read_back[abi:cxx11](std::array const&, int) oob_read.cpp:11
+ #7 0x... in main oob_read.cpp:18
+ ...
+
+ This frame has 2 object(s):
+ [32, 40) 'buf' (line 16)
+ [64, 96) 'leaked' (line 18) <== Memory access at offset 40 partially underflows this variable
+SUMMARY: AddressSanitizer: stack-buffer-overflow oob_read.cpp:11 in read_back
+```
+
+注意两个细节。第一,报错类型是 `stack-buffer-overflow`,发生在 `read_back` 第 11 行——也就是 `return std::string(buf.data(), n);` 那一行,精确到源码位置,这正是为什么编译时必须带 `-g`。第二,ASan 甚至告诉我们栈帧里有两个对象,`buf` 占 `[32, 40)`、`leaked` 占 `[64, 96)`,越界读的位置(offset 40)正好落在两者之间。这种级别的现场信息,是 ASan 区别于"加个断言慢慢找"的根本所在。
+
+## 所以,ASan 到底做了什么才能这样的呢?
+
+### 第一件:编译期插桩(CTI)
+
+ASan 不是事后分析的 profiler,而是**编译期就改写你的代码**。当你加 `-fsanitize=address`,编译器(GCC 或 Clang 都行)会在每一次内存访问——每一个 `*p`、每一次数组下标、每一次 `memcpy`——前后插入额外的检查指令。这种技术叫**编译期插桩**(compile-time instrumentation,CTI),也叫静态插桩。
+
+这里先验证一下它真的"动了你的代码"。把上面的 `oob_read.cpp` 不带 ASan 编译一份,带 ASan 编译一份,对比两者的**代码段(.text)大小**——也就是真正塞进二进制里的机器指令:
+
+```text
+普通构建 .text: 2792 字节
+ASan 构建 .text: 5736 字节 (+105%)
+```
+
+(本机 GCC 16.1.1,`g++ -std=c++20 -O1 -g`,用 `size` 看 `.text` 段。) 多出来的那一倍,就是编译器在每次内存访问前后塞进去的检查指令。注意这里有个坑:**别拿整个二进制文件的大小来比**——ASan 的运行时库 `libasan.so.8` 是**动态链接**的(`ldd` 能看到它),并没有被打进可执行文件,所以整个文件大小其实只涨了 5% 左右;真正反映插桩量的是 `.text` 代码段,那才是翻倍增长的地方。代价是体积变大、运行变慢——但比起它能抓到的 bug,这点开销在开发阶段几乎可以忽略。CTI 是**编译期**决定的事,所以你必须**编译时**就带上 `-fsanitize=address`,而且**链接时也要带**。如果你只编译主程序时加了、链接某个第三方 `.a` 库时没加,那库内部的内存访问就没被插桩,ASan 对那部分代码就是瞎的。完整流程是:
+
+```bash
+g++ -std=c++20 -O1 -fsanitize=address -g -c a.cpp -o a.o # 编译带
+g++ -std=c++20 -O1 -fsanitize=address -g main.cpp a.o -o app # 链接也带
+```
+
+`-fsanitize=address` 在编译和链接两个阶段都要出现,缺一个就白搭。
+
+### 第二件:shadow memory(影子内存)
+
+光有插桩还不够。插桩插入的检查代码,需要一个"账本"来回答"这个地址现在到底能不能访问"。这个账本就是 **shadow memory**(影子内存)。
+
+核心思想是一句很优雅的设计:**用 1 个字节的影子内存,记录 8 个字节的实际内存的可访问状态**。也就是说,ASan 把整个进程地址空间按 8 字节一组映射到一片连续的影子区,比例是 1:8。这样检查一个地址是否合法,只需要算出它对应的影子字节、读出来看一眼就行,不用维护什么复杂的哈希表。
+
+影子字节的取值,ASan 的报告末尾会直接打印出来。我们看真实输出里的图例:
+
+```text
+Shadow byte legend (one shadow byte represents 8 application bytes):
+ Addressable: 00
+ Partially addressable: 01 02 03 04 05 06 07
+ Heap left redzone: fa
+ Freed heap region: fd
+ Stack left redzone: f1
+ Stack mid redzone: f2
+```
+
+这就是 1:8 映射的全部语义。`00` 表示这 8 字节都可以访问;`01`–`07` 表示只有前几个字节合法(比如 `03` 表示前 3 字节能访问、后 5 字节不能,用于对齐尾部的部分可访问区域);`fa` 是堆分配周围的 redzone——ASan 在你 `new` 出来的每块内存四周都偷偷塞了一圈"禁入区",你一旦读到 `fa`,就是堆越界;`fd` 是已经 `free` 掉的内存,你一碰就是 use-after-free;`f1`/`f2` 是栈对象的 redzone。
+
+回头看上面那段报错的 shadow dump:
+
+```text
+=>0x72175e1f0000: f1 f1 f1 f1 00[f2]f2 f2 00 00 00 00 f3 f3 f3 f3
+```
+
+`00` 是 `buf` 本体(8 字节,1 个影子字节),紧跟着的 `[f2]` 就是栈对象之间的 mid redzone。我们越界读的地址恰好落在这个 `f2` 上——ASan 一眼就看出来了。这是 shadow memory 机制能精确到字节级的原因。
+
+### 第三件:运行时库 + quarantine
+
+光有插桩和影子区还不够,还得有人**填这个账本**。`new`/`delete`、`malloc`/`free` 这些函数,ASan 运行时库(`libasan`)会把它们整个换掉——换成自己的版本。每分配一块内存,运行时就给它在影子区里画上 redzone;每释放一块,就把对应影子区标记成 `fd`。
+
+这里还有个关键设计叫 **quarantine(隔离区)**。`free` 掉的内存,ASan 不会立刻归还给系统重新分配,而是先扔进一个隔离队列里晾着。为什么?因为 use-after-free 这种 bug,如果你 `free` 完马上又分配出去给别人用了,那块内存的影子状态就变回 `00` 了,后面误读就抓不到了。隔离一段时间,保证"已释放"的状态能被后续的误访问撞上。
+
+不过 quarantine 不是无限的——队列有上限,满了之后旧的已释放内存会按 FIFO 被真正回收。所以 ASan 对 use-after-free 的检测也不是 100%——如果隔离窗口已经滑过去了、内存已经被重新分配,那次误读就抓不到。但配合足够的测试覆盖,绝大多数 UAF 都能被逮住。
+
+### 代价:2-4 倍开销,为什么还是值得
+
+三件套加起来,ASan 的典型开销是 **2-4 倍的运行时间减速、3-5 倍的内存开销**(影子区占 1/8,加上 redzone 和 quarantine)。听起来不少,但这是跟谁比的问题。
+
+传统的内存检测工具 Valgrind(Memcheck)用的是**动态二进制插桩**(DBI)——它不重新编译你的程序,而是在运行时把每一条机器指令翻译成自己的一套中间表示、逐条分析再执行。精度高、不用重新编译,但代价是 20-50 倍的减速。跑一个原本 1 秒的测试,Valgrind 要等半分钟,很多时候根本没法纳入日常 CI。
+
+ASan 把分析成本**前移到了编译期**(CTI),运行时只做查表,所以能把开销压到 2-4 倍。这个量级意味着你可以在开发和 CI 里**常驻**开着 ASan 跑完整测试套件,而不是偶尔想起来才手动跑一次 Valgrind。这是 ASan 相对 Valgrind 最根本的优势——不是更准,而是**用得起**。
+
+::: warning 本机没装 Valgrind
+这篇的所有 ASan/UBSan 输出都是本机真跑出来的(GCC 16.1.1 / Clang 22,WSL2)。Valgrind 在本机环境里没装(`which valgrind` → not found),所以本篇不贴 Valgrind 实测输出。需要 Valgrind 的同学在 Debian/Ubuntu 上 `apt install valgrind` 即可,用法见 vol1 的 [C 语言动态内存管理](../vol1-fundamentals/c_tutorials/14-dynamic-memory.md) 一文的 valgrind 段。两条命令的本质区别记牢:**ASan 是编译期 `-fsanitize=address`(CTI),Valgrind 是运行时 `valgrind ./prog`(DBI)**。
+:::
+
+## 工具家族:五个 sanitizer 各管一类
+
+ASan 其实只是一个家族的成员。这套工具最早是 Google 的工程师作为 GCC 和 Clang 的补丁实现的,后来成了主流编译器的标配。家族里一共五个成员,每个盯着一类特定的错误:
+
+| 工具 | 编译开关 | 抓什么 | 典型开销 |
+|------|---------|--------|---------|
+| **ASan**(AddressSanitizer) | `-fsanitize=address` | 越界读写、use-after-free、double-free、栈/全局越界 | 2-4x 减速 |
+| **LSan**(LeakSanitizer) | `-fsanitize=leak` | 内存泄漏(程序退出时未释放的堆内存) | 几乎零开销 |
+| **MSan**(MemorySanitizer) | `-fsanitize=memory` | 读未初始化的内存(use of uninitialized value) | ~3x 减速 |
+| **TSan**(ThreadSanitizer) | `-fsanitize=thread` | 数据竞争(data race)、死锁 | 5-15x 减速 |
+| **UBSan**(UndefinedBehaviorSanitizer) | `-fsanitize=undefined` | 未定义行为(有符号溢出、空指针解引用、位移越界等) | 可配置,多数子项开销很小 |
+
+五兄弟里,ASan 是主力,日常开发几乎必备;LSan 默认会随 ASan 一起启用(GCC/Clang 在支持的环境下);MSan 只在 Clang 上完整可用、且必须**全程序**都编译成 MSan 版本(连 libc 都要 MSan 版,否则误报满天飞);TSan 专门盯并发,我们在 vol5 的 [并发程序调试技巧](../vol5-concurrency/ch08-debug-testing-perf/01-debugging-concurrency.md) 里专门讲过;UBSan 是个"补刀手",开销小、可以和别的组合。
+
+### ASan 和 TSan 互斥:一条铁律
+
+这五个工具不是随便组合的。最重要的约束是:**ASan 和 TSan 不能同时启用**。ASan 要用自己的影子内存布局,TSan 也要用自己的,两套机制会打架。编译器会在编译期直接拒绝你:
+
+```text
+$ g++ -std=c++20 -fsanitize=address,thread -g conflict.cpp -o conflict
+cc1plus: error: '-fsanitize=thread' is incompatible with '-fsanitize=address'
+```
+
+报错直白。这条约束的工程后果是:一个项目的 CI 里,内存错误检测和数据竞争检测得分**两套独立的构建**——一套开 ASan、一套开 TSan,各自跑一遍测试。vol5 的 TSan 篇专门讲过这个"双构建"实践,这里我们记住结论就好。
+
+至于 MSan,它跟 ASan/TSan 都不兼容(它要求所有代码都"干净"地走自己的未初始化追踪),而且只支持 Clang,所以实际项目里用得最少。LSan 和 UBSan 是两个"百搭"——LSan 几乎零开销可以常驻,UBSan 的多数子项也能跟 ASan 一起开。
+
+## 实测:ASan 抓三类典型错误
+
+光讲原理不过瘾。我们把 C++ 里最容易翻车的三类内存错误各写一个,让 ASan 一个个抓出来。下面三段都是本机真跑出来的。
+
+### 堆 use-after-free
+
+智能指针能挡住大部分 UAF,但只要项目里还有裸指针、还有 C 风格 API,这个洞就堵不完。看一个最小例子——把一个 `unique_ptr` 释放后,还拿着它先前吐出来的裸指针去读:
+
+```cpp
+// uaf.cpp —— use-after-free
+// Platform: host Standard: C++20
+// 编译: g++ -std=c++20 -O1 -fsanitize=address -g uaf.cpp -o uaf
+#include
+#include
+
+int main()
+{
+ auto p = std::make_unique(42);
+ int* raw = p.get(); // 拿到裸指针
+ p.reset(); // 这里释放 —— raw 立刻变成悬空指针
+ std::printf("悬空指针读到的值: %d\n", *raw); // use-after-free
+}
+```
+
+带 ASan 跑:
+
+```text
+=================================================================
+==37082==ERROR: AddressSanitizer: heap-use-after-free on address 0x7a948abe0010 ...
+READ of size 4 at 0x7a948abe0010 thread T0
+ #0 0x... in main uaf.cpp:12
+
+0x7a948abe0010 is located 0 bytes inside of 4-byte region [0x7a948abe0010,0x7a948abe0014)
+freed by thread T0 here:
+ #0 0x... in operator delete(void*, unsigned long) (/usr/lib/libasan.so.8+0x12e4c1)
+ ...
+ #4 0x... in main uaf.cpp:11
+
+previously allocated by thread T0 here:
+ #0 0x... in operator new(unsigned long) (/usr/lib/libasan.so.8+0x12d341)
+ ...
+ #2 0x... in main uaf.cpp:9
+
+SUMMARY: AddressSanitizer: heap-use-after-free uaf.cpp:12 in main
+```
+
+这份报告是 ASan 最有价值的地方。它不仅告诉你"第 12 行那次读是 use-after-free",还同时给出**这条内存的两段历史**:在 `uaf.cpp:9` 由 `make_unique` 分配、在 `uaf.cpp:11` 由 `reset` 释放。盯着这两行,bug 的因果链就完整了——这正是 quarantine + redzone 机制的价值:已释放的内存被标记成 `fd` 而不是立刻回收,所以后续误读能撞上。
+
+shadow dump 里那个 `[fd]` 就是铁证:
+
+```text
+=>0x7a948abe0000: fa fa[fd]fa fa fa fa fa ...
+```
+
+`fd` = freed heap region。这就是 ASan "账本"的功劳。
+
+### 全局缓冲区越界
+
+全局/静态变量同样有 redzone 保护。一个全局数组越界访问,ASan 照抓不误:
+
+```cpp
+// global_oob.cpp —— 全局数组越界
+// 编译: g++ -std=c++20 -O1 -fsanitize=address -g global_oob.cpp -o global_oob
+#include
+int g[4] = {1, 2, 3, 4};
+int main() { std::printf("g[5] = %d\n", g[5]); }
+```
+
+```text
+==38356==ERROR: AddressSanitizer: global-buffer-overflow on address 0x63ca65acd074 ...
+SUMMARY: AddressSanitizer: global-buffer-overflow global_oob.cpp:5 in main
+```
+
+报错类型明确标成 `global-buffer-overflow`。ASan 把栈、堆、全局三类区域用不同的 redzone 编码区分开(`f1`/`f2` 栈、`fa` 堆、`f9` 全局),所以你能一眼看出越界发生在哪类存储上。
+
+::: warning 关于「Clang 11 才支持全局 OOB」这个说法
+有些老资料会说"ASan 检测全局变量越界需要 Clang 11 以上"。这个说法的历史背景是:早期 ASan 对全局变量的 redzone 支持不完整,Clang 11 引入了 ODR 指示器(`-fsanitize-address-use-odr-indicator`)等改进才把全局检测做扎实。但**今天**——GCC 8.3+ / Clang 主流版本——对全局越界的检测都是默认开、开箱即用的,本机 GCC 16.1.1 上面这个例子就是默认配置一把抓到的。所以这条"版本门槛"对现在的工具链已经过时,别被老资料带歪。
+:::
+
+### 泄漏:LSan 在退出时收尾
+
+最后看内存泄漏。LSan 的工作方式和前几个不一样——它不在运行中报错,而是在 `main` 返回、程序即将退出时,扫描所有还"活着"的堆分配,把没人引用的、没有对应释放的全标出来。我们写一个故意泄漏的最小例子:
+
+```cpp
+// leak.cpp —— 故意泄漏
+// Platform: host Standard: C++20
+// 编译: g++ -std=c++20 -O1 -fsanitize=address -g leak.cpp -o leak
+#include
+#include
+int main()
+{
+ int* p = (int*)std::malloc(sizeof(int) * 4); // 拿了堆内存
+ p[0] = 42;
+ std::printf("ptr = %p\n", (void*)p); // 让指针逃逸,防止整段被优化器删掉
+ // 没有 free,程序退出时 p 指向的内存泄漏
+}
+```
+
+带 ASan 跑(GCC 16.1.1 / WSL2,LSan 默认随 ASan 启用):
+
+```text
+ptr = 0x730c4cbe0010
+=================================================================
+==364484==ERROR: LeakSanitizer: detected memory leaks
+
+Direct leak of 16 byte(s) in 1 object(s) allocated from:
+ #0 0x... in malloc (/usr/lib/libasan.so.8+0x12c161)
+ #1 0x... in main leak.cpp:8
+ ...
+
+SUMMARY: AddressSanitizer: 16 byte(s) leaked in 1 allocation(s).
+```
+
+注意:报告是在 `main` 返回**之后**才打出来的——这正是 LSan "退出时收尾"的工作方式。vol1 的 [动态内存管理](../vol1-fundamentals/ch12/02-new-delete.md) 也给过一个等价示例,可对照。
+
+::: warning LSan 的"静默退出"陷阱
+LSan 在主流 Linux 环境(GCC 16.1.1 / Clang 22)下默认随 ASan 启用,上面这个例子本机能稳定抓到。但要小心一个真实陷阱:**泄漏只有在进程正常退出时才会被扫描**。如果你的程序是被 `SIGKILL` 干掉的、或调了 `_exit` 绕过 `atexit` 钩子、或在某些容器/沙箱里 LSan 的退出钩子没跑起来,泄漏报告就会**静默消失**——程序看起来"没报错",但其实是 LSan 压根没机会扫描。
+
+排查办法:确认进程是正常 return 退出的;需要时显式 `ASAN_OPTIONS=detect_leaks=1` 强制开;长期运行的服务(根本不退出)用不了 LSan 这种"退出时收尾"的模型,得改用 Valgrind massif 或堆采样。别假设 LSan "没报就是没漏"。
+:::
+
+## UBSan:把"未定义行为"从沉默变成报错
+
+讲完 ASan 家族的主力,我们看补刀手 UBSan。C/C++ 有个让人血压拉满的特性:**未定义行为(UB)**。编译器对 UB 的态度是"既然标准没规定会发生什么,我就当它不会发生,放心优化"。后果是,有符号整数溢出、位移越界、空指针解引用这些错误,程序**经常看起来跑得好好的**——直到某天开了 `-O2`、换了编译器版本,优化器基于"这段不会溢出"的假设做了激进变换,程序突然算出离谱的结果。
+
+UBSan 的思路是:在每个可能产生 UB 的操作旁边插一个运行时检查,一旦真的发生 UB,立刻打印 `runtime error: ...` 报告(默认不中止程序,可以配置成中止)。开销很小,很多子项可以和 ASan 一起常驻。
+
+看一个最小例子,一次塞进去三种经典 UB:
+
+```cpp
+// ubsan.cpp —— UBSan 捕获未定义行为
+// Platform: host Standard: C++20
+// 编译: g++ -std=c++20 -O1 -fsanitize=undefined -g ubsan.cpp -o ubsan
+#include
+#include
+
+int main()
+{
+ int arr[4]{1, 2, 3, 4};
+ int idx = 10;
+ std::printf("越界下标 arr[10] = %d\n", arr[idx]); // 下标越界
+
+ int max = std::numeric_limits::max();
+ std::printf("有符号溢出: %d\n", max + 1); // 有符号整数溢出
+
+ int shift = 32;
+ std::printf("左移 32 位: %d\n", 1 << shift); // 位移量 >= 位宽
+}
+```
+
+带 UBSan 跑:
+
+```text
+ubsan.cpp:11:55: runtime error: index 10 out of bounds for type 'int [4]'
+ubsan.cpp:11:16: runtime error: load of address 0x7ffe8a0525c8 with insufficient space for an object of type 'int'
+ubsan.cpp:14:16: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
+ubsan.cpp:17:42: runtime error: shift exponent 32 is too large for 32-bit type 'int'
+```
+
+三个 UB 全被抓到,精确到 `文件:行号:列号`。UBSan 覆盖的 UB 清单很长,常见的包括:
+
+- **算术类**:有符号整数溢出/下溢、除以零;
+- **位移类**:位移量为负或大于等于位宽、左移把符号位改掉;
+- **内存/指针类**:空指针解引用、对齐错误的内存访问、对象大小不匹配(通过错误类型的指针访问);
+- **数组类**:下标越界(`-fsanitize=bounds`,这个和 ASan 的越界检测有重叠但侧重不同——ASan 看 redzone,UBSan 看编译期已知的数组尺寸)。
+
+UBSan 的开销取决于你开了哪些子项。`-fsanitize=undefined` 是一组默认子项的集合,多数都很轻;真正贵的是 `-fsanitize=integer`(无符号溢出也算,开销大、误报多,生产慎用)。日常建议:`-fsanitize=undefined` 跟 ASan 一起开,成本低、收益高。
+
+## 选型:面对一个内存 bug,该上哪个工具
+
+到这里五兄弟都亮相过了。问题来了——当你真坐到一个诡异的 bug 面前,该按什么顺序选工具?我们按"症状"来定位:
+
+- **一跑就崩 / 段错误 / 偶发崩溃**:先开 ASan 跑复现测试。越界、UAF、double-free 这三类是段错误最常见的原因,ASan 一把抓。
+- **结果偶发性错误 / 跨函数的诡异值**:怀疑 UAF 或数据竞争。先 ASan 排除 UAF,ASan 没报再单独构建一份 TSan 版本查数据竞争(别忘了两者互斥,不能同时开)。
+- **算出来的数离谱 / `-O2` 下行为变了**:几乎可以锁定 UB,直接 UBSan。
+- **读了"看起来正常"的垃圾值、行为依赖未初始化**:MSan(注意只 Clang、要全程序编译)。
+- **进程吃内存越来越多 / 怀疑泄漏**:LSan(退出时报告),或长期运行的服务用 Valgrind massif / 堆采样的方式。
+
+一个工程化的实践是:**CI 里常驻两套构建**——`ASan+UBSan` 一套、`TSan` 一套,每次提交都跑。开销可以接受(ASan+UBSan 在 2-4 倍量级),换来的是把"上线后偶发崩溃"这类最贵的 bug,在还没出门的时候摁住。
+
+::: warning ASan 不是银弹
+ASan 很强,但它有几个绕不开的限制,必须心里有数。
+
+第一,**它只能抓"实际执行到"的路径**。CTI 是运行时检测,代码没跑到就不会触发检查。如果你的测试覆盖率不够、某条越界路径从来没被触发过,ASan 抓不到——这正是为什么 ASan 要配合好的测试用例、甚至 fuzzing(模糊测试)一起用,fuzzing 负责把罕见路径跑出来,ASan 负责在这些路径上一旦出错就报。
+
+第二,**只抓内存类错误**。逻辑错误(算错了)、并发错误(数据竞争)、整数溢出这种 UB,ASan 不管——后者归 UBSan,前者归 TSan。别指望一个 flag 解决所有问题。
+
+第三,**生产环境别开**。2-4 倍减速和额外内存,在生产负载下是灾难。ASan/UBSan/TSan 都是**开发/测试/CI 阶段**的工具,发布构建里一定要去掉这些 flag。
+
+第四,**它有假阳性边界**。某些自定义的栈展开机制(`swapcontext`、`vfork`)会让 ASan 的影子区判断出错,报假阳性。报告里那句 `HINT: this may be a false positive if your program uses some custom stack unwind mechanism` 就是在提醒这个。
+:::
+
+## 小结
+
+我们这一篇从 Heartbleed 那个让全世界心惊的 over-read 洞出发,把 ASan 工具家族拆了个底朝天。几个关键结论收一下:
+
+- **ASan 三件套**:编译期插桩(CTI)改写每次内存访问、shadow memory 用 1:8 影子字节记录每 8 字节内存的可访问状态、运行时库替换 `new`/`delete` 并用 quarantine 隔离已释放内存。开销 2-4 倍减速,远低于 Valgrind 的 20-50 倍,所以能在 CI 里常驻。
+- **shadow memory 的编码**:`00` 可访问、`01`-`07` 部分可访问、`fa` 堆 redzone、`fd` 已释放、`f1`/`f2` 栈 redzone、`f9` 全局 redzone。ASan 报告末尾的 shadow dump 就是这套编码的直观呈现。
+- **工具家族**:ASan(越界/UAF)、LSan(泄漏)、MSan(未初始化读,Clang only)、TSan(数据竞争)、UBSan(UB)。**ASan 和 TSan 互斥**,CI 里分两套构建。
+- **UBSan 是补刀手**:开销小、可和 ASan 共存,把有符号溢出、位移越界、空指针解引用这些"沉默的 UB"变成显式 `runtime error`。
+- **选型口诀**:段错误先 ASan,偶发错误 ASan 排 UAF 再 TSan 查竞争,算术离谱上 UBSan,内存增长查 LSan。生产环境一律关闭。
+
+真正的内存安全不是靠工具抓出来的,是靠 RAII、智能指针、`std::span`、范围 `for` 这些**从语法层面就让你写不出越界和悬空**的手段守住的——那些是 vol1 和 vol3 的主题。ASan 这套工具的价值在于:在你还没把所有裸指针都替换掉、在第三方 C 库还没被现代封装包住的过渡期,它是那道"最后一道防线",让那些潜伏的内存 bug 在开发阶段就显形,而不是拖到线上凌晨三点炸给你看。
+
+## 参考资源
+
+- [AddressSanitizer · google/sanitizers Wiki](https://github.com/google/sanitizers/wiki/AddressSanitizer) —— ASan 官方说明,shadow memory 机制与 1:8 映射、2x 开销的权威出处
+- [Clang: AddressSanitizer](https://clang.llvm.org/docs/AddressSanitizer.html) —— Clang 侧 ASan 文档,含 `-fsanitize-address-use-odr-indicator` 等全局检测演进
+- [Clang: ThreadSanitizer](https://clang.llvm.org/docs/ThreadSanitizer.html) —— TSan 文档,ASan↔TSan 互斥的出处(详见 vol5 并发调试篇)
+- [Clang: UndefinedBehaviorSanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html) —— UBSan 各子项清单与开销
+- [Valgrind User Manual](https://valgrind.org/docs/manual/manual.html) —— DBI 方法与 Memcheck/Helgrind,20-50x 开销的对照
diff --git a/documents/vol6-performance/11-memory-safety-asan-valgrind.md b/documents/vol6-performance/11-memory-safety-asan-valgrind.md
new file mode 100644
index 000000000..92cc6708e
--- /dev/null
+++ b/documents/vol6-performance/11-memory-safety-asan-valgrind.md
@@ -0,0 +1,486 @@
+---
+title: Valgrind 与 ASan 对照:JIT 解释 vs 编译期插桩
+description: 把 Valgrind 五件套(memcheck/callgrind/cachegrind/helgrind+drd/massif)的职责拆开,用 ASan 真实编译运行六类经典内存错误,讲透「动态二进制翻译」与「编译期影子内存插桩」两条路线的本质差别
+chapter: 6
+order: 11
+platform: host
+difficulty: advanced
+cpp_standard:
+ - 11
+ - 14
+ - 17
+ - 20
+tags:
+ - host
+ - cpp-modern
+ - advanced
+ - 内存安全
+ - 调试
+ - 内存管理
+reading_time_minutes: 28
+prerequisites:
+ - 动态内存管理(new/delete 与智能指针)
+ - C 语言动态内存(malloc/free 与 valgrind 速览)
+related:
+ - ASan 工具家族与内存安全:shadow memory、Heartbleed 与 sanitizer 选型
+ - 并发程序调试技巧(TSan / Helgrind 深入)
+ - 动态内存管理
+---
+
+# Valgrind 与 ASan 对照:JIT 解释 vs 编译期插桩
+
+> PS: 这部分内容由笔者大学期间的笔记迁移而来,关键结论均已用本机 GCC 16.1.1 + valgrind 3.25.1 实编实跑核对过;若仍有疏漏,欢迎 Issue 或 PR。
+
+先说一个我们大概都干过的事:一段 C++ 代码本地跑得好好的,一上线就偶发崩溃,或者内存 RSS 一路涨到被 OOM Killer 收掉。你回头去翻代码,`new` 配 `delete` 看着都对,越界也就差那么一两个字节——光读代码根本读不出问题。这种 bug,肉眼 debug 是没希望的,必须靠工具去「看见」内存的每一次访问。
+
+这篇我们要做的,是把抓内存错误的工具按「实现路线」分成两大派,把它们拆开跑一遍。一派是 **Valgrind**——老牌的、在程序外面套一层「虚拟 CPU」去解释执行的 JIT 方案;另一派是 **AddressSanitizer(ASan)**——编译期就把检查代码插进你程序里、靠「影子内存」记账的方案。源头的旧笔记只讲了 Valgrind,对 ASan 一字未提,但这恰恰是现在工程里更常用的那条路。这篇就把这个缺口补上,并把两条路线摆在一起对照。
+
+## 一、两类内存错误,和「为什么读代码读不出来」
+
+在动手用工具之前,先把要抓的「敌人」分清楚。内存错误大致两类,抓它们的难度天差地别:
+
+**第一类:确定性的越界 / use-after-free / double-free。** 这类错误的特征是「访问了一块不该访问的地址」。它危险,但相对好抓——只要工具能标记「哪块内存是合法的、哪块不是」,越界那一刻就能当场报出来。`char buf[8]; buf[8] = 'x';` 这种 off-by-one、`free(p); return *p;` 这种悬垂指针,都属于这一类。
+
+**第二类:未初始化读取 / 内存泄漏。** 这类更阴险。未初始化读取是「地址合法、但值是垃圾」——程序不会崩,只是悄悄算错;内存泄漏是「地址一直合法、只是永远不归还」——程序也不崩,只是 RSS 慢慢涨。这俩你不能靠「合法地址表」抓,得靠另一套机制:Valgrind 给每个字节维护「这个值是不是已经初始化过」的标记,ASan 的泄漏检测(LSan)则在程序退出时扫一遍堆,看还有没有「分配了但没人指着」的块。
+
+读代码读不出来的根本原因,是这两类错误都**取决于运行时的内存状态**,而不取决于代码的字面写法。你光看 `*p`,根本不知道这一刻 `p` 指的内存是活的还是死的、是初始化过的还是垃圾。这正是为什么我们需要工具去「记录」每一次分配、每一次释放、每一次读写——把运行时的内存状态变成一份事后可查的账本。
+
+那「记录」这件事,Valgrind 和 ASan 是两条完全不同的实现路线。先把结论摆前面,后面再逐个拆。
+
+| 维度 | Valgrind(memcheck) | AddressSanitizer |
+|------|---------------------|------------------|
+| 怎么记 | 动态二进制翻译:运行时把每条机器指令翻译成带检查的版本 | 编译期插桩:编译时就在每次访存前后插检查代码 |
+| 要不要重新编译 | **不用**,拿现成二进制就能跑 | **要**,必须用 `-fsanitize=address` 重新编 |
+| 运行时开销 | 慢 20~50 倍,内存 2 倍以上(官方原话) | 慢约 2 倍,内存约 3 倍 |
+| 平台 | Linux/macOS(FreeBSD/Solaris),x86/ARM 等 | GCC/Clang/MSVC 全平台,含 Windows |
+| 谁来抓未初始化读取 | memcheck 原生能抓(V-bit) | ASan **抓不了**,得另上 `-fsanitize=memory`(MSan,Clang 专属) |
+| 抓栈上越界 | 能(但要 `-tool=memcheck` 全套) | 默认抓栈/全局红区,`detect_stack_use_after_return` 抓栈返回后访问 |
+
+这张表你先有个印象。接下来我们从「源头的痛点」开始,先看 Valgrind 这条路是怎么走通的。
+
+## 二、Valgrind:套一层「虚拟 CPU」去 JIT 解释你的程序
+
+### 2.1 它到底在干什么
+
+Valgrind 本质上是一个**动态二进制翻译(dynamic binary translation, DBT)框架**。它不是个普通的检测库,而是把你的程序整个塞进一个「虚拟 CPU」里跑。你敲 `valgrind ./myprog`,真实发生的事是:Valgrind 拦截你的每一条机器指令,把它**即时翻译**成「干原来的活 + 顺带记录内存状态」的一串新指令,然后才执行。所以你的程序不是直接在 CPU 上跑的,而是在 Valgrind 的核心(core)里被「解释」着跑。
+
+这就是它那句著名的副作用的来源——**慢 20 到 50 倍**,内存占用翻倍以上。Valgrind 官方手册原话就写着:
+
+> Programs running under Valgrind run significantly more slowly, and use much more memory -- e.g. more than twice as much as normal under the Memcheck tool.
+
+换算一下:一个跑 1 秒的程序,塞进 memcheck 可能要跑半分钟。所以 Valgrind 不是给你做日常开发时挂着的,是给你「这程序真的有内存 bug,我专门拿一段时间来揪它」用的。
+
+这个 JIT 解释的架构有个巨大的好处,也是 Valgrind 至今没被淘汰的根本原因:**不用重新编译**。你手头有一个十年前的、连源码都找不全的二进制,怀疑它泄漏——`valgrind ./老古董` 一敲就能跑。ASan 做不到这点,ASan 必须从源码重新编一遍。这是两条路线最硬的差别。
+
+### 2.2 五件套:一个框架,五个工具
+
+Valgrind 的精髓是「框架 + 工具」。core 负责翻译和调度,具体「记什么、报什么」交给可插拔的 tool。`--tool=` 选哪个,就是选哪副「检查眼镜」。让我们瞧瞧,可以看到手册里列的核心工具有这些:
+
+**Memcheck**——内存错误检测器,Valgrind 的默认工具,也是绝大多数人说的「用 Valgrind 查内存」时实际用的那个。它抓的全集是(引自手册 4.1):访问不该访问的内存(堆块越界、栈顶越界、释放后访问)、使用未初始化的值、错误的释放(double-free、`malloc` 配 `delete` 这类不匹配)、`memcpy` 源目的重叠、传给分配函数「可疑」的负数 size、`realloc` 传 0、对齐值不是 2 的幂、以及内存泄漏。一句话:memcheck 把 C/C++ 程序里最常见的内存错误几乎一网打尽。
+
+**Callgrind**——调用图 + 缓存/分支预测 profiler。它不需要你在编译时加特殊选项(但推荐 `-g`),运行结束时把分析数据写进一个文件,再用 `callgrind_annotate` 转成人能读的格式。定位「哪个函数被调了多少次、调用关系长啥样」用。
+
+**Cachegrind**——缓存 profiler。它模拟 CPU 的 I1/D1/L2 缓存,精确指出程序里 cache miss 和命中的位置,能给你每行代码、每个函数、每个模块产生了多少次 miss、多少条指令。想压缓存性能用它。
+
+**Helgrind 和 DRD**——这俩都是**线程错误检测器**,抓数据竞争、锁顺序不一致、POSIX 线程 API 误用。源笔记把 Helgrind 写成「仍然处于实验阶段」,这个说法**早就过时了**——2026 年的官方手册里 Helgrind 和 DRD 都是正式列出的稳定工具,各有独立的章节(手册第 7、8 章),不是实验功能。顺带提一句:源笔记只提了 Helgrind,**漏了 DRD**——它俩目的相同(抓线程 bug)但算法不同,DRD 通常更快、对某些场景(比如大量小对象、Boost.Thread、OpenMP)支持更好。线程错误这块我在卷五的[并发程序调试技巧](../vol5-concurrency/ch08-debug-testing-perf/01-debugging-concurrency.md)里专门讲过 TSan/Helgrind 的实战,这篇不重复,记住「线程类 bug 找 helgrind/drd、或更现代的 TSan」就行。
+
+**Massif**——堆 profiler。测程序在堆上到底吃了多少内存,给你堆块、堆管理结构、栈的增长曲线。想给程序「瘦身」、找 RSS 大户用它。
+
+> **一个容易被忽略的分工点**:memcheck 抓「对错」(这块内存能不能访问、有没有初始化),callgrind/cachegrind/massif 抓「快慢/多少」(性能和用量)。新人常把它们混为一谈,以为 Valgrind 就是查内存泄漏的——其实那只是 memcheck 一个工具的活。性能分析那几个工具(callgrind/cachegrind/massif)和 ASan 完全不在一个赛道,ASan 不碰性能 profiling。
+
+### 2.3 memcheck 的双表原理:A-bit 与 V-bit
+
+memcheck 凭什么能抓住那么多种内存错误?关键就在它维护了两张覆盖整个进程地址空间的「影子表」。手册 4.5 节讲得很清楚:
+
+**Valid-Address 表(A-bit)。** 进程地址空间的每一个字节,都对应 1 个 bit,记录「这个地址当前能不能被读写」。malloc 出来一块、A-bit 就把那几个字节标记成「有效」;free 掉、标记翻回「无效」。当指令要去读写某个字节时,先查它的 A-bit——如果显示无效,就是非法访问,memcheck 当场报错。这层抓住了:越界、use-after-free、访问未分配区域。
+
+**Valid-Value 表(V-bit)。** 进程地址空间的每一个字节,对应 8 个 bit;CPU 的每个寄存器也对应一个 bit 向量。它们记录「这个值是不是已经被初始化过了」。malloc 出来的内存,V-bit 全是「未初始化」;一旦有指令往里写了确定的值,对应字节的 V-bit 翻成「已初始化」。关键设计是:**V-bit 会跟着值「传播」**——你把一个未初始化的值从内存读进寄存器,V-bit 也跟着搬到寄存器里;你拿它做运算,结果的 V-bit 也是「未初始化」。但 memcheck 不会一读到未初始化值就报,它只在「这个值被拿去影响程序输出、或被用来生成地址」的那一刻才报。这个延迟是有意为之的——避免满屏误报。
+
+把两张表合起来看就明白了:A-bit 管「地址合不合法」,V-bit 管「值干不干净」。前者抓越界/UAF,后者抓未初始化读取。double-free 和 alloc-dealloc 不匹配则靠 memcheck 自己维护的「这块内存是用什么分配器申请的」账本去比对。
+
+这套「每字节都记账」的机制,代价就是前面说的内存翻倍——A-bit 和 V-bit 本身就要占地方。
+
+## 三、ASan:编译期插桩 + 影子内存
+
+### 3.1 思路完全反过来
+
+ASan 的实现路线和 Valgrind 正好反过来。它**不**在程序外面套虚拟 CPU,而是**在编译的时候**就把检查代码插进你的程序里。你加 `-fsanitize=address`,编译器就会在你每一次读/写内存的前后,插一小段代码:这段代码会查一张「影子内存(shadow memory)」表,判断这次访问合不合法,不合法就报错并 abort。
+
+所以 ASan 的检查是「程序自己查自己」,而不是「外面的虚拟 CPU 替它查」。这就解释了两条路线开销的巨大差距:ASan 只在被插桩的那几次访存上多花几条指令,没有「翻译整条指令流」的成本,所以**只慢约 2 倍**(Valgrind 是 20~50 倍);代价是必须重新编译,且检查只覆盖被插桩的代码——动态加载的、没带 ASan 编译的第三方 .so,它管不到(Valgrind 能,因为它在指令层全盘拦截)。
+
+### 3.2 影子内存:8 字节 → 1 字节的编码
+
+ASan 的核心机制是影子内存(shadow memory 的完整拆解见本卷[ASan 工具家族](./10-asan-family-and-memory-safety.md),那里还讲了它当年怎么堵 Heartbleed 这种越界读漏洞)。它把进程的整个地址空间按 8 字节一组映射到一张影子表里——每 8 个应用字节对应 1 个影子字节。那个影子字节的值有明确含义,我直接把本机跑出来的图例贴给你(后面那段输出是真实的):
+
+```text
+Shadow byte legend (one shadow byte represents 8 application bytes):
+ Addressable: 00
+ Partially addressable: 01 02 03 04 05 06 07
+ Heap left redzone: fa
+ Freed heap region: fd
+ Stack left redzone: f1
+ Stack mid redzone: f2
+ Stack right redzone: f3
+ Stack after return: f5
+ Stack use after scope: f8
+ Global redzone: f9
+ Global init order: f6
+ Poisoned by user: f7
+ Container overflow: fc
+ Array cookie: ac
+ Intra object redzone: bb
+ ASan internal: fe
+ Left alloca redzone: ca
+ Right alloca redzone: cb
+```
+
+翻译一下这套编码的精妙之处:
+
+- 影子字节是 `00`:这 8 字节全部可访问;
+- 是 `01`~`07`:只有前 N 字节可访问,剩下的是越界红区——这正是 ASan 抓 off-by-one 的原理,它在每个堆块、栈帧、全局变量周围都铺了一圈「红区」,红区的影子字节标记成 `fa`/`f9` 等,你一踩进红区,插桩代码查影子字节发现不是「可访问」,立刻报错;
+- 是 `fd`:这块内存已经 free 了——再访问就是 use-after-free,当场抓住。
+
+也就是说,ASan 不是像 memcheck 那样「逐字节记账地址合法性」,而是「在合法区域外围铺红区,用红区来界定边界」。这套机制对越界和 UAF 极其有效,但**它没有 V-bit**——所以 ASan 抓不了未初始化读取。这块缺口得用 MSan(MemorySanitizer,`-fsanitize=memory`)补,而 MSan 只有 Clang 实现,**GCC 到 16.1.1 都不支持 `-fsanitize=memory`**(本机实测 `unrecognized argument`)。这是 ASan 路线相对 memcheck 的一个真实短板。
+
+> **踩坑预警**:ASan 和别的 sanitizer 是「一次只能开一类」的关系。`-fsanitize=address` 和 `-fsanitize=thread`(TSan)**不能同时开**——它们对影子内存的布局假设不同,混用会直接报错或行为异常。所以抓内存错误时开 ASan,抓并发数据竞争时单独开 TSan,别想着「一把梭」。线程错误该怎么查,见[卷五的并发调试篇](../vol5-concurrency/ch08-debug-testing-perf/01-debugging-concurrency.md)。
+
+## 四、上手跑一跑:六类经典错误,ASan 真实输出
+
+光讲原理不过瘾。我们把源笔记里那六类「全是截图、没源码」的经典错误,全部用真代码写出来,用 `g++ -std=c++20 -O0 -g -fsanitize=address,undefined` 在本机(GCC 16.1.1)编译运行。下面每一段输出都是我**真跑出来的**,不是手编的。
+
+先把六类错误装进同一个程序:
+
+```cpp
+// cases.cpp — 六类经典内存错误,逐个用 ASan 复现
+// 编译: g++ -std=c++20 -O0 -g -fsanitize=address,undefined cases.cpp -o cases
+// 运行: ./cases <1..6> 不传参则只跑内存泄漏
+#include
+#include
+
+// 1. 使用未初始化内存(ASan 抓不到,要 MSan)
+int case_uninit() {
+ int* p = (int*)malloc(sizeof(int)); // 内容是垃圾
+ int v = *p; // 读到垃圾值,但地址合法
+ free(p);
+ return v;
+}
+
+// 2. use-after-free
+int case_uaf() {
+ int* p = (int*)malloc(sizeof(int));
+ *p = 42;
+ free(p);
+ return *p; // 读已释放内存
+}
+
+// 3. 堆缓冲区越界(尾部读写)
+int case_oob() {
+ int* a = (int*)malloc(4 * sizeof(int)); // 只有 a[0..3]
+ a[4] = 99; // 第 5 个元素越界
+ int r = a[4];
+ free(a);
+ return r;
+}
+
+// 4. 内存泄漏(忘记 free)
+void case_leak() {
+ int* p = (int*)malloc(sizeof(int));
+ *p = 7; // 故意不 free
+}
+
+// 5. malloc 配 delete(分配/释放不匹配)
+void case_mismatch() {
+ int* p = (int*)malloc(sizeof(int));
+ *p = 5;
+ delete p; // malloc 该配 free
+}
+
+// 6. 双重释放
+void case_double_free() {
+ int* p = (int*)malloc(sizeof(int));
+ free(p);
+ free(p); // 第二次 free
+}
+
+int main(int argc, char** argv) {
+ if (argc < 2) { case_leak(); puts("done: leak only"); return 0; }
+ switch (atoi(argv[1])) {
+ case 1: printf("uninit=%d\n", case_uninit()); break;
+ case 2: printf("uaf=%d\n", case_uaf()); break;
+ case 3: printf("oob=%d\n", case_oob()); break;
+ case 4: case_leak(); puts("done leak"); break;
+ case 5: case_mismatch(); puts("done mismatch"); break;
+ case 6: case_double_free(); puts("done double-free"); break;
+ default: puts("usage: ./cases [1..6]"); break;
+ }
+ return 0;
+}
+```
+
+编译这行你记一下,后面每个 case 都用它:`g++ -std=c++20 -O0 -g -fsanitize=address,undefined cases.cpp -o cases`。`-g` 是为了让 ASan 报告里带行号,`-O0` 是别让优化把我们的越界访问优化掉(高优化级别下,`a[4]` 这种「写了立刻读」可能被折叠,ASan 仍能抓,但调试时 `-O0` 最干净)。
+
+### 4.1 用未初始化内存 —— ASan 的盲区
+
+先跑 case 1,看 ASan 的反应:
+
+```text
+$ ./cases 1
+uninit=-1094795586
+```
+
+**ASan 一声不吭**,程序正常返回了一个垃圾值(`-1094795586`)。这就是前面说的短板:这块内存地址是合法的(malloc 来的),ASan 的影子内存里它标成「可访问」,没有 V-bit 去判断「这个值有没有被初始化过」。这块错误 memcheck 能抓(靠 V-bit),ASan 抓不到——要抓得换 MSan(`-fsanitize=memory`,Clang 专属)。这是两条路线一个**实质性的能力差异**,不是谁更强,是各管一摊。
+
+### 4.2 use-after-free —— 红区当场咬住
+
+跑 case 2:
+
+```text
+$ ./cases 2
+=================================================================
+==44083==ERROR: AddressSanitizer: heap-use-after-free on address 0x799329de0010 ...
+READ of size 4 at 0x799329de0010 thread T0
+ #0 ... in case_uaf() /tmp/asand/cases.cpp:20
+ #1 ... in main /tmp/asand/cases.cpp:56
+ ...
+
+0x799329de0010 is located 0 bytes inside of 4-byte region [0x799329de0010,0x799329de0014)
+freed by thread T0 here:
+ #0 ... in free ...
+ #1 ... in case_uaf() /tmp/asand/cases.cpp:19
+ ...
+
+previously allocated by thread T0 here:
+ #0 ... in malloc ...
+ #1 ... in case_uaf() /tmp/asand/cases.cpp:17
+ ...
+
+SUMMARY: AddressSanitizer: heap-use-after-free /tmp/asand/cases.cpp:20 in case_uaf()
+```
+
+(上面把 build-id 等无关行省略了,关键信息全在。)你看 ASan 给了三段信息:**这次非法读发生在哪**(`case_uaf()` 第 20 行的 `return *p`)、**这块内存是哪里 free 的**(第 19 行)、**它最初是哪里 malloc 的**(第 17 行)。三段凑一起,整条「申请→释放→又访问」的因果链一目了然。这就是红区机制加上「free 后影子字节翻成 `fd`」的功劳——`free` 之后那块内存对 ASan 来说不再是「可访问」,再碰就报。
+
+### 4.3 堆缓冲区越界 —— 尾部红区
+
+跑 case 3(`a[4]` 越界,`a` 只开了 4 个 int):
+
+```text
+$ ./cases 3
+=================================================================
+==44191==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7288a7be0020 ...
+WRITE of size 4 at 0x7288a7be0020 thread T0
+ #0 ... in case_oob() /tmp/asand/cases.cpp:26
+ ...
+
+0x7288a7be0020 is located 0 bytes after 16-byte region [0x7288a7be0010,0x7288a7be0020)
+allocated by thread T0 here:
+ #0 ... in malloc ...
+ #1 ... in case_oob() /tmp/asand/cases.cpp:25
+ ...
+```
+
+`located 0 bytes after 16-byte region`——这块内存是 16 字节(4 个 int),访问点正好踩在它**结尾之后的第一个字节**,也就是尾部红区的起点。这就是 ASan 抓 off-by-one 的原理:malloc 返回的块后面紧跟一圈红区,红区的影子字节是 `fa`(heap left redzone,其实是堆块周围的 poison),`a[4]` 落进红区,插桩代码一查影子字节不是 `00`,当场报错。
+
+> **一个源笔记提到、但容易误解的点**:源笔记说「Valgrind 不检查静态分配数组」。这点对老 memcheck 是真的(栈/全局数组越界历史上是 memcheck 的弱项),但 **ASan 不是这样**——ASan 对栈数组、全局变量都铺红区(影子字节 `f1`~`f3` 是栈红区、`f9` 是全局红区),栈上数组越界它抓得很利索。所以「静态数组越界查不到」这条结论,只对 Valgrind 成立,对 ASan 不成立。别把两个工具的局限混为一谈。
+
+### 4.4 内存泄漏 —— LSan 在程序退出时扫堆
+
+跑 case 4(`./cases 4`,故意不 free):
+
+```text
+$ ./cases 4
+
+=================================================================
+==44296==ERROR: LeakSanitizer: detected memory leaks
+
+Direct leak of 4 byte(s) in 1 object(s) allocated from:
+ #0 ... in malloc ...
+ #1 ... in case_leak() /tmp/asand/cases.cpp:34
+ #2 ... in main /tmp/asand/cases.cpp:58
+ ...
+
+SUMMARY: AddressSanitizer: 4 byte(s) leaked in 1 allocation(s).
+```
+
+注意这个报错是 **`LeakSanitizer`**,不是 ASan 本体——LSan 是 ASan 默认捆绑的泄漏检测器,在**程序正常退出时**扫一遍整个堆,把「分配了但没有任何指针指向」的块揪出来。它报告的是「still reachable / definitely lost」这套分类里的「definitely lost」。这和 memcheck 的泄漏检测思路一致(都是退出时扫堆),只是 LSan 是 ASan 工具链的一部分。
+
+> **守护进程怎么办?** LSan 默认在程序 `exit` 时才扫,长跑的 daemon/服务进程不会自己退出。这时可以发信号让它中途 dump:`ASAN_OPTIONS=abort_on_error=0:detect_leaks=1` 配合 `kill`,或用 LSan 的 `__lsan_do_leak_check()` API 在代码里主动触发一次扫描。Valgrind 那边对应的办法是另一个终端 `kill` 掉 memcheck 进程让它输出(源笔记提过这招)。
+
+### 4.5 malloc 配 delete —— 分配/释放不匹配
+
+跑 case 5:
+
+```text
+$ ./cases 5
+=================================================================
+==44300==ERROR: AddressSanitizer: alloc-dealloc-mismatch (malloc vs operator delete) ...
+ #0 ... in operator delete(void*, unsigned long) ...
+ #1 ... in case_mismatch() /tmp/asand/cases.cpp:42
+ ...
+
+0x71a3249e0010 is located 0 bytes inside of 4-byte region [0x71a3249e0010,0x71a3249e0014)
+allocated by thread T0 here:
+ #0 ... in malloc ...
+ #1 ... in case_mismatch() /tmp/asand/cases.cpp:40
+ ...
+```
+
+`alloc-dealloc-mismatch (malloc vs operator delete)`——ASan 替每个分配记下了「是用谁申请的」,释放时一比对,`malloc` 配 `delete` 不匹配,当场报。memcheck 抓的是同一类(手册 4.2.5「freed with an inappropriate deallocation function」),两边能力对齐。
+
+> **平台差异提醒**:这个 `alloc-dealloc-mismatch` 检查在 Windows 上**默认是关的**(MSVC 的 ASan,因为 Windows 上 `delete` 和 `free` 经常实际等价)。Linux/macOS 默认开。如果你在 Windows 上发现这类错误没被抓,查一下 `ASAN_OPTIONS=alloc_dealloc_mismatch=1`。
+
+### 4.6 双重释放
+
+跑 case 6:
+
+```text
+$ ./cases 6
+=================================================================
+==44193==ERROR: AddressSanitizer: attempting double-free on 0x6d0d527e0010 in thread T0:
+ #0 ... in free ...
+ #1 ... in case_double_free() /tmp/asand/cases.cpp:49
+ ...
+
+0x6d0d527e0010 is located 0 bytes inside of 4-byte region [0x6d0d527e0010,0x6d0d527e0014)
+freed by thread T0 here:
+ #0 ... in free ...
+ #1 ... in case_double_free() /tmp/asand/cases.cpp:48
+ ...
+```
+
+`attempting double-free`——第一次 `free` 之后影子字节翻成 `fd`,第二次再 `free` 同一地址,ASan 发现它已经是 `fd` 状态(已释放),直接判定为 double-free。还贴心地告诉你「上次是在第 48 行 free 的」。
+
+### 4.7 额外彩蛋:栈上 use-after-return
+
+ASan 还能抓一个 memcheck 历史上很难抓的东西——**栈帧返回后被访问**(函数返回了,调用方却还持有指向函数内局部变量的指针)。这个要显式开:
+
+```cpp
+// suar2.cpp
+#include
+static int* g = nullptr;
+void stash() { int local = 0xc0ffee; g = &local; } // 把局部变量地址存出去
+int main() { stash(); return *g; } // local 已随 stash 返回而消失
+```
+
+```text
+$ g++ -std=c++20 -O0 -g -fsanitize=address suar2.cpp -o suar2
+$ ASAN_OPTIONS=detect_stack_use_after_return=1 ./suar2
+=================================================================
+==44702==ERROR: AddressSanitizer: stack-use-after-return on address 0x6da50b8f0020 ...
+READ of size 4 at 0x6da50b8f0020 thread T0
+ #0 ... in main /tmp/asand/suar2.cpp:4
+ ...
+
+Address 0x6da50b8f0020 is located in stack of thread T0 at offset 32 in frame
+ #0 ... in stash() /tmp/asand/suar2.cpp:3
+
+ This frame has 1 object(s):
+ [32, 36) 'local' (line 3) <== Memory access at offset 32 is inside this variable
+HINT: this may be a false positive if your program uses some custom stack unwind mechanism ...
+SUMMARY: AddressSanitizer: stack-use-after-return /tmp/asand/suar2.cpp:4 in main
+```
+
+注意那个地址 `0x6da50b8f0020`——它在进程地址空间里**很靠前**(不是普通栈区),因为开了 `detect_stack_use_after_return` 后,ASan 会把「可能被逃逸指针指向的局部变量」挪到一块专门的「假栈(fake stack)」上,函数返回时把那块假栈标成毒,再访问就报 `stack-use-after-return`(影子字节 `f5`)。默认它是关的,因为有一定开销和少量误报(看那个 HINT)。但这种「函数返回后还在用栈内存」的 bug 极其难查,值得知道有这招。
+
+## 五、Valgrind 怎么用:把上面那些错误喂给 memcheck
+
+讲完原理,我们把第四节那同一个 `cases.cpp`(这次不带 `-fsanitize=`,普通编译)塞进 valgrind 跑一遍,看 memcheck 对同一批错误是怎么报的——两套话术正面相对,对照才看得清。本机用的是 valgrind 3.25.1。
+
+先用 `-g` 编一个干净版(valgrind 不需要 ASan 那套插桩,但要 `-g` 才能在报告里给出行号):
+
+```bash
+g++ -std=c++20 -g -O0 cases.cpp -o cases_plain
+
+# 最常用:memcheck 全量查泄漏
+valgrind --tool=memcheck --leak-check=full ./cases_plain 4
+
+# 更狠:连 still reachable 也列出来 + 跟进子进程
+valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all --trace-children=yes ./cases_plain
+```
+
+几个关键参数:`--leak-check=full` 是完全检查泄漏(给出行号),`--show-leak-kinds=all` 连「still reachable」(还有指针指着、理论上还能 free)的块也列出来(老版本的 `--show-reachable=yes` 是它的别名,仍能用但已不推荐),`--trace-children=yes` 跟进 `fork`/`exec` 出来的子进程。换工具就改 `--tool=`:`callgrind`、`cachegrind`、`helgrind`、`drd`、`massif`。
+
+### 5.1 同一个 UAF,memcheck 这么报
+
+跑 case 2(就是第四节那个 use-after-free):
+
+```text
+$ valgrind --tool=memcheck --leak-check=full ./cases_plain 2
+==453796== Memcheck, a memory error detector
+...
+==453796== Invalid read of size 4
+==453796== at 0x40011E9: case_uaf() (cases.cpp:20)
+==453796== by 0x4001377: main (cases.cpp:56)
+==453796== Address 0x4ee9080 is 0 bytes inside a block of size 4 free'd
+==453796== at 0x48529EF: free (vg_replace_malloc.c:989)
+==453796== by 0x40011E4: case_uaf() (cases.cpp:19)
+==453796== Block was alloc'd at
+==453796== at 0x484F8A8: malloc (vg_replace_malloc.c:446)
+==453796== by 0x40011CA: case_uaf() (cases.cpp:17)
+uaf=42
+...
+==453796== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
+```
+
+注意行号——`cases.cpp:20` 读、`:19` free、`:17` malloc,和第四节 ASan 报的**一模一样**(ASan 那边也是 :20/:19/:17)。同一个 bug,两个工具各自定位到同一行,只是话术不同:
+
+- ASan 说 `heap-use-after-free` + `located 0 bytes inside of 4-byte region`;
+- memcheck 说 `Invalid read of size 4` + `Address ... is 0 bytes inside a block of size 4 free'd`。
+
+memcheck 还多了句 `Block was alloc'd at ... :17`——它靠 A-bit 账本记下了这块内存的「一生」(在哪申请、在哪释放、现在又被读),整条因果链一次给全,和 ASan 的「allocated by / freed by」三段式是同一个思路、两套措辞。
+
+### 5.2 泄漏:LEAK SUMMARY 对位 LSan
+
+跑 case 4(故意不 free):
+
+```text
+$ valgrind --tool=memcheck --leak-check=full ./cases_plain 4
+==453446== HEAP SUMMARY:
+==453446== in use at exit: 4 bytes in 1 blocks
+==453446== total heap usage: 3 allocs, 2 frees, 77,828 bytes allocated
+==453446== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
+==453446== at 0x484F8A8: malloc (vg_replace_malloc.c:446)
+==453446== by 0x400123D: case_leak() (cases.cpp:34)
+==453446== by 0x40013B5: main (cases.cpp:58)
+==453446== LEAK SUMMARY:
+==453446== definitely lost: 4 bytes in 1 blocks
+==453446== indirectly lost: 0 bytes in 0 blocks
+==453446== possibly lost: 0 bytes in 0 blocks
+==453446== still reachable: 0 bytes in 0 blocks
+==453446== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
+```
+
+`definitely lost: 4 bytes`——对位第四节 ASan 那边 LSan 的 `Direct leak of 4 byte(s)`。两边都是「程序退出时扫一遍堆」,只是 memcheck 把泄漏分成 `definitely lost / indirectly lost / possibly lost / still reachable` 四档(更细),LSan 默认只报 `Direct` 和 `Indirect` 两档。行号同样是 `:34`,和 ASan 一致。
+
+> **别再去下源码包手动编译。** 源笔记给的安装流程是 `tar -jxvf valgrind-3.12.0.tar.bz2 && ./configure && make && sudo make install`——`3.12.0` 是 2016 年的版本,**十年前了**,而且对现代内核/新 CPU 指令(比如较新的 AVX)支持差,跑新编的程序容易各种报错。现在直接用发行版包:Debian/Ubuntu 是 `apt install valgrind`、Fedora/RHEL 是 `dnf install valgrind`、Arch 是 `pacman -S valgrind`,装上就是 3.2x 版本(本机 3.25.1)。
+
+## 六、两条路线怎么选
+
+说了这么多,到底什么时候用哪个?给你一套实战决策:
+
+**默认用 ASan。** 日常开发、CI 里挂着的内存错误检测,首选 ASan——它快(慢 2 倍 vs 20~50 倍,CI 能忍),跨平台(Windows/macOS/Linux 通吃,MSVC 也支持),报告干净。现代 C++ 项目里,`-fsanitize=address,undefined` 几乎是调试构建的标配。我们卷一的[动态内存管理](../vol1-fundamentals/ch12/02-new-delete.md)里讲 ASan 抓泄漏、卷五的并发调试里讲 TSan,都是这条路上的工具。
+
+**这几类场景,必须上 Valgrind:**
+
+1. **只有二进制、没源码**,或者重新编译成本太高(比如巨大的遗留项目)。ASan 必须重新编,Valgrind 拿现成二进制就能跑。
+2. **要抓未初始化读取,但只有 GCC**。ASan 没 V-bit,MSan 又只有 Clang——用 GCC 编的项目要抓未初始化,memcheck 是现成的。
+3. **要性能 profiling**(callgrind/cachegrind/massif)。这几个工具 ASan 完全没有对应物,想看 cache miss、堆增长曲线、调用图,只有 Valgrind 这一套。
+4. **要全盘覆盖,包括没带 ASan 编译的第三方库**。Valgrind 在指令层拦截,连没源码的 .so 里的内存错误也能抓;ASan 只覆盖被插桩的代码。
+
+反过来,**这几类 Valgrind 干不了、或干不好,得用 ASan**:抓栈数组/全局数组越界(ASan 的栈/全局红区是强项)、跑得快(CI 友好)、Windows 平台(Valgrind 基本不支持 Windows)、抓 stack-use-after-return(ASan 有专门的 fake stack 机制)。
+
+一句话收口:**ASan 是「开发期」的标配,Valgrind 是「疑难杂症 / 性能 / 遗留二进制」的专科」。** 它们不是替代关系,是互补关系——很多团队是 CI 里挂 ASan 做日常守门,遇到 ASan 抓不到的怪问题再上 Valgrind 复查。
+
+## 七、回到 C++:工具是兜底,RAII 才是治本
+
+讲了一整篇工具,最后必须把话拉回来:**这些工具再强,也是「事后抓 bug」,不是「消灭 bug」。** 真正让内存错误从根上消失的,是 C++ 的 RAII 和智能指针。
+
+回头看那六类错误,你会发现它们**清一色都建立在「裸 malloc/free、裸指针」**上:
+
+- 泄漏?用 `std::unique_ptr` / `std::vector`,对象出作用域自动释放,根本没机会忘 `free`;
+- use-after-free?智能指针的所有权语义让「这块内存还能不能用」变成编译期就能约束的事;
+- double-free?`unique_ptr` 不能拷贝、移动后原指针置空,物理上就 double 不了;
+- 越界?`std::vector` 配 `.at()` 会抛异常、`std::span` 带边界,别用裸 `[]` 配手工长度。
+
+C 风格的 `malloc`/`free`/裸指针把「内存什么时候释放、谁能访问」全部丢给程序员记——人脑记这些必然出错,所以才需要 Valgrind 和 ASan 这种「记账工具」来兜底。Modern C++ 的思路是把这套记账**搬到类型系统里**:资源的生命周期绑死在对象上,编译器替你保证释放。这是从「工具抓 bug」到「语言消灭 bug」的根本跃迁,我们卷一的[动态内存管理](../vol1-fundamentals/ch12/02-new-delete.md)整篇就在讲这个。
+
+但这**不**意味着 Modern C++ 项目就不需要 ASan/Valgrind 了。只要你的代码还会调 C 库、还会用 `new`/`delete`、还会碰第三方没有 RAII 包装的接口,内存错误就还有缝可钻。所以正确的姿势是:**先用 RAII 把 99% 的内存错误在写代码时就消灭,再用 ASan 把漏网的那 1% 在测试期抓出来,最后拿 Valgrind 兜底那些最古怪的疑难杂症。** 三层防线,缺一不可。
diff --git a/documents/vol6-performance/12-sanitizer-toolchain-and-memory-safety.md b/documents/vol6-performance/12-sanitizer-toolchain-and-memory-safety.md
new file mode 100644
index 000000000..925d0ef4b
--- /dev/null
+++ b/documents/vol6-performance/12-sanitizer-toolchain-and-memory-safety.md
@@ -0,0 +1,332 @@
+---
+title: "Sanitizer 工具链全景:从 -fsanitize 到内核 KASAN/KFENCE"
+description: "把用户态 -fsanitize=address/memory/undefined/thread 和内核态 KASAN/KMSAN/UBSAN/KCSAN/KFENCE 摆在同一张表里对照,讲透『编译期插桩 vs 采样』两条路线和『调试 vs 上生产』的分层防御"
+chapter: 6
+order: 12
+platform: host
+difficulty: advanced
+cpp_standard: [11, 14, 17, 20]
+reading_time_minutes: 22
+prerequisites:
+ - "ASan 工具家族与内存安全:shadow memory、Heartbleed 与 sanitizer 选型"
+ - "Valgrind 与 ASan 对照:JIT 解释 vs 编译期插桩"
+related:
+ - "ASan 工具家族与内存安全:shadow memory、Heartbleed 与 sanitizer 选型"
+ - "Valgrind 与 ASan 对照:JIT 解释 vs 编译期插桩"
+ - "并发程序调试技巧(ThreadSanitizer)"
+ - "动态内存管理(new/delete 与智能指针)"
+tags:
+ - host
+ - cpp-modern
+ - advanced
+ - 内存安全
+ - 调试
+ - 工具链
+---
+
+# Sanitizer 工具链全景:从 -fsanitize 到内核 KASAN/KFENCE
+
+> PS: 这部分内容由大学期间的笔记迁移而来、并经查证核对;用户态 sanitizer 本机实跑,内核态工具本机无法运行、以 kernel.org 官方文档为准。若有疏漏,欢迎 Issue 或 PR。
+
+前面两篇我们已经把用户态的 ASan / UBSan / MSan / TSan 和 Valgrind 拆得很细了——shadow memory 怎么记账、JIT 解释和编译期插桩两条路线差在哪、五种 sanitizer 之间为什么互斥。但如果你只把目光停在「`g++ -fsanitize=address` 加个 flag」上,会漏掉一个更大的图景:**sanitizer 不是用户态的专利,内核里也有一整套对应的工具**,而且两者的设计取舍完全不一样**。
+
+这一篇我们要做的,是把整张 sanitizer 工具链拉平了看。用户态的 `-fsanitize=*` 一边,内核态的 `CONFIG_KASAN / CONFIG_KMSAN / CONFIG_KFENCE` 一边——它们抓的是同一类 bug(越界、释放后使用、未初始化、数据竞争),但站在完全不同的约束下:用户态可以为了抓 bug 把程序拖慢 2~5 倍,内核态不行,内核一旦拖慢 5 倍整台机器就废了。所以内核这边演化出了「采样」这条路——KFENCE 用极低开销换「能在生产环境一直开着」,和 KASAN 那种「只能调试期开」的重型工具分层共存。
+
+## 先把用户态这一侧收个口
+
+在往内核走之前,先把用户态 sanitizer 的四个 flag 用真报告钉一下,后面好和内核做对照。详细的 shadow memory 原理和 Heartbleed 故事在上一篇已经讲透,这里只放最小可复现的代码和真实终端输出,方便对照「每一种 bug 对应哪个 flag」。
+
+四个 flag 一句话分工:`-fsanitize=address`(ASan,越界/UAF/泄漏)、`-fsanitize=undefined`(UBSan,未定义行为)、`-fsanitize=memory`(MSan,未初始化读)、`-fsanitize=thread`(TSan,数据竞争)。
+
+### ASan:一次抓到三种错
+
+堆越界、释放后使用、内存泄漏,ASan 一把全收。我们把三个错误分别写成最小例子(放一个程序里 ASan 会在第一个错误处 abort,后面两个看不到,所以拆开):
+
+```cpp
+// uaf.cpp —— 释放后使用(use-after-free)
+#include
+int main() {
+ int* p = new int(7);
+ delete p;
+ printf("*p = %d\n", *p); // p 已 delete,悬空
+ return 0;
+}
+```
+
+用 `g++ -std=c++20 -O0 -g -fsanitize=address -fno-omit-frame-pointer uaf.cpp -o uaf` 编出来,跑出来:
+
+```text
+=================================================================
+==118313==ERROR: AddressSanitizer: heap-use-after-free on address 0x72c9e1de0010 at pc 0x5d222d6ed26f bp 0x7ffc31d299a0 sp 0x7ffc31d29990
+READ of size 4 at 0x72c9e1de0010 thread T0
+ #0 0x5d222d6ed26e in main /tmp/sanit/uaf.cpp:6
+ ...
+SUMMARY: AddressSanitizer: heap-use-after-free /tmp/sanit/uaf.cpp:6 in main
+```
+
+`-g` 让报告带上 `uaf.cpp:5` 这种源码定位,这是 ASan 能不能用的分水岭——没有调试符号,报告只剩一串地址,等于白报。栈上的越界它一样抓,换一个跨函数的栈缓冲:
+
+```cpp
+// stack_oob.cpp —— 栈缓冲越界
+#include
+void fill(char* p) { // 跨函数,检测能跨栈帧
+ for (int i = 0; i <= 8; ++i) p[i] = 'A'; // 合法下标 0..7,8 越界
+}
+int main() {
+ char buf[8];
+ fill(buf);
+ printf("done\n");
+ return 0;
+}
+```
+
+同样的 flag 编出来跑:
+
+```text
+=================================================================
+==119120==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x6ec9ef2f0028 at pc 0x5f38ab644200 bp 0x7fff6db78e20 sp 0x7fff6db78e10
+WRITE of size 1 at 0x6ec9ef2f0028 thread T0
+ #0 0x5f38ab6441ff in fill(char*) /tmp/sanit/stack_oob.cpp:4
+ #1 0x5f38ab64429d in main /tmp/sanit/stack_oob.cpp:8
+ ...
+Address 0x6ec9ef2f0028 is located in stack of thread T0 at offset 40 in frame
+ #0 0x5f38ab644220 in main /tmp/sanit/stack_oob.cpp:6
+```
+
+注意它不光告诉你越界,还告诉你「这块内存是 `main` 栈帧里 offset 40 的那个 `buf`」——栈红区(redzone)连栈上数组的归属都标出来了。这就是 shadow memory 的威力,上一篇详细拆过,这里不再展开。
+
+内存泄漏走的是 ASan 自带的 LeakSanitizer(LSan),程序退出时扫一次:
+
+```cpp
+// leak.cpp —— 忘记 delete
+#include
+int main() {
+ int* leak = new int(99);
+ *leak = 100;
+ printf("leak = %d (故意不 delete)\n", *leak);
+ return 0;
+}
+```
+
+```text
+=================================================================
+==118322==ERROR: LeakSanitizer: detected memory leaks
+
+Direct leak of 4 byte(s) in 1 object(s) allocated from:
+ #0 0x7c2b9dd2d341 in operator new(unsigned long) (/usr/lib/libasan.so.8+0x12d341)
+ #1 0x609649f361ba in main /tmp/sanit/leak.cpp:4
+```
+
+ASan 的代价是实打实的:程序慢 2~5 倍、内存多 3~5 倍。所以**生产构建一定要摘掉 `-fsanitize=address`**,只在调试和测试期开。这条约束听起来无所谓,但到了内核那边,同样的「开销太大」直接催生出了完全不同的工具——这就是后面 KFENCE 的来历。
+
+### UBSan:专治未定义行为
+
+ASan 管的是「这块内存能不能碰」,UBSan 管的是「这个操作本身合不合法」。有符号整数溢出、数组下标越界、空指针解引用、错误位移,这些在 C++ 标准里是未定义行为(UB),不一定会崩,但结果不可预测:
+
+```cpp
+// ub.cpp —— 三种 UB
+#include
+#include
+int main() {
+ int32_t big = 2147483647; // INT32_MAX
+ int32_t sum = big + 1; // (1) 有符号加法溢出 → UB
+ int arr[4] = {0,1,2,3};
+ int idx = 10;
+ int v = arr[idx]; // (2) 下标越界 → UBSan 的 bounds 检查
+ printf("sum=%d v=%d\n", sum, v);
+ return 0;
+}
+```
+
+用 `g++ -std=c++20 -O0 -g -fsanitize=undefined ub.cpp -o ub`(默认 recover,把所有 UB 都打出来再继续):
+
+```text
+ub.cpp:6:13: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
+ub.cpp:9:20: runtime error: index 10 out of bounds for type 'int [4]'
+ub.cpp:9:9: runtime error: load of address 0x7fffed140f28 with insufficient space for an object of type 'int'
+sum=-2147483648 v=0
+```
+
+这里有个真实的坑要先提醒:**UBSan 和 ASan 可以一起开**(`-fsanitize=address,undefined`),很多人这么干,因为一个管内存一个管算术,互补。但 UBSan 默认「打印完继续跑」(recover),如果你希望碰到第一个 UB 就 abort(更接近线上行为),加 `-fno-sanitize-recover=all`。反过来,ASan 是碰到就 abort 的,改不了。
+
+### MSan:未初始化读,只有 Clang 有
+
+MSan 抓的是「用了没初始化的值」,这是 ASan 抓不到的一类——内存合法、访问也合法,但值是垃圾。坑在于:**MSan 只有 Clang 实现,GCC 压根不支持这个 flag**:
+
+```cpp
+// msan.cpp —— 用了没初始化的变量
+#include
+int main() {
+ int x; // 故意不初始化
+ if (x) // 拿垃圾值做分支判断 → MSan 抓
+ printf("x is truthy\n");
+ else
+ printf("x is zero\n");
+ return 0;
+}
+```
+
+GCC 直接报错:
+
+```text
+$ g++ -std=c++20 -fsanitize=memory msan.cpp -o msan
+g++: error: unrecognized argument to '-fsanitize=' option: 'memory'
+```
+
+换成 Clang 就能编能跑(`clang++ -std=c++20 -O0 -g -fsanitize=memory -fno-omit-frame-pointer msan.cpp -o msan`):
+
+```text
+==118932==WARNING: MemorySanitizer: use-of-uninitialized-value
+ #0 0x58f3129f5677 (/tmp/sanit/msan+0xd7677)
+ ...
+SUMMARY: MemorySanitizer: use-of-uninitialized-value
+```
+
+> **踩坑预警**:MSan 有个硬限制——**整个程序(包括它链接的所有库)都必须用 MSan 插桩编译**。你直接 `clang++ -fsanitize=memory` 链一个没插桩的 `libc++` 或第三方库,会爆出一堆假阳性,因为 MSan 把库返回的值都当未初始化。所以 MSan 在实际项目里很少用,通常要配合「用 MSan 重新构建整个 toolchain」才能跑干净。这一点上一篇讲过,这里强调一下,因为内核那边的 KMSAN 也有类似的「全链路插桩」要求。
+
+至于 TSan(数据竞争),它和 ASan 互斥、开销 5~15 倍,专门抓并发 bug,讲并发那一卷的「并发程序调试技巧」已经拆透了,这里只标一下它在全景图里的位置,不重复。
+
+## 现在问题来了:内核怎么办?
+
+把用户态的四个 flag 记牢之后,接下来才是这一篇真正想讲的东西。**内核也是 C 代码,也会越界、也会 UAF、也会有数据竞争,能不能直接把 `-fsanitize=address` 套到内核上?**
+
+答案是:**能,而且内核确实这么干了,但代价大到你只能在调试时开**。这就是 KASAN——Kernel AddressSanitizer。它的底层和用户态 ASan 是同一套(shadow memory + 编译期插桩),但内核有自己的约束:
+
+1. **影子内存要占内核虚拟地址空间的一大块**。用户态 ASan 的影子内存是「进程地址空间的 1/8」,内核这边直接划走内核 VAS 的一大段(`KASAN_SHADOW_START` 到 `KASAN_SHADOW_END`)。在 64 位内核上地址空间够大(128 TB),还能撑住;32 位就紧张得多,所以早期 KASAN 只能跑在 64 位上,直到 5.11 才有 Linus Walleij 给 ARM-32 做的精简版。
+
+2. **整机的每一处内存访问都被插桩**。内核不是一个进程,是所有进程共享的底层,一旦开 KASAN,整机性能直接塌方——这就是为什么 `CONFIG_KASAN` 只用于调试内核,生产内核绝对不开。
+
+3. **它要配特定的内存分配器**。内核用 SLAB 或 SLUB 分配器,KASAN 要在分配器里埋红区、给释放的页打「投毒」标记(`KASAN_SANITIZE_*`),才能在 UAF/OOB 时立刻逮到。这和用户态 ASan 拦截 `malloc/free` 是同一思路,只是换到了 `kmalloc/kfree`。
+
+源笔记里写「KASAN 适用于 x86_64 和 AArch64,4.x 及以上」,这个版本号要核一下。实际上 KASAN 是 **Linux 4.0** 合入主线(最初支持 x86_64),AArch64 跟进,**5.11** 才补上 ARM-32 的优化版。机制没错,但别记成笼统的「4.x」。
+
+### KASAN 长什么样(官方报告样式)
+
+KASAN 报告长什么样?按 kernel.org dev-tools/kasan 文档的示例结构,把官方例子里的 `kmalloc_oob_right` 换成虚拟的 `buggy_driver_write`(字段和层级完全对应官方报告),大致是这样:
+
+```text
+==================================================================
+BUG: KASAN: slab-out-of-bounds in buggy_driver_write+0x3e/0x60 [buggy]
+Write of size 1 at addr ffff888006c42185 by task cat/1234
+
+CPU: 0 PID: 1234 Comm: cat Tainted: G B
+Call Trace:
+ dump_stack_lvl+0x49/0x63
+ print_report+0x171/0x486
+ kasan_report+0xb1/0x130
+ buggy_driver_write+0x3e/0x60 [buggy]
+ ...
+
+Allocated by task 1234:
+ kasan_save_stack+0x1e/0x40
+ __kasan_kmalloc+0x81/0xa0
+ kmalloc_trace+0x21/0x30
+ buggy_driver_init+0x2a/0x60 [buggy]
+ ...
+
+The buggy address belongs to the object at ffff888006c42180
+ which belongs to the cache kmalloc-8 of size 8
+The buggy address is located 5 bytes inside of
+ 8-byte region [ffff888006c42180, ffff88800642188)
+```
+
+和用户态 ASan 的报告结构几乎一模一样:**先说在哪炸(slab-out-of-bounds、越界写、发生在哪个驱动函数),再给分配栈(这块内存是谁、在哪一次 `kmalloc` 分出来的)**。内核报告多了「属于哪个 slab cache(`kmalloc-8`)、在对象的第几字节」这种内核分配器专属的信息。看懂了用户态 ASan 报告,内核 KASAN 报告基本也能读。
+
+## 全景对照表:用户态 ↔ 内核态
+
+到这里把两边对齐,这张表是这一篇的核心——源笔记原是一张外部 PNG,我们用 Markdown 自己画:
+
+| 抓的 bug | 用户态 flag | 内核工具 | 内核合入版本 | 能否上生产 |
+|---------|-----------|---------|------------|----------|
+| 越界 / UAF / 双重释放 | `-fsanitize=address` (ASan) | **KASAN** | 4.0(x86_64)/ 5.11(ARM-32 优化) | 否,仅调试 |
+| 未初始化读 | `-fsanitize=memory` (MSan,仅 Clang) | **KMSAN** | 5.16 起可用补丁分支,**6.1** 起主线完整可用,仅 Clang 14.0.6+ + 仅 x86_64 | 否,开销巨大 |
+| 未定义行为(溢出/越界/位移) | `-fsanitize=undefined` (UBSan) | **UBSAN** | 4.5 合入 | 部分检查可上(见下) |
+| 数据竞争 | `-fsanitize=thread` (TSan) | **KCSAN** | 5.8 合入,基于采样 | 否,仅调试 |
+| 内存泄漏 | ASan 自带 LSan | **kmemleak** / eBPF `memleak` | kmemleak 早已存在 | 谨慎,有误报 |
+| 采样式内存错误 | (用户态无对应) | **KFENCE** | **5.12** | **可以,默认就常开** |
+| 访问模式分析(非 bug 检测) | (无) | **DAMON** | **5.15** | 可以,就是为生产设计的 |
+
+这张表里有几个一定要记牢的对应关系:
+
+- **ASan ↔ KASAN**:同一个 shadow memory 思路搬到内核,代价是整机性能塌方,只能调试开。
+- **MSan ↔ KMSAN**:都只认 Clang,都要全链路插桩,都开销巨大。KMSAN 官方文档明说「not intended for production use, because it drastically increases kernel memory footprint and slows the whole system down」。
+- **UBSan ↔ UBSAN**:内核 UBSAN 在 4.5 合入,而且**它的一部分检查(比如 `CONFIG_UBSAN_BOUNDS`)在现代发行版内核里默认开启**,因为这部分开销很低——这是内核 sanitizer 里少数能「常驻」的。
+- **TSan ↔ KCSAN**:注意 TSan 是编译期全插桩,KCSAN 不一样——它基于**采样**(watchpoint),开销可控,但相应的,它检测数据竞争靠的是「碰巧采到」,不是 TSan 那种「理论上一定检测到」。5.8 合入主线(google/kernel-sanitizers 仓库明说「in mainline since 5.8」)。
+
+源笔记里把 KMSAN 标成「6.1 及以上版本」——**这个版本号是对的**,别记错。KMSAN 的补丁系列由 Google 的 Alexander Potapenko 维护了多年,直到 2021 年底都还只是分支补丁、未进主线(kernel.org 官方示例报告跑在打了补丁的 `5.16.0-rc3+` 上,用的就是 google/kmsan 分支,不是主干);Google 官方仓库 (google/kmsan) 的 README 明确写着「Linux 6.1+ contains a fully-working KMSAN implementation which can be used out of the box」,即 **6.1 起主线完整可用**。所以 KMSAN 是这一批内核 sanitizer 里进主线最晚的。注意别把「5.16 补丁分支能跑」和「6.1 进主线」搞混——这是这类版本号最常见的误读。
+
+## KFENCE:把 sanitizer 搬上生产的关键一招
+
+KASAN 的问题太明显了——只能调试开,但你公司线上跑的内核出了内存 bug 怎么办?你总不能拿一台生产机器换成开了 KASAN 的调试内核去复现,那样业务早就挂了。真正缺的是一个**开销低到能一直开着的内存错误检测器**。
+
+这就是 KFENCE(Kernel Electric-Fence),**Linux 5.12 合入主线**。它的思路和 KASAN 完全不同,不再「检测每一次访问」,而是改成**采样**:
+
+- KFENCE 维护一个固定大小的对象池(默认 `CONFIG_KFENCE_NUM_OBJECTS=255`,每个对象占 2 页——1 页放对象、1 页当守卫页 guard page,池里对象页和守卫页交错排布,所以每个对象页两边都是守卫页;默认配置下整个池约 2 MiB)。
+- 内核的 slab 分配器(`kmalloc`)会**被一个采样定时器钓进 KFENCE 池**:KFENCE 有个以毫秒为单位的采样间隔(启动参数 `kfence.sample_interval`,默认可由 `CONFIG_KFENCE_SAMPLE_INTERVAL` 配),每个采样间隔里下一次 `kmalloc` 分配就被「钓」交给 KFENCE 来管。
+- 一旦进了 KFENCE 池,这次分配就被放在两个守卫页之间——任何越界读写都会踩到守卫页,立刻触发 page fault,内核报出精确的错误和分配栈。
+- 释放后,KFENCE 把这页标记成「不可访问」,再有人碰它就是 use-after-free,同样立刻报。
+
+采样的代价是:**绝大多数分配根本不经过 KFENCE**,所以大部分 bug 它抓不到——你得跑足够长时间、让足够多的分配流经 KFENCE 池,才有机会逮到。但换来的是**极低的开销**(官方说接近零,实际生产负载几乎感知不到),于是它成了**第一个能一直开在生产内核上的内存 sanitizer**。事实上,只要架构支持、且开了 SLAB 或 SLUB,KFENCE 在很多发行版里默认就是开的。
+
+源笔记原话「KFENCE 必须运行长时间,但开销足够低,甚至可以在生产环境中运行」——机制描述没错,我们补上版本号(5.12)和「采样」这个关键词,再强调一下「默认常开」这个工程意义。它取代的是更老的 `kmemcheck`(那个在 4.15 就被删了,因为开销太大、和 KFENCE 思路冲突)。
+
+## DAMON:另一条「采样」路线,但目标不是抓 bug
+
+提到「采样」,顺带要把 DAMON(Data Access MONitor)讲一下,因为它和 KFENCE 在哲学上是同一类——**不去全量跟踪,而是采样代表性样本**。但 DAMON 不是 sanitizer,它不抓 bug,它**监控内存访问模式**:
+
+- **Linux 5.15 合入主线**,目的是帮开发者(和内核自己)看清「进程到底在怎么访问内存」,从而优化布局、指导回收。
+- DAMON 把目标进程的地址空间切成等大的区域,**采样**每个区域里的若干代表页,记录访问频次,形成直方图。区域如果是热点,就再细分——这种「智能放大」让它在超大地址空间上也能低成本运行。
+- 内核组件是「生产者」(产出访问模式),用户态(或内核)是「消费者」。消费者甚至能根据访问模式反过来调 `madvise()` 改内存属性——比如把确认冷的数据区建议内核换出。
+
+DAMON 的接口有三个:用户态的 `damo` 工具(来自 awslabs/damo)、`/sys/kernel/mm/damon/admin/` 下的 sysfs、以及给内核开发者的内核 API。旧的 debugfs 接口已废弃。它和 KFENCE 放在一起看,你会发现内核在 5.12~5.15 这一波,系统性地用「采样」补上了「全量插桩太贵」这个口子——KFENCE 抓 bug,DAMON 看模式,都能上生产。
+
+## 三层防御:把工具按场景摆好
+
+把用户态和内核态的 sanitizer 摆在一起,内存安全的工具链其实是个**分层的防御纵深**,每一层有不同的开销/覆盖权衡:
+
+::: tip 开发期:全量插桩,抓到为止
+开发自测、CI、fuzzing 阶段,**开销不是问题,覆盖最重要**。用户态开 `-fsanitize=address,undefined`(再单独跑一轮 `-fsanitize=thread`),内核调试构建开 `CONFIG_KASAN` + `CONFIG_KCSAN` + `CONFIG_UBSAN`。这一层假设 bug 一定能被全量插桩逮到,代价是程序/整机慢几倍,只在非生产环境承受。
+:::
+
+::: tip 测试/准生产:采样插桩,长期运行
+预发、灰度、长时间负载测试,**不能接受整机塌方,但要跑足够久才能暴露罕见 bug**。这一层用 KFENCE——采样、低开销、能一直开着,让成千上万的分配流经守卫页池,逮到那些「跑一万次才出现一次」的越界和 UAF。用户态这一层目前没有完全对等的东西(Valgrind 太慢、ASan 太重),所以内核这边 KFENCE 的工程价值特别突出。
+:::
+
+::: tip 生产:默认开启的轻量检查 + 事后分析
+真正的线上内核,**只开开销可忽略的检查**:KFENCE(默认常开)、`CONFIG_UBSAN_BOUNDS` 这类轻量 UBSAN 子集、再加上 DAMON 做访问模式分析指导优化。出了事故靠事后工具——内核 oops 日志、kdump/crash 分析、eBPF 的 `memleak-bpfcc` 跟踪未释放的分配。这一层不再指望「当场抓 bug」,而是「留够证据,事后能查」。
+:::
+
+这套分层就是为什么内核要同时养 KASAN 和 KFENCE 两个看似重复的工具——**同一个 bug(比如 UAF),开发期用 KASAN 抓,生产期用 KFENCE 抓**,工具不重复,场景不重叠。用户态目前只有第一层(开发期插桩)用得顺手,第二、第三层还没有 kernel 那么成熟的工具,这也是为什么「在 C++ 用户态里彻底搞定内存安全」比内核还难——内核好歹有 KFENCE 能兜底生产,用户态出了线上 UAF,经常只能等它崩了再去看 core dump。
+
+## 顺带一提:静态分析和事后工具
+
+除了上面这些运行时 sanitizer,内核和用户态都还有一组**不靠运行、靠看代码或看日志**的工具,源笔记里也提到了,这里收个尾,不展开:
+
+- **静态分析**:内核侧有 `sparse`、`smatch`、`Coccinelle`、`checkpatch.pl`,用户态有 `clang-tidy`、`cppcheck`。它们不跑代码、开销为零,但只能抓「代码模式上明显有问题」的那类,抓不到运行时才暴露的 UAF/OOB。和 sanitizer 是互补不是替代——静态分析抓规范、sanitizer 抓运行时。
+- **事后分析**:内核 oops/panic 日志、`kdump`/`crash` 工具分析 dump、`[K]GDB` 调试。这些是 bug 已经炸了之后的取证手段,和「提前抓 bug」的 sanitizer 不在一个阶段。
+
+C++ 用户态的事后分析,在「动态内存管理」那一章我们用 `-fsanitize=address` 在退出时报泄漏见过一次,在「并发程序调试技巧」用 TSan 见过并发 bug 的事后定位。整个工具链是**开发期 sanitizer → 生产期轻量检查 → 事后分析**一条龙,缺哪一环,对应的那类 bug 就会在那个阶段反复咬你。
+
+## 小结
+
+这一篇把 sanitizer 工具链从用户态拉到内核态,几个关键结论收一下:
+
+- 用户态四个 flag 分工明确:ASan(越界/UAF/泄漏)、UBSan(未定义行为)、MSan(未初始化读,仅 Clang)、TSan(数据竞争)。它们之间大多互斥(ASan/MSan/TSan 不能两两同开),UBSan 能和 ASan 叠加。本机 GCC 16.1.1 / Clang 22 全部真跑出了报告。
+- 内核态有完全对应的工具:KASAN(↔ASan,4.0)、KMSAN(↔MSan,6.1 起主线完整可用)、UBSAN(↔UBSan,4.5)、KCSAN(↔TSan,5.8)。机制同源,但受内核约束,大多只能调试开。
+- **KFENCE(5.12)是分水岭**:用「采样 + 守卫页」把内存错误检测的开销压到能上生产,默认常开,填补了 KASAN 留下的生产空白。
+- **DAMON(5.15)**走同一条采样路线,但不抓 bug,监控访问模式,指导内存优化。
+- 整套工具链是三层防御:开发期全量插桩(KASAN/ASan)→ 准生产采样(KFENCE)→ 生产轻量检查 + 事后分析(UBSAN 子集/kdump)。
+- 源笔记两处版本号已核正:KFENCE 版本 = 5.12(源没标,补上);KMSAN 源写「6.1 及以上」其实是对的,核过 Google 官方仓库 README 确认 6.1 起主线完整可用——补丁在 5.16 分支已能跑,但进主线是 6.1。
+
+下一篇我们继续在性能与正确性这条线上走,去看编译器优化怎么在不改变语义的前提下把代码变快——以及它在什么时候会「偷偷」改变语义,让你精心写的并发代码跑出和你预期不一样的结果。
+
+## 参考资源
+
+- [kernel.org: Kernel Address Sanitizer (KASAN)](https://www.kernel.org/doc/html/latest/dev-tools/kasan.html) —— KASAN 机制、配置项与示例报告
+- [kernel.org: Kernel Memory Sanitizer (KMSAN)](https://www.kernel.org/doc/html/latest/dev-tools/kmsan.html) —— KMSAN 要求 Clang 14.0.6+,仅 x86_64,明确「not for production」
+- [kernel.org: Kernel Electric-Fence (KFENCE)](https://www.kernel.org/doc/html/latest/dev-tools/kfence.html) —— KFENCE 采样机制、`CONFIG_KFENCE_NUM_OBJECTS`、生产可用定位
+- [kernel.org: UndefinedBehaviorSanitizer (UBSAN)](https://www.kernel.org/doc/html/latest/dev-tools/ubsan.html) —— 内核 UBSAN 各子检查与开销
+- [kernel.org: Kernel Concurrency Sanitizer (KCSAN)](https://www.kernel.org/doc/html/latest/dev-tools/kcsan.html) —— KCSAN 基于 watchpoint 的采样竞态检测
+- [kernel.org: DAMON](https://www.kernel.org/doc/html/latest/admin-guide/mm/damon/usage.html) —— DAMON sysfs/schemes 接口与访问模式监控
+- [Clang: UndefinedBehaviorSanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html) —— 用户态 UBSan 各子检查清单
+- [Clang: MemorySanitizer](https://clang.llvm.org/docs/MemorySanitizer.html) —— MSan 全链路插桩要求与用法
diff --git a/documents/vol6-performance/index.md b/documents/vol6-performance/index.md
index 15689de2a..207b4b9ad 100644
--- a/documents/vol6-performance/index.md
+++ b/documents/vol6-performance/index.md
@@ -22,4 +22,7 @@ tags:
内联与编译器优化
性能与大小评估
AVX/AVX2 深入
+ ASan 工具家族与内存安全
+ Valgrind 与 ASan 对照:JIT 解释 vs 编译期插桩
+ Sanitizer 工具链全景:从 -fsanitize 到内核 KASAN/KFENCE
diff --git a/documents/vol8-domains/index.md b/documents/vol8-domains/index.md
index 201ce0beb..11ed95006 100644
--- a/documents/vol8-domains/index.md
+++ b/documents/vol8-domains/index.md
@@ -29,7 +29,6 @@ tags:
嵌入式开发
- 网络编程 — 规划中
GUI 与图形 — 规划中
数据存储 — 规划中
算法与数据结构 — 规划中
diff --git a/documents/vol8-domains/networking/.pages b/documents/vol8-domains/networking/.pages
deleted file mode 100644
index 4623ca546..000000000
--- a/documents/vol8-domains/networking/.pages
+++ /dev/null
@@ -1 +0,0 @@
-title: 网络编程
diff --git a/documents/vol8-domains/networking/index.md b/documents/vol8-domains/networking/index.md
deleted file mode 100644
index ea5826bf7..000000000
--- a/documents/vol8-domains/networking/index.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-title: "网络编程"
-description: "Socket、HTTP、异步 I/O、WebSocket、RPC"
-platform: host
-tags:
- - cpp-modern
- - host
- - intermediate
----
-
-# 网络编程
-
-> 状态:规划中
-
-## 概述
-
-本子领域覆盖 C++ 网络编程,包括 Socket 基础、HTTP、异步 I/O、WebSocket、RPC。
-
-预计 13 篇文章。
-
-## 章节导航
-
-> 内容编写中,敬请期待。
diff --git a/scripts/tags.py b/scripts/tags.py
index 972160d06..8f8088365 100644
--- a/scripts/tags.py
+++ b/scripts/tags.py
@@ -48,6 +48,9 @@
# General
'基础', '入门', '进阶', '实战', '优化', '工程实践',
+ # Domains / engineering practices (notebook-migration batch)
+ '网络编程', '内存安全', '调试', '测试',
+
# Platforms
'host', 'stm32f1',