一、Kernel Exception(KE)概述
Android OS由3層組成,最底層是Kernel,上面是Native bin/lib,最上層是Java層:
Android OS 3層結(jié)構(gòu)
任何軟件都有可能發(fā)生異常,比如野指針,跑飛、死鎖等等。
當異常發(fā)生在kernel層,我們就叫它為KE(kernel exception),同理,發(fā)生在Native就是NE,Java層就是JE。這篇文章僅關(guān)注底層的KE。
1. KE類別
kernel有以下2種(oops、panic)
崩潰類別
-
oops (類似assert,有機會恢復)
oops是美國人比較常有的口語。就是有點意外,吃驚,或突然的意思。內(nèi)核行為表現(xiàn)為通知感興趣模塊,打印各種信息,如寄存器值,堆棧信息…
當出現(xiàn)oops時,我們就可以根據(jù)寄存器等信息調(diào)試并解決問題。/proc/sys/kernel/panic_on_oops
為1時導致panic。我們默認設(shè)置為1,即oops會發(fā)生panic。
-
Panic – 困惑,恐慌,它表示Linux kernel遇到了一個不知道該怎么繼續(xù)的情況。內(nèi)核行為表現(xiàn)為通知感興趣模塊,死機或者重啟。
在kernel代碼里,有些代碼加了錯誤檢查,發(fā)現(xiàn)錯誤可能直接調(diào)用了panic(),并輸出信息提供調(diào)試
-
panic
2. KE常用調(diào)試方法
凡是程序就有bug。bug總是出現(xiàn)在預料之外的地方。據(jù)說世界上第一個bug是繼電器式計算機中飛進一只蛾子,倒霉的飛蛾夾在繼電器之間導致了計算機故障。由于這個小蟲子,程序中的錯誤就被稱為了bug。
有Bug就需要Debug,而調(diào)試是一種很個性化的工作,十個人可能有十種調(diào)試方法。但從手段上來講,大致可分為兩類,在線調(diào)試 (Online Debug) 和離線調(diào)試 (Offline Debug).
3.在線調(diào)試
Online debug, 指的是在程序的運行過程中監(jiān)視程序的行為,分析是否符合預期。通常會借助一些工具,如GDB和Trace32等。有時候也會借助一些硬件設(shè)備的協(xié)助,如仿真器/JTAG,但是準備環(huán)境非常困難,而且用起來也很麻煩,除非一些runtime問題需要外很少使用。
4.離線調(diào)試,
Offline debug, 指的是在程序的運行中收集需要的信息,在Bug發(fā)生后根據(jù)收集到的信息來分析的一種手段。通常也分為兩種方式,一種是Logging,一種是Memory Dump。
Logging
日志或者相關(guān)信息的收集,可以比較清晰的看到代碼的執(zhí)行過程,對于邏輯問題是一種有效的分析手段,由于其簡單易操作,也是最為重要的一種分析手法。
Memory Dump
翻譯過來叫做內(nèi)存轉(zhuǎn)儲,指的是在異常發(fā)生的時刻將內(nèi)存信息全部轉(zhuǎn)儲到外部存儲器,即將異常現(xiàn)場信息備份下來以供事后分析。是針對CPU執(zhí)行異常的一種非常有效的分析手段。在Windows平臺,程序異常發(fā)生之后可以選擇啟動調(diào)試器來馬上調(diào)試。在Linux平臺,程序發(fā)生異常之后會轉(zhuǎn)儲core dump,而此coredump可以用調(diào)試器GDB來進行調(diào)試。而內(nèi)核的異常也可以進行類似的轉(zhuǎn)儲。
二、Kernel空間布局
在分析KE前,你要了解kernel內(nèi)存布局,才知道哪些地址用來做什么,可能會是什么問題。
在內(nèi)核空間中存在如下重要的段:
1. vmlinux代碼/數(shù)據(jù)段:
任何程序都有TEXT(可執(zhí)行代碼),RW(數(shù)據(jù)段),ZI段(未初始化數(shù)據(jù)段),kernel也有,對應的是.text,.data,.bss
2.module區(qū)域:
kernel可以支持ko(模塊),因此需要一段空間用于存儲代碼和數(shù)據(jù)段。
3. vmalloc區(qū)域:
kernel除了可以申請連續(xù)物理地址的內(nèi)存外,還可以申請不連續(xù)的內(nèi)存(虛擬地址是連續(xù)的),可以避免內(nèi)存碎片化而申請不到內(nèi)存。
4. io map區(qū)域:
留給io寄存器映射的區(qū)域,有些版本沒有io map區(qū)域而是直接用vmalloc區(qū)域了。
5.memmap:
kernel是通過page結(jié)構(gòu)體描述內(nèi)存的,每一個頁框都有對應的page結(jié)構(gòu)體,而memmap就是page結(jié)構(gòu)體數(shù)組。
還有其他段小的段沒有列出來,可能根據(jù)不同的版本而差別。
6. ARM64bit kernel布局
目前智能機已進入64bit,因此就存在32bit布局和64bit布局,下面一一講解。
ARM64可以使用多達48bit物理、虛擬地址(擴充成64bit,高位全為1或0)。對linux kernel來講,目前配置為39bit的kernel空間。
由于多達512GB的空間,因此完全可以將整個RAM映射進來,0xFFFFFFC000000000之后就是一一映射了,就無所謂high memory了。
vmalloc區(qū)域功能除了外設(shè)寄存器也直接映射到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。
內(nèi)核代碼開始的地址是0xC0008000,前面放頁表(起始地址為0xC0004000),如果支持模塊(*.ko)那么地址在0xBF000000。
由于kernel沒辦法將所有內(nèi)存都映射進來,畢竟kernel自己只占1G,如果RAM超過1G,就無法全部映射。怎么辦呢?只能先映射一部分了,這部分叫l(wèi)ow memory。其他的就按需映射,VMALLOC區(qū)域就是用于按需映射的。
ARM的外設(shè)寄存器和內(nèi)存一樣,都統(tǒng)一地址編碼,因此0xF0000000以上的一段空間用于映射外設(shè)寄存器,便于操作硬件模塊。
0xFFFF0000是特殊地址,CPU用于存放異常向量表,kernel異常絕大部分都是CPU異常(MMU發(fā)出的abort/undef inst.等異常)。
以上是粗略的說明,還需查看代碼獲取完整的分析信息(內(nèi)核在不停演進,有些部分可能還會變化)
三、printk 概述
1. kernel log
最初學編程時,大家一定用過printf(),在kernel里有對應的函數(shù),叫printk()。
最簡單的調(diào)試方法就是用printk()印出你想知道的信息了,而前面章節(jié)講到oops/panic時,它們就通過printk()將寄存器信息/堆棧信息打印到kernel log buffer里。
可以看到kernel log可以通過串口輸出,也可以在發(fā)生oops/panic后將buffer保存成文件打包到db里,然后拿到串口log或db對kernel進行調(diào)試分析了。
通常手機會保留串口測試點,但要抓串口log一般都要拆機,比較麻煩。前面講到可以將kernel log保存成文件打包在db里,db是什么東西?
四、AEE db log機制
db是叫AEE(Android Exception Engine,集成在Mediatek手機軟件里)的模塊檢查到異常并收集異常信息生成的文件,里面包含調(diào)試所需的log等關(guān)鍵信息。db有點像飛機的黑匣子。
對于KE來說,db里包含了如下文件(db可以通過GAT工具解開,請參考附錄里的FAQ):
-
__exp_main.txt:異常類型,調(diào)用棧等關(guān)鍵信息。
-
_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調(diào)試
-
SYS_REBOOT_REASON:重啟時的硬件記錄的信息。
-
SYS_VERSION_INFO:kernel版本,用于和vmlinux對比,只有匹配的vmlinux才能用于分析這個異常。
-
SYS_WDT_LOG:看門狗復位信息
以上這些文件一般足以調(diào)試KE了,除非一些特別的問題需要其他信息,比如串口log等等。
1. 系統(tǒng)重啟時關(guān)鍵信息
ram console除了保持last kmsg外,還有重要的系統(tǒng)信息,這些非常有助于我們調(diào)試。這些信息保存在ram console的頭部ram_console_buffer里。
ram console
這個結(jié)構(gòu)體里的off_linux指向了struct last_reboot_reason,里面保存了重要的信息:
ram console
以上重要的信息在重啟后將被打包到db里的SYS_REBOOT_REASON文件里。對這只文件的各個欄位解讀請查看:
五、前期異常處理
1.CPU異常捕獲
對于野指針、跑飛之類的異常會被MMU攔截并報告給CPU,這一系列都是硬件行為。
這類問題比較難定位,也是占KE比例的大頭,原因通常是內(nèi)存被踩壞、指針use atfer free等多種因素,在當時可能不會立即出現(xiàn)異常,而是到使用這塊內(nèi)存才有可能崩潰。
2.軟件異常捕獲
在kernel代碼里,一般會通過BUG(),BUG_ON(),panic()來攔截超出預期的行為,這是軟件主動回報異常的功能。
在內(nèi)核調(diào)用可以用來方便標記bug,提供斷言并輸出信息。最常用的兩個是BUG()和BUG_ON()。當被調(diào)用的時候,它們會引發(fā)oops,導致棧的回溯和錯誤信息的打印。使用方式如下
if (condition)
BUG();
或者 :
BUG_ON(condition); //只是在BUG基礎(chǔ)上多層封存而已:
` #define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while(0)`
3. 32bit kernel:
BUG() 的實現(xiàn)采用了埋入未定義指令(0xE7F001F2,記住這個值,log里看到這個值,你就應該知道是調(diào)用了BUG()/BUG_ON()了)的方式
64bit kernel:
原生的kernel,BUG()是直接調(diào)用panic()的:
不過Mediatek修改了BUG()的實現(xiàn),這樣有更多的調(diào)試信息輸出(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()流程
經(jīng)過前面的流程,走到了die()函數(shù),該函數(shù)主要輸出便于調(diào)試的寄存器信息/堆棧信息等重要資料,我們通過log分析KE就是分析這些資料,因此要知道整個流程。die() => panic()的大致流程如下:
die()流程圖
在學習這些流程時,建議結(jié)合代碼和KE的log一起看,你就知道log里那些信息在代碼哪處打印出來的了。
1.die()總流程
先從die()入手,看下die()總流程:
die()總流程
走到debug_locks_off()就有l(wèi)og輸出了,如下:
debug_locks_off() log輸出
如果這個異常是代碼里調(diào)用BUG()/BUG_ON()引起,那么有額外log說明
輸出的log大致如下:
log
2. __die()流程
絕大部分的關(guān)鍵信息是由__die()函數(shù)輸出的,流程如下:
__die()流程
異常類型信息
開始印出異常類型等信息,看一份kernel log有沒有oops,直接搜索關(guān)鍵字Internal error就可以了:
輸出的信息大致如下:
log
3. module信息
接下來是module信息,不過我們不建議使用module,這邊也不打算介紹了。
4.CPU寄存器信息
然后是重要的CPU寄存器信息(32bit的代碼,64bit類同):
CPU信息
輸出的信息大致如下:
log信息
5.寄存器附近的內(nèi)存
有助于我們分析問題的內(nèi)存信息,問題很可能就出在里面。
輸出的信息大致如下:
6. 調(diào)用棧
有時問題可以直接從調(diào)用棧看出來,由此可見調(diào)用棧是多么重要。
輸出的信息大致如下:
7.PC附近指令
可以看到PC附近的指令:
輸出的信息大致如下:
8.分析log
到這里die()函數(shù)就完成了它的使命,將重要信息輸出來了。接下來你要如何調(diào)試呢?這個就看個人的功力了,你可以:
-
通過PC指向的函數(shù),用addr2line(后面的GNU tools有介紹)定位到哪只文件的哪一行,大致可以知道發(fā)生了什么,如果無法一下子定位,也可以通過結(jié)合printk()多次觀察KE時的log排查。如果是由BUG()/BUG_ON()引起的KE,則就可以著手修復問題了。
-
查看調(diào)用棧,有些時候調(diào)用棧可以說明流程,看看代碼是否有按預期跑,如果沒有,可以結(jié)合printk()定位問題。
-
如果你想看函數(shù)參數(shù)或全局變量信息,那么你需要用《進階篇: ramdump分析》的知識調(diào)試了。
七、panic()流程
流程走到panic()就里死(異常重啟)不遠了,關(guān)鍵的信息已輸出到kernel log。那么panic()做了什么呢?
1. panic()流程
panic()流程
panic()有標志性的log輸出,大致如下:
kernel panic 異常
因此我們也可以通過搜索關(guān)鍵字Kernel panic查找是否有panic發(fā)生。
2. panic通知鏈
panic()會調(diào)用棧通知鏈上的回調(diào)函數(shù)同時感興趣的模塊,比如我們的aee注冊了回調(diào)函數(shù),用于保存kernel log/mini dump等關(guān)鍵信息,并將其保存到emmc的expdb分區(qū),等等重啟后將其回讀并保存成KE db。
3. expdb
重啟過程DRAM會丟失,因此信息只能保存在flash上了,在分區(qū)表里有一項就是expdb了:
流程大致如下(版本不停演進,可能有很大變化,僅供參考):
重啟后,aee將回讀aeedb分區(qū)資料并轉(zhuǎn)化為KE db。
八、nested panic
有時die()/panic()流程不一定能正常走完,可能走到某一步又發(fā)生了異常,則就形成了嵌套,這種情況,我們一般不會關(guān)注后面的異常,而是關(guān)注最開始的那個異常。
為了避免異常嵌套,在發(fā)生第2次異常時,我們就攔截下來,我們在3個地方用于攔截nested panic:
-
do_PrefetchAbort()
-
do_DataAbort()
-
do_undefinstr()
攔截后不走die()/panic()流程,因為這些流程可能會再次發(fā)生異常,走我們寫的函數(shù)aee_stop_nested_panic()函數(shù):
在里面盡量少用kernel模塊,很有可能也會發(fā)生異常,僅僅將寄存器等重要信息輸出到ram console就等死(死循環(huán)等等看門狗復位!)。這時你抓回來的db里的SYS_LAST_KMSG就可以看到這些資料,大致如下(不同版本稍有區(qū)別):
里面包含了寄存器信息、堆棧信息和調(diào)用棧,我們就可以通過工具(addr2line)還原當時異常的位置。
不過nested panic能參考的信息很少,不像普通的KE那樣豐富。
-
寄存器
+關(guān)注
關(guān)注
31文章
5336瀏覽量
120231 -
存儲器
+關(guān)注
關(guān)注
38文章
7484瀏覽量
163765 -
JAVA
+關(guān)注
關(guān)注
19文章
2966瀏覽量
104702
原文標題:八、nested panic
文章出處:【微信號:哆啦安全,微信公眾號:哆啦安全】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論