本文发自 http://www.binss.me/blog/how-to-debug-linux-kernel/,转载请注明出处。

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

结果:

binss@ubuntu:~$ sudo cat /sys/module/kvm/sections/.text
0xffffffffa00e4000
binss@ubuntu:~$ sudo cat /sys/module/kvm/sections/.data
0xffffffffa0143000
binss@ubuntu:~$ 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

结果:

binss@ubuntu:~$ sudo cat /sys/module/kvm_intel/sections/.text
0xffffffffa01a3000
binss@ubuntu:~$ sudo cat /sys/module/kvm_intel/sections/.data
0xffffffffa01cb000
binss@ubuntu:~$ 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