MIT6.S081 Lab4:traps

本文介绍了RISC-V架构下系统调用的实现,包括函数参数在寄存器中的存储、函数调用的查找以及printf函数的地址。此外,详细讨论了栈帧和backtrace机制,展示了如何通过栈帧指针遍历调用栈并打印返回地址。最后,探讨了实现用户级定时报警功能,包括sigalarm和sigreturn系统调用的细节,以及如何在时钟中断中优雅地切换执行流程。
摘要由CSDN通过智能技术生成

实验目的

This lab explores how system calls are implemented using traps. You will first do a warm-up exercises with stacks and then you will implement an example of user-level trap handling.

RISC-V assembly [easy]

回答下列问题

1 Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?
a2到a7存储函数参数,这里是a2存储13

2.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.)
没有这样的代码,g 被内联到f中,f进一步被内联到main中

3.At what address is the function printf located?
0x630 (这是个地址标签,不是代表真实的内存地址)

4.What value is in the register ra just after the jalr to printf in main?
0x38(该指令中,ra存储的当前PC+4)

5.Run the following code.

unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

What is the output?
HE110 World
%x是打印十六进制,0x00646c72小端存储为72-6c-64-00,对照ASCII码表,72:r 6c:l 64:d 00:充当字符串结尾标识
上面输出的结果是基于RISC-V架构是小端序的。
若为大端存储,i应改为0x726c6400,不需改变57616

6.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);

第二个参数 3 通过 a1 传递,而第三个参数对应的寄存器 a2 在调用前不会被设置为任何具体的值,而是会
包含调用发生前的任何已经在里面的值。

Backtrace[moderate]

打印各级函数调用的返回地址,编译器用栈帧指针指向一个栈帧,栈帧中保存着caller func的栈帧指针。可以利用这些栈帧指针来遍历栈帧,并且打印对应的返回地址
提示

  • 在kernel/defs.h中添加函数原型,这样可以在sys_sleep中调用backtrace
  • GCC编译器将栈帧指针保存在寄存器s0中。将下列函数添加到kernel/riscv.h中,该内联汇编用于读取当前栈帧指针
static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}
  • 根据栈帧的内存布局,相对栈帧指针,返回地址有固定的偏移(-8),上一级栈帧指针也有固定的偏移(-16)
  • 可以通过PGROUNDDOWN(fp) 和 PGROUNDUP(fp) 来计算栈页的顶部和底部地址(注意栈顶是低地址,栈底是高地址),可以基于此来终止backtrace中的循环
  • backtrace函数写好后,将它卸载panic(kernel/printf.c)中,就可以在发生panic时打印各级调用的返回地址
    背景知识
    fp 指向当前栈帧的开始地址,sp 指向当前栈帧的结束地址。 (栈从高地址往低地址生长,所以 fp 虽然是帧开始地址,但是地址比 sp 高)
    栈帧中从高到低第一个 8 字节 fp-8 是 return address,也就是当前调用层应该返回到的地址。
    栈帧中从高到低第二个 8 字节 fp-16 是 previous address,指向上一层栈帧的 fp 开始地址。
    剩下的为保存的寄存器、局部变量等。一个栈帧的大小不固定,但是至少 16 字节。
    在 xv6 中,使用一个页来存储栈,如果 fp 已经到达栈页的上界,则说明已经到达栈底。

实验内容
1.

//kernel/printf.c

void 
backtrace(void)
{
	printf("backtrace:\n");
	uint64 fp = r_fp();
	while(PGROUNDUP(fp) - PGROUNDDOWN(fp) == PGSIZE) {  //还没有到达栈底
	    //return address offset is 8
		uint64 ret_addr = *(uint64*)(fp - 8);
		printf("%p\n", ret_addr);
		//pre frame pointer offset is 16
		fp = *(uint64*)(fp - 16);
	}
}

2.sys_sleep中调用backtrace

实验结果
运行bttest通过

Alarm[hard]

In this exercise you’ll add a feature to xv6 that periodically alerts a process as it uses CPU time. This might be useful for compute-bound processes that want to limit how much CPU time they chew up, or for processes that want to compute but also want to take some periodic action. More generally, you’ll be implementing a primitive form of user-level interrupt/fault handlers; you could use something similar to handle page faults in the application, for example. Your solution is correct if it passes alarmtest and usertests.
You should add a new sigalarm(interval, handler) system call. If an application calls sigalarm(n, fn), then after every n “ticks” of CPU time that the program consumes, the kernel should cause application function fn to be called. When fn returns, the application should resume where it left off. A tick is a fairly arbitrary unit of time in xv6, determined by how often a hardware timer generates interrupts. If an application calls sigalarm(0, 0), the kernel should stop generating periodic alarm calls.
这项练习要实现定期的警报。首先是要通过test0,如何调用处理程序是主要的问题
背景知识

