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 Process cat (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 Process head (pid: 2331, threadinfo=c27fe000 task=c3226150) Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7 bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000 00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70 Call Trace: [] 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將裝載內核到用於轉儲捕捉內核的內核保留區域。

否則,將使用啓動參數"crashkernel=Y@X"指定第二個內核保留內核區域的開始地址,其中,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。然後用參數"crashkernel=Y@X"啓動系統內核,如:crashkernel=64M@16M,表示告訴系統內核保留從物理地址0x01000000 (16MB)開始的64MB大小給轉儲捕捉內核使用。通常x86和x86_64平臺設置"crashkernel=64M@16M",ppc64平臺設置"crashkernel=128M@32M"。

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而不是當前運行內核編譯內核模塊。

-m MODULE 給編譯產生的內核模塊命名MODULE,替代缺省下的隨機命名。產生的內核模塊被拷貝到當前目錄。

-o FILE 發送標準輸出到命名文件FILE。在bulk模式,每個CPU的文件名將用"FILE_CPU序號"表示。

-c CMD 開始探測,運行CMD,當CMD完成時退出。

-x PID 設置target()

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章