Linux内核oops panic简析

源码基于:Linux 5.4

0. 前言
内核异常的级别大致分为三个:BUG、oops、panic。

BUG 是指那些不符合内核的正常设计,但内核能够检测出来并且对系统运行不会产生影响的问题,比如在原子上下文中休眠,在内核中用 BUG 标识。

Oops 就意外着内核出了异常,此时会将产生异常时出错原因,CPU的状态,出错的指令地址、数据地址及其他寄存器,函数调用的顺序甚至是栈里面的内容都打印出来,然后根据异常的严重程度来决定下一步的操作:杀死导致异常的进程或者挂起系统。

panic 本意是“恐慌”的意思,这里意旨 kernel 发生了致命错误导致无法继续运行下去的情况。根据实际情况 Oops最终也可能会导致panic 的发生。

本文将简单分析下这三种异常的流程。

1. BUG()
有过驱动调试经验的人肯定都知道这个东西,这里的 BUG 跟我们一般认为的 “软件缺陷” 可不是一回事,这里说的 BUG() 其实是linux kernel中用于拦截内核程序超出预期的行为,属于软件主动汇报异常的一种机制。这里有个疑问,就是什么时候会用到呢?一般来说有两种用到的情况:

一是软件开发过程中,若发现代码逻辑出现致命 fault 后就可以调用BUG()让kernel死掉(类似于assert),这样方便于定位问题,从而修正代码执行逻辑;
另外一种情况就是,由于某种特殊原因(通常是为了debug而需抓ramdump),我们需要系统进入kernel panic的情况下使用;
对于 arm64 来说 BUG() 定义如下:

arch/arm64/include/asm/bug.h
 
#ifndef _ARCH_ARM64_ASM_BUG_H
#define _ARCH_ARM64_ASM_BUG_H
 
#include <linux/stringify.h>
 
#include <asm/asm-bug.h>
 
#define __BUG_FLAGS(flags)                \
    asm volatile (__stringify(ASM_BUG_FLAGS(flags)));
 
#define BUG() do {                    \
    __BUG_FLAGS(0);                    \
    unreachable();                    \
} while (0)
 
#define __WARN_FLAGS(flags) __BUG_FLAGS(BUGFLAG_WARNING|(flags))
 
#define HAVE_ARCH_BUG
 
#include <asm-generic/bug.h>
 
#endif /* ! _ARCH_ARM64_ASM_BUG_H */

注意最后的 define HAVE_ARCH_BUG ,对于arm64 架构来说,会通过 include asm-generict/bug.h 对 BUG() 进行重定义。

include/asm-generic/bug.h
 
#ifndef HAVE_ARCH_BUG
#define BUG() do { \
    printk("BUG: failure at %s:%d/%s()!\n", __FILE__, __LINE__, __func__); \
    barrier_before_unreachable(); \
    panic("BUG!"); \
} while (0)
#endif
 
#ifndef HAVE_ARCH_BUG_ON
#define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while (0)
#endif
也就是在 arm64 架构中 BUG() 和 BUG_ON() 都是执行的 panic()。

而对于 arm 32位架构来说,BUG() 会向CPU 下发一条未定义指令而触发ARM 发起未定义指令异常,随后进入 kernel 异常处理流程,通过调用die() 经历Oops 和 panic,下面会单独分析 die() 函数,详细看第 3 节。

2. oops
oops 意外着内核出了异常,此时会将产生异常时出错原因,CPU的状态,出错的指令地址、数据地址及其他寄存器,函数调用的顺序甚至是栈里面的内容都打印出来,然后根据异常的严重程度来决定下一步的操作:杀死导致异常的进程或者挂起系统。

例如,在编写驱动或内核模块时,常常会显示或隐式地对指针进行非法取值或使用不正确的指针,导致内核发生一个 oops 错误。当处理器在内核空间中访问一个分发的指针时,因为虚拟地址到物理地址的映射关系还没有建立,会触发一个缺页中断,在缺页中断中该地址是非法的,内核无法正确地为该地址建立映射关系,所以内核触发一个oops 错误。代码如下:

