深刻分析Linux Kernel Exception 框架(基礎篇)

極力推薦Android 開發大總結文章:歡迎收藏 程序員Android 力薦 ,Android 開發者須要的必備技能 java

Kernel Exception 在工做中偶爾會遇到,發生此異常時候常常會致使手機死機或重啓。那麼如何解決這些問題,本篇文章將帶你從KE小白變小牛。linux

注: 本文部份內容參考MTK,若有侵權,請告知,立馬關閉此文,中止侵權。知識共享,感謝支持!android

本篇文章主要介紹 Android 開發中的部分知識點,經過閱讀本篇文章,您將收穫如下內容:程序員

基礎篇: 經過log分析KE 1.Kernel Exception 2.kernel空間佈局 3.瞭解printk 4.ram console 5.前期異常處理 6.die()流程 7.panic()流程 8.nested panic編程

基礎篇: 經過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,有機會恢復)

  • oops是美國人比較常有的口語。就是有點意外,吃驚,或忽然的意思。內核行爲表現爲通知感興趣模塊,打印各類信息,如寄存器值,堆棧信息…
  • 當出現oops時,咱們就能夠根據寄存器等信息調試並解決問題。
  • /proc/sys/kernel/panic_on_oops爲1時致使panic。咱們默認設置爲1,即oops會發生panic。

2. panic

  • Panic – 困惑,恐慌,它表示Linux kernel遇到了一個不知道該怎麼繼續的狀況。內核行爲表現爲通知感興趣模塊,死機或者重啓。
  • 在kernel代碼裏,有些代碼加了錯誤檢查,發現錯誤可能直接調用了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內存佈局,才知道哪些地址用來作什麼,可能會是什麼問題。

在內核空間中存在以下重要的段:

  • vmlinux代碼/數據段: 任何程序都有TEXT(可執行代碼),RW(數據段),ZI段(未初始化數據段),kernel也有,對應的是.text,.data,.bss

  • module區域: kernel能夠支持ko(模塊),所以須要一段空間用於存儲代碼和數據段。

  • vmalloc區域: kernel除了能夠申請連續物理地址的內存外,還能夠申請不連續的內存(虛擬地址是連續的),能夠避免內存碎片化而申請不到內存。

  • io map區域: 留給io寄存器映射的區域,有些版本沒有io map區域而是直接用vmalloc區域了。

  • memmap: kernel是經過page結構體描述內存的,每個頁框都有對應的page結構體,而memmap就是page結構體數組。

還有其餘段小的段沒有列出來,可能根據不一樣的版本而差異。

目前智能機已進入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對問題的調試將很是重要,這裏有專門的課程介紹,請看:

  • MediaTek On-Line> Quick Start)> Deep in MTK Turnkey Solution Logging Tools> 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):

  • __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等等。

##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,這一系列都是硬件行爲,具體請看:

  • MediaTek On-LineQuick Start> 深刻分析Android native exception框架> 流程-異常處理
  • 在上面章節裏的內核異常處理流程,有一處不一樣,走到arm_notify_die()後,判斷是kernel mode就直接調用die()了,而不是force_sig_info()

這類問題比較難定位,也是佔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()函數就完成了它的使命,將重要信息輸出來了。接下來你要如何調試呢?這個就看我的的功力了,你能夠:

  • 經過PC指向的函數,用addr2line(後面的GNU tools有介紹)定位到哪隻文件的哪一行,大體能夠知道發生了什麼,若是沒法一會兒定位,也能夠經過結合printk()屢次觀察KE時的log排查。若是是由BUG()/BUG_ON()引發的KE,則就能夠着手修復問題了。
  • 查看調用棧,有些時候調用棧能夠說明流程,看看代碼是否有按預期跑,若是沒有,能夠結合printk()定位問題。
  • 若是你想看函數參數或全局變量信息,那麼你須要用《進階篇: ramdump分析》的知識調試了。

##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:

  • do_PrefetchAbort()
  • do_DataAbort()
  • do_undefinstr()

攔截後不走die()/panic()流程,由於這些流程可能會再次發生異常,走咱們寫的函數aee_stop_nested_panic()函數:
在裏面儘可能少用kernel模塊,頗有可能也會發生異常,僅僅將寄存器等重要信息輸出到ram console就等死(死循環等等看門狗復位!)。這時你抓回來的db裏的SYS_LAST_KMSG就能夠看到這些資料,大體以下(不一樣版本稍有區別):
裏面包含了寄存器信息、堆棧信息和調用棧,咱們就能夠經過工具(addr2line)還原當時異常的位置。

不過nested panic能參考的信息不多,不像普通的KE那樣豐富。

至此,本篇已結束,若有不對的地方,歡迎您的建議與指正。同時期待您的關注,感謝您的閱讀,謝謝!

若有侵權,請聯繫小編,小編對此深感抱歉,屆時小編會刪除文章,當即中止侵權行爲,請您多多包涵。

既然都看到這裏,領兩個紅包在走吧! 如下兩個紅包天天均可以領取

1.支付寶搜索 522398497,或掃碼支付寶紅包海報。

支付寶掃一掃,天天領取大紅包

2.微信紅包,微信掃一掃便可領取紅包

微信掃一掃,天天領取微信紅包
相關文章
相關標籤/搜索