6.3 ECALL指令之前的状态

接下来,我将切换到gdb的世界中。 大家可以看我共享的屏幕,我们将要跟踪一个XV6的系统调用,也就是Shell将它的提示信息通过write系统调用走到操作系统再输出到console的过程。你们可以看到,用户代码sh.c初始了这一切。

上图中选中的行,是一个write系统调用,它将“$ ”写入到文件描述符2。接下来我将打开gdb并启动XV6。

作为用户代码的Shell调用write时,实际上调用的是关联到Shell的一个库函数。你可以查看这个库函数的源代码,在usys.s。

上面这几行代码就是实际被调用的write函数的实现。这是个非常短的函数,它首先将SYS_write加载到a7寄存器,SYS_write是常量16。这里告诉内核,我想要运行第16个系统调用,而这个系统调用正好是write。之后这个函数中执行了ecall指令,从这里开始代码执行跳转到了内核。内核完成它的工作之后,代码执行会返回到用户空间,继续执行ecall之后的指令,也就是ret,最终返回到Shell中。所以ret从write库函数返回到了Shell中。

为了展示这里的系统调用,我会在ecall指令处放置一个断点,为了能放置断点,我们需要知道ecall指令的地址,我们可以通过查看由XV6编译过程产生的sh.asm找出这个地址。sh.asm是带有指令地址的汇编代码(注,asm文件3.7有介绍)。我这里会在ecall指令处放置一个断点,这条指令的地址是0xde6。

现在,我要让XV6开始运行。我期望的是XV6在Shell代码中正好在执行ecall之前就会停住。

完美,从gdb可以看出,我们下一条要执行的指令就是ecall。我们来检验一下我们真的在我们以为自己在的位置,让我们来打印程序计数器(Program Counter),正好我们期望在的位置0xde6。

我们还可以输入info reg打印全部32个用户寄存器,

这里有一些数值我们还不知道,也不关心,但是这里的a0,a1,a2是Shell传递给write系统调用的参数。所以a0是文件描述符2;a1是Shell想要写入字符串的指针;a2是想要写入的字符数。我们还可以通过打印Shell想要写入的字符串内容,来证明断点停在我们认为它应该停在的位置。

可以看出,输出的确是美元符($)和一个空格。所以,我们现在位于我们期望所在的write系统调用函数中。

有一件事情需要注意,上图的寄存器中,程序计数器(pc)和堆栈指针(sp)的地址现在都在距离0比较近的地址,这进一步印证了当前代码运行在用户空间,因为用户空间中所有的地址都比较小。但是一旦我们进入到了内核,内核会使用大得多的内存地址。

系统调用的时间点会有大量状态的变更,其中一个最重要的需要变更的状态,并且在它变更之前我们对它还有依赖的,就是是当前的page table。我们可以查看STAP寄存器。

这里输出的是物理内存地址,它并没有告诉我们有关page table中的映射关系是什么,page table长什么样。但是幸运的是,在QEMU中有一个方法可以打印当前的page table。从QEMU界面,输入ctrl a + c可以进入到QEMU的console,之后输入info mem,QEMU会打印完整的page table。

这是个非常小的page table,它只包含了6条映射关系。这是用户程序Shell的page table,而Shell是一个非常小的程序,这6条映射关系是有关Shell的指令和数据,以及一个无效的page用来作为guard page,以防止Shell尝试使用过多的stack page。我们可以看出这个page是无效的,因为在attr这一列它并没有设置u标志位(第三行)。attr这一列是PTE的标志位,第三行的标志位是rwx表明这个page可以读,可以写,也可以执行指令。之后的是u标志位,它表明PTE_u标志位是否被设置,用户代码只能访问u标志位设置了的PTE。再下一个标志位我也不记得是什么了(注,从4.3可以看出,这个标志位是Global)。再下一个标志位是a(Accessed),表明这条PTE是不是被使用过。再下一个标志位d(Dirty)表明这条PTE是不是被写过。

现在,我们有了这个小小的page table。顺便说一下,最后两条PTE的虚拟地址非常大,非常接近虚拟地址的顶端,如果你读过了XV6的书,你就知道这两个page分别是trapframe page和trampoline page。你可以看到,它们都没有设置u标志,所以用户代码不能访问这两条PTE。一旦我们进入到了supervisor mode,我们就可以访问这两条PTE了。

对于这里page table,有一件事情需要注意:它并没有包含任何内核部分的地址映射,这里既没有对于kernel data的映射,也没有对于kernel指令的映射。除了最后两条PTE,这个page table几乎是完全为用户代码执行而创建,所以它对于在内核执行代码并没有直接特殊的作用。

学生提问:PTE中a标志位是什么意思?

Robert教授:这表示这条PTE是不是被代码访问过,是不是曾经有一个被访问过的地址包含在这个PTE的范围内。d标志位表明是否曾经有写指令使用过这条PTE。这些标志位由硬件维护以方便操作系统使用。对于比XV6更复杂的操作系统,当物理内存吃紧的时候,可能会通过将一些内存写入到磁盘来,同时将相应的PTE设置成无效,来释放物理内存page。你可以想到,这里有很多策略可以让操作系统来挑选哪些page可以释放。我们可以查看a标志位来判断这条PTE是否被使用过,如果它没有被使用或者最近没有被使用,那么这条PTE对应的page适合用来保存到磁盘中。类似的,d标志位告诉内核,这个page最近被修改过。

不过XV6没有这样的策略。

接下来,我会在Shell中打印出write函数的内容。

程序计数器现在指向ecall指令,我们接下来要执行ecall指令。现在我们还在用户空间,但是马上我们就要进入内核空间了。

Last updated