1. lab1
堆栈:在大多数系统中,堆栈从高地址向低地址增长。那么 addi sp, sp, -16 就是将堆栈指针sp向下移动16个字节,为新的函数调用创建空间。
s0 ,也就是fp, 是帧指针:用于保存上一个函数(也就是调用当前函数的函数)的堆栈帧的位置。这样在当前函数结束返回时,就可以恢复到上一个函数的堆栈环境
user/_call: file format elf64-littleriscv
Disassembly of section .text:
0000000000000000 <g>:
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int g(int x) {
0: 1141 addi sp,sp,-16 #开始一个新的栈帧
2: e422 sd s0,8(sp) #保存旧的帧指针,将s0寄存器中的内容存储到以sp为基地址,偏移量为8字节的内存地址
4: 0800 addi s0,sp,16 #更新新的帧指针
return x+3;
}
6: 250d addiw a0,a0,3 #x=x+3
8: 6422 ld s0,8(sp) #恢复旧的帧指针
a: 0141 addi sp,sp,16 #结束这个栈帧
c: 8082 ret #返回
000000000000000e <f>:
int f(int x) { #f函数调用lg函数,因此f函数的汇编代码和g函数一致
e: 1141 addi sp,sp,-16
10: e422 sd s0,8(sp)
12: 0800 addi s0,sp,16
return g(x);
}
14: 250d addiw a0,a0,3
16: 6422 ld s0,8(sp)
18: 0141 addi sp,sp,16
1a: 8082 ret
000000000000001c <main>:
void main(void) {
1c: 1141 addi sp,sp,-16
1e: e406 sd ra,8(sp)
20: e022 sd s0,0(sp)
22: 0800 addi s0,sp,16
printf("%d %d\n", f(8)+1, 13);
24: 4635 li a2,13 #li: 将一个立即数加载到寄存器中,将13放置到a2寄存器中
26: 45b1 li a1,12 #12放置到a1寄存器中
28: 00000517 auipc a0,0x0 #立即数左移12位,加到当前的PC值上,也就是0x28
2c: 7b050513 addi a0,a0,1968 # 7d8 <malloc+0xea> #a0=a0+1968 = 0x7d8
30: 00000097 auipc ra,0x0 # 将pc+0 加载到ra寄存器,ra存放0x30
34: 600080e7 jalr 1536(ra) # 630 <printf> #pc设置为ra+1536=0x630,并将pc+4写入ra(进行函数跳转)
exit(0);
38: 4501 li a0,0
3a: 00000097 auipc ra,0x0
3e: 27e080e7 jalr 638(ra) # 2b8 <exit>
在RISC-V中,sp
(栈指针寄存器)用来指向栈顶的当前位置,而s0
(也叫做fp
,帧指针寄存器)用来指向当前函数的栈帧基址。栈帧是为每次函数调用分配的栈上的内存块,它通常包含:
- 函数的局部变量
- 函数的参数(如果不是通过寄存器传递)
- 返回地址(即函数执行完毕后应该跳转回的地址)
- 上一个栈帧的帧指针(用于在函数返回时恢复
fp
)
在函数调用的开始,会执行一系列的操作来建立新的栈帧:
sp
向下移动以分配足够的空间给局部变量。- 将当前的帧指针
s0
(指向上一个函数的栈帧基址)保存到栈上,用于之后恢复。 - 设置新的帧指针
s0
为当前的栈顶,即指向新建的栈帧的开始位置。
在函数即将返回时,会执行相反的操作来清理栈帧:
- 使用
ld
指令从栈上恢复旧的帧指针s0
的值。 - 移动
sp
以丢弃当前函数的栈帧。 - 通过
ret
指令跳转回调用者的地址。
2. lab2 bttest函数
要求你在 kernel/printf.c
中实现一个 backtrace()
函数,用于打印函数调用栈上发生错误位置之上的函数调用列表。然后,在 sys_sleep
中插入一个调用到这个函数,并运行 bttest
,bttest
调用了 sys_sleep
。
/* backtrace : 输出所有栈帧 */
void backtrace(void)
{
uint64 fp = r_fp(); // 读出当前FP指针的值,使用上面添加的嵌入式汇编函数
//RISC-V的用户栈空间占一个页面,可通过PGROUNDDOWN得到所在页面的低地址
uint64 KernelBottom = PGROUNDDOWN(fp); // 获取用户栈最低地址
uint64 KernelTop = PGROUNDUP(fp); //获取用户栈的最高地址
printf("backtrace:\n");
// 如果没有到最后一级,则持续向前一级回溯
while(fp >= KernelBottom && fp < KernelTop)
{
// 从FP-8这个位置取出上一级函数返回地址,打印出来
uint64 ReturnAddr = *(uint64*)(fp - 8);
printf("%p\n", ReturnAddr);
// 回溯到上一层函数栈帧
fp = *(uint64*)(fp - 16);
}
return;
}
这里重要的是要理解stack frame栈帧的结构:
在函数调用时,一个新的栈帧会被创建在栈顶,用于存储这次函数调用的相关信息
fp 寄存器其实指向的是上一级栈帧的最后一个位置,这是因为RISC-V中栈指针(stack pointer)是遵循满递减原则的。那么FP-8一定存放的是上一级函数的返回地址ra,FP-16这个地址一定存放的是上一级栈帧指针Previous FP,这是我们实现回溯的重要条件。
1. return address: 函数执行完需要返回到的下一条指令的地址。这是调用函数后,程序将继续执行的内存地址。当当前函数执行完毕后,程序将跳转到这个地址继续执行。
2. Frame Pointer to Previous Frame (FP):这是指向前一个(调用当前函数的)函数的栈帧的指针。通过这个指针,我们可以找到之前函数的栈帧,从而能够在函数返回时恢复之前函数的环境。
3. Saved Registers:这部分存储了函数执行前需要保存的寄存器的值。因为在函数执行过程中,寄存器的值可能会被修改,所以在函数开始执行前,需要保存这些寄存器的原始值,以便在函数返回后能够恢复。
4. Local Variables:这部分存储了函数的局部变量。这些变量只在函数执行期间存在,函数返回后,这些变量就会被销毁。
3. lab3 alarm
实现:两个系统调用,一个进程执行定时打断转而执行其他函数之后返回
sigalarm(n, fn): 设置一个进程被时钟打断的间隔interval和响应函数hander, 在程序每消耗n个“ticks”的CPU时间之后,内核调用fn函数,当 fn
返回时,应用程序应该从它离开的地方继续执行。
usertrap: 操作系统内核用于处理从用户模式引发的陷阱函数,识别陷阱的原因,并按照不同的情况做出相应的处理。陷阱通常是由于异常(比如除零错误或非法内存访问)、中断(比如来自硬件的信号)或系统调用(比如一个程序请求操作系统服务)引起的
思路:每次经过一个时钟中断,对passedticks加1,当达到interval时便要调用handler()函数,同时将passticks置零用于下次调用定时函数,整体流程:
所以说:
1. 如何调用定时函数handler? handler是由用户提供的,但是在处理定时中断时调用的是kernel/trap.c 中的usertrap(),而此时页表已经切换为内核页表。由于handler是用户空间下的函数虚拟地址,因此不能直接调用,故将p->trapframe->epc置为p->handler,这样在返回到用户空间时,程序计数器为handler定时函数的地址
(在RISC-V架构中,trapframe
是一个在发生异常(比如中断或系统调用)时用于保存CPU状态的数据结构。trapframe->epc
是该数据结构中的一个字段,epc
代表“Exception Program Counter”。
epc
寄存器存放的是发生异常或中断时的程序计数器(PC)的值。程序计数器指向下一条要执行的指令的地址。当异常发生时,系统需要保存当前的执行状态,以便异常处理完成后能够从正确的位置恢复程序的执行。因此,epc
寄存器会保存触发异常时即将被执行的指令的地址。在异常处理结束后,操作系统会从 epc
指定的地址恢复程序的执行,除非异常处理程序决定跳转到其他位置,此时会修改 epc
的值。例如,在定时器中断处理程序中,如果操作系统使用一个警报处理程序,那么在处理程序完成后,epc
可能会被设置为警报处理程序返回地址,以确保程序能够继续执行正确的代码流。)
2. 如何返回?
在上面,调用定时函数 handler 实际上是通过修改 trapframe->epc 进而在返回到用户空间时调用定时函数. 但这也同时产生了一个问题, 即原本的 epc已被覆盖, 无法回到中断前的用户代码执行的位置, 同时在执行 handler() 函数后, 相关的寄存器的值也会受到影响。
因此考虑要在 sigalarm() 函数中将寄存器值进行保存, 在 sigreturn() 函数中进行恢复. 这样在执行完 sigreturn() 后程序能够回到原来的执行位置。在系统调用时用户代码中断时会将寄存器记录到 p->trapframe 中, 而前者由于在 usertrap() 覆盖了 p->trapframe->epc, 才能够执行定时函数, 执行完后又会导致一些寄存器的值被修改。因此, 考虑在 struct proc 中保存一个 trapframe 的副本, 在覆盖 epc 之前先保存副本, 然后在 sys_sigreturn() 中将副本还原到 p->trapframe 中, 从而在 sigreturn 系统调用结束后恢复用户寄存器状态时能够将执行定时函数前的寄存器状态进行恢复。
参考阅读