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 | GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2 |
起步操作
开始的时候,可以通过 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
加载到 a1
。exec
这个系统调用的调用号是 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
中。而后直接使用 exec
将 path
中的程序创建出来
创建完成
通过上述的流程,可以正确地创建出真正意义上的第一个用户态程序,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 许可协议。转载请注明出处!