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()𤣵到PID。這允許腳本作為指定進程的過濾器。

-l PROBE 代替運行一個探測腳本,它僅列出所有匹配給定模式PROBE可用的探測點,模式PROBE可用通配符。

--kmap[=FILE] 指定符號文件,缺省文件為/boot/System.map-VER-SION。探測的函數的地址和名字需要通過內核或內核模塊的符號表解析。如果內核編譯時沒有調試信息或者探測是在沒有調試信息的匯編語言言論的,這將是有用的。

--ignore-vmlinux 忽略vmlinux文件。

(3)Systemtap腳本語法

Systemtap腳本語法類似於C語言,它使用了三種數據類型:整數(integers)、字符串(strings)和關聯數組(associative Arrays)。它有與C語言一樣的控制結構。Systemtap腳本語法詳細內容請參考《Systemtap tutorial》。

Systemtap腳本由探測點(probe)和探測輸出函數組成。每個Systemtap腳本至少定義一個探測點。函數是探測點的處理例程。

#!/usr/bin/env stap    #Systemtap腳本的標志
#
# 顯示在最后5秒內調用最后10個系統調用
display the top 10 syscalls called in last 5 seconds
#
global syscalls          #定義全局變量
function print_top () {  #定義函數
        cnt=0            #局部變量
        log ("SYSCALL\t\t\t\tCOUNT")      #打印表頭標題“SYSCALL COUNT”
        foreach ([name] in syscalls-) {   #查詢每個系統調用的計數值
                printf("%-20s\t\t%5d\n",name, syscalls[name])  #按格式打印
                if (cnt++ == 10)
                        break
        }
        printf("--------------------------------------\n")
        delete syscalls                 #刪除全局變量
}
probe syscall.* {       #在系統調用探測點
        syscalls[probefunc()]++      #系統調用計數
}
probe timer.ms(5000) {
        print_top ()       #調用函數
}

kdb內核調試器

Kdb(Kernel Debug)是SGI公司開發的遵循GPL的內建Linux內核調試工具。標准的Linux內核不包括kdb,需要從ftp://oss.sgi.com/www/projects/kdb/download/ix86 下載對應標准版本內核的kdb補丁,對標准內核打補丁,然后,編譯打過補丁的內核代碼。目前kdb支持包括x86(IA32)、IA64和MIPS在內的體系結構。

Kdb調試器是Linux內核的一部分,提供了檢查內存和數據結構的方法。通過附加命令,它可以格式化顯示給定地址或ID的基本系統數據結構。kdb當前的命令集可以完全控制內核的操作,包括單步運行一個處理器、在指定的指令執行處理暫停、在訪問或修改指定虛擬內存的位置暫停、在輸入-輸出地址空間對一個寄存器訪問處暫停、通過進程ID跟蹤任務、指令反匯編等。

安裝kdb

標准內核不包含kdb,因此,用戶需要先下載kdb補丁,如:kdb-v4.4-2.6.24-x86-2.bz2,接着,應打補丁、配置、編譯和安裝內核。

(1)打補丁

下載和解壓縮補丁,將補丁打進標准內核中。方法如下:

$ upzip kdb-v4.4-2.6.24-x86-2.bz2
$cp kdb-v4.4-2.6.24-x86-2  linux-2.6.24/
$ cd linux-2.6.24
$ patch -p1 < kdb-v4.4-2.6.24-x86-2.bz
$ make xconfig

(2)配置新內核

運行make xconfig,在配置界面上選擇CONFIG_KDB選項,為了更好地調試,建議用戶從配置界面上選擇CONFIG_FRAME_POINTER選項,盡管該選項使用了格外的寄存器並產生稍慢一些的內核。

(3)編譯與安裝新內核

按下面步驟重新編譯和安裝新內核:

#make   
#make install  
#make modules_install

使用kdb調試命令

運行支持kdb的內核后,在控制台上按下 Pause(或 Break)鍵將啟動調試。當內核發生 oop或到達某個斷點時,也會啟動 kdb。kdb提示符如下所示:

Entering kdb (current=0xc03b0000,pid 0)on processor 0 due to Keyboard Entry

[0]kdb>

在kdb提示符下,用戶可以輸入kdb命令,詳細的kdb命令使用說明請參考man kdb文檔,一些常見的命令說明如表1.

表1 常見kdb命令說明

命令 命令說明
' 命令可以用於顯示所有kdb命令。
bp 設置或顯示一個斷點。
bph 設置一個硬件斷點。
bc 清除一個斷點。
bl 列出所有當前斷點。
bt 顯示當前進程的堆棧跟蹤情況。
go 退出調試器並重啟內核運行。
Id 反匯編指令。
md 顯示指定地址內容。
mds 以符號形式顯示內存。
mm 修改內存。
reboot 立即重啟機器。
rd 顯示寄存器內容。
ss 單步執行(一次一條指令)。
ssb 單步執行CPU直到到達一分支。

下面以調試scull驅動程序為例簡單說明kdb的使用方法:

假定scull驅動程序內核模塊已裝載入內核,先在驅動程序的函數scull_read 中設置一個斷點,方法如下:

[1]kdb> bp scull_read

Instruction(i) BP #0 at 0xc8833514 (scull_read) is enabled on cpu 1

[1]kdb> go

  命令bp在函數scull_read 開始處設置了一個斷點,接着,命令go退出調試器,重啟內核運行。內核下一次進入函數scull_read 時暫停運行。產生如下的狀態:

Entering kdb (0xc3108000) on processor 0 due to Breakpoint @ 0xc8833515 
Instruction(i) breakpoint #0 at 0xc8833514 
scull_read+0x1:   movl   %esp,%ebp 
[0]kdb>

  kdb當前scull_read斷點位置。可用命令bt查看堆棧跟蹤記錄,檢查函數調用層次樹,方法如下:

[0]kdb> bt 
   EBP       EIP         Function(args) 
0xc3109c5c 0xc8833515  scull_read+0x1 
0xc3109fbc 0xfc458b10  scull_read+0x33c255fc( 0x3, 0x803ad78, 0x1000, 
0x1000, 0x804ad78) 
0xbffffc88 0xc010bec0  system_call 
[0]kdb>

再可用命令mds顯示指定內存的數據,如:查詢 scull_devices 指針的值方法如下:

[0]kdb> mds scull_devices 1

c8836104: c4c125c0 ....

上面命令查看指針scull_devices所指位置的一個雙字(4個字節)數據,表示設備結構數組的起始地址為c4c125c0。再用mds查看設備結構的數據,方法如下:

[0]kdb> mds c4c125c0

c4c125c0: c3785000 ....

c4c125c4: 00000000 ....

c4c125c8: 00000fa0 ....

c4c125cc: 000003e8 ....

c4c125d0: 0000009a ....

c4c125d4: 00000000 ....

c4c125d8: 00000000 ....

c4c125dc: 00000001 ....

  上面8行分別對應結構Scull_Dev的8個成員。再與數據結構Scull_Dev的定義相對照,可知這8個數據的含義。

還可以使用命令mm修改數據。例如:將結構Scull_Dev的某一成員值設置為0x50,方法如下:

[0]kdb> mm c4c125d0 0x50

0xc4c125d0 = 0x50

kgdb

kgdb調試原理

調試器GNU gdb主要用於調試用戶級程序,通過串口線或網絡將兩台計算機以主機/目標機(host machine/target machine)方式連接時,gdb還可用於調試linux內核。這種方式需要給內核打進包含kgdb驅動程序在內的補丁。

kgdb是Linux內核的源代碼級調試器,與gdb配合使用可以調試Linux內核。在Linux內核的kgdb配合下,內核開發者可以用類似於調試應用程序的方式通過gdb調試內核,可以方便以使用gdb的命令在內核代碼放置斷點、單步調試內核和觀察內核變量值等。

kgdb進行源碼級內核調試的原理圖如圖1所示。在兩台計算中機,一台用作開發計算機,稱為主機或開發機;一台用作測試計算機,稱為目標機或測試機。兩台計算機可通過串行線或以太網進行通信。內核在測試機上調試,gdb在開發機上運行,gdb通過串行線用null modem與調試的內核通信。兩台計算機也可以使用一台計算機上的兩個虛擬機進行替代。

Linux kernel debug method 04.png

圖1 kgdb進行源碼級內核調試原理圖

目前,kgdb支持i386, x86_64, ppc, arm, mips和ia64等處理器構架,開發機和測試機可用串行線或以太網進行連接通信。

kgdb補丁將下面的內容加入到內核代碼中:

●gdb stub - gdb stub("樹樁")是調試器的核心,它處理來自開發機上gdb的請求。當測試機運行了帯有kgdb的內核時,gdb stub控制測試機中所有的處理器。

●對出錯處理例程的修改- 當一個不期望的錯誤發生時,內核將控制傳遞給kgdb調試器。不含有kgdb的內核在出現不可預測錯誤時會崩潰(panic),通過對出錯處理的修改,kgdb允許開發者分析不可預測的出錯。

●串行通信-該部件通過內核的串行驅動程序,為內核中的stub提供接口,負責在串行連接線上發送和接收數據,還負責處理開發機上gdb發送的處理控制斷點請求。

建立kdbg聯機調試的方法

下面說明建立kdbg聯機調試的步驟:

(1)軟件建立和應用kgdb補丁

1)下載Linux內核源代碼:linux-2.6.15.5.tar.bz2。

2)下載與內核版本對應的kgdb補丁:linux-2.6.15.5-kgdb-2.4.tar.bz2。

3)解壓縮軟件包,方法如下:

cd ${BASE_DIR}

tar -jxvf linux-2.6.15.5.tar.bz2

cd ${BASE_DIR}/linux-2.6.15.5

tar -jxvf linux-2.6.15.5-kgdb-2.4.tar.bz2

在${BASE_DIR}/linux-2.6.15.5目錄中,給Linux內核打kgdb補丁,方法如下:

patch -p1 < ${BASE_DIR}/linux-2.6.15.5-kgdb-2.4/core-lite.patch

patch -p1 < ${BASE_DIR}/linux-2.6.15.5-kgdb-2.4/i386.patch

