目录
一、前言——segfault 本质与目标
1.1 一句话定义
1.2 常见根因谱系
1.3 排障要拿到的「四件套」
二、机制深入——从非法访问到进程死亡
2.1 地址空间与页表(极简)
2.2 信号投递与默认行为
2.3 「崩点」≠「根因点」(最重要的心智模型)
2.4 和 SIGBUS / SIGABRT 的边界
三、决策树与工具矩阵
3.1 决策树
3.2 工具对比矩阵
3.3 环境默认推荐
四、方案一:dmesg / journalctl 解读
4.1 采集命令
4.2 日志字段精读
4.3 error 码(x86 用户态常见位)
4.4 从 ip 到源码的快速路径
五、方案二:coredump + gdb
5.1 打开并确认 core 管道
5.2 影响 core 是否生成的检查清单
5.3 gdb 加载 core 的标准剧本
5.4 共享库符号与搜索路径
5.5 core 很大怎么办
六、方案三:gdb 交互与高级技巧
6.1 前台复现
6.2 attach 线上进程(慎用)
6.3 断在信号上
6.4 硬件断点 / 观察点
6.5 记录与回放(若支持)
6.6 反汇编级确认空指针
七、方案四:AddressSanitizer
7.1 编译与运行
7.2 报告里读什么
7.3 常用 ASAN_OPTIONS
7.4 局限(避免迷信)
八、方案五:Valgrind / 其它 Sanitizer
8.1 Valgrind memcheck
8.2 其它消毒器(按需)
九、方案六:strace / ltrace / 日志夹逼
9.1 strace:崩前最后的系统交互
9.2 ltrace:崩前最后一个库调用
9.3 日志夹逼(无工具时的土办法)
十、方案七:符号、PIE、addr2line 精算
10.1 编译建议
10.2 PIE / ASLR 下地址换算
10.3 调试机临时关闭 ASLR
10.4 nm / objdump 辅助
十一、方案八:多线程、信号与生产环境
11.1 多线程崩溃
11.2 自定义信号栈 / handler
11.3 生产最小采集包
11.4 容器与权限
十二、实战案例(逐步对照)
Makefile
12.1 空指针:dmesg → gdb 一行定位
12.2 堆越界:为何 gdb「骗人」,ASan「救命」
12.3 UAF
12.4 栈溢出:不信任 bt
12.5 野指针低地址
12.6 仅有线上 ip:coredumpctl 优先于手算
12.7 「15 分钟标准路径」清单
十三、根因分类诊断手册
十四、工程化:CI 与线上闭环
14.1 CI 建议门禁
14.2 版本与符号归档
14.3 与本系列其它工具的衔接
十五、FAQ 与常见坑
15.1 本地好好的,线上崩?
15.2 bt 全是 ??
15.3 ASan 报错但「逻辑上不该」
15.4 gdb 里看不到变量:optimized out
15.5 崩溃在第三方闭源库
15.6 fork 后子进程崩
15.7 Rust/Go 混编、JNI
十六、总结
16.1 记住三条
16.2 方案速查
16.3 最小命令卡
一、前言——segfault 本质与目标
1.1 一句话定义
Segmentation fault 是用户态进程收到 SIGSEGV:CPU 在访问某虚拟地址时触发 页错误(page fault),内核判定该访问 非法(无映射、无权限、错误类型),于是向进程投递段错误信号;默认动作是终止进程,并可能留下 core。
它 不是「C 语言语法错误」,而是 运行时内存访问与地址空间权限 的冲突。
1.2 常见根因谱系
|
空指针 |
*p 且 p==NULL |
segfault at 0 / 极低地址 |
|
未初始化指针 |
栈上野指针 |
地址随机、难复现 |
|
堆越界 |
buf[n] 写出分配块 |
当时不崩,稍后 malloc/free 崩 |
|
Use-After-Free |
free 后继续用 |
地址「像合法」,内容被复用 |
|
栈溢出 |
大数组 / 深递归 |
bt 乱、可能 SIGSEGV 在 guard page |
|
栈砸 |
溢出覆盖返回地址 |
崩在 ret / 奇怪函数 |
|
格式化串 / 错 sizeof |
memcpy 长度错 |
同越界 |
|
错误转型 / ABI |
回调签名不匹配 |
跳飞、ip 怪异 |
|
mmap/文件映射 |
映射外访问、文件被截断 |
SIGSEGV 或 SIGBUS |
|
JIT / 加载器 |
写后未改 RX、重定位错 |
执行无 X 权限页 |
1.3 排障要拿到的「四件套」
按优先级:
信号与故障地址:SIGSEGV?访问了哪个 VA?读还是写?
指令指针与映射:崩在哪个 .so / 主程序的哪一段?
调用栈:谁调用到这里?(bt full)
时间线:分配/释放/上次合法状态在哪?(ASan 最强)
目标不是「看到 Segmentation fault」这句话,而是用最短路径定位到「哪一行、为什么非法」。
二、机制深入——从非法访问到进程死亡
理解机制,才能读懂 dmesg 的 error=,并区分「崩点」和「根因点」。
2.1 地址空间与页表(极简)
进程虚拟地址
→ MMU 查页表
→ 有页且权限匹配 → 访问成功
→ 缺页 / 权限不符 → #PF(Page Fault)进内核
→ 可处理(如匿名页延迟分配、写时复制)→ 返回用户态重试
→ 不可处理(无 VMA、写只读、用户访问内核地址等)→ 投递 SIGSEGV
相关概念:
|
VMA |
进程合法映射区间(代码、堆、栈、mmap、文件) |
|
权限 |
R/W/X;NX 位禁止数据页执行 |
|
Guard page |
栈下方保护页,踩到即 SIGSEGV,防无限吞掉别的映射 |
查看进程映射:
cat /proc/$(pidof app)/maps
# 或崩溃后在 gdb:
(gdb) info proc mappings
2.2 信号投递与默认行为
内核 force_sig_fault(SIGSEGV, …)
→ 若进程未捕获:默认 terminate + core(若允许)
→ 若注册了 sa_handler/sa_sigaction:进入用户态信号处理函数
(注意:错误的信号处理器会掩盖问题或二次崩溃)
// 仅示意:生产排查期慎用「吞掉 SIGSEGV」
signal(SIGSEGV, SIG_IGN); // 极不推荐,行为未定义/难查
2.3 「崩点」≠「根因点」(最重要的心智模型)
时间线:
T0 堆块 A 被写越界,破坏相邻 chunk 元数据或空闲链表
T1 程序继续「正常」跑(看起来没崩)
T2 另一次 malloc/free 走到坏元数据 → SIGSEGV
T3 gdb 停在 ptmalloc 内部 ← 崩点
真正根因在 T0 的业务写爆 ← 根因点
因此:
-
只看崩溃栈不够 时,优先 ASan/Valgrind(在 T0 抓)。
-
纯 gdb 适合空指针、明显野指针;堆破坏要配合消毒器或「缩小复现 + bisect」。
2.4 和 SIGBUS / SIGABRT 的边界
|
SIGSEGV |
无映射、无权限的地址访问 |
|
SIGBUS |
对齐错误(部分 arch)、mmap 后文件被截短再访问 |
|
SIGABRT |
abort()、断言失败、free 双重释放被检测器/malloc 断言杀掉 |
|
SIGILL |
非法指令(坏函数指针、错架构二进制) |
|
SIGFPE |
除零等(与内存无关,勿混淆) |
先确认信号名,再选工具,避免「按 segfault 查却是 abort」。
三、决策树与工具矩阵
3.1 决策树
遇到 Segmentation fault / SIGSEGV
│
├─【能改编译 + 可复现】──► ASan(-fsanitize=address)★首选
│ 仍偶发?开 coredump,保留现场
│
├─【有 core / 能开 ulimit -c】──► gdb ./app core → bt full
│
├─【可前台跑】──► gdb –args ./app → run → bt full
│ 多线程:thread apply all bt
│
├─【不能插桩重编】──► Valgrind memcheck
│ 或安装 debuginfo 后 gdb
│
├─【怀疑崩前 I/O / 加载 .so】──► strace -f / ltrace -f
│
├─【只剩内核一行日志】──► 解析 dmesg → maps/addr2line/gdb list *
│
└─【多线程竞态嫌疑】──► 复现加压 + ASan;并行考虑 TSan
3.2 工具对比矩阵
|
dmesg |
内核记录 fault |
无 |
否 |
VA / IP / error |
事后只留痕迹 |
|
coredump+gdb |
内存快照 |
写盘 |
否(要符号) |
完整栈/变量 |
线上/必现崩溃 |
|
gdb live |
ptrace |
停顿 |
否 |
交互调试 |
本地复现 |
|
ASan |
编译插桩+影子内存 |
~2x |
是 |
根因类型+双栈 |
开发/CI |
|
Valgrind |
DBI 翻译 |
10~50x |
否 |
非法读写栈 |
无法 ASan 时 |
|
strace |
跟踪 syscall |
中 |
否 |
崩前行为 |
缩小模块 |
|
ltrace |
跟踪 PLT |
中 |
否 |
崩前库调用 |
strcpy/memcpy 嫌疑 |
|
TSan |
插桩竞态 |
5~15x |
是 |
data race |
并发崩溃 |
|
UBSan |
未定义行为 |
低~中 |
是 |
对齐/越界 UB |
补强 |
3.3 环境默认推荐
|
日常开发 |
Debug 构建默认 ASan(或 CI 必跑) |
|
联调/预发 |
-g + ulimit -c unlimited + 统一 core_pattern |
|
生产 |
保留符号/debuginfo 包;崩溃收集 core;不要长期开 ASan |
|
容器 |
明确 core 落盘或 coredumpctl;注意 seccomp/权限 |
四、方案一:dmesg / journalctl 解读
4.1 采集命令
dmesg -T | grep -iE 'segfault|segfault_demo' | tail -20
journalctl -k -b –no-pager | grep -i segfault | tail -20
# 实时盯着复现
dmesg -w
4.2 日志字段精读
典型一行(内核版本不同措辞略异):
segfault_demo[1234]: segfault at 0 ip 000055aaaaaa1234 sp 00007fffffffe100
error 6 in segfault_demo[55aaaaaa0000+3000]
|
at ADDR |
数据访问地址 |
0/NULL 附近 → 空指针;落入已释放堆需结合 maps |
|
ip |
出错指令地址 |
用来对符号;PIE 下每次启动会变 |
|
sp |
栈指针 |
异常偏低/不在栈 VMA → 栈破坏 |
|
error N |
页错误错误码 |
见下表 |
|
in file[base+size] |
所属映射 |
主程序还是 libc.so / 插件 |
4.3 error 码(x86 用户态常见位)
不同架构细节不同,x86 上常见解读思路:
|
与「页不存在 / 保护」相关 |
无映射 vs 权限不够 |
|
写访问位 |
写触发还是读触发 |
|
用户态位 |
用户态访问 |
|
指令获取位 |
取指失败(执行无 X 页)→ 函数指针/栈砸跳飞 |
实用经验:
-
at 0 + 用户写 → 空指针写。
-
ip 落在 libc 的 malloc/free → 高度怀疑更早的堆破坏,别只改 malloc。
-
ip 落在匿名可执行 JIT 区 → 查代码生成/权限切换(mprotect)。
4.4 从 ip 到源码的快速路径
# 1) 若有 core 或仍可复现:优先 gdb(自动处理 PIE)
gdb ./segfault_demo -ex "run null" -ex "bt" -batch
# 2) 仅有地址:先找运行时基址(需崩溃瞬间 maps,或关闭 ASLR 复现)
# 关闭 ASLR(仅调试机):
# sudo sysctl kernel.randomize_va_space=0
addr2line -e ./segfault_demo -f -C -p <addr>
# 3) gdb 不跑程序,只解析
gdb ./segfault_demo -ex "list *0x555555555234" -batch
适合: 进程已死、只剩内核日志。 不够: 无调用栈、无局部变量;堆破坏时 ip 具误导性。
五、方案二:coredump + gdb
这是线上「死后验尸」的主力。
5.1 打开并确认 core 管道
ulimit -c unlimited
ulimit -a | grep core
# 核心:core 写到哪里?
cat /proc/sys/kernel/core_pattern
# 常见:
# core → 当前目录 core / core.<pid>
# |/usr/lib/systemd/systemd-coredump … → 走 systemd
systemd 体系:
coredumpctl list
coredumpctl info <PID>
coredumpctl dump <PID> -o /tmp/app.core
coredumpctl gdb <PID>
# 或
coredumpctl debug <PID>
传统文件:
echo "core.%e.%p" | sudo tee /proc/sys/kernel/core_pattern
./segfault_demo null
ls -l core*
gdb ./segfault_demo core.segfault_demo.*
5.2 影响 core 是否生成的检查清单
|
ulimit -c |
shell 限制,必须 unlimited 或足够大 |
|
磁盘空间 |
df -h |
|
目录写权限 |
cwd 不可写则失败 |
|
fs.suid_dumpable |
特权/setuid 程序限制 |
|
容器 |
默认可能禁用;需挂卷或配置 runtime |
|
信号处理 |
若用户态吞掉 SIGSEGV 且不 abort,可能无 core |
|
PR_SET_DUMPABLE |
进程把自己设为不可 dump |
5.3 gdb 加载 core 的标准剧本
gdb ./segfault_demo /path/to/core
(gdb) set pagination off
(gdb) bt # 简洁栈
(gdb) bt full # 局部变量(关键)
(gdb) info threads
(gdb) thread apply all bt full
(gdb) frame 0
(gdb) list
(gdb) info registers
(gdb) x/20i $pc-32 # 崩点附近反汇编
(gdb) x/32gx $sp # 栈内容
(gdb) info proc mappings # 对照 at 地址落在哪
(gdb) print ptr
(gdb) print *obj # 若指针看似合法,看结构是否已毁
判断指针是否「看起来合法」:
(gdb) print p
(gdb) x/16xb p # 读得出来?还是 Cannot access memory?
5.4 共享库符号与搜索路径
core 在机器 A 产生、在机器 B 分析时:
(gdb) set sysroot /path/to/rootfs
(gdb) set solib-search-path /path/to/libs
(gdb) info sharedlibrary
或使用 debuginfod:
export DEBUGINFOD_URLS="https://debuginfod.archlinux.org/" # 示例
gdb ./app core # 自动拉 debuginfo
5.5 core 很大怎么办
# 过滤(若使用 systemd,可配 ProcessSizeMax 等)
# 分析时只要栈:优先保证 -g;不必每次把巨型堆全部人工看完
(gdb) set max-value-size unlimited # 按需
巨型进程可先 bt + 关键对象,再按需 dump memory。
六、方案三:gdb 交互与高级技巧
6.1 前台复现
gdb –args ./segfault_demo overflow
(gdb) set environment ASAN_OPTIONS=abort_on_error=1 # 若是 asan 版
(gdb) run
# 崩溃后
(gdb) bt full
一行批处理:
gdb -q –args ./segfault_demo null \\
-ex "set pagination off" \\
-ex run \\
-ex "bt full" \\
-ex quit
6.2 attach 线上进程(慎用)
gdb -p $(pidof my_server)
(gdb) handle SIGSEGV stop print
(gdb) continue
注意:ptrace 权限(yama/ptrace_scope)、停顿造成超时、多线程信号。
6.3 断在信号上
(gdb) catch signal SIGSEGV
(gdb) commands
> bt full
> info registers
> end
(gdb) run
6.4 硬件断点 / 观察点
(gdb) watch *0x6020a0 # 数据写断点(数量受硬件限制)
(gdb) awatch *ptr # 读或写
(gdb) break boom_null
(gdb) finish
堆破坏时可在「可疑写入」下断,比崩后再看有效。
6.5 记录与回放(若支持)
(gdb) target record-full # 部分环境
(gdb) reverse-continue
或用 rr(Mozilla rr)做确定性回放——对偶现崩溃极强。
6.6 反汇编级确认空指针
(gdb) x/10i $pc
# 常见: mov (%rax),%edx 且 rax=0
(gdb) info registers rax
七、方案四:AddressSanitizer
开发期定位 segfault 根因 的第一选择。完整专题见 ASan 文;此处强调与 segfault 的衔接。
7.1 编译与运行
gcc -g -O1 -fsanitize=address -fno-omit-frame-pointer \\
-U_FORTIFY_SOURCE \\
segfault_demo.c -o segfault_demo_asan
./segfault_demo_asan overflow
./segfault_demo_asan uaf
链接时若拆分编译,所有相关 .o 与最终链接都要带 -fsanitize=address。
7.2 报告里读什么
ASan 报告通常包含:
错误类型:heap-buffer-overflow、heap-use-after-free、stack-overflow…
出错栈:哪一行读写非法
分配栈 / 释放栈:块从哪来、在哪被 free
影子内存示意:红区(redzone)被踩
这直接打破「崩点 vs 根因点」问题:在 第一次非法访问 就停,而不是等堆元数据烂掉。
7.3 常用 ASAN_OPTIONS
export ASAN_OPTIONS=\\
detect_leaks=1:\\
abort_on_error=1:\\
halt_on_error=1:\\
print_legend=1:\\
fast_unwind_on_malloc=0
|
abort_on_error=1 |
便于 gdb 接住 |
|
halt_on_error=1 |
遇错即停 |
|
detect_stack_use_after_return=1 |
栈上 UAR(更慢) |
|
malloc_context_size=20 |
更深分配栈 |
7.4 局限(避免迷信)
-
性能约 2x,内存显著增加 → 生产慎用
-
与自定义 malloc、部分 JIT、sandbox 冲突
-
无法替代 所有逻辑 bug;未覆盖的并发应用 TSan
-
优化过高可能改变复现,建议 -O1 查 bug、再在 -O2 验证
八、方案五:Valgrind / 其它 Sanitizer
8.1 Valgrind memcheck
gcc -g -O0 segfault_demo.c -o segfault_demo
valgrind –tool=memcheck –leak-check=no \\
–track-origins=yes ./segfault_demo uaf
|
–track-origins=yes |
未初始化值来源(更慢) |
|
–num-callers=40 |
更深栈 |
|
–suppressions=file |
压制第三方噪音 |
优点: 无需 -fsanitize。 缺点: 极慢;对向量化/时间敏感路径不友好。
8.2 其它消毒器(按需)
|
UBSan -fsanitize=undefined |
对齐、空指针 UB、越界(部分) |
|
TSan -fsanitize=thread |
数据竞争导致的「随机崩」 |
|
MSan Clang |
未初始化读(需全链路插桩) |
随机崩溃 + 多线程:在 ASan 稳定后上 TSan。
九、方案六:strace / ltrace / 日志夹逼
9.1 strace:崩前最后的系统交互
strace -f -tt -o /tmp/st.log ./segfault_demo null
tail -40 /tmp/st.log
关注:
… openat(…)=3
… read(…)=…
+++ killed by SIGSEGV +++
说明崩溃发生在用户态逻辑,而非该 syscall 内部返回错误。若最后是 mmap/mprotect/dlopen,查模块加载。
9.2 ltrace:崩前最后一个库调用
ltrace -f -S ./segfault_demo overflow 2>&1 | tail -30
常见「凶手」:memcpy、strcpy、sprintf、strlen(NULL)。
9.3 日志夹逼(无工具时的土办法)
二分:在主路径打点日志 / 计数器
→ 找到最后一条成功日志与崩溃之间的代码块
→ 该块内再二分
配合 fflush 或 write(2,…) 避免缓冲丢失最后一行。
十、方案七:符号、PIE、addr2line 精算
10.1 编译建议
# 调试构建
CFLAGS="-g -O0 -fno-omit-frame-pointer -Wall -Wextra"
# 发布仍要可追溯:分离调试信息
gcc -g -O2 -o app app.c
objcopy –only-keep-debug app app.debug
strip -g app
objcopy –add-gnu-debuglink=app.debug app
10.2 PIE / ASLR 下地址换算
现代发行版默认 PIE:ip 每次不同。
方法 A(推荐): 用同一构建的 gdb ./app core,gdb 按 core 里的加载基址自动解析。
方法 B:手动
运行时 ip = 加载基址 + (文件内偏移)
文件内偏移 ≈ ip – maps 中该模块 start
再用:
addr2line -e ./app -f -C -p <文件内偏移>
或
llvm-symbolizer / eu-addr2line
# 查看是否 PIE
readelf -h ./segfault_demo | grep Type
# DYN → PIE;EXEC → 传统固定加载(少见)
10.3 调试机临时关闭 ASLR
sudo sysctl kernel.randomize_va_space=0
# 复现后务必改回 2
sudo sysctl kernel.randomize_va_space=2
仅用于本地对照地址,不要在生产长期关闭。
10.4 nm / objdump 辅助
nm -C ./segfault_demo | grep boom
objdump -d ./segfault_demo | less
与系列文 nm、ltrace 工具链可串联使用。
十一、方案八:多线程、信号与生产环境
11.1 多线程崩溃
(gdb) info threads
(gdb) thread 3
(gdb) bt full
(gdb) thread apply all bt
要点:
-
收到 SIGSEGV 的线程 不一定是逻辑写坏内存的线程。
-
查共享堆/全局时用 ASan;怀疑竞态用 TSan + 加压(stress、多连接)。
-
pthread 创建失败、栈大小过小(pthread_attr_setstacksize)可导致栈溢出。
11.2 自定义信号栈 / handler
若业务捕获 SIGSEGV 做「优雅退出」:
-
handler 内只做 async-signal-safe 极简操作(写管道、_exit)。
-
务必 留 core 或把 ucontext 中的 PC/地址打到专用日志。
-
错误的 handler 是「线上只有一句 segfault、本地复现不了」的常见原因。
11.3 生产最小采集包
崩溃时希望自动拥有:
|
二进制 build-id |
readelf -n app | grep Build |
|
版本 / commit |
嵌入字符串或包名 |
|
core 或堆栈 |
coredumpctl / Breakpad / crashpad |
|
周围日志 |
崩前 2 分钟应用日志 |
|
环境 |
内核版本、libc、容器 or not |
11.4 容器与权限
# 容器内
cat /proc/sys/kernel/core_pattern # 可能指向宿主机路径
ulimit -c
# k8s 常需 volume 或节点级 coredump 配置
十二、实战案例(逐步对照)
cd examples/segfault_demo
make && make asan
make demo
案例 segfault_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void usage(const char *argv0)
{
fprintf(stderr,
"Usage: %s null|overflow|uaf|stack|wild\\n", argv0);
}
static __attribute__((noinline)) void boom_null(void)
{
volatile int *p = NULL;
fprintf(stderr, "[demo] null deref\\n");
fflush(stderr);
*p = 42;
}
static __attribute__((noinline)) void boom_overflow(void)
{
char *p = malloc(16);
int i;
if (!p)
exit(1);
fprintf(stderr, "[demo] heap overflow write\\n");
fflush(stderr);
for (i = 0; i < 64; i++)
p[i] = (char)('A' + (i & 15));
/* 可能在 free 时因堆元数据损坏再崩 */
free(p);
}
static __attribute__((noinline)) void boom_uaf(void)
{
char *p = malloc(32);
if (!p)
exit(1);
strcpy(p, "hello");
free(p);
fprintf(stderr, "[demo] use-after-free write\\n");
fflush(stderr);
p[0] = 'X';
printf("%s\\n", p);
}
static __attribute__((noinline)) void deep(int n)
{
volatile char buf[4096];
buf[0] = (char)n;
if (n > 0)
deep(n – 1);
}
static __attribute__((noinline)) void boom_stack(void)
{
fprintf(stderr, "[demo] stack overflow via deep recursion\\n");
fflush(stderr);
deep(1000000);
}
static __attribute__((noinline)) void boom_wild(void)
{
volatile unsigned long addr = 0x1;
volatile int *p = (int *)addr;
fprintf(stderr, "[demo] wild pointer write\\n");
fflush(stderr);
*p = 1;
}
int main(int argc, char **argv)
{
if (argc < 2) {
usage(argv[0]);
return 1;
}
if (!strcmp(argv[1], "null"))
boom_null();
else if (!strcmp(argv[1], "overflow"))
boom_overflow();
else if (!strcmp(argv[1], "uaf"))
boom_uaf();
else if (!strcmp(argv[1], "stack"))
boom_stack();
else if (!strcmp(argv[1], "wild"))
boom_wild();
else {
usage(argv[0]);
return 1;
}
return 0;
}
Makefile
CC ?= gcc
CFLAGS ?= -Wall -Wextra -g -O0 -fno-omit-frame-pointer
ASAN_CFLAGS ?= -Wall -Wextra -g -O1 -fsanitize=address -fno-omit-frame-pointer -U_FORTIFY_SOURCE
TARGET := segfault_demo
ASAN := segfault_demo_asan
.PHONY: all asan clean demo help
all: $(TARGET)
$(TARGET): segfault_demo.c
$(CC) $(CFLAGS) -o $@ $<
asan: $(ASAN)
$(ASAN): segfault_demo.c
$(CC) $(ASAN_CFLAGS) -o $@ $<
demo: all asan
@echo "=== null + gdb ==="
@echo " gdb –args ./$(TARGET) null"
@echo "=== overflow + ASan ==="
@echo " ./$(ASAN) overflow"
@echo "=== uaf + ASan ==="
@echo " ./$(ASAN) uaf"
@echo "=== dmesg after crash ==="
@echo " ./$(TARGET) null ; dmesg | tail -5"
@echo "=== strace ==="
@echo " strace -f ./$(TARGET) null"
clean:
$(RM) $(TARGET) $(ASAN) core core.*
help: demo
12.1 空指针:dmesg → gdb 一行定位
./segfault_demo null
dmesg | tail -5
# 期望: segfault at 0 …
gdb -q –args ./segfault_demo null \\
-ex "set pagination off" -ex run -ex "bt full" -ex quit
# 期望栈顶在 boom_null,看到 *p=42 一类语句
知识点: at 0 + 用户写 → 空指针;栈干净、根因=崩点。


