调试运行第一个Xv6程序
Jacksing

Xv6 刚启动的时候处于 Machine Mode,完成基本配置工作后就会尽快跳转到 Supervisor Mode,在这个模式下,main 会尽快完成系统的配置工作并创建出第一个用户程序。

前置知识

Xv6 是一个支持多进程的操作系统,在 Makefile 中可以看到 CPUS 会被默认设置为 3,为了调试方便,在启动的时候,可以将其设置为 1。这样就只会在一个 CPU 上运行程序,单个断点就可以阻止整个操作系统的运行。

环境准备

系统可以采用 Ubuntu Server 20.04,调试用的 GDB 是 gdb-multiarch 9.2.0,qemu 则采用 SiFive 所提供的 qemu-system-riscv64 5.1.0。

开始运行

调试xv6 一文来操作的话,开发调试环境是可以顺利跑起来的。此时在一个终端执行 make CPUS=1 qemu-gdb 将系统跑起来,然后在另一个终端执行 gdb-multiarch 将 gdb 运行起来,此时 gdb 会自动连接到 qemu 的调试环境内,一切顺利的话,可以看到下面这样的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word".
The target architecture is assumed to be riscv:rv64
warning: No executable has been specified and target does not support
determining executable automatically. Try using the "file" command.
0x0000000000001000 in ?? ()

起步操作

开始的时候,可以通过 b _entry 在系统入口处设置一个断点,在链接控制脚本中提到过,系统的入口点会被设置为 0x80000000。但是不知道什么原因,在我的机器上,这个断点的地址会被设置为 0x80000004,而在老师课上所展示的却是 0x8000000a。但是这时候如果通过 info address _entry 查看 _entry 的地址,又可以看到其位于 0x80000000 上。具体是什么原因还需要进行后续的探究。

断点设置好了之后,可以输入 c 让系统跑起来,这时候会直接卡在断点处。为了方便调试,这时候可以使用 layout split 将 gdb 窗口划分为指令和代码两个窗口,方便查看每个代码所对应的指令。

这时候系统处于 Machine Mode,此时会配置好环境,创建好程序栈,以便转入 Supervisor Mode 运行 C 代码。这些代码暂时可以不必理会。

进入 main 函数

越过 Machine Mode 相关的代码后,就可以直接来到内核起始处,这里跟普通的 C 程序一样,都是以 main 作为入口函数。通过命令 b main 将断点设置在 main 函数处。然后直接输入 c 让程序跑起来,直到 main 函数再停下来。

main 函数内会完成系统的各种配置,包括设置好虚拟内存,页表,文件系统等等,具体可以参看代码中的注释。老师在课上有提到,这些初始化的操作是有顺序的,不能随意调换。

在这些初始化的函数中,现在需要关注的是 userinit()。在这个函数内,会完成第一个 User Mode 程序的创建。

进入 userinit() 函数

main 函数内使用 n 命令让程序逐行执行,直到 userinit() 的时候,改用 s 让程序进入函数体内。

这个函数的操作就是手动创建了一个用户进程,将这个进程设置为 RUNNABLE 状态,以便 cpu 进行调度执行。这里有个有意思的地方,userinit() 创建用户进程的方式,是直接使用程序的二进制形式来创建。

这个用户程序的二进制代码定义直接硬编码在 initcode 这个数组里,相应的可执行程序是 user/initcode

initcode 程序

initcode 的定义在 user/initcode.S。这个程序就是 Xv6 所创建的第一个用户态程序,从 userinit() 中可以看出,第一个用户态程序只能以靠手动的形式来创建,所以会要求它足够的简单。因此,initcode 更像一个楔子,用来引出真正的带有逻辑的用户程序。

从代码中可以看出,initcode 就是利用 exec 这个系统调用来创建出一个更为复杂的可执行程序。它会将 init 这个程序加载到 a0,这个便是需要创建的程序的程序名,而后将参数 argv 加载到 a1exec 这个系统调用的调用号是 7,所以需要将 7 加载到 a7 上。相关的操作完成后,就会调用 ecall 将控制权交回给操作系统。

触发 syscall

从上面可以看出,userinit 会创建一个简单的用户态程序,该程序会使用系统调用 exec 来创建一个真正有逻辑,较为复杂的用户态程序来替代自己。

系统如何创建的第一个程序具体细节此时倒不必过多关注,需要关注的就是系统调用的具体逻辑,所以可以用 b syscall 将断点设置到系统调用的触发函数上。在 userinit() 内直接使用 c 让程序直接跑到断点的位置。

syscall 这个函数位于 kernel/syscall.c 内,内核可以通过 p->trapframe->a7 获取到系统调用号,这时候使用 p num 可以查看到这个系统调用号就是当时加载到 a7 的 7。程序运行到 p->trapframe->a0 = syscalls[num](); 时,直接使用 s 陷入到 exec 这个系统调用内。

进入 exec

exec 位于 kernel/sysfile.c 里,进入这个函数后,会把需要调用的程序路径放置到 path,启动参数放置到 argv 中。而后直接使用 execpath 中的程序创建出来

创建完成

通过上述的流程,可以正确地创建出真正意义上的第一个用户态程序,init。这个程序的源代码在 user/init.c。从代码里可以看出,这个程序唯一目的就是修改文件描述符,将 0 和 1 强制设定为标准输入输出。并且维持 sh 的运行。

至此,整个系统就顺利地运行起来了。

  • 本文标题:调试运行第一个Xv6程序
  • 本文作者:Jacksing
  • 创建时间:2022-02-01 23:10:20
  • 本文链接:https://wzzzx.github.io/6-S081/run-first-xv6-program/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论