# 61 | 搭建操作系统实验环境(下):授人以鱼不如授人以渔 上一节我们做了一个实验,添加了一个系统调用,并且编译了内核。这一节,我们来尝试调试内核。这样,我们就可以一步一步来看,内核的代码逻辑执行到哪一步了,对应的变量值是什么。 ## 了解gdb 在Linux下面,调试程序使用一个叫作gdb的工具。通过这个工具,我们可以逐行运行程序。 例如,上一节我们写的syscall.c这个程序,我们就可以通过下面的命令编译。 ``` gcc -g syscall.c ``` 其中,参数-g的意思就是在编译好的二进制程序中,加入debug所需的信息。 接下来,我们安装一下gdb。 ``` apt-get install gdb ``` 然后,我们就可以来调试这个程序了。 ``` ~/syscall# gdb ./a.out GNU gdb (Ubuntu 8.1-0ubuntu3.1) 8.1.0.20180409-git ...... Reading symbols from ./a.out...done. (gdb) l 1 #include 2 #include 3 #include 4 #include 5 #include 6 #include 7 8 int main () 9 { 10 char * words = "I am liuchao from user mode."; (gdb) b 10 Breakpoint 1 at 0x6e2: file syscall.c, line 10. (gdb) r Starting program: /root/syscall/a.out Breakpoint 1, main () at syscall.c:10 10 char * words = "I am liuchao from user mode."; (gdb) n 12 ret = syscall(333, words, strlen(words)+1); (gdb) p words $1 = 0x5555555547c4 "I am liuchao from user mode." (gdb) s __strlen_sse2 () at ../sysdeps/x86_64/multiarch/../strlen.S:79 (gdb) bt #0 __strlen_sse2 () at ../sysdeps/x86_64/multiarch/../strlen.S:79 #1 0x00005555555546f9 in main () at syscall.c:12 (gdb) c Continuing. return 63 from kernel mode. [Inferior 1 (process 1774) exited normally] (gdb) q ``` 在上面的例子中,我们只要掌握简单的几个gdb的命令就可以了。 * l,即list,用于显示多行源代码。 * b,即break,用于设置断点。 * r,即run,用于开始运行程序。 * n,即next,用于执行下一条语句。如果该语句为函数调用,则不会进入函数内部执行。 * p,即print,用于打印内部变量值。 * s,即step,用于执行下一条语句。如果该语句为函数调用,则进入函数,执行其中的第一条语句。 * c,即continue,用于继续程序的运行,直到遇到下一个断点。 * bt,即backtrace,用于查看函数调用信息。 * q,即quit,用于退出gdb环境。 ## Debug kernel 看了debug一个进程还是简单的,接下来,我们来试着debug整个kernel。 第一步,要想kernel能够被debug,需要像上面编译程序一样,将debug所需信息也放入二进制文件里面去。这个我们在编译内核的时候已经设置过了,也就是把“CONFIG\_DEBUG\_INFO”和“CONFIG\_FRAME\_POINTER”两个变量设置为yes。 第二步,就是安装gdb。kernel运行在qemu虚拟机里面,gdb运行在宿主机上,所以我们应该在宿主机上进行安装。 第三步,找到gdb要运行的那个内核的二进制文件。这个文件在哪里呢?根据grub里面的配置,它应该在/boot/vmlinuz-4.15.18这里。 另外,为了方便在debug的过程中查看源代码,我们可以将/usr/src/linux-source-4.15.0整个目录,都拷贝到宿主机上来。因为内核一旦进入debug模式,就不能运行了。 ``` scp -r popsuper@192.168.57.100:/usr/src/linux-source-4.15.0 ./ ``` 在/usr/src/linux-source-4.15.0这个目录下面,vmlinux文件也是内核的二进制文件。 第四步,修改qemu的启动参数和qemu里面虚拟机的启动参数,从而使得gdb可以远程attach到qemu里面的内核上。 我们知道,gdb debug一个进程的时候,gdb会监控进程的运行,使得进程一行一行地执行二进制文件。如果像syscall.c的二进制文件a.out一样,就在本地,gdb可以通过attach到这个进程上,作为这个进程的父进程,来监控它的运行。 但是,gdb debug一个内核的时候,因为内核在qemu虚拟机里面,所以我们无法监控本地进程,而要通过qemu来监控qemu里面的内核,这就要借助qemu的机制。 qemu有个参数-s,它代表参数-gdb tcp::1234,意思是qemu监听1234端口,gdb可以attach到这个端口上来,debug qemu里面的内核。 为了完成这一点,我们需要修改ubuntutest这个虚拟机的定义文件。 ``` virsh edit ubuntutest ``` 在这里,我们能将虚拟机的定义文件修改成下面的样子,其中主要改了两项: * 在domain的最后加上了qemu:commandline,里面指定了参数-s; * 在domain中添加xmlns:qemu。没有这个XML的namespace,qemu:commandline这个参数libvirt不认。 ``` ubuntutest 0f0806ab-531d-6134-5def-c5b4955292aa 8388608 8388608 8 hvm destroy restart restart /usr/bin/qemu-system-x86_64
......
...... ``` 另外,为了远程debug成功,我们还需要修改qemu里面的虚拟机的grub和menu.list,在内核命令行中添加nokaslr,来关闭KASLR。KASLR会使得内核地址空间布局随机化,从而会造成我们打的断点不起作用。 对于grub.conf,修改如下: ``` submenu 'Advanced options for Ubuntu' $menuentry_id_option 'gnulinux-advanced-470f3a42-7a97-4b9d-aaa0-26deb3d234f9' { menuentry 'Ubuntu, with Linux 4.15.18' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-4.15.18-advanced-470f3a42-7a97-4b9d-aaa0-26deb3d234f9' { recordfail load_video gfxmode $linux_gfx_mode insmod gzio if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi insmod part_gpt insmod ext2 if [ x$feature_platform_search_hint = xy ]; then search --no-floppy --fs-uuid --set=root 470f3a42-7a97-4b9d-aaa0-26deb3d234f9 else search --no-floppy --fs-uuid --set=root 470f3a42-7a97-4b9d-aaa0-26deb3d234f9 fi echo 'Loading Linux 4.15.18 ...' linux /boot/vmlinuz-4.15.18 root=UUID=470f3a42-7a97-4b9d-aaa0-26deb3d234f9 ro nokaslr console=ttyS0 maybe-ubiquity echo 'Loading initial ramdisk ...' initrd /boot/initrd.img-4.15.18 } ``` 对于menu.list,修改如下: ``` title Ubuntu 18.04.2 LTS, kernel 4.15.18 root (hd0) kernel /boot/vmlinuz-4.15.18 root=/dev/hda1 ro nokaslr console=hvc0 console=ttyS0 initrd /boot/initrd.img-4.15.18 ``` 修改完毕后,我们需要在虚拟机里面shutdown -h now,来关闭虚拟机。注意不要reboot,因为虚拟机里面运行reboot,我们改过的那个XML会不起作用。 当我们在宿主机上发现虚拟机关机之后,就可以通过virsh start ubuntutest启动虚拟机,这个时候我们添加的参数-s才起作用。 第五步,使用gdb运行内核的二进制文件,执行gdb vmlinux。 ``` /mnt/vdc/linux-source-4.15.0# gdb vmlinux GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1 ...... To enable execution of this file add add-auto-load-safe-path /mnt/vdc/linux-source-4.15.0/vmlinux-gdb.py ...... (gdb) b sys_sayhelloworld Breakpoint 1 at 0xffffffff8109e2f0: file kernel/sys.c, line 192. (gdb) target remote :1234 Remote debugging using :1234 native_safe_halt () at ./arch/x86/include/asm/irqflags.h:61 61 } (gdb) c Continuing. [Switching to Thread 2] Thread 2 hit Breakpoint 1, sys_sayhelloworld (words=0x563cbfa907c4 "I am liuchao from user mode.", count=29) at kernel/sys.c:192 192 { (gdb) bt #0 sys_sayhelloworld (words=0x55b2811537c4 "I am liuchao from user mode.", count=29) at kernel/sys.c:192 #1 0xffffffff810039f7 in do_syscall_64 (regs=0xffffc9000133bf58) at arch/x86/entry/common.c:290 #2 0xffffffff81a00081 in entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:237 (gdb) n 195 if(count >= 1024){ (gdb) n 198 copy_from_user(buffer, words, count); (gdb) n 199 ret=printk("User Mode says %s to the Kernel Mode!", buffer); (gdb) p buffer $1 = "I am liuchao from user mode.\000\177\000\000\... (gdb) n 200 return ret; (gdb) p ret $2 = 63 (gdb) c (gdb) n do_syscall_64 (regs=0xffffc9000133bf58) at arch/x86/entry/common.c:295 295 syscall_return_slowpath(regs); (gdb) s syscall_return_slowpath (regs=) at arch/x86/entry/common.c:295 (gdb) n 268 prepare_exit_to_usermode(regs); (gdb) n do_syscall_64 (regs=0xffffc9000133bf58) at arch/x86/entry/common.c:296 296 } (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:246 246 movq RCX(%rsp), %rcx ...... (gdb) n entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:330 330 USERGS_SYSRET64 ``` 我们先设置一个断点在我们自己写的系统调用上b sys\_sayhelloworld,通过执行target remote :1234,来attach到qemu上,然后,执行c,也即continue运行内核。这个时候内核始终在Continuing的状态,也即持续在运行中,这个时候我们可以远程登录到qemu里的虚拟机上,执行各种命令。 如果我们在虚拟机里面运行syscall.c编译好的a.out,这个时候肯定会调用到内核。内核肯定会经过系统调用的过程,到达sys\_sayhelloworld这个函数,这就碰到了我们设置的那个断点。 如果执行bt,我们能看到,这个系统调用是从entry\_64.S里面的entry\_SYSCALL\_64 ()函数,调用到do\_syscall\_64函数,再调用到sys\_sayhelloworld函数的。这一点和我们在[系统调用](https://time.geekbang.org/column/article/90394)那一节分析的过程是一模一样的。 我们可以通过执行next命令,来看sys\_sayhelloworld一步一步是怎么执行的,通过p buffer查看buffer里面的内容。在这个过程中,由于内核是逐行运行的,因而我们在虚拟机里面的命令行是卡死的状态。 当我们不断地next,直到执行完毕sys\_sayhelloworld的时候,会看到,do\_syscall\_64会调用syscall\_return\_slowpath。它会调用prepare\_exit\_to\_usermode,然后会回到entry\_SYSCALL\_64,然后对于寄存器进行操作,最后调用指令USERGS\_SYSRET64回到用户态。这个返回的过程和系统调用那一节也一模一样。 看,通过debug我们能够跟踪系统调用的整个过程。你可以将我们这一门课里面学的所有的过程都debug一下,看看变量的值,从而对于内核的工作机制有更加深入的了解。 ## 总结时刻 在这个课程里面,我们写过一些程序,为了保证程序能够顺利运行,我一般会将代码完整地放到文本中,让你拷贝下来就能编译和运行。如果你运行的时候发现有问题,或者想了解一步一步运行的细节,这一节介绍的gdb是一个很好的工具。 这一节你尤其应该掌握的是,如何通过宿主机上的gdb来debug虚拟机里面的内核。这一点非常重要,会了这个,你就能够返回去,挨个研究每一章每一节的内核数据结构和运行逻辑了。 在这门课中,进程管理、内存管理、文件管理、设备管理网络管理,我们都介绍了从系统调用到底层的整个逻辑。如果你对我前面的代码解析还比较困惑,你可以尝试着去debug这些过程,只要把断点打在系统调用的入口位置就可以了。 从此,开启你的内核debug之旅吧! ## 课堂练习 这里给你留一道题目,你可以试着debug一下文件打开的过程。 欢迎留言和我分享你的疑惑和见解,也欢迎你收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习、进步。 ![](https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg)