偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

攻克Linux內(nèi)核Oops:手把手教你從崩潰到破案!

系統(tǒng) Linux
Oops 錯(cuò)誤究竟是什么呢?簡單來說,當(dāng) Linux 內(nèi)核遇到無法正常處理的嚴(yán)重錯(cuò)誤,如空指針引用、非法內(nèi)存訪問、內(nèi)核堆棧溢出等情況時(shí) ,就會(huì)輸出一段包含豐富信息的錯(cuò)誤報(bào)告,這段報(bào)告就是 Oops 信息。

作為一名長期深耕 Linux 內(nèi)核開發(fā)的博主,在這條探索之路上,我遭遇過無數(shù)的挑戰(zhàn),而 Linux 內(nèi)核 Oops 問題,絕對是其中讓人最為頭疼的難題之一。

還記得那是一個(gè)為某項(xiàng)目開發(fā)定制 Linux 內(nèi)核模塊的緊張時(shí)期,我滿心期待地將新編寫的驅(qū)動(dòng)程序模塊加載到內(nèi)核中,本以為一切會(huì)順利進(jìn)行,結(jié)果屏幕上突然跳出一大串密密麻麻的 Oops 錯(cuò)誤信息,系統(tǒng)也陷入了不穩(wěn)定的狀態(tài)。那一刻,我的心瞬間懸了起來,望著那看似雜亂無章的錯(cuò)誤提示,內(nèi)心充滿了焦慮與困惑,完全不知道問題究竟出在哪里。

這種經(jīng)歷并非個(gè)例,相信許多和我一樣在 Linux 內(nèi)核開發(fā)領(lǐng)域摸爬滾打的朋友都有過類似的痛苦遭遇。Oops 錯(cuò)誤就像隱藏在暗處的幽靈,一旦出現(xiàn),就會(huì)讓我們精心構(gòu)建的系統(tǒng)陷入混亂,耗費(fèi)大量的時(shí)間和精力去排查和修復(fù)。它不僅考驗(yàn)著我們的技術(shù)能力,更考驗(yàn)著我們的耐心和毅力。

那么,Oops 錯(cuò)誤究竟是什么呢?簡單來說,當(dāng) Linux 內(nèi)核遇到無法正常處理的嚴(yán)重錯(cuò)誤,如空指針引用、非法內(nèi)存訪問、內(nèi)核堆棧溢出等情況時(shí) ,就會(huì)輸出一段包含豐富信息的錯(cuò)誤報(bào)告,這段報(bào)告就是 Oops 信息。Oops 堪稱是內(nèi)核開發(fā)者和系統(tǒng)調(diào)試人員的得力助手,它詳細(xì)地記錄下錯(cuò)誤發(fā)生時(shí)內(nèi)核的各種狀態(tài)信息,為我們定位和解決問題提供了關(guān)鍵線索。接下來,就讓我們一起深入探尋 Linux 內(nèi)核 Oops 調(diào)試方法,揭開它神秘的面紗,希望能幫助大家在今后遇到 Oops 問題時(shí)更加從容地應(yīng)對。

一、Oops 是什么?

1.1定義闡述

在 Linux 內(nèi)核的世界里,Oops 是當(dāng)內(nèi)核檢測到嚴(yán)重錯(cuò)誤,無法繼續(xù)正常執(zhí)行當(dāng)前操作時(shí),輸出的一段詳細(xì)錯(cuò)誤信息。它就像是內(nèi)核在遇到無法處理的狀況時(shí),向開發(fā)者發(fā)出的緊急求救信號(hào)。從本質(zhì)上講,Oops 是內(nèi)核的一種自我診斷機(jī)制,通過輸出關(guān)鍵的系統(tǒng)狀態(tài)和錯(cuò)誤相關(guān)信息,為調(diào)試提供關(guān)鍵線索。

與用戶空間的 Segmentation Fault(段錯(cuò)誤)類似,Oops 同樣源于程序?qū)?nèi)存的非法訪問或其他嚴(yán)重錯(cuò)誤。比如在用戶空間中,當(dāng)一個(gè)程序試圖訪問未分配給它的內(nèi)存區(qū)域,或者訪問已釋放的內(nèi)存時(shí),就會(huì)觸發(fā) Segmentation Fault 錯(cuò)誤,導(dǎo)致程序崩潰。而在內(nèi)核中,Oops 的出現(xiàn)意味著內(nèi)核在執(zhí)行過程中遇到了類似的嚴(yán)重問題,如空指針引用、非法內(nèi)存訪問、內(nèi)核堆棧溢出等 。這些問題一旦發(fā)生,會(huì)使內(nèi)核的正常運(yùn)行受到嚴(yán)重影響,甚至導(dǎo)致系統(tǒng)死機(jī)。因此,Oops 對于內(nèi)核調(diào)試至關(guān)重要,它所包含的信息是我們深入了解內(nèi)核錯(cuò)誤原因、定位問題根源的關(guān)鍵。

1.2引發(fā)原因

(1)非法內(nèi)存訪問

這是引發(fā) Oops 最為常見的原因之一。當(dāng)內(nèi)核代碼試圖訪問未被映射到物理內(nèi)存的虛擬地址,或者訪問權(quán)限不足的內(nèi)存區(qū)域時(shí),就會(huì)觸發(fā)非法內(nèi)存訪問錯(cuò)誤。例如,在驅(qū)動(dòng)程序開發(fā)中,如果對設(shè)備內(nèi)存的映射和訪問操作不當(dāng),就很容易出現(xiàn)這種問題。假設(shè)我們正在編寫一個(gè)硬件驅(qū)動(dòng)程序,需要與特定的硬件設(shè)備進(jìn)行交互。在訪問設(shè)備的寄存器時(shí),錯(cuò)誤地計(jì)算了寄存器的地址,導(dǎo)致訪問了一個(gè)非法的內(nèi)存地址,這時(shí)就極有可能引發(fā) Oops 錯(cuò)誤。

(2)空指針引用

當(dāng)內(nèi)核代碼試圖解引用一個(gè)空指針時(shí),空指針引用錯(cuò)誤便會(huì)發(fā)生。這通常是由于代碼邏輯錯(cuò)誤,在使用指針之前沒有對其進(jìn)行有效的初始化或檢查。比如,在一個(gè)鏈表操作的內(nèi)核模塊中,當(dāng)遍歷鏈表時(shí),如果沒有正確判斷鏈表節(jié)點(diǎn)指針是否為空,就嘗試訪問節(jié)點(diǎn)的數(shù)據(jù)成員,一旦指針為空,就會(huì)觸發(fā) Oops。具體來說,假設(shè)有如下鏈表節(jié)點(diǎn)定義和遍歷代碼:

struct list_node {
    int data;
    struct list_node *next;
};

void traverse_list(struct list_node *head) {
    struct list_node *current = head;
    while (current != NULL) {
        // 錯(cuò)誤示范:沒有檢查current是否為空就訪問其成員
        printk(KERN_INFO "Data: %d\n", current->data); 
        current = current->next;
    }
}

在上述代碼中,如果head指針為空,或者在遍歷過程中current指針意外變?yōu)榭?,就?huì)發(fā)生空指針引用,進(jìn)而導(dǎo)致 Oops。

(3)內(nèi)核模塊錯(cuò)誤

內(nèi)核模塊作為可動(dòng)態(tài)加載到內(nèi)核中的代碼,若其中存在編程錯(cuò)誤,也常常會(huì)引發(fā) Oops。例如,模塊在初始化或卸載過程中,如果沒有正確處理資源的分配和釋放,就可能留下隱患。曾經(jīng)在開發(fā)一個(gè)網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)模塊時(shí),在模塊初始化函數(shù)中申請了內(nèi)存資源,但在卸載函數(shù)中卻忘記釋放這些內(nèi)存,當(dāng)多次加載和卸載該模塊后,系統(tǒng)的內(nèi)存管理就出現(xiàn)了混亂,最終引發(fā)了 Oops 錯(cuò)誤 。此外,模塊之間的兼容性問題也可能導(dǎo)致 Oops,比如不同模塊對同一內(nèi)核數(shù)據(jù)結(jié)構(gòu)的訪問和修改方式不一致,就容易引發(fā)沖突。

二、調(diào)試前的關(guān)鍵準(zhǔn)備

在調(diào)試一個(gè) bug 之前,我們所要做的準(zhǔn)備工作有:

  1. 有一個(gè)被確認(rèn)的 bug。
  2. 包含這個(gè) bug 的內(nèi)核版本號(hào),需要分析出這個(gè) bug 在哪一個(gè)版本被引入,這個(gè)對于解決問題有極大的幫助。可以采用二分查找法來逐步鎖定 bug 引入版本號(hào)。
  3. 對內(nèi)核代碼理解越深刻越好,同時(shí)還需要一點(diǎn)點(diǎn)運(yùn)氣。
  4. 該 bug 可以復(fù)現(xiàn)。如果能夠找到復(fù)現(xiàn)規(guī)律,那么離找到問題的原因就不遠(yuǎn)了。
  5. 最小化系統(tǒng)。把可能產(chǎn)生 bug 的因素逐一排除掉。

2.1確認(rèn)并定位 bug

在著手調(diào)試之前,首先要明確存在的問題,即確認(rèn)并定位 bug。確定一個(gè)被確認(rèn)的 bug 是調(diào)試的基礎(chǔ),只有明確了問題所在,才能有針對性地進(jìn)行后續(xù)的調(diào)試工作。同時(shí),獲取包含這個(gè) bug 的內(nèi)核版本號(hào)也至關(guān)重要,它能幫助我們快速定位問題出現(xiàn)的范圍。例如,在某個(gè)項(xiàng)目中,我發(fā)現(xiàn)系統(tǒng)在加載特定內(nèi)核模塊時(shí)出現(xiàn) Oops 錯(cuò)誤,通過查看系統(tǒng)日志,確定了問題出現(xiàn)的內(nèi)核版本號(hào)為 5.10.10。

若能進(jìn)一步分析出這個(gè) bug 在哪一個(gè)版本被引入,對于解決問題更是大有裨益。這里可以采用二分查找法來逐步鎖定 bug 引入版本號(hào)。假設(shè)我們懷疑某個(gè)問題是在 2.6.11 到 2.6.20 這一系列內(nèi)核版本中引入的,我們可以先從中間版本 2.6.15 開始檢查 。如果在 2.6.15 版本中沒有發(fā)現(xiàn)問題,那就說明錯(cuò)誤是在 2.6.15 之后的版本引入的;接下來,我們可以在 2.6.15 和 2.6.20 的中間版本(如 2.6.17)繼續(xù)檢查。

