内容简介: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源码
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web Security Testing Cookbook
Paco Hope、Ben Walther / O'Reilly Media / 2008-10-24 / USD 39.99
Among the tests you perform on web applications, security testing is perhaps the most important, yet it's often the most neglected. The recipes in the Web Security Testing Cookbook demonstrate how dev......一起来看看 《Web Security Testing Cookbook》 这本书的介绍吧!