保存上下文
处理异常的时候需要保存寄存器内容(上下文的一部分),需要将这些内容保存下来。但是硬件不负责这些内容的保存,因此需要用软件代码来保存这些寄存器的值。riscv采用sw
指令将各个通用寄存器以此压栈。
除了通用寄存器之外,还需要保存其他上下文内容:
- 触发异常时的PC和处理器状态,riscv中的mepc和mstatus寄存器。异常相应机制把它们保存在相应的系统寄存器中,我们还需要将他们从系统寄存器读出,然后保存在堆栈上。
- 异常号,riscv的mcause寄存器。我们还需要将他们保存在堆栈上。
- 地址空间,PA4时考虑
这些内容构成了完整的上下文信息, 异常处理过程可以根据上下文来诊断并进行处理, 同时, 将来恢复上下文的时候也需要这些信息。
接下来代码会调用C函数__am_irq_handle()
(在abstract-machine/am/src/$ISA/nemu/cte.c
中定义), 来进行异常的处理。
学到了一个东西:函数指针
重新组织
Context
结构体
实现这一过程的新指令(我理解为保存上下文中的新指令)
理解上下文形成过程,重新组织
abstract-machine/am/include/arch/$ISA-nemu.h
中定义的Context
结构体的成员,使得这些成员的定义顺序和abstract-machine/am/src/$ISA/nemu/trap.S
中构造的上下文保持一致。并且在重新组织
Context
结构体时仍然需要正确地处理地址空间信息的位置, 否则你可能会在PA4中遇到难以理解的错误.实现之后, 你可以在
__am_irq_handle()
中通过printf
输出上下文c
的内容, 然后通过简易调试器观察触发自陷时的寄存器状态, 从而检查你的Context
实现是否正确.
这里观察
abstract-machine/am/src/$ISA/nemu/trap.S
中的行为,先是顺序的往内存栈中压入普通寄存器的内容,然后再向其中压入csr寄存器的内容,到这里应该能猜出Context
结构的成员的顺序了。但是为什么是这种顺序呢?
我从互联网上看到有其他人的回答是这样的,结构体是一段连续存储的地址空间,而
trap.S
中向内存中逐个写入的操作其实就是向一段连续的地址内写入数据的操作,如果这个地址开头正好是结构体的地址,并且写入顺序和结构体的定义顺序一致,那么就相当于向一个结构体写数据。
必答题
首先要知道__am_irq_handle()是被
trap.S
调用的,在调用之前会执行一条指令:mv a0, sp
这其实就是汇编函数调用时的传参。
汇编函数调用以及传参[从汇编语言的寄存器来看函数参数传递 - 金色旭光 - 博客园 (cnblogs.com)](https://www.cnblogs.com/goldsunshine/p/14560301.html#:~:text=3|5函数调用传参总结 1 传值调用 直接拷贝一份 数值,到被调用函数,被调用函数中的数值和调用函数中的数值在内存中是两份相互独立的; 2 传地址调用 是将 数值的地址 拷贝一份到被调用函数中,数值在内存中只有一份,被调用函数通过该地址还能找到数值,可以修改这个数值。)
例如
#include<stdio.h> int main() { int a = 10; return 0; }
在汇编之后
.file "space.c" .option pic .text .align 1 .globl main .type main, @function main: addi sp,sp,-32 sd s0,24(sp) addi s0,sp,32 li a5,10 sw a5,-20(s0) li a5,0 mv a0,a5 ld s0,24(sp) addi sp,sp,32 jr ra .size main, .-main .ident "GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0" .section .note.GNU-stack,"",@progbits
其中的头三条指令就是将
main
函数压栈,作为栈底,最后也是在main
函数中退出。传值/传地址
传值调用和传地址调用最大区别就在于调用函数处理实参的方式,传值调用,就是将
数值
当做实参写入寄存器,被调用函数从寄存器中取出数值;传地址调用是将数值的地址
当作实参写入寄存器,被调用函数中从寄存器取出地址。到这里就明白了,其实该指令就是函数调用传递参数的过程,只不过是用汇编语言编写的。
而
__am_irq_handle()
函数的参数就是Context *c
,那么就可以理解了,通过trap.S
中mv a0, sp
传递进来的就是结构体指针c
的地址。
事件分发
__am_irq_handle()
函数会根据c
中的mcause
给当前的异常打包编号,编好号之后调用回调函数user_handler(ev, c)
,第一个参数就是异常编号,第二个参数就是上下文c
。然后nanos-lite
中init_irq()
执行异常的具体行为。
恢复上下文
代码将会一路返回到trap.S
的__am_asm_trap()
中, 接下来的事情就是恢复程序的上下文。
这里我漏了一个地方,riscv需要在软件层面实现PC+4。
也就是在处理具体异常的时候,根据不同异常的不同要求,分别实现是否PC+4。
这里也就出现了两个地方可以供我修改,一个是
abstract-machine/am/src/riscv/nemu/cte.c
,一个是/home/groot/ysyx-workbench/nanos-lite/src/irq.c
。但是说了软件,就不能在abstract-machine
中实现,而要在nanos-lite
中实现这一功能。实现的过程就是在
do_event
中处理异常的时候,根据异常的不同,决定是否将c->mepc
+4。
必答题
yield()
的实现过程->nanos-lite main()
---> abstact-machine yield()
--------->nemu ecall()
// 经过这一步之后pc值被改变, 改变为异常处理函数的入口.也就是在
cte_init()
中注册的地址:__am_asm_trap
.------------->nemu isa_raise_intr()
// 这里就是上下文保存+异常处理+恢复上下文的函数了
-----------------> abstract-machine __am_asm_trap
---------------------> abstract-machine __am_irq_handle()
------------------------->nanos-lite do_event()
---------------------> abstract-machine __am_irq_handle()
-----------------> abstract-machine __am_asm_trap
// 经过这一步之后pc值被还原为异常之前的pc值(或者由软件确认的是否+4的pc值)
------------->nemu mret()
---> abstact-machine yield()
->nanos-lite main()
在这里我遇见了另一个问题:如何实现DiffTest中异常行为和nemu的异常行为一致。
需要知道的一点是Spike
的异常处理是一套完整的异常处理流程,而不是像nemu
是一个简陋版。所以二者的行为肯定有不一样的地方,所以我应该在nemu
中模仿spike
的行为,将mstatus
寄存器的值按照Spike
中的行为处理,而不是完全按照手册处理。