深入理解coredump(1)-signal原理

Coredump是操作系统在进程异常终止时,将该进程当时的内存状态、寄存器状态等关键信息保存到磁盘文件的一种机制。coredump的生成依赖于信号(signal),信号可以有异常进程触发,也可以通过kill命令触发,因此需要先了解下signal工作原理才能更好的理解coredump的流程。

  • 架构:arm64
  • kernel版本:5.15

signal概述

signal是进程间通信的一种方式,即一个进程向另一个进程发送消息,接收方一般是可以自定义handler执行。同时也是kernel和userspace通信的一种方式,kernel也可以向用户进程发送signal。先以2个简单的demo进行说明用法:

  1. 进程间通信
// test_signal.c
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

void signal_handler(int signum) {
    if (signum == SIGUSR2) {
        printf("hello world\n");
        exit(EXIT_SUCCESS);
    }
}

int main() {
    signal(SIGUSR2, signal_handler);
    while(1);
    return 0;
}

上面的代码说明:对SIGUSR2(12)信号定义一个handler,当进程收到SIGUSR2信号时会打印"hello world"

# gcc -o test_signal test_signal.c
# test_signal &
# kill -12 $(pidof test_signal)
# hello world

其实是在shell中fork了一个子进程执行kill命令,该子进程向test_signal发送SIGUSR2信号,当test_signal收到后就触发了注册的signal_handler函数。该demo展示了进程间通信的用法,先提出一些疑问:

  • signal函数注册的handler是存放在哪里的;
  • kill命令是在用户空间直接向test_signal发送的吗
  • test_signal进程是怎么知道有信号过来的?什么时候执行handler
  1. kernel和userspace通信
// test_signal1.c
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

void signal_handler(int signum) {
    if (signum == SIGABRT) {
        printf("hello world\n");
        exit(EXIT_SUCCESS);
    }
}

int main() {
    signal(SIGABRT, signal_handler);
    assert(0); //触发异常
    return 0;
}

上面的代码说明:对SIGABRT(程序异常)信号定义一个handler,当程序运行时,assert(0)断点会触发异常,kernel会向该进程发送SIGABRT信号。同样提出一些疑问:

  • kernel怎么知道哪个进程出现了异常;
  • 如果进程没有定义handler,会怎么处理该异常。

kill发送信号

  1. prepare_kill_siginfo:构建siginfo信息,主要包含sig发送者pid、sig来源类型(USER、KERNEL、TIMER)以及sig ID(sig)。
  2. kill_something_info:向目标进程发送信号,逐级传参调用函数。kill_pid_info:首先根据struct pid参数获取task_struct变量(pid_task),注意这里的pid是进程号pid_t转化后的,可以理解是线程组的一个概念,因为kill命令是对一个用户进程中所有的线程都要执行发送信号,而每个线程是个独立的struct task,所以要能够找到进程中对应的所有线程。
  3. group_send_sig_info:对线程组中所有成员发送signal,最重要的实现也是在这里,单独分析。

group_send_sig_info

  1. 逐级传参,调用__send_signal向目标进程发送signal的主要逻辑在此。
  2. prepare_signal:判断signal是否需要忽略。如果是对init进程的SIGKILL和SIGTOP,需要忽略,用户空间的init进程不能退出;用户程序未定义handler,会使用默认的SIG_DFL,需要忽略;用户向kernel线程发送的信号需要忽略。
  3. 对于SIGKILL和目标进程是内核进程的信号,需要立刻处理,无需构建sigqueue挂接在pending等待处理。
  4. __sigqueue_alloc:步骤3不成立则需要构建sigqueue挂接到pending列表中,这里分配内存使用的是GFP_ATOMIC(高优先级,可以使用紧急内存)。然后把对位图置为1表示有signal需要处理。

complete_signal

  1. wants_signal:判断signal是否需要接收,signal为blocked、进程处于exiting和stop状态不需要接收。如果signal为SIGKILL必须处理。
  2. sig_fatal:如果信号是fatal的并且是非coredump类型的,就需要把信号转化成SIGKILL,对进程中每个线程进行处理。判断fatal的依据为:非SIG_KERNEL_IGNORE_MASK和SIG_KERNEL_STOP_MASK信号并且action为SIG_DFL(kernel定义的默认行为)。
  3. signal_wake_up:唤醒进程来处理signal,这里的唤醒是指把某些状态的进程重新放入runqueue,而处理signal时机是在进程下次被调度时处理do_notify_resume。

 */
