目錄
一、前言
二、內核調試方法
2.1 內核調試概述
2.2 學會分析內核源程序
2.3調試方法介紹
三、內核打印函數
3.1內核鏡像解壓前的串口輸出函數
3.2 內核鏡像解壓后的串口輸出函數
3.3 內核打印函數
四、獲取內核信息
4.1系統請求鍵
4.2 通過/proc 接口
4.3 通過/sys 接口
4.3.1.屬性
4.3.2子系統操作函數
五、處理出錯信息
5.1 oops 信息
5.1.1.oops 消息包含系統錯誤的詳細信息
5.1.2.使用 ksymoops 轉換 oops 信息
5.1.3,內核 kallsyms 選項支持符號信息
5.2 panic
5.3 通過ioctl 方法
六、內核源碼調試
七、習題
一、前言
????????編寫驅動程序難免會遇到一些問題,要快速地解決這些問題,就需要熟練掌握內核的各種調試方法。本章介紹了各種 Linux內核調試方法,內核的調試需要從內核源碼本身、調試工具等方面做好準備。通過本章的學習,可以了解不同調試方法的特點和使用方法,再根據需要選擇不同的內核調試方式。
?
二、內核調試方法
????????對于龐大的 Linux 內核軟件工程,單靠閱讀代碼查找問題已經非常圍難,需要借助調試技術解決 BUG。通過合適的調試手段,可以有效地查找和判斷 BUG 的位置和原因。
2.1 內核調試概述
????????當內核運行出現錯誤的時候,首先要明確定義和可靠地重現這個錯誤現象。如果個BUG 不能重現,修正時只能憑想象和讀代碼。內核、用戶空間和硬件之間的交互非復雜,在特定配置、特定機器、特殊負載條件下,運行某些程序可能會產生一個BUG,但在其他條件下就不一定產生。這在嵌入式 Linux 系統上很常見,例如:在 X86 平臺運行正常的驅動程序,在 ARM 平臺上就可能會出現 BUG。在跟蹤 BUG 的時候,掌提的信息越多越好。
????????內核的 BUG 是多種多樣的,可能由于不同原因出現,并且表現形式也多種多樣。BUG的范圍從完全不正確的代碼(如沒有在適當的地址存儲正確的值)到同步的錯誤(如不適當地對一個共享變量加鎖)。BUG 的表現形式也各種各樣,從系統崩潰的錯誤操作到系統性能差等。
????????通常 BUG 是一系列事件,內核代碼的錯誤使用戶程序出現錯誤。例如,一個不帶引用計數的共享結構可能引起條件競爭。沒有合適的統計,一個進程可以釋放這個結構,但是另外一個進程仍然想要用它。第二個進程可能會使用一個無效的指針訪問一個不存在的結構。這就會導致空指針訪問、讀垃圾數據,如果這個數據還沒有被覆蓋,也可能基本正常。對空指針訪問會產生 oops; 垃圾數據會導致數據錯誤(接下來可能是錯誤的行為或者 oops); 內核報告 oops 或者錯誤的行為。內核開發者必須處理這個錯誤,知道這個數據是在釋放以后訪問的,這存在一個條件競爭。修正的方法是為這個結構添加引用計數,并且可能需要加鎖保護。
????????調試內核很難,實際上內核不同于其他軟件工程。內核有操作系統獨特的問題,例
如時間管理和條件競爭,這可以使多個線程同時在內核中執行。
????????因此,調試 BUG 需要有效的調試手段。幾乎沒有一種調試工具或者方法能夠解決全部問題。即使在一些集成測試環境中,也要劃分不同測試調試功能,例如跟蹤調試、內存泄漏測試、性能測試等。掌握的調試方法越多,調試 BUG 就越方便。Linux 有很多放源碼的工具,每一個工具的調試功能都是專一的,所以這些工具的實現一般也比較簡單。
2.2 學會分析內核源程序
????????由于內核的復雜性,無論使用什么調試手段,都需要熟悉內核源碼。只有熟悉了內核各部分的代碼實現,才能夠找到準確的跟蹤點; 只有熟悉操作系統的內核機制,才能準確地判斷系統運行狀態。
????????對于初學者來說,閱讀內核源碼將是非常枯燥的工作。最好先掌握一種搜索工具,學會從源碼樹中搜索關鍵詞。當能夠對內核源碼進行情景分析的時候,你就能感到其中的樂趣了。
調試是無法逃避的任務。進行調試有很多種方法,比如將消息打印到屏幕上、使用調試器,或只是考慮程序執行的情況并仔細地分析問題所在。
????????在修正問題之前,必須先找出問題的源頭。舉例來說,對于段錯誤,需要了解段錯誤發生在代碼的哪一行。一旦發現了代碼中出錯的行,就要確定該方法中變量的值、方法被調用的方式以及錯誤如何發生的詳細情況。使用調試器將使找出所有這些信息變得很簡單。如果沒有調試器可用,還可以使用其他的工具。(請注意: 有些 Linux 軟件產品中可能并不提供調試器)。
2.3調試方法介紹
內核調試方法很多,主要有以下四類。
。打印函數。
。獲取內核信息
。處理出錯信息
。內核源碼調試
????????在調試內核之前,通常需要配置內核的調試選項。下圖給出了“Kernel hacking”菜單下的各種調試選項。不同的調試方法需要配置對應的選項。
????????每一種調試選項都有不同的調試功能,并且不是所有的調試選項在所有的平臺上都能內核調試技術被支持。這里介紹一些“Kernel hacking”的調試選項,具體配置使用可以根據情況選擇。
(1) printk and dmesg options
????????該子菜單中的若干選項用來決定 printk 打印和dmesg 輸出的一些特性,如是否在打印信息前加上時間信息、默認的打印級別以及延遲打印的時間。
(2) Compile-time checks and compiler options
????????該子菜單中的若干選項用來決定編譯時的檢查和設置一些編譯選項,如內核是否可調試(是否加“-g”選項);是否使能“ deprecated”邏輯(禁止該選項將不會得到如warning:'foo' is deprecated (declared at kernel/power/somefile.c:1234)”等信息);是否使能must check”邏輯(禁止該選項將不會進行必須檢查,如有的函數的返回值必須要求檢查,如果沒有檢查編譯器將會產生警告);設置棧的幀數上限值等。
(3) Magic SysRq key
????????CONFIG_MAGIC_SYSRQ(Magic SysRg key 選項所對應的內核源碼宏定義,后面選項類似,不再進行說明) 使能系統請求鍵,可以用于系統調試。
(4)Kernel debugging
????????CONFIGDEBUG_KERNEL 選擇調試內核選項以后,才可以顯示有關的內核調試子項。大部分內核調試選項都依賴于它。
(5) Memory Debugging
????????該子菜單中的若干選項用來選擇內核內存調試的一些選項。
(6) Debug shared IRO handlers
????????CONFIG_DEBUG_SHIRQ 共享中斷的相關調試使能。
(7) Debug Lockups and Hangs
????????該子菜單中的若干選項用來選擇內核死鎖和掛起的一些調試功能,如死鎖檢測、掛起檢測、掛起的超時設置等。
(8) Panic on Oops
????????CONFIG_PANIC_ON_OOPS 在 Oops 信息輸出后是否 Panic,內核輸出 Oops 信息不意味著內核就一定不能繼續往下運行選擇該選項意味著一旦 Oops 后內核就在一個預定的時間后重啟和一直死循環。
(9) panic timeout
????????CONFIG_PANIC_TIMEOUT 配置 Panic 的超時值,為0 表示死循環
(10) Collect scheduler debugging info
????????CONFIG_SCHED_DEBUG 調度器調試信息收集,保存在/proc/sched_debug 文件中
(11) Collect scheduler statistics
????????CONFIG_SCHEDSTATS 調度器統計信息收集,保存在/proc/schedstat 文件中
(12) Collect kernel timers statistics
????????CONFIG_TIMER_STATS 定時器統計信息收集,保存在/procimer_stats 文件中
(13) Debug preemptible kernel
????????CONFIG_DEBUG_PREEMPT 使能內核搶占調試功能。如果在非搶占安全的狀況下使用,將打印警告信息,還可以探測搶占技術下溢。
(14) Lock Debugging (spinlocks, mutexes, etc...)
????????自旋鎖、互斥鎖的一些調試選項。
(15) kobject debugging
????????CONFIG_DEBUG_KOBJECT 使能一些額外的 kobject 調試信息發送到 syslog。
(16) Debug filesystem writers count?
????????CONFIG_DEBUGWRITECOUNT 使能后能捕對 vfsmount 結構中的針對寫者進行計數的成員的錯誤使用。
(17) Debug linked list manipulation
????????CONFIG_DEBUG_LIST 使能對鏈表使用的額外檢查。
(18) Debug SG table operations
????????CONFIG_DEBUG_SG使能對集一散表的檢查能幫助驅動找到未能正確初始化集一
散表的問題。
(19) Debug notifier call chains
????????CONFIG_DEBUG_NOTIFIERS 使能對通知調用鏈的完整性檢查,幫助內核開發者確定模塊正確地從通知調用鏈上注銷。
(20) Debug credential management
????????CONFIG DEBUG CREDENTIALS 使能一些對證書管理的調試檢查
(21) RCU Debugging
RCU 的一些調試選項。
(22) Force extended block device numbers and spread them
????????CONFIG_DEBUG_BLOCK_EXT_DEVT 用于強制大多數塊設備號是從擴展空間分配并延伸它們,以便發現那些假定設備號是按預先決定的連續設備號進行分配的內核或用戶代碼路徑。使能該選項可能導致內核啟動失敗。
(23) Notifier error injection
????????CONFIG_NOTIFIER_ERROR_INJECTION 提供人為向特定通知鏈回調的功能,如錯誤。
(24) Fault-injection framework
????????CONFIG_FAULT_INJECTION 提供失敗注入框架
(25) Tracers
????????跟蹤器的一些選項。
(26) Runtime Testing
????????運行時測試選項。
(27) Enable debugging of DMA-API usage
????????CONFIG_DMA_API_DEBUG 用于設備驅動對 DMA 的API函數的使用調試。
(28) Test module loading with "hello world' module
????????CONFIG_TEST_MODULE 編譯一個“test module”模塊,用于模塊加載測試
(29) Test user/kernel boundary protections
????????CONFIG_TEST_USER_COPY 編譯一個“test_user _copy”模塊,用于測試內核空間和用戶空間的數據復制 (copy_to/from_user) 是否能正常工作。
(30) Sample kernel code
????????CONFIG_SAMPLES 用于編譯一些內核的實例代碼,如 kobiect 和 kfifo 的實例代碼
(31) KGDB: kernel debugger
????????CONFIG_KGDB 內核遠程調試的選項。
(32) Export kernel pagetable layout to userspace via debugfs
????????CONFIG_ARM_PTDUMP 通過 debugfs 向用戶空間導出內核空間的頁表布局
(33) Filter access to /dev/mem
????????CONFIG_STRICT_DEVMEM 禁止該選項則允許用戶空間訪問整個內存,包括用戶空間和內核空間的所有內存。
(34) Enable stack unwinding support (EXPERIMENTAL)
????????CONFIG_ARM_UNWIND 使用編譯器在內核自動生成的信息來提供棧展開的支持
(35) Verbose user fault messages
????????CONFIG_DEBUG_USER 當一個應用程序因為異常崩潰時,內核打印一個是什么原因造成崩潰的簡短信息。
(36) Kernel low-level debugging functions
????????CONFIG_DEBUG_LL用于在內核中包含 printascii、printch 和 printhex 函數的定義這對于調試在控制臺初始化之前代碼會很有幫助。但是這會指定一個串口,給移植性帶來了一些問題。
(37) Kernel low-level debugging port (Use S3C UART 2 for low-level debug)
????????選擇串口 2 作為內核低級別調試輸出端口。
(38) Early printk
????????CONFIG EARLY PRINTK 使能內核的早期打印輸出
(39) On-chip ETM and ETB
????????CONFIG OC ETM 使能片上入的跟蹤宏單元跟蹤緩存驅動。
( 40) Write the current PID to the CONTEXTIDR register
????????CONFIG PID IN CONTEXTIDR 使能該選項后,內會把當前進程的 PID 寫入CONTEXTIDR 寄存器的 PROCID 域。
(41)Set loadable kernel module data as NX and text as RO
????????CONFIG DEBUG SET_MODULE_RONX 用于對可加模塊的代碼段和只讀數據段的意外修改。
三、內核打印函數
????????嵌入式系統一般都可以通過串口與用戶交互。大多數 Bootloader 可以向串口打印信息,并且接收命令。內核同樣可以向串口打印信息。但是在內核啟動過程中,不同階段的打印函數不同。分析這些打印函數的實現,可以更好地調試內核。
3.1內核鏡像解壓前的串口輸出函數
如果在配置內核時選擇了以下的選項:
System Type --->
????????(2) S3C UART to use for low-level messages
Kernel hacking --->????????[*] Kernel low-level debugging functions (read help!)
????????????????Kernel low-level debugging port (Use S3C UART 2 for low-level debug)
那么在內核自解壓時就會通過串口 2打印如下信息:
.Uncompressing Linux...done,?booting?the kernel
????????這句話的打印是因為在 decompresss_kernel()函數中調用了 putstr()函數,直接向串口打印內核解壓的信息。
????????putstr()函數實現了向串口輸出字符串的功能。因為不同的處理器可以有不同的串口控制器,所以putstr()函數的實現依賴于硬件平臺。下面分析一下 Exynos4412平臺中putstr()函數的使用及實現。
?
/** misc.c* * This is a collection of several routines from gzip-1.0.3 * adapted for Linux.** malloc by Hannu Savolainen 1993 and Matthias Urlichs 1994** Modified for ARM Linux by Russell King** Nicolas Pitre <nico@visuaide.com> 1999/04/14 :* For this code to run directly from Flash, all constant variables must* be marked with 'const' and all other variables initialized at run-time * only. This way all non constant variables will end up in the bss segment,* which should point to addresses in RAM and cleared to 0 on start.* This allows for a much quicker boot time.*/unsigned int __machine_arch_type;#include <linux/compiler.h> /* for inline */
#include <linux/types.h>
#include <linux/linkage.h>static void putstr(const char *ptr);
extern void error(char *x);#include CONFIG_UNCOMPRESS_INCLUDE#ifdef CONFIG_DEBUG_ICEDCC#if defined(CONFIG_CPU_V6) || defined(CONFIG_CPU_V6K) || defined(CONFIG_CPU_V7)static void icedcc_putc(int ch)
{int status, i = 0x4000000;do {if (--i < 0)return;asm volatile ("mrc p14, 0, %0, c0, c1, 0" : "=r" (status));} while (status & (1 << 29));asm("mcr p14, 0, %0, c0, c5, 0" : : "r" (ch));
}#elif defined(CONFIG_CPU_XSCALE)static void icedcc_putc(int ch)
{int status, i = 0x4000000;do {if (--i < 0)return;asm volatile ("mrc p14, 0, %0, c14, c0, 0" : "=r" (status));} while (status & (1 << 28));asm("mcr p14, 0, %0, c8, c0, 0" : : "r" (ch));
}#elsestatic void icedcc_putc(int ch)
{int status, i = 0x4000000;do {if (--i < 0)return;asm volatile ("mrc p14, 0, %0, c0, c0, 0" : "=r" (status));} while (status & 2);asm("mcr p14, 0, %0, c1, c0, 0" : : "r" (ch));
}#endif#define putc(ch) icedcc_putc(ch)
#endifstatic void putstr(const char *ptr)
{char c;while ((c = *ptr++) != '\0') {if (c == '\n')putc('\r');putc(c);}flush();
}/** gzip declarations*/
extern char input_data[];
extern char input_data_end[];unsigned char *output_data;unsigned long free_mem_ptr;
unsigned long free_mem_end_ptr;#ifndef arch_error
#define arch_error(x)
#endifvoid error(char *x)
{arch_error(x);putstr("\n\n");putstr(x);putstr("\n\n -- System halted");while(1); /* Halt */
}asmlinkage void __div0(void)
{error("Attempting division by 0!");
}unsigned long __stack_chk_guard;void __stack_chk_guard_setup(void)
{__stack_chk_guard = 0x000a0dff;
}void __stack_chk_fail(void)
{error("stack-protector: Kernel stack is corrupted\n");
}extern int do_decompress(u8 *input, int len, u8 *output, void (*error)(char *x));void
decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,unsigned long free_mem_ptr_end_p,int arch_id)
{int ret;__stack_chk_guard_setup();output_data = (unsigned char *)output_start;free_mem_ptr = free_mem_ptr_p;free_mem_end_ptr = free_mem_ptr_end_p;__machine_arch_type = arch_id;arch_decomp_setup();putstr("Uncompressing Linux...");ret = do_decompress(input_data, input_data_end - input_data,output_data, error);if (ret)error("decompressor returned an error");elseputstr(" done, booting the kernel.\n");
}
????????從上面的代碼分析可知,在arch/arm/boot/compressed/misc.c文件中調用了putstr函數,該函數循環打印字符,直到字符串結束,如果是換行符,再補充打印一個回車符,從而實現回車換行的效果。具體的打印由 putc 函數來實現,該函數被定義在 ????????arh/arm/platsamsung/include/plat/uncompress.h 文件中。putc 首先判斷了底層調試宏開關是否打開,如果不是則直接返回,如果是則進一步檢查是否使能了 FIFO。如果 FIFO 使能則一直等待,直到FIFO 可用,如果沒有使能,則一直等待發送緩沖可用,最后將要發送的字符寫入發送寄存器中。很明顯,數據是否能夠通過串口正常發送,需要依賴于在 U-Boot 中是否將串口正確初始化,這也是內核啟動代碼對 U-Boot 的一個求。不過 U-Boot 的代碼通常都會初始化一個串口來打印信息,所以這個條件通常也是滿足的。這里的 putstr 只在內核解壓時使用,內核解壓后調用不了該函數,而內核解壓部分的代碼幾乎不會出錯,所以驅動開發者很少使用該函數。
3.2 內核鏡像解壓后的串口輸出函數
????????在內核解壓完成后,跳轉到 vmlinux 鏡像入口,這時還沒有初始化控制臺設備,但是執行系統初始化的過程中也可能出現嚴重的錯誤,導致系統崩潰。怎樣才能報告這種錯誤信息呢?可以通過 printascii 子程序來向串口打印。
????????printascii,printhex8 等子程序包含在 arch/arm/kernel/debug.S 文件中。如果要編譯鏈
接這些子程序,需要內核使能“Kernel low-level debugging functions”選項。
????????printascii 子程序實現向串口打印字符串的功能,printhex 也用了 printascii 子程序來顯示數字。在 printascii 子程序中,調用了宏(macro): addruart、waituart、senduart、busyuart,這些宏都是在arch/arm/include/debug/exynos.S 和arch/arm/include/debug samsung.S /中定義的。printascii 函數的代碼如下。
/*
* linux/arch/arm/kernel/debug.S** Copyright (C) 1994-1999 Russell King** This program is free software; you can redistribute it and/or modify* it under the terms of the GNU General Public License version 2 as* published by the Free Software Foundation.** 32-bit debugging code*/
#include <linux/linkage.h>
#include <asm/assembler.h>.text/** Some debugging routines (useful if you've got MM problems and* printk isn't working). For DEBUGGING ONLY!!! Do not leave* references to these in a production kernel!*/#if !defined(CONFIG_DEBUG_SEMIHOSTING)
#include CONFIG_DEBUG_LL_INCLUDE
#endif#ifdef CONFIG_MMU.macro addruart_current, rx, tmp1, tmp2addruart \tmp1, \tmp2, \rxmrc p15, 0, \rx, c1, c0tst \rx, #1moveq \rx, \tmp1movne \rx, \tmp2.endm#else /* !CONFIG_MMU */.macro addruart_current, rx, tmp1, tmp2addruart \rx, \tmp1.endm#endif /* CONFIG_MMU *//** Useful debugging routines*/
ENTRY(printhex8)mov r1, #8b printhex
ENDPROC(printhex8)ENTRY(printhex4)mov r1, #4b printhex
ENDPROC(printhex4)ENTRY(printhex2)mov r1, #2
printhex: adr r2, hexbufadd r3, r2, r1mov r1, #0strb r1, [r3]
1: and r1, r0, #15mov r0, r0, lsr #4cmp r1, #10addlt r1, r1, #'0'addge r1, r1, #'a' - 10strb r1, [r3, #-1]!teq r3, r2bne 1bmov r0, r2b printascii
ENDPROC(printhex2)hexbuf: .space 16.ltorg#ifndef CONFIG_DEBUG_SEMIHOSTINGENTRY(printascii)addruart_current r3, r1, r2b 2f
1: waituart r2, r3senduart r1, r3busyuart r2, r3teq r1, #'\n'moveq r1, #'\r'beq 1b
2: teq r0, #0ldrneb r1, [r0], #1teqne r1, #0bne 1bmov pc, lr
ENDPROC(printascii)ENTRY(printch)addruart_current r3, r1, r2mov r1, r0mov r0, #0b 1b
ENDPROC(printch)#ifdef CONFIG_MMU
ENTRY(debug_ll_addr)addruart r2, r3, ipstr r2, [r0]str r3, [r1]mov pc, lr
ENDPROC(debug_ll_addr)
#endif#elseENTRY(printascii)mov r1, r0mov r0, #0x04 @ SYS_WRITE0ARM( svc #0x123456 )THUMB( svc #0xab )mov pc, lr
ENDPROC(printascii)ENTRY(printch)adr r1, hexbufstrb r0, [r1]mov r0, #0x03 @ SYS_WRITECARM( svc #0x123456 )THUMB( svc #0xab )mov pc, lr
ENDPROC(printch)ENTRY(debug_ll_addr)mov r2, #0str r2, [r0]str r2, [r1]mov pc, lr
ENDPROC(debug_ll_addr)#endif
????????首先調用了 addruart_current 獲得調試串口的物理地址和虛擬地址,調用返回后 r3 保存的是物理地址、r1 是虛擬地址、r2 是一個臨時寄存器。然后跳轉到局部標號 2 去執行代碼,r0 是指向要打印的字符串的指針,判斷不為空指針后,取出一個字符,并判斷是否是字符串的結尾,如果不是則跳轉到局部標號 1 執行代碼。從局部標號 1 開始,首先等待串口可用,然后發送字符,接下來等待發送完成,最后判斷要發送的字符是否是換行字符,如果是則補一個回車字符。在函數中調用的宏都比較簡單,這里就不再詳細分析了。
????????printascii 函數的使用也非常簡單,首先聲明該函數,然后傳入要打印的字符串指針即可,實例代碼如下。
?
extern void printascii(char *);
asmlinkage void __init start_kernel(void)
{char * command_line;extern const struct kernel_param __start__param[], __stop__param[];printascii("enter start_kernel\n");
......
3.3 內核打印函數
????????Linux 內核標準的系統打印函數是 printk。printk 函數具有極好的健壯性,不受內核運行條件的限制,在系統運行期間都可以使用。printk 日志級別如下所示。
日志級別一共有8個級別,printk的日志級別定義如下(在include/linux/kernel.h中):
?
#define KERN_EMERG 0/*緊急事件消息,系統崩潰之前提示,表示系統不可用*/#define KERN_ALERT 1/*報告消息,表示必須立即采取措施*/#define KERN_CRIT 2/*臨界條件,通常涉及嚴重的硬件或軟件操作失敗*/#define KERN_ERR 3/*錯誤條件,驅動程序常用KERN_ERR來報告硬件的錯誤*/#define KERN_WARNING 4/*警告條件,對可能出現問題的情況進行警告*/#define KERN_NOTICE 5/*正常但又重要的條件,用于提醒*/#define KERN_INFO 6/*提示信息,如驅動程序啟動時,打印硬件信息*/#define KERN_DEBUG 7/*調試級別的消息*/?
????????這些級別有助于內核控制信息的緊急程度,判斷是否向串口輸出等。正如 printk 函數的日志級別,printk 函數的實現也比較復雜。printk 函數不是直接向控制臺設備或串口
打印信息,而是把打印信息先寫到緩沖區里面。下面分析一下 printk 函數的代碼實現。
?
/* kernel/printk/printk.c */
/* 不指定級別的 printk 函數用這個默認級別·.....*/
#define DEFAULT_MESSAGE_LOGLEVEL CONFIG_DEFAULT_MESSAGE_IOGLEVEL /* KERN_WARNING級別*/
......
#define MINIMUM_CONSOLE_LOGLEVEL 1 /* 控制臺可以使用的最小級別數 */
#define DEFAULT_CONSOLE_LOGLEVEL 7 /* 任何比 KERN_DEBUG 更嚴重級別的信息都顯示 */
int console printk[4] = [ /* 定義控制臺的默認打印級別 */DEFAULT_CONSOLE_LOGLEVEL, /* console loglevel */DEFAULT_MESSAGE_LOGLEVEL, /* default message_loglevel */MINIMUM_CONSOLE_LOGLEVEL, /* minimum console loglevel */DEFAULT_CONSOLE_LOGLEVEL, /* default_console_loglevel */
};
......
/* 這是 printk 函數的實現,它可以在任何上下文中調用。
*對控制臺操作之前,先嘗試獲得 console_lock 鎖,
*如果成功,那么將會把輸出記錄下來并調用控制臺驅動程序;
*如果失敗,把輸出信息寫到日志緩沖區中,并立即返回。
*console sem 信號量的擁有者在 console_unlock 函數中
*將會發現有一個新的輸出,然后會在釋放這個鎖之前將輸出信息
*通過控制臺打印
asmlinkage int printk(const char *fmt, ...)
{va_list args;int r;
#ifdef CONFIG_KGDB_KDBif (unlikely(kdb_trap_printk)) {va_start(args,fmt);r = vkdb_printf(fmt, args);va_end(args);return r;}
#endifva_start(args,fmt); /*使用變參*/r = vprintk_emit(0, -1, NULL, 0, fmt, args); /* vprintk emit 函數完成打印任務*/va_end(args);return r;
}
EXPORT_SYMBOL(printk);asmlinkage int vprintk_emit(int facility, int level, const char *dict,size_t dictlen,const char *fmt,va_list args)
{static int recursion_bug;static char textbuf[LOG_LINE_MAX];char *text = textbuf;size_t text_len;enum log_flags lflags = 0;unsigned long flags;int this_cpu;int printed_len = 0;boot_delay_msec(level);/*取決于CONFIG_BOOT_PRINTK_DELAY宏是否被定義,用于控制內核啟動階段的打印延時*/printk_delay();/*打印延時控制 */local_irq_save(flags);/*關閉本地CPU的中斷并保存中斷使能標志*/this_cpu = smp_processor_id();/*獲取當前CPU的ID號*//*如果發生了遞歸調用*/if (unlikely(logbuf_cpu == this_cpu)) {/*如果在這個CPU上調用printk時內核崩潰,那么將嘗試獲得崩潰信息,*但要確保不會發生死鎖。否則立即返回,以避免遞歸,并將 recursion_bug*標志置位,以便在以后某個適當的時刻可以打印該信息*/if (!oops_in_progress && !lockdep_recursing(current)) {recursion_bug = 1;goto out_restore_irqs;}/* 強制初始化自旋鎖和信號量,但要留足夠的時間給慢速的控制臺*以便打印出完整的oops 信息*/zap_locks();}lockdep_off(); /*遞歸深度加一*/raw_spin_lock(&logbuf_lock); /*日志緩沖區上鎖*/logbuf_cpu = this_cpu; /*保存日志CPU的ID號*/if (recursion_bug) {/* 如果出現了遞歸的 bug,打印該信息 */static const char recursion_msg[] = "BUG: recent printk recursion!";recursion_bug = 0;printed_len += strlen(recursion_msg);/*將信息記錄到日志緩沖區 */log_store(0,2,LOG_PREFIX | LOG_NEWLINE,0,NULL,0,recursion_msg, printed_len);}/*將信息格式化輸出到text 指向的緩沖區中*/text_len = vscnprintf(text,sizeof(textbuf),fmt, args);/*如果有換行符則置位LOG_NEWLINE*/if (text_len && text[text_len-1] == '\n') {text_len--;lflags |= LOG_NEWLINE;}/* 如果打印來自內核,那么裁剪一些前綴并提取打印級別和控制信息 */if (facility == 0) (int kern_level = printk_get_level(text);if (kern_level) {const char *end_of_header = printk_skip_level(text);switch (kern_level) {case'0' ...'7':if (level == -1)level = kern_level -'0';case 'd': /* KERN_DEFAULT */lflags |= LOG_PREFIX;case'c':/* KERN CONT */break;}text_len -= end_of_header - text;text = (char *)end_of_header;}} /* 如果未指定打印級別,則使用默認的打印級別*/if (level == -1)level = default_message_loglevel;/* 如果輸出信息帶有鍵值對組成的字典,則設置相應的標志 */if (dict)lflags |= LOG_PREFIXILOG_NEWLINE;if (!(lflags & LOG_NEWLINE)) {/*一個早期的新行丟失或者另一個任務要繼續打印,刷新沖突的緩存*/if (cont.len &&(lflags & LOG_PREFIXI || cont.owner != current))cont_flush(LOG_NEWLINE);/*如果可能則緩存該行,否則立即保存下來*/if (!cont_add(facility, level,text,text_len))log_store(facility; level,lflags | LOG CONT,0, dict,dictlen,text,text_len);}else{bool stored= false;/*如果一個早期的新行正被丟失并且來自于同一任務,那么它將會和現在的緩存內容*合并并刷新輸出。但如果存在一個和中斷的競態,那么將會單存該行并刷新輸出。*如果先前的print來自于不同的任務并且丟掉了新行,那么刷新并追加新行*/if (cont.len) {if (cont.owner == current && !(lflags & LOG_PREFIX))stored = cont_add(facility, level, text, text_len);cont_flush(LOG NEWLINE);}if (!stored)log_store(facility, level, lflags,0, dict,dictlen,text, text len);}printed_len += text_len;/*嘗試獲得并立即釋放控制臺信號量。這將會引起緩存的打印輸出并喚醒*/dev/kmsg和syslog()的用戶*console_trylock_forprintk()函數將會釋放logbuf lock鎖,而不管其是否*獲得了控制臺信號量*/if (console_trylock_for_printk(this_cpu))console_unlock();lockdep on(); /*遞歸深度減一*/
out_restore_irqs:local_irq_restore(flags);/*恢復本地CPU的中斷使能標志*/return printed_len;
}
EXPORT_SYMBOL(vprintk_emit);
????????由以上的代碼可知,在控制臺初始化之前,printk 的輸出只能先保存在日志緩存中所以在控制臺初始化之前系統崩潰的話,將不會在控制臺上看到 printk 的打印輸出。
????????printk 的使用方法同 printf,但可以添加打印級別,示例代碼如下。
printk("%s\n", "default level")
printk(KERN_DEBUG "%s\n","debug-level messages");
printk(KERN_INFO "%s\n","informational");
printk(KERN_NOTICE "%s\n","normal but significant condition");
printk(KERN_WARNING "s\n", "warning conditions");
printk(KERN_ERR "%s\n", "error conditions");
printk(KERN_CRIT "s\n", "critical conditions");
printk(KERN_ALERT "s\n", "action must be taken immediately");
printk(KERN EMERG "sn","system is unusable");
????????如果 printk 中沒有加調試級別,則使用默認的調試級別。注意,調試級別和格式化字符串之間沒有逗號。當前控制臺的各打印級別可以通過下面的命令來查看。
cat /proc/sys/kernel/printk
4? ? ? ? 4? ? ? ? 1? ? ? ? 7
????????上面的信息表示控制臺當前的打印級別為 4 (KERN_WARNING),凡是打印級別小于等于(數值上大于等于) 該打印級別的信息都不會在控制臺上顯示; printk 的默認打印級別是 4,即 printk 中如果不指定打印級別,則使用4 的打印級別;控制臺能夠設置的最高打印級別為1(KERN_ALERT),默認的控制臺級別為7。使用下面的命令可以修改控制臺打印級別。
# echo"7 4 1 7" > /proc/sys/kernel/printk
????????如果要查看完整的控制臺打印信息,可以使用下面的命令。# dmesg
????????如果要實時查看控制臺打印信息,可以使用下面的命令。# cat /proc/kmsg
????????printk 只能在控制臺初始化完成以后看到輸出,這對調試來說極為不方便。為了能在早期看到 printk 的打印輸出,可以首先使能“Early printk”選項,然后在 bootargs 中添加earlyprintk 參數
?
四、獲取內核信息
????????Linux 內核提供了一些與用戶空間通信的機制,大部分驅動程序與用戶空間的接口都可以作為獲取內核信息的手段。而且,內核也有專門的調試機制。
4.1系統請求鍵
????????系統請求鍵可以使 Linux 內核回溯跟蹤進程,當然這要在 Linux 的鍵盤仍然可用的前提下,并且 Linux 內核已經支持 MAGIC_SYSRQ 功能模塊。
????????大多數系統平臺(特別是 X86)都已經實現了系統請求鍵功能,它是在 drivers/charsysrq.c 中實現的。在配置內核的時候需要選擇“Magic SysRq key”菜單選項,使能配選項CONFIG_MAGIC_SYSRQ。
????????使用這項功能,必須是在文本模式的控制臺上,并且啟動 CONFIG_MAGIC_SYSRO.
????????SysRq(系統請求)鍵是復合鍵[Alt+SysRq],大多數鍵盤的 SysRq 和 PrtSc 鍵是復用的。
????????按住 SysRq 復合鍵,再輸入第三個命令鍵,可以執行相應的系統調試命令。例如,輸入t鍵,可以得到當前運行的進程和所有進程的堆棧跟蹤。回溯跟蹤將被寫到/var/log/messages 文件中。如果內核都配置好了,系統應該已經轉換了內核的符號地址。
????????但是,在串口控制臺上不能使用 SsRq 復合鍵,可以先發送一個“BREAK”,在 5s之內輸入系統請求命令鍵。
????????另外,有些硬件平臺也不能使用 SysRg 復合鍵。不過,各種目標板都可以通過/proc接口進入系統請求狀態。
S echo t > /proc/sysrq-trigger
表 13.2 列出了系統請求鍵的命令解釋。更多信息可以查閱內核文檔 Documentationsysrq.txt。
鍵命令 | 說明 |
SysRq-b | 重啟機器 |
SysRq-e | 給init之外的所有進程發送SIGTERM信號 |
SysRq-i | 給init之外的所有進程發送SIGKILL信號 |
SysRq-k | 安全訪問鍵:殺掉這個控制臺上的所有進程 |
SysRq-l | 給包括init在內的所有進程發送SIGKILL信號 |
SysRq-m | 在控制臺上顯示內存信息 |
SysRq-o | 關閉機器 |
SysRq-p | 在控制臺上顯示寄存器 |
SysRq-r | 關閉鍵盤的原始模式 |
SysRq-s | 同步所有掛接的磁盤 |
SysRq-t | 在控制臺上顯示所有的任務信息 |
SysRq-u | 卸載所有已經掛載的磁盤 |
????????神奇的系統請求鍵是輔助調試或者拯救系統的重要方法,它為控制臺上的每個用戶提供了強大的功能。在系統宕機或者運行狀態不正常的時候,通過系統請求鍵可以查詢當前進程執行的狀態,從而判斷出錯的進程和函數。
4.2 通過/proc 接口
????????proc 文件系統是一種偽文件系統。實際上,它并不占用存儲空間,而是系統運行時在內存中建立的內核狀態映射,可以瞬時地提供系統的狀態信息。
????????在用戶空間,可以作為文件系統掛接到/proc 目錄下,提供給用戶訪問;可以通過 Shell命令掛接;可以在/etc/fstab 中做出相應的設置。
s mout -t proc proc /proc
????????通過 proc 文件系統可以查看運行中的內核、查詢和控制運行中的進程和系統資源等狀態。這對于監控性能、查找系統信息、了解系統是如何配置的以及更改該配置很有用。
????????在用戶空間,可以直接訪問/proc 目錄下的條目、讀取信息或者寫入命令。但是不能使用編輯器打開修改/proc 條目,因為在編輯過程中,同步保存的數據將是不完整的命令
????????在命令行下使用 echo 命令,從命令行將輸出重定向至/proc 下指定條目中。例如,關閉系統請求鍵功能的命令:
S echo 0 > /proc/sys/kernel/sysrq
????????在命令行下查看/proc 目錄下的條目信息,應該使用命令行下的 cat 命令。例如:
$ cat /proc/cpuinfo
????????另外,/proc 接口的條目可以作為普通的文件打開訪問。這些文件也有訪問的權限大部分條目是只讀的,少數用于系統控制的條目具有寫操作屬性。在應用程序中,可以通過 open()、read()、write0等函數操作。
????????/proc 中的每個條目都有一組分配給它的非常特殊的文件訪問權限,并且每個文件屬于特定的用戶標識。這一點實現得非常仔細,從而提供給管理員和用戶正確的功能。這些特定的訪問權限如下
(1) 只讀權限:任何用戶都不能對該文件進行寫操作,用于獲取系統信息。
(2) root 寫權限:如果/proc 中的某個文件是可寫的,則通常只能由 root 用戶來寫
(3) root 讀權限:有些文件對一般系統用戶是不可見的,只對 root 用戶是可見的。
(4)其他權限:可能有不同于以上常見的三種訪問權限的組合。
????????就具體/proc 條目的功能而言,每一個條目的讀寫操作在內核中都有特定的實現。當查看/proc 目錄下的文件時,會發現有些文件是可讀的,可以從中讀出內核的特定信息:
有些文件是可寫的,可以寫入特定的配置和控制命令。
????????Linux 的一些系統工具就是通過/proc 接口讀取信息的。例如,top 命令就是讀取/proc接口下相關條目的信息,實時地顯示當前運行中的進程和系統負載。要獲得/proc 文件的所有信息,一個最佳來源就是 Linux 內核源碼本身,它包含了一些非常優秀的文檔。
4.3 通過/sys 接口
????????Sysfs 文件系統是 Linux 2.6 內核新增加的文件系統。它也是一種偽文件系統,是在內存中實現的文件系統。它可以把內核空間的數據、屬性、鏈接等輸出到用戶空間。
????????在 Linux 2.6 內核中,sysfs 和 kobject 是緊密結合的,成為動程序型的組成部分。
????????當加載或者卸載 kobject 的時候,需要注冊或者注銷操作。當注冊 kobject 時,注冊勇數除了把 kobiect 插入到 kset 鏈表中,還在 syss 中創建對應的目錄。反過來,當注銷kobject 時,注銷函數也會刪除 sysfs 中相應的目錄。
????????通常,sysfs 文件系統要掛接到/sys 目錄下,給用戶提供訪問空間。可以通過 Shell命令掛接,也可以在/etc/fstab 中做出相應的設置。
S mount -t sysfs sysfs /sys
sysfs 文件系統的目錄組織結構反映了內核數據結構的關系。/sys 的目錄結構下應包含以下子目錄。
block/ bus/ class/ devices/ firmware/ net/
????????devices/目錄下的目錄樹代表設備樹,直接映射了內核內部的設備(按照 device結構的層次關系)。
????????bus/目錄包含內核各種總線類型的目錄。每一種總線目錄包含兩個子目錄: devices/和 drives/。
????????devices/目錄包含了系統探測到的每一個設備的符號鏈接,指向 sysfs 文件系統的root/目錄下的設備。
????????drivers/目錄包含了在特定總線結構上為每一個加載的設備驅動創建的子目錄
????????class/目錄包含設備接口類型的目錄,在類型子目錄下還有設備接口的子目錄
為了方便使用 sysfs,下面介紹一些 sysfs 的編程接口。
4.3.1.屬性
????????屬性能夠以文件系統的正常文件形式輸出到用戶空間。sysfs 文件系統間接調用屬性定義的函數操作,提供讀寫內核屬性的方法。
????????屬性應該是 ASCII 文本文件,每個文件只能有一個值。可能這樣效率不高,可以通過相同類型的數組來表示。
????????不贊成使用混合類型、多行數據格式和奇異的數據格式。這樣做可能使代碼得不到認可。
簡單的屬性定義示例如下:
struct attribute{char *name;umode_t mode;
};
int sysfs_create_file(struct kobject * kobj, struct attribute * attr);
void sysfs_remove_file(struct kobject *kobj, struct attribute * attr);
? ? ? ? 定義空洞的屬性是沒有用的,所以好針對轉定的目標類型添加自己的結構屬性或者封裝好的函數。
例如,設備驅動程序可以定義下面的結構 device attribute。
?
struct device_attribute{struct attribute attr;ssize_t (*show) struct device *dev, struct device_attribute *attr,char *buf);ssize_t (*store)(struct device *dev, struct device_attribute *attr,const char *buf, size_t count);
extern int device_create_file(struct device *device,const struct device_attribute *entry);
extern void device_remove_file(struct device *dev,const struct device_attribute *attr);
使用下面的宏可以簡化 device attribute 結構對象的定義和初始化。
#define __ATTR( _name, _mode, _show, _store) {.attr = {.name = ?__stringify(_name), .mode = ?_mode ),.show = show,.store = ?store,}#define DEVICE_ATTR(_name,_mode,_show, _store)\struct device_attribute dev_attr_##_name = __ATTR(_name, _mode,_show,_store)
舉例說明如何使用上面的宏來定義屬性。
????????static DEVICE_ATTR(foo, S_IWUSR | S_IRUGO, show_foo, store_foo);
等價于:
?
static struct device_attribute dev_attr_foo = {.attr = {.name = "foo",.mode = S_IWUSR | S_IRUGO,},.show = show_foo,.store = store_foo,
};
4.3.2子系統操作函數
????????當子系統定義了一個屬性類型時,必須實現一些 sysfs 操作函數。當應用程序調用read/write 函數時,通過這些子系統函數顯示或保存屬性值。
struct sysfs_ops {ssize_t (*show) (struct kobject *, struct attribute *, char *);ssize_t (*store) (struct kobject *, struct attribute *, const char *, size_t);
};
????????當讀或寫這個 sys 文件時,sys 調用對應的函數。然后,把通用的 kobjeet結構和結構屬性指針轉換成適當的指針類型,并且調用相關的函數。舉例說明:
?
#define to_dev_attr(_attr) container_of(_attr, struct device_attribute, attr)static ssize_t dev_attr_show(struct kobject *kobj, struct attribute *attr,char *buf)
{struct device_attribute *dev_attr = to_dev_attr(attr);struct device *dev = kobj_to_dev(kobj);ssize_t ret = -EIO;if (dev_attr->show)ret = dev_attr->show(dev, dev attr, buf);if (ret >= (ssize_t)PAGE_SIZE) {print_symbol("dev_attr_show: %s returned bad count\n",(unsigned long)dev_attr->show);}return ret;
}
要讀寫屬性,還要聲明和實現 show()?和 store()函數。這兩個函數的聲明如下:
ssize_t (*show)(struct device *dev, struct device_attribute *attr,char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr, const char *buf,size_t count);
讀寫函數的操作主要是數據緩沖區的讀寫操作,下面是一個最簡單的設備屬性實現的例子。
?
static ssize_t show_name(struct device *dev, struct device_attribute *attr, chaR *buf)
{return snprintf(buf, PAGE_SIZE, "%s\n", dev->name);
}
static ssize_t store_name(struct device * dev, const char * buf)
{sscanf(buf,"%20s",dev->name);return strnlen(buf,PAGE_SIZE);
}
static DEVICE_ATTR(name, S_IRUGO, show_name, store_name);
五、處理出錯信息
????????當系統出現錯誤時,內核有兩個基本的錯誤處理機制: oops 和 panic。
5.1 oops 信息
????????盡管有了各種調試方法,系統或驅動程序的一些 BUG 仍可能直接導致系統出錯,打印出 oops 信息。通常 oops 發生以后,系統處于不穩定狀態,可能崩潰,也可能繼續運行
5.1.1.oops 消息包含系統錯誤的詳細信息
????????通常 oops 信息中包含當前進程 (Task) 的回溯(Call Trace) 和 CPU 寄存器的內容。分析在發生崩潰時發送到系統控制臺的 oops 消息,這是 Linux 調試系統崩潰的傳統方法。oops 信息是機器指令級的,是很難懂的。ksymoops 工具可以將機器指令轉換為代碼并將堆棧值映射到內核符號。在很多情況下,這些信息就足夠確定錯誤的可能原因。
????????分析 oops 信息是一項很艱苦的工作,先來看看下面這些信息吧。
Oops: machine check, sig:7NIP: CO00F290 XER: 20000000 LR: CO00FOFO SP: CO13E940 REGS: C013f890 TRAP: 0200MSR: 00009030 EE: 1 PR: O FP:O ME: 1 IR/DR: 11TASK = c013e020[0] 'swapper' Last syscall: 120last math 00000000 last altivec 00000000GPR00: 00000000 C013E940 C013E020 000001F5 C500F200 C3A89000 00000002 C023BEA8GPR08: 00000007 00000570 0000017 0000015C 84002022 1002B4DC 00000000 00000000GPR16: 00000000 00000000 00000000 00000000 00001032 0013EA90 00000000 C00047CCGPR24: C0150000 000003C0 C07368C0 C013E9C8 000005EE C3A89000 C0160000 C0160000Call backtrace:C00334C8 C0160000 C000EE4C COOACE60 C00A9584 CO0AD258 COOAD008C00A879C C00057A4 C0005860 C00047CC 00000020 C00C1404 C00C146CC00A8C08 COOCE3C8 C00C59A4 C0ODA4A4 C0OD9068 C00DA608 C00D9340CO0E9224 C00E7A54 COOEFDE4 C00E032C COOD62CC COOD6504 C00C6060C00C6214 C00C6384 C001B820 C00058C8 C00047CCKernel panic: Aiee, killing interrupt handler!Warning (Oops read): Code line not seen, dumping what data is available
????????其中打印出了處理器寄存器的值,還有進程和棧回溯信息。對照 System.map 完全可以進行分析。
5.1.2.使用 ksymoops 轉換 oops 信息
????????ksymoops 工具可以翻譯 oops 信息,從而分析發生錯誤的指令,并顯示一個跟蹤部分表明代碼如何被調用。它是根據內核鏡像的 System.map 來轉換的,因此,必須提供正在運行的內核鏡像的 System.map 文件。
????????關于如何使用 ksymoops,內核源碼 Documentation/oops-tracingtxt 中或 ksymoops 手冊頁上有完整的說明可以參考。
????????將 oops 消息復制保存在一個文件中,通過 ksymoops 工具轉換它。
S ksymoops -m System.map < oops.txt
????????這樣 oops 信息就轉換成符號信息打印到控制臺上了。如果想把結果保存下來,可以把結果重定向到文件中。
5.1.3,內核 kallsyms 選項支持符號信息
????????Linux 2.6 內核引入了 kallsyms 特性,可以通過定義 CONFIG_KALLSYMS 配置選啟動。該選項可以載入內核鏡像對應內存地址的符號的名稱,內核可以直接跟蹤回溯函數名稱,而不再打印難懂的機器碼了。這樣,就不再需 System.map 和 ksymoops 工了。因為符號表要編譯到內核鏡像中,所以內核鏡像會變大,并且符號表永久駐留在內存中,對于開發來說,這也是值得的。
5.2 panic
當系統發生嚴重錯誤的時候,將調用 panic 函數。
那么 panic函數執行了哪些操作呢? 不妨分析一下 panic函數的實現。
?
/** linux/kernel/panic.c** Copyright (C) 1991, 1992 Linus Torvalds*//** This function is used through-out the kernel (including mm and fs)* to indicate a major problem.*/
#include <linux/debug_locks.h>
#include <linux/interrupt.h>
#include <linux/kmsg_dump.h>
#include <linux/kallsyms.h>
#include <linux/notifier.h>
#include <linux/module.h>
#include <linux/random.h>
#include <linux/ftrace.h>
#include <linux/reboot.h>
#include <linux/delay.h>
#include <linux/kexec.h>
#include <linux/sched.h>
#include <linux/sysrq.h>
#include <linux/init.h>
#include <linux/nmi.h>#define PANIC_TIMER_STEP 100
#define PANIC_BLINK_SPD 18int panic_on_oops = CONFIG_PANIC_ON_OOPS_VALUE;
static unsigned long tainted_mask;
static int pause_on_oops;
static int pause_on_oops_flag;
static DEFINE_SPINLOCK(pause_on_oops_lock);int panic_timeout = CONFIG_PANIC_TIMEOUT;
EXPORT_SYMBOL_GPL(panic_timeout);ATOMIC_NOTIFIER_HEAD(panic_notifier_list);EXPORT_SYMBOL(panic_notifier_list);static long no_blink(int state)
{return 0;
}/* Returns how long it waited in ms */
long (*panic_blink)(int state);
EXPORT_SYMBOL(panic_blink);/** Stop ourself in panic -- architecture code may override this*/
void __weak panic_smp_self_stop(void)
{while (1)cpu_relax();
}/*** panic - halt the system* @fmt: The text string to print** Display a message, then perform cleanups.** This function never returns.*/
/** panic - 停止系統運行
*參數 fmt: 要打印的字符串
*顯示信息,然后清理現場,不再返回
*/
void panic(const char *fmt, ...)
{static DEFINE_SPINLOCK(panic_lock);static char buf[1024];va_list args;long i, i_next = 0;int state = 0;/** Disable local interrupts. This will prevent panic_smp_self_stop* from deadlocking the first cpu that invokes the panic, since* there is nothing to prevent an interrupt handler (that runs* after the panic_lock is acquired) from invoking panic again.*/local_irq_disable();/** It's possible to come here directly from a panic-assertion and* not have preempt disabled. Some functions called from here want* preempt to be disabled. No point enabling it later though...** Only one CPU is allowed to execute the panic code from here. For* multiple parallel invocations of panic, all other CPUs either* stop themself or will wait until they are stopped by the 1st CPU* with smp_send_stop().*/if (!spin_trylock(&panic_lock))panic_smp_self_stop();console_verbose();bust_spinlocks(1);va_start(args, fmt);vsnprintf(buf, sizeof(buf), fmt, args);va_end(args);printk(KERN_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/** If we have crashed and we have a crash kernel loaded let it handle* everything else.* Do we want to call this before we try to display a message?*/crash_kexec(NULL);/** Note smp_send_stop is the usual smp shutdown function, which* unfortunately means it may not be hardened to work in a panic* situation.*/smp_send_stop();/** Run any panic handlers, including those that might need to* add information to the kmsg dump output.*/atomic_notifier_call_chain(&panic_notifier_list, 0, buf);kmsg_dump(KMSG_DUMP_PANIC);bust_spinlocks(0);if (!panic_blink)panic_blink = no_blink;if (panic_timeout > 0) {/** Delay timeout seconds before rebooting the machine.* We can't use the "normal" timers since we just panicked.*/printk(KERN_EMERG "Rebooting in %d seconds..", 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);}}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.*/emergency_restart();}
#ifdef __sparc__{extern int stop_a_enabled;/* Make sure the user can actually press Stop-A (L1-A) */stop_a_enabled = 1;printk(KERN_EMERG "Press Stop-A (L1-A) to return to the boot prom\n");}
#endif
#if defined(CONFIG_S390){unsigned long caller;caller = (unsigned long)__builtin_return_address(0);disabled_wait(caller);}
#endiflocal_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);struct tnt {u8 bit;char true;char false;
};static const struct tnt tnts[] = {{ TAINT_PROPRIETARY_MODULE, 'P', 'G' },{ TAINT_FORCED_MODULE, 'F', ' ' },{ TAINT_UNSAFE_SMP, 'S', ' ' },{ TAINT_FORCED_RMMOD, 'R', ' ' },{ TAINT_MACHINE_CHECK, 'M', ' ' },{ TAINT_BAD_PAGE, 'B', ' ' },{ TAINT_USER, 'U', ' ' },{ TAINT_DIE, 'D', ' ' },{ TAINT_OVERRIDDEN_ACPI_TABLE, 'A', ' ' },{ TAINT_WARN, 'W', ' ' },{ TAINT_CRAP, 'C', ' ' },{ TAINT_FIRMWARE_WORKAROUND, 'I', ' ' },{ TAINT_OOT_MODULE, 'O', ' ' },
};/*** print_tainted - return a string to represent the kernel taint state.** 'P' - Proprietary module has been loaded.* 'F' - Module has been forcibly loaded.* 'S' - SMP with CPUs not designed for SMP.* 'R' - User forced a module unload.* 'M' - System experienced a machine check exception.* 'B' - System has hit bad_page.* 'U' - Userspace-defined naughtiness.* 'D' - Kernel has oopsed before* 'A' - ACPI table overridden.* 'W' - Taint on warning.* 'C' - modules from drivers/staging are loaded.* 'I' - Working around severe firmware bug.* 'O' - Out-of-tree module has been loaded.** The string is overwritten by the next call to print_tainted().*/
const char *print_tainted(void)
{static char buf[ARRAY_SIZE(tnts) + sizeof("Tainted: ")];if (tainted_mask) {char *s;int i;s = buf + sprintf(buf, "Tainted: ");for (i = 0; i < ARRAY_SIZE(tnts); i++) {const struct tnt *t = &tnts[i];*s++ = test_bit(t->bit, &tainted_mask) ?t->true : t->false;}*s = 0;} elsesnprintf(buf, sizeof(buf), "Not tainted");return buf;
}int test_taint(unsigned flag)
{return test_bit(flag, &tainted_mask);
}
EXPORT_SYMBOL(test_taint);unsigned long get_taint(void)
{return tainted_mask;
}/*** add_taint: add a taint flag if not already set.* @flag: one of the TAINT_* constants.* @lockdep_ok: whether lock debugging is still OK.** If something bad has gone wrong, you'll want @lockdebug_ok = false, but for* some notewortht-but-not-corrupting cases, it can be set to true.*/
void add_taint(unsigned flag, enum lockdep_ok lockdep_ok)
{if (lockdep_ok == LOCKDEP_NOW_UNRELIABLE && __debug_locks_off())printk(KERN_WARNING"Disabling lock debugging due to kernel taint\n");set_bit(flag, &tainted_mask);
}
EXPORT_SYMBOL(add_taint);static void spin_msec(int msecs)
{int i;for (i = 0; i < msecs; i++) {touch_nmi_watchdog();mdelay(1);}
}/** It just happens that oops_enter() and oops_exit() are identically* implemented...*/
static void do_oops_enter_exit(void)
{unsigned long flags;static int spin_counter;if (!pause_on_oops)return;spin_lock_irqsave(&pause_on_oops_lock, flags);if (pause_on_oops_flag == 0) {/* This CPU may now print the oops message */pause_on_oops_flag = 1;} else {/* We need to stall this CPU */if (!spin_counter) {/* This CPU gets to do the counting */spin_counter = pause_on_oops;do {spin_unlock(&pause_on_oops_lock);spin_msec(MSEC_PER_SEC);spin_lock(&pause_on_oops_lock);} while (--spin_counter);pause_on_oops_flag = 0;} else {/* This CPU waits for a different one */while (spin_counter) {spin_unlock(&pause_on_oops_lock);spin_msec(1);spin_lock(&pause_on_oops_lock);}}}spin_unlock_irqrestore(&pause_on_oops_lock, flags);
}/** Return true if the calling CPU is allowed to print oops-related info.* This is a bit racy..*/
int oops_may_print(void)
{return pause_on_oops_flag == 0;
}/** Called when the architecture enters its oops handler, before it prints* anything. If this is the first CPU to oops, and it's oopsing the first* time then let it proceed.** This is all enabled by the pause_on_oops kernel boot option. We do all* this to ensure that oopses don't scroll off the screen. It has the* side-effect of preventing later-oopsing CPUs from mucking up the display,* too.** It turns out that the CPU which is allowed to print ends up pausing for* the right duration, whereas all the other CPUs pause for twice as long:* once in oops_enter(), once in oops_exit().*/
void oops_enter(void)
{tracing_off();/* can't trust the integrity of the kernel anymore: */debug_locks_off();do_oops_enter_exit();
}/** 64-bit random ID for oopses:*/
static u64 oops_id;static int init_oops_id(void)
{if (!oops_id)get_random_bytes(&oops_id, sizeof(oops_id));elseoops_id++;return 0;
}
late_initcall(init_oops_id);void print_oops_end_marker(void)
{init_oops_id();printk(KERN_WARNING "---[ end trace %016llx ]---\n",(unsigned long long)oops_id);
}/** Called when the architecture exits its oops handler, after printing* everything.*/
void oops_exit(void)
{do_oops_enter_exit();print_oops_end_marker();kmsg_dump(KMSG_DUMP_OOPS);
}#ifdef WANT_WARN_ON_SLOWPATH
struct slowpath_args {const char *fmt;va_list args;
};static void warn_slowpath_common(const char *file, int line, void *caller,unsigned taint, struct slowpath_args *args)
{disable_trace_on_warning();pr_warn("------------[ cut here ]------------\n");pr_warn("WARNING: CPU: %d PID: %d at %s:%d %pS()\n",raw_smp_processor_id(), current->pid, file, line, caller);if (args)vprintk(args->fmt, args->args);print_modules();dump_stack();print_oops_end_marker();/* Just a warning, don't kill lockdep. */add_taint(taint, LOCKDEP_STILL_OK);
}void warn_slowpath_fmt(const char *file, int line, const char *fmt, ...)
{struct slowpath_args args;args.fmt = fmt;va_start(args.args, fmt);warn_slowpath_common(file, line, __builtin_return_address(0),TAINT_WARN, &args);va_end(args.args);
}
EXPORT_SYMBOL(warn_slowpath_fmt);void warn_slowpath_fmt_taint(const char *file, int line,unsigned taint, const char *fmt, ...)
{struct slowpath_args args;args.fmt = fmt;va_start(args.args, fmt);warn_slowpath_common(file, line, __builtin_return_address(0),taint, &args);va_end(args.args);
}
EXPORT_SYMBOL(warn_slowpath_fmt_taint);void warn_slowpath_null(const char *file, int line)
{warn_slowpath_common(file, line, __builtin_return_address(0),TAINT_WARN, NULL);
}
EXPORT_SYMBOL(warn_slowpath_null);
#endif#ifdef CONFIG_CC_STACKPROTECTOR/** Called when gcc's -fstack-protector feature is used, and* gcc detects corruption of the on-stack canary value*/
void __stack_chk_fail(void)
{panic("stack-protector: Kernel stack is corrupted in: %p\n",__builtin_return_address(0));
}
EXPORT_SYMBOL(__stack_chk_fail);#endifcore_param(panic, panic_timeout, int, 0644);
core_param(pause_on_oops, pause_on_oops, int, 0644);static int __init oops_setup(char *s)
{if (!s)return -EINVAL;if (!strcmp(s, "panic"))panic_on_oops = 1;return 0;
}
early_param("oops", oops_setup);
????????panic()函數首先盡可能把出錯信息打印出來,再拉響警報,然后清理現場。這時候大概系統已經崩潰,需等待一段時間讓系統重啟。在開發調試過程中,可以讓 panic 打印更多信息或調試 panic 函數,從而分析系統出錯原因。
5.3 通過ioctl 方法
????????ioctl 是對一個文件描述符響應的系統調用,它可以實現特殊命令操作。ioctl 可以替代/proc 文件系統,實現一些調試的命令。
????????使用 ioctl 獲取信息比/proc 麻煩一些,因為通過應用程序的 ioctl 函數調用并且顯示結果必須編寫、編譯一個應用程序,并且與正在測試的模塊保持一致。反過來,驅動程序代碼比實現/proc 文件相對簡單一點。
????????大多數時候 ioct 是獲取信息的最好方法,因為它比讀/proc 運行得快。假如數據必須在打印到屏幕上之前處理,以二進制格式獲取數據將比讀一個文本文件效率更高。另外,ioctl 不需要把數據分割成小于一頁的碎片。
????????ioctl 還有一個優點,就是信息獲取命令可以保留在驅動程序中,即使已經完成調試工作。不像/proc 文件,在目錄下所有人都可以看到。
????????在內核空間,ioctl 驅動程序函數原型如下。
long (*unlocked ioctl) (struct file *filp, unsigned int cmd, unsigned long arg);
????????filp 指針指向一個打開的文件所對應的 file 結構,cmd 參數是從用戶空間未加修改傳遞過來的,可選的參數 arg 以無符號長整數傳遞,可以使用整數或指針。如果調用這個函數的時候不傳遞第 3 個參數,驅動程序接收的 arg 是未定義的。因為對于額外的參數的類型檢查已經關閉,編譯器不會警告一個非法的參數傳遞給 ioctl,并且任何相關的 BUG都將很難查找。
????????大多數 ioctl 實現包含了一個大的 switch 語句,可以根據 cmd 參數選擇適當的操作。不同的命令有不同的數值,可以通過宏定義簡化編程。定制的驅動可以在頭文件中聲明這些符號。用戶程序也必須包含這些頭文件,以便使用這些符號。
????????用戶空間可以使用 ioctl 系統調用。
????????int ioctl(int d,int request, ...);
????????原型函數的省略號標志表明這個函數可以傳遞數量可變的參數。在實際系統中,系統調用不能用數量可變的參數。系統調用必須使用定義好的原型,因為用戶可以通過硬件操作來訪問。因此,這些省略號不代表變參,而是一個可選參數,傳統上定義為 char *argp。原型的省略號可以防止編譯過程的類型檢查。第了個參數的本質依賴于特定的控制命令(第2個參數)。有些命令沒有參數,有些取整型參數,有些取數據指針。使用指針可以把任意數據傳遞給 ioct 函數,設備就可以與用戶空間交互任意大小的數據塊了。
????????ioctl 函數的不規范性使內核開發者并不喜歡它。每一個 ioctl 命令是一個分離的非正式的系統調用,并且沒有辦法按照易于理解的方式整理,也很難使這些不規范的 ioctl參數在所有的系統上都能工作。例如,用戶空間進程運行 32 位模式的 64位系統,這導致強烈需要實現其他方式的多種控制操作。可行的方式包括在數據流中嵌入命令或者使用虛擬文件系統,以及 sysfs 或者驅動程序相關的文件系統。但是,事實上,ioctl?仍然是對設備操作最簡單和最直接的選擇。
六、內核源碼調試
????????因為 Linux 內核程序是 GNU GCC 編譯的,所以對應地使用GNU GDB 調試器。Linux應用程序需要 gdbserver 輔助交叉調試。那么內核源碼調試時,誰來充當 gdbserver 的角色呢?
????????KGDB 是 Linux 內核調試的一種機制。它使用遠程主機上的 GDB 調試目標板上的Linux 內核。準確地說,KGDB 是內核的功能擴展,它在內核中使用插 (Stub)的機制內核在啟動時等待遠程調試器的連接,相當于實現了 gdbserver 的功能。然后,遠程主機的調試器 GDB 負責讀取內核符號表和源碼,并且建立連接。接下來,就可以在內核源碼中設置斷點、檢查數據并進行其他操作。
????????KGDB 的調試模型如圖所示。
????????在圖中,KGDB 調試需要一臺開發主機和一臺目標板,開發主機和目標板之間通過一條串口線(null 調制解器電)連接。內源碼在開發機器上編譯且通過 GDB調試,內核鏡像下載到目標機上運行,兩者通過串口進行通信,Linux 2.6 內核還增加以太網接口通信的方式。
下面詳細說明通過串口來調試 3.14.25 內核的步驟。
(1)配編譯 Linux 內核鏡像。
內核的配置選項如下。
?
Kernel hacking --->
????????Compile-time checks and compiler options --->
????????????????[*] Compile the kernel with debug info
????????[*] KGDB: kernel debugger --->
????????????????<*> KGDB: use kgdb over the serial console
(2) 在目標板上啟動內核。
啟動開發板,在 U-Boot 中重新設置 bootargs 環境變量,添加如下啟動參數
kgdboc=ttySAC2,115200 kgdbwait
????????kgdboc 表示用串口進行連接 (kgdboe 表示通過以太網口進行連接)。ttySAC2表示使用串口 2,這里需要注意的是,串口號必須要和控制臺串口保持一致,否則連接不成功。115200 表示使用的波特率。kgdbwait 表示內核的串口驅動加載成功后,將會等待主機的gdb 連接。通過 U-Boot 加載并啟動內核,運行正常將會出現下面的信息,然后內核等待連接。
0.550000] Serial: 8250/16550 driver,4 ports,IRQ sharing disabled0.550000] 13800000.serial: ttysAcO at MMIO 0x13800000 (irg = 84, base baud0) is a s3C6400/10
0.555000] 13810000.serial: ttysAcl at MMIO 0x13810000 (irg = 85, base baud0) is a s3C6400/10
0.555000] 13820000.serial: ttysAC2 at MMIO 0x13820000 (irg = 86,base baud =
0) is a s3C6400/101.200000] console [ttysAC2] enabled
1.205000] 13830000.serial: ttysAC3 at MMIO 0x13830000 (irq = 87, base baud0) is a s3C6400/10
1.215000] kgdb: Registered I/0 driver kgdboc.
1.220000] kgdb: Waiting for connection from remote gdb..
成功看到上面的打印信息后,需要關閉串口終端軟件,否則將會和 GDB 產生沖突另外,在 Linux 主機中通過下面的命令來修改串口設備文件的權限。$ sudo chmod 666 /dev/ttyuSBO
ttyUSBO表示連接開發板的主機上的串口。
(3)啟動 gdb,建立連接。
????????創建一個gdb 啟動腳本文件,名字為,gdbinit,保存在內核源文件目錄中。腳本.gdbinit內容如下。
?
#.gdbinit
set remotebaud 115200
symbol-file vmlinux
target remote /dev/ttyUSB0
set output-radix 16
????????到內核源碼樹頂層目錄下,啟動交叉工具鏈的 gdb 工具。.gdbinit 腳本將在 gdb啟動過程中自動執行。如果一正常,目標板連接成功進入調試模式。常見的情況是連接不成功,可能是因為串口設置或者連接不正確。使用的命令及輸出如下。
$ arm-linux-gdb
GNU gdb 6.8
Copyright (C) 2008 Free Software Foundation, Inc,
License GPLv3+: GNU GPL version 3 or later <http;//gnu.org/11censes/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as --host=i686-build pc-linux-gnu --target=armcortex a8-linux-gnueabi".0xc0078b68 in kgdb_breakpoint () at /home/kevin/workspace/fs4412/kernel/1inux3.14.25/arch/arm/include/asm/outercache.h:103103outer cache.sync();
(gdb)
(4) 使用 gdb 的調試命令設置斷點,跟蹤調試。
????????找到內核源碼適當的函數位置,設置斷點,繼續執行。這樣就可以進行內核源碼的調試了。
七、習題
1.要使用 printascii 函數,需要在內核配置時使能哪個選項( )。
{B] Kernel low-level debugging functions
[A] KGDB: kernel debugger
D] printk and dmesg options
[C] Panic on Oops
2.通過哪個文件可以查看并修改當前控制臺的各個打印級別( )。
[B] /proc/kmsg
[A] /proc/devices
[C] /proc/sys/kernel/printk
[D] /var/log/dmesg
3.系統請求鍵是哪兩個鍵的復合(
[B] Ctrl+SysRq
[A]Alt+SysRq
{D] Shift+Ctrl+SysRq
[C] Shift+SysRq
4.通過 sys 接口讀取屬性,需要實現哪個接口函數 ( )。
(D] print
[C] store
[B] show
[A]read
5.哪個工具可以用來轉換 oops 信息 ( )。
[D] panic
[B] kallsyms
[A] ksymoops
[C] oops
6.使用 KGDB,通過串口調試內核,需要在 bootargs 中添加哪個參數 ( )
[C] ttyUSBO
[C] init
[B] ttySAC2
[A] kgdboc
?
答案: B? ? ? ? C? ? ? ? A? ? ? ? B? ? ? ? A? ? ? ? A