欢迎光临
我们一直在努力

Linux 段错误(Segmentation fault)怎么定位?8 种实战方法从 dmesg 到 ASan/gdb

目录

一、前言——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

    赞(0)
    未经允许不得转载:171主机测评 » Linux 段错误(Segmentation fault)怎么定位?8 种实战方法从 dmesg 到 ASan/gdb
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址