4.5. 調(diào)試系統(tǒng)故障

2018-02-24 15:49 更新

4.5.?調(diào)試系統(tǒng)故障

即便你已使用了所有的監(jiān)視和調(diào)試技術(shù), 有時故障還留在驅(qū)動里, 當(dāng)驅(qū)動執(zhí)行時系統(tǒng)出錯. 當(dāng)發(fā)生這個時, 能夠收集盡可能多的信息來解決問題是重要的.

注意"故障"不意味著"崩潰". Linux 代碼是足夠健壯地優(yōu)雅地響應(yīng)大部分錯誤:一個故障常常導(dǎo)致當(dāng)前進程的破壞而系統(tǒng)繼續(xù)工作. 系統(tǒng)可能崩潰, 如果一個故障發(fā)生在一個進程的上下文之外, 或者如果系統(tǒng)的一些至關(guān)重要的部分毀壞了. 但是當(dāng)是一個驅(qū)動錯誤導(dǎo)致的問題, 它常常只會導(dǎo)致不幸使用驅(qū)動的進程的突然死掉. 當(dāng)進程被銷毀時唯一無法恢復(fù)的破壞是分配給進程上下文的一些內(nèi)存丟失了; 例如, 驅(qū)動通過 kmalloc 分配的動態(tài)列表可能丟失. 但是, 因為內(nèi)核為任何一個打開的設(shè)備在進程死亡時調(diào)用關(guān)閉操作, 你的驅(qū)動可以釋放由 open 方法分配的東西.

盡管一個 oops 常常都不會關(guān)閉整個系統(tǒng), 你很有可能發(fā)現(xiàn)在發(fā)生一次后需要重啟系統(tǒng). 一個滿是錯誤的驅(qū)動能使硬件處于不能使用的狀態(tài), 使內(nèi)核資源處于不一致的狀態(tài), 或者, 最壞的情況, 在隨機的地方破壞內(nèi)核內(nèi)存. 常常你可簡單地卸載你的破驅(qū)動并且在一次 oops 后重試. 然而, 如果你看到任何東西建議說系統(tǒng)作為一個整體不太好了, 你最好立刻重啟.

我們已經(jīng)說過, 當(dāng)內(nèi)核代碼出錯, 一個提示性的消息打印在控制臺上. 下一節(jié)解釋如何解釋并利用這樣的消息. 盡管它們對新手看來相當(dāng)模糊, 處理器轉(zhuǎn)儲是很有趣的信息, 常常足夠來查明一個程序錯誤而不需要附加的測試.

4.5.1.?oops 消息

大部分 bug 以解引用 NULL 指針或者使用其他不正確指針值來表現(xiàn)自己的. 此類 bug 通常的輸出是一個 oops 消息.

處理器使用的任何地址幾乎都是一個虛擬地址, 通過一個復(fù)雜的頁表結(jié)構(gòu)映射為物理地址(例外是內(nèi)存管理子系統(tǒng)自己使用的物理地址). 當(dāng)解引用一個無效的指針, 分頁機制無法映射指針到一個物理地址, 處理器發(fā)出一個頁錯誤給操作系統(tǒng). 如果地址無效, 內(nèi)核無法"頁入"缺失的地址; 它(常常)產(chǎn)生一個 oops 如果在處理器處于管理模式時發(fā)生這個情況.

