Kernel Exception 问题分析流程

3a5f4ccb0ef321e267511e7f7b1fb3da.gif

和你一起终身学习,这里是程序员Android

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

一、Kernel Exception概述
二、Kernel空间布局
三、printk 概述
四、AEE db log机制
五、前期异常处理
六、die()流程
七、panic()流程
八、nested panic
九、参考文献

一、Kernel Exception(KE)概述

Android OS由3层组成,最底层是Kernel,上面是Native bin/lib,最上层是Java层:

f4c8e4599a109116ede044d465cc0f93.jpeg
Android OS 3层结构

任何软件都有可能发生异常,比如野指针,跑飞、死锁等等。
当异常发生在kernel层,我们就叫它为KE(kernel exception),同理,发生在Native就是NE,Java层就是JE。这篇文章仅关注底层的KE。

1. KE类别

kernel有以下2种(oops、panic)崩溃类别

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

oops是美国人比较常有的口语。就是有点意外,吃惊,或突然的意思。内核行为表现为通知感兴趣模块,打印各种信息,如寄存器值,堆栈信息…
当出现oops时,我们就可以根据寄存器等信息调试并解决问题。
/proc/sys/kernel/panic_on_oops为1时导致panic。我们默认设置为1,即oops会发生panic。

    1. panic

    Panic – 困惑,恐慌,它表示Linux kernel遇到了一个不知道该怎么继续的情况。内核行为表现为通知感兴趣模块,死机或者重启。
    在kernel代码里,有些代码加了错误检查,发现错误可能直接调用了panic(),并输出信息提供调试

2. KE常用调试方法

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

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

3.在线调试

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

4.离线调试,

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

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

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

二、Kernel空间布局

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

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

1. vmlinux代码/数据段:

任何程序都有TEXT(可执行代码),RW(数据段),ZI段(未初始化数据段),kernel也有,对应的是.text,.data,.bss

2.module区域:

kernel可以支持ko(模块),因此需要一段空间用于存储代码和数据段。

3. vmalloc区域:

kernel除了可以申请连续物理地址的内存外,还可以申请不连续的内存(虚拟地址是连续的),可以避免内存碎片化而申请不到内存。

4. io map区域:

留给io寄存器映射的区域,有些版本没有io map区域而是直接用vmalloc区域了。

5.memmap:

kernel是通过page结构体描述内存的,每一个页框都有对应的page结构体,而memmap就是page结构体数组。

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

6. ARM64bit kernel布局

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

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

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

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

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

  • kernel-3.10
168e3ca430dda8235777dced1ffb6c36.jpeg
kernel-3.10
  • = kernel-3.18 && < kernel-4.6

66825a120bd2952953f3feb93c6ff85c.jpeg
>= kernel-3.18 && < kernel-4.6
  • = kernel-4.6/N0.MP8 kernel-4.4(patch back)

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

7. ARM32bit kernel布局

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

366cd07ed468bd1da4a9cc0712cbe589.jpeg
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.等异常)。

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

三、printk 概述

1. kernel log

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

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

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

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

四、AEE db log机制

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

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

  • __exp_main.txt:异常类型,调用栈等关键信息。
  • _exp_detail.txt:详细异常信息
  • SYS_ANDROID_LOG:android main log
  • SYS_KERNEL_LOG:kernel log
  • SYS_LAST_KMSG:上次重启前的kernel log
  • SYS_MINI_RDUMP:类似coredump,可以用gdb/trace32调试
  • SYS_REBOOT_REASON:重启时的硬件记录的信息。
  • SYS_VERSION_INFO:kernel版本,用于和vmlinux对比,只有匹配的vmlinux才能用于分析这个异常。
  • SYS_WDT_LOG:看门狗复位信息

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

1. 系统重启时关键信息

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

6cb939668d2cb014006d2c8e95a2af8d.jpeg
ram console

这个结构体里的off_linux指向了struct last_reboot_reason,里面保存了重要的信息:

f644ed2eab1feed0c98730bac42e34f9.jpeg
ram console

以上重要的信息在重启后将被打包到db里的SYS_REBOOT_REASON文件里。对这只文件的各个栏位解读请查看:

五、前期异常处理

1.CPU异常捕获

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

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

2.软件异常捕获

