Linux kernel 調試打log流和斷點流
debug分為兩大流派。打log流和斷點流。
printk
對於小程序來說,打log太爽了,我可以花式打log,每行插一行log,一次加一行log。
在頭文件<linux/kernel.h>
中定義了 8 種可用的日誌級別字符串:KERN_EMERG,KERN_ALERT,KERN_CRIT,KERN_ERR,KERN_WARNING,KERN_NOTICE,KERN_INFO,KERN_INFO。共有 8 種優先級,用戶可以根據需要進行配置
KERN_EMERG /* system is unusable */
KERN_ALERT /* action must be taken immediately */
KERN_CRIT /* critical conditions */
KERN_ERR /* error conditions */
KERN_WARNING /* warning conditions */
KERN_NOTICE /* normal but significant condition */
KERN_INFO /* informational */
KERN_DEBUG /* debug-level messages */
打log:
printk(KERN_DEBUG "%d", HZ);
為了防止log太多撐爆ring buffer,啟動時使用參數 log_buf_len=104857600
來指定buffer大小,這裡為100M。
注意啟動參數不要帶 quite 和 splash。
QEMU + GDB
打log固然爽,然而對於kernel這種"程序",每次rebuild可以等半天。每次調試都要不斷加printk和rebuild顯然不實際,於是考慮使用GDB進行動態調試。由於是調試kernel,在不考慮kGDB等方法的情況下,最簡單的是跑一個虛擬機,然後GDB遠程連上去。QEMU為此提供了較好的支持。
編譯kernel
為了能夠動態調試kernel,在編譯前需要進行相應的配置,流程如下:
make mrproper
make x86_64_defconfig
cat <<EOF >.config-fragment
CONFIG_DEBUG_INFO=y
CONFIG_GDB_SCRIPTS=y
EOF
./scripts/kconfig/merge_config.sh .config .config-fragment
make -j $(nproc)
這裡首先清理掉編譯生存的文件,重新產生一個配置文件(.config),並和配置了DEBUG的.config-fragment進行合併,最後有多少個核就開多少路進行編譯。 注意.config很重要,先前我直接拷貝了/boot/config-xxx(當前kernel的配置)到.config,最後編譯後發現無論是break(通過修改內存產生debug異常來斷點)還是hbreak(通過debug寄存器來設置斷點)都無法斷點。只能通過 make x86_64_defconfig 生成一份新的配置,才能成功GDB。不知道因為哪個選項導致在gdb中無法斷點,苦查無果......
2017.4.11更新
今晚洗澡的時候回想起這個問題,打算花點時間解決之。手頭有兩份文件,一份是原kernel的配置文件 .config_old ,無法斷點;另一份是通過 make x86_64_defconfig 新生成的配置文件 .config_new ,可以斷點,但缺乏一些配置導致編譯的kernel無法帶起物理機。於是想通過對比的方式找出問題所在。
結果一看, .config_old 有8000行, .config_new 有4000行,用diff一比發現一大堆不同,如果研究每一個diff那今晚不用睡了。於是決定暴力地使用二分查找法:每次用 .config_old 替換掉 .config_new 一半的配置條目,看編譯後能否成功斷點。
經過若干次查找後發現問題出在 Performance monitoring 裡面,懷疑是以下幾行出了問題:
CONFIG_RELOCATABLE=y
CONFIG_RANDOMIZE_BASE=y
CONFIG_X86_NEED_RELOCS=y
CONFIG_PHYSICAL_ALIGN=0x1000000
CONFIG_RANDOMIZE_MEMORY=y
CONFIG_RANDOMIZE_MEMORY_PHYSICAL_PADDING=0xa
是否是因為ASLR的原因可能會導致gdb無法斷到正確的位置?Google一下發現這篇文章: https://www.phoronix.com/scan.php?page=news_item&px=Linux-4.8-ASLR-Kernel-Mem-Sects ,說是4.8引進了 CONFIG_RANDOMIZE_MEMORY 的新特性:
randomizing the virtual address space of kernel memory sections, the goal is to mitigate predictable memory locations.
於是利用 make menuconfig把 Processor type and features -> Randomize the kernel memory sections 關了,重新編譯後發現依然無法斷點。乾脆把其父級選項 Randomize the address of the kernel image (KASLR) 也關了,這時終於好了。此時查看 .config 發現少了以下兩行配置:
CONFIG_X86_NEED_RELOCS=y
CONFIG_RANDOMIZE_MEMORY=y
個人猜測是內存的重新佈局導致無法成功斷點。雖然開48核編譯kernel每次只需幾分鐘,但前後調試還是花費了兩個小時,蛋疼。
調試kernel
通過QEMU跑一個VM來加載kernel,同時啟動gdbserver提供調試信息。在宿主機中通過連接該server來進行調試。命令為:
sudo qemu-system-x86_64 -m 2048 \
-kernel /home/binss/work/GDB-Kernel/arch/x86/boot/bzImage \
-initrd ~/work/initrd.img-4.4.0-66-generic -gdb tcp::8889 \
-nographic -serial mon:stdio -append 'console=ttyS0' -S \
--enable-kvm
其中kernel用來指定kernel的鏡像文件,initrd用來指定initramfs,gdb用於啟動gdbserver並指定監聽的端口(也可以用-s來監聽1234端口), -nographic -serial mon:stdio -append 'console=ttyS0' 用來指將輸出重定向到當前終端,便於觀察kernel運行時的輸出。S表示在開始的時候停止直到通過gdb輸入c才繼續運行。
在運行過程中隨時可以通過Ctrl-A + C 來切換到qemu monitor進行操作(重複操作退出qemu monitor),如輸入quit可以結束當前VM。
啟動後,在另一個shell中cd到編譯kernel的目錄下,啟動gdb,依次執行以下命令:
add-auto-load-safe-path /home/binss/work/GDB-Kernel/
file /home/binss/work/GDB-Kernel/vmlinux
directory /home/binss/work/GDB-Kernel
target remote:8889
這裡從當前目錄的vmlinux(帶有符號信息的kernel,巨達幾百M)中加載符號表。也可以把以上命令保存到當前目錄( /home/binss/work/GDB-Kernel/ )的.gdbinit中,然後在 ~/.gdbinit 中添加:
add-auto-load-safe-path /home/binss/work/GDB-Kernel/.gdbinit
這樣在gdb啟動時就會自動執行以上命令。
然後我們就能夠通過函數名進行斷點了,比如斷在入口:
hbreak start_kernel
c
注意對於QEMU模擬的VM,可以使用break,但對於KVM模擬的VM,需要使用hbreak。
掛載磁盤
前面的指令拉起的VM會掛在initramfs,因為沒有指定要掛載的磁盤,可以通過hda掛載已有磁盤並配置root參數,從而成功進入某個虛擬機:
sudo qemu-system-x86_64 -m 2048 \
-kernel /home/binss/work/KVM-Learning/arch/x86/boot/bzImage \
-initrd ~/work/initrd.img-4.4.0-66-generic -hda myvm2.img \
-gdb tcp::8889 -nographic -serial mon:stdio \
-append 'root=/dev/sda1 console=ttyS0' \
--enable-kvm
當然為了加強魯棒性,建議使用UUID來指定root設備,UUID可以在進入系統後查詢/boot/grub/grub.cfg得到。
sudo qemu-system-x86_64 -m 2048 \
-kernel /home/binss/work/KVM-Learning/arch/x86/boot/bzImage \
-initrd ~/work/initrd.img-4.4.0-66-generic -hda myvm2.img \
-gdb tcp::8889 -nographic -serial mon:stdio \
-append 'root=UUID=02cf5ccd-f57f-4b25-b923-add3adb5d6c3 console=ttyS0' \
--enable-kvm
調試模塊
對於內核模塊,我們同樣能夠通過虛擬機的方式對其進行GDB。首先需要確保模塊已被加載,對於自行編譯的模塊,可以通過scp等方式將文件發到guest中,通過insmod進行安裝。注意需要保證是在當前kerenl的目錄下編譯模塊,確保它們的版本相同。
然後需要定位模塊地址(可能沒有data和bss):
sudo cat /sys/module/kvm/sections/.text
sudo cat /sys/module/kvm/sections/.data
sudo cat /sys/module/kvm/sections/.bss
結果:
[email protected]
:~$ sudo cat /sys/module/kvm/sections/.text 0xffffffffa00e4000[emailprotected]
:~$ sudo cat /sys/module/kvm/sections/.data 0xffffffffa0143000 [email protected]
:~$ sudo cat /sys/module/kvm/sections/.bss 0xffffffffa0152140
然後在host的GDB中用這些地址加載模塊:
(gdb) add-symbol-file ~/work/GDB-Kernel/arch/x86/kvm/kvm.ko 0xffffffffa00e4000 -s .data 0xffffffffa0143000 -s .bss 0xffffffffa0152140
add symbol table from file "/home/binss/work/GDB-Kernel/arch/x86/kvm/kvm.ko" at
.text_addr = 0xffffffffa00e4000
.data_addr = 0xffffffffa0143000
.bss_addr = 0xffffffffa0152140
(y or n) y
Reading symbols from /home/binss/work/GDB-Kernel/arch/x86/kvm/kvm.ko...done.
用同樣的方式加載kvm-intel的符號信息:
sudo cat /sys/module/kvm_intel/sections/.text
sudo cat /sys/module/kvm_intel/sections/.data
sudo cat /sys/module/kvm_intel/sections/.bss
結果:
[email protected]
:~$ sudo cat /sys/module/kvm_intel/sections/.text 0xffffffffa01a3000[emailprotected]
:~$ sudo cat /sys/module/kvm_intel/sections/.data 0xffffffffa01cb000 [email protected]
:~$ sudo cat /sys/module/kvm_intel/sections/.bss 0xffffffffa01cbec0
加載:
(gdb) add-symbol-file ~/work/GDB-Kernel/arch/x86/kvm/kvm-intel.ko 0xffffffffa01a3000 -s .data 0xffffffffa01cb000 -s .bss 0xffffffffa01cbec0
add symbol table from file "/home/binss/work/GDB-Kernel/arch/x86/kvm/kvm-intel.ko" at
.text_addr = 0xffffffffa01a3000
.data_addr = 0xffffffffa01cb000
.bss_addr = 0xffffffffa01cbec0
(y or n) y
Reading symbols from /home/binss/work/GDB-Kernel/arch/x86/kvm/kvm-intel.ko...done.
然後打斷點:
(gdb) hb vcpu_enter_guest
Hardware assisted breakpoint 1 at 0xffffffffa0103d37: file ./arch/x86/include/asm/processor.h, line 482.
(gdb) hb vmx_vcpu_run
Hardware assisted breakpoint 2 at 0xffffffffa01b30e0: file arch/x86/kvm/vmx.c, line 8798.
(gdb) c
Continuing.
然後就可以進行調試了。在VM中運行:
qemu-img create -f qcow2 mytest.img 5G
sudo qemu-system-x86_64 -cpu host -hda mytest.img \
-boot c \
-nographic \
-serial mon:stdio \
-vnc :1 \
-smp 1 \
-m 2048 \
--enable-kvm
回到gdb:
Thread 2 hit Breakpoint 1, vcpu_run (vcpu=<optimized out>)
at /home/binss/work/GDB-Kernel/arch/x86/kvm/x86.c:6788
6788 r = vcpu_enter_guest(vcpu);
(gdb) p vcpu
$1 = <optimized out>
(gdb) c
Continuing.
Thread 2 hit Breakpoint 2, vmx_vcpu_run (vcpu=0xffff8800778a0000)
at /home/binss/work/GDB-Kernel/arch/x86/kvm/vmx.c:8798
8798 {
(gdb) p vcpu
$5 = (struct kvm_vcpu *) 0xffff8800778a0000
(gdb) n
8799 struct vcpu_vmx *vmx = to_vmx(vcpu);
(gdb) n
8803 if (unlikely(!cpu_has_virtual_nmis() && vmx->soft_vnmi_blocked))
(gdb) p vmx
$6 = (struct vcpu_vmx *) 0xffff8800778a0000
缺陷在於編譯kernel時強制採用了 -O2 進行編譯,導致一些值被優化後顯示為<optimized out>
,可以考慮反彙編
。
參考
http://stackoverflow.com/questions/11408041/how-to-debug-the-linux-kernel-with-gdb-and-qemu https://wiki.ubuntu.com/Kernel/KernelDebuggingTricks http://www.elinux.org/Debugging_The_Linux_Kernel_Using_Gdb https://www.phoronix.com/scan.php?page=news_item&px=Linux-4.8-ASLR-Kernel-Mem-Sects