void signal_wake_up_state(struct task_struct *t, unsigned int state)
{
	set_tsk_thread_flag(t, TIF_SIGPENDING);
	if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
		kick_process(t);
}

status参数表示可以被唤醒的状态,如果是SIGKILL信号,可以唤醒的status包括INTERRUPTIBLE、STOPPED、TRACED等;如果非SIGKILL则只能唤醒INTERRUPTIBLE状态。wake_up_state唤醒失败返回0,进一步执行kick_process函数。而kick_process会发送IPI中断触发进程的重新调度。

上面梳理完了signal发送的流程,最终是把signal封装到sigqueue中挂接到目标进程struct task的pending中等待处理。

signal处理

图中列举了系统调用和中断2种返回用户空间的路径,都会调用exit_to_user_mode函数。在返回用户空间之前会调用prepare_exit_to_user_mode函数做一些准备工作。

  • prepare_exit_to_user_mode:会先检查thread_info->flags是否置位_TIF_WORK_MASK,如果置位说明有work需要处理,其中就包括检查thread_flags是否有待处理的signal(_TIF_SIGPENDING);
  • do_signal:如果上步骤中有待处理的信号就会在此处进行处理。需要单独进行说明。

do_signal

  1. get_signal:主要是有一个for循环获取pending中的signal,针对不同类型的signal有先后顺序。先通过dequeue_synchronous_signal获取sync signal(程序中代码运行异常的信号,对应el0_sync);然后才会通过dequeue_signal获取其他signal。
  2. 进行handler判断是否需要忽略、用户是否有自定义(非SIG_DFL),如果有自定义就会跳出循环执行handler_signal,handler_signal函数执行的是用户自定义的handler。
  3. signal默认的action(SIG_DFL)是在get_signal函数中实现的,通过不同条件分支判断mark执行。如下图包含忽略ign、生成coredump、停止stop和终止term。主要说下生成coredump的信号(sig_kernel_coredump),满足条件的情况下调用do_coredump来生成core文件。这里涉及到文章主题关键的流程,不展开,下篇再梳理。
  1. 执行handle_signal函数调用用户定义的handler。setup_rt_frame:用户定义的handler代码段处于用户空间,无法直接在kernel中执行,需要单独构建一个用户空间栈帧,该函数作用就在于此。setup_return函数中会把用户定义的handler地址赋值给PC寄存器,表示从此处执行代码。
static void setup_return(struct pt_regs *regs, struct k_sigaction *ka,
			 struct rt_sigframe_user_layout *user, int usig)
{
...
	regs->sp = (unsigned long)user->sigframe;
	regs->regs[29] = (unsigned long)&user->next_frame->fp;
	regs->pc = (unsigned long)ka->sa.sa_handler;
...
}

上面是针对kill方式sigal传导路径进行了说明,下面对程序代码异常的情况进行说明,signal处理的路径和kill一样,只是signal发送的路径有差异。

程序异常signal

用户进程异常后会触发中断进入了kernel,kernel有定义异常向量表可以找到不同类型的handler。

asmlinkage void noinstr el0t_64_sync_handler(struct pt_regs *regs)
{
	unsigned long esr = read_sysreg(esr_el1);

	switch (ESR_ELx_EC(esr)) {
	case ESR_ELx_EC_SVC64:
		el0_svc(regs);
		break;
	case ESR_ELx_EC_DABT_LOW:
		el0_da(regs, esr);
		break;
      ......
	default:
		el0_inv(regs, esr);
	}

assert断点的异常走的是el0_svc,就从这里入手查看signal的发送路径。

流程比较清晰:代码异常后意味着程序最终会die,根据异常的类型发送signal,发送完成后在返回用户空间之前还是会调用exit_to_user_mode(逻辑同上,就不再赘述)。

原文链接:,转发请注明来源!