1. 程式人生 > >分析Linux kernel exception-基礎篇

分析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開始吧。


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發展很快,有些模組/結構可能被移除或沒有使用了,基本就不用關注了。