1. 程式人生 > >linux 核心除錯方法

linux 核心除錯方法

kdb:只能在彙編程式碼級進行除錯;

  優點是不需要兩臺機器進行除錯。

  gdb:在除錯模組時缺少一些至關重要的功能,它可用來檢視核心的執行情況,包括反彙編核心函式。

  kgdb:能很方便的在原始碼級對核心進行除錯,缺點是kgdb只能進行遠端除錯,它需要一根串列埠線及兩臺機器來除錯核心(也可以是在同一臺主機上用vmware軟體執行兩個作業系統來除錯)

printk() 是除錯核心程式碼時最常用的一種技術。在核心程式碼中的特定位置加入printk() 除錯呼叫,可以直接把所關心的資訊打列印到螢幕上,從而可以觀察程式的執行路徑和所關心的變數、指標等資訊。 Linux 核心偵錯程式(Linux kernel debugger,kdb)是 Linux 核心的補丁,它提供了一種在系統能執行時對核心記憶體和資料結構進行檢查的辦法。Oops、KDB在文章掌握 Linux 除錯技術有詳細介紹,大家可以參考。 Kprobes 提供了一個強行進入任何核心例程,並從中斷處理器無干擾地收集資訊的介面。使用 Kprobes 可以輕鬆地收集處理器暫存器和全域性資料結構等除錯資訊,而無需對Linux核心頻繁編譯和啟動,具體使用方法,請參考使用 Kprobes 除錯核心。

/proc檔案系統

在 /proc 檔案系統中,對虛擬檔案的讀寫操作是一種與核心通訊的手段,要檢視核心迴環緩衝區中的訊息,可以使用 dmesg 工具(或者通過 /proc 本身使用 cat /proc/kmsg 命令)。清單 6 給出了 dmesg 顯示的最後幾條訊息。

清單 6. 檢視來自 LKM 的核心輸出

[root@plato]# dmesg | tail -5
cs: IO port probe 0xa00-0xaff: clean.
eth0: Link is down
eth0: Link is up, running at 100Mbit half-duplex
my_module_init called.  Module is now loaded.
my_module_cleanup called.  Module is now unloaded.


可以在核心輸出中看到這個模組的訊息。現在讓我們暫時離開這個簡單的例子,來看幾個可以用來開發有用 LKM 的核心 API。

除錯工具

  使用偵錯程式來一步步地跟蹤程式碼,檢視變數和計算機暫存器的值。在核心中使用互動式偵錯程式是一個很複雜的問題。核心在它自己的地址空間中執行。許多使用者空間下的偵錯程式所提供的常用功能很難用於核心之中,比如斷點和單步除錯等。

核心bug跟蹤

oops訊息分析

(1)oops訊息產生機制

oops(也稱 panic),稱程式執行崩潰,程式崩潰後會產生oops訊息。應用程式或核心執行緒的崩潰都會產生oops訊息,通常發生oops時,系統不會發生宕機,而在終端或日誌中列印oops資訊。

當使用NULL指標或不正確的指標值時,通常會引發一個 oops 訊息,這是因為當引用一個非法指標時,頁面對映機制無法將虛擬地址映像到實體地址,處理器就會向作業系統發出一個"頁面失效"的訊號。核心無法"換頁"到並不存在的地址上,系統就會產生一個"oops"。

oops 顯示發生錯誤時處理器的狀態,包括 CPU 暫存器的內容、頁描述符表的位置,以及其一些難理解的資訊。這些訊息由失效處理函式(arch/*/kernel/traps.c)中的printk 語句產生。較為重要的資訊就是指令指標(EIP),即出錯指令的地址。

由於很難從十六進位制數值中看出含義,可使用符號解析工具klogd。klogd 守護程序能在 oops 訊息到達記錄檔案之前對它們解碼。klogd在預設情況下執行並進行符號解碼。

通常Oops文字由klogd從核心緩衝區裡讀取並傳給syslogd,由syslogd寫到syslog檔案中,該檔案典型為/var/log/messages(依賴於/etc/syslog.conf)。如果klogd崩潰了,使用者可"dmesg > file"從核心緩衝區中讀取資料並儲存下來。還可用"cat /proc/kmsg > file"讀取資料,此時,需要使用者中止傳輸,因為kmsg是一個"永不結束的檔案"。

當保護錯誤發生時,klogd守護程序自動把核心日誌資訊中的重要地址翻譯成它們相應的符號。klogd執行靜態地址翻譯和動態地址翻譯。靜態地址翻譯使用System.map檔案將符號地址翻譯為符號。klogd守護程序在初始化時必須能找到system.map檔案。

動態地址翻譯通常對核心模組中的符號進行翻譯。核心模組的記憶體從核心動態記憶體池裡分配,核心模組中符號的位置在核心裝載後才最終確定。

Linux核心提供了呼叫,允許程式決定裝載哪些模組和它們在記憶體中位置。通過這些系統呼叫,klogd守護程序生成一張符號表用於除錯發生在可裝載模組中的保護錯誤。核心模組的裝載或者解除安裝都會自動向klogd傳送訊號,klogd可將核心模組符號的地址動態翻譯為符號字串。

(2)產生oops的樣例程式碼

使用空指標和緩衝區溢位是產生oops的兩個最常見原因。下面兩個函式faulty_write和faulty_read是一個核心模組中的寫和讀函式,分別演示了這兩種情況。當核心呼叫這兩個函式時,會產生oops訊息。

函式faulty_write刪除一個NULL指標的引用,由於0不是一個有效的指標值,核心將列印oops資訊,並接著,殺死呼叫些函式的程序。
ssize_t faulty_write (struct file *filp, const char _ _user *buf, size_t count, loff_t *pos)
{
    /* make a simple fault by dereferencing a NULL pointer */
    *(int *)0 = 0;
    return 0;
}

