在计算机系统中运行程序时,问题经常发生,而且通常很难找到根源。幸运的是,有一种叫做 coredump 的文件可以帮助我们找到问题的源头。本文将解释什么是 coredump,它是如何工作的,以及如何利用它来定位问题。
01 什么是 coredump
Core dump(核心转储)是指在程序运行过程中发生错误或异常时,操作系统将程序的内存内容保存到磁盘上的一种文件。这个文件包含了程序崩溃时的内存状态,包括变量的值、函数调用栈、寄存器状态等信息。通过分析 coredump 文件,可以了解程序崩溃的原因,以便进行调试和修复。
1.1 ulimit 命令
要生成 core dump 文件,通常需要在操作系统中设置相应的配置。在 Linux 和 Unix 系统中,可以使用 ulimit 命令设置 core dump 文件的大小限制,并使用 gcore 或 kill -SIGQUIT 命令生成 core dump 文件。
ulimit
是 Unix 和 Linux 系统中的内置命令,用于控制用户级别的系统资源限制。这些资源包括文件大小、进程数、核心文件大小、堆栈大小等等。以下是一些使用 ulimit 的基础知识。
使用 ulimit -a
可以列出所有的限制,每一行都对应一个资源类型和其对应的限制。
以下是一些比较常用的 ulimit 选项:
ulimit -u
: 查看用户可以用的最大进程数。ulimit -n
: 文件描述符的最大个数,即一个进程可以同时打开的最大文件数。ulimit -d
: 一个进程可以使用的最大数据段大小,单位为KB。ulimit -s
: 最大堆栈大小,单位为KB。ulimit -c
: 生成coredump文件的最大大小,单位为KB。ulimit -v
: 进程虚拟内存的最大值,单位KB。
我们可以在命令后添加具体的数值来设置对应的资源限制,例如:
ulimit -n 1024
: 设置最大可以打开的文件描述符数为1024。ulimit -c unlimited
: 设置coredump的大小为无限。
需要注意的是,ulimit 命令设置的资源限制是以 Shell 环境为单位的,而不是全局性的。也就是说,在一个 Shell 环境中设置的资源限制,并不会影响到其他的 Shell 环境。同时,这些限制只对当前 Shell 环境以及它派生出来的子进程生效,对已经存在的其他进程没有影响。
总的来说,ulimit 命令是一个很有用的工具,它可以帮助我们控制进程对系统资源的使用,从而防止一些程序错误导致系统资源的耗尽。
1.2 coredump 实例
1. ulimit 查看资源限制
一般情况下如果 coredump 文件没有生成,很大可能是由于受到资源限制,使用 ulimit命令查看和设置资源限制
ulimit -c # 查看是否开启 core,0 表示关闭
ulimit -c [filesize] # 设置 core 文件大小
ulimit -c unlimited # 设置 core 文件大小为无限
2. 设置 core 文件路径
在 Linux 系统中,core dump 文件的路径是由 /proc/sys/kernel/core_pattern
定义的,如果这个文件不存在,或者内容为空,那么 core dump 文件就会生成在当前目录下
也可以通过修改 /proc/sys/kernel/core_pattern
指定 core 文件生成的路径和文件名
# 查看当前 core 文件路径
cat /proc/sys/kernel/core_pattern
# 指定 core 文件路径
sudo echo "yourpath/core.%e.%p.%h.%t" > /proc/sys/kernel/core_pattern
sudo sysctl -w kernel.core_pattern=yourpath/core.%e.%p.%h.%t # # 也可以使用 sysctl 修改 kernel.core_pattern 来指定 core 文件路径
其中 core 文件名称定义中,可以使用占位符保留一些有用信息
%p: pid
%u: uid
%g: gid
%s: signal number
%t: UNIX time of dump
%h: hostname
%e: executable filename
3. 空指针 core 示例
下面这个例子中试图通过一个 NULL 指针来访问内存,这是非法的,因为 NULL 指针并没有指向任何有效的内存位置,因而在运行时,操作系统将识别这个非法操作,并生成一个 Coredump 文件。
#include <stdio.h>
int main() {
int *ptr = NULL;
printf("%d", *ptr);
return 0;
}
编译运行这段程序如下,可以看到触发了 core dumped
需要注意的是编译的时候记得加上 -g
参数保留调试信息,否则使用 GDB 调试时会找不到函数名或者变量名
$ gcc -g main.c -o main
$ ./main
[1] 277 segmentation fault (core dumped) ./main
4. 使用 GDB 调试 core 文件
找到 core 文件,然后使用命令 gdb [exec file] [core file]
调试 core 文件
$ cd your-core-path
$ ls
core.main
$ gdb ~/main ./core.main
GDB 运行后会停止在发生异常的代码处,并且将发生异常的代码打印出来,如下图中指出了异常位于 mian.c 的第 5 行代码 *ptr
是一个空指针
02 coredump 是怎么发生的
2.1 程序运行错误导致 coredump
程序执行非法操作时,例如解引用空指针,除数为零,或者访问越界的内存,操作系统就会生成一个 coredump 文件并终止程序运行。以下是一些常见导致 core dump 发生的原因:
- 空指针引用:当程序试图访问一个空指针时,操作系统会捕获这个错误并生成 core dump 文件
- 内存越界:当程序试图访问超出其分配内存范围的位置时,可能会导致内存越界错误,从而触发 core dump
- 栈溢出:当程序的函数调用栈超过其允许的最大深度时,可能会发生栈溢出错误,导致 core dump 发生
- 除零错误:当程序试图除以零时,会触发除零错误,操作系统可能会生成 core dump 文件
- 非法指令:当程序执行了无效或非法的机器指令时,操作系统通常会生成 core dump 文件
- 内存分配错误:当程序遇到内存分配错误,如申请内存失败或释放已释放的内存时,可能会触发 core dump
这些程序执行非法操作时,操作系统会向指定进程发送特定信号(signal)终止程序运行并生成 coredump,一些常见信号有:
- SIGSEGV (信号值 11):当一个进程由于无效的内存访问,如解引用空指针,或访问受保护的内存区域时,系统会向它发送此信号。这是生成coredump最常见的原因
- SIGABRT (信号值 6):当进程自身检测到一个无法恢复的问题,并选择主动终止时,它会发送这个信号给自己。例如,C库函数abort()就会发送这个信号
- SIGILL (信号值 4):当CPU检测到进程试图执行一条无效或未定义的指令时,系统会发送此信号。一个可能的原因是代码的内存被错误地当作数据修改
- SIGFPE (信号值 8):在数学运算错误时发送,比如除以零或浮点溢出
- SIGBUS (信号值 7):用于处理错误的内存访问,但这个信号在不同的系统和架构下含义可能会有所不同。在某些系统中,它用于处理对齐(alignment)问题
- SIGQUIT (信号值 3):用户通过控制台(通常是按Ctrl+\)向进程发送此信号,它不仅会停止进程的运行,还会生成coredump
Signal | Value | Action | Comment |
---|---|---|---|
SIGSEGV | 11 | Core | Invalid memory reference |
SIGABRT | 6 | Core | Abort signal from abort |
SIGILL | 4 | Core | Illegal Instruction |
SIGFPE | 8 | Core | Floating point exception |
SIGBUS | 7 | Core | Bus error (bad memory access) |
SIGQUIT | 3 | Core | Quit from keyboard |
2.2 coredump 文件生成原理
本节参考如有侵权,请告知:https://cloud.tencent.com/developer/article/1860631
coredump 文件的产生过程如下图所示:
Linux 内核实现中使用一个复杂的任务结构(task_struct)结构体来代表系统中的每个进程或线程,这个结构体被定义在 include/linux/sched.h
文件中,其中也包含了记录信号处理相关的属性,如用于存放待处理信号的 pending 信号队列和阻塞信号 blocked 等。
当一个信号被发送到进程时,它首先被添加到该进程的 pending 信号队列中。而内核在进程切换的上下文切换中,在运行被调用进程时会先检查这个 pending 信号队列。如果有待处理的信号,内核会在恢复进程运行前,调用信号的处理函数。这个过程大多数发生在 do_signal
函数中,这个函数在每次中断返回,系统调用返回,或者任何可能会改变进程状态的操作后执行。
如果一个信号是致命的(如 SIGSEGV, SIGABRT),并且进程并没有注册处理函数来处理它,那么内核会根据这个信号的默认行为来操作,比如可能会终止进程,或者产生 coredump 文件等。
通过上述分析,coredump 文件生成的过程可以总结为:程序运行产生 core 相关的信号 signal;do_signal 函数处理信号;如果默认行为是 Core 则调用 do_coredump 函数生成 core 文件。下面结合源码进一步分析该过程
1. 信号处理过程
在进程切换的上下文切换时,检查信号队列 pending 是否有待处理信号,有则调用 do_singal 函数处理,该函数中使用 get_signal_to_deliver
函数从进程的信号队列 pending 中获取一个信号,然后根据信号的类型来进行不同的操作。
get_signal_to_deliver
函数中处理 coredump 相关信号的代码实现如下,首先使用 dequeue_signal
函数获取信号,然后使用 sig_kernel_coredump
函数判断该信号是否为 coredump 相关信号,如果是则执行 core 相关动作
int get_signal_to_deliver(siginfo_t *info, struct k_sigaction *return_ka,
struct pt_regs *regs, void *cookie)
{
sigset_t *mask = ¤t->blocked;
int signr = 0;
...
for (;;) {
...
// 1. 从进程信号队列中获取一个信号
signr = dequeue_signal(current, mask, info);
...
// 2. 判断是否会生成 coredump 文件的信号
if (sig_kernel_coredump(signr)) {
// 3. 调用 do_coredump() 函数生成 coredump 文件
do_coredump((long)signr, signr, regs);
}
...
}
...
}
2. 生成 coredump 文件
当 coredump 相关信号被处理时,调用内核就会调用 do_coredump
函数来生成 coredump 文件,该函数核心代码如下,首先判断 ulimit 的资源限制情况,如果可用则创建 coredump 文件,最后将当前进程的寄存器、内存管理等相关信息写入到该文件中。
int do_coredump(long signr, int exit_code, struct pt_regs *regs)
{
char corename[CORENAME_MAX_SIZE + 1];
struct mm_struct *mm = current->mm;
struct linux_binfmt *binfmt;
struct inode *inode;
struct file *file;
int retval = 0;
int fsuid = current->fsuid;
int flag = 0;
int ispipe = 0;
binfmt = current->binfmt; // 当前进程所使用的可执行文件格式(如ELF格式)
...
// 1. 判断当前进程可生成的 coredump 文件大小是否受到资源限制
if (current->signal->rlim[RLIMIT_CORE].rlim_cur < binfmt->min_coredump)
goto fail_unlock;
...
// 2. 生成 coredump 文件名
ispipe = format_corename(corename, core_pattern, signr);
...
// 3. 创建 coredump 文件
file = filp_open(corename, O_CREAT|2|O_NOFOLLOW|O_LARGEFILE|flag, 0600);
...
// 4. 把进程的内存信息写入到 coredump 文件中
retval = binfmt->core_dump(signr, regs, file);
fail_unlock:
...
return retval;
}
如果文章对你有帮助,欢迎一键三连 👍 ⭐️ 💬 。如果还能够点击关注,那真的是对我最大的鼓励 🔥 🔥 🔥 。
参考资料
一文读懂 | coredump文件是如何生成的-腾讯云开发者社区-腾讯云
core dump 路径定义以及监控
Linux下gdb调试生成core文件并调试core文件-阿里云开发者社区
Linux进程描述符task_struct结构体详解
gdb调试coredump(使用篇)