《Linux內核設計與實現》第十八章學習筆記

第十八章 調試

【學習時間:1小時 總結博客時間:1小時15分】linux

【學習內容:出現bug的緣由、內核調試器gdb、使用Git進行二分查找】git

內核級開發的調試工做遠比用戶級開發艱難,它帶來的風險比用戶級別更高。算法

1、準備開始

1. 準備工做須要:sass

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

2. 在用戶級程序中bug經常表現得清晰(執行foo就會讓程序當即產生核心信息轉儲)可是內核中的bug表現得不像用戶級程序中那麼清晰。由於內核、用戶以及硬件之間的交互很微妙。安全

3. 調試的主要思想是讓bug重現,可是在內核中這並非很容易作到的。所以,在跟蹤bug的時候,掌握的信息越多越好。架構

2、內核中的bug

1. 內核bug產生的緣由:app

  • 錯誤代碼,例如沒有把正確的值存放在恰當的位置
  • 同步時發生的錯誤,例如共享變量鎖定不當
  • 錯誤的管理硬件,例如給錯誤的控制寄存器發送錯誤的指令
  • ……

2. 內核bug發做的症狀可能有:ide

  • 下降全部程序的運行性能
  • 毀壞數據
  • 使得系統處於死鎖狀態
  • ……

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

4. 內核開發比起用戶開發要多考慮一些獨特的問題,如定時限制、競爭條件等,它們都是容許多個線程在內核中同時運行產生的結果。工具

3、經過打印來調試

內核提供的格式化打印函數printk()有一些特殊功能:

3.1 健壯性

健壯性——在任什麼時候候、任何地方都能調用它,彈性極佳。能夠在中斷上下文和進程上下文中被調用;能夠在任何持有鎖時被調用;能夠在多處理器上同時被調用,而且沒必要使用鎖。

漏洞:在系統啓動過程當中,中斷尚未初始化以前,在某些地方不能使用它。

解決方法:調試啓動過程最開始的步驟時,可用early_printk()代替,功能與printk()徹底相同,能更早工做。

3.2 日誌等級

1. printk()和printf()在使用上最主要的區別就是前者能夠指定一個日誌級別,內核根據這個級別來判斷是否在終端上打印消息。內核把級別比某個特定值低的全部消息顯示在終端上。

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等級,調整終端的默認記錄等級。

3.3 記錄緩衝區

1. 內核消息都被記錄在環形隊列中,以隊列方式進行讀寫;大小能夠經過設置CONFIGLOGBUF_SHIFT進行調整。

2. 在單處理器上,該緩衝區大小默認爲16KB,也就是說,超過的消息將覆蓋舊消息。

3. 優點:

  • 健壯性:讀寫同步問題容易解決,在中斷上下文中也能夠方便的使用
  • 簡單性:記錄的維護更加方便

4. 劣勢:可能會丟失消息。

3.4 syslogd和klogd

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

1. klogd

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

2. syslogd

  syslogd將它接收到的全部消息添加到一個文件中,默認是/var/log/messages。

3.5 從printf()到printk()的轉換

4、oops

1. oops是內核告知用戶有不幸發生的最經常使用的方式。內核很難自我修復,也不能將本身殺死(由於內核是整個系統的管理者,不能將本身殺死,也很難自行修復),只能發佈oops。

2. 發佈oops的過程

  • 向終端上輸出錯誤消息
  • 輸出寄存器中保存的信息
  • 輸出可供跟蹤的回溯線索

3. oops發生的時機:

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

4. oops發生的可能緣由:

  • 內存訪問越界
  • 非法的指令
  • ……

5. oops中包含的重要信息對於全部體系結構都是相同的:寄存器上下文和回溯線索。

  • 回溯線索:顯示了致使錯誤發生的函數調用鏈
  • 寄存器上下文信息可能一樣有用,好比幫助衝進引起問題的現場

4.1 ksymoops

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

調用方式:

kysmoop saved_oops.txt

