原文地址:Linux内核0-使用QEMU和GDB调试Linux内核
(文章大部分转载于:https://consen.github.io/2018/01/17/debug-linux-kernel-with-qemu-and-gdb/)
排查Linux内核Bug,研究内核机制,除了查看资料阅读源码,还可通过调试器,动态分析内核执行流程。
QEMU模拟器原生支持GDB调试器,这样可以很方便地使用GDB的强大功能对操作系统进行调试,如设置断点;单步执行;查看调用栈、查看寄存器、查看内存、查看变量;修改变量改变执行流程等。
编译调试版内核
对内核进行调试需要解析符号信息,所以得编译一个调试版内核。
1 2 3 | $ cd linux-4.14 $ make menuconfig $ make -j 20 |
这里需要开启内核参数
1 2 3 4 5 | Kernel hacking ---> [*] Kernel debugging Compile-time checks and compiler options ---> [*] Compile the kernel with debug info [*] Provide GDB scripts for kernel debugging |
构建initramfs根文件系统
Linux系统启动阶段,boot loader加载完内核文件vmlinuz后,内核紧接着需要挂载磁盘根文件系统,但如果此时内核没有相应驱动,无法识别磁盘,就需要先加载驱动,而驱动又位于
这里借助BusyBox构建极简initramfs,提供基本的用户态可执行程序。
编译BusyBox,配置
1 2 | $ cd busybox-1.28.0 $ make menuconfig |
选择配置项:
1 2 | Settings ---> [*] Build static binary (no shared libs) |
执行编译、安装:
1 2 | $ make -j 20 $ make install |
会安装在_install目录:
1 2 | $ ls _install bin linuxrc sbin usr |
创建initramfs,其中包含BusyBox可执行程序、必要的设备文件、启动脚本
1 2 3 4 5 6 7 8 9 10 | $ mkdir initramfs $ cd initramfs $ cp ../_install/* -rf ./ $ mkdir dev proc sys $ sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/ $ rm linuxrc $ vim init $ chmod a+x init $ ls $ bin dev init proc sbin sys usr |
init文件内容:
1 2 3 4 5 | #!/bin/busybox sh mount -t proc none /proc mount -t sysfs none /sys exec /sbin/init |
打包initramfs:
1 | $ find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz |
调试
1 2 | $ cd busybox-1.28.0 $ qemu-system-i386 -s -kernel ./linux-4.4.203/arch/i386/boot/bzImage -initrd ./initramfs.cpio.gz -nographic -append "console=ttyS0" |
-s 是-gdb tcp::1234 缩写,监听1234端口,在GDB中可以通过target remote localhost:1234 连接;-kernel 指定编译好的调试版内核;-initrd 指定制作的initramfs;-nographic 取消图形输出窗口,使QEMU成简单的命令行程序;-append "console=ttyS0" 将输出重定向到console,将会显示在标准输出stdio。
启动后的根目录, 就是initramfs中包含的内容:
1 2 | / # ls bin dev init proc root sbin sys usr |
由于系统自带的GDB版本为7.2,内核辅助脚本无法使用,重新编译了一个新版GDB。我的系统比较新,所以gdb版本是7.11,所以不需要重新编译。
1 2 3 4 | $ cd gdb-7.9.1 $ ./configure --with-python=$(which python2.7) $ make -j 20 $ sudo make install |
启动GDB:
1 2 3 | $ cd linux-4.14 $ /usr/local/bin/gdb vmlinux (gdb) target remote localhost:1234 |
使用内核提供的GDB辅助调试功能:
1 2 3 4 5 6 7 8 9 10 | (gdb) apropos lx function lx_current -- Return current task function lx_module -- Find module by name and return the module variable function lx_per_cpu -- Return per-cpu variable function lx_task_by_pid -- Find Linux task by PID and return the task_struct variable ...(此处省略若干行) lx-symbols -- (Re-)load symbols of Linux kernel and currently loaded modules lx-version -- Report the Linux Version of the current kernel (gdb) lx-cmdline console=ttyS0 |
在函数
1 2 3 4 5 6 7 8 9 10 11 12 13 | (gdb) b cmdline_proc_show Breakpoint 1 at 0xffffffff81298d99: file fs/proc/cmdline.c, line 9. (gdb) c Continuing. Breakpoint 1, cmdline_proc_show (m=0xffff880006695000, v=0x1 <irq_stack_union+1>) at fs/proc/cmdline.c:9 9 seq_printf(m, "%s\n", saved_command_line); (gdb) bt #0 cmdline_proc_show (m=0xffff880006695000, v=0x1 <irq_stack_union+1>) at fs/proc/cmdline.c:9 #1 0xffffffff81247439 in seq_read (file=0xffff880006058b00, buf=<optimized out>, size=<optimized out>, ppos=<optimized out>) at fs/seq_file.c:234 ......(此处省略) (gdb) p saved_command_line $2 = 0xffff880007e68980 "console=ttyS0" |
获取当前进程
《深入理解Linux内核》第三版第三章–进程,讲到内核采用了一种精妙的设计来获取当前进程。
Linux把跟一个进程相关的
1 2 3 | movl $0xffffe000, %ecx /* 内核栈大小为8K,屏蔽低13位有效位。 andl $esp, %ecx movl (%ecx), p |
指令运行后,p就获得当前CPU上运行进程的描述符指针。
然而在调试器中调了下,发现这种机制早已经被废弃掉了。
而且进程的
1 2 | (gdb) p $lx_current().thread_info $5 = {flags = 2147483648} |
thread_info这个变量好像没有了,打印结果显示没有这个成员
这样做是从安全角度考虑的,一方面可以防止esp寄存器泄露后进而泄露进程描述符指针,二是防止内核栈溢出覆盖
Linux内核从2.6引入了Per-CPU变量,获取当前指针也是通过Per-CPU变量实现的。
1 2 3 4 | (gdb) p $lx_current().pid $50 = 77 (gdb) p $lx_per_cpu("current_task").pid $52 = 77 |
补充
在gdb中输入命令
1 | (gdb) apropos lx |
从stackoverflow网站上找到一篇文章gdb-lx-symbols-undefined-command,里边提到:
1 2 3 | gdb -ex add-auto-load-safe-path /path/to/linux/kernel/source/root Now the GDB scripts are automatically loaded, and lx-symbols is available. |
但是,按照上面进行操作后,进入gdb调试画面后,提示:
1 2 3 4 5 6 | To enable execution of this file add add-auto-load-safe-path /home/qemu2/qemu/linux-4.4.203/scripts/gdb/vmlinux-gdb.py line to your configuration file "/home/qemu2/.gdbinit". To completely disable this security protection add set auto-load safe-path / line to your configuration file "/home/qemu2/.gdbinit". |
上面的意思是,为了能够使能
1 | add-auto-load-safe-path /home/qemu2/qemu/linux-4.4.203/scripts/gdb/vmlinux-gdb.py |
这行代码到我的配置文件
1 | set auto-load safe-path / |
这行代码添加到配置文件
于是启动内核代码,然后在另一个命令行窗口中执行gdb调试,就像上面的操作一样,显示:
1 2 3 4 5 6 7 8 9 10 | function lx_current -- Return current task function lx_module -- Find module by name and return the module variable function lx_per_cpu -- Return per-cpu variable function lx_task_by_pid -- Find Linux task by PID and return the task_struct variable function lx_thread_info -- Calculate Linux thread_info from task variable lx-dmesg -- Print Linux kernel log buffer lx-list-check -- Verify a list consistency lx-lsmod -- List currently loaded modules lx-ps -- Dump Linux tasks lx-symbols -- (Re-)load symbols of Linux kernel and currently loaded modules |
至此,终于可以安心调试内核了。
参考:
- Tips for Linux Kernel Development
- How to Build A Custom Linux Kernel For Qemu
- Linux Kernel System Debugging
- Debugging kernel and modules via gdb
- BusyBox simplifies embedded Linux systems
- Custom Initramfs
- Per-CPU variables
- Linux kernel debugging with GDB: getting a task running on a CPU
- gdb-kernel-debugging