(2)在開發機上編譯內核

1)在${BASE_DIR}/linux-2.6.15.5/Makefile,設置EXTRAVERSION = -kgdb。

2)運行命令make xconfig或make oldconfig,出現如圖2所示的內核配置界面。在配置界面中,為目標機硬件選擇合適的選項;在"Kernel hacking"條目下,選擇kgdb的選項。

Linux kernel debug method 05.png

圖2 在Linux內核配置界面中與kgdb相關的配置選項

3)運行make bzImage編譯內核。

4)將編譯的內核從開發機上傳送到目標機上,拷貝內核映像${BASE_DIR}/linux-2.6.15.5/arch/i386/boot/bzImage到目標機/boot/vmlinuz-2.6.15.5-kgdb。然后,再拷貝映射文件${BASE_DIR}/linux-2.6.15.5/System.map到目標機/boot/System.map-2.6.15.5-kgdb。再如下建立符號鏈接:

ln -s /boot/vmlinuz-2.6.15.5-kgdb /boot/vmlinuz

ln -s /boot/System.map-2.6.15.5-kgdb /boot/System.map

5)在目標機上編輯文件/boot/grub/grub.conf, 在該文件加入含有kgdb的內核條目,方法如下:

title Linux-2.6.15.5-kgdb

root (hd0,0)

kernel /boot/vmlinuz-2.6.15.5-kgdb ro root=/dev/hda1 kgdbwait

(3)在開發機上開始調試會話

1)在啟動目標機后,它將等待開發機連接,顯示下面的消息:

Waiting for connection from remote gdb...

2)用命令cd ${BASE_DIR}/linux-2.6.15.5進入目錄linux-2.6.15.5目錄。

3)用root用戶登錄設置調試會話波特率,方法如下:

<root#> gdb ./vmlinux

(gdb) set remotebaud 115200

(gdb) target remote /dev/ttyS0

Remote debugging using /dev/ttyS0

breakpoint () at kernel/kgdb.c:1212

1212 atomic_set(&kgdb_setting_breakpoint, 0);

warning: shared library handler failed to enable breakpoint

(gdb)

4)在開發機上輸入調試命令

此時,gdb已連接到目標機上的內核,目標機上的內核正等待接收命令進行測試。輸入命令(gdb) c(表示繼續運行)時,目標機系統正常啟動,在配置內核時,如果開發機選擇了通過gdb輸出控制台消息,則控制台log消息會從gdb上顯示。

由gdb連接到測試內核,如果測試內核發生內核崩潰(kernel panic),它將首先將控制權轉移給gdb,以讓gdb分析崩潰原因。

(4)使用kgdb以太網接口

kgdb還可能通過以太網接口調試內核,用以太網接口建立連接的步驟說明如下:

1)添加下面的行到grub條目中:

kgdboe=@10.0.0.6/,@10.0.0.3/ (that's kgdboe=@LOCAL-IP/,@REMOTE-IP/)
# Sample grub.conf which will by default boot the kgdb enabled kernel
title Linux-2.6.15.5-kgdb(eth)
root (hd0,0)
kernel /boot/vmlinuz-2.6.15.5-kgdb ro root=/dev/hda1 kgdboe=@10.0.0.6/,@10.0.0.3/
console=ttyS0,115200

2)接着在gdb中用下面的命令開始調試會話:

(gdb) ./vmlinux

(gdb) target remote udp:HOSTNAME:6443

調試內核模塊

內核可加載模塊的調試具有其特殊性,內核模塊中各段的地址在模塊加載進內核后才最終確定的,開發機的gdb無法得到各種符號地址信息。因此,用戶需要使用特殊版本的gdb,可以檢測到內核模塊的裝載和卸載。另外,還需要將內核模塊的符號裝載到gdb中,這樣,gdb才能解析到符號。

(1)准備檢測內核模塊裝載和卸載代碼的gdb派生版本

此步驟在開發機上完成。

安裝在開發機上的gdb應含有內核模塊調試特征,用戶需要安裝含有檢測內核模塊裝載和卸載代碼的gdb派生版本,該版本gdb派生於標准的gdb。用戶可以從網址http://kgdb.linsyssoft.com/downloads.htm下載gdb-6.4-kgdb-2.4.tar.bz2,然后,編譯安裝生成gdb派生版本。或者下載gdbmod-2.4.bz2,解壓縮后得到可執行的gdb派生版本。

在測試機上不需要特別的安裝,內核模塊可以出現在根文件系統或一個ramdisk中。

(2)裝載內核模塊符號到gdb中

此步驟在開發機上完成。

在開發機上,用戶應裝載內核模塊的符號到gdb,讓gdb調試時可以解析到二進制代碼對應的符號。

首先,內核模塊編譯時應打開調試信息。然后,用下面方法設置內核、調試接口和定位內核模塊位置:

#cd /usr/src/linux 2.6.13 
#gdbmod 2.4 vmlinux 
(gdb) target remote /dev/ttyS0
Remote debugging using /dev/ttyS0
breakpoint () at gdbstub.c:1153
1153 }
(gdb)set solib search path /usr/linux 2.6.13/drivers/net

一旦kgdb通知一個內核模塊裝載時,gdb必須能定位模塊文件。因此,用戶需要用命令"set solib-search-path"設置內核模塊文件所在的路徑。

(3)插入內核模塊到內核

插入內核模塊到內核,方法 如下:

# insmod mymodule.ko

到此,已裝載了內核模塊符號,內核模塊可以像正常的內核代碼一樣調試了。

樣例:調試內核模塊test

1)編寫內核模塊

首先,在開發機上編寫簡單的內核模塊test,代碼如下:

void test_func()
{
    printk("test_func\n");
    printk("aaaaaaaaaaa\n");
}
 
int test_init()
{
    printk("test_init_module\n");
 
    return 0;
}
 
void test_exit()
{
    printk("test_cleanup_module\n");
}
 
module_init(test_init);
module_exit(test_exit);

2)編譯安裝內核模塊

接着,編譯內核模塊,並將內核模塊拷貝到測試機上。方法如下:

#cd /root/mymodule
#gcc -D__KERNEL__ -DMODULE -I/usr/src/linux-2.6.15/kernel/include -O -Wall -g -c -o test.ko test.c
#scp test.ko root@192.168.1.130:/root

3)開始調試

裝載內核符號到gdb中,設置內核模塊所在路徑,方法如下:

# gdbmod vmlinux
 (gdb) set solib-search-path /root/mymodule

執行命令rmt,進入測試機調試,方法如下:

(gdb) rmt
breakpoint () at kgdbstub.c:1005
1005                    atomic_set(&kgdb_setting_breakpoint, 0);

在內核模塊初始化處設置斷點。查內核源碼可知,內核模塊初始化函數init在module.c文件函數sys_init_module函數中的mod->init處調用,對應行號為2168(根據不同版本的內核,行號可能不同)。設置斷點方法如下:

(gdb) b module.c:2168
Breakpoint 1 at 0xc011cd83: file module.c, line 2168.

讓測試機上的內核繼續運行,方法如下:

(gdb) c
Continuing.
[New Thread 1352]
[Switching to Thread 1352]

測試機用命令"insmod test.ko"執行插入內核模塊操作時,開發機會在斷點處被暫停,暫停時顯示如下

Breakpoint 1, sys_init_module (name_user=0xc03401bc "\001",
mod_user=0x80904d8) at module.c:2168
2168             ret = mod->init();

用step命令進入內核模塊test的函數init,方法如下:

(gdb) step
test_init () at test.c:12
12              printk("test_init_module\n");
(gdb) n
15      }
(gdb)

對內核模塊的非init函數調試時,由於測試機上已插入模塊,模塊的符號也已加載,只需要直接需要調試的代碼處設置斷點。如:在函數test_func處設置斷點的方法如下:

(gdb)bt test_func

調試內核

用gdb調試內核類型於調試應用程序進程,kgdb支持gdb的執行控制命令、棧跟蹤和線程分析等。但kgdb不支持watchpoint,kgdb通過gdb宏來執行watchpoint。

調試內核的命令說明如下:

(1)停止內核執行

用戶在gdb終端中按下Ctrl + C鍵,gdb將發送停止消息給kgdb stub,kgdb stub控制內核的運行,並與gdb通信。

(2)繼續內核運行

gdb命令"(gdb) c"告訴kgdb stub繼續內核運行,直到遇到一個斷點,或者gdb執行Ctrl + C,或其他原因,內核運行才停頓下來。

(3)斷點

gdb斷點(Breakpoints)用於在一個函數或代碼行處暫停內核運行,設置斷點命令如:"(gdb) b module.c:2168"。

(4)進入代碼

使用命令"(gdb) step"進入一個函數或在暫停后執行下一個程序行;使用命令"(gdb) next"跳過一個函數執行下一個程序行或暫停后執行下一個程序;

(5)棧跟蹤(Stack Trace)

使用命令"(gdb) bt"或"(gdb) backtrace"顯示程序棧,它顯示了調用函數的層次列表,表明了函數的調用函數。該命令還打印調解函數的參數值。

例如,運行命令backtrace的樣例列出如下:

(gdb) backtrace
#0 breakpoint () at gdbstub.c:1160
#1 0xc0188b6c in gdb_interrupt (irq=3, dev_id=0x0, regs=0xc02c9f9c) at gdbserial.c:143
#2 0xc0108809 in handle_IRQ_event (irq=3, regs=0xc02c9f9c, action=0xc12fd200)3 0xc0108a0d in do_IRQ (regs={ebx = 1072672288, ecx = 0, edx = 1070825472, esi = 1070825472, edi = 1072672288, ebp = 1070817328, eax = 0, xds = 1072693224, xes = 1072693224, orig_eax = 253, eip = 1072672241, xcs = 16, eflags = 582, esp = 1070817308, xss = 1072672126}) at irq.c:621
#4 0xc0106e04 in ret_from_intr () at af_packet.c:1878
#5 0xc0105282 in cpu_idle () at process.c:135
#6 0xc02ca91f in start_kernel () at init/main.c:599
#7 0xc01001cf in L6 () at af_packet.c:1878
Cannot access memory at address 0x8e000