函式faulty_write產生oops資訊列出如下(注意 EIP 行和 stack 跟蹤記錄中已經解碼的符號):
Unable to handle kernel NULL pointer dereference at virtual address \
   00000000 

printing eip: c48370c3 *pde = 00000000 Oops: 0002 CPU:0 EIP: 0010:[faulty:faulty_write+3/576] EFLAGS: 00010286 eax: ffffffea ebx: c2c55ae0 ecx: c48370c0 edx: c2c55b00 esi: 0804d038 edi: 0804d038 ebp: c2337f8c esp: c2337f8c ds: 0018 es: 0018 ss: 0018 Processcat (pid: 23413,stackpage=c2337000) Stack: 00000001 c01356e6 c2c55ae0 0804d038 00000001 c2c55b00 c2336000 \

          00000001 
     0804d038 bffffbd4 00000000 00000000 bffffbd4 c010b860 00000001 \ 
          0804d038 
     00000001 00000001 0804d038 bffffbd4 00000004 0000002b 0000002b \ 
          00000004 

Call Trace: [sys_write+214/256][system_call+52/56]

Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00

上述oops訊息中,字串 3/576 表示處理器正處於函式的第3個位元組上,函式整體長度為 576 個位元組。 函式faulty_read拷貝一個字串到本地變數,由於字串比目的地陣列長造成緩衝區溢位。當函式返回時,緩衝區溢位導致產生oops資訊。因為返回指令引起指令指標找不到執行地址,這種錯誤很難發現和跟蹤。
ssize_t faulty_read(struct file *filp, char _ _user *buf, size_t count, loff_t *pos)
{
    int ret;
    char stack_buf[4];
    /* Let's try a buffer overflow */
    memset(stack_buf, 0xff, 20);
    if (count > 4)
        count = 4;
    /* copy 4 bytes to the user */
    ret = copy_to_user(buf, stack_buf, count);
    if (!ret)
        return count;
    return ret;
}

函式faulty_read產生oops資訊列出如下:
EIP: 0010:[<00000000>]

