【内核】深入分析内核panic(三)--内核错误处理流程
1 内核错误处理方式
当内核出现致命错误时,只要cpu还能正常运行,那么最重要的就是向用户输出详细的错误信息,以及保存问题出现时的错误现场。以上致命错误可包含以下两种类型:
(1)硬件能检测到的错误,如非法内存访问,非法指令等,此时cpu会触发异常,并进入异常处理流程。在异常处理流程中会触发oops或panic
(2)内核代码进入某些代码无法处理的异常分支,此时程序若继续执行可能会导致无法预知的后果,此时相关的代码会主动进入oops或panic
其中panic的含义为惊恐、恐慌,即内核将无法继续进行,它会根据配置确定是否crash dump 内存,向关心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,以下为其代码实现:
asmlinkage void noinstr el1h_64_sync_handler(struct pt_regs *regs)
{
unsigned long esr = read_sysreg(esr_el1);
switch (ESR_ELx_EC(esr)) {
case ESR_ELx_EC_DABT_CUR:
case ESR_ELx_EC_IABT_CUR:
el1_abort(regs, esr);
break;
case ESR_ELx_EC_PC_ALIGN:
el1_pc(regs, esr);
break;
…
default:
__panic_unhandled(regs, "64-bit el1h sync", esr);
}
}
el1_abort会调用do_mem_abort,该函数会根据esr_el1寄存器中DFSC的值,调用其相应的处理函数,这些函数通过以下所示的fault_info变量定义:
static const struct fault_info fault_info[] = {
…
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 0 translation fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 1 translation fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 2 translation fault" },
{ do_translation_fault, SIGSEGV, SEGV_MAPERR, "level 3 translation fault" },
{ do_bad, SIGKILL, SI_KERNEL, "unknown 8" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 1 access flag fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 2 access flag fault" },
{ do_page_fault, SIGSEGV, SEGV_ACCERR, "level 3 access flag fault" },
…
}
以下为do_mem_abort的代码流程:
void do_mem_abort(unsigned long far, unsigned int esr, struct pt_regs *regs)
{
const struct fault_info *inf = esr_to_fault_info(esr); (1)
unsigned long addr = untagged_addr(far); (2)
if (!inf->fn(far, esr, regs)) (3)
return;
if (!user_mode(regs)) { (4)
pr_alert("Unhandled fault at 0x%016lx\n", addr);
mem_abort_decode(esr);
show_pte(addr);
}
arm64_notify_die(inf->name, regs, inf->sig, inf->code, addr, esr);
}
(1)根据DFSC的值在fault_info数组中选择其相应的处理函数指针
(2)由于arm64架构可利用虚拟地址空闲的高位bit存储tag信息,以支持MTE特性。因此在获取其实际虚拟地址时需要将相应的tag信息先移除
(3)调用fault_info中获取到的回调函数,对于非法地址访问错误,其相应的回调函数为do_translation_fault
(4)若异常为未知异常,则通过以下流程直接执行错误处理
do_translation_fault根据异常是由用户态触发还是内核态触发,分别调用其对应等的处理函数,其代码如下:
static int __kprobes do_translation_fault(unsigned long far,
unsigned int esr,
struct pt_regs *regs)
{
…
if (is_ttbr0_addr(addr))
return do_page_fault(far, esr, regs); (1)
do_bad_area(far, esr, regs); (2)
return 0;
}
(1)用户态处理函数
(2)内核态处理函数
对于内核态情形,其最终会调用die_kernel_fault执行实际的错误处理,其代码如下:
static void die_kernel_fault(const char *msg, unsigned long addr,
unsigned int esr, struct pt_regs *regs)
{
…
mem_abort_decode(esr); (1)
show_pte(addr); (2)
die("Oops", regs, esr); (3)
bust_spinlocks(0);
do_exit(SIGKILL); (4)
}
(1)它会解析esr_el1寄存器的值,并分别打印其相关的内容,如EC、IL、DFSC等
(2)该函数会打印异常地址对应的页表信息,包括pgd、p4d、pud、pmd和pte等
(3)执行实际的die操作,该流程将在下一节重点介绍
(4)杀死当前进程
3.2 die处理流程
die函数主要执行oops相关流程,且若异常为中断流程中触发或设置了panic_on_oops选项,则进一步通过panic将系统挂起。其主要流程如下:
void die(const char *str, struct pt_regs *regs, int err)
{
…
ret = __die(str, err, regs); (1)
if (regs && kexec_should_crash(current))
crash_kexec(regs); (2)
…
if (in_interrupt())
panic("%s: Fatal exception in interrupt", str);
if (panic_on_oops) (3)
panic("%s: Fatal exception", str);
…
}
(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)最后,打印相关的系统信息后,使系统重启或进入死循环
其相应的代码实现如下:
void panic(const char *fmt, ...)
{
…
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) (1)
panic_smp_self_stop();
…
pr_emerg("Kernel panic - not syncing: %s\n", buf);
…
kgdb_panic(buf); (2)
if (!_crash_kexec_post_notifiers) {
printk_safe_flush_on_panic();
__crash_kexec(NULL); (3)
smp_send_stop(); (4)
} else {
crash_smp_send_stop(); (5)
}
atomic_notifier_call_chain(&panic_notifier_list, 0, buf); (6)
printk_safe_flush_on_panic();
kmsg_dump(KMSG_DUMP_PANIC); (7)
if (_crash_kexec_post_notifiers)
__crash_kexec(NULL); (8)
…
panic_print_sys_info(); (9)
if (!panic_blink)
panic_blink = no_blink;
if (panic_timeout > 0) {
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); (10)
}
}
if (panic_timeout != 0) {
if (panic_reboot_mode != REBOOT_UNDEFINED)
reboot_mode = panic_reboot_mode;
emergency_restart(); (11)
}
…
pr_emerg("---[ end Kernel panic - not syncing: %s ]---\n", buf);
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); (12)
}
}
(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
原文链接:https://www.zhihu.com/column/c_1533871448917118976 版权归原作者所有,如有侵权,请联系作者删除
【内核】深入分析内核panic(三)--内核错误处理流程的更多相关文章
- 《Linux内核分析》第三周学习笔记
<Linux内核分析>第三周学习笔记 构造一个简单的Linux系统MenuOS 郭垚 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.stud ...
- Linux内核中影响tcp三次握手的一些协议配置
在Linux的发行版本中,都存在一个/proc/目录,有的也称它为Proc文件系统.在 /proc 虚拟文件系统中存在一些可调节的内核参数.这个文件系统中的每个文件都表示一个或多个参数,它们可以通过 ...
- Linux内核读书笔记第三周 调试
内核调试的难点在于它不能像用户态程序调试那样打断点,随时暂停查看各个变量的状态. 也不能像用户态程序那样崩溃后迅速的重启,恢复初始状态. 用户态程序和内核交互,用户态程序的各种状态,错误等可以由内核来 ...
- 《Linux内核分析》第三周学习报告
<Linux内核分析>第三周学习报告 ——构造一个简单的Linux系统MenuOS 姓名:王玮怡 学号:201351 ...
- linux内核内存分配(三、虚拟内存管理)
在分析虚拟内存管理前要先看下linux内核内存的具体分配我開始就是困在这个地方.对内核内存的分类不是非常清晰.我摘录当中的一段: 内核内存地址 ============================ ...
- Linux内核初探 之 进程(三) —— 进程调度算法
一.基本概念 抢占 Linux提供抢占式多任务,基于时间片和优先级对进程进行强制挂起 非抢占的系统需要进程自己让步(yielding) 进程类型 IO消耗型 经常处于可运行态,等待IO操作过程会阻塞 ...
- 【内核】linux2.6版本内核编译配置选项(二)
目录 Linux2.6版本内核编译配置选项(一):http://infohacker.blog.51cto.com/6751239/1203633 Linux2.6版本内核编译配置选项(二):http ...
- Linux内核Makefile文件(翻译自内核手册)
--译自Linux3.9.5 Kernel Makefiles(内核目录documention/kbuild/makefiles.txt) kbuild(kernel build) 内核编译器 Thi ...
- Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3.0 ARMv7)
http://blog.chinaunix.net/uid-20543672-id-3157283.html Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3 ...
- 内核知识第六讲,内核编写规范,以及获取GDT表
内核知识第六讲,内核编写规范,以及获取GDT表 一丶内核驱动编写规范 我们都知道,在ring3下,如果我们的程序出错了.那么就崩溃了.但是在ring0下,只要我们的程序崩溃了.那么直接就蓝屏了. 那么 ...
随机推荐
- 虚拟机运行Hadoop | 各种问题解决的心路历程
ps:完成大数据技术实验报告的过程,出项各种稀奇古怪的问题.(知道这叫什么吗?经济基础决定上层建筑,我当时配置可能留下了一堆隐患,总之如果有同样的问题,希望可以帮到你) 一.虚拟机网络连接不通的各种情 ...
- .NET使用分布式网络爬虫框架DotnetSpider快速开发爬虫功能
前言 前段时间有同学在微信群里提问,要使用.NET开发一个简单的爬虫功能但是没有做过无从下手.今天给大家推荐一个轻量.灵活.高性能.跨平台的分布式网络爬虫框架(可以帮助 .NET 工程师快速的完成爬虫 ...
- spring cloud生态中Feign、Ribbon、loadbalancer的一些历史
背景 本意是想写个feign中loadbalancer组件和nacos相遇后,一个兼容相关的问题,后面发现Feign这套东西很深,想一篇文章写清楚很难,就先开一篇,讲历史. Feign.OpenFei ...
- Leader笔记:程序员小团队透明和信任管理
今天想跟大家分享一下小团队的透明管理,这也是一个管理技巧,相信很多Leader身份的同学都了解到主管有很大的一个优势,就是在组织内拥有了信息不对称能力,Leader能够听到和了解到完全不同层面上的内容 ...
- SpringBoot事件机制
1.是什么? SpringBoot事件机制是指SpringBoot中的开发人员可以通过编写自定义事件来对应用程序进行事件处理.我们可以创建自己的事件类,并在应用程序中注册这些事件,当事件被触发时,可以 ...
- Volcano 原理、源码分析(一)
0. 总结前置 1. 概述 2. Volcano 核心概念 2.1 认识 Queue.PodGroup 和 VolcanoJob 2.2. Queue.PodGroup 和 VolcanoJob 的关 ...
- Python——第四章:闭包(Closure)、装饰器(Decorators)
闭包: 本质, 内层函数对外层函数的局部变量的使用. 此时内层函数被称为闭包函数 1. 可以让一个变量常驻与内存,可随时被外层函数调用. 2. 可以避免全局变量被修改.被污染.更安全.(通 ...
- 去年最火的 JS 开源项目「GitHub 热点速览」
近日,「Best of JS」发布了过去一年在 GitHub 上 Star 数增速最快的 JavaScript 开源项目(2023 JavaScript Rising Stars),前 10 的开源项 ...
- java生成企业公章图片源代码
企业公章图片在电子签章业务中应用广泛,在电子签章应用过程中首先需要生成公章图片,然后再使用公章图片结合数字签名技术完成电子签,这样就实现了从可视化到不可篡改的数字化电子签章功能,以下是企业公章图片生成 ...
- WinForm如何将子控件插入FlowLayoutPanel开始位置
需求描述 动态将控件插入到FlowLayoutPanel控件的开始位置 实现方案 将控件添加到FlowLayoutPanel的Controls集合中,默认插到末尾 使用SetChildIndex方法更 ...