除非棧幀數作為命令backtrace的參數外,gdb僅在棧跟蹤走出了可訪問地址空間時才停止打印棧的信息。上面例子中,函數調用層次次序從上到下為:ret_from_intr, do_IRQ, handle_IRQ_event, gdb_interrupt。

放置一個斷點在函數ext2_readlink,並訪問一個符號鏈接,以便運行到該斷點。 設置斷點方法如下:

(gdb) br ext2_readlink
Breakpoint 2 at 0xc0158a05: file symlink.c, line 25.
(gdb) c
Continuing.

在測試機上運行命令"ls -l /boot/vmlinuz"顯示一個符號鏈接。在測試機上,內核會運行到上述斷點處,並暫停。然后,將斷點信息傳回開發機。在開發機上,用戶可以查看棧或運行其他調試命令。例如:運行棧跟蹤命令,顯示的結果列出如下:

Breakpoint 2, ext2_readlink (dentry=0xc763c6c0, buffer=0xbfffed84 "\214\005",buflen=4096) at symlink.c:25 25 char *s = (char *)dentry >d_inode >u.ext2_i.i_data; 
(gdb) bt
#0 ext2_readlink (dentry=0xc763c6c0, buffer=0xbfffed84 "\214\005",buflen=4096) at symlink.c:25
#1 0xc013b027 in sys_readlink (path=0xbfffff77 "/boot/vmlinuz", buf=0xbfffed84 "\214\005", bufsiz=4096) at stat.c:262
#2 0xc0106d83 in system_call () at af_packet.c:1878
#3 0x804aec8 in ?? () at af_packet.c:1878
#4 0x8049697 in ?? () at af_packet.c:1878
#5 0x400349cb in ?? () at af_packet.c:1878

上述棧跟蹤中,gdb打印一些無效的棧幀(#3~#5),這是因為gdb不知道在哪里停止棧跟蹤,可以忽略這些無效的棧幀。

系統調用readlink在函數system_call進入內核,該函數顯示在af_packet.c中,這是不對的,因為對於匯編語言文件的函數,gdb不能指出正常的代碼行。但gdb可以正確處理在C語言文件中內聯匯編代碼。更多的調用層次是:sys_readlink和ext2_readlink。

在調試完后,用戶可用"刪除"命令和"繼續"命令刪除斷點,並繼續內核的運行,方法如下:

(gdb) delete
Delete all breakpoints? (y or n) y
(gdb) c
Continuing.

(6)內聯函數

使用gdb棧跟蹤命令通常足夠找出一個函數的調用層次關系。但當其中一個棧幀在擴展的內聯函數中,或者從一個內聯函數訪問另一個內聯函數時,棧跟蹤命令是不夠用的,棧跟蹤僅顯示在內聯函數中的源代碼文件名和語句的行號,通過查看外面的函數,這可能知道調用的內聯函數,但如果調用了兩次內聯函數,它就不知道是哪個內聯函數了。

下面的處理流程可用來找出內聯函數信息:

在棧跟蹤中,gdb還與函數名一起顯示代碼地址,在調用了一個內聯函數的語句中,gdb顯示了這些代碼調用和被調用的地址。腳本disasfun.sh可用來反匯編,源代碼從vmlinux文件引用一個內核函數。文件vmlinux含有內核函數的絕對地址,因此,在匯編代碼中看見的地址是在內存中的地址。

下面是一個樣例。

配置內核時,kgdb應打開線程分析(CONFIG_KGDB_THREAD),gdb應連接到目標內核。

用"Ctrl+C"中斷內核,放置一個斷點在函數__down處,並繼續運行,方法如下:

Program received signal SIGTRAP, Trace/breakpoint trap.
breakpoint () at gdbstub.c:1160
1160    }
(gdb) break __down
Breakpoint 1 at 0xc0105a43: file semaphore.c, line 62.
(gdb) c
Continuing.

為了讓程序運行到斷點處,在目標機上運行"man lilo"。程序會運行到斷點,gdb會進入命令行模式。輸入棧跟蹤命令,顯示如下:

Breakpoint 1, __down (sem=0xc7393f90) at semaphore.c:62
62              add_wait_queue_exclusive(&sem->wait, &wait);
(gdb) backtrace
#0  __down (sem=0xc7393f90) at semaphore.c:62
#1  0xc0105c70 in __down_failed () at af_packet.c:1878
#2  0xc011433b in do_fork (clone_flags=16657, stack_start=3221199556,
     regs=0xc7393fc4, stack_size=0)
     at /mnt/work/build/old-pc/linux-2.4.6-kgdb/include/asm/semaphore.h:120
#3  0xc010594b in sys_vfork (regs={ebx = 1074823660, ecx = 1074180970,
      edx = 1074823660, esi = -1073767732, edi = 134744856, ebp = -1073767712,
      eax = 190, xds = 43, xes = 43, orig_eax = 190, eip = 1074437320,
      xcs = 35, eflags = 518, esp = -1073767740, xss = 43}) at process.c:719

在函數sys_vfork中行號顯示為719,這與文件process.c中的行號一致,查看該文件可得到確認,方法如下:

(gdb) list process.c:719
714      * do not have enough call-clobbered registers to hold all
715      * the information you need.
716      */
717     asmlinkage int sys_vfork(struct pt_regs regs)
718     {
719             return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, &regs, 0);
720     }
721
722     /*
723      * sys_execve() executes a new program.

就像gdb顯示的一樣,函數sys_vfork調用函數do_fork,再看棧跟蹤顯示的第2幀,gdb顯示它在文件semaphore.h中的行號是120,顯示的行號雖然沒有用但是正確的。查看該文件可得到確認,方法如下:

(gdb) list semaphore.h:118
113      */
114     static inline void down(struct semaphore * sem)
115     {
116     #if WAITQUEUE_DEBUG
117             CHECK_MAGIC(sem->__magic);
118     #endif
119
120             __asm__ __volatile__(                                              <-----
121                     "# atomic down operation\n\t"
122                     LOCK "decl %0\n\t"     /* --sem->count */

上述代碼中,在箭頭所指示語句處,得到的信息僅是它在do_fork的一個擴展的內聯函數down中。gdb還打印了在do_fork中從下一個被調用的函數開始代碼的絕對地址:0xc011433b。這里我們用腳本disasfun找出該地址所對應的代碼行。命令"disasfun vmlinux do_fork"輸出的部分結果顯示如下:

if ((clone_flags & CLONE_VFORK) && (retval > 0))
c011431d:       8b 7d 08                mov    0x8(%ebp),%edi
c0114320:       f7 c7 00 40 00 00       test   $0x4000,%edi
c0114326:       74 13                   je     c011433b <do_fork+0x707>
c0114328:       83 7d d4 00             cmpl   $0x0,0xffffffd4(%ebp)
c011432c:       7e 0d                   jle    c011433b <do_fork+0x707>
#if WAITQUEUE_DEBUG
CHECK_MAGIC(sem->__magic);
#endif
        __asm__ __volatile__(
c011432e:       8b 4d d0                mov    0xffffffd0(%ebp),%ecx
c0114331:       f0 ff 4d ec             lock decl 0xffffffec(%ebp)
c0114335:       0f 88 68 95 13 00       js     c024d8a3 <stext_lock+0x7bf>
                down(&sem);
        return retval;
c011433b:       8b 45 d4                mov    
0xffffffd4(%ebp),%eax              <-----
c011433e:       e9 8d 00 00 00          jmp    c01143d0 <do_fork+0x79c>
Looking at the code in fork.c  we know where above code is:
fork_out:
if ((clone_flags & CLONE_VFORK) && (retval > 0))
    down(&sem)

(7)線程分析

gdb具有分析應用程序線程的特征,它提供了應用程序創建的線程的列表,它允許開發者查看其中任何一個線程。gdb的特征可用來與kgdb一起查看內核線程。gdb能提供內核中所有線程的列表。開發者可指定一個線程進行分析。像backtrace,info regi這樣的gdb命令接着可以顯示指定線程上下文的信息。

應用程序創建的所有線程分享同一地址空間,相似地,所有內核線程共享內核地址空間。每個內核線程的用戶地址空間可能不同,因此,gdb線程可較好地分析內核代碼和駐留在內核空間的數據結構。

gdb info給出了關於gdb線程分析方面更多的信息。下面列出一個內核線程分析的樣例:

gdb命令"info threads"給出了內核線程的列表,顯示如下:

(gdb) info thr
  21 thread 516  schedule_timeout (timeout=2147483647) at sched.c:411
  20 thread 515  schedule_timeout (timeout=2147483647) at sched.c:411
  19 thread 514  schedule_timeout (timeout=2147483647) at sched.c:411
  18 thread 513  schedule_timeout (timeout=2147483647) at sched.c:411
  17 thread 512  schedule_timeout (timeout=2147483647) at sched.c:411
  16 thread 511  schedule_timeout (timeout=2147483647) at sched.c:411
  15 thread 438  schedule_timeout (timeout=2147483647) at sched.c:411
  14 thread 420  schedule_timeout (timeout=-1013981316) at sched.c:439
  13 thread 406  schedule_timeout (timeout=-1013629060) at sched.c:439
  12 thread 392  do_syslog (type=2, buf=0x804dc20 "run/utmp", len=4095)
    at printk.c:182
  11 thread 383  schedule_timeout (timeout=2147483647) at sched.c:411
  10 thread 328  schedule_timeout (timeout=2147483647) at sched.c:411
  9 thread 270  schedule_timeout (timeout=-1011908724) at sched.c:439
  8 thread 8  interruptible_sleep_on (q=0xc02c8848) at sched.c:814
  7 thread 6  schedule_timeout (timeout=-1055490112) at sched.c:439
  6 thread 5  interruptible_sleep_on (q=0xc02b74b4) at sched.c:814
  5 thread 4  kswapd (unused=0x0) at vmscan.c:736
  4 thread 3  ksoftirqd (__bind_cpu=0x0) at softirq.c:387
  3 thread 2  context_thread (startup=0xc02e93c8) at context.c:101
  2 thread 1  schedule_timeout (timeout=-1055703292) at sched.c:439
* 1 thread 0  breakpoint () at gdbstub.c:1159
(gdb)

如上所顯示,gdb為每個線程設定在gdb中唯一的id,當gdb內部引用一個線程時,可以使用這個id。例如:線程7(PID 7)具有gdb id 8。為了分析內核線程8 ,我們指定線程9給gdb。gdb接着切換到該線程中,准備做更多的分析。

下面是分析線程的命令顯示:

(gdb) thr 9
[Switching to thread 9 (thread 270)]
#0  schedule_timeout (timeout=-1011908724) at sched.c:439
439             del_timer_sync(&timer);
(gdb) bt
#0  schedule_timeout (timeout=-1011908724) at sched.c:439
#1  0xc0113f36 in interruptible_sleep_on_timeout (q=0xc11601f0, timeout=134)
    at sched.c:824
#2  0xc019e77c in rtl8139_thread (data=0xc1160000) at 8139too.c:1559
#3  0xc010564b in kernel_thread (fn=0x70617773, arg=0x6361635f,
    flags=1767859560) at process.c:491
#4  0x19 in uhci_hcd_cleanup () at uhci.c:3052
#5  0x313330 in ?? () at af_packet.c:1891
Cannot access memory at address 0x31494350
(gdb) info regi
eax            0xc38fdf7c       -1013981316
ecx            0x86     134
edx            0xc0339f9c       -1070358628
ebx            0x40f13  266003
esp             0xc3af7f74       0xc3af7f74
ebp            0xc3af7fa0       0xc3af7fa0
esi            0xc3af7f8c       -1011908724
edi            0xc3af7fbc       -1011908676
eip            0xc011346d       0xc011346d
eflags         0x86     134
cs             0x10     16
ss             0x18     24
ds             0x18     24
es             0x18     24
fs             0xffff   65535
gs             0xffff   65535
fctrl          0x0      0
fstat          0x0      0
ftag           0x0      0
fiseg          0x0      0
fioff          0x0      0
foseg          0x0      0
fooff          0x0      0
---Type <return> to continue, or q <return> to quit---
fop            0x0      0
(gdb) thr 7
[Switching to thread 7 (thread 6)]
#0  schedule_timeout (timeout=-1055490112) at sched.c:439
439             del_timer_sync(&timer);
(gdb) bt
#0  schedule_timeout (timeout=-1055490112) at sched.c:439
#1  0xc0137ef2 in kupdate (startup=0xc02e9408) at buffer.c:2826
#2  0xc010564b in kernel_thread (fn=0xc3843a64, arg=0xc3843a68,
    flags=3280222828) at process.c:491
#3  0xc3843a60 in ?? ()
Cannot access memory at address 0x1f4
(gdb)

用戶可從http://sourceware.org/gdb/download/下載進程信息宏ps和psname,ps宏提供了運行在內核的線程的名字和ID。運行結果顯示如下:

(gdb) ps
0 swapper
1 init
2 keventd
3 ksoftirqd_C
4 kswapd
5 bdflush
6 kupdated
8 khubd
270 eth0
328 portmap
383 syslogd
392 klogd
406 atd
420 crond
438 inetd
511 mingetty
512 mingetty
513 mingetty
514 mingetty
515 mingetty
516 mingetty
(gdb) 
The psname macro can be used to get name of a thread when it's id is known.
(gdb) psname 8
8 khubd
(gdb) psname 7
(gdb)
 
(7)Watchpoints

(7)Watchpoints

kgdb stub使用x86處理器的調試特征支持硬件斷點,這些斷點不需要代碼修改。它們使用調試寄存器。x86體系中的ia32處理器有4個硬件斷點可用。每個硬件斷點可以是下面三個類型之一:

執行斷點 當代碼在斷點地址執行時,觸發執行斷點。由於硬件斷點有限,建議通過gdb break命令使用軟件斷點,除非可避免修改代碼。

寫斷點 當系統對在斷點地址的內存位置進行寫操作時,觸發一個寫斷點。寫斷點可以放置可變長度的數據。寫斷點的長度指示為觀察的數據類型長度,1表示為字節數據,2表示為2字節數據,3表示為4字節數據。

訪問斷點 當系統讀或寫斷點地址的內存時,觸發一個訪問斷點。訪問斷點也有可變長度數據類型。

ia-32處理器不支持IO斷點。

因為gdb stub目前不使用gdb用於硬件斷點的協議,因此,它通過gdb宏訪問硬件斷點。硬件斷點的gdb宏說明如下:

1)hwebrk – 放置一個執行斷點。

