# 6.3 ECALL指令之前的状态

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

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-ML5OrMSu1sZV8JrCG4C%2F-ML5Q7oMs3EmQSXOpHZw%2Fimage.png?alt=media\&token=d465e9a8-bb7b-4847-bf8e-89cc1e12c3ba)

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

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-ML5OrMSu1sZV8JrCG4C%2F-ML5QkklZGS46F7Ulkzx%2Fimage.png?alt=media\&token=e1dfd288-9b00-4fe9-8502-408e59f7ca15)

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

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-ML5OrMSu1sZV8JrCG4C%2F-ML5RUaTjsUrdw2GrcYT%2Fimage.png?alt=media\&token=54b07586-6e4d-4304-b399-3696cc0152f6)

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

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-ML5OrMSu1sZV8JrCG4C%2F-ML5Tv0Tt2I5mrnm9wiZ%2Fimage.png?alt=media\&token=4eb82e08-991e-4fb5-9841-b8cc78e0f4ba)

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

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-ML5UF6bH4CcXBo4EXjF%2F-MLAYCSG9U40OjnDWtTs%2Fimage.png?alt=media\&token=0c1cfe8f-d7c5-4311-8e13-15b178c787a4)

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

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-ML5UF6bH4CcXBo4EXjF%2F-MLAYi_F9KULTLcqdfJV%2Fimage.png?alt=media\&token=1218fe66-5487-4119-8a0e-d427e521a680)

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

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-ML5UF6bH4CcXBo4EXjF%2F-MLAZAzDe6phr2w8cY5e%2Fimage.png?alt=media\&token=613b4b60-3c06-4c5d-b2e2-d1b83a4d941b)

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

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-ML5UF6bH4CcXBo4EXjF%2F-MLA_SsIyQObazOHpYd9%2Fimage.png?alt=media\&token=d0610673-48ba-477d-a537-fc278e565237)

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

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

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

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-ML5UF6bH4CcXBo4EXjF%2F-MLAbVqnXu4s5M_wDTKm%2Fimage.png?alt=media\&token=9d38fcce-bec6-450d-9c70-6ebfc4648422)

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

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-ML5UF6bH4CcXBo4EXjF%2F-MLAcP8ULSwHAM90b_AM%2Fimage.png?alt=media\&token=a02cc317-c767-46f4-9190-553ef4b5a072)

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

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-MLFmR-ec8XwDd9RO7RJ%2F-MLIg7ff-PZuCSvqWMhG%2Fimage.png?alt=media\&token=36f63570-7fb0-4f9f-9353-4361efada276)

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