arch/arm64/mm/fault.c
 
static void die_kernel_fault(const char *msg, unsigned long addr,
                 unsigned int esr, struct pt_regs *regs)
{
    bust_spinlocks(1);
 
    pr_alert("Unable to handle kernel %s at virtual address %016lx\n", msg,
         addr);
 
    mem_abort_decode(esr);
 
    show_pte(addr);
    die("Oops", regs, esr);
    bust_spinlocks(0);
    do_exit(SIGKILL);
}

通过 die() 会进行oops 异常处理,详细的 die() 函数流程看第 3 节。

当出现 oops,并且如果有源码,可以通过 arm 的 arch64-linux-gnu-objdump 工具看到出错的函数的汇编情况,也可以通过 GDB 工具分析。如果出错的地方为内核函数,可以使用 vmlinux 文件。

如果没有源码,对于没有编译符号表的二进制文件,可以使用:

arch64-linux-gnu-objdump -d oops.ko

命令来转储 oops.ko 文件

内核也提供了一个非常好用的脚本,可以快速定位问题,该脚本位于 Linux 源码目录下的 scripts/decodecode 中,会把出错的 oops 日志信息转换成直观有用的汇编代码,并且告知具体出错的汇编语句,这对于分析没有源码的 oops 错误非常有用。

3. die()
arch/arm64/kernel/traps.c
 
static DEFINE_RAW_SPINLOCK(die_lock);
 
/*
 * This function is protected against re-entrancy.
 */
void die(const char *str, struct pt_regs *regs, int err)
{
    int ret;
    unsigned long flags;
 
    raw_spin_lock_irqsave(&die_lock, flags);
 
    oops_enter();
 
    console_verbose();
    bust_spinlocks(1);
    ret = __die(str, err, regs);
 
    if (regs && kexec_should_crash(current))
        crash_kexec(regs);
 
    bust_spinlocks(0);
    add_taint(TAINT_DIE, LOCKDEP_NOW_UNRELIABLE);
    oops_exit();
 
    if (in_interrupt())
        panic("Fatal exception in interrupt");
    if (panic_on_oops)
        panic("Fatal exception");
 
    raw_spin_unlock_irqrestore(&die_lock, flags);
 
    if (ret != NOTIFY_STOP)
        do_exit(SIGSEGV);
}

oops_enter() ---> oops_exit() 为Oops 的处理流程,获取console 的log 级别,并通过 __die() 通过对Oops 感兴趣的模块进行callback,打印模块状态不为 MODULE_STATE_UNFORMED 的模块信息,打印PC、LR、SP、x0 等寄存器信息,打印调用栈信息,等等。

3.1 __die()
arch/arm64/kernel/traps.c
 
static int __die(const char *str, int err, struct pt_regs *regs)
{
    static int die_counter;
    int ret;
 
    pr_emerg("Internal error: %s: %x [#%d]" S_PREEMPT S_SMP "\n",
         str, err, ++die_counter);
 
    /* trap and error numbers are mostly meaningless on ARM */
    ret = notify_die(DIE_OOPS, str, regs, err, 0, SIGSEGV);
    if (ret == NOTIFY_STOP)
        return ret;
 
    print_modules();
    show_regs(regs);
 
    dump_kernel_instr(KERN_EMERG, regs);
 
    return ret;
}

打印 EMERG 的log,Internal error: oops.....;
notify_die() 会通知所有对 Oops 感兴趣的模块并进行callback;
print_modules() 打印模块状态不为 MODULE_STATE_UNFORMED 的模块信息;
show_regs() 打印PC、LR、SP 等寄存器的信息,同时打印调用堆栈信息;
dump_kernel_instr() 打印 pc指针和前4条指令;
这里不过多的剖析,感兴趣的可以查看下源码。

这里需要注意的是 notify_die() 会通知所有的Oops 感兴趣的模块,模块会通过函数 register_die_notifier() 将callback 注册到全局结构体变量 die_chain 中(多个模块注册进来形成一个链表),然后在通过 notify_die() 函数去解析这个 die_chain,并分别调用callback:

