6.S081-Lab4

Lab: traps

课程地址

看make后的user/call.asm.

main:

void main(void) {
  printf("%d %d\n", f(8)+1, 13);
  exit(0);
}

Question:

addi sp,sp,-16: 将栈指针 sp 减去 16 字节,为将来在栈上分配空间做准备。

sd ra,8(sp): 将寄存器 ra 中的值存储在距离 sp 偏移 8 字节的位置,以便在返回时恢复 ra 的值。

sd s0,0(sp): 将寄存器 s0 中的值存储在距离 sp 偏移 0 字节的位置,以便在返回时恢复 s0 的值。

addi s0,sp,16: 将 s0 的值设置为栈指针 sp 加上 16,这是一个将来在栈上分配空间时要使用的偏移量。

auipc ra,0x0: 将全局地址 ra 设置为当前 PC 寄存器值的高位。

jalr -46(ra) # 1c : 跳转到 main 函数的地址,即将 PC 设置为 ra-46。跳转指令的返回地址将存储在 ra 中。

li a0,0: 将寄存器 a0 设置为 0,这是将要传递给 exit 函数的参数。

auipc ra,0x0: 将全局地址 ra 设置为当前 PC 寄存器值的高位。

jalr 628(ra) # 2c8 : 跳转到 exit 函数的地址,即将 PC 设置为 ra+628。这将退出程序并返回 0。

哪些寄存器包含函数参数?例如,哪个寄存器在main函数调用printf时保存了数字13?

在RISC-V中,前六个整数参数通过a0-a5传递,其他参数通过栈传递。因此,在main函数调用printf时,数字12和13分别存储在a1和a2寄存器中。
在main函数的汇编代码中,函数f和g的调用分别在哪里?(提示:编译器可能会内联函数。)

函数f的调用位于printf的第一个参数计算之前,即在地址0x24处。由于函数g是f的子例程,编译器可能会内联调用g函数,因此在main函数的汇编代码中可能没有对函数g的显式调用。
函数printf的地址在哪里?

函数printf的地址是0x630。
在main函数中jalr到printf后寄存器ra中的值是什么?

在jalr指令执行后,寄存器ra中存储着返回地址,即下一条指令的地址。因此,在jalr到printf的调用之后,ra寄存器中包含指向exit调用的下一条指令地址的值。
函数g的汇编代码在哪里?

0000000000000000 <g>:
   0:	1141                	addi	sp,sp,-16
   2:	e422                	sd	s0,8(sp)
   4:	0800                	addi	s0,sp,16
  return x+3;
   6:	250d                	addiw	a0,a0,3
   8:	6422                	ld	s0,8(sp)
   a:	0141                	addi	sp,sp,16
   c:	8082                	ret

函数g的汇编代码在地址0x0。

Backtrace

对于调试,回溯通常很有用:在错误发生点之上的堆栈上的函数调用列表。 为了帮助回溯,编译器生成机器代码,在堆栈上维护一个堆栈帧,对应于当前调用链中的每个函数。 每个堆栈帧由返回地址和指向调用者堆栈帧的“帧指针”组成。 寄存器 s0 包含指向当前堆栈帧的指针(它实际上指向堆栈上保存的返回地址的地址加上 8)。 您的回溯应使用帧指针在堆栈中向上移动并在每个堆栈帧中打印保存的返回地址。

fp 指向当前栈帧的开始地址,sp 指向当前栈帧的结束地址。 (栈从高地址往低地址生长,所以 fp 虽然是帧开始地址,但是地址比 sp 高)
栈帧中从高到低第一个 8 字节 fp-8 是 return address,也就是当前调用层应该返回到的地址。
栈帧中从高到低第二个 8 字节 fp-16 是 previous address,指向上一层栈帧的 fp 开始地址。
剩下的为保存的寄存器、局部变量等。一个栈帧的大小不固定,但是至少 16 字节。

在 xv6 中,使用一个页来存储栈,如果 fp 已经到达栈页的上界,则说明已经到达栈底。
int g(int x) {
0: 1141 addi sp,sp,-16 // 扩张调用栈,得到一个 16 字节的栈帧
2: e422 sd s0,8(sp) // 将返回地址存到栈帧的第一个 8 字节中
4: 0800 addi s0,sp,16
return x+3;
}
6: 250d addiw a0,a0,3
8: 6422 ld s0,8(sp) // 从栈帧读出返回地址
a: 0141 addi sp,sp,16 // 回收栈帧
c: 8082 ret // 返回
注意栈的生长方向是从高地址到低地址,所以扩张是 -16,而回收是 +16。

PGROUNDDOWN(fp); 返回页面低地址
PGROUNDUP(fp); 返回页面高地址
用up-down 是一个页的大小(4096)

代码:
defs.h

// printf.c
void            printf(char*, ...);
void            panic(char*) __attribute__((noreturn));
void            printfinit(void);
void            backtrace(void);

