什么是trap? trap:CPU暂时搁置普通指令的执行,强制将控制权转移到处理该事件的特殊代码上,以下三种情况都会出现trap:
系统调用:用户程序执行ecall(用户程序不直接使用设备)
异常:用户或内核执行非法的事情(杀死违规例程)
设备中断:磁盘读写完成中断、时钟中断(用户程序不直接使用设备)
trap的一些说明
trap发生时正在执行的代码随后需要恢复
trap对于正在执行的代码是透明的
trap是怎么执行的? trap的逻辑流程
trap将控制权转移到内核
内核保存寄存器和其他状态
内核执行适当的处理程序代码
内核恢复保存的状态并从trap中返回
原始代码从它停止的地方恢复
trap的实际流程
RISC-V CPU采取的硬件操作
为内核C代码执行而准备的汇编程序集“向量”
决定如何处理陷阱的C陷阱处理程序
系统调用或设备驱动程序服务例程
trap的举例说明 初始 xv6 启动后,运行第一个程序 shell , shell 运行在用户态,想要输出一些字符串到命令行时,执行系统调用 wirte 切换到内核,输出字符串之后再切换回用户态
trap的具体过程(gdb调试说明) 以shell的write系统调用为例,打印$到命令行窗口
1 b *0x3ffffff000 # 设置断点在陷入帧
在 sh.asm 寻找 write 系统调用开始的地址,我的为 df4 ,添加断点 b *0xdf4
1 2 3 4 5 6 7 8 9 10 11 # ... 0000000000000df4 <write>: .global write write: li a7, SYS_write df4: 48c1 li a7,16 ecall df6: 00000073 ecall ret dfa: 8082 ret # ...
ecall之前 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 (gdb) c (gdb) i r pc pc 0xdf4 0xdf4 (gdb) x/3i $pc => 0xdf4: li a7,16 0xdf6: ecall 0xdfa: ret (gdb) i r a0 a0 0x2 2 (gdb) x/s $a1 0x3edf: "$@ ?" (gdb) i r a2 a2 0x1 1 (gdb) print /x $satp $1 = 0x8000000000087f63(gdb) si (gdb) x/3i 0xdf4 0xdf4: li a7,16 => 0xdf6: ecall 0xdfa: ret
ecall干的事情 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 (gdb) i r stvec stvec 0x3ffffff000 274877902848 (gdb) b *0x3ffffff000 Breakpoint 2 at 0x3ffffff000 (gdb) si Breakpoint 2, 0x0000003ffffff000 in ?? () (gdb) x/10i 0x0000003ffffff000 => 0x3ffffff000: csrrw a0,sscratch,a0 0x3ffffff004: sd ra,40(a0) 0x3ffffff008: sd sp,48(a0) 0x3ffffff00c: sd gp,56(a0) 0x3ffffff010: sd tp,64(a0) 0x3ffffff014: sd t0,72(a0) 0x3ffffff018: sd t1,80(a0) 0x3ffffff01c: sd t2,88(a0) 0x3ffffff020: sd s0,96(a0) 0x3ffffff022: sd s1,104(a0) (gdb) i r a7 a7 0x10 16 (gdb) i r pc pc 0x3ffffff000 0x3ffffff000 (gdb) i r sepc sepc 0xdf6 3574
ecall后的uservec函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 (gdb) si 0x0000003ffffff004 in ?? () (gdb) x/5i 0x0000003ffffff000 0x3ffffff000: csrrw a0,sscratch,a0 => 0x3ffffff004: sd ra,40(a0) 0x3ffffff008: sd sp,48(a0) 0x3ffffff00c: sd gp,56(a0) 0x3ffffff010: sd tp,64(a0) (gdb) i r sscratch sscratch 0x2 2 (gdb) i r a0 a0 0x3fffffe000 274877898752 (gdb) x/5i 0x0000003ffffff000 0x3ffffff000: csrrw a0,sscratch,a0 0x3ffffff004: sd ra,40(a0) => 0x3ffffff008: sd sp,48(a0) 0x3ffffff00c: sd gp,56(a0) 0x3ffffff010: sd tp,64(a0) (gdb) i r ra ra 0xe9e 0xe9e (gdb) si 0x0000003ffffff008 in ?? () (gdb) x/10g $a0 0x3fffffe000: 0x8000000000087fff 0x0000003fffffc000 0x3fffffe010: 0x00000000800028d4 0x0000000000000e02 0x3fffffe020: 0x0000000000000000 0x0000000000000e9e 0x3fffffe030: 0x0000000000003fb0 0x0505050505050505 0x3fffffe040: 0x0505050505050505 0x0505050505050505 (gdb) x/10i $pc -4 0x3ffffff076: ld sp,8(a0) => 0x3ffffff07a: ld tp,32(a0) 0x3ffffff07e: ld t0,16(a0) 0x3ffffff082: ld t1,0(a0) 0x3ffffff086: csrw satp,t1 (gdb) x/10i $pc -4 0x3ffffff086: csrw satp,t1 => 0x3ffffff08a: sfence.vma 0x3ffffff08e: jr t0 (gdb) i r satp satp 0x8000000000087fff -9223372036854218753 (gdb) si usertrap () at kernel/trap.c:38 38 {
ecall后的usertrap函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 (gdb) tui enable C: w_stvec((uint64)kernelvec); # 保存用户程序计数器 C: p->trapframe->epc = r_sepc(); # 查看scause寄存器,确定触发trap的原因 (gdb) i r scause scause 0 x8 8 # 表示当前为系统调用 # 返回用户态时,程序计数器要加4 表示下一条指令 C:p->trapframe->epc += 4 ; # 开中断 说明ecall关闭了中断 C:intr_on(); # 执行到syscall里面 # 读取a7寄存器 C:num = p->trapframe->a7; (gdb) print num $2 = 16 # 对应的系统调用是sys_write(gdb) print p->trapframe->a0 $8 = 2 (gdb) print p->trapframe->a1 $9 = 16095 (gdb) print p->trapframe->a2 $10 = 1 # 从syscall返回到usertrap # 进入usertrapret
ecall后的usertrapret函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 C:intr_off(); C:w_stvec(TRAMPOLINE + (uservec - trampoline)); C:p->trapframe->kernel_satp = r_satp(); C:p->trapframe->kernel_sp = p->kstack + PGSIZE; C:p->trapframe->kernel_trap = (uint64)usertrap; C:p->trapframe->kernel_hartid = r_tp(); C:unsigned long x = r_sstatus(); C:x &= ~SSTATUS_SPP; C:x |= SSTATUS_SPIE; C:w_sstatus(x); C:w_sepc(p->trapframe->epc); C:uint64 satp = MAKE_SATP(p->pagetable); C:uint64 fn = TRAMPOLINE + (userret - trampoline); C:((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
ecall后的userret函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 (gdb) x/5i $pc => 0x3ffffff090: csrw satp,a1 0x3ffffff094: sfence.vma 0x3ffffff098: ld t0,112(a0) 0x3ffffff09c: csrw sscratch,t0 (gdb) x/5i $pc -8 0x3ffffff106: ld t6,280(a0) 0x3ffffff10a: csrrw a0,sscratch,a0 => 0x3ffffff10e: sret (gdb) i r sscratch sscratch 0x3fffffe000 274877898752 (gdb) i r a0 a0 0x1 1
qemu查看页表 在另外 qemu 窗口按 ctrl+a 然后按 c ,进入 qemu 界面,然后查看页表(目前是用户页表)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 (qemu) info mem vaddr paddr size attr ---------------- ---------------- ---------------- ------- 0000000000000000 0000000087f60000 0000000000001000 rwxu-a- 0000000000001000 0000000087f5d000 0000000000001000 rwxu-a- 0000000000002000 0000000087f5c000 0000000000001000 rwx---- 0000000000003000 0000000087f5b000 0000000000001000 rwxu-ad 0000003fffffe000 0000000087f6f000 0000000000001000 rw---ad 0000003ffffff000 0000000080007000 0000000000001000 r-x--a- (qemu) (qemu) info mem vaddr paddr size attr ---------------- ---------------- ---------------- ------- 0000000002000000 0000000002000000 0000000000010000 rw----- 000000000c000000 000000000c000000 0000000000001000 rw---ad 000000000c001000 000000000c001000 0000000000001000 rw----- 000000000c002000 000000000c002000 0000000000001000 rw---ad 000000000c003000 000000000c003000 00000000001fe000 rw----- 000000000c201000 000000000c201000 0000000000001000 rw---ad 000000000c202000 000000000c202000 00000000001fe000 rw----- 0000000010000000 0000000010000000 0000000000002000 rw---ad 0000000080000000 0000000080000000 0000000000007000 r-x--a- 0000000080007000 0000000080007000 0000000000001000 r-x---- 0000000080008000 0000000080008000 0000000000003000 rw---ad 000000008000b000 000000008000b000 0000000000006000 rw----- 0000000080011000 0000000080011000 0000000000012000 rw---ad 0000000080023000 0000000080023000 0000000000001000 rw----- 0000000080024000 0000000080024000 0000000000003000 rw---ad 0000000080027000 0000000080027000 0000000007f34000 rw----- 0000000087f5b000 0000000087f5b000 000000000005d000 rw---ad 0000000087fb8000 0000000087fb8000 0000000000001000 rw---a- 0000000087fb9000 0000000087fb9000 0000000000046000 rw----- 0000000087fff000 0000000087fff000 0000000000001000 rw---a- 0000003ffff7f000 0000000087f77000 000000000003e000 rw----- 0000003fffffb000 0000000087fb5000 0000000000002000 rw---ad 0000003ffffff000 0000000080007000 0000000000001000 r-x--a- (qemu) (qemu) info mem vaddr paddr size attr ---------------- ---------------- ---------------- ------- 0000000000000000 0000000087f5e000 0000000000001000 rwxu-a- 0000000000001000 0000000087f5b000 0000000000001000 rwxu-a- 0000000000002000 0000000087f5a000 0000000000001000 rwx---- 0000000000003000 0000000087f59000 0000000000001000 rwxu-ad 0000003fffffe000 0000000087f6e000 0000000000001000 rw---ad 0000003ffffff000 0000000080007000 0000000000001000 r-x--a- (qemu)
gdb调试总结 一些重要寄存器概述
stvec
:内核在这里写入其陷阱处理程序的地址;RISC-V跳转到这里处理陷阱。
sepc
:当发生陷阱时,RISC-V会在这里保存程序计数器pc
(因为pc
会被stvec
覆盖)。sret
(从陷阱返回)指令会将sepc
复制到pc
。内核可以写入sepc
来控制sret
的去向。
scause
: RISC-V在这里放置一个描述陷阱原因的数字。
sscratch
:内核在这里放置了一个值,这个值在陷阱处理程序一开始就会派上用场。存放trapframe的值
sstatus
:其中的SIE 位控制设备中断是否启用。如果内核清空SIE ,RISC-V将推迟设备中断,直到内核重新设置SIE 。SPP 位指示陷阱是来自用户模式还是管理模式,并控制sret
返回的模式
ecall干的三件事情,ecall是CPU的指令,gdb调试看不到具体指令
user mode –> supervisor mode
保存程序计数器到sepc寄存器
跳转到stvec寄存器指向的指令
关闭中断
ecall后的uservec函数干的事情
获取trapframe的值:交换a0和sscratch(开始保存现场,在此之前,寄存器还是用户态的值)
保存现场
加载trapframe开头低地址的一些寄存器
内核栈、kernel_tp、usertrap指针、satp
切换到内核页,清空tlb
跳转到usertrap
ecall后的usertrap函数干的事情
更改STVEC寄存器
保存用户程序计数器
查看scause寄存器,确定触发trap的原因
返回用户态时,程序计数器要加4表示下一条指令
开中断 说明ecall关闭了中断
执行到syscall里面,从syscall返回到usertrap
进入usertrapret
ecall后的usertrapret函数干的事情
关中断
更新stvec为用户空间的trap处理代码 trampoline
存内核寄存器
内核栈、kernel_tp、usertrap指针、satp
设置sstatus寄存器,它的spp bit控制sret指令返回mode,spie bit控制了是否打开中断
设置sepc的值
读取satp的值,并存到a1寄存器
ecall后的userret函数干的事情
第一步切换页表
取回系统调用返回值 # 交换a0和sscratch,a0保存返回值,ssctratch保存trapframe
执行sret,返回用户态
sret干的事情
切换回用户态
sepc寄存器的值拷贝到pc
重新打开中断
参考: b站调试教程 中文讲义 中文risc-v book