kernel/notifier.c
 
static ATOMIC_NOTIFIER_HEAD(die_chain);
 
int notrace notify_die(enum die_val val, const char *str,
           struct pt_regs *regs, long err, int trap, int sig)
{
    struct die_args args = {
        .regs    = regs,
        .str    = str,
        .err    = err,
        .trapnr    = trap,
        .signr    = sig,
 
    };
    RCU_LOCKDEP_WARN(!rcu_is_watching(),
               "notify_die called but RCU thinks we're quiescent");
    return atomic_notifier_call_chain(&die_chain, val, &args);
}
NOKPROBE_SYMBOL(notify_die);
 
int register_die_notifier(struct notifier_block *nb)
{
    vmalloc_sync_mappings();
    return atomic_notifier_chain_register(&die_chain, nb);
}

3.2 oops同时有可能panic
从上面 die() 函数最后看到,oops_exit() 之后也有可能进入panic():

arch/arm64/kernel/traps.c
 
void die(const char *str, struct pt_regs *regs, int err)
{
    ...
 
    if (in_interrupt())
        panic("Fatal exception in interrupt");
    if (panic_on_oops)
        panic("Fatal exception");
    ...
}
处于中断 或 panic_on_oops 打开时进入 panic。

中断的可能性:

硬件 IRQ;
软件 IRQ;
NMI;
panic_on_oops 的值受 CONFIG_PANIC_ON_OOPS_VALUE 影响。当然该值也可以通过节点

/proc/sys/kernel/panic_on_oops 进行动态修改。

4. panic()
panic 本意是“恐慌”的意思,这里意旨kernel发生了致命错误导致无法继续运行下去的情况。

kernel/panic.c
 
/**
 *    panic - halt the system
 *    @fmt: The text string to print
 *
 *    Display a message, then perform cleanups.
 *
 *    This function never returns.
 */