用法:hwebrk breakpointno address

2)hwwbrk – 放置一個寫斷點。

用法:hwwbrk breakpointno length address

3)hwabrk – 放置一個訪問斷點。

用法:hwabrk breakpointno length address

4)hwrmbrk – 刪除一個斷點

用法:hwrmbrk breakpointno

5)exinfo – 告訴是否有一個軟件或硬件斷點發生。如果硬件斷點發生,打印硬件斷點的序號。

這些命令要求的參數說明如下:

breakpointno – 0~3

length - 1~3

address - 16進制內存位置(沒有0x),如:c015e9bc

使用UML調試Linux內核

用戶模式Linux(User Mode Linux,UML)不同於其他Linux虛擬化項目,UML盡量將它自己作為一個普通的程序。UML與其他虛擬化系統相比,優點說明如下:

良好的速度

UML編譯成本地機器的代碼,像主機上的其他已編譯應用程序一樣運行。它比在軟件上應用整個硬件構架的虛擬機快得多。另一方面,UML不需要考慮依賴於特定CPU的虛擬化系統的硬件特異性。

獲益於Liunx更新

每次Linux的改進,UML自動得到這些功能,虛擬化系統並不一定能從更新中獲益。

彈性編碼

內核需要與硬件或虛擬硬件交互,但UML可將交互看作其他方式。例如:可以將這些交互轉換成共享的庫,其他程序可以在使用時連接該庫。它還可作為其他應用程序的子shell啟動,能任何其他程序的stin/stdout使用。

可移植性

UML將來可以移植到x86 Windows, PowerPC Linux, x86 BSD或其他系統上運行。

從Linux2.6.9版本起,用戶模式Linux(User mode Linux,UML)已隨Linux內核源代碼一起發布,它存放於arch/um目錄下。編譯好UML的內核之后,可直接用gdb運行編譯好的內核並進行調試。

UML原理

用戶模式Linux(User mode Linux,UML)將Linux內核的一部分作為用戶空間的進程運行,稱為客戶機內核。UML運行在基於Linux系統調用接口所實現的虛擬機。UML運行的方式如圖1所示。UML像其他應用程序一樣與一個"真實"的Linux內核(稱為"主機內核")交互。應用程序還可運行在UML中,就像運行在一個正常的Linux內核下。

 

Linux kernel debug method 01.gif

圖1 UML在Linux系統中運行的位置

使用UML的優點列出如下:

如果UML崩潰,主機內核還將運行完好。

可以用非root用戶運行UML。

可以像正常進程一樣調試UML。

在不中斷任何操作下與內核進行交互。

用UML作為測試新應用程序的"沙箱",用於測試可能有傷害的程序。

可以用UML安全地開發內核。

可以同時運行不同的發布版本。

由於UML基於以Linux系統調用接口實現的虛擬機,UML無法訪問主機的硬件設備。因此,UML不適合於調試與硬件相關的驅動程序。

編譯UML模式客戶機Linux內核

(1)獲取源代碼

http://www.kernel.org/下載linux-2.6.24.tar.bz2,解壓縮源代碼,方法如下:

host% bunzip2 linux-2.6.24.tar.bz2

host% tar xf linux-2.6.24.tar

host% cd linux-2.6.24

(2)配置UML模式內核

如果使用缺省配置,那么,方法如下:

host% make defconfig ARCH=um

如果運行配置界面,方法如下:

host% make menuconfig ARCH=um

如果不使用缺省配置defconfig,那么,內核編譯將使用主機的配置文件,該配置文件在主機/boot目錄下。對於UML模式內核來說,這是不對的,它將編譯產生缺乏重要的驅動程序和不能啟動的UML。

以編譯UML時,每個make命令應加上選項"ARCH=um",或者設置環境變量"export ARCH=um"。

當再次配置時,可以先運行下面的命令清除所有原來編譯產生的影響:

host% make mrproper

host% make mrproper ARCH=um

內核提供了配置選項用於內核調試,這些選項大部分在配置界面的kernel hacking菜單項中。一般需要選取CONFIG_DEBUG_INFO選項,以使編譯的內核包含調試信息。

(3)編譯UML模式內核

編譯內核的方法如下:

host% make ARCH=um

當編譯完成時,系統將產生名為"linux"的UML二進制。查看方法如下:

host% ls -l linux

-rwxrwxr-x 2 jdike jdike 18941274 Apr 7 15:18 linux

由於UML加入了調試符號,UML模式內核變得很大,刪除這些符號將會大大縮小內核的大小,變為與標准內核接近的UML二進制。

現在,用戶可以啟動新的UML模式內核了。

(4)UML的工具

使用UML和管理UML的工具說明如下:

UMLd – 用於創建UML實例、管理實例啟動/關閉的后台程序。

umlmgr –用於管理正運行的UML實例的前台工具程序。

UML Builder – 編譯根文件系統映像(用於UML模式操作系統安裝)。

uml switch2 用於后台傳輸的用戶空間虛擬切換。

VNUML – 基於XML的語言,定義和啟動基於UML的虛擬網絡場景。

UMLazi – 配置和運行基於虛擬機的UML的管理工具。

vmon – 運行和監管多個UML虛擬機的輕量級工具,用Python 書寫。

umvs – umvs是用C++和Bash腳本寫的工具,用於管理UML實例。該應用程序的目的是簡化UML的配置和管理。它使用了模板,使得編寫不同的UML配置更容易。

