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文件统一放到一个目录,方便管理。不然散落在各个地方,找起来头疼。
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最难抓,因为有时候能跑对,有时候崩。
19.5 用SVG梳理Core Dump分析流程
下面这张图,把整个分析流程串起来了。你照着走,基本不会漏:
这张图我画得比较细。你按这个流程走,大部分段错误都能搞定。核心就三步:看栈、查变量、读源码。
19.6 一些实用技巧
最后分享几个我踩坑踩出来的经验:
- 编译加
-g选项。不加的话,core文件里只有地址,没有行号,分析起来像瞎子摸象。 - 别优化太高。
-O2以上会把代码重排,你看到的行号可能和实际执行的不一致。调试阶段用-O0或-Og。 - core文件很大?用
ulimit -c 102400限制大小,够用就行。 - 多线程程序用
thread apply all bt看所有线程的栈,崩溃的不一定是主线程。
Core Dump这东西,说白了就是程序死后的"尸检报告"。学会看它,你就能从崩溃现场还原出事故原因。刚开始可能觉得麻烦,但用熟了你会发现——这比瞎猜快多了。
公众号:蓝海资料掘金营,微信deep3321