mit 6.s081 lab4 Traps

chapter 4

trap的类型

  • syscall
  • exception(除0,访问非法内存)
  • interrupt(读/写磁盘操作结束)

trap流程
1.控制权转换给kernel
2.kernel保存寄存器以及状态,以便代码的执行的恢复
3.kernel执行对应的trap处理代码(syscall实现或者设备驱动)
4.kernel恢复之前保存的寄存器和状态,从trap返回执行之前的代码

RISC-V trap相关寄存器

  • stvec 记录trap handler的地址
  • sepc 用于保存pc,当使用sret指令后将sepc拷贝到pc
  • scause 记录trap的yuanyin
  • sscratch 方便trap handler启动时记录某些值
  • sstatus sstatus中的SIE位控制设备interrupt是否被启用,如果SIE未被启动设备中断会延迟到SIE位被设置。SPP位记录trap时是user mode还是supervisor mode,以及控制sret时返回到哪种模式

trap时RISC-V硬件执行的动作:
1.如果是设备interrupt并且SIE位被清0,不进行以下操作
2.通过对SIE清零关闭中断
3.拷贝pc到sepc
4.保存现在的模式(user或者supervisor)到sstatus的SPP位
5.根据trap的原因设置scause
6.设置为supervisor mode
7.拷贝stvec到pc
8.执行pc中的指向的指令

系统调用过程:
在这里插入图片描述

  • gdb tips
tui enable #打开多窗口
layout asm #打开汇编窗口
layout reg #打开寄存器窗口
layout src #打开源码窗口
b file:func #对某个文件的某个函数设置断点
print/x $ra #打印寄存器ra
RISC-V assembly
  • Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?
ai寄存器负责记录函数的传入参数,a2寄存器保存13
  • Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)
main的汇编代码中不存在调用f函数的地方,也没有调用g函数的地方,在编译的过程中编译器对调用f函数的代码进行的内联优化,直接将对应的表达的值计算了出来。
  • At what address is the function printf located?
0x630
有两种方法可以查看printf的地址,第一种方法是直接在汇编里面看printf符号对应的位置。第二种方法是根据指令推测,auipc ra, 0x0 -> ra = pc + 0x0 << 12 = 0x30,jalr 1536(ra) -> 跳到的地址为1536 + 0x30 = 0x630

auipc指令:立即数左移12位,和pc的值相加赋给指定的寄存器
jalr r # $ra <— PC+4 $ra <— return address
# PC <— $r load the PC with the address in $r
  • What value is in the register ra just after the jalr to printf in main?
0x38
  • Run the following code.
	unsigned int i = 0x00646c72;
	printf("H%x Wo%s", 57616, &i);

What is the output? Here’s an ASCII table that maps bytes to characters.
The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?

output为He110 World,57616使用十六进制标识,无符号整型i使用字符串的形式输出,由于RISC-V是小端,所以对应的字符为'0x72','0x6c','0x64'
如果RISC-V是大端,上述代码需要输出同样的结果,那么需要把i设为0x726c6400,不需要改变57616的值
  • In the following code, what is going to be printed after ‘y=’? (note: the answer is not a specific value.) Why does this happen?
	printf("x=%d y=%d", 3);
输出不确定值,取决于当时a2寄存器的值
Backtrace

这个exercise的主要工作是实现backtrace函数,backtrace函数的作用是打印每个stack frame中的return address。在这个exercise中在sys_sleep中调用实现的backtrace函数,在xv6命令行中调用sys_sleep,会有如下输出

backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

对于以上的地址,可以在执行

addr2line -e kernel/kernel

然后复制backtrace打印出来的地址,可以显示对应调用在文件中的位置

	kernel/sysproc.c:74
    kernel/syscall.c:224
    kernel/trap.c:85

主要步骤如下:
1.在kernel/riscv.h中添加读取fp寄存器的函数

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

2.在kernel/printf.c中添加backtrace函数,并在defs.h中添加对应的声明。
xv6中stack frame的结构如下所示:
请添加图片描述
fp指向一个stack frame的顶部,fp-8为return address,fp-16为上一个stack frame的地址,每一个stack的大小都是一个page,所以使用PGROUNDUP(fp)可以获取到stack的顶部,backtrace函数中的主要工作就是遍历所有的stack frame,获取其中的return address,遍历结束通过fp是否等于stack top来判断。

void backtrace() {
  uint64 fp = r_fp();
  printf("backtrace:\n");
  uint64 page_end = PGROUNDUP(fp);
  while (1) {
    if (fp == page_end)
      break;
    printf("%p\n", *(uint64*)(fp - 8));
    
    fp = *(uint64*)(fp - 16);
  }
}

3.在sys_sleep中添加backtrace的调用

Alarm

添加syscall sigalarm(interval, handler)和sigreturn,调用sigalarm后每个interval(ticks)都调用handler,当handler返回时用户程序应当从刚才停止的地方继续执行代码。调用sigalarm(0, 0)后系统应当停止周期性调用handler

test0: invoke handler

test0的主要任务是实现sigalarm和sigreturn这两个syscall,其中sigreturn先直接return 0,在后面会继续完善sigreturn。其次要实现周期性调用handler,步骤如下:
1.根据之前实现syscall的步骤对sigalarm和sigreturn进行声明,其中sigreturn直接return 0
sigalarm和sigreturn的原型如下:

    int sigalarm(int ticks, void (*handler)());
    int sigreturn(void);

2.在proc结构体添加成员interval和handler,记录alarm的间隔时间,handler的指针以及已运行时间

struct proc {
....
int interval;                // alarm interval
uint64 handler;           // alarm handler
int time_spend;              // time spend since last alarm handler was called
....
}

3.在sys_sigalarm中设置proc中interval和handler的值

uint64 sys_sigalarm(void) {
  int interval;
  uint64 handler;

  if(argint(0, &interval) < 0 || argaddr(1, &handler) < 0)
    return -1;

  myproc()->interval = interval;
  myproc()->handler = handler;

  return 0;
}

3.在kernel/trap.c的usertrap()函数中对时间中断进行处理,当which_dev为2时为时间中断,在这里增加time_spend,并判断time_spend有没有超过interval,如果超过则调用handler,调用handler的方式为修改proc->trapframe->epc的值,当从usertrap返回时sret会将pc变成proc->trapframe->epc

void usertrap(void) {
....
if(which_dev == 2) {
    // alarm被启动且handler没有被调用
    if (p->interval >= 0) {
      p->time_spend++;
      if (p->time_spend >= p->interval) {
        p->trapframe->epc = p->handler;
        p->time_spend = 0;
      }
    } 
    
    yield();
  }
  ....
  }
test1/test2(): resume interrupted code

test0中的做法不能在调用handler后正确返回到application中的代码中,因为没有对原来的epc进行保存,因此在proc增加一个新的trapframe用于保存原来的trap_frame。除此之外在handler的运行期间不允许再次alarm,因此需要在proc中增加alarm_runing来记录handler是否正在运行,并在sigreturn是修改这个状态
proc.h

struct proc {
....
  int alarm_running;          // 正在调用handler
....
  struct trapframe trapframe_alarm; 
....
};

usertrap()增加handler是否正在运行的判断以及trap_frame的复制

if(which_dev == 2) {
    // alarm被启动且handler没有被调用
    if (p->interval >= 0 && p->alarm_running != 1) {
      p->time_spend++;
      if (p->time_spend >= p->interval) {
        p->trapframe_alarm = *p->trapframe;
        p->alarm_running = 1;
        p->trapframe->epc = p->handler;
        p->time_spend = 0;
      }
    } 
    
    yield();
  }

sys_sigreturn中对trapframe进行恢复,以及设置运行handler的状态为0

uint64 sys_sigreturn(void) {
  *myproc()->trapframe = myproc()->trapframe_alarm;
  myproc()->alarm_running = 0;
  return 0;
}

请添加图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值