MLN - MLN (My Linux Network) 是一個perl程序,用於從配置文件創建UML系統的完整網絡,使得虛擬網絡的配置和管理更容易。MLN基於它的描述和簡單的編程語言編譯和配置文件系統模板,並用一種組織方式存儲它們。它還產生每個虛擬主機的啟動和停止腳本,在一個網絡內啟動和停止單個虛擬機。MLN可以一次使用幾個獨立的網絡、項目,甚至還可以將它們連接在一起。

Marionnet – 一個完全的虛擬網絡實驗,基於UML,帶有用戶友好的圖形界面。

運行UML

(1)啟動UML

為了運行UML實例,用戶需要運行Linux操作系統主機和帶有自己文件系統的UML客戶機。用戶可以從http://uml.nagafix.co.uk/下載UML(如:kernel)和客戶機文件系統(如:root_fs),運行UML實例的方法如下:

$ ./kernel ubda= root_fs mem=128M

上述命令中,參數mem指定虛擬機的內存大小;參數ubda表示根文件系統root_fs作為虛擬機第一個塊設備,虛擬機用/dev/udba表示虛擬機的第一個塊設備,與Linux主機系統的第一個物理塊設備/dev/sda類似。

用戶還可以自己創建虛擬塊設備,例如:建立交換分區並在UML上使用它的方法如下:

$ dd if=/dev/zero of=swap bs=1M count=128

$ ./kernel ubda= root_fs ubdb=swap mem=128M

上述命令,創建了128M的交換分區,作為第二個塊設備ubdb,接着,啟動UML模式內核,用ubdb作為它的交換分區。

(2)登錄

預打包的文件系統有一個帶有"root"密碼的root帳戶,還有一個帶有"user"密碼的user帳戶。用戶登錄后可以進入虛擬機。預打包的文件系統已安裝了各種命令和實用程序,用戶還可容易地添加工具或程序。

還有一些其他登錄方法,說明如下:

在虛擬終端上登錄

每個已配置(設備存在於/dev,並且/etc/inittab在上面運行了一個getty)的虛擬終端有它自己的xterm。.

通過串行線登錄

在啟動輸出中,找到類似下面的一行:

serial line 0 assigned pty /dev/ptyp1

粘貼用戶喜愛的終端程序到相應的tty,如:minicom,方法如下:

host% minicom -o -p /dev/ttyp1

通過網絡登錄

如果網絡正運行,用戶可用telnet連接到虛擬機。

虛擬機運行后,用戶可像一般Linux一樣運行各種shell命令和應用程序。

建立串行線和控制台

可以粘附UML串行線和控制台到多個類型的主機I/O通道,通過命令行指定,用戶可以粘附它們到主機ptys, ttys, 文件描述子和端口。常用連接方法說明如下:

讓UML控制台出現在不用的主機控制台上。

將兩個虛擬機連接在一起,一個粘到pty,另一個粘附到相應的tty。

創建可從網絡訪問的虛擬,粘附虛擬機的控制台到主機的一個端口。

(1)指定設備

用選項"con"或"ssl"(分別代表控制台和串行線)指定設備。例如:如果用戶想用3號控制台或10號串行線交互,命令行選項分別為"con3"和"ssl10"。

例如:指定pty給每個串行線的樣例選項列出如下:

ssl=pty ssl0=tty:/dev/tty0 ssl1=tty:/dev/tty1

(2)指定通道

可以粘附UML設備到多個不同類型的通道,每個類型有不同的指定方法,分別說明如下:

偽終端為:device=pty,pts終端為:device=pts

UML分配空閑的主機偽終端給。用戶可以通過粘附終端程序到相應的tty訪問偽終端,方法如下:

screen /dev/pts/n

screen /dev/ttyxx

minicom -o -p /dev/ttyxx #minicom似乎不能處理pts設備

kermit #啟動它,打開設備,然后連接設備

終端為:device=tty:tty設備文件

UML將粘附設備到指定的tty,例如:一個樣例選項列出如下:

con1=tty:/dev/tty3

上面語句將粘附UML的控制台1到主機的/dev/tty3。如果用戶指定的tty是tty/pty對的slave端,則相應的pty必須已打開。

xterms為:device=xterm

UML將運行一個xterm,並且將設備粘附到xterm。

端口為:device=port:端口號

上述選項將粘附UML設備到指定的主機端口。例如:粘附控制台1到主機的端口9000,方法如下:

con1=port:9000

粘附所有串行線到主機端口,方法如下:

ssl=port:9000

用戶可以通過telnet到該端口來訪問這些設備,每個激活的telnet會話得到不同的設備,如果有比粘附到端口的UML設備多的telnet連接到一個端口,格外的telnet會話將阻塞正存在的telnet斷線,或直到其他設備變為激活(如:通過在/etc/inittab中設置激活)。

已存在的文件描述子:device=文件描述子

如果用戶在UML命令行中建立了一個文件描述子,他可以粘附UML設備到文件描述子。這最常用於在指定所有其他控制台后將主控制台放回到stdin和stdout上。方法如下:

con0=fd:0,fd:1 con=pts

null設備:device=null

與"none"選項相比,上述選項允許打開設備,但讀將阻塞,並且寫將成功,但數據會被丟掉。

無設備:device=none

上述選項將引起設備消失。如果你正使用devfs,設備將不出現在/dev下。如果設備出現,嘗試打開它將返回錯誤-ENODEV。

用戶還可以指定不同的輸入和輸出通道給一個設備,最常用的用途是重粘附主控制到stdin和stdout。例如:一個樣例選項列出如下:

ssl3=tty:/dev/tty2,xterm

上述詩句將引起在主機/dev/tty3上的串行線3接受輸入,顯示輸出在xterm上。

如果用戶決定將主控制台從stdin/stdout移開,初始的啟動輸出將出現在用戶正運行UML所在的終端。然而,一旦控制台驅動程序已初始化,啟動及隨后的輸出將出現在控制台0所在的地方。

建立網絡

UML實例可以用網絡訪問主機、本地網絡上的其他機器和網絡的其他部分。新的輔助程序uml_net進行主機建立時需要root權限。

當前UML虛擬機有5種傳輸類型用於與其他主機交換包,分別是:ethertap,TUN/TAP, Multicast,交換機后台(switch daemon),slip,slirp和pcap。

TUN/TAP, ethertap, slip和slirp傳輸允許UML實例與主機交換包。它們可定向到主機或主機可扮作路由器提供對其他物理或虛擬機的訪問。

pcap傳輸是一個綜合的僅讀接口,用libpcap二進制從主機上的接口收集包並過濾包。這對於構建預配置的交通監管器或sniffer來說,是有用的。

后台和多播傳輸提供了完全虛擬的網絡絡其他虛擬機器。該網絡完全從物理網絡斷開,除非某一個虛擬機扮作網關。

如何選擇這些主機傳輸類型 '用戶可根據用途進行選擇,選擇方法說明如下:

ethertap – 如果用戶想對主機網絡進行訪問,並且運行以2.2以前版本時,使用它。

TUN/TAP – 如果用戶想訪問主機網絡,可使用它。TUN/TAP 運行在2.4以后的版本,比ethertap 更有效率。TUN/TAP 傳輸還能使用預置的設備,避免對uml_net輔助程序進行setuid操作。

Multicast – 如果用戶期望建立一個純虛擬網絡,並且僅想建立UML,就使用它。

交換機后台 – 如果用戶想建立一個純虛擬網絡,並且不介意為了得到較好執行效率而建立后台,就使用它。

slip – 沒有特殊理由不要運行slip后端,除非ethertap和TUN/TAP不可用。

slirp – 如果用戶在主機上沒有root權限對建立網絡進行訪問,或者如果用戶不想分配對UML分配IP時,使用它。

pcap – 對實際的網絡連接沒有太多用途,但用於主機上監管網絡交通很有用。

(1)網絡建立通用步驟

首先,用戶必須已在UML中打開虛擬網絡。如果運行下載的預編譯的內核,則已打開虛擬網絡。如果用戶自己編譯內核,則在配置界面上的"Network device support"菜單中,打開"Network device support"和三種傳輸選項。

下一步是提供網絡設備給虛擬機,通過在內核命令行中進行描述,格式如下:

eth <n> = <transport> , <transport args>

例如:一個虛擬以太網設備可以如下粘附到一個主機ethertap上:

eth0=ethertap,tap0,fe:fd:0:0:0:1,192.168.0.254

上述語句在虛擬機內部建立eth0,粘附它自己到主機/dev/tap0,指定一個以太網地址,並指定給主機tap0接口一個IP地址。

一旦用戶決定如何建立設備后,就可以啟動UML、登錄、配置設備的UML側,並設置對外界的路由。此后,UML就可以與網絡任何其他機器(物理或虛擬的)通信。

(2)用戶空間后台

http://www.user-mode-linux.org/cvs/tools/下載工具uml_net和uml_switch,編譯並安裝。uml_switch是在UML系統之間管理虛擬網絡的后台,而不用連接到主機系統的網絡。uml_switch將在UNIX域的socket上監聽連接,並在連接到UNIX域的客戶端之間轉發包。

(3)指定以太網地址

TUN/TAP, ethertap和daemon接口允許用戶給虛擬以太網設備指定硬件地址。但通常不需要指定硬件地址。如果命令行沒有指定硬件地址,它將提供地址為fe:fd:nn:nn:nn:nn,其中,nn.nn.nn.nn是設備IP地址。這種方法通常足夠保證有唯一的硬件地址。

(4)UML接口建立

一旦用命令行描述網絡設備,用戶在啟動UML和登錄后,第一件事應是建立接口,方法如下:

UML# ifconfig ethn ip-address up

此時,用戶應可以ping通主機。為了能查看網絡,用戶設置缺省的路由為到達主機,方法如下:

UML# route add default gw host ip

例如:主機IP為192.168.0.4,設置路由方法如下:

UML# route add default gw 192.168.0.4

注意:如果UML不能與物理以太網上其他主機通信,可能是因為網絡路由自動建立,可以運行"route –n"查看路由,結果類似如下:

Destination Gateway Genmask Flags Metric Ref Use Iface

192.168.0.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0

掩碼不是255.255.255.255,因此,就使用到用戶主機的路由替換它,方法如下:

UML# route del -net 192.168.0.0 dev eth0 netmask 255.255.255.0