在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)`

3. 32bit kernel:

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

eb6cebc9efa5ce3efe9db40b9e7d1d0b.jpeg

64bit kernel:

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

8f9e1e3b4717fd2302f585e87afc0e53.jpeg

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

2d3fe82549b0b0b5912de31ac7838ed2.jpeg
MTK 修改

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

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

六、die()流程

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

666c479dcc12d4464d6534054980973b.jpeg
die()流程图

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

1.die()总流程

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

129a63ad13bbb7d4b08f834af8386133.jpeg
die()总流程

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

e688d820424c2d043f7636b5d2155607.jpeg
debug_locks_off() log输出

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

c88af1abf20a71a751933015e3343b85.jpeg

输出的log大致如下:

6c863a17ba1d7ab87bcd8a802bbd5b51.jpeg
log

2. __die()流程

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

26377031f608c16ccbc65c1aa4a6074c.jpeg
__die()流程

异常类型信息

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

2399d21855852d635b09f3a6e33e0d2f.jpeg

输出的信息大致如下:

ffcd7c69cd622b6e6ea29cffff629ab5.jpeg
log

3. module信息

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

4.CPU寄存器信息

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

e62f2df259c2a2af7d9d4420630842e1.jpeg
CPU信息

输出的信息大致如下:

b18c8063692823622c7b47e67f017675.jpeg
log信息

5.寄存器附近的内存

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

acccbd64b359c972a288acea0a623b51.jpeg

输出的信息大致如下:

2865fab8aa6585cd4041b6dc4c7e8761.jpeg

6. 调用栈

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

d895a358ffd832183c5b0aef63275001.jpeg

输出的信息大致如下:

b91835a6d72d0cff12bfd124882a54e6.jpeg

7.PC附近指令

可以看到PC附近的指令:

22b329d57fbcba962bf3e6d5641a27c7.jpeg

输出的信息大致如下:

a0d33ce34567f5d255bb2e9cb1d05861.jpeg

8.分析log

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

  • 通过PC指向的函数,用addr2line(后面的GNU tools有介绍)定位到哪只文件的哪一行,大致可以知道发生了什么,如果无法一下子定位,也可以通过结合printk()多次观察KE时的log排查。如果是由BUG()/BUG_ON()引起的KE,则就可以着手修复问题了。
  • 查看调用栈,有些时候调用栈可以说明流程,看看代码是否有按预期跑,如果没有,可以结合printk()定位问题。
  • 如果你想看函数参数或全局变量信息,那么你需要用《进阶篇: ramdump分析》的知识调试了。

七、panic()流程

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

1. panic()流程

f78c63884cba34268eacd939022f323e.jpeg
panic()流程

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

6ebedc9d72237d3a290dcfc57f71c17f.jpeg
kernel panic 异常

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

2. panic通知链

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

3. expdb

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

4a87446f4f0ff6fc68c3e47d629662cd.jpeg

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

7294395708f66ae58919ce24cc59a98b.jpeg

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

八、nested panic

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

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

  • do_PrefetchAbort()
  • do_DataAbort()
  • do_undefinstr()
5934190f923896cbcb8a3ded3cc8ead7.jpeg

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

50b9ef7845ce3a8bf1258e241cf0966e.jpeg

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

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

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

九、参考文献

【腾讯文档】Android Framework 知识库
https://docs.qq.com/doc/DSXBmSG9VbEROUXF5

至此,本篇已结束。转载网络的文章,小编觉得很优秀,欢迎点击阅读原文,支持原创作者,如有侵权,恳请联系小编删除,欢迎您的建议与指正。同时期待您的关注,感谢您的阅读,谢谢!

d1adfb5981fa52e5c96f3dc60f7b5f92.jpeg

点个在看,为大佬点赞!

<think>好的,我现在需要处理用户的问题,关于内核崩溃(kernel panic)导致系统不同步以及出现同步外部中止致命异常的原因和解决方法。用户给出的错误信息是“kernel panic not syncing synchronous external abort fatal exception”,需要针对这个错误提供解决方案,并涉及Linux内核的错误处理。 首先,我需要理解这个错误的具体含义。根据用户提供的引用[3],hard panic可能是由驱动模块的中断处理程序访问空指针引起的。同步外部中止(synchronous external abort)通常与内存访问错误有关,比如访问了无效的物理地址或未对齐的内存地址。这种情况下,CPU会触发一个异常,导致内核无法继续执行,进而引发panic。 接下来,用户需要了解原因和解决方法。我应该分步骤来组织回答,先解释错误的原因,再提供排查方法和解决方案。根据引用[3],驱动模块的问题尤其是中断处理程序中的空指针访问是一个常见原因。此外,内存管理错误、硬件故障(如内存损坏)或内核配置问题也可能导致此类错误。 在排查方法方面,需要引导用户检查内核日志,使用调试工具如objdump或addr2line定位问题代码。另外,检查硬件连接和内存测试也是必要的步骤。解决方法包括更新驱动和内核补丁、修复驱动中的空指针或内存错误、调整内核配置以启用更多调试选项,以及检查硬件健康状况。 此外,用户提到了系统不同步(not syncing),这可能是因为内核在遇到无法恢复的错误时,主动停止同步文件系统以防止数据损坏,这也是panic的一部分处理流程。 需要确保回答结构清晰,符合用户要求的系统级指令,比如正确使用LaTeX格式,生成相关问题,并添加引用标识。同时,回答要使用中文,保持技术准确性,并引用提供的资料中的相关信息,特别是引用[3]中提到的中断处理程序的问题。 最后,生成的相关问题需要围绕内核崩溃的其他原因、同步外部中止的具体情况、调试工具使用以及预防措施展开,确保问题具有针对性和扩展性,帮助用户进一步理解和解决问题。</think>### 内核崩溃(Kernel Panic)导致“同步外部中止”异常的原因与解决方法 #### 1. **错误原因分析** - **同步外部中止(Synchronous External Abort)** 通常由以下原因引发: 1. **内存访问错误**:例如访问未映射的物理地址、未对齐的内存地址或权限错误[^3]。 2. **驱动中断处理问题**:驱动模块的中断处理程序(Interrupt Handler)中可能访问了空指针或无效内存地址。 3. **硬件故障**:内存损坏、总线错误或外设通信异常(如指纹模块等)[^2]。 4. **内核配置缺陷**:未正确处理内存屏障(Memory Barrier)或中断栈配置不当(如ARM64中断栈未独立分配)[^1]。 #### 2. **排查方法** 1. **分析内核日志** 通过日志定位触发异常的指令地址和调用栈。例如: ```bash dmesg | grep "Unable to handle kernel" ``` 若日志包含类似 `PC is at [函数名+偏移量]`,可通过 `addr2line` 工具定位代码位置: ```bash addr2line -e vmlinux [地址] ``` 2. **检查中断栈配置(ARM64)** 确认内核启用了独立中断栈(避免线程内核栈溢出导致数据损坏): ```bash # 查看内核配置 grep CONFIG_IRQ_STACKS /boot/config-$(uname -r) ``` 3. **驱动模块调试** 若问题与特定硬件(如指纹模块)相关: - 卸载相关驱动,观察是否复现问题。 - 使用 `objdump` 反汇编驱动模块,检查中断处理函数逻辑: ```bash objdump -d [驱动模块].ko | less ``` 4. **硬件检测** 运行内存测试工具(如 `memtest86+`)排除物理内存故障。 --- #### 3. **解决方法** - **代码修复** - **驱动中断处理**:确保中断处理函数中指针有效性验证,例如: ```c if (likely(dev->data != NULL)) { // 操作数据 } else { pr_err("Invalid data pointer\n"); return IRQ_HANDLED; } ``` - **内存访问**:使用 `ioremap()` 正确映射物理地址,避免直接访问未对齐地址。 - **内核配置优化** - 启用独立中断栈(ARM64需内核版本≥5.10): ```bash CONFIG_IRQ_STACKS=y ``` - 增加调试选项(如 `CONFIG_DEBUG_KERNEL` 和 `CONFIG_KASAN`)捕捉内存越界。 - **硬件兼容性** - 更新固件(如指纹模块固件)。 - 检查外设与主板连接稳定性。 --- #### 4. **错误处理机制** Linux内核在触发致命错误时: 1. 打印异常信息(如寄存器状态、调用栈)。 2. **停止同步文件系统**(`kernel panic not syncing`)以防止数据损坏。 3. 根据配置执行重启或进入调试模式(如ramdump)。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员Android

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值