trap过程中,程序计数器的过程如下:

  1. ecall指令将PC保存到SEPC
  2. 在usertrap中将SEPC保存到p->trapframe->epc
  3. p->trapframe->epc + 4指向下一条指令
  4. 执行系统调用
  5. 在usertrapret中将SEPC改写为p->trapframe->epc中的值
  6. 在sret中将PC设置为SEPC的值

实验过程

  1. 首先在Makefile中添加alarmtest.c的编译成用户程序
  2. 添加函数的声明
//user/user.h
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
  1. struct proc中增加字段,同时记得在allocproc中将它们初始化为0,并在freeproc中也设为0
//kernel/proc.h
//+
  int alarm_interval;          
  void (*alarm_handler)();    
  int ticks_cnt;               //how many ticks left before next alarm goes off
  int is_alarming;              //whether excuting alarm handler or not
  struct trapframe *alarm_trapframe;   //alarm trapframe
  1. sys_sigalarmsys_sigreturn 具体实现
//kernel/trap.c
int sigalarm(int ticks, void(*handler)())
{
 // 设置 myproc 中的相关属性
	struct proc *p = myproc();
	p->alarm_interval = ticks;
	p->alarm_handler = handler;
	p->ticks_cnt = ticks;
	return 0;
}
int sigreturn(void)
{
	struct proc *p = myproc();
// 将 trapframe 恢复到时钟中断之前的状态,恢复原本正在执行的程序流
	memmove(p->trapframe, p->alarm_trapframe, sizeof(struct trapframe));
	p->is_alarming = 0;
	return 0;
}

//kernel/sysproc.c
uint64
sys_sigalarm(void) 
{
	int n;
	uint64 fn;
	if(argint(0, &n) < 0 || argaddr(1, &fn) < 0)
		return -1;	
	return sigalarm(n, ( void(*)() )fn);
}
uint64
sys_sigreturn(void) {
	return sigreturn();
}

  1. 在proc.c中添加初始化和释放代码
//kernel/proc.c
static struct proc*
allocproc(void)
{
// ......
  found:
  	p->pid = allocpid();
//......
//+
//initialize alarm part
 	if((p->alarm_trapframe == (struct trapframe*)kalloc()) == 0) {
		freeproc(p);
		release(&p->lock);
		return 0;	
	}
 	p->is_alarming = 0;
	p->alarm_interval = 0;
 	p->alarm_handler = 0;
 	p->ticks_cnt = 0;
 //......

static void
freeproc(struct proc *p)
{
// ......
	//+
	if(p->alarm_trapframe)
		kfree((void*)p->alarm_trapframe);
	p->is_alarming = 0;
	p->alarm_interval = 0;
	p->alarm_handler = 0;
	p->ticks_cnt = 0;
//......
}
  1. 修改usertrap(),在sigalarm前,将当前trapframe存在alarm_trapframe中,方便在sigreturn恢复
//kernel/trap.c
/*...
*/
// give up the CPU if this is a timer interrupt.
  if(which_dev == 2){
  	if(p->alarm_interval != 0 && --p->ticks_cnt <= 0 && p->is_alarming == 0) {
		//save the register
		p->ticks_cnt = p->alarm_interval;
		memmove(p->alarm_trapframe, p->trapframe, sizeof(struct trapframe));
		//change the trapframe epc
		p->trapframe->epc = (uint64)p->alarm_handler;
		
		p->is_alarming = 1;
	}
    yield();
 /*...
 */

这样,在每次时钟中断的时候,如果进程有已经设置的时钟(alarm_interval != 0),则进行 ticks_cnt 倒数。当 alarm_ticks 倒数到小于等于 0 的时候,如果没有正在处理的时钟,则尝试触发时钟,将原本的程序流保存起来(*alarm_trapframe = *trapframe)(usertrap()中),然后通过修改 pc 寄存器的值,将程序流转跳到 alarm_handler 中,alarm_handler 执行完毕后再恢复原本的执行流(*trapframe = *alarm_trapframe)(sigreturn中)。这样从原本程序执行流的视角,就是不可感知的中断了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值