极客时间已完结课程限时免费阅读

61 | 搭建操作系统实验环境(下):授人以鱼不如授人以渔

61 | 搭建操作系统实验环境(下):授人以鱼不如授人以渔-极客时间

61 | 搭建操作系统实验环境(下):授人以鱼不如授人以渔

讲述:刘超

时长10:24大小9.54M

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

了解 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 <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <linux/kernel.h>
5 #include <sys/syscall.h>
6 #include <string.h>
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 不认。
<domain type='qemu' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
<name>ubuntutest</name>
<uuid>0f0806ab-531d-6134-5def-c5b4955292aa</uuid>
<memory unit='KiB'>8388608</memory>
<currentMemory unit='KiB'>8388608</currentMemory>
<vcpu placement='static'>8</vcpu>
<os>
<type arch='x86_64' machine='pc-i440fx-trusty'>hvm</type>
<boot dev='hd'/>
</os>
<clock offset='utc'/>
<on_poweroff>destroy</on_poweroff>
<on_reboot>restart</on_reboot>
<on_crash>restart</on_crash>
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2'/>
<source file='/mnt/vdc/ubuntutest.img'/>
<backingStore/>
<target dev='vda' bus='virtio'/>
<alias name='virtio-disk0'/>
<address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/>
</disk>
......
<interface type='bridge'>
<mac address='fa:16:3e:6e:89:ce'/>
<source bridge='br0'/>
<target dev='tap1'/>
<model type='virtio'/>
<alias name='net0'/>
<address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>
</interface>
......
</devices>
<qemu:commandline>
<qemu:arg value='-s'/>
</qemu:commandline>
</domain>
另外,为了远程 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=<optimized out>) 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 函数的。这一点和我们在系统调用那一节分析的过程是一模一样的。
我们可以通过执行 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 一下文件打开的过程。
欢迎留言和我分享你的疑惑和见解,也欢迎你收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习、进步。
分享给需要的人,Ta购买本课程,你将得20
生成海报并分享

赞 7

提建议

上一篇
60 | 搭建操作系统实验环境(上):授人以鱼不如授人以渔
下一篇
62 | 知识串讲:用一个创业故事串起操作系统原理(一)
 写留言

精选留言(7)

  • 魏颖琪
    2019-09-20
    终于在virtualbox中做完了这两课的实验。请问老师,云服务是否很少用 vbox,基本都用qemu?
    6
  • yanger
    2020-02-01
    缺省很多变量被编译优化了,有O0编译kernel的方法吗
    共 1 条评论
    3
  • LDxy
    2019-08-17
    打断点的指令b后面是不是既可以跟行号也可以跟函数名?

    作者回复: 是的

    2
  • 邹德虎
    2022-05-23
    如果没有虚拟机而是实体的服务器,怎么调试内核呢?
  • 莫名
    2020-09-24
    编译、调试较新版本的内核稍微有些区别,大体流程基本相同。
  • kkxue
    2019-08-16
    赞👍
  • 许童童
    2019-08-16
    跟着老师一起精进。