成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

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

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

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

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

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

一、Oops 是什么?

1.1定義闡述

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

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

1.2引發(fā)原因

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

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

(2)空指針引用

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

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

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

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

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

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

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

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

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

2.1確認并定位 bug

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

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

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

2.2環(huán)境搭建

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

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

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

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

除了上述工具,還需要安裝一些與內(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” 命令一次性安裝多個依賴包,確保調(diào)試環(huán)境的完整性。

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

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

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

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

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

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

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

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

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

  • 一是軟件開發(fā)過程中,若發(fā)現(xiàn)代碼邏輯出現(xiàn)致命 fault 后就可以調(diào)用BUG()讓kernel死掉(類似于assert),這樣方便于定位問題,從而修正代碼執(zhí)行邏輯;
  • 另外一種情況就是,由于某種特殊原因(通常是為了debug而需抓ramdump),我們需要系統(tǒng)進入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)來說,會通過 include asm-generict/bug.h對 BUG() 進行重定義。

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() 會向CPU 下發(fā)一條未定義指令而觸發(fā)ARM 發(fā)起未定義指令異常,隨后進入 kernel 異常處理流程,通過調(diào)用die() 經(jīng)歷Oops 和 panic。

3.2OOPS —錯誤報告框架

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

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

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

如果沒有源碼,對于沒有編譯符號表的二進制文件,可以使用:

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

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

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 級別,并通過 __die() 通過對Oops 感興趣的模塊進行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() 會通知所有對 Oops 感興趣的模塊并進行callback;
  • print_modules() 打印模塊狀態(tài)不為 MODULE_STATE_UNFORMED 的模塊信息;
  • show_regs() 打印PC、LR、SP 等寄存器的信息,同時打印調(diào)用堆棧信息;
  • dump_kernel_instr() 打印 pc指針和前4條指令;

這里不過多的剖析,感興趣的可以查看下源碼。這里需要注意的是 notify_die() 會通知所有的Oops 感興趣的模塊,模塊會通過函數(shù) register_die_notifier() 將callback 注冊到全局結(jié)構(gòu)體變量 die_chain 中(多個模塊注冊進來形成一個鏈表),然后在通過 notify_die() 函數(shù)去解析這個 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同時有可能panic

從上面 die() 函數(shù)最后看到,oops_exit() 之后也有可能進入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 打開時進入 panic。

中斷的可能性:

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

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

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