4.2 kallsyms

  如今不須要使用sysmoops工具,由於用戶使用時可能會發生不少問題。新版本中引入了kallsyms特性,能夠經過定義CONFIG_KALLSYMS配置選項啓用。

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 自旋鎖內睡眠選項

……

6、引起bug並打印信息

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

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

if (bad_thing)
    BUG();

或使用更好的形式:

BUG_ON(bad_thing);

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

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

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

7、神奇的系統請求鍵

這個功能能夠經過定義CONFIG_ MAGIC _SYSRQ配置選項來啓用。SysRq(系統請求)鍵在大多數鍵盤上都是標準鍵。

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

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

echo 1 > /proc/sys/kernel/sysrq

支持Sysrq的幾個命令:

8、內核調試器的傳奇

8.1 gdb

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

gdb vmlinux /proc/kcore

其中vmlinx文件是未經壓縮的內核映像,區別於zImage或bImage,它存放於源代碼樹的根目錄上。

/proc/kcore做爲一個參數選項,是做爲core文件來用的,經過它可以訪問到內核駐留的高端內存。只有超級用戶才能讀取此文件的數據可使用gdb的全部命令來獲取信息。如:

p global_variable //打印一個變量的值

disassemble function //反彙編一個函數

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

3. gdb的侷限性:

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

8.2 kgdb

1. kgdb是一個補丁,可讓咱們在遠程主機上經過串口利用gdb的全部功能對內核進行調試。這須要兩臺計算機:第一臺運行帶有kgdb補丁的內核,第二臺經過串行線使用gdb對第一臺進行調試。

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

  • 讀取和修改變量值
  • 設置斷點
  • 設置關注變量
  • 單步執行
  • 某些版本的gdb甚至容許執行函數

9、探測系統

9.1 使用uid做爲選擇條件

1. 通常狀況下,加入特性時,只要保留原有的算法而把新算法加入到其餘位置上,基本就能保證安全。能夠把用戶id(UID)做爲選擇條件來實現這種功能,經過某種選擇條件,安排到底執行哪一種算法:

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

9.2 使用條件變量

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

能夠經過某種接口提供對這個變量的操控,也能夠直接經過調試器進行操控。

9.3 使用統計量

這種方法經常使用於使用者須要掌握某個特定事件的發生規律的時候。 經過建立統計量並提供某種機制訪問其統計結果。

注意:這種方法不是SMP安全的,理想的辦法是經過原子操做進行實現。

9.4 重複頻率限制

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

  • 重複頻率限制:就是限制調試信息,最多幾秒打印一次,能夠根據本身的須要調節頻率。例如printk()函數的調節頻率,能夠用printk_ratelimit()函數限制
  • 發生次數限制:要調試信息至多輸出幾回,超過次數限制後就不能再輸出。這種方法能夠用來確認在特定狀況下某段代碼的確被執行了

注意:

  • 用到的變量都應該是靜態的,而且限制在函數的局部範圍之內,這樣才能保證變量的值在經歷屢次函數調用後仍然可以保留下來
  • 這些例子的代碼都不是SMP安全或搶佔安全的,只須要用原子操做改造一下

10、用二分查找法找出罪惡的變動

在問題內核和良好內核之間使用二分法,能很容易地對引起bug的代碼進行定位。

11、使用Git進行二分搜索

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

git bisect start   //告知git要進行二分搜索
git bisect bad <revision>   //已知出現問題的最先內核版本
git bisect bad  //當前版本就是引起bug的最第一版本的狀況下使用這條命令
git bisect good <revision>  //最新的可正常運行的內核版本

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

若是版本運行正常:
git bisect good

若是版本運行異常:
git bisect bad

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

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

總結

  經過對本章的學習,我瞭解到調試過程實際上是一種尋求實現與目標誤差的行爲,從內核內置的調試架構到調試程序,從記錄日誌到用git二分法查找。此時夯實基礎,爲之後的學習積累經驗。

相關文章
相關標籤/搜索