调试内存转储文件¶
在程序崩溃的时候,我们可能会看到这么一行提示
[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 文件去覆盖。
可以通过修改 /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
等信号中止程序,让操作系统帮我们生成内存转储。当然,如果程序崩溃次数比较多,也需要注意好内存转储的清理,防止占用大量磁盘空间。