headPic

6.S081:xv6 trap过程

OAP

在系统调用、中断以及异常(如page fault)时会陷入内核来执行处理。

这里以系统调用为例。

1. 以系统调用为例

一般先由进程调用一条系统调用的指令,在这里以第一个进程ecall为例:

 1# user/initcode.S
 2# exec(init, argv)
 3.globl start
 4start:
 5        la a0, init
 6        la a1, argv
 7        li a7, SYS_exec
 8        ecall
 9
10# for(;;) exit();
11exit:
12        li a7, SYS_exit
13        ecall
14        jal exit
15
16# char init[] = "/init\0";
17init:
18  .string "/init\0"
19
20# char *argv[] = { init, 0 };
21.p2align 2
22argv:
23  .long init
24  .long 0

上述过程已经足够清晰,这里的initcode.S的二进制编码早已在kernel/proc.c:userinit函数中作为数组被复制到第一个进程的内存中

2. ecall探究!

ecall指令会使得硬件做一些事情:

ecall工作
以上任务最主要的是:

  1. 关闭中断
  2. 把pc复制到sepc用于恢复
  3. scause保存trap原因
  4. stvec(system trap vector)早已指向用户进程地址空间下的trampoline代码,将其复制给pc后,CPU要执行的即为trampoline中的第一行代码!此外,sscratch保存的是进程的p->trapframe地址

在ecall执行后:

  1. 进入trampoline.S后,执行的是uservec代码:
    1. 首先将a0和sscratch中的值交换,此时a0中的就是p->trapframe的地址(sscratch寄存器是控制寄存器,不能作为基址寄存器)
    2. 随后将a0作为基址寄存器保存通用寄存器到trapframe中
    3. 随后保存sscratch中a0的值到trapframe
    4. 再然后依次从p->trapframe中恢复kernel_sp,kernel_hartid,kernel_trap(即usertrap函数的地址),satp
    5. 上述数据恢复了之后跳转到usertrap函数中进行处理
  2. kernel/trap.c:usertrap
    1. 在stvec中保存kernelvec函数的地址,因为此时已经在内核中,内核中的trap要用不同的函数处理
    2. 将sepc保存到trapframe的epc中(sepc是pc的值)
    3. 从scause中读取trap原因,分配不同的函数进行处理,此例中为syscall
    4. 首先将p->trapframe->epc加上4,因为之前指向ecall,此时需要它指向下一条指令来执行
    5. 打开中断,进行syscall系统调用
  3. kernel/syscall.c:syscall

a7寄存器中读出系统调用的标号,将其执行,并将返回值赋值给a0

  1. kernel/trap.c:usertrapret
    1. 开始返回过程
    2. 由于即将返回用户态,因此stvec中要塞入uservec的地址
    3. 将kernel satp,trap,kernel_sp,kernel_hartid的数据保存到trapframe中
    4. 从p->trapframe->epc中恢复pc的值到sepc中
    5. 恢复satp中的值为user pagetable
    6. 执行userret,从内核态返回用户态
  2. kernel/trampoline.S:userret
    1. 恢复user pagetable
    2. 从trapframe中恢复寄存器的数据
    3. 保存trapframe的地址到sscratch中
    4. sret返回用户态,并完成一系列的硬件操作

之后的很多实验都会需要爆改usertrap函数

同理代码还是得debug一下才能知道这里怎么跑的,debug过程中从用户态跳到内核态是进不了trampoline的,此时得在stvec保存的地址处打个断点

3. 参考

3.1 老哥1

3.2 老哥2

3.3 xv6 book