Unable to handle kernel paging request at virtual address ffffffff printing eip: ffffffff Oops: 0000[#5] SMP CPU: 0 EIP: 0060:[] Not tainted EFLAGS: 00010296(2.6.6) EIP is at 0xffffffff eax: 0000000c ebx: ffffffff ecx: 00000000 edx: bfffda7c esi: cf434f00 edi: ffffffff ebp: 00002000 esp: c27fff78 ds: 007b es: 007b ss: 0068 Processhead (pid: 2331,threadinfo=c27fe000 task=c3226150) Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7 bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000 00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70 Call Trace:[] sys_read+0x42/0x70[] syscall_call+0x7/0xb

Code: Bad EIP value.

在上述oops訊息中,由於緩衝區溢位,僅能看到函式呼叫棧的一部分,看不見函式名vfs_read和faulty_read,並且程式碼(Code)處僅輸出"bad EIP value.",列在棧上開始處的地址"ffffffff"表示核心棧已崩潰。

(3)oops資訊分析

面對產生的oops資訊,首先應查詢源程式發生oops的位置,通過檢視指令指令暫存器EIP的值,可以找到位置,如:EIP: 0010:[faulty:faulty_write+3/576]。

再查詢函式呼叫棧(call stack)可以得到更多的資訊。從函式呼叫棧可辨別出局部變數、全域性變數和函式引數。例如:在函式faulty_read的oops資訊的函式呼叫棧中,棧頂為ffffffff,棧頂值應為一個小於ffffffff的值,為此值,說明再找不回撥用函式地址,說明有可能因緩衝區溢位等原因造成指標錯誤。

在x86構架上,使用者空間的棧從0xc0000000以下開始,遞迴值bfffda70可能是使用者空間的棧地址。實際上它就是傳遞給read系統呼叫的緩衝區地址,系統呼叫read進入核心時,將使用者空間緩衝區的資料拷貝到核心空間緩衝區。

如果oops資訊顯示觸發oops的地址為0xa5a5a5a5,則說明很可能是因為沒有初始化動態記憶體引起的。

另外,如果想看到函式呼叫棧的符號,編譯核心時,請開啟CONFIG_KALLSYMS選項。

klogd 提供了許多資訊來幫助分析。為了使 klogd 正確地工作,必須在 /boot 中提供符號表檔案 System.map。如果符號表與當前核心不匹配,klogd 就會拒絕解析符號。

有時核心錯誤會將系統完全掛起。例如程式碼進入一個死迴圈,系統不會再響應任何動作。這時可通過在一些關鍵點上插入 schedule 呼叫可以防止死迴圈。

系統崩潰重啟動

由於核心執行錯誤,在某些極端情況下,核心會執行崩潰,核心崩潰時會導致宕機。為了解決此問題,核心引入了快速裝載和重啟動新核心機制。核心通過kdump在崩潰時觸發啟動新核心,儲存舊記憶體映像以便於除錯,讓系統在新核心上執行 ,從而避免了宕機,增強了系統的穩定性。

(1)工具kexec介紹

kexec是一套系統呼叫,允許使用者從當前正執行的核心裝載另一個核心。使用者可用shell命令"yum install kexec-tools"安裝kexec工具包,安裝後,就可以使用kexec命令。

工具kexec直接啟動進入一個新核心,它通過系統呼叫使使用者能夠從當前核心裝載並啟動進入另一個核心。在當前核心中,kexec執行BootLoader的功能。在標準系統啟動和kexec啟動之間的主要區別是:在kexec啟動期間,依賴於硬體構架的韌體或BIOS不會被執行來進行硬體初始化。這將大大降低重啟動的時間。

為了讓核心的kexec功能起作用,核心編譯配置是應確認先擇了"CONFIG_KEXEC=y",在配置後生成的.config檔案中應可看到此條目。

工具kexec的使用分為兩步,首先,用kexec將除錯的核心裝載進記憶體,接著,用kexec啟動裝載的核心。

裝載核心的語法列出如下:

kexec -l kernel-image --append=command-line-options --initrd=initrd-image

上述命令中,引數kernel-image為裝載核心的對映檔案,該命令不支援壓縮的核心映像檔案bzImage,應使用非壓縮的核心對映檔案vmlinux;引數initrd-image為啟動時使用initrd對映檔案;引數command-line-options為命令列選項,應來自當前核心的命令列選項,可從檔案"/proc/cmdline"中提取,該檔案的內容列出如下:

^-^$ cat /proc/cmdline

ro root=/dev/VolGroup00/LogVol00 rhgb quiet

例如:使用者想啟動的核心對映為/boot/vmlinux,initrd為/boot/initrd,則kexec載入命令列出如下:

Kexec –l /boot/vmlinux –append=/dev/VolGroup00/LogVol00 initrd=/boot/initrd

還可以加上選項-p或--load-panic,表示裝載新核心在系統核心崩潰使用。

在核心裝載後,用下述命令啟動裝載的核心,並進行新的核心中執行:

kexec -e

當kexec將當前核心遷移到新核心上執行時,kexec拷貝新核心到預保留記憶體塊,該保留位置如圖1所示, 原系統核心給kexec裝載核心預保留一塊記憶體(在圖中的陰影部分),用於裝載新核心,其他記憶體區域在未裝載新核心時,由原系統核心使用。

Linux kernel debug method 02.png

圖1 kexec裝載的核心所在預保留位置示意圖

在x86構架的機器上,系統啟動時需要使用第一個640KB實體記憶體,用於核心裝載,kexec在重啟動進入轉儲捕捉的核心之前備份此區域。相似地,PPC64構架的機器在啟動裡需要使用第一個32KB物理核心,並需要支援64K頁,kexec備份第一個64KB記憶體。

(2)kdump介紹

kdump是基於kexec的崩潰轉儲機制(kexec-based Crash Dumping),無論核心核心需要轉儲時,如:系統崩潰時,kdump使用kexec快速啟動進入轉儲捕捉的核心。在這裡,原執行的核心稱為系統核心或原核心,新裝載執行的核心稱為轉儲捕捉的核心或裝載核心或新核心。

在重啟動過程中,原核心的記憶體映像被儲存下來,並且轉儲捕捉的核心(新裝載的核心)可以訪問轉儲的映像。使用者可以使用命令cp和scp將記憶體對映拷貝到一個本地硬碟上的轉儲檔案或通過網路拷貝到遠端計算機上。

當前僅x86, x86_64, ppc64和ia64構架支援kdump和kexec。

當系統核心啟動時,它保留小部分記憶體給轉儲(dump)捕捉的核心,確保了來自系統核心正進行的直接記憶體訪問(Direct Memory Access:DMA)不會破壞轉儲捕捉的核心。命令kexec –p裝載新核心到這個保留的記憶體。

在崩潰前,所有系統核心的核心映像編碼為ELF格式,並存儲在核心的保留區域。ELF頭的開始實體地址通過引數elfcorehdr=boot傳遞到轉儲捕捉的核心。

通過使用轉儲捕捉的核心,使用者可以下面兩種方式訪問記憶體映像或舊記憶體:

(1)通過/dev/oldmem裝置介面,捕捉工具程式能讀取裝置檔案並以原始流的格式寫出記憶體,它是一個記憶體原始流的轉儲。分析和捕捉工具必須足夠智慧以判斷查詢正確資訊的位置。

(2)通過/proc/vmcore,能以ELF格式檔案輸出轉儲資訊,使用者可以用GDB(GNU Debugger)和崩潰除錯工具等分析工具除錯轉儲檔案。

(3)建立快速重啟動機制和安裝工具

1)安裝工具kexec-tools

可以下載原始碼編譯安裝工具kexec-tools。由於工具kexec-tools還依賴於一些其他的庫,因此,最好的方法是使用命令"yum install kexec-tools"從網上下載安裝並自動解決依賴關係。

2)編譯系統和轉儲捕捉的核心

可編譯獨立的轉儲捕捉核心用於捕捉核心的轉儲,還可以使用原系統核心作為轉儲捕捉核心,在這種情況下,不需要再編譯獨立的轉儲捕捉核心,但僅支援重定位核心的構架才可以用作轉儲捕捉的核心,如:構架i386和ia64支援重定位核心。

對於系統和轉儲捕捉核心來說,為了開啟kdump支援,核心需要設定一些特殊的配置選項,下面分別對系統核心和轉儲捕捉核心的配置選項進行說明:

系統核心的配置選項說明如下:

  • 在選單條目"Processor type and features."中開啟選項"kexec system call",使核心編譯安裝kexe系統呼叫。配置檔案.config生成語句"CONFIG_KEXEC=y"。
  • 在選單條目"Filesystem"->"Pseudo filesystems."中開啟選項"sysfs file system support",使核心編譯安裝檔案系統sysfs.配置檔案.config生成語句"CONFIG_SYSFS=y"。
  • 在選單條目"Kernel hacking."中開啟選項"Compile the kernel with debug info ",使核心編譯安裝後支援除錯資訊輸出,產生除錯符號用於分析轉儲檔案。配置檔案.config生成語句"CONFIG_DEBUG_INFO=Y"。

