MIT_6.s081_Lab4:Xv6 and Trap
于2022年3月5日2022年3月5日由Sukuna发布
Lab4_1 RISC-V Assembly
我们需要运行对call.c这份代码的编译,然后回答一些问题
make fs.img编译之后我们可以找到下面的
代码语言:javascript复制int g(int x) {
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
000000000000000e <f>:
int f(int x) {
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 %dn", f(8) 1, 13);
24: 4635 li a2,13
26: 45b1 li a1,12
28: 00000517 auipc a0,0x0
2c: 7b050513 addi a0,a0,1968 # 7d8 <malloc 0xea>
30: 00000097 auipc ra,0x0
34: 600080e7 jalr 1536(ra) # 630 <printf>
exit(0);
38: 4501 li a0,0
3a: 00000097 auipc ra,0x0
3e: 27e080e7 jalr 638(ra) # 2b8 <exit>
1.Q:Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf? 哪些寄存器存储了函数的参数?比如说调用printf的时候13存在哪个寄存器里面?
A:a0~a7共8个参数,其中调用printf的时候13就存在a2这个寄存器里面
2.Q: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.) 汇编代码中哪一部分出现了对函数f的调用,函数g呢?
A:其实并没有,编译器把g函数内联到f函数里面,再把f函数内联到main里面
3.Q:At what address is the function printf located? printf的入口地址在哪里?
A:根据jalr 1536(ra)
可以知道printf的地址在0x630这个里面
4.Q:What value is in the register ra just after the jalr to printf in main? printf执行完返回到main的时候ra寄存器的值是多少?
显然ra中应是jalr下一条指令的地址,即0x38。
5.Q:Run the following code.
代码语言:javascript复制 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 change57616
to a different value?
输出是什么?RISC-V是小端存储的.如果是大端存储的呢?
Here’s a description of little- and big-endian and a more whimsical description.
输出”HE110 World”. 57616=0xe110。RISC-V 是 little-endian,&i处存储的字节依次为0x72:r, 0x6c:l, 0x64:d,0x00:Null char (空字符)。
如果 RISC-V 是 big-endian,则i = 0x726c6400;. 57616不需改变,因为不管怎样它的十六进制形式都不会变。
6.Q:In the following code, what is going to be printed after 'y='
? (note: the answer is not a specific value.) Why does this happen?
执行下一条语句,会输出y=多少呢?
printf("x=%d y=%d", 3);
因为函数a0~a7都存着参数,所以说对应的输出第二个参数的内容,也就是a1寄存器的内容.
Lab4_2 BackTrace
添加一个新的功能,打印函数调用栈.在这个机器中,我们知道有一个结构叫做栈帧,可以保存当前函数调用某个函数之前的一些寄存器,返回地址和一些局部变量的信息,比如说C语言的main函数,main函数调用一个函数f1(),在进入f1()函数的执行之前,编译器会帮我们把main函数的一些寄存器、局部变量的信息保存在栈帧中放入堆栈.其中栈帧中有一个特别的元素就是上一个栈帧的地址.
本实验的要求就是在kernel/printf.c 中实现backtrace()函数。在sys_sleep中插入对该函数的调用。然后运行bttest,它调用sys_sleep。输出应如下:
1) 在def.h添加backtrace()函数的声明.
2) GCC 编译器将当前执行的函数的帧指针存储在寄存器s0中,s0就对应上面的fp指针.
代码语言:javascript复制static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
3) 完成backtrace()
我们知道fp指针往下走两个字节就存储的上一个函数的栈帧的最高地址,所以说我们可以循环一下,每一次循环就取上一个函数的栈帧最高地址,输出返回地址即可.
代码语言:javascript复制void
backtrace(void){
uint64 fp = r_fp(), top = PGROUNDUP(fp);
printf("backtrace:n");
for(;fp<top;fp=*(uint64*)(fp-16)){
printf("%pn", *((uint64*)(fp-8)));
}
}
Lab4_3 Alarm
在本练习中,您将向xv6添加一项功能,该功能会在使用CPU时间的情况下定期向进程发出警报。这对于想要限制消耗多少CPU时间的计算密集型进程,或者对于想要进行计算但还希望采取一些定期操作的进程很有用。
您应该添加一个新的sigalarm(interval,handler)系统调用。 如果应用程序调用sigalarm(n,fn),则在程序每消耗n个“ tick” CPU时间之后,内核应导致调用应用程序函数fn。 当fn返回时,应用程序应从中断处恢复。 滴答是xv6中相当随意的时间单位,由硬件计时器产生中断的频率决定。 如果应用程序调用sigalarm(0,0),则内核应停止生成定期警报调用。
test0:调用处理程序
1.修改Makefile,为sigalarm和sigreturn系统调用添加桩代码。 2.现在,sys_sigreturn应该只返回零。 3.sys_sigalarm()应该将nl和handler存储在proc结构的新字段中(在kernel/proc.h 中)。 4.您需要跟踪该进程自上次调用alarm handler以来已经过去了多少ticks。为此,您还需要struct proc中的一个新字段。您可以在proc.c的allocproc() 中初始化这个字段。 5.每当tick时,硬件时钟都会强制中断,该中断在kernel/trap.c的usertrap()中处理。 6.时钟中断是:if(which_dev == 2) … 7.您需要修改usertrap()以在进程的警报间隔到期时,用户进程执行handler。
test1/test2(): 恢复中断的代码
8.handler完成后,控制权返回到用户程序最初被中断的指令。必须确保寄存器内容恢复,以及重置警报计数器。以便定期调用handler。 9.user alarm handler需要在完成后调用sigreturn系统调用。这意味着您可以将代码添加到usertrap和sys_sigreturn中,它们会协同工作以使用户进程在处理完警报后正确恢复。 10.确保正确地保存和恢复寄存器。 11.当计时器到期时,让 usertrap 在 struct proc 中保存足够的状态,以便sigreturn可以正确返回 到被中断的用户代码。 12.防止对处理程序的重入调用——如果处理程序尚未返回,内核不应再次调用它。
首先就是把添加系统调用的路子走一遍.
1) 在user.h中添加声明:
代码语言:javascript复制int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
2) 在user.pl添加函数入口以生成汇编代码.
代码语言:javascript复制entry("sigalarm");
entry("sigreturn");
3) 在syscall.h添加系统调用号.
代码语言:javascript复制#define SYS_sigalarm 22
#define SYS_sigreturn 23
4) 在syscall.c中添加关于这两个系统调用的声明.
代码语言:javascript复制......
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);
......
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,
5) 在proc的进程结构体中添加成员,保存什么时候中断,中断执行什么,过多久就进行一次中断,还保存中断的时候栈顶指针和保存的栈帧.
代码语言:javascript复制int interval;// interupt count
void (*handler)(); //handle programme
int count; //tick interupt count
uint64 interrupt_ra; //trapframe pointer.
struct trapframe *saved_trapframe;//saved_trapframe
int isworking;
6) 在allocproc函数中初始化这些成员.
代码语言:javascript复制 p->interval = 0;
p->handler = 0;
p->count = 0;
p->is working = 0;
7) 完成sigalarm的系统调用,做法就是从寄存器获得参数然后赋值给proc的元素中.
代码语言:javascript复制uint64
sys_sigalarm(void){
struct proc* p = myproc();
int in;
uint ft;
int interval;
uint64 pt;
if(argint(0, &in) < 0)
return -1;
if(argaddr(1, &ft) < 0)
return -1;
p->interval=in;
p->handler=(void*)ft;
return 0;
}
8) 完成时钟
代码语言:javascript复制if(which_dev == 2){
//the proc no call the sysalaram.
if(p->interval==0||p->working)
{
yield();
usertrapret();
}
else{
p->count ;
if(p->count==p->interval)
{
//save the trapframe.
p->saved_trapframe=(struct trapframe*)kalloc();
memmove(p->saved_trapframe,p->trapframe,PGSIZE);
//save the trapped address
p->interrupt_ra=p->trapframe->epc;
//the programme will start at handler.
p->trapframe->epc=(uint64)p->handler;
usertrapret();
yield();
}
}
}
处理的方式是分类,如果之前没有调用过sigalarm的话,interval为0,就和原来一样,如果不是的话就代表调用过.接着count ,如果count和interval一样的话就代表要跳转了,保存当前的trapframe以及当前被中断的指令的地址,修改epc进行跳转.还有就是要记录working,如果这个函数已经被打断了就不要再重新打断一次.
9) 完成返回的操作.
代码语言:javascript复制uint64 sys_sigreturn(void)
{
struct proc* p = myproc();
p->count=0;
p->trapframe->epc=p->interrupt_ra;
memmove(p->trapframe,p->saved_trapframe,PGSIZE);
p->isworking = 0;
return 0;
}
把之前保存的断点地址和栈帧读出来,清空时钟即可.