sysproc.h

uint64
sys_sleep(void)
{
  backtrace();

riscv.h

//frame pointer 
static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}

printf.c


/**
 * 
 * 
reg    | name  | saver  | description
-------+-------+--------+------------
x0     | zero  |        | hardwired zero
x1     | ra    | caller | return address
x2     | sp    | callee | stack pointer
x3     | gp    |        | global pointer
x4     | tp    |        | thread pointer
x5-7   | t0-2  | caller | temporary registers
x8     | s0/fp | callee | saved register / frame pointer
x9     | s1    | callee | saved register
x10-11 | a0-1  | caller | function arguments / return values
x12-17 | a2-7  | caller | function arguments
x18-27 | s2-11 | callee | saved registers
x28-31 | t3-6  | caller | temporary registers
pc     |       |        | program counter
 * 
                    .
      +->          .
      |   +-----------------+   |
      |   | return address  |   |
      |   |   previous fp ------+
      |   | saved registers |
      |   | local variables |
      |   |       ...       | <-+
      |   +-----------------+   |
      |   | return address  |   |
      +------ previous fp   |   |
          | saved registers |   |
          | local variables |   |
      +-> |       ...       |   |
      |   +-----------------+   |
      |   | return address  |   |
      |   |   previous fp ------+
      |   | saved registers |
      |   | local variables |
      |   |       ...       | <-+
      |   +-----------------+   |
      |   | return address  |   |
      +------ previous fp   |   |
          | saved registers |   |
          | local variables |   |
  $fp --> |       ...       |   |
          +-----------------+   |
          | return address  |   |
          |   previous fp ------+
          | saved registers |
  $sp --> | local variables |
          +-----------------+
*/

void  backtrace(void)
{
//read the current frame pointer. r_fp() uses in-line assembly to read s0.
    uint64 fp= r_fp();
  uint64 pd= PGROUNDDOWN(fp);   
    uint64 pu= PGROUNDUP(fp);   

   printf("backtrace:\n");
  printf("fp:%p\n",fp);
   printf("pd:%p\n",pd);
    printf("pu:%p\n",pu);


  while (fp>pd)
  {
    
   //print
    printf("%p\n", *(uint64*)(fp-8));
        fp = *(uint64*)(fp - 16); // previous fp
  }
  

}

Alarm

在本练习中,您将向 xv6 添加一项功能,该功能会在进程使用 CPU 时间时定期向其发出警报。 这对于想要限制它们占用多少 CPU 时间的受计算限制的进程,或者对于想要计算但也想采取一些定期操作的进程来说可能很有用。 更一般地说,您将实现一种原始形式的用户级中断/故障处理程序; 例如,您可以使用类似的方法来处理应用程序中的页面错误。 如果您的解决方案通过了 alarmtest 和 ‘usertests -q’,那么您的解决方案是正确的.
您应该添加一个新的 sigalarm(interval, handler) 系统调用。 如果应用程序调用 sigalarm(n, fn),则在程序消耗 CPU 时间的每 n 个“滴答”后,内核应该调用应用程序函数 fn。 当 fn 返回时,应用程序应该从它停止的地方恢复。 在 xv6 中,滴答是一个相当任意的时间单位,由硬件定时器产生中断的频率决定。 如果应用程序调用 sigalarm(0, 0),内核应该停止生成周期性警报调用。

首先的流程先添加系统调用函数sigalarm,sigreturn可以参考Lab2,
这个tips有很多信息,需要注意 比如:

  • Every tick, the hardware clock forces an interrupt, which is handled in usertrap() in kernel/trap.c.
  • Make sure to restore a0. sigreturn is a system call, and its return value is stored in a0.
  • Prevent re-entrant calls to the handler----if a handler hasn’t returned yet, the kernel shouldn’t call it again. test2 tests this
  • Have usertrap save enough state in struct proc when the timer goes off that sigreturn can correctly return to the interrupted user code.

总之要好好看hits!!!

接下来
proc.h 中新增几个字段
interval : 每次隔多少时间间隔调用一次函数
handler:保存用户的回调函数地址
ticks_pass: 计时器每次trps 陷入后++ 到interval后触发回调
tick_status: 用于控制函数防止重入
alarm_trapframe: 用于保存trapframe 当函数从kernel回到user space时设置

proc.c allocproc

  struct trapframe *alarm_trapframe; 
  int interval;             // alarm interval
  uint64 handler;              // pointer to the handler function
  int ticks_pass;               // how many ticks between this call and last call, initialize proc fields in allocproc() in proc.c.
  int tick_status;          // 0 stop 1 running 防止重入
};

proc.c freeproc

  if(p->alarm_trapframe)
   kfree((void*)p->alarm_trapframe);  
  p->interval=0;
  p->ticks_pass=0;
  p->handler=0;
  p->tick_status=0;

syspro.c