轉儲捕捉核心配置選項(不依賴於處理器構架)說明如下:

  • 在選單條目"Processor type and features"中開啟選項"kernel crash dumps",配置檔案.config生成語句" CONFIG_CRASH_DUMP=y"。
  • 在選單條目"Filesystems"->"Pseudo filesystems"中開啟選項"/proc/vmcore support",配置檔案.config生成語句"CONFIG_PROC_VMCORE=y"。

轉儲捕捉核心配置選項(依賴於處理器構架i386和x86_64)說明如下:

  • 在處理器構架i386上,在選單條目"Processor type and features"中開啟高階記憶體支援,配置檔案.config生成語句"CONFIG_HIGHMEM64G=y"或"CONFIG_HIGHMEM4G"。
  • 在處理器構架i386和x86_64上,在選單條目"rocessor type and features"中關閉對稱多處理器支援,配置檔案.config生成語句"CONFIG_SMP=n"。如果配置檔案中的設定為"CONFIG_SMP=y",則可在裝載轉儲捕捉核心的核心命令列上指定"maxcpus=1"。
  • 如果想構建和使用可重定位核心,在選單條目"rocessor type and featuresIf"中開啟選項"Build a relocatable kernel",配置檔案.config生成語句"CONFIG_RELOCATABLE=y"。
  • 在選單"Processor type and features"下的條目"Physical address where the kernel is loaded"設定合適的值用於核心裝載的實體地址。它僅在打開了"kernel crash dumps"時出現。合適的值依賴於核心是否可重定位。

如果設定了值"CONFIG_PHYSICAL_START=0x100000",則表示使用可重定位核心。它將編譯核心在實體地址1MB處,核心是可重定位的,因此,核心可從任何實體地址執行。Kexec BootLoader將裝載核心到用於轉儲捕捉核心的核心保留區域。

否則,將使用啟動引數"[email protected]"指定第二個核心保留核心區域的開始地址,其中,Y表示記憶體區域的大小,X表示保留給轉儲捕捉核心的記憶體區域的開始地址,通過X為16MB (0x1000000),因此使用者可設定"CONFIG_PHYSICAL_START=0x1000000"。

在配置完核心後,編譯和安裝核心及核心模組。

3)擴充套件的crashkernel語法

在系統核心的啟動命令列選項中,通常語法"crashkernel=size[@offset]"對於大多資料配置已夠用了,但有時候保留的記憶體依賴於系統RAM。此時可通過擴充套件的crashkernel命令列對記憶體進行 限制避免從機器上移去一部分核心後造成系統不可啟動。擴充套件的crashkernel語法列出如下:

crashkernel=<range1>:<size1>[,<range2>:<size2>,...][@offset]

其中,range=start-[end]。

例如:crashkernel=512M-2G:64M,2G-:128M,含義為:如果記憶體小於512M,不設定保留記憶體,如果記憶體為512M到2G之間,設定保留記憶體區域為64M,如果記憶體大於128M,設定保留記憶體區域為128M。

4)啟動進入系統核心

必要時更新BootLoader。然後用引數"[email protected]"啟動系統核心,如:[email protected],表示告訴系統核心保留從實體地址0x01000000 (16MB)開始的64MB大小給轉儲捕捉核心使用。通常x86和x86_64平臺設定"[email protected]",ppc64平臺設定"[email protected]"。

5)裝載轉儲捕捉核心

在啟動進入系統核心後,需要裝載轉儲捕捉核心。根據處理器構架和對映檔案的型別(可否重定位),可以選擇裝載不壓縮的vmlinux或壓縮的bzImage/vmlinuz核心映像。選擇方法說明如下:

對於i386和x86_64平臺:

  • 如果核心不是可重定位的,使用vmlinux。
  • 如果核心是可重定位的,使用bzImage/vmlinuz。

對於ppc64平臺:

  • 使用vmlinux。

對於ia64平臺:

  • 使用vmlinux或vmlinuz.gz。
如果使用者使用不壓縮的vmlinux映像,那麼使用下面的命令裝載轉儲捕捉核心:
kexec -p <dump-capture-kernel-vmlinux-image> \
   --initrd=<initrd-for-dump-capture-kernel> --args-linux \
   --append="root=<root-dev> <arch-specific-options>"

如果使用者使用壓縮的bzImage/vmlinuz映像,那麼使用下面的命令裝載轉儲捕捉核心:
kexec -p <dump-capture-kernel-bzImage>\
  --initrd=<initrd-for-dump-capture-kernel> \
   --append="root=<root-dev> <arch-specific-options>"

注意:引數--args-linux在ia64平臺中不用指定。

下面是在裝載轉儲捕捉核心時使用的構架特定命令列選項:

  • 對於i386, x86_64和ia64平臺,選項為"1 irqpoll maxcpus=1 reset_devices"。
  • 對於ppc64平臺,選項為"1 maxcpus=1 noirqdistrib reset_devices"。