void panic(const char *fmt, ...)
{
    static char buf[1024];
    va_list args;
    long i, i_next = 0, len;
    int state = 0;
    int old_cpu, this_cpu;
    bool _crash_kexec_post_notifiers = crash_kexec_post_notifiers;
 
 
    //禁止本地中断,避免出现死锁,因为无法防止中断处理程序(在获得panic锁后运行)再次被调用panic
    local_irq_disable();
    //禁止任务抢占
    preempt_disable_notrace();
 
    //通过this_cpu确认是否调用panic() 的cpu是否为panic_cpu;
    //即,只允许一个CPU执行该代码,通过 panic_smp_self_stop() 保证当一个CPU执行panic时,
    //其他CPU处于停止或等待状态;
    this_cpu = raw_smp_processor_id();
    old_cpu  = atomic_cmpxchg(&panic_cpu, PANIC_CPU_INVALID, this_cpu);
 
    if (old_cpu != PANIC_CPU_INVALID && old_cpu != this_cpu)
        panic_smp_self_stop();
 
    //把console的打印级别放开
    console_verbose();
    bust_spinlocks(1);
    va_start(args, fmt);
    len = vscnprintf(buf, sizeof(buf), fmt, args);
    va_end(args);
 
    if (len && buf[len - 1] == '\n')
        buf[len - 1] = '\0';
 
    //解析panic所携带的message,前缀为Kernel panic - not syncing
    pr_emerg("Kernel panic - not syncing: %s\n", buf);
#ifdef CONFIG_DEBUG_BUGVERBOSE
    /*
     * Avoid nested stack-dumping if a panic occurs during oops processing
     */
    if (!test_taint(TAINT_DIE) && oops_in_progress <= 1)
        dump_stack();
#endif
 
    //如果kgdb使能,即CONFIG_KGDB为y,在停掉所有其他CPU之前,跳转kgdb断点运行
    kgdb_panic(buf);
 
    if (!_crash_kexec_post_notifiers) {
        printk_safe_flush_on_panic();
        //会根据当前是否设置了转储内核(使能CONFIG_KEXEC_CORE)确定是否实际执行转储操作;
        //如果执行转储则会通过 kexec 将系统切换到新的kdump 内核,并且不会再返回;
        //如果不执行转储,则继续后面流程;
        __crash_kexec(NULL);
 
        //停掉其他CPU,只留下当前CPU干活
        smp_send_stop();
    } else {
        /*
         * If we want to do crash dump after notifier calls and
         * kmsg_dump, we will need architecture dependent extra
         * works in addition to stopping other CPUs.
         */
        crash_smp_send_stop();
    }
 
    //通知所有对panic感兴趣的模块进行回调,添加一些kmsg信息到输出
    atomic_notifier_call_chain(&panic_notifier_list, 0, buf);
 
    /* Call flush even twice. It tries harder with a single online CPU */
    printk_safe_flush_on_panic();
 
    //dump 内核log buffer中的log信息
    kmsg_dump(KMSG_DUMP_PANIC);
 
    /*
     * If you doubt kdump always works fine in any situation,
     * "crash_kexec_post_notifiers" offers you a chance to run
     * panic_notifiers and dumping kmsg before kdump.
     * Note: since some panic_notifiers can make crashed kernel
     * more unstable, it can increase risks of the kdump failure too.
     *
     * Bypass the panic_cpu check and call __crash_kexec directly.
     */
    if (_crash_kexec_post_notifiers)
        __crash_kexec(NULL);
 
#ifdef CONFIG_VT
    unblank_screen();
#endif
    console_unblank();
 
    //关掉所有debug锁
    debug_locks_off();
    console_flush_on_panic(CONSOLE_FLUSH_PENDING);
 
    panic_print_sys_info();
 
    if (!panic_blink)
        panic_blink = no_blink;
 
    //如果sysctl配置了panic_timeout > 0则在panic_timeout后重启系统
    //首先,这里会每隔100ms重启 NMI watchdog
    if (panic_timeout > 0) {
        /*
         * Delay timeout seconds before rebooting the machine.
         * We can't use the "normal" timers since we just panicked.
         */
        pr_emerg("Rebooting in %d seconds..\n", panic_timeout);
 
        for (i = 0; i < panic_timeout * 1000; i += PANIC_TIMER_STEP) {
            touch_nmi_watchdog();
            if (i >= i_next) {
                i += panic_blink(state ^= 1);
                i_next = i + 3600 / PANIC_BLINK_SPD;
            }
            mdelay(PANIC_TIMER_STEP);
        }
    }
    //其次,这里确定reboot_mode,并重启系统
    if (panic_timeout != 0) {
        /*
         * This will not be a clean reboot, with everything
         * shutting down.  But if there is a chance of
         * rebooting the system it will be rebooted.
         */
        if (panic_reboot_mode != REBOOT_UNDEFINED)
            reboot_mode = panic_reboot_mode;
        emergency_restart();
    }
#ifdef __sparc__
    {
        extern int stop_a_enabled;
        /* Make sure the user can actually press Stop-A (L1-A) */
        stop_a_enabled = 1;
        pr_emerg("Press Stop-A (L1-A) from sun keyboard or send break\n"
             "twice on console to return to the boot prom\n");
    }
#endif
#if defined(CONFIG_S390)
    disabled_wait();
#endif
    pr_emerg("---[ end Kernel panic - not syncing: %s ]---\n", buf);
 
    /* Do not scroll important messages printed above */
    suppress_printk = 1;
    local_irq_enable();
    for (i = 0; ; i += PANIC_TIMER_STEP) {
        touch_softlockup_watchdog();
        if (i >= i_next) {
            i += panic_blink(state ^= 1);
            i_next = i + 3600 / PANIC_BLINK_SPD;
        }
        mdelay(PANIC_TIMER_STEP);
    }
}
 
EXPORT_SYMBOL(panic);

详细信息见代码注释。

panic_timeout 是根据节点 /proc/sys/kernel/panic 值配置,用以指定在重启系统之前需要 wait 的时长,详细可以查看:《panic 内核参数》;

4.1 panic_print_sys_info()
kernel/panic.c
 
