1. 什么时候需要一份Core Dump测试手册
先讲个我实际碰到过的场景。有次线上服务凌晨三点崩溃,日志最后一行停在一条看似正常的业务日志上,事故报告写的是"服务异常退出"。因为没有core文件,整个排查持续了两天,最终靠猜测和反复压测才勉强定位到一处野指针。在那之后我养成了一个习惯:任何要上线的C/C++服务,必须先验证这台机器上到底能不能正常产生core文件。这就是这份测试操作手册存在的意义。
Core Dump是Linux内核在进程异常终止时自动生成的内存镜像文件,里面保存了进程终止那一刻的完整内存状态、寄存器值、堆栈信息。你可以把它理解成飞机的黑匣子——事故发生瞬间的现场记录。它能解决的问题非常明确:服务崩了之后,不用靠猜、不用靠一遍遍复现,直接用gdb加载core文件就能看到崩溃发生在哪一行、调用链是怎样的、变量值是多少。
这份手册适合谁?适合刚接触Linux开发不久、写过C/C++但还没正经排查过崩溃问题的工程师,也适合运维同学——当开发跟你说"帮忙开一下core dump"的时候,你得知道改哪些参数、往哪儿找文件。我会从底层机制讲起,再给出可一键执行的配置、可编译运行的最小复现程序、gdb调试的完整思路,以及我在实际环境中踩过的一些坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Core Dump生成的内核机制与开关设置
2.1 为什么新装的Linux环境默认不生成Core文件
很多人在自己电脑上写了个段错误程序,跑完以为能生成core文件,结果当前目录空荡荡。这不是你的程序有问题,是内核和shell联手把core"扔掉"了。
Linux里有两层开关同时管着core文件的生成。第一层是进程的资源限制(RLIMIT_CORE),也就是常说的ulimit -c。第二层是内核参数kernel.core_pattern,它决定core文件写到哪个目录、用什么文件名格式。两层必须配合好,任何一层不给力,core文件都出不来。
先看看当前环境的状态:
bash复制ulimit -c
cat /proc/sys/kernel/core_pattern
如果ulimit -c显示0,说明当前shell启动的进程允许生成的core文件大小被限制为0字节——等于直接禁止。如果core_pattern显示的是管道符开头的字符串(比如|/usr/lib/systemd/systemd-coredump),说明core文件被内核转交给了systemd-coredump处理,不会以普通文件形式出现在当前目录。
2.2 进程层面的开关:ulimit
ulimit -c的单位是512字节块(不同shell可能有差异)。设置为unlimited最省心:
bash复制ulimit -c unlimited
这个命令对当前shell及其子进程生效。注意,如果你是在终端里执行的,然后手动启动服务程序,服务程序会继承这个设置;但如果你是通过systemd服务启动的程序,需要另配。
如果只是想临时验证某个程序,不想影响当前shell的全局设置,可以这样:
bash复制bash -c 'ulimit -c unlimited && ./your_program'
2.3 内核层面的配置:core_pattern
/proc/sys/kernel/core_pattern是真正的核心开关。推荐直接写到sysctl配置文件里,重启不丢:
bash复制# 查看当前值
sysctl kernel.core_pattern
# 临时修改
sysctl -w kernel.core_pattern=/data/coredump/core.%e.%p.%t
# 永久生效
echo "kernel.core_pattern=/data/coredump/core.%e.%p.%t" >> /etc/sysctl.d/99-coredump.conf
sysctl -p /etc/sysctl.d/99-coredump.conf
core_pattern里的百分号占位符是重点,下表是我实际用下来最常用的几个:
| 占位符 | 含义 | 典型使用场景 |
|---|---|---|
%e |
可执行文件名(带路径时只保留文件名) | 快速区分是哪个程序崩溃的 |
%p |
进程PID | 区分同一程序多次运行实例 |
%t |
时间戳(自1970-01-01以来的秒数) | 配合日期排查崩溃时间点 |
%s |
触发core的信号编号 | 一眼看出是段错误还是abort |
%h |
主机名 | 多机器采集时确认来源 |
%% |
输出一个百分号 | 字面量 |
我常用的格式是core.%e.%p.%s.%t,文件命名里同时带程序和PID,排查时不用翻日志去对应。
2.4 systemd参与的默认行为
现在主流发行版(CentOS 7+、Ubuntu 16.04+)默认的core_pattern往往是:
code复制|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h
这个管道符意味着:内核不直接写文件,而是把core内容通过管道交给systemd-coredump进程处理。systemd-coredump会把core文件存到/var/lib/systemd/coredump/,同时用journald记录崩溃元数据。这个机制本身很好,但坑在于很多人不知道,还在当前目录找core文件,自然找不到。
如果不想改全局配置,又想用systemd机制排查,可以用:
bash复制coredumpctl list
coredumpctl info
coredumpctl gdb
coredumpctl gdb能直接用gdb打开最近一次崩溃的core文件,连文件名都不用管。我个人习惯:开发测试机上直接把core_pattern改成普通文件路径,方便;生产环境保留systemd机制,配合coredumpctl查更安全,还能防止core文件写爆磁盘。
3. 用最小复现程序验证Core Dump是否生效
配置改完之后,马上验证一件事:这个环境是不是真的能产出core文件。不要等程序自然崩溃再去验证,那样没法区分是"没配置好"还是"程序没崩溃"。我习惯用一个极简的C程序做冒烟测试。
3.1 五种主动触发崩溃的方式
创建一个test_crash.c,里面有多种崩溃姿势,方便以后反复使用:
c复制#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void crash_by_null_pointer(void) {
int *p = NULL;
*p = 42; // 空指针解引用,触发SIGSEGV
}
void crash_by_abort(void) {
abort(); // 主动终止,触发SIGABRT
}
void crash_by_stack_overflow(void) {
volatile char buf[1024 * 1024];
buf[0] = 1;
crash_by_stack_overflow(); // 无限递归,栈耗尽
}
void crash_by_invalid_instruction(void) {
__builtin_trap(); // 非法指令,触发SIGILL
}
void *worker_thread(void *arg) {
sleep(1);
int *p = NULL;
*p = 1; // 工作线程里崩溃
return NULL;
}
void crash_in_thread(void) {
pthread_t tid;
pthread_create(&tid, NULL, worker_thread, NULL);
pthread_join(tid, NULL);
}
int main(int argc, char *argv[]) {
printf("started, pid=%d\n", getpid());
int type = argc > 1 ? atoi(argv[1]) : 1;
switch (type) {
case 1: crash_by_null_pointer(); break;
case 2: crash_by_abort(); break;
case 3: crash_by_stack_overflow(); break;
case 4: crash_by_invalid_instruction(); break;
case 5: crash_in_thread(); break;
default: crash_by_null_pointer(); break;
}
return 0;
}
编译时务必加上-g,调试信息是后面gdb分析的基础;-O0是为了防止编译器优化掉关键代码:
bash复制gcc -g -O0 -o test_crash test_crash.c -lpthread
3.2 执行并确认Core文件落盘
执行冒烟测试前,先确认当前shell的ulimit设置:
bash复制ulimit -c unlimited
./test_crash 1
正常情况下,程序会输出started, pid=xxxx后立即段错误退出。然后检查core文件:
bash复制ls -lh core.*
file core.*
file命令输出里会标明"ELF 64-bit LSB core file",并且能直接读出这个core文件由哪个程序产生、属于哪个PID,这个信息很有用——文件名可能被误改,但core文件头里的信息骗不了人。
如果第一个姿势成功了,剩下的逐一跑一遍,确认类型2到5都能正常生成core。这也相当于给这台服务器做了一次"core dump功能体检"。
3.3 生成失败时的快速定位顺序
core文件没出来,按下面顺序排查,基本一查一个准:
ulimit -c:确认不是0,不是unlimited也要确认大于程序内存占用。cat /proc/sys/kernel/core_pattern:看有没有指向有效目录。如果是管道符给systemd,去/var/lib/systemd/coredump/找。- 配置的目录是否存在且对运行用户可写:很多人配了
/data/coredump但没建目录,core文件自然是空的。 - 确认程序是不是被某个进程管理器重启了:systemd服务默认会覆盖ulimit,需要在service文件里配
LimitCORE=infinity。
帮人排查过太多次"为啥没core",90%是前两条中的某一条。
4. 用GDB还原崩溃现场:完整的Core分析流程
4.1 基本加载与调用栈查看
core文件拉到手,第一件事就是用gdb加载。格式固定为gdb <可执行文件> <core文件>:
bash复制gdb ./test_crash core.test_crash.12345.1700000000
进入gdb交互界面后,最常用的是bt(backtrace)查看崩溃时的调用栈:
code复制(gdb) bt
#0 0x00000000004005e6 in crash_by_null_pointer () at test_crash.c:6
#1 0x0000000000400643 in main (argc=2, argv=0x7ffd5a2e0d08) at test_crash.c:30
#2 0x00007f8a4b2acb6b in __libc_start_main () from /lib64/libc.so.6
这个输出信息量很大:#0是崩溃点,在test_crash.c第6行,函数是crash_by_null_pointer;#1是它的调用者,main函数里第30行。下面是libc的启动函数,通常不用关心。
4.2 精确定位崩溃行与变量信息
调用栈只能知道你"在哪个函数里崩的",要精确到是哪一行、内存里发生了什么,还要做两步。
第一步是查看崩溃指令的汇编:
code复制(gdb) x/i $rip
=> 0x4005e6 <crash_by_null_pointer+10>: mov DWORD PTR [rax], 0x2a
这条指令把0x2a(十进制42)写入rax寄存器指向的地址。再配合查寄存器:
code复制(gdb) info registers rax
rax 0x0 0
rax是0,一条mov写零地址,SIGSEGV的真相水落石出——空指针解引用。
第二步是查看当前栈帧的源码上下文和局部变量:
code复制(gdb) list
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <pthread.h>
4 #include <unistd.h>
5
6 void crash_by_null_pointer(void) {
7 int *p = NULL;
8 *p = 42;
9 }
(gdb) info locals
No symbol table info available.
如果编译时加了-g但没加-O0,局部变量可能被优化掉,info locals偶尔会报"no symbol table info",这是编译器把变量放寄存器或优化没了,不影响栈回溯。
4.3 查看完整线程栈:崩溃线程不一定在主线程
实际生产环境中,崩溃经常发生在某个工作线程里。用info threads查看全部线程:
code复制(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7f8a4b2b2700 (LWP 12345) "test_crash" 0x00007f8a4b2acb6b in __libc_start_main () from /lib64/libc.so.6
2 Thread 0x7f8a4a9e1700 (LWP 12346) "test_crash" crash_by_null_pointer () at test_crash.c:8
带*的是当前gdb选中的线程,不一定是对应崩溃线程。崩溃线程可以通过core文件里的信号信息确定,也能通过thread apply all bt给所有线程打一遍调用栈:
code复制(gdb) thread apply all bt
Thread 2 (Thread 0x7f8a4a9e1700 (LWP 12346)):
#0 0x00000000004005e6 in crash_by_null_pointer () at test_crash.c:8
#1 0x0000000000400637 in worker_thread () at test_crash.c:20
#2 0x00007f8a4b0a1aa1 in start_thread () from /lib64/libpthread.so.0
#3 0x00007f8a4b1e9bcd in clone () from /lib64/libc.so.6
Thread 1 (Thread 0x7f8a4b2b2700 (LWP 12345)):
#0 0x00007f8a4b2acb6b in __libc_start_main () from /lib64/libc.so.6
一眼就能看出来,真正崩的是Thread 2,在worker_thread里调用了空指针函数。线上排查多线程崩溃时,这个命令是首选,不要光看当前线程。
4.4 查看崩溃信号与内存映像
gdb启动时会自动读取core文件头里的信号信息,通常在加载后第一行就显示"Core was generated by ./test_crash 1'"和"Program terminated with signal SIGSEGV, Segmentation fault"。如果没看到,可以手动用info signals或直接看core文件的readelf -n`:
bash复制readelf -n core.test_crash.12345.1700000000
输出里包含CORE段,能看到崩溃时的信号编号、时间、PID等信息。这个在自动化核对core文件归属时很有用。
4.5 高级排查:死锁现场、堆内存信息
core能看的不只是崩溃点。如果程序hang住然后被kill -ABRT,core里能看到所有线程卡在哪个锁上;如果是SIGKILL干掉的,core里同样有完整线程栈。这类场景下,thread apply all bt配合frame <编号>切换栈帧、info frame查当前帧详情,能辅助判断锁竞争和资源等待。
另一个实用的命令是x系列——按地址查看内存内容。比如怀疑某块缓冲区溢出,可以先print &buf拿到变量地址,再x/32bx 地址看内存里的原始字节。这比gdb之外的工具直接多了。
5. 把Core Dump测试纳入日常开发与运维流程
会手动分析core只是第一步。真正让core dump这项工作产生价值,是把它变成日常开发、测试、运维流程里自动运转的一环。
5.1 自动化回归:让崩溃用例在持续集成里自动产出Core
如果你的项目里有崩溃类的bug单,最怕的就是"修好了但以后又回归"。我的做法是把复现程序写成一个独立的测试用例,放在CI里跑:
脚本逻辑也很简单:
bash复制#!/bin/bash
set -e
ulimit -c unlimited
mkdir -p ./artifacts/coredump
echo "kernel.core_pattern=./artifacts/coredump/core.%e.%p" > /proc/sys/kernel/core_pattern 2>/dev/null || true
./test_crash 1 || true
CORE_FILE=$(ls -t artifacts/coredump/core.* 2>/dev/null | head -1)
if [ -n "$CORE_FILE" ]; then
echo "core file generated: $CORE_FILE"
# 用gdb提取关键栈信息输出到日志
gdb -batch -ex "bt" ./test_crash "$CORE_FILE" > artifacts/coredump/bt.log 2>&1
exit 0
else
echo "ERROR: no core file generated"
exit 1
fi
这里有个细节:./test_crash 1 || true。因为程序返回码是139(128+11,SIGSEGV),直接执行会让CI把整个步骤标记为失败,但我们的目的就是让它崩溃并采core,所以加|| true把返回码吞掉,后续用core文件的存在性做断言。
5.2 上线前的Core Dump健康检查脚本
我每次给C/C++服务做上线检查,会顺手跑一个脚本确认core环境正常:
bash复制#!/bin/bash
# check_coredump_env.sh
echo "=== core_pattern ==="
cat /proc/sys/kernel/core_pattern
echo "=== core_uses_pid ==="
cat /proc/sys/kernel/core_uses_pid
echo "=== ulimit -c ==="
ulimit -c
echo "=== disk free ==="
df -h /var/lib/systemd/coredump 2>/dev/null || df -h /data/coredump 2>/dev/null || df -h /tmp
echo "=== coredumpctl (if systemd) ==="
coredumpctl list --no-pager 2>/dev/null | tail -5 || true
磁盘空间容易被忽略,core文件有时候能到几个GB甚至更大,生产环境一定要确认存放路径的磁盘配额。
5.3 Core文件的保留策略与定期清理
core文件不清理,生产环境迟早出事。线上跑了几周才发现/var/lib/systemd/coredump占了80G的案例我见过不止一次。两种常见清理策略:
定时任务清理旧文件:
bash复制# 删除7天前的core文件
find /data/coredump -name 'core.*' -type f -mtime +7 -delete
用logrotate管理(更规范):
conf复制/data/coredump/core.* {
daily
rotate 7
maxsize 2G
missingok
compress
postrotate
/bin/kill -s HUP $(pidof systemd-journald) 2>/dev/null || true
endscript
}
我个人在生产服务器上同时开了两条路:所有core文件先落盘到独立分区,同时保留systemd的journal索引。这样要深挖有文件,要快速看崩溃统计有coredumpctl list。
5.4 容器化服务怎么处理
容器里的core dump是另一个高频困惑点。注意:kernel.core_pattern是宿主机内核参数,容器内改不了,而且docker默认的core_pattern直接就是管道给systemd-coredump,容器里看到的现象往往是"程序崩了但当前目录没有core文件"。
容器内要拿到core,有几个思路:
第一,启动容器时加参数:
bash复制docker run --ulimit core=-1 --mount type=bind,src=/data/coredump,dst=/coredump \
-e CORE_PATTERN=/coredump/core.%e.%p your_image
然后在容器里设置sysctl -w kernel.core_pattern=/coredump/core.%e.%p(需要容器以特权模式或允许CAP_SYS_ADMIN)。
第二,程序内部用prctl系统调用给自己设置core pattern:
c复制#include <sys/prctl.h>
#include <stdio.h>
int main() {
prctl(PR_SET_DUMPABLE, 1, 0, 0, 0);
// ...
}
但这只能解决dumpable权限,改不了core_pattern本身。
最省事的还是在宿主机上调整core_pattern为普通文件路径,再通过volume挂载进容器,让程序崩溃时直接写到宿主机磁盘。容器适合跑服务,不适合做调试环境,如果只是想快速验证容器里程序的崩溃栈,直接docker logs看段错误时的stderr输出,配合-g编译的符号信息,比折腾core文件快得多。
6. 常见坑与避坑经验:我在Core Dump测试中踩过的雷
技术上最核心的部分讲完了,下面这部分才是我最想分享的。下面是这些年我跟core dump打交道时踩过的典型坑,都整理成了现象+原因+处理方式的对照:
| 现象 | 根本原因 | 处理方式 |
|---|---|---|
| 程序段错误退出,但当前目录没有core文件 | ulimit -c为0,或core_pattern是管道字符 | ulimit -c unlimited;cat /proc/sys/kernel/core_pattern确认是不是` |
| 配置了core文件路径,但还是没生成 | 目录不存在或当前用户无写权限 | 先mkdir -p再chmod,目录权限至少755 |
| core文件存在,但gdb加载报"not a core file" | 写core时进程崩溃太严重,文件截断 | 确认磁盘空间,换崩溃方式或加大RLIMIT_CORE |
| 文件名全是core.PID,不知道是哪个程序 | 没在core_pattern里加%e |
统一格式为core.%e.%p.%t |
| gdb里加载bt没有符号,只看到地址 | 可执行文件是strip过的,或编译时没加-g | 用未strip的版本;线上strip的话保留debug符号文件备用 |
| 崩溃线程栈看着不对劲,有乱码/地址 | 栈溢出导致栈被破坏,bt不可信 | 用info registers看rsp/rip自行分析,别硬信bt |
| ulimit -c unlimited设了但systemd服务还是没core | systemd服务默认LimitCORE=0 | 在service文件里加LimitCORE=infinity,然后daemon-reload |
| 生产环境不敢随便改core_pattern | 担心core文件泄露敏感内存数据 | 核心思路:设置core文件权限,崩溃后立即清理,敏感进程禁用core |
6.1 栈损坏时怎么抢救
多线程程序里栈被踩烂的情况特别多。当bt输出的内容像乱码时,不要慌。核心思路是绕开栈回溯,靠寄存器定位。
查看寄存器:
code复制(gdb) info registers rip rsp rbp
rip 0x401234 0x401234
rsp 0x7ffc5e2f0e40 0x7ffc5e2f0e40
rbp 0x7ffc5e2f0e60 0x7ffc5e2f0e60
尝试手动回溯:$rbp一般指向当前栈帧底,而$rbp指向的地址处存放的是上一个栈帧的rbp值。x/xg $rbp看一下,如果值合理,可以往上继续读:
code复制(gdb) x/gx $rbp
0x7ffc5e2f0e60: 0x00007ffc5e2f0e80
(gdb) x/gx 0x00007ffc5e2f0e80
0x7ffc5e2f0e80: 0x00007ffc5e2f0ea0
如果这个链还能走下去,说明栈底部分没被完全破坏;如果断链,配合二进制文件的符号表和info symbol 地址也能大概判断是在哪个函数。
6.2 core文件权限与敏感数据扩散问题
core文件里包含进程内存的全部内容。这意味着:密码、密钥、会话token、业务数据,全在core文件里裸奔。生产环境的数据库进程如果崩溃并生成core,文件里大概率有查询缓存、连接串。
所以我的生产环境策略是:
- 对高敏感服务(数据库、支付核心),在启动脚本里
ulimit -c 0,直接不生成core,改用日志埋点和远程诊断。 - 对普通服务,core文件所在目录,组内共享读权限即可,不让其他账号可读。
- 线上core文件保留时间不超过7天,用定时任务或logrotate强制过期清理。
- 如果怀疑攻击者可能主动触发崩溃来制造core文件窃取内存,那就关闭prctl的dumpable位,或者干脆全机不开core。
bash复制# 设置新进程默认不产生core
echo "core" > /proc/sys/kernel/core_pattern # 或者用 sysctl -w kernel.core_pattern=core
# 如果不想改pattern,也可以对单个可执行文件关掉dumpable
6.3 core文件对应的可执行文件必须严格版本匹配
调试线上core时最隐蔽的坑是版本不匹配。开发机上的二进制和线上跑的二进制如果版本不同,gdb加载时符号会错乱,bt输出的行号和真实代码对应的可能不是同一份,误导排查方向。
正确做法:
bash复制# 线上采集core时,同时把对应二进制的md5记录到元数据
md5sum /usr/local/bin/your_service > core.md5
# 回开发机后,用同一版本的编译产物调试
git checkout <线上版本tag>
make
gdb ./your_service /path/to/core.xxx
如果线上是release版strip过符号,一定保留对应的带符号版本(比如.debug文件),gdb加载时手动指定符号文件:
bash复制(gdb) symbol-file /path/to/your_service.debug
6.4 coredumpctl的局限与替代方案
coredumpctl很方便,但它是systemd生态的,不是所有场景都好用。比如容器里的进程崩溃时,systemd-coredump抓到的是宿主机视角的进程信息,堆栈里看到的可能是容器init进程,不是业务进程。再比如高并发下多个进程同时崩溃,systemd-coredump处理会有队列延迟,core采集不及时。
这个时候,用一个独立的core采集daemon更可靠。我写过一个简单的bash版守护逻辑,配合inotify监控core目录,新core文件一到就自动压缩、打上时间戳、推送到对象存储:
bash复制#!/bin/bash
# watch_core.sh
WATCH_DIR=/data/coredump
REMOTE_DIR=/backup/coredump/$(hostname)
inotifywait -m -e close_write "$WATCH_DIR" --format '%f' | while read FILE
do
cd "$WATCH_DIR"
case "$FILE" in
core.*)
gzip "$FILE"
# 从文件名里解出PID和时间戳
ts=$(stat -c %Y "$FILE.gz")
target="$REMOTE_DIR/$(date -d @$ts +%Y%m%d)/$FILE.gz"
mkdir -p "$(dirname "$target")"
mv "$FILE.gz" "$target"
;;
esac
done
不建议直接在产品环境跑这种daemon,但测试环境很实用。产品环境的core采集还是要走厂商的崩溃监控方案或自研agent,重点在于文件收集、元数据记录、快速回传三步。
7. 从一个实际跟踪过的崩溃问题看完整测试流程
理论讲再多,不如完整走一遍。下面这个案例是我在一个内部测试环境里跟踪过的(程序是简化的),从头到尾串起整个core dump测试链路。
程序是一个多线程队列消费服务,线上偶发崩溃,复现率不高。我的处理分三步:第一步,锁现场。在测试环境复现时,保证core_pattern指向独立目录并确认磁盘空间足够,同时在service配置里加上LimitCORE=infinity,因为原先是systemd托管启动,ulimit被吞了。
第二步,抓现场。跑了一周,core文件终于出来了:
bash复制$ ls -lh /data/coredump/
-rw-r----- 1 root root 2.3G May 20 03:17 core.consumer.88231.1716153420
2.3G,内存占用大的服务core也大,所以再次提醒磁盘规划要预留空间。
第三步,还原现场。用带符号的测试版本二进制加载:
bash复制gdb ./consumer /data/coredump/core.consumer.88231.1716153420
进gdb之后,第一屏信息显示:
code复制[New LWP 88231]
[New LWP 88238]
...
Core was generated by `./consumer -c config.ini'.
Program terminated with signal SIGSEGV, Segmentation fault.
然后执行:
code复制(gdb) thread apply all bt
输出几百行。我不急着看所有线程,先看signal SIGSEGV对应的线程栈:
code复制(gdb) info threads
12 Thread ... consumer worker_pool_task ()
(gdb) thread 12
(gdb) bt
#0 worker_pool_task () at task_handler.c:187
#1 0x000000000040a3c1 in start_thread () from /lib64/libpthread.so.0
task_handler.c:187,打开源码一看,是一个从队列取出任务指针后未判空就使用的逻辑。理论上队列生产者保证非空,但由于某个异常分支未正确入队,消费者取到了NULL。加一个空指针判断和一行日志,问题解决。
值得说的是,这次崩溃的线程并不是主线程,如果没有thread apply all bt这一步,很可能在主线程栈上白找半天。这就是core dump测试的意义——几秒钟定位问题,而不是靠人肉扫日志猜。
8. 日常测试中的几点习惯性建议
最后按惯例分享几个我实际工作中的习惯,不保证对每个人都适用,但至少能帮你少走一些弯路。
第一,养成"任何新环境初始化脚本里都带上core dump配置"的习惯。不管是新买的云服务器、新装的测试机、还是新拉的Docker容器,先跑一遍上面的环境检查脚本,再判断要不要开core。这样后面出问题不会卡在"为什么没有core文件"这种底层问题上。
第二,core文件一定要配合元数据一起留。文件名里带时间戳和PID还不够,我在正式环境里会在崩溃发生时同时记录环境变量、内核日志、服务启动参数到同一个目录下,这能解决很多"有core但不知道当时跑了什么参数"的尴尬。
第三,掌握gdb的批处理模式。手动调试是学习阶段的事,真正排查重复性问题时,把常用的分析命令写成脚本自动化输出,效率高很多:
bash复制gdb -batch \
-ex "set pagination off" \
-ex "thread apply all bt" \
-ex "info registers" \
-ex "quit" \
./consumer /data/coredump/core.consumer.88231.1716153420 > crash_report.txt 2>&1
第四,不要迷信core dump。有些错误不需要core也能快速定位(比如逻辑错误直接日志就能看到),有些错误core里就是没有有效信息(比如OOM killer直接发SIGKILL,进程来不及生成完整core)。该用core的地方用core,不该用时别浪费时间。
我自己到目前为止,用core dump解决过的崩溃问题几十个是有的,从简单的空指针到复杂的内存踩踏,每一轮的思路其实都是:先保证core能生成,再用gdb还原现场,最后结合源码给出结论。把这套流程反复练熟,遇到再诡异的崩溃心里都有底。