UML# route add -host 192.168.0.4 dev eth0

添加缺省的路由到主機,將允許UML與用戶以太網上任何機器交換包。

(5)多播

在多個UML之間建立一個虛擬網絡的最簡單方法是使用多播傳輸。用戶的系統必須在內核中打開多播(multicast),並且在主機上必須有一個多播能力的網絡設備。通常它是eth0。

為了使用多播,運行兩個UML,命令行帶有"eth0=mcast"選項。登錄后,用戶在每個虛擬機上用不同的IP地址配置以太網設備,方法如下:

UML1# ifconfig eth0 192.168.0.254

UML2# ifconfig eth0 192.168.0.253

這兩個虛擬機應能相互通信。

傳輸設置的整個命令行選項列出如下:

ethn=mcast,ethernet address,multicast address,multicast port,ttl

(6)TUN/TAP和uml_net輔助程序

TUN/TAP驅動程序實現了虛擬網卡的功能,TUN 表示虛擬的是點對點設備,TAP表示虛擬的是以太網設備,這兩種設備針對網絡包實施不同的封裝。利用TUN/TAP驅動,可以將tcp/ip協議棧處理好的網絡分包傳給任何一個使用TUN/TAP驅動的進程,由進程重新處理后再發到物理鏈路中。

TUN/TAP是與主機交換包的較好機制,主機建立TUN/TAP較簡單的方法是使用uml_net輔助程序,它包括插入tun.o內核模塊、配置設備、建立轉發IP、路由和代理ARP。

如果在設備的主機側指定了IP地址,uml_net將在主機上做所有的建立工作。粘附設備到TUN/TAP設備的命令行格式列出如下:

eth <n> =tuntap,,, <host IP address>

例如:下面參數將粘附UML的eth0到下一個可用的tap設備,指定IP地址192.168.0.254么tap設備的主機側,並指定一個基於IP地址的以太網地址。

eth0=tuntap,,,192.168.0.254

(7)帶有預配置的tap設備的TUN/TAP

如果用戶沒有更好的uml_net,可以預先建立TUN/TAP。步驟如下:

用工具tunctl創建tap設備,方法如下:

host# tunctl -u uid

上述命令中,uid是用戶的ID或UML將運行登錄的用戶名。

配置設備IP地址,方法如下:

host# ifconfig tap0 192.168.0.254 up

建立路由和ARP,方法如下:

host# bash -c 'echo 1 > /proc/sys/net/ipv4/ip_forward'

host# route add -host 192.168.0.253 dev tap0

host# bash -c 'echo 1 > /proc/sys/net/ipv4/conf/tap0/proxy_arp'

host# arp -Ds 192.168.0.253 eth0 pub

注意:這個配置沒有重啟機時失效,每次主機啟動時,應重新設置它。最好的方法是用一個小應用程序,每次啟動時,讀出配置文件重新建立設置的配置。

使用網橋

為了不使用2個IP地址和ARP,還可通過對UML使用網橋提供對用戶LAN直接訪問,方法如下:

host# brctl addbr br0

host# ifconfig eth0 0.0.0.0 promisc up

host# ifconfig tap0 0.0.0.0 promisc up

host# ifconfig br0 192.168.0.1 netmask 255.255.255.0 up

host# brctl stp br0 off

host# brctl setfd br0 1

host# brctl sethello br0 1

host# brctl addif br0 eth0

host# brctl addif br0 tap0

注意:用戶應該用eth0的IP地址通過ifconfig建立"br0"。

運行UML

一旦設備建立好后,運行UML,命令格式為: eth0=tuntap,devicename,例如:一個樣例列出如下:

eth0=tuntap,tap0

如果用戶不再使用tap設置,可以用下面命令刪除它:

host# tunctl -d tap device

最后,tunctl有一個"-b"(用於簡捷模式)切換,僅輸出它所創建的tap設備的名字。它很適合於被一個腳本使用,方法如下:

host# TAP=`tunctl -u 1000 -b`

(8)交換機后台

交換機后台uml_switch以前稱為uml_router,它提供了創建整個虛擬網絡的機制。缺省下,它不提供對主機網絡的連接。

首先,用戶需要運行uml_switch,無參數運行時,表示它將監聽缺省的unix域socket。使用選項"-unix socket"可指定不同的socket,"-hub"可將交換機后台變為集線器(Hub)。如果用戶期望交換機后台連接到主機網絡(允許UML訪問通過主機訪問外部的網絡),可使用選項"-tap tap0"。

uml_switch還可作為后台運行,方法如下:

host% uml_switch [ options ] < /dev/null > /dev/null

內核命令行交換機的通用命令行格式列出如下:

ethn=daemon,ethernet address,socket type,socket

通常只需要指定參數"daemon",其他使用缺省參數,如果用戶運行沒有參數的交換機后台,在同一台機器上使用選項"eth0=daemon"運行UML,etho驅動程序會直接粘附它自己到交換機后台。參數socket為unix域socket的文件名,用於uml_switch和UML之間網絡通信。

(9)Slirp

slirp通常使用外部程序/usr/bin/slirp,僅通過主機提供IP網絡連接。它類似於防火牆的IP偽裝,躍然傳輸有用戶空間進行,而不是由內核進行。slirp不在主機上建立任何接口或改變路由。slirp在主機上不需要root權限或運行setuid。

slirp命令行的通用格式為:

ethn=slirp,ethernet address,slirp path

在UML上,用戶應使用沒有網關IP的etho設置缺省路由,方法如下:

UML# route add default dev eth0

slirp提供了UML可使用的大量有用IP地址,如:10.0.2.3,是DNS服務器的一個別名,定義在主機/etc/resolv.conf中,或者它是slirp的選項"dns"中給定的IP地址。

(10)pcap

pcap對網絡上傳輸的數據包進行截獲和過濾。通過命令行或pcap傳輸粘附到UML以太網設備uml_mconsole工具,語法格式如下:

ethn=pcap,host interface,filter expression,option1,option2

其中,expression和option1、option2是可選的。

這個接口是主機上用戶想嗅探(sniff)的任何網絡設備,過濾器表達式(filter expression)與工具tcpdump使用的一樣,option1為"promisc"或"nopromisc",控制pcap是否將主機接口設為"promiscuous"(混雜)模式;option2為"optimize "或"nooptimize",表示是否使用pcap表達式優化器。

一個設置pcap的樣例列出如下:

eth0=pcap,eth0,tcp

eth1=pcap,eth0,!tcp

上述語句將引起在主機eth0上的UML eth0將所有的tcp發出,並且在主機eth0上的UML eth1將發出所有非tcp包。

(11)用戶建立主機

主機上的網絡設備需要配置IP地址,還需要用值為1484的mtu配置tap設備。slip設置還需要配置點到點(pointopoint)地址,方法如下:

host# ifconfig tap0 arp mtu 1484 192.168.0.251 up

host# ifconfig sl0 192.168.0.251 pointopoint 192.168.0.250 up

如果正建立tap設備,就將路由設置到UML IP。方法如下:

UML# route add -host 192.168.0.250 gw 192.168.0.251

為了允許網絡上其他主機看見這個虛擬機,化理ARP設置如下:

host# arp -Ds 192.168.0.250 eth0 pub

最后,將主機設置到路由包,方法如下:

host# echo 1 > /proc/sys/net/ipv4/ip_forward

在虛擬機間共享文件系統

在虛擬機間共享文件系統的方法是使用ubd(UML Block Device)塊設備驅動程序的寫拷貝(copy-on-write,COW)分層能力實現。COW支持在僅讀的共享設備上分層讀寫私有設備。一個虛擬機的寫數據存儲在它的私有設備上,而讀來自任一請求塊有效的設備。如果請求的塊有效,讀取私有設備,如果無效,就讀取共享設備。

用這種方法,數據大部分在多個虛擬機間共享,每個虛擬機有多個小文件用於存放虛擬機所做的修改。當大量UML從一個大的根文件系統啟動時,這將節約大量磁盤空間。它還提供執行性能,因為主機可用較小的內存緩存共享數據,主機的內存而不是硬盤提供UML硬盤請求服務。

可通過簡單地加COW文件的名字到合適的ubd,實現加一個COW層到存在的塊設備文件。方法如下:

ubd0=root_fs_cow,root_fs_debian_22

上述語句中,"root_fs_cow"是私有的COW文件,"root_fs_debian_22"是存在的共享文件系統。COW文件不必要存在,如果它不存在,驅動程序將創建並初始化它。一旦COW文件已初始化,可在以命令行中使用它,方法如下:

ubd0=root_fs_cow

后備文件(backing file)的名字存在COW文件頭中,因此在命令行中繼續指定它將是多余的。

COW文件是稀疏的,因此它的長度不同於硬盤的實際使用長度。可以用命令"ls –ls"查看硬盤的實際消耗,用"ls –l"查看COW文件和后備文件(backing file)的長度,方法如下:

host% ls -l cow.debian debian2.2

-rw-r--r-- 1 jdike jdike 492504064 Aug 6 21:16 cow.debian

-rwxrw-rw- 1 jdike jdike 537919488 Aug 6 20:42 debian2.2

host% ls -ls cow.debian debian2.2

880 -rw-r--r-- 1 jdike jdike 492504064 Aug 6 21:16 cow.debian

525832 -rwxrw-rw- 1 jdike jdike 537919488 Aug 6 20:42 debian2.2

從上述顯示結構,用戶會發現COW文件實際硬盤消耗小於1M,面不是492M。

一旦文件系統用作一個COW文件的僅讀后備文件,不要直接從它啟動或修改它。這樣,會使使用經的任何COW文件失效。后備文件在創建時它的修改時間mtime和大小size存放在COW文件頭中,它們必須相匹配。如果不匹配,驅動程序將拒絕使用COW文件。

如果用戶手動地改變后備文件或COW頭,將得到一個崩潰的文件系統。

操作COW文件的方法說明如下:

(1)刪除后備文件

由於UML存放后備文件名和它的修改時間mtime在COW頭中,如果用戶刪除后文件文件,這些信息將變成無效的。因此,刪除后備文件的步驟如下:

用保護時間戳的方式刪除文件。通常,使用"-p"選項。拷貝操作命令"cp –a"中的"-a"隱含了"-p"。

通過啟動UML更新COW頭,命令行指定COW文件和新的后備文件的位置,方法如下:

ubda=COW file,新的后備文件位置

UML將注意到命令行和COW頭之間的不匹配,檢查新后文件路徑的大小和修改時間mtime,並更新COW頭。

如果當用戶刪除后備文件時忘記保留時間戳,用戶可手動整理mtime,方法如下:

host% mtime=UML認定的修改時間mtime; \

touch --date="`date -d 1970-01-01\ UTC\ $mtime\ seconds`" 后備文件

注意如果對真正修改過而不是剛刪除的后備文件進行上述操作,那么將會文件崩潰,用戶將丟失文件系統。

(2)uml_moo :將COW文件與它的后備文件融合

依賴於用戶如何使用UML和COW設備,系統可能建議每隔一段時間融合COW文件中的變化到后備文件中。用戶可以用工具uml_moo完成該操作,方法如下:

host% uml_moo COW file new backing file

由於信息已在COW文件頭中,因此,不必指定后備文件。

uml_moo在缺省下創建一個新的后備文件,它還有一個破壞性的融合選項,直接將融合COW文件到它當前的后備文件。當后備文件僅有一個COW文件與它相關時,該選項很有用。如果多個COW與一個后備文件相關,融合選項"-d"將使所有其他的COW無效。但是,如果硬盤空間不夠時,使用融合選項"-d"很方便快捷,方法如下:

host% uml_moo -d COW file

(3)uml_mkcow :創建新COW文件

正常創建COW文件的方法是以UML命令行中指定一個不存在的COW文件,讓UML創建COW文件。但是,用戶有時想創建一個COW文件,但不想啟動UML。此時,可以使用uml_mkcow工具。方法如下:

host% uml_mkcow 新COW文件 存在的后備文件

如果用戶想銷毀一個存在的COW文件,可以加"-f"選項強制重寫舊的COW文件,方法如下:

host% uml_mkcow -f 存在的COW文件 存在的后備文件

創建UML的文件系統

如果根文件系統硬盤空間不夠大,或者想使用不同於ext2的文件系統,用戶就可能想創建和掛接新的UML文件系統,用戶可以用如下方法創建UML的根文件系統:

(1)創建文件系統的文件

使用命令dd創建一個合適尺寸的空文件,用戶可以創建稀疏文件,該文件直到實際使用時才分配硬盤空間。例如:下面的命令創建一個100M填滿0的稀疏文件:

host% dd if=/dev/zero of=new_filesystem seek=100 count=1 bs=1M

(2)指定文件給一個UML設備

在UML命令行上加入下面的選項:

ubdd=new_filesystem

上述命令中,ubdd應確保沒被使用。

(3)創建和掛接文件系統

創建和掛接文件系統方法如下:

host# mkreiserfs /dev/ubdd

UML# mount /dev/ubdd /mnt

主機文件訪問

如果用戶在UML中想訪問主機上的文件,用戶可將主機當作獨立的機器,可以使用nfs從主機掛接目錄,或者用scp和rcp拷貝文件到虛擬機,因為UML運行在主機上,它能象其他進程一樣訪問這些文件,並使它們在虛擬機內部可用,而不需要使用網絡。

還可以使用hostfs虛擬文件系統,用戶通過它可以掛接一個主機目錄到UML文件系統,並像在主機上一樣訪問該目錄中的文件。

(1)使用hostfs

首先,確認虛擬機內部是否有hostfs可用,方法如下:

UML# cat /proc/filesystems

如果沒有列出hostfs,則需要重編譯內核,配置hostfs,將它編譯成一個內核模塊,並用"insmod"插入該內核模塊。

掛接hostfs文件系統,例如:將hostfs掛接到虛擬機的/mnt/host下,方法如下:

UML# mount none /mnt/host -t hostfs

如果用戶不想掛接主機的root目錄,他可以用"-o"選項指定掛接的子目錄。例如:掛接主機的/home到虛擬機的/mnt/home,方法如下:

UML# mount none /mnt/home -t hostfs -o /home

(2)hostfs命令行選項

在UML命令行選項可使用hostfs選項,用來指定多個hostfs掛接到一個主機目錄或阻止hostfs用戶從主機上銷毀數據,方法如下:

hostfs=directory,options

當前可用的選項是"append",用來阻止所有的文件在追加方式打開,並不允許刪除文件。

(3)hostfs作為根文件系統

還可以通過hostfs從主機上的目錄而不是在一個文件中的標准文件系統啟動UML。最簡單的方法是用loop掛接一個存在的root_fs文件,方法如下:

host# mount root_fs uml_root_dir -o loop

用戶需要將/etc/fstab中的文件類型改變為"hostfs",fstab中的該行列出如下:

none / hostfs defaults 1 1

接着用戶可以用chown將目錄中root擁有的所有文件改變為用戶擁有,方法如下:

host# find . -uid 0 -exec chown user {} \;

如果用戶不想用上面的命令改變文件屬主,用戶可以用root身份運行UML。

接着,確保UML內核編譯進hostfs,而不是以內核模塊方式包含hostfs。那么,加入下面的命令行運行UML:

root=/dev/root rootflags=/path/to/uml/root rootfstype=hostfs

加入上述選項后,UML應該像正常的一樣啟動。

(4)編譯hostfs

如果hostfs不在內核中,用戶需要編譯hostfs,用戶可以將它編譯進內核或內核模塊。用戶在內核配置界面上選項hostfs,並編譯和安裝內核。

內核調試

因為UML運行為正常的Linux進程,用戶可以用gdb像調試其他進程一樣調試內核,稍微不同的是:因為內核的線程已用系統調用ptrace進行攔截跟蹤,因此,gdb不能ptrace它們。UML已加入了解決此問題的機制。

為了調試內核,用戶需要從源代碼編譯,確保打開CONFIG_DEBUGSYM和CONFIG_PT_PROXY配置選項。它們分別用來確保編譯內核帶有"-g"選項和打開ptrace代理,以便gdb能與UML一起工作調試內核。

(1)在gdb下啟動內核

用戶可以在命令行中放入"debug"選項,在啟動UML時將內核放在gdb的控制之下。用戶可以得到一個運行gdb的xterm,內核將送一些命令到gdb,停在"start_kernel"處,用戶可以輸入"next", "step"或"cont"運行內核。

(2)檢查睡眠的進程

並非每個bug在當前運行的進程中,有時候,當進程在信號量上或其他類似原因死鎖時,原本不應該掛起的進程在內核中掛起。這種情況下,用戶在gdb中用"Ctrl+C"時,得到一個跟蹤棧,用戶將可以看見到不相關的空閑線程。

用戶本想看到的是不應該睡眠的進程的棧,為了看到睡眠的進程,用戶可以在主機上用命令ps得到該進程的主機進程id。

用戶將gdb與當前線程分離,方法如下:

(UML gdb) det

然后將gdb粘附到用戶感興趣的線程上,方法如下:

(UML gdb) att <host pid>

查看該線程的棧,方法如下:

(UML gdb) bt

(3)在UML上運行ddd

ddd可以工作於UML,用戶可以主機上運行ddd,它給gdb提供了圖形界面。運行ddd的步驟如下:

啟動ddd,方法如下:

host% ddd linux

得到gdb的pid

用命令ps可以得到ddd啟動的gdb的pid。

運行UML

在運行UML的命令行中加上選項"debug=parent gdb-pid=<pid>",啟動並登錄UML。

在ddd的gdb命令行中輸入"att 1",gdb顯示如下:

0xa013dc51 in __kill ()

(gdb)

在gdb中輸入"c",UML將繼續運行,用戶可接着像調試其他進程一樣調試了。

(4)調試內核模塊

gdb已支持調試動態裝載入進程的代碼,這需要在UML下調試內核模塊。調試內核模塊有些復雜,用戶需要告訴gdb裝入UML的對象文件名以及它在內存中的位置。接着,它能讀符號表,並從裝載地址指出所有的符號。

當用戶在rmmod內核模塊后重裝載它時,可得到更多信息。用戶必須告訴gdb忘記所有它的符號,包括主UML的符號,接着再裝載回所有的符號。

用戶可以使用腳本umlgdb進行內核模塊的重裝載和讀取它的符號表。用戶還可以手動進行一步步處理完成符號表的獲取工作。下面分別說明這兩種方法。

1)運行腳本umlgdb調試內核模塊

運行腳本umlgd較容易獲取內核模塊的符號表。

首先,用戶應告訴內核模塊所在的位置,在腳本中有一個列表類似如下:

set MODULE_PATHS {

"fat" "/usr/src/uml/linux-2.6.18/fs/fat/fat.ko"

"isofs" "/usr/src/uml/linux-2.6.18/fs/isofs/isofs.ko"

"minix" "/usr/src/uml/linux-2.6.18/fs/minix/minix.ko"

}

用戶將上述列表改為將調試的內核模塊的路徑,接着,從UML的頂層目錄運行該腳本,顯示如下:

                • GDB pid is 21903 ********

Start UML as: ./linux <kernel switches> debug gdb-pid=21903

GNU gdb 5.0rh-5 Red Hat Linux 7.1

Copyright 2001 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for details.

This GDB was configured as "i386-redhat-linux"...

(gdb) b sys_init_module

Breakpoint 1 at 0xa0011923: file module.c, line 349.

(gdb) att 1

在用戶運行UML后,用戶只需要在"att 1"按回車,並繼續執行它。方法如下:

Attaching to program: /home/jdike/linux/2.4/um/./linux, process 1

0xa00f4221 in __kill ()

(UML gdb) c

Continuing.

此時,當用戶用insmod插入內核模塊,顯示列出如下:

      • Module hostfs loaded ***

Breakpoint 1, sys_init_module (name_user=0x805abb0 "hostfs",

mod_user=0x8070e00) at module.c:349

349 char *name, *n_name, *name_tmp = NULL;

(UML gdb) finish

