在系统调用、中断以及异常(如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指令会使得硬件做一些事情:
以上任务最主要的是:
- 关闭中断
- 把pc复制到sepc用于恢复
scause
保存trap原因stvec(system trap vector)
早已指向用户进程地址空间
下的trampoline
代码,将其复制给pc后,CPU要执行的即为trampoline中的第一行代码!此外,sscratch保存的是进程的p->trapframe
地址
在ecall执行后:
- 进入
trampoline.S
后,执行的是uservec
代码:- 首先将a0和sscratch中的值交换,此时a0中的就是p->trapframe的地址(sscratch寄存器是控制寄存器,不能作为基址寄存器)
- 随后将a0作为基址寄存器保存通用寄存器到trapframe中
- 随后保存sscratch中a0的值到trapframe
- 再然后依次从p->trapframe中恢复kernel_sp,kernel_hartid,kernel_trap(即usertrap函数的地址),satp
- 上述数据恢复了之后跳转到
usertrap函数
中进行处理
- kernel/trap.c:usertrap
- 在stvec中保存kernelvec函数的地址,因为此时已经在内核中,内核中的trap要用不同的函数处理
- 将sepc保存到trapframe的epc中(sepc是pc的值)
- 从scause中读取trap原因,分配不同的函数进行处理,此例中为syscall
- 首先将p->trapframe->epc加上4,因为之前指向ecall,此时需要它指向下一条指令来执行
- 打开中断,进行syscall系统调用
- kernel/syscall.c:syscall
从a7寄存器
中读出系统调用的标号,将其执行,并将返回值赋值给a0
- kernel/trap.c:usertrapret
- 开始返回过程
- 由于即将返回用户态,因此stvec中要塞入uservec的地址
- 将kernel satp,trap,kernel_sp,kernel_hartid的数据保存到trapframe中
- 从p->trapframe->epc中恢复pc的值到sepc中
- 恢复satp中的值为user pagetable
- 执行userret,从内核态返回用户态
- kernel/trampoline.S:userret
- 恢复user pagetable
- 从trapframe中恢复寄存器的数据
- 保存trapframe的地址到sscratch中
- sret返回用户态,并完成一系列的硬件操作
之后的很多实验都会需要爆改usertrap
函数
同理代码还是得debug一下才能知道这里怎么跑的,debug过程中从用户态跳到内核态是进不了trampoline的,此时得在stvec保存的地址
处打个断点
3. 参考
3.1 老哥1
3.2 老哥2
3.3 xv6 book