12.2 堆越界:为何 gdb「骗人」,ASan「救命」
# 纯 gdb:可能崩在 free/malloc 内部
gdb -q –args ./segfault_demo overflow -ex run -ex bt -ex quit
# ASan:指向你的 for 循环越界写
./segfault_demo_asan overflow
知识点: 崩点在分配器 ≠ 根因在分配器。

12.3 UAF
./segfault_demo_asan uaf
# 报告含: freed by … / allocated by …
valgrind –track-origins=yes ./segfault_demo uaf
12.4 栈溢出:不信任 bt
./segfault_demo stack
# 可能长时间后 SIGSEGV;bt 深层重复 deep()
# 查: 递归终止条件、线程栈大小、巨型 VLA

12.5 野指针低地址
./segfault_demo wild
dmesg | tail -3
# at 1 或极低地址 → 未初始化/错误常量指针

12.6 仅有线上 ip:coredumpctl 优先于手算
ulimit -c unlimited
./segfault_demo null
coredumpctl gdb -1 # 若走 systemd
# 在 gdb 里 bt,避免手算 PIE
12.7 「15 分钟标准路径」清单
□ 固定复现命令与输入数据
□ 能编?→ ASan 跑通并保存完整报告
□ 不能?→ ulimit -c;复现;gdb core;bt full + thread apply all bt
□ 栈在 libc 分配器?→ 回头查近期写缓冲区代码,开 ASan
□ 多线程随机?→ 加压 + TSan
□ 记录:build-id、命令、栈、dmesg 一行、是否仅特定机型
十三、根因分类诊断手册
|
at 0 / 低地址 |
NULL/野指针 |
gdb bt |
查指针来源是否未赋值 |
|
崩在 malloc/free/malloc_consolidate |
更早堆破坏 |
ASan |
查 memcpy 长度、越界 for |
|
随机崩溃、改输入又好 |
未初始化 / 竞态 |
ASan + TSan |
加压、固定种子 |
|
bt 乱、重复递归帧 |
栈溢出 |
ASan / 查递归 |
增大栈仅是权宜 |
|
崩在 ret / 奇异地址执行 |
栈砸 / 坏函数指针 |
ASan + -fstack-protector |
查溢出写 |
|
仅某 .so 加载后崩 |
ABI/初始化 |
strace+gdb |
查构造函数/符号版本 |
|
mmap 文件相关 |
映射越界/截断 |
maps + 信号类型 |
区分 SEGV/BUS |
|
优化打开才崩 |
UB / 未初始化 |
UBSan -O2 对照 |
修 UB 非关优化 |
十四、工程化:CI 与线上闭环
14.1 CI 建议门禁
# 伪配置
build_asan: CFLAGS=-fsanitize=address -g -O1
test: ./run_unit_tests # 失败即挂
# 可选夜间 job: valgrind 子集
14.2 版本与符号归档
-
每个发布包保存:app + app.debug + 依赖库清单
-
core 分析流水线:自动 coredumpctl → 符号化 → 工单
14.3 与本系列其它工具的衔接
|
谁崩了 |
ps / dmesg |
|
崩时在忙什么 |
top、strace |
|
内存根因 |
本篇 + ASan |
|
CPU 热点误判为「卡死」 |
perf / tiptop(非 SEGV) |
|
网络假死 |
ss(非 SEGV) |
十五、FAQ 与常见坑
15.1 本地好好的,线上崩?
-
未初始化内存:Debug 下碰巧为 0
-
优化级别不同暴露 UB
-
并发与负载不同
-
库版本 / CPU 特性不同 → 用线上同构建二进制 + core,不要只在本地用 Debug 猜。
15.2 bt 全是 ??
file ./app
readelf -S ./app | grep debug
# 安装 libc 等 debuginfo;配置 debuginfod
15.3 ASan 报错但「逻辑上不该」
-
假阳性极少,优先信 ASan
-
查是否有内联汇编、自建分配器未拦截
-
确认所有 TUs 都开了 sanitize
15.4 gdb 里看不到变量:optimized out
用 -O0/-O1 复现;或看反汇编与寄存器。
15.5 崩溃在第三方闭源库
-
用其符号包(若有)
-
strace/ltrace 看传入参数(缓冲区长度、NULL)
-
最小复现发给厂商;自己侧先 ASan 排除己方越界踩坏对方
15.6 fork 后子进程崩
gdb –args ./app
(gdb) set follow-fork-mode child
(gdb) set detach-on-fork off
或 strace -f。
15.7 Rust/Go 混编、JNI
segfault 仍可能来自 C 侧;用混合调试符号,ASan 需覆盖 C 库部分。
十六、总结
16.1 记住三条
SIGSEGV 是页权限/映射问题的症状;堆破坏时崩点常常「冤枉」分配器。
能复现就 ASan;死了就 core+gdb;只剩日志就解析 dmesg + 符号。
四件套:故障地址、指令位置、调用栈、分配/释放时间线。
16.2 方案速查
|
P0 |
ASan |
开发期根因定位 |
|
P0 |
coredump+gdb |
死后完整栈 |
|
P1 |
gdb 前台 |
本地交互 |
|
P1 |
dmesg |
最快痕迹 |
|
P2 |
Valgrind |
无插桩兜底 |
|
P2 |
strace/ltrace |
缩小行为窗口 |
|
P2 |
TSan/UBSan |
并发与 UB |
16.3 最小命令卡
# 痕迹
dmesg -T | grep -i segfault | tail
# 死后
ulimit -c unlimited
./app … ; coredumpctl gdb -1 # 或 gdb ./app core
# 生前
gdb -q –args ./app … -ex run -ex "bt full" -ex quit
# 根因
gcc -g -O1 -fsanitize=address -fno-omit-frame-pointer -o app app.c && ./app
# 兜底
valgrind –track-origins=yes ./app
strace -f -o /tmp/st.log ./app





