分析Linux kernel exception-基礎篇
轉載自MTKFAQ:
KE概念
Android OS由3層組成,最底層是kernel,上面是native bin/lib,最上層是java層:
任何軟體都有可能發生異常,比如野指標,跑飛、死鎖等等。
異常發生在kernel層,我們就叫它為KE(kernel exception),同理,發生在native就是NE,java層就是JE。這篇文章僅關注底層的KE。
KE類別
kernel有2中崩潰類別,
oops (類似assert,有機會恢復)
- oops是美國人比較常有的口語。就是有點意外,吃驚,或突然的意思。核心行為表現為通知感興趣模組,列印各種資訊,如暫存器值,堆疊資訊…
- 當出現oops時,我們就可以根據暫存器等資訊除錯並解決問題。
- /proc/sys/kernel/panic_on_oops為1時導致panic。我們預設設定為1,即oops會發生panic。
panic
- Panic – 困惑,恐慌,它表示Linux kernel遇到了一個不知道該怎麼繼續的情況。核心行為表現為通知感興趣模組,宕機或者重啟。
- 在kernel程式碼裡,有些程式碼加了錯誤檢查,發現錯誤可能直接呼叫了panic(),並輸出資訊提供除錯。
其實不管分類幾種,都表示kernel出現故障,需要修復。那如何除錯呢?就要看在發生異常時留了哪些資訊幫我們定位問題了。
常用除錯方法
凡是程式就有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
下面我們由淺入深剖析各種除錯方法,先從logging開始吧。
kernel space
在分析KE前,你要了解kernel記憶體佈局,才知道哪些地址用來做什麼,可能會是什麼問題。目前智慧機已進入64bit,因此就存在32bit佈局和64bit佈局,下面一一講解。
ARM32bit kernel佈局
這是一張示意圖(有些地址可能會有差異)
整個地址空間是4G,kernel被配置為1G,程式佔3G。
任何程式都有TEXT(可執行程式碼),RW(資料段),ZI段(未初始化資料段),kernel也有,對應的是.text,.data,.bss。而核心程式碼開始的地址是0xC0008000,前面放頁表(起始地址為0xC0004000),如果支援模組(*.ko)那麼地址在0xBF000000。
由於kernel沒辦法將所有記憶體都對映進來,畢竟kernel自己只佔1G,如果RAM超過1G,就無法全部對映。怎麼辦呢?只能先對映一部分了,這部分叫low memory。其他的就按需對映,VMALLOC區域就是用於按需對映的。
ARM的外設暫存器和記憶體一樣,都統一地址編碼,因此0xF0000000以上的一段空間用於對映外設暫存器,便於操作硬體模組。
0xFFFF0000是特殊地址,CPU用於存放異常向量表,kernel異常絕大部分都是CPU異常(MMU發出的abort/undef inst.等異常)。
ARM64bit kernel佈局
這是39bit的kernel空間,由於多達512GB的空間,因此完全可以將整個RAM對映進來,0xFFFFFFC000000000之後就是一一映射了,就無所謂high memory了。
vmalloc還是存在,因為可以將不連續的實體記憶體拼接成連續的虛擬記憶體,可以解決部分記憶體碎片問題。而且外設暫存器也直接對映到vmalloc了,就沒有32bit佈局裡的IO map space了。
modules對應的就是*.ko核心模組了。
以上是粗略的說明,還需檢視程式碼獲取完整的分析資訊(核心在不停演進,有些部分可能還會變化)。
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
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等等。
什麼是ram console?
系統重啟時關鍵資訊
ram console除了保持last kmsg外,還有重要的系統資訊,這些非常有助於我們除錯。這些資訊儲存在ram console的頭部ram_console_buffer裡。
struct ram_console_buffer
{
uint32_t sig;
/* for size comptible */
uint32_t off_pl;
uint32_t off_lpl; /* last preloader: struct reboot_reason_pl*/
uint32_t sz_pl;
uint32_t off_lk;
uint32_t off_llk; /* last lk: struct reboot_reason_lk */
uint32_t sz_lk;
uint32_t padding[3];
uint32_t sz_buffer;
uint32_t off_linux; /* struct last_reboot_reason */
uint32_t off_console;
/* console buffer*/
uint32_t log_start;
uint32_t log_size;
uint32_t sz_console;
};
這個結構體裡的off_linux指向了struct last_reboot_reason,裡面儲存了重要的資訊:
struct last_reboot_reason
{
uint32_t fiq_step;
uint32_t exp_type; /* 0xaeedeadX: X=1 (HWT), X=2 (KE), X=3 (nested panic) */
uint32_t reboot_mode;
uint32_t last_irq_enter[NR_CPUS];
uint64_t jiffies_last_irq_enter[NR_CPUS];
uint32_t last_irq_exit[NR_CPUS];
uint64_t jiffies_last_irq_exit[NR_CPUS];
uint64_t jiffies_last_sched[NR_CPUS];
char last_sched_comm[NR_CPUS][TASK_COMM_LEN];
uint8_t hotplug_data1[NR_CPUS], uint8_t hotplug_data2;
uint64_t hotplug_data3;
uint32_t mcdi_wfi, mcdi_r15, deepidle_data, sodi_data, spm_suspend_data;
uint64_t cpu_dormant[NR_CPUS];
uint32_t clk_data[8], suspend_debug_flag;
uint8_t cpu_dvfs_vproc_big, cpu_dvfs_vproc_little, cpu_dvfs_oppidx, cpu_dvfs_status;
uint8_t gpu_dvfs_vgpu, gpu_dvfs_oppidx, gpu_dvfs_status;
uint64_t ptp_cpu_big_volt, ptp_cpu_little_volt, ptp_gpu_volt, ptp_temp;
uint8_t ptp_status;
uint8_t thermal_temp1, thermal_temp2, thermal_temp3, thermal_temp4, thermal_temp5;
uint8_t thermal_status;
void *kparams;
};
以上重要的資訊在重啟後將被打包到db裡的SYS_REBOOT_REASON檔案裡。對這隻檔案的各個欄位解讀請檢視:
- HW reboot除錯資訊
什麼是Crash?
當linux系統核心發生崩潰的時候,可以通過KEXEC+KDUMP等方式收集核心崩潰之前的記憶體,生成一個轉儲檔案vmcore。核心開發者通過分析該vmcore檔案就可以診斷出核心崩潰的原因,從而進行作業系統的程式碼改進。那麼Crash就是一個被廣泛使用的核心崩潰轉儲檔案分析工具。
前面講過gdb除錯方法,但gdb始終是除錯native的工具,不支援kernel資訊顯示,比如task資訊之類的。crash補足了這個短板,由Dave Anderson開發和維護的一個記憶體轉儲分析工具,是基於GDB開發的 (GDB適用於使用者程序的coredump,而Crash擴充套件了GDB,使其適用於linux kernel coredump),目前它的最新版本是7.0.5。在沒有統一標準的記憶體轉儲檔案的格式的情況下,Crash工具支援眾多的記憶體轉儲檔案格式,包括:
- Live linux系統
- kdump產生的正常的和壓縮的記憶體轉儲檔案
- 由makedumpfile命令生成的壓縮的記憶體轉儲檔案
- 由Netdump生成的記憶體轉儲檔案
- 由Diskdump生成的記憶體轉儲檔案
- 由Kdump生成的Xen的記憶體轉儲檔案
- IBM的390/390x的記憶體轉儲檔案
- LKCD生成的記憶體轉儲檔案
- Mcore生成的記憶體轉儲檔案
而我們前面講到的SYS_COREDUMP,則可以用crash來除錯。
安裝/使用方法
搭建crash分析kernel ramdump平臺
常用命令
crash使用gdb作為它的內部引擎,crash中的很多命令和語法都與gdb相同。如果曾經使用過gdb,就會發現crash並不是很陌生。如果想獲得crash更多的命令和相關命令的詳細說明,可以使用crash的內部命令help來獲取:
命令 | 說明 | 例子 |
* | 指標的快捷方式,用於代替struct/union | *page 0xc02943c0:顯示0xc02943c0地址的page結構體 |
files | 顯示已開啟的所有檔案的資訊 | files 462:顯示程序462的已開啟檔案資訊 |
mach | 顯示與機器相關的引數資訊 | mach:顯示CPU型號,核數,記憶體大小等 |
sys | 顯示特殊系統的資料 | sys config:顯示CONFIG_xxx配置巨集狀態 |
timer | 無引數。按時間的先後順序顯示定時器佇列的資料 | timer:顯示詳細資訊 |
mod | 顯示已載入module的詳細資訊 | mod:列出所有已載入module資訊 |
runq | 顯示runqueue資訊 | runq:顯示所有runqueue裡的task |
tree | 顯示基數樹/紅黑樹結構 | tree -t rbtree -o vmap_area.rb_node vmap_area_root:顯示所有紅黑樹vmap_area.rb_node節點地址 |
fuser | 顯示哪些task使用了指定的檔案/socket | fuser /usr/lib/libkfm.so.2.0.0:顯示使用了該檔案的所有程序 |
mount | 顯示已掛載的檔案系統資訊 | mount:當前已掛載的檔案系統資訊 |
ipcs | 顯示System V IPC資訊 | ipcs:顯示系統中System V IPC資訊 |
ps | 顯示程序狀態 | ps:類似ps命令 |
struct | 顯示結構體的具體內容 | struct vm_area_struct c1e44f10:顯示c1e44f10結構 |
union | 顯示聯合體的具體內容,用法與struct一致 | union bdflush_param:顯示bdflush_param結構 |
waitq | 列出在等待佇列中的所有task。引數可以指定佇列的名稱、記憶體地址等 | waitq buffer_wait:顯示buffer_wait等待佇列資訊 |
irq | 顯示中斷編號的所有資訊 | irq 18:顯示中斷18的資訊 |
list | 顯示連結串列的內容 | list task_struct.p_pptr c169a000:顯示c169a000地址所指task裡p_pptr連結串列 |
log | 顯示核心的日誌,以時間的先後順序排列 | log -m:顯示kernel log |
dev | 顯示資料關聯著的塊裝置分配,包括埠使用、記憶體使用及PCI裝置資料 | dev:顯示字元/塊裝置相關資訊 |
sig | 顯示一個或者多個task的signal-handling資料 |
sig 8970:顯示程序8970的訊號處理相關資訊 |
task | 顯示指定內容或者程序的task_struct的內容 | task -x:顯示當前程序task_struct等內容 |
swap | 無引數。顯示已配置好的交換裝置資訊 | swap:交換裝置資訊 |
search | 在給定範圍的使用者、核心虛擬記憶體或者實體記憶體搜尋值 | search -u deadbeef:在使用者記憶體搜尋0xdeadbeef |
bt | 顯示呼叫棧資訊 | bt:顯示當前呼叫棧 |
net | 顯示各種網路相關的資料 | net:顯示網路裝置列表 |
vm | 顯示task的基本虛擬記憶體資訊 | vm:類似於/proc/self/maps |
btop | 把一個16進位制地址轉換成它的分頁號 | N/A |
ptob | 該命令與btop相反,是把一個分頁號轉換成地址 | N/A |
vtop | 顯示使用者或核心虛擬記憶體所對應的實體記憶體 | N/A |
ptov | 該命令與vtop相反。把實體記憶體轉換成虛擬記憶體 | N/A |
pte | 16進位制頁表項轉換為物理頁地址和頁的位設定 | N/A |
alias | 顯示或建立一個命令的別名 | alias kp kmem -p:以後用kp命令相當於kmem -p |
foreach | 用指定的命令列舉 | foreach bt:顯示所有程序的呼叫棧 |
repeat | 迴圈執行指定命令 | repeat -1 p jiffies:每個1s執行p jiffies |
ascii | 把16進製表示的字串轉化成ascii表示的字串 | ascii 62696c2f7273752f:結果為/usr/lib |
set | 設定要顯示的內容,內容一般以程序為單位,也可以設定當前crash的內部變數 | set -p:切換到崩潰程序的上下文環境 |
p | print的縮寫,打印表達式的值。表示式可以為變數,也可以為結構體 | N/A |
dis | disassemble的縮寫。把一個命令或者函式分解成彙編程式碼 | dis sys_signal:反彙編sys_signal函式 |
whatis | 搜尋資料或者型別的資訊 | whatis linux_binfmt:顯示linux_binfmt結構體 |
eval | 計算表示式的值,及把計算結果或者值顯示為16、10、8和2進位制 | N/A |
kmem | 顯示當前kernel使用記憶體狀況 | kmem -i:顯示kernel使用記憶體狀況 |
sym | 顯示符號所在的虛擬地址,或虛擬地址對應的符號 | sym jiffies:顯示jiffies地址 |
rd | 顯示指定記憶體的內容。缺少的輸出格式是十六進位制輸出 | rd -a linux_banner:顯示linux_banner內容 |
wr | 根據引數指定的寫記憶體。在定位系統出錯的地方時,一般不使用該命令 | wr my_debug_flag 1:修改my_debug_flag值為1 |
gdb | 執行GDB原生命令 | gdb help:執行gdb的help命令 |
extend | 動態裝載或解除安裝crash額外的動態連結庫 | N/A |
q | 退出 | N/A |
exit | 同q,退出 | N/A |
help | 幫助命令 | N/A |
參考
Crash工具主頁:http://people.redhat.com/anderson/
到這裡,基本上對KE除錯有基本的瞭解,剩下的就是對kernel的熟悉程度了。越熟悉,除錯起來越容易,也可以根據問題對症下藥。
kernel內容非常龐大,可能不知道如何下手,建議先看Unix/Linux核心相關的書籍,瞭解核心的經典實現方法,然後再結合原始碼去研究Linux核心。這樣做的原因是避免從一開始就陷入細節。
核心重點關注這幾個部分:程序管理及排程,記憶體管理,檔案及檔案系統,Cache,I/O,SMP(多CPU)。 參考的書籍有(最好是看英文原版):- 《Linux核心設計與實現》
- 《Linux核心原始碼情景分析》
- 《深入理解Linux核心》
等等。
另外要注意,linux kernel發展很快,有些模組/結構可能被移除或沒有使用了,基本就不用關注了。