本文发自 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