6.s081 : trap
作者:互联网
Traps
Calling Convention
C数据类型和对齐
在RV32编译器中, int
是32bits, long
和pointer
和int
相同, 都是32bits.
在RV64编译器中, int
是32bits, 但long
和pointer
是64bits.
在RV32和RV64, long long
是64bits的整数, float
是32bits的浮点数, double
是64bits的浮点数, long double
是128bits的浮点数.
char
和unsigned char
是8-bit, unsigned short
是16-bit, 当存储在RISC-V整数寄存器时, 是用0填充的.
signed char
是8-bit有符号整数, short
是16-bit有符号整数, 当存储在寄存器中时, 是符号填充的.
RVG调用传统
a0-a7: 8个整数寄存器.
fa0-fa7: 8个浮点寄存器.
-
如果传递的参数是
struct
, 那么struct
指针对齐. 如果传递的参数是浮点类型, 并且i<8, 那么传递进浮点寄存器fai
(如果浮点参数是union
的部分, 或者是结构的数组, 那么传递进整数寄存器), 如果是整数, 那么传递进整数寄存器ai
. -
小于指针字节大小的参数传递进寄存器的最低有效位.
-
当两倍于指针字节大小的原始参数被传递到栈, 它们本身是对齐的. 当传递进整数寄存器, 它们保存在一个对齐的奇偶寄存器对(奇保存最低有效位). 例如, 在RV32中,
void foo(int, long long)
将int
传递到a0寄存器,long long
传递到a2和a3寄存器中. -
如果参数大于两倍指针字节大小, 是通过引用传递的(
struct
的有的部分没有通过寄存器传递, 那么就通过栈传递, 栈指针sp
指向还没被传递的第一个参数). -
函数返回值保存在整数寄存器a0和a1和浮点寄存器fa0和fa1. 只有当要返回的浮点是只包含一个或两个浮点值的结构时, 在浮点寄存器中返回. 其他返回值如果符合两个指针字节大小就在a0和a1中返回, 如果大于两个, 那么返回值是通过内存传递的, 调用函数会在调用时分配内存并用指针指向这块内存)
-
栈是向下生长的, 栈指针16 byte对齐.
七个整数寄存器t0-t6和12个浮点寄存器ft0-ft11是只在函数调用有效, 并且, 是caller保存的.
12个整数寄存器s0-s11和12个浮点寄存器fs0-fs11也只在函数调用有效, 并由callee保存.
Lec5
c语言如何转换成汇编.
- 处理器不能理解c语言, 处理器能够理解二进制编码后的汇编.
- 每个处理器都有一个关联的ISA(Instruction Sets Architecture).
- 要让c语言运行到处理器上, 首先要写出c程序, 之后c程序需要被编译成汇编, 之后汇编会被翻译成二进制文件(.obj或.o).
寄存器是用来进行任何运算和数据读取的最快方式.
- callee saved: 寄存器在函数调用时不会保存.
- caller saved: 寄存器在函数调用时会保存(可以被其他被调用函数重写).
所有寄存器都是64bit, 如果有一个32bit整数, 会通过前面补32个0(无符号)或1(有符号)来使这个整数变成64bit并存入寄存器.
栈的每个区域都是一个stack frame, 每次执行一次函数调用就会产生一个stack frame. 函数通过移动stack pointer来完成stack frame的空间分配.
栈是向下生长的, 创建一个新的stack frame时, 回对当前stack pointer减. stack frame包含着保存的寄存器, 局部变量, 如果函数参数多于8个, 那么多余的就会保存在栈中.
-
返回值保存在stack frame的第一位
-
指向前一个stack frame的指针保存在栈的固定位置(返回值之后)
-
sp(stack pointer)
指向栈的底部并表示当前stack frame的位置 -
fp(frame pointer)
指向当前stack frame的顶部
CH4 Traps and system calls
三种事件导致cpu停止运行当前指令, 将控制转移到处理该事件的特殊代码:
- 系统调用: user程序运行
ecall
指令来要求内核完成一些任务. - 异常: user或kernel指令做了一些非法操作.
- 设备中断: 设备发出信号说明自己需要得到注意(设备硬件结束读或写).
将这三种情况称为trap
. trap
是透明的, 也就是说, 代码运行时发生trap
, 之后恢复, 代码本身不需要知道发生了什么. 通常的发生顺序是:
trap
要求将控制转移到内核.- 内核保存寄存器和其他的状态, 以便
trap
结束后恢复原代码. - 内核运行正确的处理代码(系统调用的实现代码或设备驱动).
- 内核恢复保存的状态并从
trap
返回. - 起初代码从它被打断的地方恢复.
用作恢复运行的代码的寄存器和状态非常重要:
sp
(stack pointer): 指向栈的指针.pc
(program counter): 程序计数器.- 表明当前mode的标志位, 这个标志位表明当前是supervisor mode还是user mode.
satp
(supervisor address traslation and protection): 包含了指向pagetable的物理内存地址.stvec
(supervisor trap vector base address register): 指向内核中处理trap的指令的起始地址.sepc
(supervisor exception program counter): 在trap中保存程序计数器的值(pc之后会被stvec重写). sret指令将sepc复制到pc来从trap返回.sscratch
(supervisor scratch register): 内核会在其存放一个值, 以便于trap开始运行.sstatus
: sstatus中的SIE bit控制硬件中断是否开启, SPP bit表明trap是发生在user mode还是supervisor mode, 并控制sret返回到什么模式.scause
: trap发生的原因.
以上的寄存器只能在supervisor mode处理, 在user mode下不能被读写. 同时, supervisor mode下, 可以使用PTE_U
为0的PTE, supervisor mode中的代码不能读写任意物理地址, 也需要通过page table访问内存.
每个cpu都有这些寄存器, 一个trap
可以被多个cpu同时处理.
RISC-V硬件对所有trap
, 都做以下处理:
- 如果
trap
是设备中断, 并sstatus
的SIE bit被清除, 下面几步都不做. - 清除SIE bit.
- 将
pc
复制到sepc
. - 在
sstatus
的SPP bit保存目前模式(user/supervisor). - 设置
scause
来反应trap
的原因. - 将
stvec
复制到pc
. - 在新
pc
开始运行.
cpu不切换到内核页表, 不切换到内核栈, 不保存任何出了pc
的寄存器. 这些都是由内核软件完成的. cpu完成最少的工作为了给软件提供灵活性从而提高效率.
user空间下trap
的执行流程
以shell
中调用write
系统调用为例子:
uservec
(kernel/trampoline.s)usertrap
(kernel/trap.c)syscall
(kernel/syscall.c)sys_write
(kernel/sysproc.c)usertrapret
(kernel/trap.c)userret
(kernel/trampoline.s)
ecall
指令前的状态
wirte
函数的实现, 首先将SYS_write
加载到a7
, 即运行第16个系统调用. 之后执行ecall
指令, 从这开始, 代码跳转到内核, 在内核完成任务后, 会继续执行ecall
之后的指令ret
.
给ecall
加上断点, 继续运行. 此时pc
为
此时寄存器内容为
由于pc
和sp
的值都很小, 当前代码运行在user mode下
此时的用户页表, 只包含6条pte, 第三行是无效页来作为guard page. 最后两条pte非常大, 映射在虚拟地址的顶部, 分别是trampoline
page(0x3ffffff000)和trapframe
page(0x3fffffe000). 这两条pte都没有设置PTE_U
, 所以只能在supervisor mode下访问.
目前还是在user mode下, 接下来要执行ecall
, 要进入supervisor mode.
ecall指令之后的状态
执行ecall
之后, 目前的pc
在0x3ffffff004, 也就是trampoline
页处.
此时页表仍然是shell
的用户页表, 同时寄存器的值也没变. 在将这些寄存器的值保存之前, 不能使用任何寄存器
内核事先设置好了stvec
的内容为0x3ffffff000.
目前已经在supervisor mode下, 由于可以读取trampoline
页的内容, 通过ecall
走到trampoline
页的, ecall
实际改变三件事:
ecall
将代码从user改到supervisor mode.ecall
将pc
值存放在sepc
寄存器中.
ecall
跳转到stvec
指向的指令(trampoline
).
接下来我们需要:
- 保存32个用户寄存器的内容.
- 切换到kernel page table.
- 创建一个kernel stack, 并将
sp
(stack pointer)指向那个kernel stack. - 跳转到内核c代码中的某些位置(trap.c).
ecall
可以完成以上一些任务, 但为了提供最大灵活性, ecall
没有完成.
usrevec
函数
由于在RISC-V中, supervisor mode下, 代码不允许直接访问物理内存, 只能使用page table的内容. 由于是在supervisor mode下, 是可以修改satp
中的值的, 但当前寄存器都保存着用户寄存器, 所以要腾出寄存器来完成操作.
-
第一个任务是腾出一个寄存器来完成一些操作. 通过
trampoline
开头的csrrw
指令, 将a0
寄存器中的内容和sscratch
寄存器中的内容交换, 现在,a0
中的内容已经保存下来了,uservec
可以通过操作a0
来完成一些任务. 同时,sscratch
中的值保存在a0
,sscratch
的值在内核进入user space之前, 会将trapframe
中的地址(0x3fffffe000)保存在sscratch
中.这是返回到用户空间之前执行的最后两条指令, 会将
a0
和sscratch
的值交换,a0
又是通过传递参数获取trapframe
页的地址.这是内核返回到用户空间最后的c函数, c函数的最后一件事是调用
fn
函数, 传递TRAPFRAME
(trapframe
的地址, 保存在a0
)和user page table(保存在a1
).fn
就是trampoline
中的代码. -
xv6在每个user page table都映射了
trapframe
页(0x3fffffe000), 每个进程都有自己的trapframe
页.
由于a0
和sscratch
交换, 此时a0
保存着trapframe
的地址
接下来通过对a0
中保存着的地址操作来将user
寄存器保存到trapframe
中.
保存完寄存器, 仍然uservec
中, 接下来需要设置sp
.
将a0指向的内存地址+8也就是kernel_sp
加载到sp
. trapframe
中的kernel_sp
是由kernel进入用户空间之前设置好的, 它的值是这个进程的kernel stack, 也就是虚拟地址的顶端.
下一条指令是向tp
寄存器写入数据. 通过将cpu编号也就是hartid
保存在tp
寄存器中, 可以来确定当前运行在哪个cpu上.
下一条指令是向t0
寄存器写入数据, 这里写入的是我们要执行的第一个c函数的指针, 也就是usertrap
.
下一条指令是向t1
寄存器写入数据, 这里写入的是kernel page table的地址.
下一条指令是交换satp
和t1
, 这条指令完成后, 程序会从user page table切换到kernel page table.
最后一条指令是从trampoline
跳跃到c代码中(t0
保存的是usertrap
的地址).
所以, 我们以kernel stack, kernel pagetable的状态跳转到usertrap
函数.
usertrap
函数
usertrap
的任务是决定trap
的原因, 处理它并返回.
usertrap
做的第一件事是更改stvec
寄存器. 将stvec
指向kernelvec
, 这是内核空间trap
处理代码的位置.
并且, 需要知道当前的进程, 通过myproc()
函数来查找, myproc()
会根据当前cpu核的编号hartid
(uservec
时保存在tp
寄存器)找出当前运行的进程.
找到了当前进程, 接下来要保存用户pc
, 仍然在sepc
中, 但可能会发生这种情况: 当程序还在trap
中被处理时, 会切换到另一个进程, 并进入那个进程的用户空间, 那个进程再调用一个系统调用而导致sepc
被覆盖. 所以要用trapframe
来保存这个pc
.
接下来需要找出出发trap
的原因. 由于是系统调用, 所以scause=8
所以可以进到if语句中. 接下来会查看是否进程被killed, 如果是, 就直接返回.
由于此时sepc
中的值是用户触发trap
时的pc
, 当我们恢复用户程序时, 希望在下一条指令恢复, 所以对保存的pc
加4.
中断会被trap
硬件关闭, 所以显示打开中断.
接着就调用syscall
函数.
系统调用的参数会存放在a0
, a1
, ..., 并且会在a7
存放系统调用号, 每个系统调用号对应一个系统调用. syscall
从a7
获取系统调用号, 并用其索引syscalls
. 运行系统调用并返回.
从syscall
返回后, 回到usertrap
函数, 会再次检查进程是否被killed, 因为不能恢复一个killed进程.
最后usertrap
调用usertrapret
函数.
usertrapret
函数
usertrapret
首先会关闭中断. 因为要更新stvec
寄存器来指向user space的trap
处理代码. 之前在usertrap
中, 将stvec
指向kernel space的trap
处理代码, 而此时我们仍然在内核中, 如果这是发生一个中断, 那么程序指向会走向user space的trap
处理代码.
接着, 为了下一次从用户空间转换到内核空间可以用到这些数据, 将其保存到寄存器中.
- 存储了kernel page table的指针.
- 存储了当前用户进程的kernel stack.
- 存储了
usertrap
函数指针, 这样trampoline
会跳转到这个函数(通过写入t0
寄存器). - 从
tp
寄存器读取当前cpu编号(hartid
), 并存储到trapframe
中.
接下来要修改sstatus
寄存器, 其SPP bit控制了sret
指令的行为. 如果该位为0, 指向sret
时返回到user mode. 其SPIE bit控制了中断是否打开, 由于在usertrapret
中关闭了中断, 所以需要打开, 最后将修改的数据写入sstatus
寄存器.
由于在trampoline
的sret
会将pc
设为sepc
寄存器中的值, 所以要把保存在trapframe
中的sepc
值写入sepc
.
接下来由于要进入user space, 所以要根据user page table的地址生成相应的satp
值.并将这个指针作为第二个参数传递给汇编代码(trampoline
). 而第一个参数就是TRAPFRAME
(trapframe
的地址). 之后计算出要跳转的汇编代码的地址(trampoline
中的userret
函数).
userret
函数
userret
先切换page table, 通过usertrapret
传入的第二个参数(保存在a1
寄存器中), 将user page table存储在satp
寄存器中. 由于user page table也映射了trampoline
, 所以程序不会崩溃.
此时, a0
的值为trapframe
的地址. 112(a0
)是trapframe
中保存的a0
的值. 也就是通过a0
的值找出trapframe
中a0
的值, 找到这个值后, 将其保存在t0
寄存器中, 再将t0
和sscratch
交换.
之后就恢复除a0
外的所有寄存器.(trapframe
中的a0
是执行系统调用的返回值).
除了a0
, 用户寄存器都恢复了, 此时a0
还保存着trapframe
的地址, 接下来要交换sscratch
(之前从trapframe
中取出a0
放入sscratch
, 所以sscratch
保存的是系统调用的返回值).
交换后, a0
保存着的是返回值, sscratch
保存着的是trapframe
的地址值, 以供下一次trap
使用.
sret
是最后一条指令, 执行完后会:
- 程序切换回user mode(之前
sstatus
中设置了SPP bit为0, 说明返回user mode). SEPC
复制到pc
.- 重新打开中断.
标签:kernel,保存,a0,user,寄存器,s081,trap 来源: https://www.cnblogs.com/rainbowg0/p/15089499.html