#define PANIC_PRINT_TASK_INFO        0x00000001
#define PANIC_PRINT_MEM_INFO        0x00000002
#define PANIC_PRINT_TIMER_INFO        0x00000004
#define PANIC_PRINT_LOCK_INFO        0x00000008
#define PANIC_PRINT_FTRACE_INFO        0x00000010
#define PANIC_PRINT_ALL_PRINTK_MSG    0x00000020
 
static void panic_print_sys_info(void)
{
    if (panic_print & PANIC_PRINT_ALL_PRINTK_MSG)
        console_flush_on_panic(CONSOLE_REPLAY_ALL);
 
    if (panic_print & PANIC_PRINT_TASK_INFO)
        show_state();
 
    if (panic_print & PANIC_PRINT_MEM_INFO)
        show_mem(0, NULL);
 
    if (panic_print & PANIC_PRINT_TIMER_INFO)
        sysrq_timer_list_show();
 
    if (panic_print & PANIC_PRINT_LOCK_INFO)
        debug_show_all_locks();
 
    if (panic_print & PANIC_PRINT_FTRACE_INFO)
        ftrace_dump(DUMP_ALL);
}

panic_print 默认值为 0,可以通过 /proc/sys/kernel/panic_print 节点配置,详细看 《panic 内核参数》;

当 panic 发生的时候,用户可以通过如下bit 位配置打印系统信息:

bit 0:打印所有的进程信息;
bit 1:打印系统内存信息;
bit 2:打印定时器信息;
bit 3:打印当 CONFIG_LOCKEDP 打开时的锁信息;
bit 4:打印所有 ftrace;
bit 5:打印串口所有信息;
 

相关博文:

Linux 内核参数:panic 相关

内存管理可能出现的bug和panic
————————————————
版权声明:本文为CSDN博主「私房菜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/shift_wwx/article/details/130727534

linux内核调试(二)内核错误处理流程

架构:ARMv8

内核版本:5.14.0-rc5

1 内核错误处理方式

  当内核出现致命错误时,只要cpu还能正常运行,那么最重要的就是向用户输出详细的错误信息,以及保存问题出现时的错误现场。以上致命错误可包含以下两种类型:
(1)硬件能检测到的错误,如非法内存访问,非法指令等,此时cpu会触发异常,并进入异常处理流程。在异常处理流程中会触发oops或panic

(2)内核代码进入某些代码无法处理的异常分支,此时程序若继续执行可能会导致无法预知的后果,此时相关的代码会主动进入oops或panic

  其中panic的含义为惊恐、恐慌,即内核将无法继续进行,它会根据配置确定是否dump crash内存,向关心panic事件的模块发送notifier通知,以及打印panic相关的系统信息,最后将系统挂起或重启。

  oops的严重程度低于panic,因此在一般情况下其只是输出相关的错误信息,并退出进程,而并不会挂起内核。但是若oops发生在中断上下文,或内核配置了panic_on_oops选项,则它也会进入panic

2 arm64异常信息寄存器

  对于arm64架构,若cpu由于内存访问错误等原因进入异常,则可通过esr寄存器获取异常原因,并通过far寄存器获取异常内存的地址信息。其中esr寄存器定义如下:

  上图中EC表示异常类型,如以下为其中的一些典型取值:
(1)b100000:来自低异常等级的指令错误,如用户态的非法指令

(2)b100001:当前异常等级的指令错误

(3)b100010:pc对齐错误

(4)b100100:来自低异常等级的data abort异常,如用户态的内存异常

(5)b100101:当前异常等级的data abort异常

(6)b100110:栈指针sp对齐错误

(7)b101111:serror中断,它属于异步异常,一般来自外部abort,如内存访问总线时产生的abort异常等

IL表示异常发生时的指令长度,其取值如下:
(1)0:表示16位的thumb指令长度

(2)1:表示32位的arm指令长度

