作为一名整天和 C++ 打交道的"底层打工人",如果哪天上班没见着个 Segmentation fault (core dumped),我甚至会觉得今天这日子过得有点不真实。
说实话,现在很多同学一听到"崩溃排查"就头大,觉得那是 SRE 或者老鸟的活儿,自己写个业务代码就够了。就算生产环境炸了、程序崩了~ 看日志排查不就行了?
可生产环境偶尔来个"随机奔溃",复现N次都失败......日志里只留下一句"Segmentation fault",却找不到任何有用的错误细节!!根本救不了命!
日志有时真的不够用!!!!
这时候,如果你手里有一份 Core 文件,就相当于拿到了案发现场的完整监控录像,谁先动的手、在哪里崩的,看得一清二楚,完整留存了进程的内存状态、寄存器信息、函数调用栈等等信息,能快速定位崩溃根因,告别"盲猜式排查"。
一、何为 "Core Dump" 文件 ?
你写了个C/C++程序,编译,运行,然后你的程序做了件操作系统忍不了的事:访问了不该访问的内存。比如解引用一个空指针,比如往一个已经free掉的内存地址写数据,又比如数组下标越界到姥姥家了。操作系统一看,嚯,你小子想干啥?当场就是一个 SIGSEGV 信号甩过来,二话不说把进程毙了。
毙就毙吧,但操作系统还算仁义,在"行刑"之前,它会问一句:"要不要给你留个全尸?" 这个"全尸",就是Core Dump文件。
Core Dump(核心转储)就是程序在异常终止时内存快照 ,操作系统将其崩溃瞬间的内存镜像、CPU寄存器状态、函数调用栈、进程信息等关键数据,按照ELF标准格式保存到磁盘的文件。
日志会骗人,因为写日志的代码本身可能就带着 Bug,且日志是你主动自己打印的,只能记录你知道可能会出事的地方,但内存不会骗你。它直接告诉你哪个变量在哪一行烂掉了。当你面对一个一个月才偶发一次的随机崩溃时(比如高并发下的内存竞争),无法实时调试,Core Dump 就是你唯一的救命稻草,明白吧?
举个例子。
程序空指针崩了:
char* p = nullptr;
*p = 'A';
日志里可能只会留下:
Segmentation fault
但 Core Dump 里会有:
- 哪一行崩的
- 哪个线程崩的
- 哪块内存非法
- 栈有没有被踩坏
- 当时的CPU寄存器状态(程序计数器停在哪儿了,栈指针指向哪儿)
- 完整的函数调用栈(谁调了谁,参数都是啥)
- 所有变量的值(全局的、局部的、堆上的)
- 甚至包括打开的文件描述符 和信号掩码
这俩根本不是一个维度。这不比日志香多了?
Core Dump 最适合用的地方就是那些很难复现的偶发性崩溃。在本地调试也可以用gcore这种工具主动生成core dump来分析某个怀疑有问题的进程,还有自动化系统在程序崩溃时自动捕获core dump推送到分析平台,用于持续集成的错误监控。
尤其是 C/C++ 这类没自动垃圾回收的语言,一出空指针、内存越界,日志可能啥都没留,Core Dump 直接给你整个现场。
先给大家补两个前置知识:
- Core 文件的本质:它是标准的 ELF 格式文件,和可执行程序、动态库是同一种格式,你可以用 readelf、objdump 这些工具直接解析。它核心包含两类数据:
- PT_NOTE段:存放进程 PID、UID、信号信息、CPU 寄存器状态、线程列表等元数据,是 GDB 解析 core 文件的核心
- PT_LOAD段:进程崩溃瞬间的用户态内存完整镜像,包括代码段、数据段、堆、栈、共享库映射等,相当于把进程的内存空间完整 "冻住" 了
- 边界区分 :我们今天讲的是用户态进程的 Core Dump,和内核崩溃转储的 kdump 不是一回事,kdump 是内核挂了的时候用的,新手别搞混了。
那为什么叫 "Core"呢?
因为在老 UNIX 年代,内存叫 core memory(磁芯内存),名字就这么留下来了。
二、Core Dump的产生机制
很多人以为Core Dump是程序自己生成的,其实不是的,它由操作系统内核生成。内核就像程序的"监考老师",全程盯着程序运行,一旦发现程序"作弊"------比如非法访问内存、触发致命错误,就会立刻叫停程序,然后把当时的"考场状态"拍下来,这就是Core Dump的底层逻辑。
内核发现进程收到某些致命信号时,就会决定要不要转储。比方说 SIGSEGV(段错误,典型空指针或内存越界)、SIGABRT(abort() 调用,常来自 assert 失败或 double free)、SIGFPE(浮点异常)等等。这些信号触发后,内核会检查当前进程的 core file size 限制(ulimit -c),如果允许,就根据 /proc/sys/kernel/core_pattern 的规则生成文件。
**整个过程大体步骤:**进程异常→内核捕获信号→检查配置→冻结进程→导出内存映像→写入core文件→清理资源→终结进程。
整个过程完整细化步骤:
- 进程在用户态触发异常(比如访问非法内存),CPU 切换到内核态,抛出硬件异常
- 内核解析异常,判断是用户态进程的致命错误,给进程发送对应的致命信号
- 内核在信号分发时,检查该信号的处理方式:如果是默认处理(SIG_DFL),且该信号默认行为是生成 Core Dump,进入转储流程
- 内核检查进程的 rlimit(core file size)限制,如果为 0,直接终止进程,不生成 core
- 检查进程的 dumpable 标志(/proc/pid/dumpable),如果为 0,不生成 core
- 解析 /proc/sys/kernel/core_pattern,判断是普通文件路径还是管道程序
- 如果是文件路径:检查路径权限、磁盘空间,创建 core 文件,把进程的内存镜像、元数据写入文件
- 如果是管道:启动管道程序,把 core dump 数据通过标准输入传给管道程序处理
- 转储完成后,内核终止进程,释放资源
常见触发场景一般与内存访问异常有关:
- 非法内存访问:空指针、野指针、数组越界写入。
- 非法指令:比如栈被破坏后跳转到未知区域。
- 断言失败 (assert)。
- 堆栈溢出、栈破坏。
- 双重释放、释放后再使用(glibc 检测到会发 SIGABRT)。
Linux 下默认会触发 Core Dump 的完整信号列表:
|---------|----------|------------------------------------------------------------------|
| 信号名 | 信号编号 | 典型触发场景 |
| SIGSEGV | 11 | 段错误,非法内存访问(空指针、野指针、越界、无权限访问) |
| SIGABRT | 6 | 程序主动调用 abort (),常见于 assert 失败、double free、glibc 内存 corruption 检测 |
| SIGFPE | 8 | 浮点异常,除零错误、数值溢出、非法浮点运算 |
| SIGILL | 4 | 非法指令,栈破坏后跳转到无效地址、CPU 不支持的指令集 |
| SIGBUS | 7 | 总线错误,内存对齐错误、mmap 映射文件被截断、物理地址非法 |
| SIGQUIT | 3 | 键盘 Ctrl+\ 触发,用户主动终止进程并生成 core |
| SIGTRAP | 5 | 断点陷阱,调试器使用 |
| SIGSYS | 31 | 非法系统调用 |
为什么我的电脑有时候不产生 Core 文件呢?
现在的发行版为了怕把硬盘撑爆,默认把 core 文件大小限制设成了 0。
你得在 shell 里输入 ulimit -c unlimited。不过这只是临时的,重启就歇菜。真要在生产环境搞,还得去翻 /etc/security/limits.conf。
还有个骚操作,现在的 Ubuntu 或 CentOS 喜欢把 core 文件交给 systemd-coredump 管。以前在当前目录下找 core.1234,现在你得用 coredumpctl list 去翻。说实话,这设计挺反人类的,我还是习惯改 /proc/sys/kernel/core_pattern,把文件直接扔到指定的 /tmp/cores 目录下,格式起得漂亮点,带上时间戳和 PID,看着都舒心。
另外一些Core Dump 不产生的原因:
- 核心转储路径没有写权限,或磁盘满。
- /proc/sys/kernel/core_pattern 指向一个管道程序,但管道程序失败。
- 进程调用了 setrlimit 限制了 core 大小。
- 进程以 SUID/SGID 权限运行,出于安全考虑,内核默认可不不生成。
- 当前目录不可写,且 core_pattern 设置了相对路径。
- 使用了 prctl(PR_SET_DUMPABLE, 0) 禁用 dump。
- 不是所有信号都会产生 core,比如 SIGKILL(9 号信号)就直接杀掉,进程是连写遗书的机会都没有的,这也是为什么有时候你 kill -9 后没有 core 文件。
- systemd 管理的服务,没有在 service 文件里设置 LimitCORE,不继承 shell 的 ulimit 配置。
👉 【就业避坑】C++ 就业前景全解析:为什么劝退声不断,大厂核心岗仍刚需 C++?
👉 【大厂标准】Linux C/C++ 后端开发系统学习路线
👉 【音视频】音视频流媒体高级开发核心学习路径
👉 【Qt进阶】C++ Qt 桌面 & 嵌入式开发一条龙学习攻略
👉 【内核底层】Linux 内核硬核修炼指南
👉 【面试冲刺】C/C++ 高频八股面试题 1000 题(三)
👉 【项目实战】手撕线程池:C++ 程序员的能力试金石
三、启用与配置 Core Dump
接下来,咱们先搞定最基础的一步:怎么让你的程序崩了的时候,乖乖给你吐出这个 Core 文件。
我敢说,80% 的人第一次用 Core Dump,都卡在了第一步:程序崩了,根本没生成 Core 文件。
这通常是因为 Linux 为了保护磁盘空间(毕竟一个 Core 文件可能好几个 GB),默认把这个功能关掉了。你可以输入下面这个命令看一眼:
# 查看当前限制
ulimit -c
# 如果输出0,表示禁用了
# 设为无限制
ulimit -c unlimited
注意啊,这个设置只对当前shell和它的子进程生效。想永久生效要写到/etc/security/limits.conf里,加一行* soft core unlimited。
但这只是临时开启,重启就没了。而且默认生成的 Core 文件就叫 core,如果多个程序崩溃,后来的会把前面的覆盖掉。作为一个有修养的程序员,我们得让它生得有尊严,带上进程号和时间戳。
修改 /proc/sys/kernel/core_pattern 就能实现:
# 需要 root 权限,让 Core 文件生成在指定目录,并带上程序名、进程号和时间
echo "/tmp/core-%e-%p-%t" > /proc/sys/kernel/core_pattern
3.1 先搞懂 soft limit 和 hard limit
ulimit 里的资源限制分两种,很多人搞不清,导致配置不生效:
- soft limit:当前进程实际生效的限制,普通用户可以把它调高到 hard limit 的上限
- hard limit:资源的最大上限,只有 root 用户可以修改
你用ulimit -c unlimited改的只是 soft limit,如果 hard limit 本来就设的很小,普通用户是改不动的。所以永久配置的时候,建议同时设置 soft 和 hard:
# /etc/security/limits.conf 末尾添加
# * 代表所有用户,也可以指定特定用户,比如root、www-data
* soft core unlimited
* hard core unlimited
3.2 核心转储的存储方式与命名规则
传统方式 ------ 当前工作目录下的 core 文件
传统方式下,core 文件直接生成在进程的工作目录里,文件名就是简单的core或core.<pid>。但这种方式有个致命坑 ------ 多个进程的 core 文件会互相覆盖,而且找不到崩溃时间、进程名等关键信息,非常不推荐。
通过/proc/sys/kernel/core_pattern自定义路径与命名
这是最灵活、最推荐的方式,修改core_pattern可以自定义 core 文件的存储路径和命名规则,支持丰富的占位符:
|---------|--------------------|
| 占位符 | 含义 |
| %t | 转储时间戳(秒级) |
| %e | 可执行文件名 |
| %p | 进程 ID |
| %s | 导致转储的信号编号 |
| %h | 主机名(多机器集群必备) |
| %u | 进程 UID(多租户环境必备) |
| %g | 进程 GID |
| %E | 可执行文件的完整路径(/ 替换为!) |
示例配置:
# 统一存到/var/core目录,带上时间、程序名、PID、信号编号
echo '/var/core/core-%t-%e-%p-s%s' > /proc/sys/kernel/core_pattern
我的习惯是专门建个/var/core目录,挂个独立分区。不然哪天 core 文件爆了把系统盘撑爆,哭都没地方哭。
补充一个老教程的坑:/proc/sys/kernel/core_uses_pid这个参数,只有当 core_pattern 是默认的core时才生效,只要你改了 core_pattern,这个参数就完全失效了,别再被老教程误导。
使用管道将 core dump 传递给处理程序
现代 Linux 发行版大多走这个路子,core_pattern设成以|开头的管道命令,内核会直接把 core dump 数据通过标准输入,传给管道后的程序处理。最常见的就是systemd-coredump,你也可以写自己的自定义脚本,实现自动分析、告警、归档等功能。
3.3 systemd 环境下的 core dump 管理
现在绝大多数服务器都用 systemd,这部分是重中之重,90% 的人都在这里踩过坑。
systemd-coredump 服务
systemd-coredump 是 systemd 自带的 core dump 管理服务,默认core_pattern会被设成|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h,内核把 core dump 直接丢给它处理。
它的优势很明显:自动压缩存储、自动带时间戳命名、不会覆盖、自带管理工具,不用自己写脚本清理。配置文件在/etc/systemd/coredump.conf:
[Coredump]
# 存储方式:external=存到/var/lib/systemd/coredump,journal=存到日志,none=不存储
Storage=external
# 自动压缩
Compress=yes
# 单个core文件最大大小
ProcessSizeMax=10G
# 总存储最大占用
MaxUse=100G
# 保留时长
KeepFree=20G
coredumpctl 命令的使用
coredumpctl是配套的管理工具,不用自己找 core 文件,一条命令就能完成分析、导出、清理,非常好用:
# 查看所有捕获的core dump列表
coredumpctl list
# 查看最近一次崩溃的详细信息
coredumpctl info
# 直接用GDB分析最近的core dump
coredumpctl debug
# 分析指定程序的core dump
coredumpctl debug /usr/bin/myapp
# 导出指定PID的core文件到当前目录
coredumpctl dump PID -o myapp.core
# 删除3天前的所有core dump
coredumpctl --until="3 days ago" delete
它会自动找到对应的二进制文件,加载符号表,直接进入 GDB,省得你手动找路径、配参数。
超级高频坑:systemd 服务不生成 core 文件
划重点!systemd 管理的服务,不会继承你当前 shell 的 ulimit 配置!哪怕你在 shell 里设了ulimit -c unlimited,手动运行程序能生成 core,systemd 启动的服务就是不生成,原因就在这里。
正确的配置方式,必须在.service 文件里显式设置:
# /etc/systemd/system/your-service.service
[Service]
ExecStart=/usr/bin/your-app
# 核心配置:开启core dump无限制
LimitCORE=infinity
# 可选:指定工作目录,确保有写权限
WorkingDirectory=/var/core
改完之后必须执行这两条命令,否则配置不生效:
systemctl daemon-reload
systemctl restart your-service
3.4 永久生效配置完整示例
# 1. 临时开启(当前shell生效)
ulimit -c unlimited
echo '/var/core/core-%t-%e-%p-s%s' > /proc/sys/kernel/core_pattern
# 2. 永久生效(重启不失效)
# 2.1 配置limits.conf,给所有用户开启core无限制
echo '* soft core unlimited' >> /etc/security/limits.conf
echo '* hard core unlimited' >> /etc/security/limits.conf
# 2.2 配置sysctl.conf,永久设置core_pattern
echo 'kernel.core_pattern = /var/core/core-%t-%e-%p-s%s' >> /etc/sysctl.conf
# 立即生效
sysctl -p
当然,不同发行版的配置细节略有差异,你最好对着自己的系统文档核对一下,别直接复制粘贴就完事。
四、Core Dump的捕获方法
4.1 手动触发Core Dump------gcore命令
gcore这命令特别实用,它能在不杀死、不中断进程的情况下,主动生成完整的core dump文件:
# 查看进程PID
pidof myapp
# 生成core dump,指定输出路径和前缀
gcore -o /tmp/myapp-core 12345
生成的/tmp/myapp-core.12345文件,和内核自动生成的core dump完全一致。
最典型的使用场景就是当线上服务出现内存泄漏、死锁、CPU占用100%,但不能重启服务时,这时候用gcore把进程内存快照抓下来,慢慢分析,完全不影响线上业务。尤其是死锁排查,这是神器。
4.2 程序内主动控制Core Dump生成
很多时候,我们的程序是守护进程、后台服务,不会继承shell的ulimit配置,这时候可以在代码里主动开启core dump,避免配置不生效的问题:
#include <sys/resource.h>
#include <sys/prctl.h>
// 程序启动时调用,主动开启core dump
int enable_core_dump() {
struct rlimit rlim = {
.rlim_cur = RLIM_INFINITY,
.rlim_max = RLIM_INFINITY
};
// 设置core文件大小无限制
if (setrlimit(RLIMIT_CORE, &rlim) != 0) {
perror("setrlimit failed");
return -1;
}
// 开启dumpable标志,避免exec后被系统关闭
if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) != 0) {
perror("prctl failed");
return -1;
}
return 0;
}
注意:这个函数要在程序启动的最开始调用,fork子进程之前,这样子进程也能继承配置。
4.3 崩溃时自动处理
生产环境里,我们不可能天天盯着服务器找core文件,最好的方式是写一个自定义管道脚本,内核把core dump传给脚本后,自动完成:生成调用栈、压缩存储、推送告警、清理旧文件,一条龙服务。
给大家一个我线上用了很多年的极简脚本:
#!/bin/bash
# /opt/coredump-handler.sh
# 内核传入的参数(按顺序):%e %p %t %s %u %g
EXE_NAME=$1
PID=$2
TIMESTAMP=$3
SIGNAL=$4
UID=$5
GID=$6
# 配置项
CORE_DIR="/var/core"
LOG_FILE="/var/log/coredump-handler.log"
KEEP_DAYS=7
MAX_CORE_SIZE=10G
# 创建目录
mkdir -p ${CORE_DIR}
cd ${CORE_DIR} || exit 1
# 生成文件名
CORE_FILE="core-${EXE_NAME}-${PID}-${TIMESTAMP}-signal${SIGNAL}"
DATE=$(date +"%Y-%m-%d %H:%M:%S")
# 记录日志
echo "[${DATE}] 捕获到Core Dump: 程序=${EXE_NAME}, PID=${PID}, 信号=${SIGNAL}, UID=${UID}" >> ${LOG_FILE}
# 保存core文件并压缩,限制最大大小,避免撑爆磁盘
cat | head -c ${MAX_CORE_SIZE} | gzip -c > ${CORE_FILE}.gz
# 自动生成完整调用栈(需要提前安装gdb,且二进制带符号)
if [ -x /usr/bin/gdb ]; then
EXE_PATH=$(readlink -f /proc/${PID}/exe 2>/dev/null)
if [ -f "${EXE_PATH}" ]; then
gdb -ex "set pagination off" -ex "bt full" -ex "thread apply all bt full" -ex "quit" \
${EXE_PATH} <(zcat ${CORE_FILE}.gz) > ${CORE_FILE}.backtrace 2>&1
echo "[${DATE}] 已生成调用栈: ${CORE_FILE}.backtrace" >> ${LOG_FILE}
fi
fi
# 自动清理超过保留天数的旧文件
find ${CORE_DIR} -type f -mtime +${KEEP_DAYS} -name "core-*" -delete >> ${LOG_FILE} 2>&1
# 可选:添加企业微信/钉钉/飞书告警,把调用栈和崩溃信息推送到群里
# curl -X POST https://your-webhook-url -d "..."
给脚本加执行权限,然后修改core_pattern:
chmod +x /opt/coredump-handler.sh
echo "|/opt/coredump-handler.sh %e %p %t %s %u %g" > /proc/sys/kernel/core_pattern
这样以后只要有程序崩溃,脚本会自动处理,再也不用手动找core文件了,凌晨两点程序崩了,手机上就能看到调用栈,不用爬起来登服务器。
4.4 容器与Kubernetes环境的Core Dump捕获
容器共享宿主机的内核,core_pattern是内核级参数,必须在宿主机节点上配置,容器内的ulimit需要单独设置。
Docker环境配置
# 1. 先在宿主机上配置core_pattern
echo "/var/core/core-%e-%p-%t" > /proc/sys/kernel/core_pattern
# 2. 启动容器时,设置core ulimit,挂载宿主机的core目录
docker run \
--ulimit core=-1:-1 \ # -1代表unlimited
-v /var/core:/var/core \ # 挂载宿主机core目录,容器内崩溃的core会直接写到宿主机
myimage ./myapp
Kubernetes环境配置
这是现在生产环境的主流,分两步配置:
1)节点统一配置core_pattern:用DaemonSet在所有集群节点上执行配置,或者在节点初始化时直接配置:
echo "/var/core/core-%e-%p-%t" > /proc/sys/kernel/core_pattern
**2)Pod配置:**给Pod设置ulimit,挂载宿主机的core目录,确保容器内的程序崩溃后,core文件能持久化到宿主机:
apiVersion: v1
kind: Pod
metadata:
name: coredump-demo
spec:
containers:
- name: app
image: your-app-image
# 核心配置:设置core文件大小无限制
securityContext:
capabilities:
add: ["SYS_PTRACE"] # 可选,gdb调试、gcore抓dump需要
ulimits:
- name: core
soft: -1 # -1代表unlimited
hard: -1
# 挂载宿主机core目录
volumeMounts:
- name: core-dir
mountPath: /var/core
# 宿主机目录挂载
volumes:
- name: core-dir
hostPath:
path: /var/core
type: DirectoryOrCreate
注意:容器内生成的core文件,必须用生成时的容器镜像里的二进制来分析,不能用宿主机的二进制,否则符号不匹配,无法解析。
4.5 多进程/多线程程序的捕获技巧
- 多进程程序:父进程崩溃会生成core,子进程崩溃也会生成core,只要子进程继承了ulimit和dumpable标志。如果是fork+exec的子进程,要注意exec后dumpable标志可能会被系统清除,需要在子进程里重新设置prctl(PR_SET_DUMPABLE, 1)。
- 多线程程序 :只要有一个线程触发致命信号,整个进程都会终止,生成的core文件会包含所有线程的完整状态,包括寄存器、栈、局部变量。分析时用thread apply all bt full就能看到所有线程的完整调用栈,快速定位到实际导致崩溃的线程。
五、Core Dump分析工具与全流程实操
5.1 前置必备
很多朋友分析core文件,bt全是问号,90%的原因是符号没搞对。这里给大家讲清楚生产环境的标准玩法:分离调试符号,既不影响线上二进制的大小,又能完美分析core文件。
完整步骤:
# 1. 编译程序,带-g生成调试符号,-O2优化不影响符号信息
g++ -g -O2 test.cpp -o test
# 2. 把调试符号单独提取到test.debug文件
objcopy --only-keep-debug test test.debug
# 3. 剥离原二进制里的所有符号,减小体积,用于线上发布
strip test
# 4. 给剥离后的二进制添加符号文件链接,gdb会自动加载对应的test.debug
objcopy --add-gnu-debuglink=test.debug test
这样处理后,线上发布strip后的test二进制,体积很小,不影响性能;分析core文件的时候,把test.debug和test放在同一个目录,gdb会自动加载符号,完美还原调试信息。
系统库符号缺失的解决方法
也许由于某些原因导致系统库的调试符号没装,bt里libc.so.6、libstdc++.so里的函数全是问号。现在主流发行版都支持debuginfod,开启后gdb会自动下载对应的系统库调试符号,不用自己手动找包:
# Ubuntu/Debian
export DEBUGINFOD_URLS="https://debuginfod.ubuntu.com"
# CentOS/RHEL
export DEBUGINFOD_URLS="https://debuginfod.centos.org"
# 然后再启动gdb,会自动下载符号
gdb ./test core.file
5.2 核心分析工具------GDB全流程实操
用GDB分析core文件,最基础的命令是:
# 格式:gdb 【崩溃时的二进制文件】 【对应的core文件】
gdb ./myapp /path/to/core.file
如果用了systemd-coredump,一条命令直接进入调试:
coredumpctl debug
这里给大家一个从零到定位问题的完整可复现案例,跟着做一遍,你就彻底掌握core分析的核心流程了。
第一步:写一个有bug的程序
test.cpp,模拟栈溢出导致的崩溃:
#include <iostream>
#include <cstring>
void buggy_function(char* src) {
char buf[16];
strcpy(buf, src); // 明显的越界,长字符串会溢出栈,覆盖返回地址
}
int main() {
char* bad_str = "this is a very long string that will overflow the stack!!!";
buggy_function(bad_str);
return 0;
}
第二步:编译、运行、触发崩溃
# 编译,带-g保留调试符号
g++ -g -O0 test.cpp -o test
# 开启core dump
ulimit -c unlimited
# 运行程序,触发崩溃
./test
# 输出:Segmentation fault (core dumped)
第三步:GDB加载core文件,完整分析流程
# 加载二进制和core文件
gdb ./test core-test-12345-1234567890
进入GDB后,按下面的步骤来定位:
1. 先确认崩溃的基本信息
GDB加载后,首先会输出核心信息:
Core was generated by `./test'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x00007f8a12345678 in ?? ()
这里直接看到:程序是被SIGSEGV(段错误)终止的,符合我们的预期。
2. 查看完整调用栈,定位崩溃位置
第一步必打bt full,它会输出当前线程的完整调用栈,包括所有局部变量、函数参数,比单纯的bt信息全得多:
(gdb) bt full
#0 0x00007f8a12345678 in ?? ()
No symbol table info available.
#1 0x00007461206e69727473 in ?? ()
No symbol table info available.
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
这里看到栈被破坏了,全是问号------因为我们的strcpy溢出了栈,把栈上的返回地址给覆盖了,这是新手常遇到的疑难场景。
3. 栈被破坏?看CPU寄存器!
用info registers查看所有CPU寄存器的值,找到崩溃的指令地址和栈指针:
(gdb) info registers
rax 0x0 0
rbx 0x7ffd12345678 140724567890123
rcx 0x7f8a12345678 140234567890123
rdx 0x0 0
rsi 0x7ffd12345678 140724567890123
rdi 0x7ffd12345670 140724567890112
rbp 0x7461206e69727473 0x7461206e69727473
rsp 0x7ffd12345660 0x7ffd12345660
rip 0x00007f8a12345678 0x7f8a12345678
这里看到rbp寄存器的值是0x7461206e69727473,转换成ASCII码就是ts ri ta,正好是我们超长字符串里的内容,直接实锤:栈被我们的字符串覆盖了!
4. 反汇编+源码,精准定位bug
用disassemble /m buggy_function,把源码和汇编指令对应起来,看栈的布局:
(gdb) disassemble /m buggy_function
Dump of assembler code for function buggy_function(char*):
2 void buggy_function(char* src) {
0x000055f8a1234560 <+0>: push %rbp
0x000055f8a1234561 <+1>: mov %rsp,%rbp
0x000055f8a1234564 <+4>: sub $0x20,%rsp
0x000055f8a1234568 <+8>: mov %rdi,-0x18(%rbp)
3 char buf[16];
4 strcpy(buf, src);
0x000055f8a123456c <+12>: mov -0x18(%rbp),%rdx
0x000055f8a1234570 <+16>: lea -0x10(%rbp),%rax
0x000055f8a1234574 <+20>: mov %rdx,%rsi
0x000055f8a1234577 <+23>: mov %rax,%rdi
0x000055f8a123457a <+26>: call 0x55f8a1234400 <strcpy@plt>
5 }
0x000055f8a123457f <+31>: nop
0x000055f8a1234580 <+32>: leave
0x000055f8a1234581 <+33>: ret
End of assembler dump.
这里一眼就能看到:buf数组在rbp-0x10的位置,也就是rbp-16字节,而rbp寄存器里存的是上一个栈帧的地址,ret指令会把栈上的返回地址弹出到rip寄存器。我们的strcpy把buf之后的rbp和返回地址都覆盖了,程序自然就崩了。
5. 最后看源码,确认bug
(gdb) list buggy_function
1 #include <iostream>
2 #include <cstring>
3
4 void buggy_function(char* src) {
5 char buf[16];
6 strcpy(buf, src);
7 }
5.3 GDB核心命令
除了上面的基础流程,这些命令排查复杂问题很好用,建议收藏吃灰备用:
|------------------------------------------------|---------------------------------|
| 命令 | 作用 |
| bt / bt full | 查看当前线程的调用栈 / 带局部变量、参数的完整调用栈 |
| thread apply all bt / thread apply all bt full | 查看所有线程的调用栈 / 完整调用栈(死锁、多线程问题必打) |
| frame N / f N | 切换到第N层栈帧 |
| info locals | 查看当前栈帧的所有局部变量 |
| print 变量名 / p 变量名 | 打印变量/表达式的值 |
| list / l | 显示当前栈帧对应的源码 |
| info registers | 查看所有CPU寄存器的值 |
| disassemble /m 函数名 | 带源码的反汇编,精准定位指令 |
| x /nxb 内存地址 | 查看指定内存地址的内容(n=数量,x=十六进制,b=字节) |
| info sharedlibrary | 查看所有加载的动态库,确认符号是否加载 |
| set sysroot 目录 | 指定系统库根目录,解决交叉编译、容器core分析的库不匹配问题 |
5.4 图形化分析工具
说实在的我更喜欢命令行,不过有时候大堆栈看着眼花,可以用这些工具:
- ddd:命令行下的图形化GDB前端,老牌经典
- kdbg:KDE桌面的GDB图形化工具
- VSCode C/C++插件:直接打开core文件,可视化调试,对新手非常友好
六、高频崩溃场景与排查实战案例
这里给大家整理了工作中最常见的崩溃场景,每个案例都有明确的排查思路和解决方案,看完就能直接套用。
6.1 空指针访问(最常见的SIGSEGV)
这是C/C++里最常见的崩溃,没有之一。
触发场景:访问空指针指向的成员变量、调用成员函数、解引用空指针
排查步骤:
- GDB加载core文件,bt直接看到崩溃的函数和行号
- 切换到对应的栈帧,p this查看this指针是否为0x0,p 指针变量查看指针是否为空
- 回溯指针的赋值逻辑,找到为什么没有初始化就被使用
典型GDB输出:
(gdb) bt
#0 Test::print (this=0x0) at test.cpp:6
#1 0x0000555555554789 in main () at test.cpp:13
直接看到this指针是0x0,空指针访问,定位问题。
6.2 双重释放/释放后使用(SIGABRT)
这是第二常见的崩溃,glibc的malloc检测到内存corruption后,会主动调用abort()触发SIGABRT。
触发场景:同一个指针free两次、free后继续使用、堆内存越界覆盖了malloc的元数据
排查步骤:
- GDB加载core文件,bt看到崩溃在__GI_abort、malloc_printerr、_int_free里
- 看malloc_printerr的输出,直接提示double free or corruption
- 切换到main函数的栈帧,查看指针的生命周期,找到重复释放/越界的位置
典型GDB输出:
(gdb) bt
#0 __GI_raise (sig=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1 0x00007f8a12345859 in __GI_abort () at abort.c:79
#2 0x00007f8a123b026e in __libc_message (action=do_abort, fmt="*** %s ***: terminated\n") at ../sysdeps/posix/libc_fatal.c:155
#3 0x00007f8a124529ba in malloc_printerr (str="double free or corruption (fasttop)") at malloc.c:5347
#4 0x00007f8a124548dc in _int_free (av=0x7f8a1251db80 <main_arena>, p=0x5555555592a0, have_lock=0) at malloc.c:4207
#5 0x00005555555546f9 in main () at test.cpp:7
直接看到是double free,定位到第7行的第二个free。
6.3 栈溢出/栈破坏
这是新手最头疼的场景,栈被破坏后,bt全是问号,看不到调用栈。
触发场景:无限递归、大局部数组、数组越界写入覆盖了栈上的返回地址
排查步骤:
- 先看info registers,看rbp、rip寄存器的值是否被字符串覆盖
- 用disassemble反汇编崩溃附近的函数,看栈的布局
- 用x /64xb $rsp查看栈顶的内存内容,看是否有可识别的字符串、数据
- 回溯最近的数组、字符串操作,找到越界写入的位置
关键技巧:编译时加-fno-omit-frame-pointer -fstack-protector,开启栈保护,栈被破坏时会直接报错,提示stack smashing detected,精准定位到出问题的函数。
6.4 死锁排查(程序卡死无响应)
死锁的时候程序不会崩溃,不会自动生成core文件,这时候用gcore手动抓dump,完美还原现场!
触发场景:多个线程互相持有对方需要的锁,循环等待,程序卡死
排查步骤:
- 程序卡死时,不要kill,用pidof myapp找到PID
- 用gcore -o deadlock-core PID生成core文件
- GDB加载core文件,执行thread apply all bt full
- 查看每个线程的调用栈,找到卡在pthread_mutex_lock的线程
- 查看每个线程持有的锁和等待的锁,找到循环等待的关系,定位死锁
典型现象:线程1持有mutex1,等待mutex2;线程2持有mutex2,等待mutex1,完美的死锁循环。
6.5 总线错误SIGBUS
这个场景很多新手没见过,容易和SIGSEGV搞混。
触发场景:内存对齐错误(比如访问一个4字节的int,地址不是4的倍数)、mmap映射的文件被截断、访问不存在的物理地址
排查步骤:
- GDB里bt看到崩溃的指令,确认是内存访问指令
- 查看访问的内存地址,确认是否对齐
- 查看是否有mmap操作,确认映射的文件是否被修改、截断
关键区别 :SIGSEGV是访问了非法的虚拟地址 ,SIGBUS是访问了合法的虚拟地址,但对应的物理地址无效/对齐错误。
七、Core Dump 生产环境实践
7.1 分环境配置策略
不同环境的业务诉求不同,配置策略也要区分开:
|---------|-----------------------------------------------------------------------------------------------------|
| 环境 | 配置建议 |
| 开发/测试环境 | 全开:ulimit -c unlimited,core_pattern设到固定目录,保留所有core文件,开启完整调试符号,方便快速排查 |
| 预发/压测环境 | 限制:core文件最大2G,自动压缩,保留7天,自动生成调用栈并推送告警,二进制分离调试符号,和镜像一起归档 |
| 生产环境 | 严控:仅给核心业务进程开启,独立分区挂载core目录,避免撑爆系统盘;core文件最大限制10G,自动加密存储,保留3天,自动清理;敏感业务(支付、用户数据)禁用core dump,避免敏感信息泄露 |
7.2 安全红线
Core文件里包含进程的完整内存镜像,里面可能有明文密码、密钥、用户隐私数据、业务敏感信息:
- 绝对不能随便外传、上传到公网、发给第三方厂商,哪怕是测试用的core文件,也可能泄露敏感信息
- 生产环境的core文件必须加密存储,严格控制访问权限,只有核心运维和开发人员能访问
- 分析完的core文件,必须用shred彻底删除,不能直接rm,避免数据被恢复
- 绝对不要把core文件提交到代码仓库,哪怕是临时提交
- SUID/SGID程序、涉及敏感数据的业务,谨慎开启core dump,避免数据泄露
7.3 编译选项
- 必须加-g生成调试符号,哪怕是生产环境,也可以分离符号,不影响线上运行
- 优化级别建议用-O2,-O3可能会有指令重排、栈帧优化,导致调试时栈帧不完整,bt看不到完整调用栈
- 建议加-fno-omit-frame-pointer,保留栈帧指针,哪怕开了优化,也能保证调用栈的完整性,方便调试
- 测试环境建议加-fstack-protector-strong开启栈保护,栈被破坏时会直接报错,精准定位问题
- 不要用-fomit-frame-pointer,这个会去掉栈帧指针,调试的时候bt全是问号,非常难排查
7.4 Core文件管理
- 必须给core文件单独挂载独立分区,绝对不能和系统盘、业务数据盘放在一起,避免core文件太多撑爆磁盘,导致系统崩溃、业务不可用
- 必须配置自动清理策略,用logrotate或者systemd-coredump自带的清理功能,设置保留天数,避免磁盘被占满
- 生产环境建议用管道脚本自动处理core文件,生成调用栈后,只保留压缩后的core文件和调用栈日志,方便回溯
- 建立core文件归档机制,线上重大事故的core文件,要和对应的二进制、符号表、代码版本一起归档,方便后续复盘
八、Core Dump 高频踩坑问题 FAQ
1. 配置了ulimit -c unlimited,程序崩溃还是不生成core文件,怎么办?
按这个顺序一步步排查:
- 先确认ulimit -c的输出是不是unlimited,注意:这个只对当前shell和子进程生效,systemd服务、crontab任务、守护进程不会继承!
- 如果是systemd管理的服务,必须在.service文件里加LimitCORE=infinity,然后daemon-reload+restart,否则配置不生效
- 检查/proc/sys/kernel/core_pattern的路径,确认进程对这个路径有写权限,目录存在,磁盘没满,inode没耗尽
- 检查进程的dumpable标志:cat /proc/<pid>/dumpable,输出1才是开启的,0的话不会生成core
- 检查程序有没有给致命信号注册自定义处理函数,如果注册了非SIG_DFL的handler,内核不会自动生成core
- 检查是不是被OOM killer杀了,OOM会发SIGKILL(9号信号),不会生成core,用dmesg | grep -i oom查看
- 检查是不是SUID/SGID程序,出于安全考虑,内核默认不会给SUID程序生成core,需要设置/proc/sys/fs/suid_dumpable为2,有安全风险,谨慎使用
2. GDB加载core文件后,bt全是问号,怎么解决?
常见原因和对应解决方法:
- 最常见:二进制和core文件不匹配!必须用生成core文件时的那个二进制,不能重新编译,哪怕代码一行没改,编译出来的也可能不一样,编译选项、依赖库、编译器版本都必须完全一致
- 调试符号没加载:确认二进制编译时加了-g,分离的符号文件在对应目录,用symbol-file test.debug手动加载符号
- 系统库符号缺失:开启debuginfod自动下载系统库调试符号,或者手动安装对应版本的debuginfo包
- 栈被破坏了:用info registers看rip寄存器,disassemble反汇编,x看栈内存,手动定位问题
- 优化开太高:编译时用了-O3或者-fomit-frame-pointer,去掉了栈帧指针,重新编译时加-fno-omit-frame-pointer
3. Core文件太大,动不动就几个G甚至几十G,怎么办?
用coredump_filter过滤不需要的内存段,比如共享内存段、文件映射段,这些通常不需要。默认值是0x33,改成0x7只保留匿名私有内存段,能大大减小core文件大小:
echo 0x7 > /proc/self/coredump_filter
生成时直接压缩:把core_pattern设成管道,用gzip直接压缩,比如
|/bin/gzip -c > /var/core/core.%e.%p.gz,能减小70%以上的体积
- 限制core文件最大大小:用ulimit或者systemd的LimitCORE设置最大大小,避免core文件无限大
- 手动生成core时,用gcore -a选项,只导出需要的内存段
4. 程序被OOM killer杀了,没有生成core文件,怎么办?
OOM killer是内核的内存管理机制,当系统内存不足时,会选择占用内存最多的进程,发送SIGKILL信号杀掉,释放内存。而SIGKILL信号不会触发Core Dump,所以不会生成core文件。
解决方法:
- 先确认是不是OOM杀的:dmesg | grep -i 'out of memory'或者journalctl -k | grep -i oom,能看到OOM killer的完整日志
- 优化程序的内存使用,排查内存泄漏,避免内存占用过高
- 给系统配置足够的swap,或者调整OOM killer的评分,避免关键业务进程被误杀
- 用cgroup限制进程的最大内存使用,提前触发OOM,避免系统级别的OOM
5. Core文件是truncated(截断的),分析不了,怎么办?
原因是core文件只写了一半,就因为各种原因停止写入了,解决方法:
- 检查磁盘空间和inode,确保有足够的空间存放core文件
- 确认ulimit -c是unlimited,没有限制core文件大小
- 确认core_pattern的路径有足够的权限,进程可以写入
- 调整coredump_filter,过滤不需要的内存段,减小core文件大小