xv6-riscv 版本的每个RISC-V CPU都有一组控制寄存器,内核通过向这些寄存器写入内容来告诉CPU如何处理陷阱,内核可以读取这些寄存器来明确已经发生的陷阱。RISC-V文档包含了完整的内容。riscv.h(kernel/riscv.h:1)包含在xv6中使用到的内容的定义。以下是最重要的一些寄存器概述:
stvec:内核在这里写入其陷阱处理程序的地址;RISC-V跳转到这里处理陷阱。sepc:当发生陷阱时,RISC-V会在这里保存程序计数器pc(因为pc会被stvec覆盖)scause: RISC-V在这里放置一个描述陷阱原因的数字。sscratch:内核在这里放置了一个值,这个值在陷阱处理程序一开始就会派上用场。sstatus:其中的SIE位控制设备中断是否启用。如果内核清空SIE,RISC-V将推迟设备中断,直到内核重新设置SIE。SPP位指示陷阱是来自用户模式还是管理模式,并控制sret返回的模式。上述寄存器都用于在管理模式下处理陷阱,在用户模式下不能读取或写入。在机器模式下处理陷阱有一组等效的控制寄存器,xv6仅在计时器中断的特殊情况下使用它们。
在用户空间中,使用系统调用会触发trap机制
例如:write()函数
.global write
write:
li a7, SYS_write
ecall
ret

这是一个汇编指令,他会做下面操作
1.清除SIE以禁用中断。
2.将pc复制到sepc。
3.将当前模式(用户或管理)保存在状态的SPP位中。
4.设置scause以反映产生陷阱的原因。
5.将模式设置为管理模式。
6.将stvec复制到pc。
7.在新的pc上开始执行。
注意:stvec指向的地址是 uservec,将stvec复制到pc后,下面会到从uservec开始执行
# trampoline.S
uservec:
# 交换 a0 和 sscratch 寄存器的值
# so that a0 is TRAPFRAME
csrrw a0, sscratch, a0
# 将寄存器保存到当前进程的 trapframe 中
sd ra, 40(a0)
# ... 保存寄存器
sd t6, 280(a0)
# 同时也保存 a0
csrr t0, sscratch
sd t0, 112(a0)
# 从 user mode 的 traptable 中恢复一些内核的信息
ld sp, 8(a0)
ld tp, 32(a0)
ld t0, 16(a0)
ld t1, 0(a0)
csrw satp, t1
sfence.vma zero, zero
# a0 is no longer valid, since the kernel page
# table does not specially map p->tf.
# jump to usertrap(), which does not return
jr t0
p->trapframe 的起始地址会被保存在 sscratch 寄存器中usertrap 首先将所有的寄存器保存在 p->trapframe 中p->trapframe中的内核信息加载到寄存器当中
usertrapvoid usertrap(void) {
int which_dev = 0;
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");
// 设置 stvec 为 kernelvec
w_stvec((uint64)kernelvec);
struct proc *p = myproc();
// 保存PC, 否则可能会有其他的 usertrap 修改它
p->trapframe->epc = r_sepc();
if(r_scause() == 8){
// system call
if(p->killed)
exit(-1);
// 系统调用返回下一条命令
p->trapframe->epc += 4;
// 做完寄存器的操作之后打开设备中断
intr_on();
syscall(); // 系统调用
} else if((which_dev = devintr()) != 0){
// ok
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
if(p->killed)
exit(-1);
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();
usertrapret();
}
usertrapret函数该部分代码就是做一些返回用户模式的准备
void usertrapret(void)
{
struct proc *p = myproc();
// we're about to switch the destination of traps from
// kerneltrap() to usertrap(), so turn off interrupts until
// we're back in user space, where usertrap() is correct.
intr_off();
// send syscalls, interrupts, and exceptions to trampoline.S
w_stvec(TRAMPOLINE + (uservec - trampoline));
// set up trapframe values that uservec will need when
// the process next re-enters the kernel.
p->trapframe->kernel_satp = r_satp(); // kernel page table
p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()
// set up the registers that trampoline.S's sret will use
// to get to user space.
// set S Previous Privilege mode to User.
unsigned long x = r_sstatus();
x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
x |= SSTATUS_SPIE; // enable interrupts in user mode
w_sstatus(x);
// set S Exception Program Counter to the saved user pc.
w_sepc(p->trapframe->epc);
// tell trampoline.S the user page table to switch to.
uint64 satp = MAKE_SATP(p->pagetable);
// jump to trampoline.S at the top of memory, which
// switches to the user page table, restores user registers,
// and switches to user mode with sret.
uint64 fn = TRAMPOLINE + (userret - trampoline);
((void (*)(uint64, uint64))fn)(TRAPFRAME, satp);
}
.globl userret
userret:
# userret(TRAPFRAME, pagetable)
# switch from kernel to user.
# usertrapret() calls here.
# a0: TRAPFRAME, in user page table.
# a1: user page table, for satp.
# switch to the user page table.
csrw satp, a1
sfence.vma zero, zero
# put the saved user a0 in sscratch, so we
# can swap it with our a0 (TRAPFRAME) in the last step.
ld t0, 112(a0)
csrw sscratch, t0
# restore all but a0 from TRAPFRAME
ld ra, 40(a0)
# ...恢复寄存器
ld t6, 280(a0)
# restore user a0, and save TRAPFRAME in sscratch
csrrw a0, sscratch, a0
# return to user mode and user pc.
# usertrapret() set up sstatus and sepc.
sret
因为是发生在内核状态下的,所以相较于系统调用,就简单很多
# kernel/kernelvec.S
.globl kerneltrap
.globl kernelvec
.align 4
kernelvec:
# 在栈上开辟一块空间用于保存寄存器
addi sp, sp, -256
sd ra, 0(sp)
# ... 保存所有寄存器
sd t6, 240(sp)
# 调用 C 处理程序
call kerneltrap
# 恢复寄存器到之前的状态
ld ra, 0(sp)
# ... 恢复所有的寄存器(除了 tp)
ld t6, 240(sp)
# 恢复栈指针
addi sp, sp, 256
# 返回到之前的运行状态
sret
kerneltrapkerneltrap的处理和usertrap还是很了类似的
// interrupts and exceptions from kernel code go here via kernelvec,
// on whatever the current kernel stack is.
void kerneltrap()
{
int which_dev = 0;
uint64 sepc = r_sepc();
uint64 sstatus = r_sstatus();
uint64 scause = r_scause();
if ((sstatus & SSTATUS_SPP) == 0)
panic("kerneltrap: not from supervisor mode");
if (intr_get() != 0)
panic("kerneltrap: interrupts enabled");
if ((which_dev = devintr()) == 0)
{
printf("scause %p\n", scause);
printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
panic("kerneltrap");
}
// give up the CPU if this is a timer interrupt.
if (which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
yield();
// the yield() may have caused some traps to occur,
// so restore trap registers for use by kernelvec.S's sepc instruction.
w_sepc(sepc);
w_sstatus(sstatus);
}
这部分还是卡了很久,因为出现了很多汇编,需要了解xv6的寄存器,以及RSICV指令集,还有栈桢相关的知识,后续继续一步一步完成,继续努力