简介
当程序运行过程中异常终止或崩溃时,操作系统(Linux 内核)会将程序当时的完整内存状态以及关键寄存器和进程信息保存到一个文件中,这个行为就是 Core Dump(核心转储)。可以把它理解成一张“内存快照”,但实际包含的远不止内存,还有程序指针、栈指针、内存管理信息等。对于开发人员来说,core dump 文件是定位那些难以重现的 bug(例如指针异常等内存访问错误)的有力武器,它能再现崩溃瞬间的场景。
核心转储如何产生
有读者可能会问:kill -9 命令杀死进程会导致 core dump 吗?答案是否定的。究竟哪些情况才会触发转储呢?
在 Linux 系统中,信号是一种异步事件处理机制,每种信号都有默认的处理动作。你可以通过 signal(7) 手册页查看完整的信号列表及其默认操作。默认操作主要包括:终止进程(Term)、忽略(Ign)、终止并产生核心转储(Core)、暂停进程(Stop)、继续运行暂停进程(Cont)。如果我们使用信号的默认动作,下列信号在发生时就会生成 core dump:
| Signal |
Action |
Comment |
说明 |
| SIGABRT |
Core |
Abort signal from abort |
来自 abort 的终止信号 |
| SIGBUS |
Core |
Bus error (bad memory access) |
总线错误(内存访问错误) |
| SIGFPE |
Core |
Floating-point exception |
浮点异常 |
| SIGILL |
Core |
Illegal Instruction |
非法指令 |
| SIGIOT |
Core |
IOT trap. A synonym for SIGABRT |
IOT 陷阱,SIGABRT 的同义词 |
| SIGQUIT |
Core |
Quit from keyboard |
从键盘退出 |
| SIGSEGV |
Core |
Invalid memory reference |
无效的内存引用 |
| SIGSYS |
Core |
Bad system call (SVr4) |
错误的系统调用 |
| SIGTRAP |
Core |
Trace/breakpoint trap |
跟踪/断点陷阱 |
| SIGUNUSED |
Core |
Synonymous with SIGSYS |
SIGSYS 的同义词 |
| SIGXCPU |
Core |
CPU time limit exceeded (4.2BSD) |
超出 CPU 时间限制 |
| SIGXFSZ |
Core |
File size limit exceeded (4.2BSD) |
超出文件大小限制 |
因此,使用 Ctrl+Z 挂起进程(发送 SIGTSTP,默认停止)或 Ctrl+C 终止进程(发送 SIGINT,默认终止)都不会产生 core dump。类似地,kill -9 发送 SIGKILL,其默认行为也是终止进程,而不会有转储。相反,使用 Ctrl+\ 则会向进程发送 SIGQUIT 信号,默认产生 core dump。此外,程序主动调用 abort() 函数、发生非法内存访问、非法指令等情况也会触发核心转储。
不会生成 core dump 文件的情况
即便满足信号条件,也不是每次都会顺利生成 core dump 文件。以下场景中,内核转储将失败:
- 进程没有在目标目录写入核心文件的权限(默认核心文件为
core 或 core.pid 存在于当前工作目录)。如果是目录不可写,或者已经存在一个不可写的同名普通文件(或该文件是目录、符号链接),都会失败。
- 已经存在一个可写的同名普通文件,但该文件有多个硬链接。
- 存放核心转储的文件系统已满、inode 已耗尽、以只读方式挂载,或用户已达到文件系统配额。
- 要创建的目录根本不存在。
- 进程的
RLIMIT_CORE(核心文件大小)或 RLIMIT_FSIZE(文件大小)资源限制被设为零;可使用 ulimit 命令(或 getrlimit(2))进行调整。
- 进程正在执行的二进制文件未开启读权限(安全措施,防止不可读的可执行文件产生可能包含镜像内容的核心转储)。
- 进程正在执行一个 set-user-ID 或 set-group-ID 程序,且该程序所属的用户/组不同于进程的实际用户/组,或者进程正在执行带有文件 capabilities 的程序(详细说明参见
prctl(2) 和 /proc/sys/fs/suid_dumpable)。
/proc/sys/kernel/core_pattern 为空且 /proc/sys/kernel/core_uses_pid 为 0。如果前者为空而后者为 1,则核心文件名称形如 .pid,需用 ls -a 才能看到。
- 内核编译时未开启
CONFIG_COREDUMP 选项(Linux 3.7 以后)。
此外,如果使用了 madvise(2) 的 MADV_DONTDUMP 标志,进程的部分地址空间也会被 core dump 排除。
启用内核转储
可以使用 ulimit 命令检查当前核心转储功能是否启用。-c 参数代表核心文件大小限制,如果输出为 0,表示禁止产生 core dump。
root@firefly:~# ulimit -c
0
执行以下命令即可打开内核转储,unlimited 表示不限制 core 文件的大小:
root@firefly:~# ulimit -c unlimited
root@firefly:~# ulimit -c
unlimited
然后,我们可以编写一个简单的 C 程序来验证转储是否生效。在服务器上交叉编译:
#include <stdio.h>
int main(void)
{
int *a = NULL;
*a = 0x1;
return 0;
}
编译命令:
aarch64-linux-gnu-gcc -g test.c -o test
将生成的可执行文件拷贝到开发板上运行:
root@firefly:~/code# ./test
Segmentation fault (core dumped)
root@firefly:~/code# ls
core test
root@firefly:~/code# file core
core: ELF 64-bit LSB core file, ARM aarch64, version 1 (SYSV), SVR4-style, from './test', real uid: 0, effective uid: 0, real gid: 0, effective gid: 0, execfn: './test', platform: 'aarch64'
可以看到已经生成了名为 core 的核心文件。接下来将 core 文件和可执行文件 test 一起拷贝回服务器,用 GDB 进行分析:
➜ mnt sudo aarch64-linux-gnu-gdb test core
.....
GNU gdb (Linaro_GDB-2017.05) 7.12.1.20170417-git
......
warning: Could not load shared library symbols for 2 libraries, e.g. /lib/aarch64-linux-gnu/libc.so.6.
Use the "info sharedlibrary" command to see the complete listing.
Do you need "set solib-search-path" or "set sysroot"?
Core was generated by `./test'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x00000055815836f4 in main () at test.c:6
6 *a=0x1;
(gdb) l 6
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int *a=NULL;
6 *a=0x1;
7 return 0;
8 }
(gdb)
GDB 明确指出 test.c 的第 6 行收到了 SIGSEGV 信号,导致了段错误。通过 list 命令可以定位到出错的源代码行。
在专用目录生成内核转储
默认情况下,core 文件会在程序的当前工作目录生成。为了方便管理,通常希望固定保存路径。
内核转储的存储位置可以通过 sysctl 变量 kernel.core_pattern 来指定。例如,在 /etc/sysctl.conf 中追加以下配置:
root@firefly:~# vim /etc/sysctl.conf
# 在末尾追加以下两行
kernel.core_pattern = /root/core/%t-%e-%p-%c.core
kernel.core_uses_pid = 0
# 使配置生效
root@firefly:~# sysctl -p
kernel.core_pattern = /root/core/%t-%e-%p-%c.core
kernel.core_uses_pid = 0
随后运行测试程序,core 文件就会出现在 /root/core/ 目录下:
root@firefly:~/mnt# ./test
Segmentation fault (core dumped)
root@firefly:~/mnt# ls /root/core/
1664718591-test-2699-18446744073709551615.core
kernel.core_pattern 支持的格式符如下:
| 格式符 |
说明 |
| %% |
% 字符本身 |
| %p |
被转储进程的 PID |
| %u |
真实用户 ID |
| %g |
真实组 ID |
| %s |
触发转储的信号编号 |
| %t |
转储时刻(Unix 时间戳,秒) |
| %h |
主机名(同 uname(2) 返回的 nodename) |
| %e |
可执行文件名 |
| %c |
转储文件的大小上限(内核 2.6.24 后可用) |
压缩转储文件
如果生成的 core 文件很大,可以在 kernel.core_pattern 中利用管道机制,在写入磁盘前自动压缩。
修改 /etc/sysctl.conf:
vim /etc/sysctl.conf
kernel.core_pattern = |/usr/local/sbin/core_helper %t %e %p %c
kernel.core_uses_pid = 0
sysctl -p
接着编写辅助脚本 /usr/local/sbin/core_helper:
#!/bin/sh
exec gzip -> /root/core/$1-$2-$3-$4.core.gz
赋予可执行权限:
chmod 777 /usr/local/sbin/core_helper
现在,当进程触发 core dump 时,就会在 /root/core 下生成已压缩的 .core.gz 文件:
root@firefly:~/mnt# ./test
Segmentation fault (core dumped)
root@firefly:~/mnt# ls /root/core/
1664720072-test-2723-18446744073709551615.core.gz
启用整个系统的内核转储功能
前面通过命令行设置的 ulimit 仅对当前会话有效,重启后失效。要让系统永久生效,有以下两种方法之一:
- 在
/etc/rc.local 中添加 ulimit -c unlimited;
- 在
/etc/security/limits.conf 末尾增加如下两行:
@root soft core unlimited
@root hard core unlimited
利用内核转储掩码排除共享内存
大型应用程序通常会运行多个进程,如果所有进程的共享内存都被转储,不仅浪费磁盘空间,还会因转储时间过长导致服务中断时间变长。因为共享内存在不同进程中是相同的,所以只需要在一个进程中转储共享内存区域即可。
通过设置 /proc/<PID>/coredump_filter 的掩码,可以控制转储哪些内存映射。掩码各个位的含义如下:
bit 0 转储匿名私有映射。
bit 1 转储匿名共享映射。
bit 2 转储文件支持的私有映射。
bit 3 转储文件支持的共享映射。
bit 4 (自 Linux 2.6.24 起) 转储 ELF 标头。
bit 5 (自 Linux 2.6.28 起) 转储私有大页面。
bit 6 (自 Linux 2.6.28) 转储共享大页面。
bit 7 (自 Linux 4.4 起) 转储私有 DAX 页面。
bit 8 (自 Linux 4.4 起) 转储共享 DAX 页面。
查看当前进程的过滤掩码:
cat /proc/<PID>/coredump_filter
若希望跳过所有共享内存区域,只需将掩码设置为 1。
通过以上配置,你就可以灵活地捕获 Linux 程序的崩溃现场,利用 core dump 结合 GDB 迅速定位难以复现的缺陷。