反之,如果在 2.6.15 版本中出現(xiàn)了問題,那就說明錯(cuò)誤是在 2.6.15 之前的版本引入的,我們就需要檢查 2.6.13 版本。通過不斷重復(fù)這樣的篩選過程,最終就能將問題鎖定在兩個(gè)相繼發(fā)行的版本之間,從而更容易對引發(fā)這個(gè) bug 的代碼變更進(jìn)行定位。

2.2環(huán)境搭建

搭建一個(gè)完備的調(diào)試環(huán)境是進(jìn)行 Linux 內(nèi)核 Oops 調(diào)試的基礎(chǔ),它為我們提供了必要的工具和條件,使得調(diào)試工作能夠順利進(jìn)行。在這個(gè)過程中,需要安裝和配置一系列的工具,這些工具相互協(xié)作,共同助力我們解決內(nèi)核 Oops 問題。

GCC(GNU Compiler Collection)作為一款強(qiáng)大的編譯器,是編譯內(nèi)核和內(nèi)核模塊必不可少的工具。以 Ubuntu 系統(tǒng)為例,在終端中輸入命令 “sudo apt-get install build-essential”,即可輕松完成 GCC 的安裝。這行命令會(huì)自動(dòng)下載并安裝 GCC 以及相關(guān)的編譯依賴庫,確保 GCC 能夠正常工作。安裝完成后,我們可以通過 “gcc -v” 命令來查看 GCC 的版本信息,驗(yàn)證是否安裝成功。

GDB(GNU Debugger)則是調(diào)試的核心工具,它允許我們在內(nèi)核運(yùn)行時(shí)進(jìn)行單步執(zhí)行、設(shè)置斷點(diǎn)、查看變量值等操作,幫助我們深入了解內(nèi)核的運(yùn)行狀態(tài),從而找到問題的根源。在 Ubuntu 系統(tǒng)上,同樣可以使用 “sudo apt-get install gdb” 命令進(jìn)行安裝。安裝完成后,在調(diào)試時(shí),我們可以使用 “gdb vmlinux” 命令來加載內(nèi)核符號(hào)表,這里的 “vmlinux” 是內(nèi)核的可執(zhí)行文件,加載符號(hào)表后,GDB 就能準(zhǔn)確地定位到內(nèi)核代碼中的具體位置,為調(diào)試提供極大的便利。

make 工具在構(gòu)建內(nèi)核和內(nèi)核模塊時(shí)發(fā)揮著重要作用,它能夠根據(jù) Makefile 文件中的規(guī)則,自動(dòng)編譯和鏈接源代碼,生成可執(zhí)行文件或模塊。安裝 make 同樣很簡單,在 Ubuntu 系統(tǒng)中,執(zhí)行 “sudo apt-get install make” 即可。安裝完成后,我們可以通過 “make -v” 命令查看 make 的版本,確認(rèn)安裝無誤。

除了上述工具,還需要安裝一些與內(nèi)核調(diào)試相關(guān)的依賴包,如 libncurses5-dev、bison、flex、libssl-dev、libelf-dev 等。這些依賴包提供了內(nèi)核編譯和調(diào)試所需的各種庫和工具。在 Ubuntu 系統(tǒng)中,可以使用 “sudo apt-get install libncurses5-dev bison flex libssl-dev libelf-dev” 命令一次性安裝多個(gè)依賴包,確保調(diào)試環(huán)境的完整性。

2.3內(nèi)核配置優(yōu)化

為了更有效地進(jìn)行內(nèi)核調(diào)試,對內(nèi)核配置進(jìn)行優(yōu)化是關(guān)鍵步驟。通過 make menuconfig 命令,我們可以進(jìn)入內(nèi)核配置界面,這是一個(gè)基于文本的交互式界面,類似于一個(gè)菜單樹,我們可以通過上下左右鍵進(jìn)行選擇和操作。

在這個(gè)界面中,開啟 Magic SysRq key 選項(xiàng)尤為重要。Magic SysRq key 是一個(gè)強(qiáng)大的系統(tǒng)請求鍵,它可以在系統(tǒng)出現(xiàn)問題時(shí),通過組合鍵的方式向內(nèi)核發(fā)送特定的命令,獲取系統(tǒng)的關(guān)鍵信息,如內(nèi)存使用情況、任務(wù)列表等,為調(diào)試提供重要線索。例如,當(dāng)系統(tǒng)出現(xiàn)死機(jī)等異常情況時(shí),我們可以按下 Alt + SysRq + m 組合鍵,內(nèi)核會(huì)將內(nèi)存信息輸出到控制臺(tái),幫助我們分析內(nèi)存使用是否存在問題。

Kernel debugging 選項(xiàng)的開啟也不可或缺,它會(huì)在內(nèi)核中添加大量的調(diào)試信息,使得我們在調(diào)試時(shí)能夠獲取更詳細(xì)的內(nèi)核運(yùn)行狀態(tài)信息。比如,開啟該選項(xiàng)后,內(nèi)核在出現(xiàn) Oops 錯(cuò)誤時(shí),會(huì)輸出更多關(guān)于錯(cuò)誤發(fā)生時(shí)的上下文信息,包括寄存器的值、函數(shù)調(diào)用棧等,這些信息對于準(zhǔn)確分析錯(cuò)誤原因至關(guān)重要。

此外,還有一些其他的調(diào)試相關(guān)選項(xiàng)也可以根據(jù)具體需求開啟,如 Debug slab memory allocations 用于調(diào)試內(nèi)存分配問題,Spinlock and rw-lock debugging: basic checks 用于檢查自旋鎖和讀寫鎖的基本問題等。這些選項(xiàng)就像是調(diào)試過程中的得力助手,能夠幫助我們從不同角度發(fā)現(xiàn)和解決內(nèi)核中的問題。

三、內(nèi)核異常詳解

3.1BUG() —開發(fā)者觸發(fā)的邏輯錯(cuò)誤

BUG 是指那些不符合內(nèi)核的正常設(shè)計(jì),但內(nèi)核能夠檢測出來并且對系統(tǒng)運(yùn)行不會(huì)產(chǎn)生影響的問題,比如在原子上下文中休眠,在內(nèi)核中用 BUG 標(biāo)識(shí)。

有過驅(qū)動(dòng)調(diào)試經(jīng)驗(yàn)的人肯定都知道這個(gè)東西,這里的 BUG 跟我們一般認(rèn)為的 “軟件缺陷” 可不是一回事,這里說的 BUG() 其實(shí)是linux kernel中用于攔截內(nèi)核程序超出預(yù)期的行為,屬于軟件主動(dòng)匯報(bào)異常的一種機(jī)制。這里有個(gè)疑問,就是什么時(shí)候會(huì)用到呢?一般來說有兩種用到的情況:

  • 一是軟件開發(fā)過程中,若發(fā)現(xiàn)代碼邏輯出現(xiàn)致命 fault 后就可以調(diào)用BUG()讓kernel死掉(類似于assert),這樣方便于定位問題,從而修正代碼執(zhí)行邏輯;
  • 另外一種情況就是,由于某種特殊原因(通常是為了debug而需抓ramdump),我們需要系統(tǒng)進(jìn)入kernel panic的情況下使用;

對于 arm64 來說 BUG() 定義如下:

arch/arm64/include/asm/bug.h
#ifndef _ARCH_ARM64_ASM_BUG_H
#define _ARCH_ARM64_ASM_BUG_H
#include <linux/stringify.h>
#include <asm/asm-bug.h>
#define __BUG_FLAGS(flags)				\
	asm volatile (__stringify(ASM_BUG_FLAGS(flags)));

#define BUG() do {					\
	__BUG_FLAGS(0);					\
	unreachable();					\
} while (0)
#define __WARN_FLAGS(flags) __BUG_FLAGS(BUGFLAG_WARNING|(flags))
#define HAVE_ARCH_BUG
#include <asm-generic/bug.h>
#endif /* ! _ARCH_ARM64_ASM_BUG_H */

注意最后的 define HAVE_ARCH_BUG ,對于arm64 架構(gòu)來說,會(huì)通過 include asm-generict/bug.h對 BUG() 進(jìn)行重定義。

include/asm-generic/bug.h

#ifndef HAVE_ARCH_BUG
#define BUG() do { \
	printk("BUG: failure at %s:%d/%s()!\n", __FILE__, __LINE__, __func__); \
	barrier_before_unreachable(); \
	panic("BUG!"); \
} while (0)
#endif

#ifndef HAVE_ARCH_BUG_ON
#define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while (0)
#endif

也就是在 arm64 架構(gòu)中 BUG() 和 BUG_ON() 都是執(zhí)行的 panic()。而對于 arm 32位架構(gòu)來說,BUG() 會(huì)向CPU 下發(fā)一條未定義指令而觸發(fā)ARM 發(fā)起未定義指令異常,隨后進(jìn)入 kernel 異常處理流程,通過調(diào)用die() 經(jīng)歷Oops 和 panic。

3.2OOPS —錯(cuò)誤報(bào)告框架

Oops 就意外著內(nèi)核出了異常,此時(shí)會(huì)將產(chǎn)生異常時(shí)出錯(cuò)原因,CPU的狀態(tài),出錯(cuò)的指令地址、數(shù)據(jù)地址及其他寄存器,函數(shù)調(diào)用的順序甚至是棧里面的內(nèi)容都打印出來,然后根據(jù)異常的嚴(yán)重程度來決定下一步的操作:殺死導(dǎo)致異常的進(jìn)程或者掛起系統(tǒng)。

例如,在編寫驅(qū)動(dòng)或內(nèi)核模塊時(shí),常常會(huì)顯示或隱式地對指針進(jìn)行非法取值或使用不正確的指針,導(dǎo)致內(nèi)核發(fā)生一個(gè) oops 錯(cuò)誤。當(dāng)處理器在內(nèi)核空間中訪問一個(gè)分發(fā)的指針時(shí),因?yàn)樘摂M地址到物理地址的映射關(guān)系還沒有建立,會(huì)觸發(fā)一個(gè)缺頁中斷,在缺頁中斷中該地址是非法的,內(nèi)核無法正確地為該地址建立映射關(guān)系,所以內(nèi)核觸發(fā)一個(gè)oops 錯(cuò)誤。代碼如下:

arch/arm64/mm/fault.c
static void die_kernel_fault(const char *msg, unsigned long addr,
			     unsigned int esr, struct pt_regs *regs)
{
	bust_spinlocks(1);

	pr_alert("Unable to handle kernel %s at virtual address %016lx\n", msg,
		 addr);

	mem_abort_decode(esr);

	show_pte(addr);
	die("Oops", regs, esr);
	bust_spinlocks(0);
	do_exit(SIGKILL);
}

