6.s081 lab4小结

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

     在函数调用的开始,会执行一系列的操作来建立新的栈帧:

  1. sp向下移动以分配足够的空间给局部变量。
  2. 将当前的帧指针s0(指向上一个函数的栈帧基址)保存到栈上,用于之后恢复。
  3. 设置新的帧指针s0为当前的栈顶,即指向新建的栈帧的开始位置。

     在函数即将返回时,会执行相反的操作来清理栈帧:

  1. 使用ld指令从栈上恢复旧的帧指针s0的值。
  2. 移动sp以丢弃当前函数的栈帧。
  3. 通过ret指令跳转回调用者的地址。

2. lab2 bttest函数

要求你在 kernel/printf.c 中实现一个 backtrace() 函数,用于打印函数调用栈上发生错误位置之上的函数调用列表。然后,在 sys_sleep 中插入一个调用到这个函数,并运行 bttestbttest 调用了 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 系统调用结束后恢复用户寄存器状态时能够将执行定时函数前的寄存器状态进行恢复。  

参考阅读

[MIT 6.S081] Lab 4: traps_mit6.s081lab4-CSDN博客

6.S081——Lab4——trap lab_6.s081 lab4-CSDN博客

  • 10
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值