什么是trap?

trap:CPU暂时搁置普通指令的执行,强制将控制权转移到处理该事件的特殊代码上,以下三种情况都会出现trap:

  • 系统调用:用户程序执行ecall(用户程序不直接使用设备)
  • 异常:用户或内核执行非法的事情(杀死违规例程)
  • 设备中断:磁盘读写完成中断、时钟中断(用户程序不直接使用设备)

trap的一些说明

  • trap发生时正在执行的代码随后需要恢复
  • trap对于正在执行的代码是透明的

trap是怎么执行的?

trap的逻辑流程

  • trap将控制权转移到内核
  • 内核保存寄存器和其他状态
  • 内核执行适当的处理程序代码
  • 内核恢复保存的状态并从trap中返回
  • 原始代码从它停止的地方恢复

trap的实际流程

  • RISC-V CPU采取的硬件操作
  • 为内核C代码执行而准备的汇编程序集“向量”
  • 决定如何处理陷阱的C陷阱处理程序
  • 系统调用或设备驱动程序服务例程

trap的举例说明

初始 xv6 启动后,运行第一个程序 shell , shell 运行在用户态,想要输出一些字符串到命令行时,执行系统调用 wirte 切换到内核,输出字符串之后再切换回用户态
image-20250524153035067

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
############# ecall之前 ############
# 运行到断点处
(gdb) c
# 查看指令指针,确实运行到了断点处
(gdb) i r pc
pc 0xdf4 0xdf4
# 查看pc下三行命令
(gdb) x/3i $pc
=> 0xdf4: li a7,16
0xdf6: ecall
0xdfa: ret
# 查看a0的值
(gdb) i r a0
a0 0x2 2
# 查看a1对应字符串
(gdb) x/s $a1
0x3edf: "$@?"
# 查看a2对应的值
(gdb) i r a2
a2 0x1 1
# 系统调用的三个参数
# 上述三行表示 想要写入一个$符号到命令行(2)
# 查看satp的值(第一次查看info mem 具体内容见下面qemu部分)
(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
############# ecall干的事情 ############
# ecall干的三件事情,ecall是CPU的指令,gdb调试看不到具体指令
# user mode --> supervisor mode
# 保存程序计数器到sepc寄存器
# 跳转到stvec寄存器指向的指令

# 查看stvec寄存器的值,stvec记录了trampoline的位置
(gdb) i r stvec
stvec 0x3ffffff000 274877902848
# 在上述地址加断点,进入ecall里面,目前在trampoline page
(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)
# 查看a7寄存器,存放了系统调用号,16表示sys_write
(gdb) i r a7
a7 0x10 16
# 现在是supervisor mode pc的值设为了stvec的值
(gdb) i r pc
pc 0x3ffffff000 0x3ffffff000
# 将原来的pc保存到了sepc里面
(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
############# ecall后的uservec函数 ############
# 交换a0和sscratch(开始保存现场,在此之前,寄存器还是用户态的值)
(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指向tramframe page
a0 0x3fffffe000 274877898752
# 装载ra到a0的偏移40的位置
(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 # 现在这个存放了0xe9e
0x3fffffe030: 0x0000000000003fb0 0x0505050505050505
0x3fffffe040: 0x0505050505050505 0x0505050505050505
# 一些保存寄存器
# 加载trapframe开头低地址的一些寄存器
(gdb) x/10i $pc-4
0x3ffffff076: ld sp,8(a0) # 加载内核栈
=> 0x3ffffff07a: ld tp,32(a0) # 加载kernel_tp
0x3ffffff07e: ld t0,16(a0) # 加载usertrap指针
0x3ffffff082: ld t1,0(a0) # 加载satp
0x3ffffff086: csrw satp,t1 # 写到satp
# 继续单步执行,进入内核页(可查看内核页表已经出现,第二次查看info mem)
# 切换到内核页之后,trampoline的映射不变
(gdb) x/10i $pc-4
0x3ffffff086: csrw satp,t1
=> 0x3ffffff08a: sfence.vma
0x3ffffff08e: jr t0
(gdb) i r satp
satp 0x8000000000087fff -9223372036854218753
# t0是usertrap的地址,指向到这一步跳到usertrap,继续走到内核态 usertrap,内核C代码
(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
############# ecall后的usertrap函数 ############
(gdb) tui enable # 打开源码对照
# 更改STVEC寄存器
C: w_stvec((uint64)kernelvec);
# 保存用户程序计数器
C: p->trapframe->epc = r_sepc();
# 查看scause寄存器,确定触发trap的原因
(gdb) i r scause
scause 0x8 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
############# ecall后的usertrapret函数 ############
# 关中断
C:intr_off();
# 更新stvec为用户空间的trap处理代码 trampoline
C:w_stvec(TRAMPOLINE + (uservec - trampoline));
# 存内核寄存器
C:p->trapframe->kernel_satp = r_satp(); # kernel page table
C:p->trapframe->kernel_sp = p->kstack + PGSIZE; # process's kernel stack
C:p->trapframe->kernel_trap = (uint64)usertrap;
C:p->trapframe->kernel_hartid = r_tp(); # hartid for cpuid()
# 设置sstatus寄存器,它的spp bit控制sret指令返回mode,spie bit控制了是否打开中断
C:unsigned long x = r_sstatus();
C:x &= ~SSTATUS_SPP;
C:x |= SSTATUS_SPIE;
C:w_sstatus(x); # 写入sstatus
# 设置sepc的值
C:w_sepc(p->trapframe->epc);
# 读取satp的值,并存到a1寄存器
C:uint64 satp = MAKE_SATP(p->pagetable);
C:uint64 fn = TRAMPOLINE + (userret - trampoline); # 计算跳转的汇编地址
C:((void (*)(uint64,uint64))fn)(TRAPFRAME, satp); # 传递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
############# ecall后的userret函数 ############
# 回到trampoline的userret函数
# 第一步切换页表(第三次查看info mem)
(gdb) x/5i $pc
=> 0x3ffffff090: csrw satp,a1
0x3ffffff094: sfence.vma # 刷新tlb
0x3ffffff098: ld t0,112(a0) # 取实际的系统调用返回的a0,注意不是当前a0寄存器的值
0x3ffffff09c: csrw sscratch,t0 # 将a0返回值写入到sscratch,a0寄存器当前的值是trapframe
# 从trapframe恢复用户寄存器

# 取回系统调用返回值 # 交换a0和sscratch,a0保存返回值,ssctratch保存trapframe
(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
# 执行sret,返回用户态
# 切换回用户态
# sepc寄存器的值拷贝到pc
# 重新打开中断

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
# 第一次查看info mem
(qemu) info mem
vaddr paddr size attr # rwx表示可读、写、执行
---------------- ---------------- ---------------- ------- # u表示用户态可以访问
0000000000000000 0000000087f60000 0000000000001000 rwxu-a- # a表示当前pte是否使用过
0000000000001000 0000000087f5d000 0000000000001000 rwxu-a- # d表示pte是否写过
0000000000002000 0000000087f5c000 0000000000001000 rwx----
0000000000003000 0000000087f5b000 0000000000001000 rwxu-ad # gaurd page
0000003fffffe000 0000000087f6f000 0000000000001000 rw---ad # trapframe page
0000003ffffff000 0000000080007000 0000000000001000 r-x--a- # trampline page
(qemu)

# 第二次查看info mem,内核态
(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)

# 第三次查看info mem
(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将推迟设备中断,直到内核重新设置SIESPP位指示陷阱是来自用户模式还是管理模式,并控制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