在裝載轉儲捕捉核心時需要注意的事項說明如下:

  • 預設設定下,ELF頭以ELF64格式儲存,以支援多於4GB核心的系統,在i386上,kexec自動檢查物理RAM尺寸是否超過4GB限制,如果沒有超過,使用ELF32。因此,在非PAE系統上ELF頭總是使用ELF32格式。
  • 選項--elf32-core-headers可用於強制產生ELF32頭,這是必要的,因為在32位系統上,GDB當前不能開啟帶有ELF64頭的vmcore檔案。
  • 在轉儲捕捉核心中,啟動引數irqpoll減少了由於共享中斷引起的驅動程式初始化失敗。
  • 使用者必須以命令mount輸出的根裝置名的格式指定<root-dev>。
  • 啟動引數"1"將轉儲捕捉核心啟動進入不支援網路的單使用者模式。如果使用者想使用網路,需要設定為3。
  • 通常不必讓轉儲捕捉核心以SMP方式執行。因此,通常編譯一個單CPU轉儲捕捉核心或裝載轉儲捕捉核心時指定選項"maxcpus=1"。

6)核心崩潰時觸發核心啟動

在裝載轉儲捕捉核心後,如果系統發生崩潰(Kernel Panic),系統將重啟動進入轉儲捕捉核心。重啟動的觸發點在函式die(), die_nmi()和sysrq處理例程(按ALT-SysRq-c組合鍵)。

下面條件將執行一個崩潰觸發點:

  • 如果檢測到硬體鎖住,並且配置了"NMI watchdog",系統將呼叫函式die_nmi()啟動進入轉儲捕捉核心。
  • 如果呼叫了函式die(),並且該執行緒的pid為0或1,或者在中斷上下文中呼叫die(),或者設定了panic_on_oops並呼叫了die(),系統將啟動進入轉儲捕捉核心。
  • 在powerpc系統,當一個軟復位產生時,所有的CPU呼叫die(),並且系統將啟動進入轉儲捕捉核心。
  • 為了測試目的,使用者可以使用"ALT-SysRq-c","echo c > /proc/sysrq-trigger"觸發一個崩潰,或者寫一個核心模組強制核心崩潰。

7)寫出轉儲檔案

在轉儲捕捉核心啟動後,可用下面的命令寫出轉儲檔案:

cp /proc/vmcore <dump-file>

使用者還可以將轉儲記憶體作為裝置/dev/oldmem以線性原始流檢視進行訪問,使用下面的命令建立該裝置:

mknod /dev/oldmem c 1 12

使用命令dd拷貝轉儲記憶體的特定部分,拷貝整個記憶體的命令列出如下:

dd if=/dev/oldmem of=oldmem.001

8)轉儲檔案分析

在分析轉儲映像之前,使用者應重啟動進入一個穩定的核心。使用者可以用GDB對拷貝出的轉儲進行有限分析。編譯vmlinux時應加上-g選項,才能生成除錯用的符號,然後,用下面的命令除錯vmlinux:

gdb vmlinux <dump-file>

SysRq魔術組合鍵列印核心資訊

SysRq"魔術組合鍵"是一組按鍵,由鍵盤上的"Alt+SysRq+[CommandKey]"三個鍵組成,其中CommandKey為可選的按鍵。SysRq魔術組合鍵根據組合鍵的不同,可提供控制核心或列印核心資訊的功能。SysRq魔術組合鍵的功能說明如表1所示。

表1 SysRq組合鍵的功能說明
鍵名 功能說明
b 在沒有同步或解除安裝硬碟的情況下立即啟動。
c 為了獲取崩潰轉儲執行kexe重啟動。
d 顯示被持的所有鎖。
e 傳送訊號SIGTERM給所有程序,除了init外。
f 將呼叫oom_kill殺死記憶體熱程序。
g 在平臺ppc和sh上被kgdb使用。
h 顯示幫助資訊。
i 傳送訊號SIGKILL給所有的程序,除了init外。
k 安全訪問金鑰(Secure Access Key,SAK)殺死在當前虛擬終端上的所有程式。
m 轉儲當前的記憶體資訊到控制檯。
n 用於設定實時任務為可調整nice的。
o 將關閉系統(如果配置為支援)。
p 列印當前暫存器和標識到控制檯。
q 將轉儲所有正執行定時器的列表。
r 關閉鍵盤Raw模式並設定為XLATE模式。
s 嘗試同步所有掛接的檔案系統。
t 將轉儲當前的任務列表和它們的資訊到控制檯。
u 嘗試以僅讀的方式重掛接所有已掛接的檔案系統。
v 轉儲Voyager SMP處理器資訊到控制檯。
w 轉儲的所有非可中斷(已阻塞)狀態的任務。
x 在平臺ppc/powerpc上被xmon(X監視器)介面使用。
0~9 裝置控制檯日誌級別,控制將列印到控制檯的核心資訊。例如:0僅列印緊急資訊,如:PANIC和OOPS資訊。

預設SysRq組合鍵是關閉的。可用下面的命令開啟此功能:

# echo 1 > /proc/sys/kernel/sysrq

關閉此功能的命令列出如下:

# echo 0 > /proc/sys/kernel/sysrq

如果想讓此功能總是起作用,可在/etc/sysctl.conf檔案中設定kernel.sysrq值為1。 系統重新啟動以後,此功能將會自動開啟。

開啟SysRq組合鍵功能後,有終端訪問許可權的使用者就可以自用它列印核心資訊了。

