6.s081 : trap

Traps

Calling Convention

C数据类型和对齐

6.s081 : trap

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

6.s081 : trap

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并存入寄存器.

6.s081 : trap

栈的每个区域都是一个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指令前的状态

6.s081 : trap

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

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

6.s081 : trap

此时寄存器内容为

6.s081 : trap

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

6.s081 : trap

6.s081 : trap

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

6.s081 : trap

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

ecall指令之后的状态

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

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

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

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

6.s081 : trap

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

    6.s081 : trap

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

    6.s081 : trap

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

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

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

6.s081 : trap

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

6.s081 : trap

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

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

usertrap函数

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

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

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

接着就调用syscall函数.

6.s081 : trap

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

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

6.s081 : trap

最后usertrap调用usertrapret函数.

usertrapret函数

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

6.s081 : trap

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

  • 存储了kernel page table的指针.
  • 存储了当前用户进程的kernel stack.
  • 存储了usertrap函数指针, 这样trampoline会跳转到这个函数(通过写入t0寄存器).
  • tp寄存器读取当前cpu编号(hartid), 并存储到trapframe中.

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

userret函数

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

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

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

6.s081 : trap

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

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

  • 程序切换回user mode(之前sstatus中设置了SPP bit为0, 说明返回user mode).
  • SEPC复制到pc.
  • 重新打开中断.
上一篇:2021秋软工实践 Lab6 团队展示与选题报告


下一篇:centos8.2 时间同步