一、Kernel Exception(KE)概述
Android OS由3層組成,最底層是Kernel,上面是Native bin/lib,最上層是Java層:
Android OS 3層結構
任何軟件都有可能發生異常,比如野指針,跑飛、死鎖等等。
當異常發生在kernel層,我們就叫它為KE(kernel exception),同理,發生在Native就是NE,Java層就是JE。這篇文章僅關注底層的KE。
1. KE類別
kernel有以下2種(oops、panic)
崩潰類別
-
oops (類似assert,有機會恢復)
oops是美國人比較常有的口語。就是有點意外,吃驚,或突然的意思。內核行為表現為通知感興趣模塊,打印各種信息,如寄存器值,堆棧信息…
當出現oops時,我們就可以根據寄存器等信息調試并解決問題。/proc/sys/kernel/panic_on_oops
為1時導致panic。我們默認設置為1,即oops會發生panic。
-
Panic – 困惑,恐慌,它表示Linux kernel遇到了一個不知道該怎么繼續的情況。內核行為表現為通知感興趣模塊,死機或者重啟。
在kernel代碼里,有些代碼加了錯誤檢查,發現錯誤可能直接調用了panic(),并輸出信息提供調試
-
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
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)
7. 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.等異常)。
以上是粗略的說明,還需查看代碼獲取完整的分析信息(內核在不停演進,有些部分可能還會變化)
三、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里。
ram console
這個結構體里的off_linux指向了struct last_reboot_reason,里面保存了重要的信息:
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()了)的方式
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
六、die()流程
經過前面的流程,走到了die()函數,該函數主要輸出便于調試的寄存器信息/堆棧信息等重要資料,我們通過log分析KE就是分析這些資料,因此要知道整個流程。die() => panic()的大致流程如下:
die()流程圖
在學習這些流程時,建議結合代碼和KE的log一起看,你就知道log里那些信息在代碼哪處打印出來的了。
1.die()總流程
先從die()入手,看下die()總流程:
die()總流程
走到debug_locks_off()就有log輸出了,如下:
debug_locks_off() log輸出
如果這個異常是代碼里調用BUG()/BUG_ON()引起,那么有額外log說明
輸出的log大致如下:
log
2. __die()流程
絕大部分的關鍵信息是由__die()函數輸出的,流程如下:
__die()流程
異常類型信息
開始印出異常類型等信息,看一份kernel log有沒有oops,直接搜索關鍵字Internal error就可以了:
輸出的信息大致如下:
log
3. module信息
接下來是module信息,不過我們不建議使用module,這邊也不打算介紹了。
4.CPU寄存器信息
然后是重要的CPU寄存器信息(32bit的代碼,64bit類同):
CPU信息
輸出的信息大致如下:
log信息
5.寄存器附近的內存
有助于我們分析問題的內存信息,問題很可能就出在里面。
輸出的信息大致如下:
6. 調用棧
有時問題可以直接從調用棧看出來,由此可見調用棧是多么重要。
輸出的信息大致如下:
7.PC附近指令
可以看到PC附近的指令:
輸出的信息大致如下:
8.分析log
到這里die()函數就完成了它的使命,將重要信息輸出來了。接下來你要如何調試呢?這個就看個人的功力了,你可以:
-
通過PC指向的函數,用addr2line(后面的GNU tools有介紹)定位到哪只文件的哪一行,大致可以知道發生了什么,如果無法一下子定位,也可以通過結合printk()多次觀察KE時的log排查。如果是由BUG()/BUG_ON()引起的KE,則就可以著手修復問題了。
-
查看調用棧,有些時候調用??梢哉f明流程,看看代碼是否有按預期跑,如果沒有,可以結合printk()定位問題。
-
如果你想看函數參數或全局變量信息,那么你需要用《進階篇: ramdump分析》的知識調試了。
七、panic()流程
流程走到panic()就里死(異常重啟)不遠了,關鍵的信息已輸出到kernel log。那么panic()做了什么呢?
1. panic()流程
panic()流程
panic()有標志性的log輸出,大致如下:
kernel panic 異常
因此我們也可以通過搜索關鍵字Kernel panic查找是否有panic發生。
2. panic通知鏈
panic()會調用棧通知鏈上的回調函數同時感興趣的模塊,比如我們的aee注冊了回調函數,用于保存kernel log/mini dump等關鍵信息,并將其保存到emmc的expdb分區,等等重啟后將其回讀并保存成KE db。
3. expdb
重啟過程DRAM會丟失,因此信息只能保存在flash上了,在分區表里有一項就是expdb了:
流程大致如下(版本不停演進,可能有很大變化,僅供參考):
重啟后,aee將回讀aeedb分區資料并轉化為KE db。
八、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那樣豐富。
-
寄存器
+關注
關注
31文章
5421瀏覽量
123299 -
存儲器
+關注
關注
38文章
7632瀏覽量
166380 -
JAVA
+關注
關注
20文章
2984瀏覽量
106841
原文標題:八、nested panic
文章出處:【微信號:哆啦安全,微信公眾號:哆啦安全】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
Linux中Kernel的運行原理概述
Real Time Kernel概述
Linux Kernel Panic的產生的原因?
Java中包、接口與異常處理(exception) 實驗
ESP8266的重啟原因及常見的Fatal Exception的原因的說明

如何自定義Exception Hooks
kernel panic流程分析

kernel日志寫入logd介紹

評論