注意:SysRq組合鍵在X windows上是無法使用的。必須先要切換到文字虛擬終端下。如果在圖形介面,可以按Ctrl+Alt+F1切換到虛擬終端。在串列埠終端上,需要先在終端上傳送Break訊號,然後在5秒內輸入sysrq組合鍵。如果使用者有root許可權,可把commandkey字元寫入到/proc/sysrq-trigger檔案,觸發一個核心資訊列印,列印的資訊存放在/var/log/messages中。下面是一個命令樣例:
^-^$ echo 't' > sysrq-trigger
^-^vim /var/log/messages
Oct 29 17:51:43 njllinux kernel: SysRq : Show State
Oct 29 17:51:43 njllinux kernel:  task                        PC stack   pid father
Oct 29 17:51:43 njllinux kernel: init          S ffffffff812b76a0     0     1      0
Oct 29 17:51:43 njllinux kernel: ffff81013fa97998 0000000000000082 0000000000000000 ffff81013fa9795c
Oct 29 17:51:43 njllinux kernel: 000000003fa97978 ffffffff81583700 ffffffff81583700 ffff81013fa98000
Oct 29 17:51:43 njllinux kernel: ffffffff813cc5b0 ffff81013fa98350 000000003c352a50 ffff81013fa98350
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: 000300000004 ffff8101333cb090
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040c2e>] sys_pause+0x19/0x22
Oct 29 17:51:43 njllinux kernel: [<ffffffff8100c291>] tracesys+0xd0/0xd5
Oct 29 17:51:43 njllinux kernel:
Oct 29 17:51:43 njllinux kernel: lighttpd      S ffffffff812b76a0     0  3365      1
Oct 29 17:51:43 njllinux kernel: ffff810132d49b18 0000000000000082 0000000000000000 ffff810132d49adc
Oct 29 17:51:43 njllinux kernel: ffff81013fb2d148 ffffffff81583700 ffffffff81583700 ffff8101354896a0
Oct 29 17:51:43 njllinux kernel: ffffffff813cc5b0 ffff8101354899f0 0000000032d49ac8 ffff8101354899f0
Oct 29 17:51:43 njllinux kernel: Call Trace:
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040722>] ? __mod_timer+0xbb/0xcd
Oct 29 17:51:43 njllinux kernel: [<ffffffff8129b2ee>] schedule_timeout+0x8d/0xb4
Oct 29 17:51:43 njllinux kernel: [<ffffffff81040100>] ? process_timeout+0x0/0xb
Oct 29 17:51:43 njllinux kernel: [<ffffffff8129b2e9>] ? schedule_timeout+0x88/0xb4
Oct 29 17:51:43 njllinux kernel: [<ffffffff810b9498>] do_sys_poll+0x2a8/0x370
……

命令strace

命令strace 顯示程式呼叫的所有系統呼叫。使用 strace 工具,使用者可以清楚地看到這些呼叫過程及其使用的引數,瞭解它們與作業系統之間的底層互動。當系統呼叫失敗時,錯誤的符號值(如 ENOMEM)和對應的字串(如Out of memory)都能被顯示出來。

starce 的另一個用處是解決和動態庫相關的問題。當對一個可執行檔案執行ldd時,它會告訴你程式使用的動態庫和找到動態庫的位置

strace命令列選項說明如表1。常用的選項為-t, -T, -e, -o等。

表1 命令strace的命令列選項說明
選項 說明
-c 統計每個系統呼叫執行的時間、次數和出錯的次數等。
-d 輸出一些strace自身的除錯資訊到標準輸出。
-f 跟蹤當前程序由系統呼叫fork產生的子程序。
-ff 如果使用選項-o filename,則將跟蹤結果輸出到相應的filename.pid中,pid是各程序的程序號。
-F 嘗試跟蹤vfork呼叫.在-f時,vfork不被跟蹤。
-h 輸出簡要的幫助資訊。
-i 在系統呼叫的時候列印指令指標。
-q 禁止輸出關於粘附和脫離的資訊,發生在輸出重定向到檔案且直接而不是粘附執行命令時。
-r 依賴於每個系統呼叫的入口列印相對時間戳。
-t 在輸出中的每一行前加上時間資訊。
-tt 在輸出中的每一行前加上時間資訊,包括毫秒。
-ttt 毫秒級輸出,以秒錶示時間。
-T 顯示系統呼叫所花費的時間。
-v 輸出所有的系統呼叫的資訊。一些關於環境變數,狀態,輸入輸出等呼叫由於使用頻繁,預設不輸出。
-V 輸出strace的版本資訊。
-x 以十六進位制形式輸出非ASCII標準字串。
-xx 所有字串以十六進位制形式輸出。
-a column 以特定的列數對齊返回值,預設值為40。
-e expr 指定一個表示式,用來控制如何跟蹤.格式如下:
[qualifier=][!]value1[,value2]...
qualifier只能是 trace,abbrev,verbose,raw,signal,read,write其中之一。value是用來限定的符號或數字。預設的qualifier是 trace。感嘆號是否定符號。
-eopen 等價於 -e trace=open,表示只跟蹤open呼叫。而-etrace!=open表示跟蹤除了open以外的其他呼叫。
-e trace=set 只跟蹤指定的系統呼叫。例如:-e trace=open,close,rean,write表示只跟蹤這四個系統呼叫。預設的為set=all。
-e trace=file 只跟蹤檔名作為引數的系統呼叫,一般為檔案操作。
-e trace=process 只跟蹤有關程序控制的系統呼叫。
-e trace=network 只跟蹤與網路有關的所有系統呼叫。
-e strace=signal 跟蹤所有與系統訊號有關的系統呼叫。
-e trace=ipc 跟蹤所有與程序間通訊有關的系統呼叫。
-o filename 將strace的輸出寫入檔案filename。
-p pid 跟蹤指定的程序pid。
-s strsize 指定最大字串列印長度,預設值為32。
-u username 以username的UID和GID執行命令。
例如:命令strace pwd的輸出部分列出如下:
execve("/bin/pwd", ["pwd"], [/* 39 vars */]) = 0
uname({sys="Linux", node="sammy", ...}) = 0
brk(0)                                  = 0x804c000
old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4001...
	fstat64(3, {st_mode=S_IFREG|0644, st_size=115031, ...}) = 0
