# 6.8 userret函数

现在程序执行又到了trampoline代码。

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-MLjftBZMVOlWHkHvfqZ%2F-MLvobMS3bwAzHvQrEaC%2Fimage.png?alt=media\&token=a4d63ddb-7d03-44f1-a115-397bcbac748a)

第一步是切换page table。在执行*csrw satp, a1*之前，page table应该还是巨大的kernel page table。这条指令会将user page table（在usertrapret中作为第二个参数传递给了这里的userret函数，所以存在a1寄存器中）存储在SATP寄存器中。执行完这条指令之后，page table就变成了小得多的user page table。但是幸运的是，user page table也映射了trampoline page，所以程序还能继续执行而不是崩溃。（注，sfence.vma是清空页表缓存，详见4.4）。

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-MLjftBZMVOlWHkHvfqZ%2F-MLvqb4-QJhWy6QNobaG%2Fimage.png?alt=media\&token=b5c58897-9c79-4565-83b1-0506c023d7d2)

在uservec函数中，第一件事情就是交换SSRATCH和a0寄存器。而这里，我们将SSCRATCH寄存器恢复成保存好的用户的a0寄存器。在这里a0是trapframe的地址，因为C代码usertrapret函数中将trapframe地址作为第一个参数传递过来了。112是a0寄存器在trapframe中的位置。（注，这里有点绕，本质就是通过当前的a0寄存器找出存在trapframe中的a0寄存器）我们先将这个地址里的数值保存在t0寄存器中，之后再将t0寄存器的数值保存在SSCRATCH寄存器中。

为止目前，所有的寄存器内容还是属于内核。

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-MLjftBZMVOlWHkHvfqZ%2F-MLvsytGeLtbgkFXNMPB%2Fimage.png?alt=media\&token=d0bdb26e-35f2-409c-811e-7174a4032f37)

接下来的这些指令将a0寄存器指向的trapframe中，之前保存的寄存器的值加载到对应的各个寄存器中。之后，我们离能真正运行用户代码就很近了。

> 学生提问：现在trapframe中的a0寄存器是我们执行系统调用的返回值吗？
>
> Robert教授：是的，系统调用的返回值覆盖了我们保存在trapframe中的a0寄存器的值（详见6.6）。我们希望用户程序Shell在a0寄存器中看到系统调用的返回值。所以，trapframe中的a0寄存器现在是系统调用的返回值2。相应的SSCRATCH寄存器中的数值也应该是2，可以通过打印寄存器的值来验证。

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-MLjftBZMVOlWHkHvfqZ%2F-MLvv7FhOrjnCMu36Esi%2Fimage.png?alt=media\&token=73354fc5-036b-4d65-b64f-a41922005f2d)

现在我们打印所有的寄存器，

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-MLjftBZMVOlWHkHvfqZ%2F-MLvwArQQ3oUTukAG5sj%2Fimage.png?alt=media\&token=e9a6aa1d-e02c-4a40-87f5-86348fccc41c)

我不确定你们是否还记得，但是这些寄存器的值就是我们在最最开始看到的用户寄存器的值。例如SP寄存器保存的是user stack地址，这是一个在较小的内存地址；a1寄存器是我们传递给write的buffer指针，a2是我们传递给write函数的写入字节数。

a0寄存器现在还是个例外，它现在仍然是指向trapframe的指针，而不是保存了的用户数据。

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-MLjftBZMVOlWHkHvfqZ%2F-MLvx9YkdyGWTK0KuIoG%2Fimage.png?alt=media\&token=93f7a3a1-fbca-4728-ab36-dc6aab618a4f)

接下来，在我们即将返回到用户空间之前，我们交换SSCRATCH寄存器和a0寄存器的值。前面我们看过了SSCRATCH现在的值是系统调用的返回值2，a0寄存器是trapframe的地址。交换完成之后，a0持有的是系统调用的返回值，SSCRATCH持有的是trapframe的地址。之后trapframe的地址会一直保存在SSCRATCH中，直到用户程序执行了另一次trap。现在我们还在kernel中。

sret是我们在kernel中的最后一条指令，当我执行完这条指令：

* 程序会切换回user mode
* SEPC寄存器的数值会被拷贝到PC寄存器（程序计数器）
* 重新打开中断

现在我们回到了用户空间。打印PC寄存器，

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-MLjftBZMVOlWHkHvfqZ%2F-MLvyskX7IyOTCLRT2QI%2Fimage.png?alt=media\&token=421a14db-519c-4d52-97b4-dbc80bd2b316)

这是一个较小的指令地址，非常像是在用户内存中。如果我们查看sh.asm，可以看到这个地址是write函数的ret指令地址。

![](https://1977542228-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MHZoT2b_bcLghjAOPsJ%2F-MLjftBZMVOlWHkHvfqZ%2F-MLvzIzBZ2gjrZbhYZwE%2Fimage.png?alt=media\&token=1a388243-ac78-4aaa-b786-b0dbdb62d78f)

所以，现在我们回到了用户空间，执行完ret指令之后我们就可以从write系统调用返回到Shell中了。或者更严格的说，是从触发了系统调用的write库函数中返回到Shell中。

> 学生提问：你可以再重复一下在sret过程中，中断会发生什么吗？
>
> Robert教授：sret打开了中断。所以在supervisor mode中的最后一个指令，我们会重新打开中断。用户程序可能会运行很长时间，最好是能在这段时间响应例如磁盘中断。

最后总结一下，系统调用被刻意设计的看起来像是函数调用，但是背后的user/kernel转换比函数调用要复杂的多。之所以这么复杂，很大一部分原因是要保持user/kernel之间的隔离性，内核不能信任来自用户空间的任何内容。

另一方面，XV6实现trap的方式比较特殊，XV6并不关心性能。但是通常来说，操作系统的设计人员和CPU设计人员非常关心如何提升trap的效率和速度。必然还有跟我们这里不一样的方式来实现trap，当你在实现的时候，可以从以下几个问题出发：

* 硬件和软件需要协同工作，你可能需要重新设计XV6，重新设计RISC-V来使得这里的处理流程更加简单，更加快速。
* 另一个需要时刻记住的问题是，恶意软件是否能滥用这里的机制来打破隔离性。

好的，这就是这节课的全部内容。
