4. Core Dump分析:Core Dump的生成与配置、使用GDB分析Core文件、常见崩溃模式分析

4.1 Core Dump是什么?说白了就是程序的“死亡快照”

Core Dump,我习惯叫它“内核转储”。但别被名字吓到,它其实就是程序崩溃那一刻,内存里所有数据的快照。包括堆栈、寄存器、全局变量、局部变量……所有东西都被原封不动地保存下来。

你想想看,程序跑着跑着突然挂了,你肯定想知道“最后一刻发生了什么”。Core Dump就是帮你回答这个问题的。我个人觉得,它是调试中最强大的工具,没有之一。

嗯,这里要注意:Core Dump不是默认开启的。很多系统上,你根本看不到core文件。为什么?因为默认的ulimit设置是0,也就是禁止生成。

4.2 生成与配置:让Core Dump乖乖出来

先检查当前环境:

ulimit -c

如果输出是0,那就需要手动开启:

ulimit -c unlimited

这只是临时生效。想永久生效,得改配置文件。我个人习惯在 /etc/security/limits.conf 里加一行:

* soft core unlimited
* hard core unlimited

然后还要设置core文件的命名规则和存放路径。这个通过 /proc/sys/kernel/core_pattern 控制。举个例子:

echo "/var/crash/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

这里的 %e 是可执行文件名,%p 是进程PID,%t 是时间戳。这样每个core文件都有唯一的名字,不会互相覆盖。

警告: 我曾经在生产环境上吃过亏——core_pattern没配好,结果core文件全堆在根目录,直接把磁盘撑爆了。建议指定一个专用目录,并定期清理。

4.3 使用GDB分析Core文件:一步步还原案发现场

有了core文件,接下来就是用GDB来分析。命令很简单:

gdb /path/to/program /path/to/core

进去之后,第一件事就是看堆栈:

(gdb) bt

这会打印出函数调用链。从最顶层(崩溃点)到最底层(main函数)。我一般先看最上面几行,那里就是“案发现场”。

然后看具体是哪一行代码崩溃:

(gdb) frame 0
(gdb) info locals

frame 0 切换到最顶层栈帧,info locals 显示所有局部变量的值。很多时候,崩溃原因一目了然——比如指针是0x0,或者数组下标越界。

再深入一点,可以看寄存器的值:

(gdb) info registers

特别是 rip(指令指针)和 rsp(栈指针)。如果 rip 指向一个奇怪的地址,那很可能是函数指针被篡改了。

小技巧: 我习惯在GDB里用 list 命令查看源码上下文。比如 list 42 显示第42行附近的代码。配合 bt full 可以看到每个栈帧的局部变量,非常实用。

4.4 常见崩溃模式分析:我遇到的几种典型情况

这些年我调试过的core文件,少说也有上百个。总结下来,崩溃模式就那么几种。我挑几个典型的说说。

4.4.1 空指针解引用

这是最常见的。GDB里看到 0x0 或者 0x8 之类的地址,基本就是空指针。举个例子:

(gdb) bt
#0  0x00007f... in foo (p=0x0) at test.c:10
#1  0x00007f... in main () at test.c:20

看到 p=0x0 了吗?这就是问题所在。代码里没有检查指针是否为空就直接用了。

我曾经遇到过一个案例,空指针不是直接传进来的,而是通过一个全局变量间接引用的。查了半天才发现,某个初始化函数没被调用。

4.4.2 缓冲区溢出

这种崩溃往往发生在 memcpy、strcpy 或者数组操作的地方。GDB里看到的堆栈可能很诡异——返回地址被覆盖了,导致跳转到非法地址。

举个例子:

(gdb) bt
#0  0x0000000000400000 in ?? ()
#1  0x4141414141414141 in ?? ()

看到 0x41414141 了吗?那是大写字母A的ASCII码。说明栈上的返回地址被字符串覆盖了。这种问题,用 address sanitizer 或者 valgrind 更容易定位。

4.4.3 释放后使用(Use-After-Free)

这种崩溃最头疼。因为崩溃点往往离真正的bug很远。GDB里看到的堆栈可能完全正常,但某个指针指向的内存已经被释放了。

我一般这样排查:

  1. 看 info locals 里有没有可疑的指针
  2. 用 print *ptr 看指针指向的内容是否合理
  3. 如果内容全是 0xdeadbeef 或者 0xbaadf00d,那基本就是释放后使用了
核心思路: 崩溃模式虽然多,但本质都是内存操作出了问题。空指针、越界、释放后使用,这三板斧能解决90%的问题。

4.5 实战技巧:如何快速定位崩溃原因

我总结了一个“三步法”:

步骤 操作 目的
第一步 bt 看堆栈 找到崩溃点
第二步 frame 0 + info locals 查看局部变量
第三步 print *ptr 检查指针 确认内存状态

这三步走完,大部分问题都能定位。如果还不行,那就需要更高级的手段了——比如用 watch 命令监视某个内存地址的变化,或者用 reverse debugging 回放执行过程。

嗯,说到reverse debugging,这个功能在GDB 7.0之后就有了。但说实话,我很少用,因为性能开销太大。大多数时候,三步法就够了。

4.6 避坑指南:我踩过的几个坑

  • Core文件太大? 可以用 ulimit -c 100000 限制大小,单位是KB。我一般设成100MB,够用又不占空间。
  • Core文件打不开? 检查一下文件权限。有时候core文件是root用户的,普通用户读不了。
  • GDB提示“no debugging symbols”? 编译时加 -g 选项。生产环境可以用 -g -O2,既有优化又有调试信息。
  • 多线程程序崩溃? 用 thread apply all bt 看所有线程的堆栈。有时候崩溃线程不是主线程,而是某个工作线程。

我曾经遇到一个最坑的情况:core文件生成了,但GDB打开后堆栈全是问号。后来发现是编译时strip掉了符号表。从那以后,我每次发布版本都会保留一份带符号表的二进制文件。

好了,Core Dump分析就讲到这里。说白了,它就是程序崩溃后的“黑匣子”。只要你会用GDB,再配合我上面说的三步法,大部分崩溃问题都能迎刃而解。