old_mmap(NULL, 115031, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40017000
close(3)                                = 0
open("/lib/tls/libc.so.6", O_RDONLY)    = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\360U\1"..., 1024) = 1024
fstat64(3, {st_mode=S_IFREG|0755, st_size=1547996, ...}) = 0

用函式printk列印核心資訊

Linux核心用函式printk列印除錯資訊,該函式的用法與C庫列印函式printf格式類似,但在核心使用。使用者可在核心程式碼中的某位置加入函式printk,直接把所關心的資訊打列印到螢幕上或日誌檔案中。

函式printk根據日誌級別(loglevel)對除錯資訊進行分類。日誌級別用巨集定義,展開為一個字串,在編譯時由前處理器將它和訊息文字拼接成一個字串,因此函式printk中的日誌級別和格式字串間不能有逗號。

下面兩個 printk 的例子,一個是除錯資訊,一個是臨界資訊:
printk(KERN_DEBUG "Here I am: %s:%i\n", _ _FILE_ _, _ _LINE_ _); 
printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);

樣例:在使用者空間或核心中開啟及關閉列印除錯訊息 使用者還可以在核心或使用者空間應用程式定義統一的函式列印除錯資訊,可在Makefile檔案中開啟或關閉除錯函式。定義方法列出如下:
/*debug_on_off.h*/
#undef PDEBUG             /* undef it, just in case */ 
#ifdef SCULL_DEBUG 
#ifdef _ _KERNEL_ _ 
    /* This one if debugging is on, and kernel space */ 
#define PDEBUG(fmt,args...) printk(KERN_DEBUG "scull: " fmt, ## args)
#else 
    /* This one for user space */ 
#define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args) 
#endif 
#else 
#define PDEBUG(fmt, args...) /* not debugging: nothing */ 
#endif

在檔案Makefile加上下面幾行:
# Comment/uncomment the following line to disable/enable debugging 
DEBUG = y 
 
# Add your debugging flag (or not) to CFLAGS 
ifeq ($(DEBUG),y) 
 DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" 
else 
 DEBFLAGS = -O2 
endif 
 
CFLAGS += $(DEBFLAGS)

更改makefile中的DEBUG值,需要除錯資訊時,DEBUG = y,不需要時,DEBUG賦其它值。再用make編譯即可。

核心探測kprobe

kprobe(核心探測,kernel probe)是一個動態地收集除錯和效能資訊的工具,如:收集暫存器和全域性資料結構等除錯資訊,無需對Linux核心頻繁編譯和啟動。使用者可以在任何核心程式碼地址進行陷阱,指定除錯斷點觸發時的處理例程。工作機制是:使用者指定一個探測點,並把使用者定義的處理函式關聯到該探測點,當核心執行到該探測點時,相應的關聯函式被執行,然後繼續執行正常的程式碼路徑。

kprobe允許使用者編寫核心模組新增除錯資訊到核心。當在遠端機器上除錯有bug的程式而日誌/var/log/messages不能看出錯誤時,kprobe顯得非常有用。使用者可以編譯一個核心模組,並將核心模組插入到除錯的核心中,就可以輸出所需要的除錯資訊了。

核心探測分為kprobe, jprobe和kretprobe(也稱return probe,返回探測)三種。kprobe可插入核心中任何指令處;jprobe插入核心函式入口,方便於訪問函式的引數;return probe用於探測指定函式的返回值。

核心模組的初始化函式init安裝(或註冊)了多個探測函式,核心模組的退出函式exit將登出它們。註冊函式(如:register_kprobe())指定了探測器插入的地方、探測點觸發的處理例程。

(1)配置支援kprobe的核心

配置核心時確信在.config檔案中設定了CONFIG_KPROBES、CONFIG_MODULES、CONFIG_MODULE_UNLOAD、CONFIG_KALLSYMS_ALL和CONFIG_DEBUG_INFO。

配置了CONFIG_KALLSYMS_ALL,kprobe可用函式kallsyms_lookup_name從地址解析程式碼。配置了CONFIG_DEBUG_INFO後,可以用命令"objdump -d -l vmlinux"檢視源到物件的程式碼對映。

除錯檔案系統debugfs含有kprobe的除錯介面,可以檢視註冊的kprobe列表,還可以關閉/開啟kprobe。

檢視系統註冊probe的方法列出如下:

#cat /debug/kprobes/list
c015d71a  k  vfs_read+0x0
c011a316  j  do_fork+0x0
c03dedc5  r  tcp_v4_rcv+0x0

第一列表示探測點插入的核心地址,第二列表示核心探測的型別,k表示kprobe,r表示kretprobe,j表示jprobe,第三列指定探測點的"符號+偏移"。如果被探測的函式屬於一個模組,模組名也被指定。

開啟和關閉kprobe的方法列出如下:

#echo ‘1’ /debug/kprobes/enabled
#echo ‘0’ /debug/kprobes/enabled

(2)kprobe樣例

Linux核心原始碼在目錄samples/kpobges下提供了各種kprobe型別的探測處理例程編寫樣例,分別對應檔案kprobe_example.c、jprobe_example.c和kretprobe_example.c,使用者稍加修改就可以變成自己的核心探測模組。下面僅說明kprobe型別的探測例程。