一個 oops 顯示了出錯時的處理器狀態(tài), 包括CPU 寄存器內(nèi)容和其他看來不可理解的信息. 消息由錯誤處理的 printk 語句產(chǎn)生( arch/*/kernel/traps.c )并且如同前面 "printk" 一節(jié)中描述的被分派.

我們看一個這樣的消息. 這是來自在運行 2.6 內(nèi)核的 PC 上一個 NULL 指針導(dǎo)致的結(jié)果. 這里最相關(guān)的信息是指令指針(EIP), 錯誤指令的地址.


Unable to handle kernel NULL pointer dereference at virtual address 00000000
 printing eip:  
d083a064  
Oops: 0002 [#1]  
SMP  
CPU:  0  
EIP:  0060:[<d083a064>]  Not tainted  
EFLAGS: 00010246  (2.6.6)  
EIP is at faulty_write+0x4/0x10 [faulty]  
eax: 00000000  ebx: 00000000  ecx: 00000000  edx: 00000000  
esi: cf8b2460  edi: cf8b2480  ebp: 00000005  esp: c31c5f74  
ds: 007b  es: 007b  ss: 0068  

Process bash (pid: 2086, threadinfo=c31c4000 task=cfa0a6c0) 
Stack: c0150558 cf8b2460 080e9408 00000005 cf8b2480 00000000 cf8b2460 cf8b2460 fffffff7 080e9408 c31c4000 c0150682 cf8b2460 080e9408 00000005 cf8b2480 00000000 00000001 00000005 c0103f8f 00000001 080e9408 00000005 00000005 
Call Trace:
 [<c0150558>] vfs_write+0xb8/0x130
 [<c0150682>] sys_write+0x42/0x70
 [<c0103f8f>] syscall_call+0x7/0xb

Code: 89 15 00 00 00 00 c3 90 8d 74 26 00 83 ec 0c b8 00 a6 83 d0 

寫入一個由壞模塊擁有的設(shè)備而產(chǎn)生的消息, 一個故意用來演示失效的模塊. faulty.c 的 write 方法的實現(xiàn)是瑣細的:


ssize_t faulty_write (struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
        /* make a simple fault by dereferencing a NULL pointer */
        *(int *)0 = 0;
        return 0;
}

如你能見, 我們這里做的是解引用一個 NULL 指針. 因為 0 一直是一個無效的指針值, 一個錯誤發(fā)生, 由內(nèi)核轉(zhuǎn)變?yōu)榍懊嬲故镜?oops 消息. 調(diào)用進程接著被殺掉.

錯誤模塊有不同的錯誤情況在它的讀實現(xiàn)中:


ssize_t faulty_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
    int ret;
    char stack_buf[4];

    /* Let's try a buffer overflow */
    memset(stack_buf, 0xff, 20);
    if (count > 4)

        count = 4; /* copy 4 bytes to the user */
    ret = copy_to_user(buf, stack_buf, count);
    if (!ret)

        return count;
    return ret;
}

這個方法拷貝一個字串到一個本地變量; 不幸的是, 字串長于目的數(shù)組. 當(dāng)函數(shù)返回時導(dǎo)致的緩存區(qū)溢出引起一次 oops . 因為返回指令使指令指針到不知何處, 這類的錯誤很難跟蹤, 并且你得到如下的:


EIP: 0010:[<00000000>]
Unable to handle kernel paging request at virtual address ffffffff

 printing eip:  
