MIT 6.S081 xv6 调试指北
前言: xv6 调试折磨了我整整一上午,在询问老师 + 谷歌之后,稍有心得,记录之
运行 qemu 模拟器
在 64 位(x86) 的电脑上模拟 RISC-V 架构的 CPU。在计组实验中,通过硬件电路设计 CPU,而 qemu 通过 C 语言来模拟 CPU 的取指、译码和执行操作。
在 xv6-labs-2020
目录下输入 make qemu
并回车,随后出现单独窗口。qemu 的虚拟 BIOS 将从 xv6.img
文件中含有的虚拟硬盘映像加载引导文件,随后启动 xv6 内核程序。
接下来可以在 shell 界面执行指令。
[root@localhost xv6-labs-2020]# make qemu
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
xv6 kernel is booting
hart 2 starting
hart 1 starting
init: starting sh
$ ls
. 1 1 1024
.. 1 1 1024
README 2 2 2059
xargstest.sh 2 3 93
cat 2 4 24208
......
primes 2 23 26584
find 2 24 26008
console 3 25 0
$ cat README
xv6 is a re-implementation of Dennis Ritchie's and Ken Thompson's Unix
Version 6 (v6). xv6 loosely follows the structure and style of v6,
......
You will need a RISC-V "newlib" tool chain from
https://github.com/riscv/riscv-gnu-toolchain, and qemu compiled for
riscv64-softmmu. Once they are installed, and in your shell
search path, you can run "make qemu".
$
qemu 下远程调试 xv6
可以通过 gdb 程序的远程调试功能和 qemu 的远程gdb调试接口来对程序进行调试。
在内核开发中经常遇到远程调试技术。在远程调试中,调试器(gdb) 在主机 A
上,而被调试的程序在主机 B
上运行。通常情况下主机 A
具有完善的开发环境的主机中运行,而被调试的程序可能直接在硬件设备上运行,且不具备完整的环境来正常的运行调试器。因此在主机 B
上仅运行一个简单的 debug 程序(stub),仅负责设置断点、启动、停止等基本操作,而在主机 A
上由 gdb 执行更为复杂的操作。
在本例中调试器 gdb 在 open-euler 虚拟机下运行,而待调试的 xv6 程序在 qemu 模拟的 cpu 上运行,两者通过 localhost 端口进行通信。
同时,也可以使用虚拟机 Bochs 来执行 xv6 程序,虽然程序速度会变慢,但该虚拟机具有内置的调试器(功能不如 gdb 完善)而不需要远程调试功能。
1、开启 gdb 状态下的 qemu
执行下列指令可以在 qemu 下运行 xv6 程序,同时支持远程调试
[root@localhost xv6-labs-2020]# make qemu-gdb
*** Now run 'gdb' in another window.
qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 -S -gdb tcp::25000
此时虚拟机启动,并在执行第一条指令之前停止运行,等待远程 gdb 的连接,可以发现,监听的端口为 25000
。
2、开启 gdb
接着使 gdb 连接到 qemu 的 stub。打开新的终端窗口,切换至相同的 xv6 目录,输入
[root@localhost cs]# cd xv6-labs-2020/
[root@localhost xv6-labs-2020]# riscv64-unknown-elf-gdb kernel/kernel
GNU gdb (GDB) 10.1
Copyright (C) 2020 Free Software Foundation, Inc.
.................................
... 乱七八糟的输出信息,此处省略 ...
.................................
Reading symbols from kernel/kernel...
warning: File "/home/cs/xv6-labs-2020/.gdbinit" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
To enable execution of this file add
add-auto-load-safe-path /home/cs/xv6-labs-2020/.gdbinit
line to your configuration file "/root/.gdbinit".
To completely disable this security protection add
set auto-load safe-path /
line to your configuration file "/root/.gdbinit".
For more information about this security protection see the
"Auto-loading safe path" section in the GDB manual. E.g., run from the shell:
info "(gdb)Auto-loading safe path"
(gdb)
注意此时虽然执行了 gdb xxx
,但是并没有开始调试程序。因为此时 gdb 为待执行的程序提供的是用户模式下的 Linux 进程,而内核程序 kernel
希望在原生的 x86 硬件环境中以特权模式运行。
虽然内核程序不在此时的 gdb 环境下运行,但 gdb 仍需要读入此时内核的 ELF 程序映像,使得 gdb 可以提取到调试过程中所需信息,比如 C 函数的地址、符号、以及源码的行号。以上过程发生在屏幕显示
Reading symbols from kernel/kernel...
的过程中。
3、使用 gdb 远程连接 qemu
此时输入如下指令
(gdb) target remote localhost:25000
Remote debugging using localhost:25000
0x0000000000001000 in ?? ()
(gdb)
与 qemu 进行连接,端口号(localhost)需要与之前的监听端口相同。此时返回 xv6 当前地址。
4、开始调试
此时我们在 exec
函数部分设置断点,接着运行虚拟机,直到虚拟机运行至断点处停止
(gdb) b exec
Breakpoint 1 at 0x80004b0c: file kernel/exec.c, line 14.
(gdb) c
Continuing.
[Switching to Thread 1.2]
Thread 2 hit Breakpoint 1, exec (path=path@entry=0x3fffffdf00 "/init",
argv=argv@entry=0x3fffffde00) at kernel/exec.c:14
14 {
(gdb)
于此同时,第一次开启的终端 qemu-gdb 界面显示
xv6 kernel is booting
hart 2 starting
hart 1 starting
此时 xv6 内核进行初始化,并开始载入并执行它的第一个用户模式进程,/init
程序,故执行到 exec
处,触发中断。
若此时接着运行,输入 c
并回车
(gdb) c
Continuing.
[Switching to Thread 1.1]
Thread 1 hit Breakpoint 1, exec (path=path@entry=0x3fffffbf00 "sh",
argv=argv@entry=0x3fffffbe00) at kernel/exec.c:14
14 {
(gdb)
在第一次开启的中断 qemu-gdb 界面显示
init: starting sh
在此次运行中,xv6 内核继续执行,直至使用 exec
调用 sh
,现在如果继续输入 c
并回车,会发现 gdb 调试界面持续显示 continuing
,因为此时 shell
程序已经启动,在等待用户输入交互指令。
(gdb) cContinuing.
此时,如果在 qemu 界面输入 cat README
,则在 gdb 界面再次中断
(gdb) cContinuing.Thread 1 hit Breakpoint 1, exec (path=path@entry=0x3fffff9f00 "cat", argv=argv@entry=0x3fffff9e00) at kernel/exec.c:1414 {(gdb)
说明此时 shell
调用 exec
函数执行 cat
指令,执行到 exec
时产生中断。如果在 gdb 界面输入如下指令
(gdb) p argv[0]$1 = 0x87f49000 "cat"(gdb) p argv[1]$2 = 0x87f48000 "README"(gdb) p argv[2]$3 = 0x0(gdb)
可以发现 gdb 的 p
指令打印出了 exec
函数入口中的 argv[]
参数值,符合我们在 qemu 中的输入指令。
使用 gdb 进行快乐调试
我们已经理解了使用 gdb 远程连接 qemu 进行调试的基本过程,现在就可以愉快的使用 gdb 指令进行调试了。接着上面的运行程序,我们可以
-
查看当前位置的源代码
(gdb) list910 static int loadseg(pde_t *pgdir, uint64 addr, struct inode *ip, uint offset, uint sz);1112 int13 exec(char *path, char **argv)14 {15 char *s, *last;16 int i, off;17 uint64 argc, sz = 0, sp, ustack[MAXARG+1], stackbase;18 struct elfhdr elf;(gdb)
-
单步调试
(gdb) n22 struct proc *p = myproc();(gdb) n24 begin_op();(gdb) n26 if((ip = namei(path)) == 0){(gdb) n30 ilock(ip);(gdb)
-
设置断点,并跳转至断点
(gdb) break 42Breakpoint 2 at 0x80004bea: file kernel/exec.c, line 42.(gdb) cContinuing.[Switching to Thread 1.3]Thread 3 hit Breakpoint 2, exec (path=path@entry=0x3fffff9f00 "cat", argv=argv@entry=0x3fffff9e00) at kernel/exec.c:4242 for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){(gdb)
-
结束当前函数,返回函数调用点
(gdb) finishRun till exit from #0 exec (path=path@entry=0x3fffff9f00 "cat", argv=argv@entry=0x3fffff9e00) at kernel/exec.c:42
调试指令有很多,不在详述,但基本方法与汇编实验调试过程相似,熟练掌握指令后就可以快乐 debug 了。
具体程序代码调试(太长不看版)
假设我们需要调试程序 find
。
首先打开终端 1
,输入
make qemu-gdb
打开终端 2
,输入(千万注意前边需要加横线 _
)
riscv64-unknown-elf-gdb _find
打开调试界面,并载入 _find
调试信息。随后连接远程主机
(gdb) target remote localhost:xxxx
在 main
函数处打上断点
(gdb) b main
运行
(gdb) c
这时终端 1
显示出 $
号,代表 xv6 的 shell
开始运行,在 $
之后输入 find path filename
,并回车
$ find . a
此时终端 1
开始执行 find
程序,进入 find
程序后,遇到在 main
处打上的断点,故在终端 2
上显示出断点信息
Thread 1 hit Breakpoint 1, main (argc=3, argv=0x2fb0) at user/find.c:6666 int main(int argc, char *argv[]) {
接着就可以使用 n
指令一步一步调试程序了。
总结
qemu-gdb 用于开启虚拟机,在虚拟机中对程序进行交互,执行输入输出;而 riscv64-unknown-elf-gdb 用于调试程序,设置断点,查看变量。因此需要同时在两个不同终端开启 qemu-gdb 和 gdb,前者用于查看程序输出信息,或给予程序必要的输入,后者用于调试。