panic 本意是“恐慌”的意思,這里意旨 kernel 發(fā)生了致命錯誤導(dǎo)致無法繼續(xù)運行下去的情況。根據(jù)實際情況 Oops最終也可能會導(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)死鎖,因為無法防止中斷處理程序(在獲得panic鎖后運行)再次被調(diào)用panic
	local_irq_disable();
    //禁止任務(wù)搶占
	preempt_disable_notrace();

    //通過this_cpu確認是否調(diào)用panic() 的cpu是否為panic_cpu;
	//即,只允許一個CPU執(zhí)行該代碼,通過 panic_smp_self_stop() 保證當(dāng)一個CPU執(zhí)行panic時,
    //其他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的打印級別放開
	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斷點運行
	kgdb_panic(buf);

	if (!_crash_kexec_post_notifiers) {
		printk_safe_flush_on_panic();
        //會根據(jù)當(dāng)前是否設(shè)置了轉(zhuǎn)儲內(nèi)核(使能CONFIG_KEXEC_CORE)確定是否實際執(zhí)行轉(zhuǎn)儲操作;
        //如果執(zhí)行轉(zhuǎn)儲則會通過 kexec 將系統(tǒng)切換到新的kdump 內(nèi)核,并且不會再返回;
        //如果不執(zhí)行轉(zhuǎn)儲,則繼續(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感興趣的模塊進行回調(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)
    //首先,這里會每隔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);

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

(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 默認值為 0,可以通過 /proc/sys/kernel/panic_print 節(jié)點配置,當(dāng) panic 發(fā)生的時候,用戶可以通過如下bit 位配置打印系統(tǒng)信息:

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

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

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

4.1內(nèi)核配置

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

Kernel hacking  --->

啟用選項例如:

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

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

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

下面這些選項可以最大限度地利用該特性:

CONFIG_PREEMPT = y

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

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

5.1 printk 函數(shù)運用

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

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

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

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

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

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

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

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

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

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

#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)用者未將日志級別提供給 printk,那么系統(tǒng)就會使用默認值 KERN_WARNING "<4>"(表示只有 KERN_WARNING 級別以上的日志消息會被記錄)。由于默認值存在變化,所以在使用時最好指定 LOG 級別。有 LOG 級別的一個好處就是我們可以選擇性的輸出 LOG。

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

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

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

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

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

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

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

#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)

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

CONFIG_LOG_BUF_SHIFT=18

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

※ 這個紀錄緩沖區(qū)之所以稱為環(huán)形,是因為它的讀寫都是按照環(huán)形隊列的方式進行操作的。

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

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

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

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

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

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

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

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

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

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

在調(diào)試過程中,合理地在關(guān)鍵代碼處插入 printk 輸出調(diào)試信息是非常有效的方法。比如,在一個網(wǎng)絡(luò)設(shè)備驅(qū)動程序中,當(dāng)我們懷疑數(shù)據(jù)包的接收處理過程存在問題時,可以在接收函數(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);
    // 進一步處理數(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,提供斷言并輸出信息。最常用的兩個是 BUG () 和 BUG_ON ()。

定義在中:

#ifndef HAVE_ARCH_BUG

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

②dump_stack()

有些時候,只需要在終端上打印一下棧的回溯信息來幫助你調(diào)試。這時可以使用 dump_stack ()。這個函數(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)用這兩個宏時,會立刻引發(fā) Oops 錯誤。它們的主要作用是標(biāo)記代碼中那些不應(yīng)該出現(xiàn)的情況,一旦這些宏被觸發(fā),就表明代碼中存在潛在的嚴重問題。比如,在一段代碼中,我們期望某個指針永遠不會為空,那么就可以使用BUG_ON(ptr == NULL)來進行斷言,如果在運行過程中ptr真的為空,就會觸發(fā) Oops,從而讓開發(fā)者能夠及時發(fā)現(xiàn)這個潛在的錯誤。

(2)使用場景

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

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

5.3 dump_stack 函數(shù)

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

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

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

5.4 GDB調(diào)試工具

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

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

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

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

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

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

5.5 objdump 工具

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

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

5.6 decodecode腳本

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

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

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

6.1MEMWATCH

MEMWATCH 由 Johan Lindh 編寫,是一個開放源代碼 C 語言內(nèi)存錯誤檢測工具,您可以自己下載它。只要在代碼中添加一個頭文件并在 gcc 語句中定義了 MEMWATCH 之后,您就可以跟蹤程序中的內(nèi)存泄漏和錯誤了。MEMWATCH 支持 ANSIC,它提供結(jié)果日志紀錄,能檢測雙重釋放(double-free)、錯誤釋放(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 中的代碼將分配兩個 512 字節(jié)的內(nèi)存塊,然后指向第一個內(nèi)存塊的指針被設(shè)定為指向第二個內(nèi)存塊。結(jié)果,第二個內(nèi)存塊的地址丟失,從而產(chǎn)生了內(nèi)存泄漏。現(xiàn)在我們編譯清單 1 的 memwatch.c。下面是一個 makefile 示例:test1

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

當(dāng)您運行 test1 程序后,它會生成一個關(guān)于泄漏的內(nèi)存的報告。清單 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)致問題的行。如果您釋放一個已經(jīng)釋放過的指針,它會告訴您。對于沒有釋放的內(nèi)存也一樣。日志結(jié)尾部分顯示統(tǒng)計信息,包括泄漏了多少內(nèi)存,使用了多少內(nèi)存,以及總共分配了多少內(nèi)存。

6.2 YAMD

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

您可以使用下面的命令來啟動 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 并打開兩個編譯時間標(biāo)記。對于鏈接(link)語句,YAMD 只需要 -g 選項。

6.3 Electric Fence

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

6.4 strace

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

將跟蹤信息發(fā)送到應(yīng)用程序及內(nèi)核開發(fā)者都很有用。在清單 6 中,分區(qū)的一種格式有錯誤,清單顯示了 strace 的開頭部分,內(nèi)容是關(guān)于調(diào)出創(chuàng)建文件系統(tǒng)操作(mkfs )的。strace 確定哪個調(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錯誤案例分析

7.1案例引入

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

[  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)信息提取

  • 出錯地址:從 “Unable to handle kernel NULL pointer dereference at virtual address 0000000000000000” 可以看出,這是一個空指針解引用錯誤,出錯的虛擬地址為 0x0000000000000000。
  • 寄存器值:通過 “pc : custom_function+0x28/0x1000 [custom_module]” 可知程序計數(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)的值 ,這些寄存器值反映了出錯時內(nèi)核的運行狀態(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í)行到出錯點的路徑,對于分析錯誤原因非常關(guān)鍵。

(2)工具運用

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

gdb vmlinux
file vmlinux

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

b custom_function+0x28

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

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

objdump -d custom_module.ko

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

7.3解決辦法

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

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

static void custom_function(void) {
    int *ptr = NULL;
    // 錯誤操作:沒有對ptr進行初始化就解引用
    *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,然后在沒有進行任何初始化的情況下就被解引用,這正是導(dǎo)致空指針解引用錯誤的原因。

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

#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");

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

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

2011-01-10 14:41:26

2021-09-18 14:26:49

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

2022-01-17 07:50:37

Linux Patch項目

2021-07-14 09:00:00

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

2025-05-07 00:31:30

2011-05-03 15:59:00

黑盒打印機

2009-06-15 16:58:57

Java安裝Linux

2020-06-01 16:25:43

WindowsLinux命令

2023-04-26 12:46:43

DockerSpringKubernetes

2022-01-08 20:04:20

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

2022-07-27 08:16:22

搜索引擎Lucene

2022-12-07 08:42:35

2022-03-14 14:47:21

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

2011-02-22 13:46:27

微軟SQL.NET

2021-02-26 11:54:38

MyBatis 插件接口

2021-12-28 08:38:26

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

2019-10-29 15:46:07

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

2024-05-30 10:30:39

2022-08-25 14:41:51

集群搭建

2021-12-10 18:19:55

指標(biāo)體系設(shè)計
點贊
收藏

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

主站蜘蛛池模板: 国产精品区二区三区日本 | 久草视频在线播放 | 一区二区三区免费观看 | 欧美精品综合在线 | 亚洲精品在线视频 | 在线免费观看视频你懂的 | 日韩午夜影院 | 黑人久久 | 国产亚洲精品美女久久久久久久久久 | 久久久精 | 欧美精品一区二区三区四区五区 | 亚洲综合精品 | 久久久高清 | 激情综合五月 | 激情视频一区 | 亚洲视频在线观看一区二区三区 | 亚洲精品免费视频 | 日韩成人在线一区 | 亚洲人久久 | 黄篇网址 | 国产欧美精品在线观看 | 日日夜夜草 | 成人av在线播放 | 污污免费网站 | а天堂中文最新一区二区三区 | 亚洲高清一区二区三区 | 精品国产一区二区在线 | 羞羞网站免费观看 | 午夜视频网站 | 第四色播日韩第一页 | 国产一区二区三区免费视频 | 羞羞色在线观看 | 不卡一区 | 欧美久久久久久久 | 呦呦在线视频 | 欧美成人一区二免费视频软件 | 日韩亚洲视频 | 欧美一级淫片免费视频黄 | 在线观看国产精品一区二区 | 成人免费在线播放视频 | 黄频免费 |