我理解的就是触发了一些底层的错误,从而产生的当时进程上下文的记录,以用于后续的分析。
这些底层的错误包括:
coredump文件产生的原理
什么情况下进程会收到信号
当进程接收到某些信号而导致异常退出时,就会生成 coredump 文件。
当进程从内核态返回到用户态前,内核会查看进程的信号队列中是否有信号没有处理,如果有就调用 do_signal 内核函数处理信号。
进程从内核态返回到用户态的地方有很多,如从系统调用返回、从硬中断处理程序返回和从进程调度程序返回等
从处理信号到生成coredump文件
do_signal
get_signal_to_deliver
// 1. 从进程信号队列中获取一个信号
signr = dequeue_signal(current, mask, info)
// 2. 判断是否会生成 coredump 文件的信号
if (sig_kernel_coredump(signr)) {
// 3. 调用 do_coredump() 函数生成 coredump 文件
do_coredump((long)signr, signr, regs)
生成coredump文件过程
do_coredump
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);
如何使用core_pattern文件生成文件路径名
format_corename()根据core_pattern中的设置,生成coredump文件名。并且判断coredump文件生成方式,ispipe为真则通过管道传输给其他应用处理;否则直接保存成文件。
static int format_corename(struct core_name *cn, struct coredump_params *cprm)
{
const struct cred *cred = current_cred();
const char *pat_ptr = core_pattern;
int ispipe = (*pat_ptr == '|');------------------------------------------|表示通过pipe处理coredump文件。
int pid_in_pattern = 0;
int err = 0;
cn->used = 0;
cn->corename = NULL;
if (expand_corename(cn, core_name_size))
return -ENOMEM;
cn->corename[0] = '\0';
if (ispipe)
++pat_ptr;
/* Repeat as long as we have more pattern to process and more output
space */
while (*pat_ptr) {
if (*pat_ptr != '%') {
err = cn_printf(cn, "%c", *pat_ptr++);
} else {
switch (*++pat_ptr) {
/* single % at the end, drop that */
case 0:
goto out;
/* Double percent, output one percent */
case '%':
err = cn_printf(cn, "%c", '%');
break;
/* pid */
case 'p':
pid_in_pattern = 1;
err = cn_printf(cn, "%d",
task_tgid_vnr(current));-------------------------%p表示记录当前进程组的pid。
break;
/* global pid */
case 'P':-------------------------------------------------------%P表示记录当前进程组的pid。
err = cn_printf(cn, "%d",
task_tgid_nr(current));
break;
case 'i':
err = cn_printf(cn, "%d",
task_pid_vnr(current));--------------------------%i表示记录当前线程的pid。
break;
case 'I':------------------------------------------------------%I表示记录当前线程的pid。
err = cn_printf(cn, "%d",
task_pid_nr(current));
break;
/* uid */
case 'u':-------------------------------------------------------%u表示当前用户id。
err = cn_printf(cn, "%u",
from_kuid(&init_user_ns,
cred->uid));
break;
/* gid */
case 'g':-------------------------------------------------------%g表示group id。
err = cn_printf(cn, "%u",
from_kgid(&init_user_ns,
cred->gid));
break;
case 'd':
err = cn_printf(cn, "%d",
__get_dumpable(cprm->mm_flags));------------------------%d表示dump的用户类型:SUID_DUMP_DISABLE/SUID_DUMP_USER/SUID_DUMP_ROOT。
break;
/* signal that caused the coredump */
case 's':
err = cn_printf(cn, "%d",
cprm->siginfo->si_signo);----------------------------%s记录产生coredump的信号。
break;
/* UNIX time of coredump */
case 't': {
time64_t time;
time = ktime_get_real_seconds();
err = cn_printf(cn, "%lld", time);---------------------------%t记录产生coredump的时间。
break;
}
/* hostname */
case 'h':--------------------------------------------------------%h记录主机名。
down_read(&uts_sem);
err = cn_esc_printf(cn, "%s",
utsname()->nodename);
up_read(&uts_sem);
break;
/* executable */
case 'e':
err = cn_esc_printf(cn, "%s", current->comm);----------------%e记录进程中comm名称。
break;
case 'E':
err = cn_print_exe_file(cn);---------------------------------%E记录可执行文件名称。
break;
/* core limit size */
case 'c':
err = cn_printf(cn, "%lu",
rlimit(RLIMIT_CORE));------------------------------%c记录coredump的limit值。
break;
default:
break;
}
++pat_ptr;
}
if (err)
return err;
}
out:
if (!ispipe && !pid_in_pattern && core_uses_pid) {
err = cn_printf(cn, ".%d", task_tgid_vnr(current));
if (err)
return err;
}
return ispipe;
}
我们大致说明了coredump的生成机制,也就是coredump文件的生成流程。
但是实际上,这个流程并不是coredump独用,而是一套通用流程。
在这个流程中,有信号产生方、信号存储方、信号消费方。
在这三个阶段,其中信号和中断有类似的地方,但是二者并不等价。
信号本质上是在软件层次上对中断机制的一种模拟,其主要有以下几种来源:
程序错误:除零,非法内存访问等。
外部信号:终端 Ctrl-C 产生 SGINT 信号,定时器到期产生SIGALRM等
显式请求:kill函数允许进程发送任何信号给其他进程或进程组。
信号如何被接收的在今天这篇文章并不做介绍,只对信号存储和消费两部分进行说明。
首先每个进程的结构体里都有信号的信息存储:
struct task_struct {
...
int sigpending;
...
struct signal_struct *sig;
sigset_t blocked;
struct sigpending pending;
...
}
sigpending 表示进程是否有信号需要处理(1表示有,0表示没有);成员 blocked 表示被屏蔽的信息,每个位代表一个被屏蔽的信号;成员 sig 表示信号相应的处理方法,其类型是 struct signal_struct。
在进程从用户态陷入内核态时,内核态完成了相关工作之后,返回用户态之前,都会例行对进程的信号队列进行检查。
这段逻辑是汇编实现的,因为在linux内核中,用户态的切换就是汇编来控制的。【可能有同学对用户态和内核态的切换不太了解,其实用大白话讲就是,只是对cpu的寄存器的赋值进行更换,跟普通的进程切换可能没什么区别,但是同时又有区别,因为用户态和内核态的堆栈是分离开的,这样二者在切换的时候是需要在两个堆栈上进行操作,并且保存上下文信息】
ENTRY(ret_from_sys_call)
...
ret_with_reschedule:
...
cmpl $0, sigpending(%ebx) // 检查进程的sigpending成员是否等于1
jne signal_return // 如果是就跳转到 signal_return 处执行
restore_all:
RESTORE_ALL
ALIGN
signal_return:
sti // 开启硬件中断
testl $(VM_MASK),EFLAGS(%esp)
movl %esp,%eax
jne v86_signal_return
xorl %edx,%edx
call SYMBOL_NAME(do_signal) // 调用do_signal()函数进行处理
jmp restore_all
可以看到在内核代码中,指定了哪些信号可以产生dump core文件