樣例kprobe_example是kprobe型別的探測例程核心模組,顯示了在函式do_fork被呼叫時如何使用kprobe轉儲棧和選擇的暫存器。當核心函式do_fork被呼叫建立一個新程序時,在控制檯和/var/log/messages中將顯示函式printk列印的跟蹤資料。樣例kprobe_example列出如下(在samples/kprobe_example.c中):

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
 
/* 對於每個探測,使用者需要分配一個kprobe物件*/
static struct kprobe kp = {
	.symbol_name	= "do_fork",
};
 
/* 在被探測指令執行前,將呼叫預處理例程 pre_handler,使用者需要定義該例程的操作*/
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx,"
			" flags = 0x%lx\n",
		p->addr, regs->ip, regs->flags);  /*列印地址、指令和標識*/
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, nip = 0x%lx,"
			" msr = 0x%lx\n",
		p->addr, regs->nip, regs->msr);
#endif
 
	/* 在這裡可以呼叫核心介面函式dump_stack打印出棧的內容*/
	return 0;
}
 
/* 在被探測指令執行後,kprobe呼叫後處理例程post_handler */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
				unsigned long flags)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "post_handler: p->addr = 0x%p, flags = 0x%lx\n",
		p->addr, regs->flags);
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "post_handler: p->addr = 0x%p, msr = 0x%lx\n",
		p->addr, regs->msr);
#endif
}
 
/*在pre-handler或post-handler中的任何指令或者kprobe單步執行的被探測指令產生了例外時,會呼叫fault_handler*/
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
	printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",
		p->addr, trapnr);
	/* 不處理錯誤時應該返回*/
	return 0;
}
 
/*初始化核心模組*/
static int __init kprobe_init(void)
{
	int ret;
	kp.pre_handler = handler_pre;
	kp.post_handler = handler_post;
	kp.fault_handler = handler_fault;
 
	ret = register_kprobe(&kp);  /*註冊kprobe*/
	if (ret < 0) {
		printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
		return ret;
	}
	printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);
	return 0;
}
 
static void __exit kprobe_exit(void)
{
	unregister_kprobe(&kp);
	printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
}
 
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");

Systemtap除錯

(1)Systemtap原理

Systemtap是一個基於kprobe除錯核心的開源軟體。除錯者只需要寫一些指令碼,通過Systemtap提供的命令列介面對正在執行的核心進行診斷除錯,不需要修改或插入除錯程式碼、重新編譯核心、安裝核心和重啟動等工作,使核心除錯變得簡單容易。Systemtap除錯過程與在gdb偵錯程式中用斷點命令列除錯類似。

Systemtap用類似於awk語言的指令碼語言編寫除錯指令碼,該指令碼命名事件並給這些事件指定處理例程。只要指定的事件發生,Linux核心將執行對應的處理例程。

有幾種型別的事件,如:進入或退出一個函式,一個定時器超時或整個systemtap會話開始或停止。處理例程是一系列指令碼語言語句指定事件發生時所做的工作,包括從事件上下文提取資料,儲存它們進入內部變數或列印結果。

Systemtap的執行過程如圖2所示,使用者除錯時用Systemtap編寫除錯指令碼,Systemtap的翻譯模組(translator)將指令碼經語法分析(parse)、功能處理(elaborate)和翻譯後生成C語言除錯程式,然後,執行C編譯器編譯(build)建立除錯核心模組。再接著將該核心模組裝載入核心,通過kprobe機制,核心的hook啟用所有的探測事件。當任何處理器上有這些事件發生時,對應的處理例程被觸發工作,kprobe機制在核心獲取的除錯資料通過檔案系統relayfs傳回Systemtap,輸出除錯資料probe.out。在除錯結束時,會話停止,核心斷開hook連線,並解除安裝核心模組。整個操作過程由單個命令列程式strap驅動控制。

Linux kernel debug method 03.png

圖2 Systemtap執行過程

(2)stap程式

stap程式是Systemtap工具的前端,它接受用systemtap指令碼語言編寫的探測指令,翻譯這些指令到C語言程式碼,編譯C程式碼產生並裝載核心模組到正執行的Linux核心,執行請求的跟蹤或探測函式。使用者可在一個命名檔案中提供指令碼或從命令列中提供除錯語句。

命令stap的用法列出如下:

stap [ OPTIONS ] FILENAME [ ARGUMENTS ]

stap [ OPTIONS ] - [ ARGUMENTS ]

stap [ OPTIONS ] -e SCRIPT [ ARGUMENTS ]

stap [ OPTIONS ] -l PROBE [ ARGUMENTS ]

選項[ OPTIONS ]說明如下:

-h 顯示幫助資訊。

-V 顯示版本資訊。

-k 在所有操作完成後,保留臨時目錄。對於檢查產生的C程式碼或重使用編譯的核心物件來說,這是有用的。

-u 非優化編譯模式。.

-w 關閉警告資訊。

-b 讓核心到使用者資料傳輸使用bulk模式。

-t 收集時間資訊:探測執行的次數、每個探測花費的平均時間量。

-sNUM 核心到使用者資料傳輸使用NUM MB 的緩衝區。當多個處理器工作在bulk模式時,這是單個處理器的緩衝區大小。

-p NUM Systemtap在通過NUM個步驟後停止。步驟數為1-5: parse, elaborate, translate, compile, run。

-I DIR 新增tapset庫(用於翻譯C程式碼的函式集)搜尋目錄。

-D NAME=VALUE 新增C語言巨集定義給核心模組Makefile,用於重寫有限的引數。

-R DIR 在給定的目錄查詢Systemtap執行原始碼。

-r RELEASE 為給定的核心釋出版本RELEASE而不是當前執行核