19. Core Dump分析:生成core文件、分析崩溃现场、定位段错误

段错误,说白了就是程序访问了不该访问的内存。我刚开始做嵌入式那会儿,一看到"Segmentation fault"就头皮发麻。后来发现,只要学会用Core Dump,这玩意儿其实没那么可怕。

Core Dump是什么?就是程序崩溃时,操作系统把内存里的现场信息"拍"下来,存成一个文件。你拿着这个文件,就能像回放监控录像一样,看到程序死前那一刻的状态。

19.1 让系统生成core文件

很多Linux系统默认不开core文件生成。你得先确认一下:

# 查看当前core文件大小限制
ulimit -c

# 如果输出是0,说明没开。改成不限制大小:
ulimit -c unlimited

嗯,这里要注意:ulimit只对当前终端有效。你关了终端再开,又得重新设。想永久生效,得改配置文件:

# 编辑 /etc/security/limits.conf
# 添加这两行(*代表所有用户)
* soft core unlimited
* hard core unlimited

我遇到过一种情况:明明设了unlimited,core文件还是没生成。后来发现是core文件的存放路径有问题。你可以这样指定路径和文件名:

# 设置core文件生成到 /tmp/cores 目录
# 文件名格式:core.程序名.进程ID
echo "/tmp/cores/core.%e.%p" > /proc/sys/kernel/core_pattern

我个人习惯把core文件统一放到一个目录,方便管理。不然散落在各个地方,找起来头疼。

小技巧:调试阶段建议把core文件生成到当前目录,省事。发布版本再改到固定目录。

19.2 触发段错误的常见场景

段错误不是随机发生的。我总结了几种最常见的"死法":

场景 代码示例 原因
空指针解引用 int *p = NULL; *p = 10; 访问地址0x0
数组越界 int arr[5]; arr[100] = 1; 踩到别人的内存
栈溢出 void f() { f(); } 无限递归撑爆栈
释放后使用 free(p); *p = 1; 野指针操作
写只读内存 char *s = "hello"; s[0] = 'H'; 字符串常量在只读段

你看,这些错误说白了就一句话:程序访问了不属于它的内存

19.3 用GDB分析core文件

有了core文件,接下来就是分析。GDB是咱们的利器:

# 加载core文件
gdb ./my_program core.my_program.12345

# 或者先启动gdb,再加载
gdb
(gdb) file ./my_program
(gdb) core core.my_program.12345

进去之后,第一件事就是看程序死在哪一行:

(gdb) bt
#0  0x00005555555546a7 in process_data (buf=0x0, len=100) at main.c:25
#1  0x0000555555554712 in main (argc=1, argv=0x7fffffffe3a8) at main.c:40

bt命令打印调用栈。从下往上看:main调了process_data,传了个空指针buf=0x0,然后在第25行崩了。

想看第25行是什么代码?用list

(gdb) list
20  void process_data(char *buf, int len) {
21      for (int i = 0; i < len; i++) {
22          buf[i] = 0;  // 这里崩了!buf是NULL
23      }
24  }

嗯,一目了然。传进来的buf是空指针,还往里写数据,不崩才怪。

核心命令速记:
  • bt — 看调用栈,知道从哪来到哪去
  • frame N — 切换到第N层栈帧
  • info locals — 看当前函数的局部变量
  • p variable — 打印某个变量的值
  • list — 看源代码上下文

19.4 实战:定位一个典型的段错误

咱们拿一个真实案例走一遍。假设我有这么个程序:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int id;
    char name[32];
    float score;
} Student;

void print_student(Student *s) {
    printf("ID: %d, Name: %s, Score: %.1f\n", s->id, s->name, s->score);
}

int main() {
    Student *stu = (Student *)malloc(sizeof(Student));
    stu->id = 1001;
    strcpy(stu->name, "Zhang San");
    stu->score = 95.5f;
    
    free(stu);
    print_student(stu);  // 释放后还在用!
    
    return 0;
}

编译运行:

gcc -g -o test test.c
./test
Segmentation fault (core dumped)

好,core文件生成了。咱们来分析:

gdb ./test core.test.12345
(gdb) bt
#0  0x00007ffff7e6c7a0 in __strlen_avx2 () from /lib/libc.so.6
#1  0x00007ffff7e0a3c1 in __printf_fp_l () from /lib/libc.so.6
#2  0x00005555555546f2 in print_student (s=0x5555555592a0) at test.c:12
#3  0x0000555555554754 in main () at test.c:22

看到没?print_student在调用printf时崩了。为什么?因为s->name指向的内存已经被释放了。咱们验证一下:

(gdb) frame 2
(gdb) p s->name
$1 = "Zhang San"  // 嗯?还能看到值?
(gdb) p s->score
$2 = 0.0  // 分数已经变成0了!内存被污染了

这就是典型的"释放后使用"。内存虽然被free了,但数据可能还在原地,也可能已经被别人覆盖了。这种bug最难抓,因为有时候能跑对,有时候崩。

警告:free之后一定要把指针置NULL。不然你都不知道这个指针还能不能用。我见过太多人在这上面栽跟头。

19.5 用SVG梳理Core Dump分析流程

下面这张图,把整个分析流程串起来了。你照着走,基本不会漏:

Core Dump分析流程 1. 配置core生成 ulimit -c unlimited 2. 复现崩溃 运行程序触发段错误 3. 获取core文件 core.程序名.进程ID 4. 加载 gdb -c core bt 看调用栈 从哪来到哪去 frame N 切栈帧 定位崩溃函数 info locals 查变量 看变量值是否异常 list 看源码 确认崩溃行 分析根因 空指针/越界/释放后使用 修复代码 加判空/检查边界 验证修复 重新编译运行 未修复则回到分析步骤 红色路径:崩溃定位 → 绿色路径:修复验证 → 虚线:迭代

这张图我画得比较细。你按这个流程走,大部分段错误都能搞定。核心就三步:看栈、查变量、读源码

19.6 一些实用技巧

最后分享几个我踩坑踩出来的经验:

  • 编译加-g选项。不加的话,core文件里只有地址,没有行号,分析起来像瞎子摸象。
  • 别优化太高-O2以上会把代码重排,你看到的行号可能和实际执行的不一致。调试阶段用-O0-Og
  • core文件很大?ulimit -c 102400限制大小,够用就行。
  • 多线程程序thread apply all bt看所有线程的栈,崩溃的不一定是主线程。
避坑指南:我曾经在嵌入式板子上调试,发现core文件总是生成不全。后来发现是flash空间不够,core文件写到一半写不下了。所以记得检查磁盘空间。

Core Dump这东西,说白了就是程序死后的"尸检报告"。学会看它,你就能从崩溃现场还原出事故原因。刚开始可能觉得麻烦,但用熟了你会发现——这比瞎猜快多了。

公众号:蓝海资料掘金营,微信deep3321