Run till exit from #0 sys_init_module (name_user=0x805abb0 "hostfs",

mod_user=0x8070e00) at module.c:349

0xa00e2e23 in execute_syscall (r=0xa8140284) at syscall_kern.c:411

411 else res = EXECUTE_SYSCALL(syscall, regs);

Value returned is $1 = 0

(UML gdb)

p/x (int)module_list + module_list->size_of_struct

$2 = 0xa9021054

(UML gdb) symbol-file ./linux

Load new symbol table from "./linux" ' (y or n) y

Reading symbols from ./linux...

done.

(UML gdb)

add-symbol-file /home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o 0xa9021054

add symbol table from file "/home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o" at

.text_addr = 0xa9021054

(y or n) y

Reading symbols from /home/jdike/linux/2.4/um/arch/um/fs/hostfs/hostfs.o...

done.

(UML gdb) p *module_list

$1 = {size_of_struct = 84, next = 0xa0178720, name = 0xa9022de0 "hostfs",

size = 9016, uc = {usecount = {counter = 0}, pad = 0}, flags = 1,

nsyms = 57, ndeps = 0, syms = 0xa9023170, deps = 0x0, refs = 0x0,

init = 0xa90221f0 <init_hostfs>, cleanup = 0xa902222c <exit_hostfs>,

ex_table_start = 0x0, ex_table_end = 0x0, persist_start = 0x0,

persist_end = 0x0, can_unload = 0, runsize = 0, kallsyms_start = 0x0,

kallsyms_end = 0x0,

archdata_start = 0x1b855 <Address 0x1b855 out of bounds>,

archdata_end = 0xe5890000 <Address 0xe5890000 out of bounds>,

kernel_data = 0xf689c35d <Address 0xf689c35d out of bounds>}

>> Finished loading symbols for hostfs ...

(2)手動調試內核模塊

在調試器中啟動內核,並用insmod或modprobe裝載內核模塊。在gdb中執行下面命令:

(UML gdb) p module_list

這是已裝載進內核的內核模塊列表,通常用戶期望的內核模塊在module_lis中。如果不在,就進入下一個鏈接,查看name域,直到找到用戶調試的內核模塊。獲取該結構的地址,並加上module.size_of_struct值,gdb可幫助獲取該值,方法如下:

(UML gdb) printf "%#x\n", (int)module_list module_list->size_of_struct

從內核模塊開始處的偏移偶爾會改變,因此,應檢查init和cleanup的地址,方法如下:

(UML gdb) add-symbol-file /path/to/module/on/host that_address

如果斷點不在正確的位置或不工作等 ,用戶可以查看內核模塊結構,init和cleanup域應該類似如下:

init = 0x588066b0 <init_hostfs>, cleanup = 0x588066c0 <exit_hostfs>

如果名字正確,但它們有偏移,那么,用戶應該將偏移加到add-symbol-file所在地址上。

當用戶想裝載內核模塊的新版本時,需要讓gdb刪除舊內核模塊的所有符號。方法如下:

(UML gdb) symbol-file

接着,從內核二進制重裝載符號,方法如下:

(UML gdb) symbol-file /path/to/kernel

然后,重復上面的裝載符號過程。還需要重打開斷點。

(5)粘附gdb到內核

如果用戶還沒有在gdb下運行內核,用戶可以通過給跟蹤線程發送一個SIGUSR1,用於以后粘附gdb到內核。控制台第一行的輸出鑒別它的id,顯示類似如下:

tracing thread pid = 20093

發送信號的方法如下:

host% kill -USR1 20093

上述命令運行后,用戶將可看見帶有gdb運行的xterm。

如果用戶已將mconsole(UML的控制台)編譯進UML,那么可用mconsole客戶端啟動gdb,方法如下:

(mconsole) (mconsole) config gdb=xterm

上述命令運行后,用戶將可看見帶有gdb運行的xterm。

(6)使用可替換的調試器

UML支持粘附到一個已運行的調試器,而不是啟動gdb本身。當gdb是一些UI的子進程(如:emacs或ddd)時,這將是有用的。它還被用於在UML上運行非gdb的調試器。下面是一個使用strace作為可替代調試器的例子。

用戶需要得到調試器的pid,並將pid用"gdb-pid=<pid>"選項與"debug"選項一起傳遞。

如果用戶在UI下使用gdb,那么,應告訴UML"att 1",那么,UI將粘附到UML。

下面以替換調試器strace為例,用戶可以用strace調試實際的內核,方法如下:

在shell中運行下述命令

host%

sh -c 'echo pid=$$; echo -n hit return; read x; exec strace -p 1 -o strace.out'

用"debug"和"gdb-pid=<pid>"運行UML。

strace輸出將出現在輸出文件中。

注意:運行下面的命令,結果不同於前面命令。

host% strace ./linux

上述命令將僅strace主UML線程,跟蹤的線程不做任何實際的內核操作。它僅標識出虛擬機。而使用上述的strce將顯示虛擬機低層的活動情況。

斷言語句

在代碼里面老能看到 BUG_ON() , WARN_ON() 這樣的宏 , 類似 我們日常編程里面的斷言(assert) 。

在include/asm-generic/bug.h

#ifdef CONFIG_BUG
 
#ifdef CONFIG_GENERIC_BUG
#ifndef __ASSEMBLY__
struct bug_entry {
	unsigned long	bug_addr;
#ifdef CONFIG_DEBUG_BUGVERBOSE
	const char	*file;
	unsigned short	line;
#endif
	unsigned short	flags;
};
#endif		/* __ASSEMBLY__ */
 
#define BUGFLAG_WARNING	(1<<0)
#endif	/* CONFIG_GENERIC_BUG */
 
#ifndef HAVE_ARCH_BUG
#define BUG() do { \
	printk("BUG: failure at %s:%d/%s()!\n", __FILE__, __LINE__, __FUNCTION__); \
	panic("BUG!"); \
} while (0)
#endif
 
#ifndef HAVE_ARCH_BUG_ON
#define BUG_ON(condition) do { if (unlikely(condition� BUG(); } while(0)
#endif
 
#ifndef __WARN
#ifndef __ASSEMBLY__
extern void warn_on_slowpath(const char *file, const int line);
#define WANT_WARN_ON_SLOWPATH
#endif
#define __WARN() warn_on_slowpath(__FILE__, __LINE__)
#endif
 
#ifndef WARN_ON
#define WARN_ON(condition) ({						\
	int __ret_warn_on = !!(condition);				\
	if (unlikely(__ret_warn_on�					\
		__WARN();						\
	unlikely(__ret_warn_on);					\
})
#endif
 
#else /* !CONFIG_BUG */
#ifndef HAVE_ARCH_BUG
#define BUG()
#endif
 
#ifndef HAVE_ARCH_BUG_ON
#define BUG_ON(condition) do { if (condition) ; } while(0)
#endif
 
#ifndef HAVE_ARCH_WARN_ON
#define WARN_ON(condition) ({						\
	int __ret_warn_on = !!(condition);				\
	unlikely(__ret_warn_on);					\
})
#endif
#endif
 
#define WARN_ON_ONCE(condition)	({				\
	static int __warned;					\
	int __ret_warn_once = !!(condition);			\
								\
	if (unlikely(__ret_warn_once�				\
		if (WARN_ON(!__warned� 			\
			__warned = 1;				\
	unlikely(__ret_warn_once);				\
})
 
#ifdef CONFIG_SMP
# define WARN_ON_SMP(x)			WARN_ON(x)
#else
# define WARN_ON_SMP(x)			do { } while (0)
#endif

同步鎖調試

鎖驗證器

內核鎖驗證器(Kernel lock validator)可以在死鎖發生前檢測到死鎖,即使是很少發生的死鎖。它將每個自旋鎖與一個鍵值相關,相似的鎖僅處理一次。加鎖時,查看所有已獲取的鎖,並確信在其他上下文中沒有已獲取的鎖,在新獲取鎖之后被獲取。解鎖時,確信正被解開的鎖在已獲取鎖的頂部。

.

Validate spinlocks vs interrupts behavior.

當加鎖動態發生時,鎖驗證器映射所有加鎖規則,該檢測由內核的spinlocks、rwlocks、mutexes和rwsems等鎖機制觸發。不管何時鎖合法性檢測器子系統檢測到一個新加鎖場景,它檢查新規則是否違反正存在的規則集,如果新規則與正存在的規則集一致,則加入新規則,內核正常運行。如果新規則可能創建一個死鎖場景,那么這種創建死鎖的條件會被打印出來。

當判斷加鎖的有效性時,所有可能的"死鎖場景"會被考慮到:假定任意數量的CPU、任意的中斷上下文和任務上下文群、運行所有正存在的加鎖場景的任意組合。在一個典型系統中,這意味着有成千上萬個獨立的場景。這就是為什么稱它為"加鎖正確性"驗證器,對於所有被觀察的規則來說,鎖驗證器用數學的確定性證明死鎖不可能發生,假定鎖驗證器實現本身正確,並且它內部的數據結構不會被其他內核子系統弄壞。

還有,驗證器的屬性"所有可能的場景"也使查找變得復雜,特別是多CPU、多上下文競爭比單個上下文規則復雜得多,

為了增加驗證器的效率,不是將每個鎖實例進行映射,而是映射每個鎖類型。例如:內核中所有的結構inode對象有inode->inotify_mutex,如果緩存了10000個inode,將會有10000個鎖對象。但->inotify_mutex是單個鎖類型,所有->inotify_mutex發生的加鎖活動都歸入單個鎖類型。

Lock-class


驗證器操作的基本對象是鎖類Lock-class,一個鎖類是一組鎖,邏輯上有同樣的加鎖規則,盡管鎖可能有多個實例。例如:在結構inode中的一個鎖是一個類,而每個節點有它自己的鎖類實例。

驗證器跟蹤鎖類的狀態和不同鎖類之間的依賴性。驗證器維護一個有關狀態和依賴性是否正確的滾動證據。

不像一個鎖實例,鎖類lock-class它本身從不消失:當lock-class注冊使用后,所有隨后鎖類的使用都會被附加到該lock-class上。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM