其他分享
首页 > 其他分享> > 6.s081 : trap

6.s081 : trap

作者:互联网

Traps

Calling Convention

C数据类型和对齐

QQ20210611-193828@2x.png

在RV32编译器中, int是32bits, longpointerint相同, 都是32bits.

在RV64编译器中, int是32bits, 但longpointer是64bits.

在RV32和RV64, long long是64bits的整数, float是32bits的浮点数, double是64bits的浮点数, long double是128bits的浮点数.

charunsigned char是8-bit, unsigned short是16-bit, 当存储在RISC-V整数寄存器时, 是用0填充的.

signed char是8-bit有符号整数, short是16-bit有符号整数, 当存储在寄存器中时, 是符号填充的.

RVG调用传统

QQ20210611-200835@2x.png

a0-a7: 8个整数寄存器.

fa0-fa7: 8个浮点寄存器.

七个整数寄存器t0-t6和12个浮点寄存器ft0-ft11是只在函数调用有效, 并且, 是caller保存的.

12个整数寄存器s0-s11和12个浮点寄存器fs0-fs11也只在函数调用有效, 并由callee保存.

Lec5

c语言如何转换成汇编.

寄存器是用来进行任何运算和数据读取的最快方式.

所有寄存器都是64bit, 如果有一个32bit整数, 会通过前面补32个0(无符号)或1(有符号)来使这个整数变成64bit并存入寄存器.

image.png

栈的每个区域都是一个stack frame, 每次执行一次函数调用就会产生一个stack frame. 函数通过移动stack pointer来完成stack frame的空间分配.

栈是向下生长的, 创建一个新的stack frame时, 回对当前stack pointer减. stack frame包含着保存的寄存器, 局部变量, 如果函数参数多于8个, 那么多余的就会保存在栈中.

CH4 Traps and system calls

三种事件导致cpu停止运行当前指令, 将控制转移到处理该事件的特殊代码:

  1. 系统调用: user程序运行ecall指令来要求内核完成一些任务.
  2. 异常: user或kernel指令做了一些非法操作.
  3. 设备中断: 设备发出信号说明自己需要得到注意(设备硬件结束读或写).

将这三种情况称为trap. trap是透明的, 也就是说, 代码运行时发生trap, 之后恢复, 代码本身不需要知道发生了什么. 通常的发生顺序是:

  1. trap要求将控制转移到内核.
  2. 内核保存寄存器和其他的状态, 以便trap结束后恢复原代码.
  3. 内核运行正确的处理代码(系统调用的实现代码或设备驱动).
  4. 内核恢复保存的状态并从trap返回.
  5. 起初代码从它被打断的地方恢复.

用作恢复运行的代码的寄存器和状态非常重要:

以上的寄存器只能在supervisor mode处理, 在user mode下不能被读写. 同时, supervisor mode下, 可以使用PTE_U为0的PTE, supervisor mode中的代码不能读写任意物理地址, 也需要通过page table访问内存.

每个cpu都有这些寄存器, 一个trap可以被多个cpu同时处理.

RISC-V硬件对所有trap, 都做以下处理:

  1. 如果trap是设备中断, 并sstatus的SIE bit被清除, 下面几步都不做.
  2. 清除SIE bit.
  3. pc复制到sepc.
  4. sstatus的SPP bit保存目前模式(user/supervisor).
  5. 设置scause来反应trap的原因.
  6. stvec复制到pc.
  7. 在新pc开始运行.

cpu不切换到内核页表, 不切换到内核栈, 不保存任何出了pc的寄存器. 这些都是由内核软件完成的. cpu完成最少的工作为了给软件提供灵活性从而提高效率.

user空间下trap的执行流程

shell中调用write系统调用为例子:

  1. uservec(kernel/trampoline.s)
  2. usertrap(kernel/trap.c)
  3. syscall(kernel/syscall.c)
  4. sys_write(kernel/sysproc.c)
  5. usertrapret(kernel/trap.c)
  6. userret(kernel/trampoline.s)

ecall指令前的状态

QQ20210613-150420@2x.png

wirte函数的实现, 首先将SYS_write加载到a7, 即运行第16个系统调用. 之后执行ecall指令, 从这开始, 代码跳转到内核, 在内核完成任务后, 会继续执行ecall之后的指令ret.

ecall加上断点, 继续运行. 此时pc

QQ20210613-151317@2x.png

此时寄存器内容为

QQ20210613-151453@2x.png

由于pcsp的值都很小, 当前代码运行在user mode下

image.png

image (1).png

此时的用户页表, 只包含6条pte, 第三行是无效页来作为guard page. 最后两条pte非常大, 映射在虚拟地址的顶部, 分别是trampoline page(0x3ffffff000)和trapframe page(0x3fffffe000). 这两条pte都没有设置PTE_U, 所以只能在supervisor mode下访问.

QQ20210613-152309@2x.png

目前还是在user mode下, 接下来要执行ecall, 要进入supervisor mode.

ecall指令之后的状态

QQ20210613-152459@2x.png

执行ecall之后, 目前的pc在0x3ffffff004, 也就是trampoline页处.

image (2).png

此时页表仍然是shell的用户页表, 同时寄存器的值也没变. 在将这些寄存器的值保存之前, 不能使用任何寄存器

QQ20210613-153422@2x.png

内核事先设置好了stvec的内容为0x3ffffff000.

目前已经在supervisor mode下, 由于可以读取trampoline页的内容, 通过ecall走到trampoline页的, ecall实际改变三件事:

  1. ecall将代码从user改到supervisor mode.
  2. ecallpc值存放在sepc寄存器中.

QQ20210613-153759@2x.png

  1. ecall跳转到stvec指向的指令(trampoline).

接下来我们需要:

ecall可以完成以上一些任务, 但为了提供最大灵活性, ecall没有完成.

usrevec函数

由于在RISC-V中, supervisor mode下, 代码不允许直接访问物理内存, 只能使用page table的内容. 由于是在supervisor mode下, 是可以修改satp中的值的, 但当前寄存器都保存着用户寄存器, 所以要腾出寄存器来完成操作.

QQ20210613-155200@2x.png

由于a0sscratch交换, 此时a0保存着trapframe的地址

QQ20210613-161050@2x.png

接下来通过对a0中保存着的地址操作来将user寄存器保存到trapframe中.

QQ20210613-161239@2x.png

保存完寄存器, 仍然uservec中, 接下来需要设置sp.

QQ20210613-162908@2x.png

将a0指向的内存地址+8也就是kernel_sp加载到sp. trapframe中的kernel_sp是由kernel进入用户空间之前设置好的, 它的值是这个进程的kernel stack, 也就是虚拟地址的顶端.

QQ20210613-163142@2x.png

下一条指令是向tp寄存器写入数据. 通过将cpu编号也就是hartid保存在tp寄存器中, 可以来确定当前运行在哪个cpu上.

QQ20210613-163452@2x.png

下一条指令是向t0寄存器写入数据, 这里写入的是我们要执行的第一个c函数的指针, 也就是usertrap.

QQ20210613-163617@2x.png

QQ20210613-163710@2x.png

下一条指令是向t1寄存器写入数据, 这里写入的是kernel page table的地址.

QQ20210613-163815@2x.png

QQ20210613-163919@2x.png

下一条指令是交换satpt1, 这条指令完成后, 程序会从user page table切换到kernel page table.

QQ20210613-164100@2x.png

最后一条指令是从trampoline跳跃到c代码中(t0保存的是usertrap的地址).

QQ20210613-164405@2x.png

所以, 我们以kernel stack, kernel pagetable的状态跳转到usertrap函数.

usertrap函数

usertrap的任务是决定trap的原因, 处理它并返回.

usertrap做的第一件事是更改stvec寄存器. 将stvec指向kernelvec, 这是内核空间trap处理代码的位置.

QQ20210613-165041@2x.png

并且, 需要知道当前的进程, 通过myproc()函数来查找, myproc()会根据当前cpu核的编号hartid(uservec时保存在tp寄存器)找出当前运行的进程.