ffffffff  
Oops: 0000 [#5]  
SMP  
CPU:  0  
EIP:  0060:[<ffffffff>]  Not tainted  
EFLAGS: 00010296  (2.6.6)  
EIP is at 0xffffffff  
eax: 0000000c  ebx: ffffffff  ecx: 00000000  edx: bfffda7c  
esi: cf434f00  edi: ffffffff  ebp: 00002000  esp: c27fff78  
ds: 007b  es: 007b  ss: 0068  

Process head (pid: 2331, threadinfo=c27fe000 task=c3226150) 
Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7 bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000 00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70 
Call Trace: [<c0150612>] sys_read+0x42/0x70 [<c0103f8f>] syscall_call+0x7/0xb 
Code: Bad EIP value. 

這個情況, 我們只看到部分的調(diào)用堆棧( vfs_read 和 faulty_read 丟失 ), 內(nèi)核抱怨一個"壞 EIP 值". 這個抱怨和在開頭列出的犯錯的地址 ( ffffffff ) 都暗示內(nèi)核堆棧已被破壞.

通常, 當(dāng)你面對一個 oops, 第一件事是查看發(fā)生問題的位置, 常常與調(diào)用堆棧分開列出. 在上面展示的第一個 oops, 相關(guān)的行是:


EIP is at faulty_write+0x4/0x10 [faulty] 

這里我們看到, 我們曾在函數(shù) faulty_write, 它位于 faulty 模塊( 在方括號中列出的 ). 16 進制數(shù)指示指令指針是函數(shù)內(nèi) 4 字節(jié), 函數(shù)看來是 10 ( 16 進制 )字節(jié)長. 常常這就足夠來知道問題是什么.

如果你需要更多信息, 調(diào)用堆棧展示給你如何得知在哪里壞事的. 堆棧自己是 16 機制形式打印的; 做一點工作, 你經(jīng)常可以從堆棧的列表中決定本地變量的值和函數(shù)參數(shù). 有經(jīng)驗的內(nèi)核開發(fā)者可以從這里的某些模式識別中獲益; 例如, 如果你看來自 faulty_read oops 的堆棧列表:


Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7
 bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000
 00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70 

堆棧頂部的 ffffffff 是我們壞事的字串的一部分. 在 x86 體系, 缺省地, 用戶空間堆棧開始于 0xc0000000; 因此, 循環(huán)值 0xbfffda70 可能是一個用戶堆棧地址; 實際上, 它是傳遞給 read 系統(tǒng)調(diào)用的緩存地址, 每次下傳過系統(tǒng)調(diào)用鏈時都被復(fù)制. 在 x86 (又一次, 缺省地), 內(nèi)核空間開始于 0xc0000000, 因此這個之上的值幾乎肯定是內(nèi)核空間的地址, 等等.

最后, 當(dāng)看一個 oops 列表, 一直監(jiān)視本章開始討論的"slab 毒害"值. 例如,如果你得到一個內(nèi)核 oops, 里面的犯錯地址時 0xa5a5a5a5a5, 你幾乎肯定 - 某個地方在初始化動態(tài)內(nèi)存.

請注意, 只在你的內(nèi)核是打開 CONFIG_KALLSYMS 選項而編譯時可以看到符號的調(diào)用堆棧. 否則, 你見到一個裸的, 16 機制列表, 除非你以別的方式對其解碼, 它是遠遠無用的.

4.5.2.?系統(tǒng)掛起

盡管內(nèi)核代碼的大部分 bug 以 oops 消息結(jié)束, 有時候它們可能完全掛起系統(tǒng). 如果系統(tǒng)掛起, 沒有消息打印. 例如, 如果代碼進入一個無限循環(huán), 內(nèi)核停止調(diào)度,[15] 并且系統(tǒng)不會響應(yīng)任何動作, 包括魔術(shù) Ctrl-Alt-Del 組合鍵. 你有 2 個選擇來處理系統(tǒng)掛起-- 或者事先阻止它們, 或者能夠事后調(diào)試它們.

你可阻止無限循環(huán)通過插入 schedule 引用在戰(zhàn)略點上. schedule 調(diào)用( 如你可能猜到的 )調(diào)度器, 因此, 允許別的進程從當(dāng)前進程偷取 CPU 數(shù)據(jù). 如果一個進程由于你的驅(qū)動的bug而在內(nèi)核空間循環(huán), schedule 調(diào)用使你能夠殺掉進程在跟蹤發(fā)生了什么之后.

你應(yīng)當(dāng)知道, 當(dāng)然, 如何對 schedule 的調(diào)用可能創(chuàng)造一個附加的重入調(diào)用源到你的驅(qū)動, 因為它允許別的進程運行. 這個重入正常地不應(yīng)當(dāng)是問題, 假定你在你的驅(qū)動中已經(jīng)使用了合適的加鎖. 然而, 要確認在你的驅(qū)動持有一個自旋鎖的任何時間不能調(diào)用 schedule.

如果你的驅(qū)動真正掛起了系統(tǒng), 并且你不知道在哪里插入 schedule 調(diào)用, 最好的方式是加入一些打印消息并且寫到控制臺(如果需要, 改變 console_loglevel 值).

有時候系統(tǒng)可能看來被掛起, 但是沒有. 例如, 這可能發(fā)生在鍵盤以某個奇怪的方式保持鎖住的時候. 這些假掛起可通過查看你為此目的運行的程序的輸出來檢測. 一個你的顯示器上的時鐘或者系統(tǒng)負載表是一個好的狀態(tài)監(jiān)控器; 只要他繼續(xù)更新, 調(diào)度器就在工作.

對許多的上鎖一個必不可少的工具是"魔術(shù) sysrq 鍵", 在大部分體系上都可用. 魔鍵 sysrq 是 PC 鍵盤上 alt 和 sysrq 鍵組合來發(fā)出的, 或者在別的平臺上使用其他特殊鍵(詳見 documentation/sysrq.txt), 在串口控制臺上也可用. 一個第三鍵, 與這 2 個一起按下, 進行許多有用的動作中的一個:

r 關(guān)閉鍵盤原始模式; 用在一個崩潰的應(yīng)用程序( 例如 X 服務(wù)器 )可能將你的鍵盤搞成一個奇怪的狀態(tài).

k 調(diào)用"安全注意鍵"( SAK ) 功能. SAK 殺掉在當(dāng)前控制臺的所有運行的進程, 給你一個干凈的終端.

s 進行一個全部磁盤的緊急同步.

u umount. 試圖重新加載所有磁盤在只讀模式. 這個操作, 常常在 s 之后馬上調(diào)用, 可以節(jié)省大量的文件系統(tǒng)檢查時間, 在系統(tǒng)處于嚴重麻煩時.

b boot. 立刻重啟系統(tǒng). 確認先同步和重新加載磁盤.

p 打印處理器消息.

t 打印當(dāng)前任務(wù)列表.

m 打印內(nèi)存信息.

有別的魔術(shù) sysrq 功能存在; 完整內(nèi)容看內(nèi)核源碼的文檔目錄中的 sysrq.txt. 注意魔術(shù) sysrq 必須在內(nèi)核配置中顯式使能, 大部分的發(fā)布沒有使能它, 因為明顯的安全理由. 對于用來開發(fā)驅(qū)動的系統(tǒng), 然而, 使能魔術(shù) sysrq 值得為它自己建立一個新內(nèi)核的麻煩. 魔術(shù) sysrq 可能在運行時關(guān)閉, 使用如下的一個命令:

echo 0 > /proc/sys/kernel/sysrq
如果非特權(quán)用戶能夠接觸你的系統(tǒng)鍵盤, 你應(yīng)當(dāng)考慮關(guān)閉它, 來阻止有意或無意的損壞. 一些以前的內(nèi)核版本缺省關(guān)閉 sysrq, 因此你需要在運行時使能它, 通過向同樣的 /proc/sys 文件寫入 1.

sysrq 操作是非常有用, 因此它們已經(jīng)對不能接觸到控制臺的系統(tǒng)管理員可用. 文件 /proc/sysrq-trigger 是一個只寫的入口點, 這里你可以觸發(fā)一個特殊的 sysrq 動作, 通過寫入關(guān)聯(lián)的命令字符; 接著你可收集內(nèi)核日志的任何輸出數(shù)據(jù). 這個 sysrq 的入口點是一直工作的, 即便 sysrq 在控制臺上被關(guān)閉.

如果你經(jīng)歷一個"活掛", 就是你的驅(qū)動粘在一個循環(huán)中, 但是系統(tǒng)作為一個整體功能正常, 有幾個技術(shù)值得了解. 經(jīng)常地, sysrq p 功能直接指向出錯的函數(shù). 如果這個不行, 你還可以使用內(nèi)核剖析功能. 建立一個打開剖析的內(nèi)核, 并且用命令行中 profile=2 來啟動它. 使用 readprofile 工具復(fù)位剖析計數(shù)器, 接著使你的驅(qū)動進入它的循環(huán). 一會兒后, 使用 readprofile 來看內(nèi)核在哪里消耗它的時間. 另一個更高級的選擇是 oprofile, 你可以也考慮下. 文件 documentation/basic_profiling.txt 告訴你啟動剖析器所有需要知道的東西.

在追逐系統(tǒng)掛起時一個值得使用的防范措施是以只讀方式加載你的磁盤(或者卸載它們). 如果磁盤是只讀或者卸載的, 就沒有風(fēng)險損壞文件系統(tǒng)或者使它處于不一致的狀態(tài). 另外的可能性是使用一個通過 NFS, 網(wǎng)絡(luò)文件系統(tǒng), 來加載它的全部文件系統(tǒng)的計算機, 內(nèi)核的"NFS-Root"功能必須打開, 在啟動時必須傳遞特殊的參數(shù). 在這個情況下, 即便不依靠 sysrq 你也會避免文件系統(tǒng)破壞, 因為文件系統(tǒng)的一致有 NFS 服務(wù)器來管理, 你的設(shè)備驅(qū)動不會關(guān)閉它.

[15] 實際上, 多處理器系統(tǒng)仍然在其他處理器上調(diào)度, 甚至一個單處理器的機器可能重新調(diào)度, 如果內(nèi)核搶占被使能. 然而, 對于大部分的通常的情況( 單處理器不使能搶占), 系統(tǒng)一起停止調(diào)度.

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號