深入理解 kernel panic 的流程
我们在项目开发过程中,很多时候会出现由于某种原因经常会导致手机系统死机重启的情况(重启分Android重启跟kernel重启,而我们这里只讨论kernel重启也就是 kernel panic 的情况),死机重启基本算是影响最严重的系统问题了,有稳定复现的,也有概率出现的,解题难度也千差万别,出现问题后,通常我们会拿到类似这样的kernel log信息(下面log仅以调用BUG()为例,其它异常所致的死机log信息会有一些不同之处):
这是linux 内核在死机之前输出的相关重要信息,包括PC指针、调用栈等在内的非常重要的便于Debug的线索,比如我们可以借助GUN tools(add2Line)工具结合内核符号映射表vmlinux来定位当前PC指针所在的代码具体行数(定位到出错代码行并不意味着就找到了问题的根本原因跟修复异常,这个需要根据异常的复杂程度而论)。深入理解这些关键打印log信息的含义和机制非常有助于我们对于此类死机问题的定位和分析(对于内存被踩、硬件不稳定导致的一类问题分析有局限性),这也是我们需要深入学习内核异常流程的初衷。
这里我们必须弄清楚几个问题:
这些死机前留下的关键register信息是怎么来的,有什么用,具体含义是什么?
如何利用这些遗留的线索找到出问题代码具体在哪支文件,在哪一行?
内核发生致命异常到死机的总流程是怎样的,类似死机问题应该如何着手分析?
为此,本文就从最常见的主动触发BUG()为例解析上面的疑问及分析整个kernel panic流程。
什么是BUG() ?
有过驱动调试经验的人肯定都知道这个东西,这里的BUG跟我们一般认为的“软件缺陷”可不是一回事,这里说的BUG()其实是linux kernel中用于拦截内核程序超出预期的行为,属于软件主动汇报异常的一种机制。这里有个疑问,就是什么时候会用到呢?一般来说有两种用到的情况,一是软件开发过程中,若发现代码逻辑出现致命fault后就可以调用BUG()让kernel死掉(类似于assert),这样方便于定位问题,从而修正代码执行逻辑;另外一种情况就是,由于某种特殊原因(通常是为了debug而需抓ramdump),我们需要系统进入kernel panic的情况下使用。
BUG()跟BUG_ON(1)其实本质是一回事,后者只是在前者的基础上做了简单的封装而已,BUG()的实现 本质是埋入一条未定义指令:0xe7f001f2,触发ARM发起Undefined Instruction异常(PS:ARM有分10种异常类型,详细可以复习ARM异常模型章节)。
BUG() 流程分析
BUG()到系统重启的总流程图:

调用BUG()会向CPU下发一条未定义指令而触发ARM发起未定义指令异常,随后进入kernel异常处理流程历经 Oops,die(),__die()等流程输出用于调试分析的关键线索,最后进入panic()结束自己再获得重生的过程,这个就是整个过程的基本流程,下面先来看die()具体做了什么呢?
die() 流程
源码:
总流程大致如下:

通常来说,代码分析过程结合kernel log一起看会理解来得更加深刻,如果是BUG()/BUG_ON(1)导致的异常,那么走到report_bug 就可以看到下面标志性 log:
所以如果在log中看到了这个 “[ cut here ]” 的信息就推断是软件发生致命fault而主动call了BUG()所致的系统重启了,就可以根据相关信息尝试定位分析修复异常了.这里要注意的是还有另外一种__WARN()的情况也会打印出 “[ cut here ]” 的标志性log但是内核并不会挂掉,容易造成误导:
所以其实从显现上很好区分两种情况,如果是BUG()/BUG_ON(1)那么内核一定会挂掉重启,而__WARN()只会dump_stack()而不会死机, 从源码跟log信息也可以容易区分两种情况,如果是BUG()/BUG_ON(1)的话一定有类似下面的log输出,只要搜索关键字:“Internal error: Oops” 即可。
【文章福利】小编推荐自己的Linux内核技术交流群:【749907784】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!(含视频教程、电子书、实战项目及代码)


__die() 流程分析
从上面输出的log信息还不足以定位具体出问题的代码位置,包括定位异常所需要的最关键的 PC指针、调用栈等这些对于调试来说至关重要的线索信息都是在__die()中输出。
流程图:

打印出标志性log信息:
log 显示异常str是Oops - BUG,error-code 为0,die计数器次数:1
Oops 的本意为 “哎呀” 的一个俚语,这里意形象的意指kernel出现了一件意外而不知道该如何处理的事件。
notify_die() 会通知对Oops感兴趣的模块执行相关回调,比如mtk的aee异常引擎模块就是通过注册到die_chain通知链上的。
mtk的aee异常引擎在kernel初始化的时候会去注册到die_chain通知链,而且我们可以看到其实还注册了panic通知链。
而对我们调试追踪有用的关键信息是在 __show_regs() 里面打印的:
这里打印出了重要的pc停下的位置、相关寄存器信息,发生的是user还是kernel的异常、发生异常的cpu、进程pid等信息。

接下来 dump_mem() 用于dump出当前线程的内存信息:
使用 dump_backtrace(regs, tsk) 打印出调试最直观的调用栈信息:
通过上面的调用栈信息结合GUN Tools(add2Line)基本就可以定位发生异常的具体代码位置了。
最后会通过dump_instr(KERN_EMERG, regs) 打印出pc指针和前4条指令:
看到这个 e7f001f2 了吧,是不是很眼熟?这个就是BUG()中埋入的未定义指令!
到这一步,大部分关键信息都已经输出了,可以通过add2Line工具定位出具体死在的代码行号,大致看看发生了什么,如果是BUG()导致的异常,那么就可以考虑分析和修复异常了,因为BUG()属于主动汇报异常,一般来说debug难度会相对其它的被动上报方式容易得多.
例如:
从上面log知PC死在的地址,通过add2Line工具结合内核符号映射表 vmlinux 就可以定位出具体代码所在文件行号:
定位到了具体代码行号就可以进一步分析代码log找出问题原因修复异常了(一般来说BUG()导致的异常比较好解,其它的情况难度就是天差地别了..)。那么接下来kernel要干什么呢?重要信息都输出完了接下来就直接走 kernel panic 流程了.
panic 流程
panic 本意是“恐慌”的意思,这里意旨kernel发生了致命错误导致无法继续运行下去的情况。
流程图:

最后附上总时序图:

原文作者:人人极客社区