通過 die() 會(huì)進(jìn)行oops 異常處理,詳細(xì)的 die() 函數(shù)流程看第 3 節(jié)。當(dāng)出現(xiàn) oops,并且如果有源碼,可以通過 arm 的 arch64-linux-gnu-objdump 工具看到出錯(cuò)的函數(shù)的匯編情況,也可以通過 GDB 工具分析。如果出錯(cuò)的地方為內(nèi)核函數(shù),可以使用 vmlinux 文件。

如果沒有源碼,對于沒有編譯符號(hào)表的二進(jìn)制文件,可以使用:

arch64-linux-gnu-objdump -d oops.ko

命令來轉(zhuǎn)儲(chǔ) oops.ko 文件內(nèi)核也提供了一個(gè)非常好用的腳本,可以快速定位問題,該腳本位于 Linux 源碼目錄下的 scripts/decodecode 中,會(huì)把出錯(cuò)的 oops 日志信息轉(zhuǎn)換成直觀有用的匯編代碼,并且告知具體出錯(cuò)的匯編語句,這對于分析沒有源碼的 oops 錯(cuò)誤非常有用。

3.3die() — 硬件異常處理函數(shù)

arch/arm64/kernel/traps.c

static DEFINE_RAW_SPINLOCK(die_lock);

/*
 * This function is protected against re-entrancy.
 */
void die(const char *str, struct pt_regs *regs, int err)
{
	int ret;
	unsigned long flags;

	raw_spin_lock_irqsave(&die_lock, flags);

	oops_enter();

	console_verbose();
	bust_spinlocks(1);
	ret = __die(str, err, regs);

	if (regs && kexec_should_crash(current))
		crash_kexec(regs);

	bust_spinlocks(0);
	add_taint(TAINT_DIE, LOCKDEP_NOW_UNRELIABLE);
	oops_exit();

	if (in_interrupt())
		panic("Fatal exception in interrupt");
	if (panic_on_oops)
		panic("Fatal exception");

	raw_spin_unlock_irqrestore(&die_lock, flags);

	if (ret != NOTIFY_STOP)
		do_exit(SIGSEGV);
}

oops_enter() ---> oops_exit() 為Oops 的處理流程,獲取console 的log 級(jí)別,并通過 __die() 通過對Oops 感興趣的模塊進(jìn)行callback,打印模塊狀態(tài)不為 MODULE_STATE_UNFORMED 的模塊信息,打印PC、LR、SP、x0 等寄存器信息,打印調(diào)用棧信息,等等。

(1)__die()

arch/arm64/kernel/traps.c

static int __die(const char *str, int err, struct pt_regs *regs)
{
	static int die_counter;
	int ret;

	pr_emerg("Internal error: %s: %x [#%d]" S_PREEMPT S_SMP "\n",
		 str, err, ++die_counter);

	/* trap and error numbers are mostly meaningless on ARM */
	ret = notify_die(DIE_OOPS, str, regs, err, 0, SIGSEGV);
	if (ret == NOTIFY_STOP)
		return ret;

	print_modules();
	show_regs(regs);

	dump_kernel_instr(KERN_EMERG, regs);

	return ret;
}

打印 EMERG 的log,Internal error: oops.....;

  • notify_die() 會(huì)通知所有對 Oops 感興趣的模塊并進(jìn)行callback;
  • print_modules() 打印模塊狀態(tài)不為 MODULE_STATE_UNFORMED 的模塊信息;
  • show_regs() 打印PC、LR、SP 等寄存器的信息,同時(shí)打印調(diào)用堆棧信息;
  • dump_kernel_instr() 打印 pc指針和前4條指令;

這里不過多的剖析,感興趣的可以查看下源碼。這里需要注意的是 notify_die() 會(huì)通知所有的Oops 感興趣的模塊,模塊會(huì)通過函數(shù) register_die_notifier() 將callback 注冊到全局結(jié)構(gòu)體變量 die_chain 中(多個(gè)模塊注冊進(jìn)來形成一個(gè)鏈表),然后在通過 notify_die() 函數(shù)去解析這個(gè) die_chain,并分別調(diào)用callback:

kernel/notifier.c

static ATOMIC_NOTIFIER_HEAD(die_chain);

int notrace notify_die(enum die_val val, const char *str,
	       struct pt_regs *regs, long err, int trap, int sig)
{
	struct die_args args = {
		.regs	= regs,
		.str	= str,
		.err	= err,
		.trapnr	= trap,
		.signr	= sig,

	};
	RCU_LOCKDEP_WARN(!rcu_is_watching(),
			   "notify_die called but RCU thinks we're quiescent");
	return atomic_notifier_call_chain(&die_chain, val, &args);
}
NOKPROBE_SYMBOL(notify_die);

int register_die_notifier(struct notifier_block *nb)
{
	vmalloc_sync_mappings();
	return atomic_notifier_chain_register(&die_chain, nb);
}

(2)oops同時(shí)有可能panic

從上面 die() 函數(shù)最后看到,oops_exit() 之后也有可能進(jìn)入panic():

arch/arm64/kernel/traps.c

void die(const char *str, struct pt_regs *regs, int err)
{
    ...

	if (in_interrupt())
		panic("Fatal exception in interrupt");
	if (panic_on_oops)
		panic("Fatal exception");
    ...
}

處于中斷或panic_on_oops 打開時(shí)進(jìn)入 panic。

中斷的可能性:

  • 硬件 IRQ;
  • 軟件 IRQ;
  • NMI;

panic_on_oops 的值受 CONFIG_PANIC_ON_OOPS_VALUE 影響。當(dāng)然該值也可以通過節(jié)點(diǎn)/proc/sys/kernel/panic_on_oops 進(jìn)行動(dòng)態(tài)修改。

3.4panic() —系統(tǒng)終止函數(shù)

panic 本意是“恐慌”的意思,這里意旨 kernel 發(fā)生了致命錯(cuò)誤導(dǎo)致無法繼續(xù)運(yùn)行下去的情況。根據(jù)實(shí)際情況 Oops最終也可能會(huì)導(dǎo)致panic 的發(fā)生。

kernel/panic.c

/**
 *	panic - halt the system
 *	@fmt: The text string to print
 *
 *	Display a message, then perform cleanups.
 *
 *	This function never returns.
 */
void panic(const char *fmt, ...)
{
	static char buf[1024];
	va_list args;
	long i, i_next = 0, len;
	int state = 0;
	int old_cpu, this_cpu;
	bool _crash_kexec_post_notifiers = crash_kexec_post_notifiers;


    //禁止本地中斷,避免出現(xiàn)死鎖,因?yàn)闊o法防止中斷處理程序(在獲得panic鎖后運(yùn)行)再次被調(diào)用panic
	local_irq_disable();
    //禁止任務(wù)搶占
	preempt_disable_notrace();

    //通過this_cpu確認(rèn)是否調(diào)用panic() 的cpu是否為panic_cpu;
	//即,只允許一個(gè)CPU執(zhí)行該代碼,通過 panic_smp_self_stop() 保證當(dāng)一個(gè)CPU執(zhí)行panic時(shí),
    //其他CPU處于停止或等待狀態(tài);
	this_cpu = raw_smp_processor_id();
	old_cpu  = atomic_cmpxchg(&panic_cpu, PANIC_CPU_INVALID, this_cpu);

	if (old_cpu != PANIC_CPU_INVALID && old_cpu != this_cpu)
		panic_smp_self_stop();

    //把console的打印級(jí)別放開
	console_verbose();
	bust_spinlocks(1);
	va_start(args, fmt);
	len = vscnprintf(buf, sizeof(buf), fmt, args);
	va_end(args);

	if (len && buf[len - 1] == '\n')
		buf[len - 1] = '\0';

    //解析panic所攜帶的message,前綴為Kernel panic - not syncing
	pr_emerg("Kernel panic - not syncing: %s\n", buf);
#ifdef CONFIG_DEBUG_BUGVERBOSE
	/*
	 * Avoid nested stack-dumping if a panic occurs during oops processing
	 */
	if (!test_taint(TAINT_DIE) && oops_in_progress <= 1)
		dump_stack();
#endif

    //如果kgdb使能,即CONFIG_KGDB為y,在停掉所有其他CPU之前,跳轉(zhuǎn)kgdb斷點(diǎn)運(yùn)行
	kgdb_panic(buf);

	if (!_crash_kexec_post_notifiers) {
		printk_safe_flush_on_panic();
        //會(huì)根據(jù)當(dāng)前是否設(shè)置了轉(zhuǎn)儲(chǔ)內(nèi)核(使能CONFIG_KEXEC_CORE)確定是否實(shí)際執(zhí)行轉(zhuǎn)儲(chǔ)操作;
        //如果執(zhí)行轉(zhuǎn)儲(chǔ)則會(huì)通過 kexec 將系統(tǒng)切換到新的kdump 內(nèi)核,并且不會(huì)再返回;
        //如果不執(zhí)行轉(zhuǎn)儲(chǔ),則繼續(xù)后面流程;
		__crash_kexec(NULL);

		//停掉其他CPU,只留下當(dāng)前CPU干活
		smp_send_stop();
	} else {
		/*
		 * If we want to do crash dump after notifier calls and
		 * kmsg_dump, we will need architecture dependent extra
		 * works in addition to stopping other CPUs.
		 */
		crash_smp_send_stop();
	}

    //通知所有對panic感興趣的模塊進(jìn)行回調(diào),添加一些kmsg信息到輸出
	atomic_notifier_call_chain(&panic_notifier_list, 0, buf);

	/* Call flush even twice. It tries harder with a single online CPU */
	printk_safe_flush_on_panic();

    //dump 內(nèi)核log buffer中的log信息
	kmsg_dump(KMSG_DUMP_PANIC);

	/*
	 * If you doubt kdump always works fine in any situation,
	 * "crash_kexec_post_notifiers" offers you a chance to run
	 * panic_notifiers and dumping kmsg before kdump.
	 * Note: since some panic_notifiers can make crashed kernel
	 * more unstable, it can increase risks of the kdump failure too.
	 *
	 * Bypass the panic_cpu check and call __crash_kexec directly.
	 */
	if (_crash_kexec_post_notifiers)
		__crash_kexec(NULL);

#ifdef CONFIG_VT
	unblank_screen();
#endif
	console_unblank();

    //關(guān)掉所有debug鎖
	debug_locks_off();
	console_flush_on_panic(CONSOLE_FLUSH_PENDING);

	panic_print_sys_info();

	if (!panic_blink)
		panic_blink = no_blink;

    //如果sysctl配置了panic_timeout > 0則在panic_timeout后重啟系統(tǒng)
    //首先,這里會(huì)每隔100ms重啟 NMI watchdog
	if (panic_timeout > 0) {
		/*
		 * Delay timeout seconds before rebooting the machine.
		 * We can't use the "normal" timers since we just panicked.
		 */
		pr_emerg("Rebooting in %d seconds..\n", panic_timeout);

		for (i = 0; i < panic_timeout * 1000; i += PANIC_TIMER_STEP) {
			touch_nmi_watchdog();
			if (i >= i_next) {
				i += panic_blink(state ^= 1);
				i_next = i + 3600 / PANIC_BLINK_SPD;
			}
			mdelay(PANIC_TIMER_STEP);
		}
	}
    //其次,這里確定reboot_mode,并重啟系統(tǒng)
	if (panic_timeout != 0) {
		/*
		 * This will not be a clean reboot, with everything
		 * shutting down.  But if there is a chance of
		 * rebooting the system it will be rebooted.
		 */
		if (panic_reboot_mode != REBOOT_UNDEFINED)
			reboot_mode = panic_reboot_mode;
		emergency_restart();
	}
#ifdef __sparc__
	{
		extern int stop_a_enabled;
		/* Make sure the user can actually press Stop-A (L1-A) */
		stop_a_enabled = 1;
		pr_emerg("Press Stop-A (L1-A) from sun keyboard or send break\n"
			 "twice on console to return to the boot prom\n");
	}
#endif
#if defined(CONFIG_S390)
	disabled_wait();
#endif
	pr_emerg("---[ end Kernel panic - not syncing: %s ]---\n", buf);

	/* Do not scroll important messages printed above */
	suppress_printk = 1;
	local_irq_enable();
	for (i = 0; ; i += PANIC_TIMER_STEP) {
		touch_softlockup_watchdog();
		if (i >= i_next) {
			i += panic_blink(state ^= 1);
			i_next = i + 3600 / PANIC_BLINK_SPD;
		}
		mdelay(PANIC_TIMER_STEP);
	}
}

EXPORT_SYMBOL(panic);

詳細(xì)信息見代碼注釋。panic_timeout 是根據(jù)節(jié)點(diǎn) /proc/sys/kernel/panic 值配置,用以指定在重啟系統(tǒng)之前需要 wait 的時(shí)長。

(1)panic_print_sys_info()

kernel/panic.c

#define PANIC_PRINT_TASK_INFO		0x00000001
#define PANIC_PRINT_MEM_INFO		0x00000002
#define PANIC_PRINT_TIMER_INFO		0x00000004
#define PANIC_PRINT_LOCK_INFO		0x00000008
#define PANIC_PRINT_FTRACE_INFO		0x00000010
#define PANIC_PRINT_ALL_PRINTK_MSG	0x00000020

static void panic_print_sys_info(void)
{
	if (panic_print & PANIC_PRINT_ALL_PRINTK_MSG)
		console_flush_on_panic(CONSOLE_REPLAY_ALL);

	if (panic_print & PANIC_PRINT_TASK_INFO)
		show_state();

	if (panic_print & PANIC_PRINT_MEM_INFO)
		show_mem(0, NULL);

	if (panic_print & PANIC_PRINT_TIMER_INFO)
		sysrq_timer_list_show();

	if (panic_print & PANIC_PRINT_LOCK_INFO)
		debug_show_all_locks();

	if (panic_print & PANIC_PRINT_FTRACE_INFO)
		ftrace_dump(DUMP_ALL);
}

panic_print 默認(rèn)值為 0,可以通過 /proc/sys/kernel/panic_print 節(jié)點(diǎn)配置,當(dāng) panic 發(fā)生的時(shí)候,用戶可以通過如下bit 位配置打印系統(tǒng)信息:

  • bit 0:打印所有的進(jìn)程信息;
  • bit 1:打印系統(tǒng)內(nèi)存信息;
  • bit 2:打印定時(shí)器信息;
  • bit 3:打印當(dāng) CONFIG_LOCKEDP 打開時(shí)的鎖信息;
  • bit 4:打印所有 ftrace;
  • bit 5:打印串口所有信息;

四、內(nèi)核調(diào)試配置選項(xiàng)

學(xué)習(xí)編寫驅(qū)動(dòng)程序要構(gòu)建安裝自己的內(nèi)核(標(biāo)準(zhǔn)主線內(nèi)核)。最重要的原因之一是:內(nèi)核開發(fā)者已經(jīng)建立了多項(xiàng)用于調(diào)試的功能。但是由于這些功能會(huì)造成額外的輸出,并導(dǎo)致能下降,因此發(fā)行版廠商通常會(huì)禁止發(fā)行版內(nèi)核中的調(diào)試功能。

4.1內(nèi)核配置

為了實(shí)現(xiàn)內(nèi)核調(diào)試,在內(nèi)核配置上增加了幾項(xiàng):

Kernel hacking  --->

啟用選項(xiàng)例如:

slab layer debugging(slab層調(diào)試選項(xiàng))

4.2調(diào)試原子操作

從內(nèi)核 2.5 開發(fā),為了檢查各類由原子操作引發(fā)的問題,內(nèi)核提供了極佳的工具。內(nèi)核提供了一個(gè)原子操作計(jì)數(shù)器,它可以配置成,一旦在原子操作過程中,進(jìn)城進(jìn)入睡眠或者做了一些可能引起睡眠的操作,就打印警告信息并提供追蹤線索。所以,包括在使用鎖的時(shí)候調(diào)用 schedule (),正使用鎖的時(shí)候以阻塞方式請求分配內(nèi)存等,各種潛在的 bug 都能夠被探測到。

下面這些選項(xiàng)可以最大限度地利用該特性:

CONFIG_PREEMPT = y

五、核心調(diào)試方法

當(dāng) Linux 內(nèi)核出現(xiàn) Oops 錯(cuò)誤時(shí),掌握有效的調(diào)試方法至關(guān)重要。接下來,我們將詳細(xì)介紹幾種核心調(diào)試方法,這些方法在定位和解決 Oops 問題時(shí)非常實(shí)用。

5.1 printk 函數(shù)運(yùn)用

printk 堪稱 Linux 內(nèi)核中的 “萬能調(diào)試助手”,它擁有強(qiáng)大的健壯性。無論在內(nèi)核的中斷上下文還是進(jìn)程上下文,printk 都能穩(wěn)定地發(fā)揮作用。這意味著,當(dāng)內(nèi)核在處理緊急的中斷事件,或者在正常的進(jìn)程執(zhí)行流程中出現(xiàn)問題時(shí),我們都可以借助 printk 輸出關(guān)鍵的調(diào)試信息。它還可以在任何持有鎖時(shí)被調(diào)用,并且能夠在多處理器環(huán)境下同時(shí)被調(diào)用,無需額外的鎖機(jī)制來保證線程安全 。不過,在系統(tǒng)功能啟動(dòng)的初期,終端還未完成初始化時(shí),printk 存在一定的局限性,此時(shí)它無法正常工作。

printk ()內(nèi)核提供的格式化打印函數(shù);健壯性是 printk 最容易被接受的一個(gè)特質(zhì),幾乎在任何地方,任何時(shí)候內(nèi)核都可以調(diào)用它(中斷上下文、進(jìn)程上下文、持有鎖時(shí)、多處理器處理時(shí)等)。

printk 支持 8 種不同的日志級(jí)別,從高到低依次為:

  • KERN_EMERG(0),表示系統(tǒng)不可用,是最為緊急的情況,比如系統(tǒng)硬件出現(xiàn)嚴(yán)重故障,導(dǎo)致系統(tǒng)無法繼續(xù)運(yùn)行;
  • KERN_ALERT(1),意味著必須立即采取行動(dòng),通常用于報(bào)告那些可能導(dǎo)致系統(tǒng)崩潰或嚴(yán)重影響系統(tǒng)運(yùn)行的問題;
  • KERN_CRIT(2),代表嚴(yán)重情況,如硬盤故障、內(nèi)存不足等;
  • KERN_ERR(3),表示錯(cuò)誤情況,用于輸出一般性的錯(cuò)誤信息,幫助開發(fā)者定位代碼中的錯(cuò)誤;
  • KERN_WARNING(4),即警告情況,提示一些可能會(huì)引發(fā)問題的潛在風(fēng)險(xiǎn),但系統(tǒng)仍可繼續(xù)運(yùn)行;
  • KERN_NOTICE(5),表示正常但重要的情況,用于記錄一些需要關(guān)注的系統(tǒng)狀態(tài)變化;
  • KERN_INFO(6),提供一般信息,如系統(tǒng)啟動(dòng)過程中的一些關(guān)鍵步驟、設(shè)備驅(qū)動(dòng)的加載信息等;
  • KERN_DEBUG(7),用于調(diào)試信息,在開發(fā)和調(diào)試階段,通過輸出大量詳細(xì)的調(diào)試信息,幫助開發(fā)者深入了解內(nèi)核的運(yùn)行狀態(tài) 。這些日志級(jí)別可以通過修改 /proc/sys/kernel/printk 文件來調(diào)整輸出級(jí)別。例如,當(dāng)我們將該文件中的第一個(gè)數(shù)字設(shè)置為 7 時(shí),意味著只有日志級(jí)別小于等于 7(即 KERN_DEBUG 及以上級(jí)別)的信息才會(huì)被輸出,這樣可以在調(diào)試時(shí)獲取更詳細(xì)的信息。而在正式發(fā)布的系統(tǒng)中,通常會(huì)將該值設(shè)置為較低的數(shù)字,如 4,以減少不必要的日志輸出,提高系統(tǒng)性能。

在系統(tǒng)啟動(dòng)過程中,終端初始化之前,在某些地方是不能調(diào)用的。如果真的需要調(diào)試系統(tǒng)啟動(dòng)過程最開始的地方,有以下方法可以使用:

  • 使用串口調(diào)試,將調(diào)試信息輸出到其他終端設(shè)備。
  • 使用 early_printk (),該函數(shù)在系統(tǒng)啟動(dòng)初期就有打印能力。但它只支持部分硬件體系。

printk 和 printf 一個(gè)主要的區(qū)別就是前者可以指定一個(gè) LOG 等級(jí)。內(nèi)核根據(jù)這個(gè)等級(jí)來判斷是否在終端上打印消息。內(nèi)核把比指定等級(jí)高的所有消息顯示在終端。

可以使用下面的方式指定一個(gè) LOG 級(jí)別:printk(KERN_CRIT “Hello, world!\n”); 注意,第一個(gè)參數(shù)并不一個(gè)真正的參數(shù),因?yàn)槠渲袥]有用于分隔級(jí)別(KERN_CRIT)和格式字符的逗號(hào)(,)。KERN_CRIT 本身只是一個(gè)普通的字符串(事實(shí)上,它表示的是字符串 "<2>";表 1 列出了完整的日志級(jí)別清單)。

作為預(yù)處理程序的一部分,C 會(huì)自動(dòng)地使用一個(gè)名為 字符串串聯(lián) 的功能將這兩個(gè)字符串組合在一起。組合的結(jié)果是將日志級(jí)別和用戶指定的格式字符串包含在一個(gè)字符串中。

