Linux內核分析 讀書筆記 (第十八章)

第十八章 調試

18.1 準備開始

1. 須要的只是:css

  • 一個bug
  • 一個藏匿bug的內核版本
  • 相關內核代碼的知識和運氣

2. 在跟蹤bug的時候,掌握的信息越多越好。linux

18.2 內核中的bug

1. 內核bug多種多樣,產生的緣由有不少:從錯誤代碼(沒有把正確的值存放在恰當的位置);到同步時發生的錯誤(共享變量鎖定不當);再到錯誤的管理硬件(給錯誤的控制寄存器發送錯誤的指令)。nginx

2. 從下降全部程序的運行性能到毀壞數據再到使得系統處於死鎖狀態均可能是bug發做時的症狀。git

3. 從隱藏在源代碼中的錯誤到展示在目睹者面前的bug,每每是經歷一系列連鎖反應的事件纔可能觸發的。算法

 18.3 經過打印來調試

printk()是內核提供的格式化打印函數,除了和C庫提供的printf()函數功能相同外還有一些自身特殊的功能。sass

18.3.1 健壯性

1.健壯性是printk()函數最容易讓人們接受的一個特質,在任什麼時候候內核的任何地方都能調用它。安全

除非在啓動過程的初期就要在終端輸出,不然能夠認爲printk()在什麼狀況下都能工做。app

  • 能夠在中斷上下文和進程上下文中被調用
  • 能夠在任何持有鎖時被調用
  • 能夠在多處理器上同時被調用,調用者沒必要使用鎖。

2.printk()函數也會有漏洞,解決辦法是提供一個變體函數early _ printk(),但這種辦法在某些硬件體系結構上沒法實現,缺乏可移植性。ide

18.3.2 日誌等級

1.printk()能夠指定一個日誌級別,內核根據這個級別來判斷是否在終端上打印消息。內核把級別比某個特定值低的全部消息顯示在終端上。函數

2. KERN_ WARNING和KERN_ DEBUG都是簡單的宏定義,加進printk()函數要打印的消息的開頭。內核用這個指定的記錄等級和當前終端的記錄等級console_loglevel來決定是否是向終端上打印。

3.若是沒有指定一個等級記錄,函數會選用默認的DEFAULT _ MESSAGE _ LOGLEVEL,如今的默認等級爲KERN _ WARNING。但默認值未來存在變化性,因此仍是應該指定一個記錄等級。

4.內核將最重要的記錄等級KERN _ EMERG定爲<0>,可有可無的記錄等級KERN _ DEBUG定爲<7>

5.有兩種賦予記錄等級的方法:

  • 保持終端的默認記錄等級不變,給全部調試信息KERN _ CRIT或更低的等級。
  • 給全部調試信息KERN _ DEBUG等級,調整終端的默認記錄等級。

18.3.3 記錄緩衝區

內核消息都被保存在一個環形隊列中,該緩衝區的大小能夠在編譯時經過設置CONFIG _ LOG _ BUF _ SHIFT進行調整,在單處理器的系統上默認值是16kb,也就是說內核在同一時間只能保存16kb的內核消息,再多的話新消息就會覆蓋老消息。讀寫都是按照環形隊列方式操做的。

  • 優勢:健壯性,在中斷上下 文中也能夠方便的使用;簡單性,使記錄維護起來更容易。
  • 缺點:可能會丟失消息。

18.3.4 syslogd和klogd

1.在標準的Linux系統上,用戶空間的守護進程klogd從記錄緩衝區中獲取內核消息,再經過syslogd守護進程將他們保存在系統日誌文件中。

2.klogd:既能夠從/proc/kmsg文件中,也能夠經過syslog()系統調用讀取得到的內核信息,默認狀況下選擇讀取/proc方式實現。兩種狀況klogd都會阻塞,直到有新的內核消息可供讀出,被喚醒以後,默認處理是將消息傳給syslogd。在啓動時,能夠經過-c標誌來改變終端的記錄等級。

3.syslogd:把它接收到的全部消息添加到一個文件中,默認是/var/log/messages,也可經過/etc/syslog.conf配置文件從新指定。

