首页 > 其他 > 详细

6.s081 : trap

时间:2021-08-02 14:46:33      阅读:21      评论:0      收藏:0      [点我收藏+]

Traps

Calling Convention

C数据类型和对齐

技术分享图片

在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调用传统

技术分享图片

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停止运行当前指令, 将控制转移到处理该事件的特殊代码:

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

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

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

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

  • 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, 都做以下处理:

  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指令前的状态

技术分享图片

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

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

技术分享图片

此时寄存器内容为

技术分享图片

由于pcsp的值都很小, 当前代码运行在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实际改变三件事:

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

技术分享图片

  1. 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中.

    技术分享图片

    这是返回到用户空间之前执行的最后两条指令, 会将a0sscratch的值交换, a0又是通过传递参数获取trapframe页的地址.

    技术分享图片

    这是内核返回到用户空间最后的c函数, c函数的最后一件事是调用fn函数, 传递TRAPFRAME(trapframe的地址, 保存在a0)和user page table(保存在a1). fn就是trampoline中的代码.

  • xv6在每个user page table都映射了trapframe页(0x3fffffe000), 每个进程都有自己的trapframe页.

技术分享图片

由于a0sscratch交换, 此时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的地址.

技术分享图片

技术分享图片

下一条指令是交换satpt1, 这条指令完成后, 程序会从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存放系统调用号, 每个系统调用号对应一个系统调用. syscalla7获取系统调用号, 并用其索引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寄存器.

技术分享图片

由于在trampolinesret会将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的值找出trapframea0的值, 找到这个值后, 将其保存在t0寄存器中, 再将t0sscratch交换.

技术分享图片

之后就恢复除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.
  • 重新打开中断.

6.s081 : trap

原文:https://www.cnblogs.com/rainbowg0/p/15089499.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!