> For the complete documentation index, see [llms.txt](https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec06-isolation-and-system-call-entry-exit-robert/6.3-before-ecall.md).

# 6.3 ECALL指令之前的状态

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

![](/files/-ML5Q7oMs3EmQSXOpHZw)

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

![](/files/-ML5QkklZGS46F7Ulkzx)

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

![](/files/-ML5RUaTjsUrdw2GrcYT)

上面这几行代码就是实际被调用的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。

![](/files/-ML5Tv0Tt2I5mrnm9wiZ)

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

![](/files/-MLAYCSG9U40OjnDWtTs)

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

![](/files/-MLAYi_F9KULTLcqdfJV)

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

![](/files/-MLAZAzDe6phr2w8cY5e)

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

![](/files/-MLA_SsIyQObazOHpYd9)

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

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

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

![](/files/-MLAbVqnXu4s5M_wDTKm)

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

![](/files/-MLAcP8ULSwHAM90b_AM)

这是个非常小的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函数的内容。

![](/files/-MLIg7ff-PZuCSvqWMhG)

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