MIT 6.S081 第四章笔记 | 陷入指令和中断
4.1 ISA & Assembly Language
\(ISA\): \(Instruction \space Set\)。
\(workflow\): \(C \rightarrow Assembly(.S/.asm) \rightarrow binary(object.o)\)
汇编语言没有明确的工作流,只是一行一行的执行指令。汇编语言是基于寄存器进行操作的,而不是内存。
\(RISC-V \space vs \space x86\)
- \(RISC-V\)是精简指令集,指令数少,效率更高,不好维护。
- \(x86\):复杂指令集,指令很多并且可以实现复杂功能
4.2 Calling convention
调用约定(\(calling \space convention\))是规定子过程如何获取参数以及如何返回的方案,调用约定一般规定了: - 参数、返回值、返回地址等放置的位置(寄存器、栈或内存)。
RISC-V通过寄存器来传递参数,而不是栈。\(a_0
\rightarrow a_7\) 是int
参数,\(f_0 \rightarrow f_7\)
是float
参数。
- 如何将调用子过程的准备工作与恢复现场的工作划分到调用者(\(caller\))与被调用者(\(callee\))身上。
小于一个指针字(RISCV64中是8字节,RISCV32是4字节)的参数传入时将参数放在寄存器的最低位,因为RISC-V是小端系统,当2个指针字的参数传入时,低位的1个指针字放在偶数寄存器,比如a0上,高位的1个指针字放在奇数寄存器,比如a1上。当高于2个指针字的参数传入时以引用的方式传入。struct参数没有传到寄存器的部分将以栈的方式传入,sp栈指针将指向第一个没有传入到寄存器的参数。
从函数返回的值,如果是整数将放在a0和a1中,如果是小数将放置在fa0和fa1寄存器中。对于更大的返回值,将放置在内存中,\(caller\) 开辟这个内存,并且把指向这个内存的指针作为第一个参数传递给 \(callee\)
由 \(caller\) 保存的寄存器不会在函数调用之间被保存,又名易失性寄存器,如果要在过程调用后恢复该值,则调用方需要将这些寄存器压入堆栈或复制到其他位置,而 \(callee\) 保存的寄存器会被保存,称为非易失性寄存器,可以期望这些寄存器在被调用者返回后保持相同的值。比如函数A调用了函数B,所有函数A保存的寄存器在函数B被调用后可以被B重写覆盖。
4.3 Stack
栈从高地址向低地址增长,每个大的\(box\)叫一个\(stack \space frame\)(栈帧),栈帧由函数调用来分配,每个栈帧大小不一定一样,但是栈帧的最高处一定是\(return \space address\)。
sp是stack pointer,用于指向栈顶(低地址),保存在寄存器中
fp是frame pointer,用于指向当前帧底部(高地址),保存在寄存器中,同时每个函数栈帧中保存了调用当前函数的函数(父函数)的fp(保存在to prev frame那一栏中)
这些栈帧都是由编译器编译生成的汇编文件生成的
4.4 trap
3种可能的情况使得CPU暂停对正常指令的执行,并强制将控制权转移到处理该事件的特殊代码:1. syscall,移交给kernel 2. exception,指令执行了非法操作 3. 设备中断。以上情况合并称为trap。
trap应该对于被打断的指令是透明的,也就是说被打断的指令不应该知道这个地方产生了trap,产生trap之后现场应该得以恢复并继续执行被打断的指令。
xv6对trap的处理分为四个阶段:1. 对RISC-V CPU的硬件的一些操作 2. 一些为运行kernel C语言文件做准备的汇编文件 3. 用C实现的trap handler(暂且翻译为异常处理程序) 4. system call / device-driver service routine
通常对于user space的trap、kernel space的trap和计时器中断会有不同的trap handler
4.5 RISC-V trap machinery
RISC-V CPU有一系列的控制寄存器可以通知kernel发生了trap,也可以由kernel写入来告诉CPU怎样处理trap。
stvec
:异常处理程序的地址。由内核填入。sepc
: 保存trap发生时当前的PC
寄存器值,之后PC
的值被stvec
覆盖。通过sret
指令将sepc
值也入PC
,达到回复现场的作用。scause
: 填入产生trap的原因,有CPU写入。sscartch
: 异常处理程序使用的一组寄存器。sstatus
: SIE位控制设备中断是否被开启,SPP位指示trap是来自内核还是用户程序。
以上寄存器都只在监管模式被启动。
当发生除了计时器中断以外的其他类型的trap时,RISC-V将执行以下步骤:
- 如果trap是一个设备产生的中断,而SIE又被清除的情况下,不做下方的任何动作。
- 清除SIE来disable一切中断。
- 把pc复制到sepc。
- 把当前的模式(user / supervisor)保存到SPP。
- 设置scause寄存器来指示产生trap的原因。
- 将当前的模式设置为supervisor。
- 将stvec的值复制到pc。
- 开始执行pc指向的trap handler的代码。
注意CPU并没有切换到kernel页表,也没有切换到kernel栈,也不会保存除PC外的其他寄存器值。这些操作都应该由内核完成。
4.6 Traps from user space
如果用户程序进行系统调用(调用ecall
指令)、执行非法操作或者设备中断时,就会产生trap。
当user
space中发生trap时,会将stvec
的值复制到pc
,而此时stvec
的值是trampoline.S
中的uservec
,因此跳转到uservec
函数。该函数先保存一些现场的寄存器,恢复kernel栈指针、kernel
page
table到satp寄存器,再跳转到usertrap
(kernel/trap.c
)这个用户trap通用执行器。然后返回usertrapret
(kernel/trap.c
),跳回到kernel/trampoline.S
,最后用userret(kernel/trampoline.S)
通过sret
跳回到user
space。
RISC-V在trap中不会改变页表,因此user page
table必须有对uservec
的mapping,uservec
是stvec
指向的trap
vector
instruction。uservec要切换satp到kernel页表,同时kernel页表中也要有和user页表中对uservec相同的映射。RISC-V将uservec
保存在trampoline
页中,并将TRAMPOLINE
放在kernel页表和user页表的相同位置处(MAXVA)。