ISS表示每种类型的具体原因,它的取值会根据EC的不同而不同,如以EC为data abort为例,其相应的ISS定义如下(具体含义可参考armv8 trm):

  其中DFSC(data fault status code)用于给出data abort相关的信息,以下为其部分定义:

  另外对于data abort类型异常,abort地址对于分析异常原因至关重要,因此armv8架构通过far寄存器提供了该地址的值(虚拟地址),其相应的寄存器定义如下:

3 异常处理流程

  内核发生同步异常后,会根据异常发生时所处的异常等级(在当前异常等级,还是在低于当前异常等级中触发),和其所使用的栈指针类型(sp_el0还是sp_el1),跳转到相应的异常处理入口。

  异常处理函数在执行一些上下文保存,栈指针切换等基础工作后,将跳转到特定类型的handler。如cpu在异常发生时处于arm64模式下,且使用的栈指针为sp_el1时,则其将会跳转到el1h_64_sync_handler中。

  该函数会根据esr_el1寄存器中EC中的值,获取其对应的异常类型,然后调用特定异常类型相关的处理函数。在该函数中一般会通过esr_el1寄存器中ISS的值获取其具体的异常原因,并执行相应的处理。

  在处理流程中,若异常确实为非法操作引起(异常并不一定是错误,如缺页异常,断点、单步调试等debug异常都是正常的代码处理逻辑),则会调用oops或panic向用户报告错误,并退出当前进程或挂起系统。

  由于内核的异常种类繁多,而其处理流程又大同小异,因此下面将以arm64模式下,内核非法地址访问为例。其相应的处理流程如下:

3.1 data abort处理流程

  el1h_64_sync_handler首先读取esr_el1寄存器的值,然后解析其中EC的内容,并根据EC值调用其对应的处理函数,如对于data abort将会调用el1_abort,以下为其代码实现:

 
  1. asmlinkage void noinstr el1h_64_sync_handler(struct pt_regs *regs)

  2. {

  3. unsigned long esr = read_sysreg(esr_el1);

  4. switch (ESR_ELx_EC(esr)) {

  5. case ESR_ELx_EC_DABT_CUR:

  6. case ESR_ELx_EC_IABT_CUR:

  7. el1_abort(regs, esr);

  8. break;

  9. case ESR_ELx_EC_PC_ALIGN:

  10. el1_pc(regs, esr);

  11. break;

  12. default:

  13. __panic_unhandled(regs, "64-bit el1h sync", esr);

  14. }

  15. }

  el1_abort会调用do_mem_abort,该函数会根据esr_el1寄存器中DFSC的值,调用其相应的处理函数,这些函数通过以下所示的fault_info变量定义:

 
  1. static const struct fault_info fault_info[] = {

  2. { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 0 translation fault" },

  3. { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 1 translation fault" },

  4. { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 2 translation fault" },

  5. { do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 3 translation fault" },

  6. { do_bad, SIGKILL, SI_KERNEL, "unknown 8" },

  7. { do_page_fault, SIGSEGV, SEGV_ACCERR, "level 1 access flag fault" },

  8. { do_page_fault, SIGSEGV, SEGV_ACCERR, "level 2 access flag fault" },

  9. { do_page_fault, SIGSEGV, SEGV_ACCERR, "level 3 access flag fault" },

  10. }

以下为do_mem_abort的代码流程:

 
  1. void do_mem_abort(unsigned long far, unsigned int esr, struct pt_regs *regs)

  2. {

  3. const struct fault_info *inf = esr_to_fault_info(esr); (1)

  4. unsigned long addr = untagged_addr(far); (2)

  5. if (!inf->fn(far, esr, regs)) (3)

  6. return;

  7. if (!user_mode(regs)) { (4)

  8. pr_alert("Unhandled fault at 0x%016lx\n", addr);

  9. mem_abort_decode(esr);

  10. show_pte(addr);

  11. }

  12. arm64_notify_die(inf->name, regs, inf->sig, inf->code, addr, esr);

  13. }

(1)根据DFSC的值在fault_info数组中选择其相应的处理函数指针

(2)由于arm64架构可利用虚拟地址空闲的高位bit存储tag信息,以支持MTE特性。因此在获取其实际虚拟地址时需要将相应的tag信息先移除

(3)调用fault_info中获取到的回调函数,对于非法地址访问错误,其相应的回调函数为do_translation_fault

(4)若异常为未知异常,则通过以下流程直接执行错误处理

do_translation_fault根据异常是由用户态触发还是内核态触发,分别调用其对应等的处理函数,其代码如下:

 
  1. static int __kprobes do_translation_fault(unsigned long far,

  2. unsigned int esr,

  3. struct pt_regs *regs)

  4. {

  5. if (is_ttbr0_addr(addr))

  6. return do_page_fault(far, esr, regs); (1)

  7. do_bad_area(far, esr, regs); (2)

  8. return 0;

  9. }

(1)用户态处理函数

(2)内核态处理函数

  对于内核态情形,其最终会调用die_kernel_fault执行实际的错误处理,其代码如下:

 
  1. static void die_kernel_fault(const char *msg, unsigned long addr,

  2. unsigned int esr, struct pt_regs *regs)

  3. {

  4. mem_abort_decode(esr); (1)

  5. show_pte(addr); (2)

  6. die("Oops", regs, esr); (3)

  7. bust_spinlocks(0);

  8. do_exit(SIGKILL); (4)

  9. }

(1)它会解析esr_el1寄存器的值,并分别打印其相关的内容,如EC、IL、DFSC等

(2)该函数会打印异常地址对应的页表信息,包括pgd、p4d、pud、pmd和pte等

(3)执行实际的die操作,该流程将在下一节重点介绍

(4)杀死当前进程

3.2 die处理流程

  die函数主要执行oops相关流程,且若异常为中断流程中触发或设置了panic_on_oops选项,则进一步通过panic将系统挂起。其主要流程如下:

 
  1. void die(const char *str, struct pt_regs *regs, int err)

  2. {

  3. ret = __die(str, err, regs); (1)

  4. if (regs && kexec_should_crash(current))

  5. crash_kexec(regs); (2)

  6. if (in_interrupt())

  7. panic("%s: Fatal exception in interrupt", str);

  8. if (panic_on_oops) (3)

  9. panic("%s: Fatal exception", str);

  10. }

(1)调用die相关通知链对应的通知,使其执行die相关的操作,并打印oops相关的信息

(2)若需要crash系统,则通过该函数启动一个新的crash内核,并通过新内核将系统内存信息dump出来,以供事后分析。如可通过kdump或ramdump方式配置相应的crash内核

(3)若异常发生在中断中,或设置了panic_on_oops,则调用panic挂起系统

3.3 panic处理流程

  当内核走到panic时表明其已无法继续运行下去,因此需要执行一些系统挂死前的准备工作,其主要包含以下部分:
(1)在smp系统中,一个cpu正在处理panic时,可能另一个cpu也会触发panic。而该流程主要用于一些错误信息收集、内存转储等工作,并不需要也不支持并发操作。因此对于后续触发的cpu不需要执行该流程

(2)若正在使用kgdb对内核进行调试,则显然希望调试器能继续执行调试工作。故此时不会真正将系统挂死,而是将控制权转交给调试器

(3)若内核配置了kdump等内存转储功能,则在panic时将启动转储相关的流程

(4)在smp系统挂死之前,需要停止所有其它cpu的运行,以使系统真正地停下来

(5)最后,打印相关的系统信息后,使系统重启或进入死循环

  其相应的代码实现如下:

 
  1. void panic(const char *fmt, ...)

  2. {

  3. this_cpu = raw_smp_processor_id();

  4. old_cpu = atomic_cmpxchg(&panic_cpu, PANIC_CPU_INVALID, this_cpu);

  5. if (old_cpu != PANIC_CPU_INVALID && old_cpu != this_cpu) (1)

  6. panic_smp_self_stop();

  7. pr_emerg("Kernel panic - not syncing: %s\n", buf);

  8. kgdb_panic(buf); (2)

  9. if (!_crash_kexec_post_notifiers) {

  10. printk_safe_flush_on_panic();

  11. __crash_kexec(NULL); (3)

  12. smp_send_stop(); (4)

  13. } else {

  14. crash_smp_send_stop(); (5)

  15. }

  16. atomic_notifier_call_chain(&panic_notifier_list, 0, buf); (6)

  17. printk_safe_flush_on_panic();

  18. kmsg_dump(KMSG_DUMP_PANIC); (7)

  19. if (_crash_kexec_post_notifiers)

  20. __crash_kexec(NULL); (8)

  21. panic_print_sys_info(); (9)

  22. if (!panic_blink)

  23. panic_blink = no_blink;

  24. if (panic_timeout > 0) {

  25. pr_emerg("Rebooting in %d seconds..\n", panic_timeout);

  26. for (i = 0; i < panic_timeout * 1000; i += PANIC_TIMER_STEP) {

  27. touch_nmi_watchdog();

  28. if (i >= i_next) {

  29. i += panic_blink(state ^= 1);

  30. i_next = i + 3600 / PANIC_BLINK_SPD;

  31. }

  32. mdelay(PANIC_TIMER_STEP); (10)

  33. }

  34. }

  35. if (panic_timeout != 0) {

  36. if (panic_reboot_mode != REBOOT_UNDEFINED)

  37. reboot_mode = panic_reboot_mode;

  38. emergency_restart(); (11)

  39. }

  40. pr_emerg("---[ end Kernel panic - not syncing: %s ]---\n", buf);

  41. suppress_printk = 1;

  42. local_irq_enable();

  43. for (i = 0; ; i += PANIC_TIMER_STEP) {

  44. touch_softlockup_watchdog();

  45. if (i >= i_next) {

  46. i += panic_blink(state ^= 1);

  47. i_next = i + 3600 / PANIC_BLINK_SPD;

  48. }

  49. mdelay(PANIC_TIMER_STEP); (12)

  50. }

  51. }

(1)若先前已经有cpu正在处理panic流程,则本cpu不再重复处理,只需将当前cpu停止

(2)打印panic原因信息

(3)若panic流程会执行内存转储,则所有系统相关信息都会被保存到转储文件中,因此就不需要调用后面的通知链,因此可直接调用转储操作。但是转储操作也不是100%保险,因此若不是对其绝对信任,则会设置_crash_kexec_post_notifiers,它会先执行通知链调用和log dump相关流程,再调用内核转储操作
  __crash_kexec函数会根据当前是否设置了转储内核确定是否实际执行转储操作,若执行转储则会通过kexec将系统切换到新的kdump内核,并且不再会返回。若不执行转储则继续执行后续流程

(4 - 5)停止当前cpu之外的其它cpu运行

(6)调用关心panic事件相关模块向其注册的通知

(7)dump内核log buffer中的log信息

(8)若设置了_crash_kexec_post_notifiers,则根据是否设置了kexec内核,确定是否执行内存转储操作

(9)若不执行内存转储,则打印系统相关信息

(10)若设置了panic_timeout超时值,则执行超时等待操作

(11)若设置了panic_timeout超时值,在超时等待完成后重启系统

(12)若未设置panic_timeout超时值,则将系统设置为死循环状态,使其挂死

4 如何手动触发oops和panic

  在编码流程中,可能有一些非期望的代码分支,当系统进入这些分支表明出现了一些问题或严重错误。根据问题严重等级的不同,我们可能希望程序能打印一些警告信息,或者将系统设置为oops,甚至panic状态。

为此,内核提供了一些相关的宏和函数用于支持上述需求,以下为其中一些常用的定义:
(1)WARN_ON():打印警告信息和调用栈,但不会进入oops或panic

(2)BUG_ON():打印bug相关信息,并进入oops流程

(3)panic():该函数将直接出发panic流程,将系统设置为挂死状态

  除了通过编码方式以外,用户还可以通过sysrq魔术键触发panic流程,下面为通过proc方式触发sysrq相关panic流程的命令:

  echo c > /proc/sysrq-trigger

编辑于 2022-07-23 17:44

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值