QQ20210613-165304@2x.png

找到了当前进程, 接下来要保存用户pc, 仍然在sepc中, 但可能会发生这种情况: 当程序还在trap中被处理时, 会切换到另一个进程, 并进入那个进程的用户空间, 那个进程再调用一个系统调用而导致sepc被覆盖. 所以要用trapframe来保存这个pc.

QQ20210613-165651@2x.png

接下来需要找出出发trap的原因. 由于是系统调用, 所以scause=8

QQ20210613-182600@2x.png

所以可以进到if语句中. 接下来会查看是否进程被killed, 如果是, 就直接返回.

QQ20210613-182709@2x.png

由于此时sepc中的值是用户触发trap时的pc, 当我们恢复用户程序时, 希望在下一条指令恢复, 所以对保存的pc加4.

QQ20210613-182903@2x.png

中断会被trap硬件关闭, 所以显示打开中断.

QQ20210613-183121@2x.png

接着就调用syscall函数.

QQ20210613-183302@2x.png

系统调用的参数会存放在a0, a1, ..., 并且会在a7存放系统调用号, 每个系统调用号对应一个系统调用. syscalla7获取系统调用号, 并用其索引syscalls. 运行系统调用并返回.

syscall返回后, 回到usertrap函数, 会再次检查进程是否被killed, 因为不能恢复一个killed进程.

QQ20210613-183815@2x.png

最后usertrap调用usertrapret函数.

usertrapret函数

usertrapret首先会关闭中断. 因为要更新stvec寄存器来指向user space的trap处理代码. 之前在usertrap中, 将stvec指向kernel space的trap处理代码, 而此时我们仍然在内核中, 如果这是发生一个中断, 那么程序指向会走向user space的trap处理代码.

QQ20210613-184656@2x.png

接着, 为了下一次从用户空间转换到内核空间可以用到这些数据, 将其保存到寄存器中.

QQ20210613-185153@2x.png

接下来要修改sstatus寄存器, 其SPP bit控制了sret指令的行为. 如果该位为0, 指向sret时返回到user mode. 其SPIE bit控制了中断是否打开, 由于在usertrapret中关闭了中断, 所以需要打开, 最后将修改的数据写入sstatus寄存器.

QQ20210613-185614@2x.png

由于在trampolinesret会将pc设为sepc寄存器中的值, 所以要把保存在trapframe中的sepc值写入sepc.

QQ20210613-190207@2x.png

接下来由于要进入user space, 所以要根据user page table的地址生成相应的satp值.并将这个指针作为第二个参数传递给汇编代码(trampoline). 而第一个参数就是TRAPFRAME(trapframe的地址). 之后计算出要跳转的汇编代码的地址(trampoline中的userret函数).

QQ20210613-193427@2x.png

userret函数

userret先切换page table, 通过usertrapret传入的第二个参数(保存在a1寄存器中), 将user page table存储在satp寄存器中. 由于user page table也映射了trampoline, 所以程序不会崩溃.

QQ20210613-193601@2x.png

此时, a0的值为trapframe的地址. 112(a0)是trapframe中保存的a0的值. 也就是通过a0的值找出trapframea0的值, 找到这个值后, 将其保存在t0寄存器中, 再将t0sscratch交换.

QQ20210613-194425@2x.png

之后就恢复除a0外的所有寄存器.(trapframe中的a0是执行系统调用的返回值).

QQ20210613-194531@2x.png

除了a0, 用户寄存器都恢复了, 此时a0还保存着trapframe的地址, 接下来要交换sscratch(之前从trapframe中取出a0放入sscratch, 所以sscratch保存的是系统调用的返回值).

QQ20210613-194945@2x.png

交换后, a0保存着的是返回值, sscratch保存着的是trapframe的地址值, 以供下一次trap使用.

sret是最后一条指令, 执行完后会:

标签:kernel,保存,a0,user,寄存器,s081,trap
来源: https://www.cnblogs.com/rainbowg0/p/15089499.html