內(nèi)核使用這個(gè)指定 LOG 級(jí)別與當(dāng)前終端 LOG 等級(jí) console_loglevel 來決定是不是向終端打印。下面是可使用的 LOG 等級(jí):

#define KERN_EMERG      "<0>"   /* system is unusable                            */
#define KERN_ALERT        "<1>"   /* action must be taken immediately     */ 
#define KERN_CRIT           "<2>"   /* critical conditions                                */
#define KERN_ERR            "<3>"   /* error conditions                                   */
#define KERN_WARNING  "<4>"   /* warning conditions                              */
#define KERN_NOTICE       "<5>"   /* normal but significant condition         */
#define KERN_INFO            "<6>"   /* informational                                       */
#define KERN_DEBUG        "<7>"   /* debug-level messages                       */
#define KERN_DEFAULT     "<d>"   /* Use the default kernel loglevel           */

注意,如果調(diào)用者未將日志級(jí)別提供給 printk,那么系統(tǒng)就會(huì)使用默認(rèn)值 KERN_WARNING "<4>"(表示只有 KERN_WARNING 級(jí)別以上的日志消息會(huì)被記錄)。由于默認(rèn)值存在變化,所以在使用時(shí)最好指定 LOG 級(jí)別。有 LOG 級(jí)別的一個(gè)好處就是我們可以選擇性的輸出 LOG。

比如平時(shí)我們只需要打印 KERN_WARNING 級(jí)別以上的關(guān)鍵性 LOG,但是調(diào)試的時(shí)候,我們可以選擇打印 KERN_DEBUG 等以上的詳細(xì) LOG。而這些都不需要我們修改代碼,只需要通過命令修改默認(rèn)日志輸出級(jí)別:

mtj@ubuntu :~$ cat /proc/sys/kernel/printk
4 4 1 7
mtj@ubuntu :~$ cat /proc/sys/kernel/printk_delay
0
mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit
5
mtj@ubuntu :~$ cat /proc/sys/kernel/printk_ratelimit_burst
10

第一項(xiàng)定義了 printk API 當(dāng)前使用的日志級(jí)別。這些日志級(jí)別表示了控制臺(tái)的日志級(jí)別、默認(rèn)消息日志級(jí)別、最小控制臺(tái)日志級(jí)別和默認(rèn)控制臺(tái)日志級(jí)別。printk_delay 值表示的是 printk 消息之間的延遲毫秒數(shù)(用于提高某些場景的可讀性)。

注意,這里它的值為 0,而它是不可以通過 /proc 設(shè)置的。printk_ratelimit 定義了消息之間允許的最小時(shí)間間隔(當(dāng)前定義為每 5 秒內(nèi)的某個(gè)內(nèi)核消息數(shù))。消息數(shù)量是由 printk_ratelimit_burst 定義的(當(dāng)前定義為 10)。

如果您擁有一個(gè)非正式內(nèi)核而又使用有帶寬限制的控制臺(tái)設(shè)備(如通過串口), 那么這非常有用。注意,在內(nèi)核中,速度限制是由調(diào)用者控制的,而不是在 printk 中實(shí)現(xiàn)的。

如果一個(gè) printk 用戶要求進(jìn)行速度限制,那么該用戶就需要調(diào)用 printk_ratelimit 函數(shù)。

內(nèi)核消息都被保存在一個(gè) LOG_BUF_LEN 大小的環(huán)形隊(duì)列中。關(guān)于 LOG_BUF_LEN 定義:

#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)

※ 變量 CONFIG_LOG_BUF_SHIFT 在內(nèi)核編譯時(shí)由配置文件定義,對于 i386 平臺(tái),其值定義如下(在 linux26/arch/i386/defconfig 中):

CONFIG_LOG_BUF_SHIFT=18

記錄緩沖區(qū)操作:① 消息被讀出到用戶空間時(shí),此消息就會(huì)從環(huán)形隊(duì)列中刪除。② 當(dāng)消息緩沖區(qū)滿時(shí),如果再有 printk () 調(diào)用時(shí),新消息將覆蓋隊(duì)列中的老消息。③ 在讀寫環(huán)形隊(duì)列時(shí),同步問題很容易得到解決。

※ 這個(gè)紀(jì)錄緩沖區(qū)之所以稱為環(huán)形,是因?yàn)樗淖x寫都是按照環(huán)形隊(duì)列的方式進(jìn)行操作的。

在標(biāo)準(zhǔn)的 Linux 系統(tǒng)上,用戶空間的守護(hù)進(jìn)程 klogd 從紀(jì)錄緩沖區(qū)中獲取內(nèi)核消息,再通過 syslogd 守護(hù)進(jìn)程把這些消息保存在系統(tǒng)日志文件中。klogd 進(jìn)程既可以從 /proc/kmsg 文件中,也可以通過 syslog () 系統(tǒng)調(diào)用讀取這些消息。默認(rèn)情況下,它選擇讀取 /proc 方式實(shí)現(xiàn)。klogd 守護(hù)進(jìn)程在消息緩沖區(qū)有新的消息之前,一直處于阻塞狀態(tài)。

一旦有新的內(nèi)核消息,klogd 被喚醒,讀出內(nèi)核消息并進(jìn)行處理。默認(rèn)情況下,處理例程就是把內(nèi)核消息傳給 syslogd 守護(hù)進(jìn)程。syslogd 守護(hù)進(jìn)程一般把接收到的消息寫入 /var/log/messages 文件中。不過,還是可以通過 /etc/syslog.conf 文件來進(jìn)行配置,可以選擇其他的輸出文件。

dmesg 命令也可用于打印和控制內(nèi)核環(huán)緩沖區(qū)。這個(gè)命令使用 klogctl 系統(tǒng)調(diào)用來讀取內(nèi)核環(huán)緩沖區(qū),并將它轉(zhuǎn)發(fā)到標(biāo)準(zhǔn)輸出(stdout)。這個(gè)命令也可以用來清除內(nèi)核環(huán)緩沖區(qū)(使用 -c 選項(xiàng)),設(shè)置控制臺(tái)日志級(jí)別(-n 選項(xiàng)),以及定義用于讀取內(nèi)核日志消息的緩沖區(qū)大?。?s 選項(xiàng))。注意,如果沒有指定緩沖區(qū)大小,那么 dmesg 會(huì)使用 klogctl 的 SYSLOG_ACTION_SIZE_BUFFER 操作確定緩沖區(qū)大小。

  • a) 雖然 printk 很健壯,但是看了源碼你就知道,這個(gè)函數(shù)的效率很低:做字符拷貝時(shí)一次只拷貝一個(gè)字節(jié),且去調(diào)用 console 輸出可能還產(chǎn)生中斷。所以如果你的驅(qū)動(dòng)在功能調(diào)試完成以后做性能測試或者發(fā)布的時(shí)候千萬記得盡量減少 printk 輸出,做到僅在出錯(cuò)時(shí)輸出少量信息。否則往 console 輸出無用信息影響性能。
  • b) printk 的臨時(shí)緩存 printk_buf 只有 1K,所有一次 printk 函數(shù)只能記錄 <1K 的信息到 log buffer,并且 printk 使用的 “ringbuffer”.

內(nèi)核 printk 和日志系統(tǒng)的總體結(jié)構(gòu):

動(dòng)態(tài)調(diào)試:

動(dòng)態(tài)調(diào)試是通過動(dòng)態(tài)的開啟和禁止某些內(nèi)核代碼來獲取額外的內(nèi)核信息。首先內(nèi)核選項(xiàng) CONFIG_DYNAMIC_DEBUG 應(yīng)該被設(shè)置。所有通過 pr_debug ()/dev_debug () 打印的信息都可以動(dòng)態(tài)的顯示或不顯示??梢酝ㄟ^簡單的查詢語句來篩選需要顯示的信息。

  • 源文件名
  • 函數(shù)名
  • 行號(hào)(包括指定范圍的行號(hào))
  • 模塊名
  • 格式化字符串

將要打印信息的格式寫入 /dynamic_debug/control 中。

nullarbor:~ # echo 'file svcsock.c line 1603 +p' >

在調(diào)試過程中,合理地在關(guān)鍵代碼處插入 printk 輸出調(diào)試信息是非常有效的方法。比如,在一個(gè)網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)程序中,當(dāng)我們懷疑數(shù)據(jù)包的接收處理過程存在問題時(shí),可以在接收函數(shù)的關(guān)鍵步驟處插入 printk 語句,輸出數(shù)據(jù)包的相關(guān)信息,如數(shù)據(jù)包的長度、源地址、目的地址等。假設(shè)我們有如下代碼:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/skbuff.h>

static int __init net_driver_init(void) {
    // 初始化相關(guān)變量和設(shè)備
    return 0;
}

static void __exit net_driver_exit(void) {
    // 釋放資源
}

module_init(net_driver_init);
module_exit(net_driver_exit);
MODULE_LICENSE("GPL");

// 假設(shè)這是數(shù)據(jù)包接收函數(shù)
void net_rx_handler(struct sk_buff *skb) {
    printk(KERN_INFO "Received packet, length: %u\n", skb->len);
    // 進(jìn)一步處理數(shù)據(jù)包
}

在上述代碼中,通過在net_rx_handler函數(shù)中插入 printk 語句,我們可以清晰地看到接收到的數(shù)據(jù)包的長度信息,這對于判斷數(shù)據(jù)包是否正常接收以及后續(xù)的處理邏輯是否正確提供了重要依據(jù)。

5.2 BUG 與 BUG_ON 宏

①BUG () 和 BUG_ON ()

一些內(nèi)核調(diào)用可以用來方便標(biāo)記 bug,提供斷言并輸出信息。最常用的兩個(gè)是 BUG () 和 BUG_ON ()。

定義在中:

#ifndef HAVE_ARCH_BUG

當(dāng)調(diào)用這兩個(gè)宏的時(shí)候,它們會(huì)引發(fā) OOPS,導(dǎo)致棧的回溯和錯(cuò)誤消息的打印。※ 可以把這兩個(gè)調(diào)用當(dāng)作斷言使用,如:BUG_ON (bad_thing);

②dump_stack()

有些時(shí)候,只需要在終端上打印一下棧的回溯信息來幫助你調(diào)試。這時(shí)可以使用 dump_stack ()。這個(gè)函數(shù)只在終端上打印寄存器上下文和函數(shù)的跟蹤線索。

if (!debug_check) { 
       printk(KERN_DEBUG “provide some information…/n”); 
       dump_stack(); 
   }

(1)功能作用

