ProgramAndroidAndroid FrameworkAndroid开发

深入分析Linux Kernel Exception 框架(基础

2018-01-24  本文已影响32人  程序员Android1

Kernel Exception 在工作中偶尔会遇到,发生此异常时候经常会导致手机死机或重启。那么如何解决这些问题,本篇文章将带你从KE小白变小牛。

注:
本文部分内容参考MTK,如有侵权,请告知,立马关闭此文,停止侵权。知识共享,感谢支持!

本篇文章主要介绍 Android 开发中的部分知识点,通过阅读本篇文章,您将收获以下内容:

基础篇: 通过log分析KE
1.Kernel Exception
2.kernel空间布局
3.了解printk
4.ram console
5.前期异常处理
6.die()流程
7.panic()流程
8.nested panic

欢迎关注微信公众号:程序员Android
公众号ID:ProgramAndroid
获取更多信息

微信公众号:ProgramAndroid

我们不是牛逼的程序员,我们只是程序开发中的垫脚石。
我们不发送红包,我们只是红包的搬运工。

基础篇: 通过log分析KE

1.Kernel Exception

KE(Kernel Exception)概念

Android OS由3层组成,最底层是kernel,上面是native bin/lib,最上层是java层:


Android OS 3层结构

任何软件都有可能发生异常,比如野指针,跑飞、死锁等等。

异常发生在kernel层,我们就叫它为KE(kernel exception),同理,发生在native就是NE,java层就是JE。这篇文章仅关注底层的KE。

KE类别

kernel有2种崩溃类别

1. oops (类似assert,有机会恢复)

2. panic

常用调试方法

凡是程序就有bug。bug总是出现在预料之外的地方。据说世界上第一个bug是继电器式计算机中飞进一只蛾子,倒霉的飞蛾夹在继电器之间导致了计算机故障。由于这个小虫子,程序中的错误就被称为了bug。

有Bug就需要Debug,而调试是一种很个性化的工作,十个人可能有十种调试方法。但从手段上来讲,大致可分为两类,在线调试 (Online Debug) 和离线调试 (Offline Debug).

在线调试,

Online debug, 指的是在程序的运行过程中监视程序的行为,分析是否符合预期。通常会借助一些工具,如GDB和Trace32等。有时候也会借助一些硬件设备的协助,如仿真器/JTAG,但是准备环境非常困难,而且用起来也很麻烦,除非一些runtime问题需要外很少使用。

离线调试,

Offline debug, 指的是在程序的运行中收集需要的信息,在Bug发生后根据收集到的信息来分析的一种手段。通常也分为两种方式,一种是Logging,一种是Memory Dump。

Logging, 日志或者相关信息的收集,可以比较清晰的看到代码的执行过程,对于逻辑问题是一种有效的分析手段,由于其简单易操作,也是最为重要的一种分析手法。

Memory Dump, 翻译过来叫做内存转储,指的是在异常发生的时刻将内存信息全部转储到外部存储器,即将异常现场信息备份下来以供事后分析。是针对CPU执行异常的一种非常有效的分析手段。在Windows平台,程序异常发生之后可以选择启动调试器来马上调试。在Linux平台,程序发生异常之后会转储core dump,而此coredump可以用调试器GDB来进行调试。而内核的异常也可以进行类似的转储。
下面我们由浅入深剖析各种调试方法,先从logging开始吧。

2.Kernel空间布局

在分析KE前,你要了解kernel内存布局,才知道哪些地址用来做什么,可能会是什么问题。

在内核空间中存在如下重要的段:

还有其他段小的段没有列出来,可能根据不同的版本而差别。

目前智能机已进入64bit,因此就存在32bit布局和64bit布局,下面一一讲解。

ARM64bit kernel布局

ARM64可以使用多达48bit物理、虚拟地址(扩充成64bit,高位全为1或0)。对linux kernel来讲,目前配置为39bit的kernel空间。

由于多达512GB的空间,因此完全可以将整个RAM映射进来,0xFFFFFFC000000000之后就是一一映射了,就无所谓high memory了。

vmalloc区域功能除了外设寄存器也直接映射到vmalloc了,就没有32bit布局里的IO map space了。

不同版本的kernel,布局稍有差别:

kernel-3.10

kernel-3.10

>= kernel-3.18 && < kernel-4.6

>= kernel-3.18 && < kernel-4.6

>= kernel-4.6/N0.MP8 kernel-4.4(patch back)

>= kernel-4.6/N0.MP8 kernel-4.4(patch back)

ARM32bit kernel布局

这是一张示意图(有些地址可能会有差异)


ARM32bit kernel布局

整个地址空间是4G,kernel被配置为1G,程序占3G。

内核代码开始的地址是0xC0008000,前面放页表(起始地址为0xC0004000),如果支持模块(*.ko)那么地址在0xBF000000。

由于kernel没办法将所有内存都映射进来,毕竟kernel自己只占1G,如果RAM超过1G,就无法全部映射。怎么办呢?只能先映射一部分了,这部分叫low memory。其他的就按需映射,VMALLOC区域就是用于按需映射的。

ARM的外设寄存器和内存一样,都统一地址编码,因此0xF0000000以上的一段空间用于映射外设寄存器,便于操作硬件模块。

0xFFFF0000是特殊地址,CPU用于存放异常向量表,kernel异常绝大部分都是CPU异常(MMU发出的abort/undef inst.等异常)。

以上是粗略的说明,还需查看代码获取完整的分析信息(内核在不停演进,有些部分可能还会变化)

3.了解printk

kernel log

最初学编程时,大家一定用过printf(),在kernel里有对应的函数,叫printk()。

最简单的调试方法就是用printk()印出你想知道的信息了,而前面章节讲到oops/panic时,它们就通过printk()将寄存器信息/堆栈信息打印到kernel log buffer里。

了解kernel log对问题的调试将非常重要,这里有专门的课程介绍,请看:

可以看到kernel log可以通过串口输出,也可以在发生oops/panic后将buffer保存成文件打包到db里,然后拿到串口log或db对kernel进行调试分析了。

通常手机会保留串口测试点,但要抓串口log一般都要拆机,比较麻烦。前面讲到可以将kernel log保存成文件打包在db里,db是什么东西?

AEE db

db是叫AEE(Android Exception Engine,集成在Mediatek手机软件里)的模块检查到异常并收集异常信息生成的文件,里面包含调试所需的log等关键信息。db有点像飞机的黑匣子。

对于KE来说,db里包含了如下文件(db可以通过GAT工具解开,请参考附录里的FAQ):

以上这些文件一般足以调试KE了,除非一些特别的问题需要其他信息,比如串口log等等。

4.ram console

什么是ram console?

请参考:
MediaTek On-Line> Quick Start> Deep in MTK Turnkey Solution Logging Tools

系统重启时关键信息

ram console除了保持last kmsg外,还有重要的系统信息,这些非常有助于我们调试。这些信息保存在ram console的头部ram_console_buffer里。

ram console
这个结构体里的off_linux指向了struct last_reboot_reason,里面保存了重要的信息:
ram console
以上重要的信息在重启后将被打包到db里的SYS_REBOOT_REASON文件里。对这只文件的各个栏位解读请查看:
MediaTek On-Line> Quick Start> 深入分析看门狗框架> 分析方法> HW reboot> HW reboot调试信息

5.前期异常处理

CPU异常捕获

对于野指针、跑飞之类的异常会被MMU拦截并报告给CPU,这一系列都是硬件行为,具体请看:

这类问题比较难定位,也是占KE比例的大头,原因通常是内存被踩坏、指针use atfer free等多种因素,在当时可能不会立即出现异常,而是到使用这块内存才有可能崩溃。

分析问题的手段也是多样化,比如用watch point,MMU protect或加debug code等(请参考附录FAQ)

软件异常捕获

在kernel代码里,一般会通过BUG(),BUG_ON(),panic()来拦截超出预期的行为,这是软件主动回报异常的功能。

这些问题分析通常有固定的套路,请参考后面的:《实例篇: 案例分析》

在内核调用可以用来方便标记bug,提供断言并输出信息。最常用的两个是BUG()和BUG_ON()。当被调用的时候,它们会引发oops,导致栈的回溯和错误信息的打印。使用方式如下

if (condition)
BUG();
或者 :
BUG_ON(condition); //只是在BUG基础上多层封存而已:
#define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while(0)

32bit kernel:

BUG() 的实现采用了埋入未定义指令(0xE7F001F2,记住这个值,log里看到这个值,你就应该知道是调用了BUG()/BUG_ON()了)的方式


64bit kernel:

原生的kernel,BUG()是直接调用panic()的:

不过Mediatek修改了BUG()的实现,这样有更多的调试信息输出(die()有寄存器等信息输出)

MTK 修改

当你看到如下log时,就应该知道是BUG()/BUG_ON()引起的了!

[ 147.234926]<0>-(0)[122:kworker/u8:3]Unable to handle kernel paging request at virtual address 0000dead

6.die()流程

经过前面的流程,走到了die()函数,该函数主要输出便于调试的寄存器信息/堆栈信息等重要资料,我们通过log分析KE就是分析这些资料,因此要知道整个流程。die() => panic()的大致流程如下:


die()流程图

在学习这些流程时,建议结合代码和KE的log一起看,你就知道log里那些信息在代码哪处打印出来的了。

die()总流程

先从die()入手,看下die()总流程:


die()总流程

走到debug_locks_off()就有log输出了,如下:


debug_locks_off() log输出

如果这个异常是代码里调用BUG()/BUG_ON()引起,那么有额外log说明



输出的log大致如下:


log

__die()流程

绝大部分的关键信息是由__die()函数输出的,流程如下:


__die()流程

异常类型信息

开始印出异常类型等信息,看一份kernel log有没有oops,直接搜索关键字Internal error就可以了:


输出的信息大致如下:


log

module信息

接下来是module信息,不过我们不建议使用module,这边也不打算介绍了。

CPU寄存器信息

然后是重要的CPU寄存器信息(32bit的代码,64bit类同):


CPU信息

输出的信息大致如下:

log信息

寄存器附近的内存

有助于我们分析问题的内存信息,问题很可能就出在里面。

输出的信息大致如下:

调用栈

有时问题可以直接从调用栈看出来,由此可见调用栈是多么重要。

输出的信息大致如下:

PC附近指令

可以看到PC附近的指令:

输出的信息大致如下:

分析log

到这里die()函数就完成了它的使命,将重要信息输出来了。接下来你要如何调试呢?这个就看个人的功力了,你可以:

7.panic()流程

流程走到panic()就里死(异常重启)不远了,关键的信息已输出到kernel log。那么panic()做了什么呢?

panic()流程

panic()流程

panic()有标志性的log输出,大致如下:


kernel panic 异常

因此我们也可以通过搜索关键字Kernel panic查找是否有panic发生。

panic通知链

panic()会调用栈通知链上的回调函数同时感兴趣的模块,比如我们的aee注册了回调函数,用于保存kernel log/mini dump等关键信息,并将其保存到emmc的expdb分区,等等重启后将其回读并保存成KE db。

expdb

重启过程DRAM会丢失,因此信息只能保存在flash上了,在分区表里有一项就是expdb了:



流程大致如下(版本不停演进,可能有很大变化,仅供参考):



重启后,aee将回读aeedb分区资料并转化为KE db。

8.nested panic

有时die()/panic()流程不一定能正常走完,可能走到某一步又发生了异常,则就形成了嵌套,这种情况,我们一般不会关注后面的异常,而是关注最开始的那个异常。

为了避免异常嵌套,在发生第2次异常时,我们就拦截下来,我们在3个地方用于拦截nested panic:


拦截后不走die()/panic()流程,因为这些流程可能会再次发生异常,走我们写的函数aee_stop_nested_panic()函数:



在里面尽量少用kernel模块,很有可能也会发生异常,仅仅将寄存器等重要信息输出到ram console就等死(死循环等等看门狗复位!)。这时你抓回来的db里的SYS_LAST_KMSG就可以看到这些资料,大致如下(不同版本稍有区别):



里面包含了寄存器信息、堆栈信息和调用栈,我们就可以通过工具(addr2line)还原当时异常的位置。

不过nested panic能参考的信息很少,不像普通的KE那样丰富。

至此,本篇已结束,如有不对的地方,欢迎您的建议与指正。期待您的关注,
感谢您的阅读,谢谢!

​欢迎关注微信公众号:程序员Android
公众号ID:ProgramAndroid
获取更多信息

微信公众号:ProgramAndroid

我们不是牛逼的程序员,我们只是程序开发中的垫脚石。
我们不发送红包,我们只是红包的搬运工。

点击阅读原文,获取更多福利


上一篇下一篇

猜你喜欢

热点阅读