程序收到 SIGSEGV 后停在 memcpy(),问题就一定出在这个函数吗?通常不是。越界写可能早已破坏对象,悬空指针也可能在很久以后才被解引用。GDB 崩溃分析真正要做的是保存现场,然后回答:哪个线程收到什么信号,调用路径是什么,关键对象何时开始不合理。
先保证 core 文件可用
分析前需要四样东西:core 文件、产生它的可执行文件、完全匹配的共享库,以及调试符号。重新编译一个“差不多”的二进制通常不够,因为地址布局、优化和源码行都可能变化。
传统 core 可先这样开启:
1 | ulimit -c unlimited |
core_pattern 可能把 core 交给 systemd-coredump 或其他收集程序,因此当前目录没文件不等于没有生成。容器、服务单元的 LimitCORE、磁盘配额和安全策略也会改变结果。
进入 GDB 后按证据顺序看
1 | set pagination off |
先看 info files 和共享库是否匹配,再找带有致命信号的线程。对死锁或 watchdog 触发的 dump,真正有用的可能是另一个持锁线程,因此 thread apply all bt full 往往比只看当前线程更重要。
沿栈向上走时,每一帧只回答一个具体问题:传入的长度是否越界,指针是否指向已释放区域,对象字段是否违反状态机约束。看到一个异常值后,继续确认它来自函数参数、共享对象还是已经损坏的栈,别急着把最后赋值者当成根因。
优化构建会改变你看到的东西
-g 负责调试信息,-O0 才是关闭优化,两者不是同一个开关。生产二进制可以保留优化并单独保存调试符号;构建系统应记录 commit、编译器、flags 和 build ID。常用的折中设置是保留帧指针,但这会影响性能和 ABI 选择,应通过项目构建配置统一决定。
1 | readelf -n ./demo | grep -A3 'Build ID' |
如果 GDB 显示 <optimized out>、栈回溯突然断裂或多个源码行合并,不代表变量从未存在。编译器可能把变量留在寄存器、消除存储或内联函数。此时结合反汇编、寄存器和调用约定检查,必要时用同一输入在 ASan/UBSan 构建上复现。
什么时候 core 也不够
- 堆越界在数分钟后才触发崩溃,core 只留下最后受害者。
- 数据竞争依赖时序,离线快照看不到发生顺序。
- 栈或 unwind 信息已损坏,回溯结果不可信。
- 二进制、共享库或 debuginfo 不匹配,源码行只是错配结果。
- 进程被
SIGKILL终止,通常没有用户态崩溃现场可供回溯。
这类问题需要更早的证据:Sanitizer、崩溃前环形日志、请求 ID、关键状态变更记录,或可控的复现输入。工具越靠近错误首次发生的位置,推断链越短。
证据边界
core 是某一时刻的快照,能证明寄存器、内存和线程当时是什么状态,不能单独证明哪一行最早破坏了它们。文中的命令适用于常见 Linux ELF 环境;转储范围、路径和隐私内容受 core_pattern、coredump_filter、systemd 配置及权限控制。core 可能包含密钥和用户数据,上传或长期保存前必须脱敏并限制访问。