在 Linux 內(nèi)核開發(fā)中,BUG 和 BUG_ON 宏就像是隱藏在代碼中的 “問題探測器”。當(dāng)調(diào)用這兩個(gè)宏時(shí),會(huì)立刻引發(fā) Oops 錯(cuò)誤。它們的主要作用是標(biāo)記代碼中那些不應(yīng)該出現(xiàn)的情況,一旦這些宏被觸發(fā),就表明代碼中存在潛在的嚴(yán)重問題。比如,在一段代碼中,我們期望某個(gè)指針永遠(yuǎn)不會(huì)為空,那么就可以使用BUG_ON(ptr == NULL)來進(jìn)行斷言,如果在運(yùn)行過程中ptr真的為空,就會(huì)觸發(fā) Oops,從而讓開發(fā)者能夠及時(shí)發(fā)現(xiàn)這個(gè)潛在的錯(cuò)誤。

(2)使用場景

在開發(fā)過程中,當(dāng)我們懷疑代碼邏輯存在致命錯(cuò)誤,或者某些條件在正常情況下絕對不應(yīng)該成立時(shí),就可以巧妙地使用 BUG 和 BUG_ON 宏。例如,在一個(gè)內(nèi)存管理模塊中,假設(shè)我們有一個(gè)函數(shù)用于分配內(nèi)存,并且在函數(shù)內(nèi)部做了一些假設(shè),如分配的內(nèi)存大小必須大于 0。此時(shí),我們可以在函數(shù)開頭使用BUG_ON(size <= 0)來檢查傳入的內(nèi)存大小參數(shù)。

如果在實(shí)際運(yùn)行中,由于某些原因?qū)е聅ize小于等于 0,就會(huì)觸發(fā) Oops,這樣我們就能迅速定位到這個(gè)錯(cuò)誤的源頭,避免在后續(xù)的代碼執(zhí)行中出現(xiàn)更嚴(yán)重的問題。再比如,在一個(gè)多線程同步的場景中,我們使用信號(hào)量來控制對共享資源的訪問。假設(shè)某個(gè)線程在獲取信號(hào)量之前,不應(yīng)該直接訪問共享資源,那么可以在訪問共享資源的代碼處使用BUG_ON(sem_count < 1)來確保信號(hào)量的狀態(tài)是正確的,如果違反了這個(gè)假設(shè),就會(huì)觸發(fā) Oops,幫助我們發(fā)現(xiàn)潛在的同步問題。

5.3 dump_stack 函數(shù)

當(dāng)內(nèi)核出現(xiàn) Oops 錯(cuò)誤時(shí),dump_stack 函數(shù)就如同一位 “線索偵探”,發(fā)揮著關(guān)鍵作用。它能夠打印出寄存器上下文和函數(shù)跟蹤線索,為我們提供了深入了解內(nèi)核運(yùn)行狀態(tài)的關(guān)鍵信息。

寄存器上下文包含了內(nèi)核在錯(cuò)誤發(fā)生時(shí)各個(gè)寄存器的值,這些值反映了當(dāng)時(shí)內(nèi)核的執(zhí)行環(huán)境,如程序計(jì)數(shù)器(PC)指示了當(dāng)前正在執(zhí)行的指令地址,棧指針(SP)指向了當(dāng)前的棧頂位置等。通過分析這些寄存器的值,我們可以大致了解內(nèi)核在出錯(cuò)時(shí)的執(zhí)行流程和狀態(tài)。

函數(shù)跟蹤線索則展示了函數(shù)的調(diào)用關(guān)系,它從當(dāng)前出錯(cuò)的函數(shù)開始,逐步回溯到調(diào)用它的上層函數(shù),形成一條完整的函數(shù)調(diào)用鏈。例如,假設(shè)我們有一個(gè)內(nèi)核模塊,其中包含多個(gè)函數(shù)之間的嵌套調(diào)用。當(dāng)在某個(gè)函數(shù)中出現(xiàn) Oops 錯(cuò)誤時(shí),調(diào)用 dump_stack 函數(shù)后,我們可能會(huì)得到如下的函數(shù)跟蹤線索:function_c -> function_b -> function_a,這清晰地表明了function_c是在function_b中被調(diào)用,而function_b又是在function_a中被調(diào)用的,從而幫助我們梳理出代碼的執(zhí)行路徑,快速定位到問題可能出現(xiàn)的函數(shù)范圍 。通過這些線索,我們能夠更準(zhǔn)確地分析錯(cuò)誤發(fā)生的原因,為解決 Oops 問題提供有力的支持。

5.4 GDB調(diào)試工具

(1)工作環(huán)境配置

使用 GDB 調(diào)試 Linux 內(nèi)核 Oops 問題,首先需要進(jìn)行一系列的環(huán)境配置。確保系統(tǒng)中已經(jīng)安裝了 GDB,可以通過包管理器進(jìn)行安裝,如在 Ubuntu 系統(tǒng)中,使用 “sudo apt - get install gdb” 命令即可完成安裝。準(zhǔn)備好編譯好的內(nèi)核源碼,這是進(jìn)行調(diào)試的基礎(chǔ),只有擁有完整的內(nèi)核源碼,GDB 才能準(zhǔn)確地定位到代碼中的具體位置。還需要準(zhǔn)備帶有調(diào)試信息的內(nèi)核鏡像,通常在編譯內(nèi)核時(shí),通過配置編譯選項(xiàng),如添加 “-g” 選項(xiàng),來生成包含調(diào)試信息的內(nèi)核鏡像。例如,在編譯內(nèi)核時(shí),修改 Makefile 文件,在 CFLAGS 變量中添加 “-g”,然后重新編譯內(nèi)核,這樣生成的內(nèi)核鏡像就包含了豐富的調(diào)試信息,能夠被 GDB 識(shí)別和利用。

(2)基本調(diào)試流程

下面結(jié)合一個(gè)實(shí)際的 Oops 案例來演示 GDB 的基本調(diào)試流程。假設(shè)我們的內(nèi)核在運(yùn)行某個(gè)驅(qū)動(dòng)程序時(shí)出現(xiàn)了 Oops 錯(cuò)誤,首先,使用 “gdb vmlinux” 命令啟動(dòng) GDB,并加載內(nèi)核符號(hào)表,這里的 “vmlinux” 是編譯生成的內(nèi)核文件。接著,通過 “file vmlinux” 命令再次確認(rèn)加載的內(nèi)核文件。然后,使用 “target remote /dev/ttyS0” 命令連接到目標(biāo)機(jī)的串口,這里假設(shè)我們通過串口進(jìn)行調(diào)試。連接成功后,使用 “l(fā)oad” 命令加載帶有調(diào)試信息的內(nèi)核鏡像。接下來,就可以設(shè)置斷點(diǎn)來暫停內(nèi)核的執(zhí)行,以便進(jìn)行調(diào)試。

比如,我們懷疑問題出在驅(qū)動(dòng)程序的某個(gè)函數(shù)中,就可以使用 “b function_name” 命令在該函數(shù)處設(shè)置斷點(diǎn),其中 “function_name” 是我們要設(shè)置斷點(diǎn)的函數(shù)名。設(shè)置好斷點(diǎn)后,使用 “c” 命令繼續(xù)執(zhí)行內(nèi)核,當(dāng)執(zhí)行到斷點(diǎn)處時(shí),內(nèi)核會(huì)暫停運(yùn)行。此時(shí),我們可以使用 “info registers” 命令查看當(dāng)前寄存器的值,使用 “backtrace” 命令查看函數(shù)調(diào)用棧,還可以使用 “print variable_name” 命令查看變量的值,通過這些操作來分析內(nèi)核的運(yùn)行狀態(tài),找出問題所在。

例如,在調(diào)試一個(gè)網(wǎng)絡(luò)驅(qū)動(dòng)程序時(shí),我們發(fā)現(xiàn)系統(tǒng)在接收數(shù)據(jù)包時(shí)出現(xiàn) Oops 錯(cuò)誤。通過上述步驟,我們在驅(qū)動(dòng)程序的接收函數(shù)處設(shè)置斷點(diǎn),當(dāng)執(zhí)行到斷點(diǎn)時(shí),查看寄存器的值發(fā)現(xiàn)某個(gè)與數(shù)據(jù)包處理相關(guān)的寄存器值異常,進(jìn)一步查看函數(shù)調(diào)用棧和相關(guān)變量的值,最終發(fā)現(xiàn)是由于在數(shù)據(jù)包校驗(yàn)過程中,一個(gè)校驗(yàn)和計(jì)算錯(cuò)誤導(dǎo)致了 Oops,通過這樣的調(diào)試流程,我們成功定位并解決了問題。

5.5 objdump 工具

objdump 是一個(gè)功能強(qiáng)大的反匯編工具,在調(diào)試 Linux 內(nèi)核 Oops 問題時(shí),它能幫助我們深入分析內(nèi)核模塊或相關(guān)二進(jìn)制文件的匯編代碼。通過使用 “objdump -d” 命令,我們可以對內(nèi)核模塊或二進(jìn)制文件進(jìn)行反匯編操作。例如,對于一個(gè)名為 “module.ko” 的內(nèi)核模塊,我們可以在終端中輸入 “objdump -d module.ko” 命令,此時(shí),objdump 會(huì)將該模塊的二進(jìn)制代碼轉(zhuǎn)換為匯編代碼,并輸出到終端。

在分析出錯(cuò)地址的匯編代碼時(shí),我們首先需要從 Oops 信息中獲取出錯(cuò)的地址。然后,在 objdump 輸出的匯編代碼中,找到與該地址對應(yīng)的匯編指令。通過仔細(xì)分析這些匯編指令,我們可以了解內(nèi)核在出錯(cuò)時(shí)的具體操作,判斷是否存在指令錯(cuò)誤、內(nèi)存訪問異常等問題。比如,在一個(gè) Oops 案例中,Oops 信息顯示出錯(cuò)地址為 “0x12345678”,我們使用 objdump 對相關(guān)的內(nèi)核模塊進(jìn)行反匯編后,在輸出的匯編代碼中找到該地址對應(yīng)的指令是 “mov [eax], ebx”,通過進(jìn)一步分析發(fā)現(xiàn),此時(shí) “eax” 寄存器的值是一個(gè)非法的內(nèi)存地址,從而找到了導(dǎo)致 Oops 的原因是非法內(nèi)存訪問。objdump 工具為我們從底層匯編代碼的角度分析 Oops 問題提供了有力的支持,幫助我們更深入地理解內(nèi)核錯(cuò)誤的根源。

5.6 decodecode腳本

