跳转至

调试内存转储文件

在程序崩溃的时候,我们可能会看到这么一行提示

[1]    14439 segmentation fault (core dumped)  ./a.out

那么其中的 core dumped 就代表了内存转储的意思。内存转储看上去很高大上,其实就是把程序运行时候的内存以及状态都保存到了文件里而已。通过这个转储文件,我们就能复原程序崩溃时候的现场,通过调试器尝试定位问题了。

Linux 上调试内存转储

下面来写一段经典的访问空指针的程序。

#include <iostream>
using namespace std;

void crash() {
    int count = 0;
    while (1) {
        if (count == 10) {
            *static_cast<int*>(0) = 1;
        }
        cout << count << endl;
        ++count;
    }
}

int main() {
    crash();
    return 0;
}

编译运行,程序不出意外地崩溃了。

$ g++ -g segfault.cpp
$ ./a.out
0
1
2
3
4
5
6
7
8
9
[1]    14795 segmentation fault (core dumped)  ./a.out

但是,我们也没有看到有什么 core 文件,这是因为默认情况下 Linux 系统中的限制了 core 文件大小为 0,所以不会有任何的内存转储保存。我们需要通过 ulimit 指令来检查和设置 core 文件大小限制。

$ ulimit -c 
0
$ ulimit -c unlimited
$ ulimit -c
unlimited

这样就可以暂时将 core 文件大小限制设置为无限制。然后再次运行我们的程序,可以看到这次有 core 文件生成了。

Note

通过 ulimit 设置的限制只在当前的 Shell 有用。如果想永久设置,可以参考 limits.conf 文档设置 /etc/security/limits.conf

$ ./a.out
0
1
2
3
4
5
6
7
8
9
[1]    15430 segmentation fault (core dumped)  ./a.out
$ ls
a.out  core  segfault.cpp
$ file core
core: ELF 64-bit LSB core file, x86-64, version 1 (SYSV), SVR4-style, from './a.out', real uid: 1000, effective uid: 1000, real gid: 1000, effective gid: 1000, execfn: './a.out', platform: 'x86_64'
core 文件权限

$ ls -lh core
-rw------- 1 howard howard 496K  July 27 21:27 core
可以看到,core 文件默认的权限是只有当前用户可读可写。这是因为 core 文件是程序运行内存的快照,在一些敏感的场合,例如用于用户登录的服务,可能会保存了一些明文的密码等,所以要尽可能让更少的用户可以访问 core 文件。

修改 core 文件的保存路径

默认情况下,core 文件就是以 core 这个文件名保存到程序的运行目录下。如果有多个程序使用了同样的运行目录,则在 core 文件已经存在的情况下,不会生成新的 core 文件去覆盖。

可以通过修改 /proc/sys/kernel/core_uses_pid 文件让生成 core 文件名加上 pid 号。例如 echo 1 > /proc/sys/kernel/core_uses_pid 后,生成的 core 文件名将会变成 core.$pid,其中 $pid 表示该进程的 PID。

还可以通过修改 /proc/sys/kernel/core_pattern 来控制生成 core 文件保存的位置以及文件名格式。

使用 gdb,我们可以加载 core 文件进行调试,和使用 gdb 启动被调试程序差不多,同样需要编译时生成调试信息,不同的是需要在 gdb 命令后加 core 文件名。

$ gdb a.out core
# ...
Reading symbols from a.out...
[New LWP 15648]
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  crash () at segfault.cpp:8
8                   *static_cast<int*>(0) = 1;
(gdb) print count
$1 = 10
(gdb) bt
#0  crash () at segfault.cpp:8
#1  0x000055a405baf206 in main () at segfault.cpp:16

可以看到调试器加载了 core 文件,恢复了程序运行时候的现场,我们可以通过检查程序崩溃时候的变量以及调用栈等,来排查 bug。除了不可以运行程序以外,其他都和普通的调试没有区别。

Note

在开启 O1 以上的优化级别时,编译器可能会省略用于回溯的栈帧指针,这不利于调试程序。为了解决这个问题,可以使用编译器的 -fno-omit-frame-pointer 选项来保留栈帧指针。另外,如果程序崩溃的时候,破坏了用于回溯的栈帧信息,那么即使拥有 core 文件,可能也比较难以定位问题。

手动生成 core 文件

除了程序异常退出会生成 core 文件以外,我们也可以通过 gcore 命令来获得一个运行中的进程的内存转储。首先我们写一个死循环程序:

#include <iostream>
#include <unistd.h>
using namespace std;

void loop() {
    int count = 0;
    while (1) {
        sleep(1);
        cout << count << endl;
        ++count;
    }
}

int main() {
    loop();
    return 0;
}

编译运行,然后开第二个终端进行操作。首先删除已有的 core 文件,然后设置 core 文件限制,使用 pidof 找到进程的 PID,用 gcore 生成转储文件。需要注意使用 gcore 命令需要有 root 权限。

$ g++ -g loop.cpp
$ ./a.out
0
1
2
3
...
$ rm core
$ ulimit -c unlimited
$ pidof a.out
15932
$ sudo gcore 15932
0x00007fe9eabbb00a in clock_nanosleep () from /lib/x86_64-linux-gnu/libc.so.6
warning: Memory read failed for corefile section, 4096 bytes at 0xffffffffff600000.
Saved corefile core.15932
[Inferior 1 (process 15932) detached]
$ ls
a.out  loop.cpp  core.15932

可以看到 gcore 生成的 core 文件会带有 PID 后缀。而程序在转储之后不受影响,可以继续运行。接下来可以用上面提到的方法加载 core 文件进行调试。

谨慎使用 gcore 命令

对内存占用较高的程序使用 gcore 命令的话可能需要较长时间来保存内存快照,可能会造成程序运行的长时间卡顿。

切记保存现场

内存转储是程序运行现场的快照,对于调试有着重要作用,在遇到难以复现或者日志难以排查的 bug 时,一定要注意设置好操作系统参数,保存并利用好 core 文件来帮助我们开发者进行调试。例如,假如遇到程序死锁等问题,需要重启程序时,最好先用 gcore 保存好现场,然后再重启,或者使用 SIGABRT 等信号中止程序,让操作系统帮我们生成内存转储。当然,如果程序崩溃次数比较多,也需要注意好内存转储的清理,防止占用大量磁盘空间。


最后更新: 2021-09-06 13:26:56
本页作者: Howard Lau