内容简介:Linux kernel调试
本文发自 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
结果:
[email protected]:~$ sudo cat /sys/module/kvm/sections/.text 0xffffffffa00e4000[email protected]:~$ 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[email protected]:~$ 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
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- iOS常用调试方法:断点调试
- 断点调试和日志调试之间的平衡点:函数计算调试之 Python 篇
- .NET高级调试系列-Windbg调试入门篇
- VisualStudio 通过外部调试方法快速调试库代码
- GDB 调试 Mysql 实战(二)GDB 调试打印
- 使用gdb调试工具上手调试php和swoole源码
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。