在 Linux 源碼目錄下,有一個(gè)名為 scripts/decodecode 的腳本,它就像是一把 “解碼鑰匙”,專門用于將 oops 日志信息轉(zhuǎn)換為直觀的匯編代碼。這個(gè)腳本的作用不可小覷,當(dāng)我們面對復(fù)雜的 oops 日志信息時(shí),往往很難直接從中分析出問題的關(guān)鍵所在。而 decodecode 腳本能夠?qū)⑦@些晦澀難懂的 oops 日志信息進(jìn)行轉(zhuǎn)換,以匯編代碼的形式呈現(xiàn)出來,讓我們能夠更直觀地了解內(nèi)核在出錯(cuò)時(shí)的執(zhí)行情況。

使用 decodecode 腳本的方法相對簡單,我們只需在終端中切換到 Linux 源碼目錄,然后執(zhí)行 “./scripts/decodecode oops_log_file” 命令,其中 “oops_log_file” 是包含 oops 日志信息的文件。腳本執(zhí)行后,會(huì)輸出轉(zhuǎn)換后的匯編代碼,我們可以根據(jù)這些匯編代碼來分析出錯(cuò)的原因。例如,在一個(gè)內(nèi)核調(diào)試過程中,我們獲取到了一份 oops 日志文件,通過執(zhí)行 decodecode 腳本,將日志信息轉(zhuǎn)換為匯編代碼后,發(fā)現(xiàn)其中一段匯編代碼在進(jìn)行內(nèi)存操作時(shí),使用了錯(cuò)誤的寄存器索引,導(dǎo)致了內(nèi)存訪問錯(cuò)誤,從而引發(fā)了 Oops。通過 decodecode 腳本,我們能夠快速定位到問題的關(guān)鍵,提高了調(diào)試的效率和準(zhǔn)確性 。

六、內(nèi)存調(diào)試工具

6.1MEMWATCH

MEMWATCH 由 Johan Lindh 編寫,是一個(gè)開放源代碼 C 語言內(nèi)存錯(cuò)誤檢測工具,您可以自己下載它。只要在代碼中添加一個(gè)頭文件并在 gcc 語句中定義了 MEMWATCH 之后,您就可以跟蹤程序中的內(nèi)存泄漏和錯(cuò)誤了。MEMWATCH 支持 ANSIC,它提供結(jié)果日志紀(jì)錄,能檢測雙重釋放(double-free)、錯(cuò)誤釋放(erroneous free)、沒有釋放的內(nèi)存(unfreedmemory)、溢出和下溢等等。

清單 1. 內(nèi)存樣本(test1.c)

#include <stdlib.h>
#include <stdio.h>
#include "memwatch.h"
int main(void)
{
 char *ptr1;
 char *ptr2;
 ptr1 = malloc(512);
 ptr2 = malloc(512);
 ptr2 = ptr1;
 free(ptr2);
 free(ptr1);
}

清單 1 中的代碼將分配兩個(gè) 512 字節(jié)的內(nèi)存塊,然后指向第一個(gè)內(nèi)存塊的指針被設(shè)定為指向第二個(gè)內(nèi)存塊。結(jié)果,第二個(gè)內(nèi)存塊的地址丟失,從而產(chǎn)生了內(nèi)存泄漏。現(xiàn)在我們編譯清單 1 的 memwatch.c。下面是一個(gè) makefile 示例:test1

gcc -DMEMWATCH -DMW_STDIO test1.c memwatch
c -o test1

當(dāng)您運(yùn)行 test1 程序后,它會(huì)生成一個(gè)關(guān)于泄漏的內(nèi)存的報(bào)告。清單 2 展示了示例 memwatch.log 輸出文件。

清單 2. test1 memwatch.log 文件

MEMWATCH 2.67 Copyright (C) 1992-1999 Johan Lindh
...
double-free: <4> test1.c(15), 0x80517b4 was freed from test1.c(14)
...
unfreed: <2> test1.c(11), 512 bytes at 0x80519e4
{FE FE FE FE FE FE FE FE FE FE FE FE ..............}
Memory usage statistics (global):
 N)umber of allocations made: 2
 L)argest memory usage : 1024
 T)otal of all alloc() calls: 1024
 U)nfreed bytes totals : 512

MEMWATCH 為您顯示真正導(dǎo)致問題的行。如果您釋放一個(gè)已經(jīng)釋放過的指針,它會(huì)告訴您。對于沒有釋放的內(nèi)存也一樣。日志結(jié)尾部分顯示統(tǒng)計(jì)信息,包括泄漏了多少內(nèi)存,使用了多少內(nèi)存,以及總共分配了多少內(nèi)存。

6.2 YAMD

YAMD 軟件包由 Nate Eldredge 編寫,可以查找 C 和 C++ 中動(dòng)態(tài)的、與內(nèi)存分配有關(guān)的問題。在撰寫本文時(shí),YAMD 的最新版本為 0.32。請下載 yamd-0.32.tar.gz。執(zhí)行 make 命令來構(gòu)建程序;然后執(zhí)行 make install 命令安裝程序并設(shè)置工具。一旦您下載了 YAMD 之后,請?jiān)?test1.c 上使用它。請刪除 #include memwatch.h 并對 makefile 進(jìn)行如下小小的修改:使用 YAMD 的 test1

gcc -g test1.c -o test1

清單 3 展示了來自 test1 上的 YAMD 的輸出。

清單 3. 使用 YAMD 的 test1 輸出

YAMD version 0.32
Executable: /usr/src/test/yamd-0.32/test1
...
INFO: Normal allocation of this block
Address 0x40025e00, size 512
...
INFO: Normal allocation of this block
Address 0x40028e00, size 512
...
INFO: Normal deallocation of this block
Address 0x40025e00, size 512
...
ERROR: Multiple freeing At
free of pointer already freed
Address 0x40025e00, size 512
...
WARNING: Memory leak
Address 0x40028e00, size 512
WARNING: Total memory leaks:
1 unfreed allocations totaling 512 bytes
*** Finished at Tue ... 10:07:15 2002
Allocated a grand total of 1024 bytes 2 allocations
Average of 512 bytes per allocation
Max bytes allocated at one time: 1024
24 K alloced internally / 12 K mapped now / 8 K max
Virtual program size is 1416 K
End.

YAMD 顯示我們已經(jīng)釋放了內(nèi)存,而且存在內(nèi)存泄漏。讓我們在清單 4 中另一個(gè)樣本程序上試試 YAMD。

清單 4. 內(nèi)存代碼(test2.c)

#include <stdlib.h>
#include <stdio.h>
int main(void)
{
 char *ptr1;
 char *ptr2;
 char *chptr;
 int i = 1;
 ptr1 = malloc(512);
 ptr2 = malloc(512);
 chptr = (char *)malloc(512);
 for (i; i <= 512; i++) {
   chptr[i] = 'S';
 } 
 ptr2 = ptr1;
 free(ptr2);
 free(ptr1);
 free(chptr);
}

您可以使用下面的命令來啟動(dòng) YAMD:

./run-yamd /usr/src/test/test2/test2

清單 5 顯示了在樣本程序 test2 上使用 YAMD 得到的輸出。YAMD 告訴我們在 for 循環(huán)中有 “越界(out-of-bounds)” 的情況。

清單 5. 使用 YAMD 的 test2 輸出

Running /usr/src/test/test2/test2
Temp output to /tmp/yamd-out.1243
*********
./run-yamd: line 101: 1248 Segmentation fault (core dumped)
YAMD version 0.32
Starting run: /usr/src/test/test2/test2
Executable: /usr/src/test/test2/test2
Virtual program size is 1380 K
...
INFO: Normal allocation of this block
Address 0x40025e00, size 512
...
INFO: Normal allocation of this block
Address 0x40028e00, size 512
...
INFO: Normal allocation of this block
Address 0x4002be00, size 512
ERROR: Crash
...
Tried to write address 0x4002c000
Seems to be part of this block:
Address 0x4002be00, size 512
...
Address in question is at offset 512 (out of bounds)
Will dump core after checking heap.
Done.

MEMWATCH 和 YAMD 都是很有用的調(diào)試工具,它們的使用方法有所不同。對于 MEMWATCH,您需要添加包含文件 memwatch.h 并打開兩個(gè)編譯時(shí)間標(biāo)記。對于鏈接(link)語句,YAMD 只需要 -g 選項(xiàng)。

6.3 Electric Fence

多數(shù) Linux 分發(fā)版包含一個(gè) Electric Fence 包,不過您也可以選擇下載它。Electric Fence 是一個(gè)由 Bruce Perens 編寫的 malloc () 調(diào)試庫。它就在您分配內(nèi)存后分配受保護(hù)的內(nèi)存。如果存在 fencepost 錯(cuò)誤(超過數(shù)組末尾運(yùn)行),程序就會(huì)產(chǎn)生保護(hù)錯(cuò)誤,并立即結(jié)束。通過結(jié)合 Electric Fence 和 gdb,您可以精確地跟蹤到哪一行試圖訪問受保護(hù)內(nèi)存。ElectricFence 的另一個(gè)功能就是能夠檢測內(nèi)存泄漏。

6.4 strace

strace 命令是一種強(qiáng)大的工具,它能夠顯示所有由用戶空間程序發(fā)出的系統(tǒng)調(diào)用。strace 顯示這些調(diào)用的參數(shù)并返回符號(hào)形式的值。strace 從內(nèi)核接收信息,而且不需要以任何特殊的方式來構(gòu)建內(nèi)核。

將跟蹤信息發(fā)送到應(yīng)用程序及內(nèi)核開發(fā)者都很有用。在清單 6 中,分區(qū)的一種格式有錯(cuò)誤,清單顯示了 strace 的開頭部分,內(nèi)容是關(guān)于調(diào)出創(chuàng)建文件系統(tǒng)操作(mkfs )的。strace 確定哪個(gè)調(diào)用導(dǎo)致問題出現(xiàn)。清單 6. mkfs 上 strace 的開頭部分