18.4 oops

1.oops是內核告知用戶有不幸發生的最經常使用的方式。

2.由於內核是整個系統的管理者,不能採起像在用戶空間出現運行錯誤時使用的那些簡單手段,由於他很難自我修復,也不能將本身殺死,只能發佈oops,過程爲:向終端上輸出錯誤消息、輸出寄存器中保存的信息、輸出可供跟蹤的回溯線索。一般發佈oops以後,內核會處於一種不穩定狀態。

2.oops發生的時機:
  • 發生在中斷上下文:內核沒法繼續,會陷入混亂,致使系統死機
  • 發生在idle進程或init進程(0號進程和1號進程),同上
  • 發生在其餘進程運行時,內核會殺死該進程並嘗試着繼續執行
3.oops中包含的重要信息:

寄存器上下文和回溯線索

  • 回溯線索:顯示了致使錯誤發生的函數調用鏈,以便咱們觀察發生了什麼。
  • 寄存器上下文信息:幫助衝進引起問題的現場。

18.4.1 ksymoops

回溯線索中的地址須要轉化成有意義的符號名稱才能夠方便使用,須要調用ksymoops命令,還必須提供編譯內核時產生的System.map。若是用的是模塊,還須要一些模塊信息。調用方法:

kysmoop saved_oops.txt

18.4.2 kallsyms

如今的版本中不須要使用sysmoops這個工具,由於可能會發生不少問題,新版本中引入了kallsyms疼,能夠經過定義CONFIG _ KALLSYMS配置選項啓用。

18.5 內核調試配置選項

在編譯時,爲了方便調試和測試內核代碼,內核提供了許多配置選項。這些選項都在內核配置編譯器的內核開發菜單中,都依賴於CONFIG_ DEBUG_ KERNEL。

  • slab layer debugging slab層調試選項
  • high-memory debugging 高端內存調試選項
  • I/O mapping debugging I/O映射調試選項
  • spin-lock debugging 自旋鎖調試選項
  • stack-overflow debugging 棧溢出檢查選項
  • sleep-inside-spinlock checking 自旋鎖內睡眠選項

18.6 引起bug並打印信息

1.一些內調用能夠用來方便標記bug,提供斷言並輸出信息。最經常使用的兩個是BUG()和BUG_ON()。當被調用時會引起oops,致使棧的回溯和錯誤信息的打印。

2.大部分體系把BUG()和BUG_ON()定義成某種非法操做,這樣天然會產生須要的oops。能夠把這些調用當作斷言使用,想要斷言某種狀況不應發生:

if (bad_thing)
    BUG();

或使用更好的形式:

BUG_ON(bad_thing);

 

2.BUILD _ BUG_ ON()與BUG_ON()做用相同,僅在編譯時調用。

 

 

3.引用panic()能夠引起更嚴重的錯誤,不但會打印錯誤信息,還會掛起整個系統。

 

 

4.dump_stack()只在終端上打印寄存器上下文和函數的跟蹤線索。

 

18.7 神奇的系統請求鍵

1.該功能能夠經過定義CONFIG _ MAGIC _ SYSRQ配置選項來啓用。SysRq(系統請求)鍵在大多數鍵盤上都是標準鍵。

2.當該功能被啓用時,不管內核出於什麼狀態,均可以經過特殊的組合鍵和內核進行通訊。

3.除了配置選項之外,還要經過一個sysctl用來標記該特性的開或關,須要啓動它時使用以下命令:

echo 1 > /proc/sys/kernel/sysrq

18.8 內核調試器的傳奇

18.8.1 gdb

1.可使用標準的GNU調試器對正在運行的內核進行查看。針對內核啓動調試器的方法與針對進程的方法大體相同:

gdb vmlinux /proc/kcore
  • vmlinx:未經壓縮的內核映像,區別於zImage或bImage,它存放於源代碼樹的根目錄上。
  • /proc/kcore做爲一個參數選項,是做爲core文件來用的,經過它可以訪問到內核駐留的高端內存。只有超級用戶才能讀取此文件的數據。

2.可使用gdb的全部命令來獲取信息。

打印一個變量的值:
p global_variable
反彙編一個函數:
disassemble function

3. 若是編譯內核的時候使用了-g參數(在內核的Makefile文件的CFLAGS變量中加入-g)gdb還能夠提供更多的信息。

4.侷限性:

  • 沒有辦法修改內核數據
  • 不能單步執行內核代碼

  • 不能加斷點

18.8.2 kgdb

1.kgdb是一個補丁 ,可讓咱們在遠端主機上經過串口利用gdb的全部功能對內核進行調試。

這須要兩臺計算機:第一臺運行帶有kgdb補丁的內核,第二臺經過串行線使用gdb對第一臺進行調試。

2.經過kgdb,gdb的全部功能都能使用:

  • 讀取和修改變量值
  • 設置斷點
  • 設置關注變量
  • 單步執行

18.9 探測系統

18.9.1 使用uid做爲選擇條件

1.通常狀況下,只要保留原有的算法而把你的新算法加入到其餘位置上,基本就能保證安全。

能夠把用戶id(UID)做爲選擇條件來實現這種功能,經過某種選擇條件,安排到底執行哪一種算法:

if (current-> uid !=7777) { /* 老算法…… */ } else { /* 新算法…… */ }

2.除了uid爲7777的用戶之外,其餘全部的用戶都是用的老算法,能夠建立一個UID爲7777用戶,專門用來測試新算法。

18.9.2 使用條件變量

若是代碼與進程無關,或者但願有一個針對全部狀況都能使用的機制來控制某個特性,可使用條件變量。這比使用UID更簡單,只須要建立一個全局變量做爲一個條件選擇開關。若是該變量爲0,就使用某一個分支上的代碼;不然,選擇另一個分支。能夠經過某種接口提供對這個變量的操控,也能夠直接經過調試器進行操控。

18.9.3 使用統計量

1.這種方法經常使用於使用者須要掌握某個特定事件的發生規律的時候。

2.方法是建立統計量,並提供某種機制訪問其統計結果。

3.這種實現並不是是SMP安全的,更好的方式是用原子操做。

18.9.4 重複頻率限制

當系統的調試信息過多的時候,有兩種方式能夠防止這類問題發生:

  • 重複頻率限制:

      限制調試信息,最多幾秒打印一次,能夠根據本身的須要調節頻率。

  • 發生次數限制

    這種方法是要調試信息至多輸出幾回,超過次數限制後就不能再輸出。
    這種方法能夠用來確認在特定狀況下某段代碼的確被執行了。

注:無論哪一種方法用到的變量須要是靜態的、局部的。而且限制在函數的局部範圍之內,這樣才能保證變量的值在經歷屢次函數調用後仍然可以保留下來。不是SMP安全或搶佔安全的,更好的方式是用原子操做。

18.10  使用二分查找法找出罪惡的變動

18.11 使用Git進行二分搜索

1.Git源碼管理工具提供了一個有用的二分搜索機制,若是使用Git來控制Linux源碼樹的副本,則Git將自動運行二分搜索進程。此外,Git會在修訂版本中進行二分搜索,能夠具體找到哪次提交的代碼引起了bug。

告知git要進行二分搜索:
git bisect start 

提供一個出現問題的最先內核版本:
 git bisect bad <revision> 

當前版本就是引起bug的最第一版本的狀況下使用這條命令:

git bisect bad

 提供一個最新的可正常運行的內核版本:
 git bisect good <revision>

2.這以後,git就會利用二分搜索法在Linux源碼樹中,自動檢測正常的版本內核和有bug的內核版本之間那個版本有隱患,而後再編譯、運行以及測試正被檢測的版本。

若是這個版本正常:

git bisect good

若是這個版本運行有異常:

git bisect bad

 

3.對於每個命令,Git將在每個版本的基礎上反覆二分搜索源碼樹,而且返回所查的下一個內核版本,直到不能再進行二分搜索位置,最終Git會打印出有問題的版本號。

4,指定Git僅僅在與錯誤相關的目錄列表中去二分搜索提交的補丁: git bisect start - arch/x86

相關文章
相關標籤/搜索