uint64
sys_sigalarm(void)
{

  int xticks;
 uint64 handler;
  acquire(&tickslock);

    argint(0, &xticks);
    if(xticks<0){
      return -1;
    }
     argaddr(1, &handler);
    if(handler<0){
      return -1;
    } 

  struct proc* p = myproc();
  
  p->interval=xticks;
  p->handler=handler;

  release(&tickslock);
  return xticks;
}

uint64
sys_sigreturn(void)
{

   acquire(&tickslock);

  struct proc* p = myproc();
//  memmove(p->trapframe,p->alarm_trapframe,280);
  *p->trapframe=*p->alarm_trapframe;

      p->ticks_pass=0;
      p->tick_status=0;

release(&tickslock);
   // cuz this func return value would be set into a0
  return p->trapframe->a0;
}

sys_sigreturn 函数的return结果会直接写入 a0寄存器,所以为了恢复之前的值就让它直接作为返回值就可以, *p->trapframe=*p->alarm_trapframe; 恢复了之前的现场,
p->ticks_pass=0;
p->tick_status=0;
要结合 traps.c 来看

traps.c

大概逻辑就是 每次如果是ticks导致的中断就会进入 whick_dev==2 然后我们判断
有没有 interval 的存在 如果有就让计时器++ , 同时用 tick_status 来防止重入,然后在调用handler之前用alarm_trapframe 来保存之前的trapframe中的寄存器状态.

这里为啥把要调用的函数直接赋给 epc 呢,原因是函数在返回时,调用 ret 指令,使用 trapframe 内事先保存的寄存器的值进行恢复。这里我们更改 epc 寄存器的值,在返回后,就直接调用的是 handler 处的指令,即执行 handler 函数.

 if(which_dev == 2){
  
    if(p->interval){
      p->ticks_pass++;
      if(p->ticks_pass==p->interval && !p->tick_status){
     
     // 防止重入
        p->tick_status=1;
      *p->alarm_trapframe=*p->trapframe;

      // memmove(p->alarm_trapframe,p->trapframe,280);
      //
      p->trapframe->epc=p->handler;
     
      }
    }

  yield();
  }
  

运行结果


xv6 kernel is booting

hart 2 starting
hart 1 starting
init: starting sh
$ alarmtest
test0 start
...............alarm!
test0 passed
test1 start
.alarm!
..alarm!
.alarm!
.alarm!
.alarm!
.alarm!
.alarm!
.alarm!
.alarm!
.alarm!
test1 passed
test2 start
...........alarm!
test2 passed
test3 start
test3 passed

和trap机制有关的还有一组特殊寄存器,这些寄存器被称为control registers, 包含如下寄存器:

  • stvec: 内核向其中保存了trap handler函数的地址。RISCV将会跳转至此处理trap
  • sepc:当一个trap发生时,RISCV保存当时的PC寄存器至至此(如用户程序发起系统调用后,之后从内核态返回时,需要继续执行当前的指令,所以需要将用户程序发起系统调用时的指令地址保存起来,riscv使用sepc寄存器来保存该值)。RISCV使用 sret 指令从内核态返回至用户态,sepc值将重设至pc寄存器
  • scause:保存一个number,用于描述发生trap的原因
  • sscratch:从书中没有看书该寄存器的作用,但是在xv6中,使用该寄存器保存了trapframe的内存地址,trapframe用于保存用户程序的所有寄存器。
  • sstatus:The SIE bit in sstatus controls whether device interrupts are enabled. If the kernel clears SIE, the RISC-V will defer device interrupts until the kernel sets SIE. The SPP bit indicates whether a trap came from user mode or supervisor mode, and controls to what mode sret returns.

tips:

  • *p->trapframe=*p->alarm_trapframe;和memmove(p->trapframe,p->alarm_trapframe,280);的区别:

*p->trapframe = *p->alarm_trapframe; 和 memmove(p->trapframe, p->alarm_trapframe, 280); 都是 C 语言中用于复制内存区域的操作,但是它们的语法和使用方式不同,因此它们的作用和使用场景也不同。

*p->trapframe = *p->alarm_trapframe; 是将 p->alarm_trapframe 所指向的内存区域中的数据复制到 p->trapframe 所指向的内存区域中,其本质是对结构体进行赋值操作。这种方式适用于结构体较小的情况,可以使用简单的赋值语句完成复制操作。

memmove(p->trapframe, p->alarm_trapframe, 280); 则是将 p->alarm_trapframe 所指向的内存区域中的数据复制到 p->trapframe 所指向的内存区域中,其本质是对内存区域进行复制操作。这种方式适用于需要复制的内存区域较大,或者需要在内存区域中移动数据的情况。

另外,需要注意的是,memmove 函数比较安全,因为它会自动处理内存区域重叠的情况,而 memcpy 函数则不会处理重叠的情况。因此,在涉及内存区域重叠的情况下,建议使用 memmove 函数。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值