execve("/sbin/mkfs.jfs", ["mkfs.jfs", "-f", "/dev/test1"], &
...
open("/dev/test1", O_RDWR|O_LARGEFILE) = 4
stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0
ioctl(4, 0x40041271, 0xbfffe128) = -1 EINVAL (Invalid argument)
write(2, "mkfs.jfs: warning - cannot setb" ..., 98mkfs.jfs: warning -
cannot set blocksize on block device /dev/test1: Invalid argument )
 = 98
stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0
open("/dev/test1", O_RDONLY|O_LARGEFILE) = 5
ioctl(5, 0x80041272, 0xbfffe124) = -1 EINVAL (Invalid argument)
write(2, "mkfs.jfs: can\'t determine device"..., ..._exit(1)
 = ?

清單 6 顯示 ioctl 調(diào)用導(dǎo)致用來格式化分區(qū)的 mkfs 程序失敗。ioctl BLKGETSIZE64 失敗。( BLKGET-SIZE64 在調(diào)用 ioctl 的源代碼中定義。) BLKGETSIZE64 ioctl 將被添加到 Linux 中所有的設(shè)備,而在這里,邏輯卷管理器還不支持它。因此,如果 BLKGETSIZE64 ioctl 調(diào)用失敗,mkfs 代碼將改為調(diào)用較早的 ioctl 調(diào)用;這使得 mkfs 適用于邏輯卷管理器。

七、Linux內(nèi)核Oops錯(cuò)誤案例分析

7.1案例引入

下面我們來看一個(gè)實(shí)際的 Linux 內(nèi)核 Oops 錯(cuò)誤案例,假設(shè)我們在開發(fā)一個(gè)自定義的內(nèi)核模塊時(shí),遇到了如下的 Oops 錯(cuò)誤信息:

[  10.234567] Unable to handle kernel NULL pointer dereference at virtual address 0000000000000000
[  10.234572] Mem abort info:
[  10.234574]   ESR = 0x96000045
[  10.234577]   EC = 0x25: DABT (current EL), IL = 32 bits
[  10.234580]   SET = 0, FnV = 0
[  10.234582]   EA = 0, S1PTW = 0
[  10.234584] Data abort info:
[  10.234586]   ISV = 0, ISS = 0x00000045
[  10.234588]   CM = 0, WnR = 1
[  10.234590] user pgtable: 4k pages, 39-bit VAs, pgdp=0000000108782000
[  10.234594] [0000000000000000] pgd=0000000000000000, p4d=0000000000000000, pud=0000000000000000
[  10.234603] Internal error: Oops: 96000045 [#1] PREEMPT SMP
[  10.234608] Modules linked in: custom_module(O+)
[  10.234616] CPU: 0 PID: 1234 Comm: some_process Tainted: G           O      5.15.0 #1
[  10.234621] Hardware name: Some_Hardware_Model (DT)
[  10.234623] pstate: 60400009 (nZCv daif +PAN -UAO -TCO BTYPE=--)
[  10.234628] pc : custom_function+0x28/0x1000 [custom_module]
[  10.234638] lr : custom_function+0x24/0x1000 [custom_module]
[  10.234644] sp : ffffffc01391bb20
[  10.234647] x29: ffffffc01391bb20 x28: ffffff811e6db3b8 
[  10.234652] x27: 0000000000000003 x26: 0000000000000000 
[  10.234658] x25: 0000000000000019 x24: 0000000000000000 
[  10.234662] x23: 0000000000000000 x22: ffffffc011fa28c0 
[  10.234667] x21: ffffffc011fa4380 x20: ffffffc009035000 
[  10.234672] x19: ffffffc011fa2900 x18: 0000000000000000 
[  10.234677] x17: 0000000000000000 x16: 0000000000000000 
[  10.234682] x15: 180f0a0700000000 x14: 00656c75646f6d5f 
[  10.234688] x13: 0000000000000000 x12: 0000000000000018 
[  10.234692] x11: 0101010101010101 x10: ffffffff7f7f7f7f 
[  10.234697] x9 : ffffffc0100a07d0 x8 : 74696e6920656c75 
[  10.234702] x7 : 646f6d2073706f6f x6 : ffffffc012055ae9 
[  10.234707] x5 : ffffffc012055ae8 x4 : ffffff81feeb1b70 
[  10.234712] x3 : 0000000000000000 x2 : 0000000000000000 
[  10.234717] x1 : ffffff8119b2eac0 x0 : 0000000000000000 
[  10.234722] Call trace:
[  10.234725]  custom_function+0x28/0x1000 [custom_module]
[  10.234732]  another_function+0xb4/0x210
[  10.234739]  yet_another_function+0x68/0x210
[  10.234747]  some_kernel_function+0x1cb4/0x2258
[  10.234752]  __do_sys_some_syscall+0xe0/0x100
[  10.234758]  __arm64_sys_some_syscall+0x28/0x34
[  10.234763]  el0_svc_common.constprop.0+0x154/0x204
[  10.234769]  do_el0_svc+0x8c/0x98
[  10.234774]  el0_svc+0x20/0x30
[  10.234780]  el0_sync_handler+0xd8/0x184
[  10.234785]  el0_sync+0x1a0/0x1c0
[  10.234790] 
[  10.234790] PC: 0xffffffc009034f28:....
[  10.239344] Code: 910003fd 91000000 95fee5c3 d2800000 (b900001f) 
[  10.239349] ---[ end trace 0000000000000002 ]---

7.2分析過程

(1)信息提取

  • 出錯(cuò)地址:從 “Unable to handle kernel NULL pointer dereference at virtual address 0000000000000000” 可以看出,這是一個(gè)空指針解引用錯(cuò)誤,出錯(cuò)的虛擬地址為 0x0000000000000000。
  • 寄存器值:通過 “pc : custom_function+0x28/0x1000 [custom_module]” 可知程序計(jì)數(shù)器(PC)指向custom_function函數(shù)內(nèi)偏移 0x28 的位置;“l(fā)r : custom_function+0x24/0x1000 [custom_module]” 表明鏈接寄存器(LR)指向custom_function函數(shù)內(nèi)偏移 0x24 的位置;還有其他眾多寄存器的值,如 “sp : ffffffc01391bb20” 表示棧指針(SP)的值 ,這些寄存器值反映了出錯(cuò)時(shí)內(nèi)核的運(yùn)行狀態(tài)。
  • 調(diào)用棧:從 “Call trace:” 后面的信息可以看到函數(shù)的調(diào)用關(guān)系,從custom_function開始,依次經(jīng)過another_function、yet_another_function等函數(shù),這些調(diào)用關(guān)系展示了程序執(zhí)行到出錯(cuò)點(diǎn)的路徑,對于分析錯(cuò)誤原因非常關(guān)鍵。

(2)工具運(yùn)用

①首先,根據(jù)出錯(cuò)地址和函數(shù)名,我們可以使用 GDB 進(jìn)行調(diào)試。假設(shè)我們已經(jīng)準(zhǔn)備好編譯好的內(nèi)核源碼和帶有調(diào)試信息的內(nèi)核鏡像,啟動(dòng) GDB 并加載內(nèi)核符號(hào)表:

gdb vmlinux
file vmlinux

②然后,通過 Oops 信息中 PC 指向的函數(shù)和偏移,在 GDB 中設(shè)置斷點(diǎn):

b custom_function+0x28

③接著,使用 “info registers” 命令查看當(dāng)前寄存器的值,與 Oops 信息中的寄存器值進(jìn)行對比分析,進(jìn)一步確認(rèn)出錯(cuò)時(shí)的狀態(tài)。

④利用 “backtrace” 命令查看函數(shù)調(diào)用棧,與 Oops 信息中的調(diào)用棧進(jìn)行核對,檢查是否存在異常的函數(shù)調(diào)用。我們還可以使用 objdump 工具對custom_module模塊進(jìn)行反匯編分析。假設(shè)custom_module模塊的文件名為 “custom_module.ko”,執(zhí)行如下命令:

objdump -d custom_module.ko

④通過反匯編代碼,找到 PC 指向的偏移 0x28 處的匯編指令,分析該指令的操作,判斷是否存在指令錯(cuò)誤或內(nèi)存訪問異常等問題。例如,如果該指令是對某個(gè)指針進(jìn)行解引用操作,而該指針為空,就會(huì)導(dǎo)致空指針解引用錯(cuò)誤,與 Oops 信息中的錯(cuò)誤類型相符合。

7.3解決辦法

經(jīng)過上述分析,我們發(fā)現(xiàn)問題出在custom_function函數(shù)中對一個(gè)指針的使用上。假設(shè)該函數(shù)的代碼如下:

#include <linux/module.h>
#include <linux/kernel.h>

static void custom_function(void) {
    int *ptr = NULL;
    // 錯(cuò)誤操作:沒有對ptr進(jìn)行初始化就解引用
    *ptr = 10; 
}

static int __init custom_module_init(void) {
    printk(KERN_INFO "Custom module initialized\n");
    custom_function();
    return 0;
}

static void __exit custom_module_exit(void) {
    printk(KERN_INFO "Custom module exited\n");
}

module_init(custom_module_init);
module_exit(custom_module_exit);
MODULE_LICENSE("GPL");

從代碼中可以明顯看出,ptr指針被初始化為 NULL,然后在沒有進(jìn)行任何初始化的情況下就被解引用,這正是導(dǎo)致空指針解引用錯(cuò)誤的原因。

解決辦法很簡單,就是在使用指針之前對其進(jìn)行正確的初始化。修改后的代碼如下:

#include <linux/module.h>
#include <linux/kernel.h>

static void custom_function(void) {
    int value = 10;
    int *ptr = &value;
    *ptr = 10; 
}

static int __init custom_module_init(void) {
    printk(KERN_INFO "Custom module initialized\n");
    custom_function();
    return 0;
}

static void __exit custom_module_exit(void) {
    printk(KERN_INFO "Custom module exited\n");
}

module_init(custom_module_init);
module_exit(custom_module_exit);
MODULE_LICENSE("GPL");

在修改后的代碼中,我們先定義了一個(gè)變量value,然后將ptr指針指向value,這樣就確保了ptr在被解引用時(shí)指向的是一個(gè)有效的內(nèi)存地址,從而避免了空指針解引用錯(cuò)誤。重新編譯內(nèi)核模塊并加載到系統(tǒng)中,Oops 錯(cuò)誤應(yīng)該就不會(huì)再出現(xiàn)了。

責(zé)任編輯:武曉燕 來源: 深度Linux
相關(guān)推薦

2011-01-10 14:41:26

2022-01-17 07:50:37

Linux Patch項(xiàng)目

2021-09-18 14:26:49

Linux Linux 啟動(dòng)流程Linux 系統(tǒng)

2025-05-07 00:31:30

2011-05-03 15:59:00

黑盒打印機(jī)

2021-07-14 09:00:00

JavaFX開發(fā)應(yīng)用

2020-06-01 16:25:43

WindowsLinux命令

2009-06-15 16:58:57

Java安裝Linux

2021-02-26 11:54:38

MyBatis 插件接口

2011-02-22 13:46:27

微軟SQL.NET

2021-12-28 08:38:26

Linux 中斷喚醒系統(tǒng)Linux 系統(tǒng)

2023-04-26 12:46:43

DockerSpringKubernetes

2022-12-07 08:42:35

2022-03-14 14:47:21

HarmonyOS操作系統(tǒng)鴻蒙

2022-01-08 20:04:20

攔截系統(tǒng)調(diào)用

2022-07-27 08:16:22

搜索引擎Lucene

2022-08-25 14:41:51

集群搭建

2024-05-30 10:30:39

2019-10-29 15:46:07

區(qū)塊鏈區(qū)塊鏈技術(shù)

2021-12-10 18:19:55

指標(biāo)體系設(shè)計(jì)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)