<audio id="audio" title="61 | 搭建操作系统实验环境（下）：授人以鱼不如授人以渔" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f3/dd/f32e425858449891e10cb7ed546227dd.mp3"></audio>

上一节我们做了一个实验，添加了一个系统调用，并且编译了内核。这一节，我们来尝试调试内核。这样，我们就可以一步一步来看，内核的代码逻辑执行到哪一步了，对应的变量值是什么。

## 了解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 &lt;stdio.h&gt;
2       #include &lt;stdlib.h&gt;
3       #include &lt;unistd.h&gt;
4       #include &lt;linux/kernel.h&gt;
5       #include &lt;sys/syscall.h&gt;
6       #include &lt;string.h&gt;
7
8       int main ()
9       {
10        char * words = &quot;I am liuchao from user mode.&quot;;
(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 = &quot;I am liuchao from user mode.&quot;;
(gdb) n
12        ret = syscall(333, words, strlen(words)+1);
(gdb) p words
$1 = 0x5555555547c4 &quot;I am liuchao from user mode.&quot;
(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不认。

```
&lt;domain type='qemu' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'&gt;
  &lt;name&gt;ubuntutest&lt;/name&gt;
  &lt;uuid&gt;0f0806ab-531d-6134-5def-c5b4955292aa&lt;/uuid&gt;
  &lt;memory unit='KiB'&gt;8388608&lt;/memory&gt;
  &lt;currentMemory unit='KiB'&gt;8388608&lt;/currentMemory&gt;
  &lt;vcpu placement='static'&gt;8&lt;/vcpu&gt;
  &lt;os&gt;
    &lt;type arch='x86_64' machine='pc-i440fx-trusty'&gt;hvm&lt;/type&gt;
    &lt;boot dev='hd'/&gt;
  &lt;/os&gt;
  &lt;clock offset='utc'/&gt;
  &lt;on_poweroff&gt;destroy&lt;/on_poweroff&gt;
  &lt;on_reboot&gt;restart&lt;/on_reboot&gt;
  &lt;on_crash&gt;restart&lt;/on_crash&gt;
  &lt;devices&gt;
    &lt;emulator&gt;/usr/bin/qemu-system-x86_64&lt;/emulator&gt;
    &lt;disk type='file' device='disk'&gt;
      &lt;driver name='qemu' type='qcow2'/&gt;
      &lt;source file='/mnt/vdc/ubuntutest.img'/&gt;
      &lt;backingStore/&gt;
      &lt;target dev='vda' bus='virtio'/&gt;
      &lt;alias name='virtio-disk0'/&gt;
      &lt;address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/&gt;
    &lt;/disk&gt;
......
    &lt;interface type='bridge'&gt;
      &lt;mac address='fa:16:3e:6e:89:ce'/&gt;
      &lt;source bridge='br0'/&gt;
      &lt;target dev='tap1'/&gt;
      &lt;model type='virtio'/&gt;
      &lt;alias name='net0'/&gt;
      &lt;address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/&gt;
    &lt;/interface&gt;
......
  &lt;/devices&gt;
  &lt;qemu:commandline&gt;
    &lt;qemu:arg value='-s'/&gt;
  &lt;/qemu:commandline&gt;
&lt;/domain&gt;

```

另外，为了远程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 &quot;I am liuchao from user mode.&quot;, count=29) at kernel/sys.c:192
192     {
(gdb) bt
#0  sys_sayhelloworld (words=0x55b2811537c4 &quot;I am liuchao from user mode.&quot;, 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 &gt;= 1024){
(gdb) n
198             copy_from_user(buffer, words, count);
(gdb) n
199             ret=printk(&quot;User Mode says %s to the Kernel Mode!&quot;, buffer);
(gdb) p buffer
$1 = &quot;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=&lt;optimized out&gt;) 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一下文件打开的过程。

欢迎留言和我分享你的疑惑和见解，也欢迎你收藏本节内容，反复研读。你也可以把今天的内容分享给你的朋友，和他一起学习、进步。

<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">
