首页 > 其他分享 >MIT6.S081 Lab Traps

MIT6.S081 Lab Traps

时间:2024-04-24 14:59:23浏览次数:9  
标签:kernel 调用 Traps MIT6 backtrace alarm trapframe printf S081

本实验主要是关于如何使用陷阱实现系统调用的。

RISC-V assembly (easy)

这个部分主要是回答一些问题。

首先我们按照实验的指示,运行下面的命令得到一份容易读懂的汇编和 C 结合的代码,位于 user/call.asm

make fs.img

Question 1

Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?

哪些寄存器是用来存放函数的参数的?例如,在 mainprintf 的调用中,那个寄存器存放了参数 13

根据实验补充材料 RISC-V Calling conventions 中所述,

The RISC-V calling convention passes arguments in registers when possible. Up to eight integer registers, a0–a7, and up to eight floating-point registers, fa0–fa7, are used for this purpose.

对于整数参数,使用寄存器 a0-a7 来存放。

而在本程序 mainprintf 的调用中:

void main(void) {
  printf("%d %d\n", 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>

可以发现,\(13\) 这个数被存进了 a2 寄存器,也就是说第二个小问的答案是 a2

Question 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.)

在上一问贴出的 printf 调用代码中,可以发现,本应该存放参数 f(8)+1 的寄存器 a1,被直接放入了数值 12。因此对 f 函数的调用被内联了,没有相应的汇编调用。

同样,在 f 函数中本应调用 g 的地方:

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

可以发现,这段汇编代码直接将第一个参数加上 \(3\) 以后返回,而我们知道 g 函数的作用就是返回参数加 \(3\) 的值。因此对 g 的调用也被内联了,汇编程序中找不到调用。

Question 3

At what address is the function printf located?

答案:0x630。(注:不同环境下可能会产生不同的结果)

注意看这两行汇编代码:

30:	00000097          	auipc	ra,0x0
34:	600080e7          	jalr	1536(ra) # 630 <printf>

auipc 的作用是将当前的 PC 加上给定立即数的前二十位,写入寄存器中。因此此时 ra 的值应该是 0x30

下一句话跳转到了 1536(ra) 也就是 1536 + 0x30 = 0x630 的位置。因此,可以得出 printf 的地址是 0x630

其实可以直接从后面的注释中得到答案。

Question 4

What value is in the register ra just after the jalr to printf in main?

jalr 指令会把 ra 设置为调用结束的返回地址,因此应该是下一条指令的位置,也就是 0x34 + 4 = 0x38

Question 5

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?

Here's a description of little- and big-endian and a more whimsical description.

答案:He110 World

跟在 H 后面的是十六进制的 57616,它的十六进制表示是 e110

跟在 Wo 之后的是将 &i 处的数据以字符串形式输出。我们知道 RISC-V 的数据是小端法表示的,低地址存放低字节,因此在 &i 地址之后的几个字节依次是 72 6c 64 00(十六进制),转化为字符串就是 "rld\0"

Question 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);

答案应该是一个不确定的值,这个值取决于运行时寄存器 a2 的值。

作为 printf 格式控制符第二个 %d,应该会从 printf 第三个参数中获取,这个参数是由寄存器 a2 指定的,但是因为在调用 printf 的时候没有给出第三个参数,因此 a2 的值不会被设置,因此会保留调用前的值,这个值无法确定。

Backtrace (moderate)

For debugging it is often useful to have a backtrace: a list of the function calls on the stack above the point at which the error occurred.

Implement a backtrace() function in kernel/printf.c. Insert a call to this function in sys_sleep, and then run bttest, which calls sys_sleep. Your output should be as follows:

backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

After bttest exit qemu. In your terminal: the addresses may be slightly different but if you run addr2line -e kernel/kernel (or riscv64-unknown-elf-addr2line -e kernel/kernel) and cut-and-paste the above addresses as follows:

    $ addr2line -e kernel/kernel
    0x0000000080002de2
    0x0000000080002f4a
    0x0000000080002bfc
    Ctrl-D

You should see something like this:

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

The compiler puts in each stack frame a frame pointer that holds the address of the caller's frame pointer. Your backtrace should use these frame pointers to walk up the stack and print the saved return address in each stack frame.

简单地说,就是创建一个函数 backtrace(),可以打印目前栈上所有被调用的函数的地址。

修改 kernel/defs.hkernel/riscv.h

Add the prototype for backtrace to kernel/defs.h so that you can invoke backtrace in sys_sleep.

The GCC compiler stores the frame pointer of the currently executing function in the register s0. Add the following function to kernel/riscv.h:

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

and call this function in backtrace to read the current frame pointer. This function uses in-line assembly to read s0.

首先在 defs.h 里面需要添加大概一个函数原型:backtrace。很简单,因此不给出代码了。

然后将 Hints 中给出的 r_fp 函数复制到 kernel/riscv.h 的末尾,这个函数的作用是获得当前函数的帧指针(frame pointer)。

大概的原理是通过内嵌汇编,将存储帧指针的寄存器 s0 的值拷贝到一个返回值中。因为这个函数将会是内联的(有 inline 标识),所以获得的将是调用者的帧指针,而不是 r_fp 这个函数的帧指针。

编写 backtrace

The compiler puts in each stack frame a frame pointer that holds the address of the caller's frame pointer. Your backtrace should use these frame pointers to walk up the stack and print the saved return address in each stack frame.

These lecture notes have a picture of the layout of stack frames. Note that the return address lives at a fixed offset (-8) from the frame pointer of a stackframe, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.

想要获得栈上的函数调用列表非常简单,根据题目的提示,只需要将栈上存储的返回地址依次输出即可。

通过调用 r_fp,我们可以获得我们当前函数(backtrace)函数的帧指针,从而获得backtrace 的返回地址(也就是 backtrace 调用结束后即将返回到的地方)

这里有几点需要注意:

  • 在 RISC-V 中,并不是每个函数的栈帧中都一定会存储返回地址。对于叶子调用,也就是不会再进行别的调用的函数,是可以不在栈帧中存放返回地址的,因为 RISC-V 是通过 ra 寄存器获得 ret 时应该跳转的地址的,不需要读取栈中的内容,这和我们熟悉的 x86_64 的规则不同。但是,在 backtrace 这个函数中,我们可以肯定其栈帧中一定会有返回地址,因为它调用了 printf,因而不是一个叶调用
  • 每一个函数栈帧中的返回地址,是其调用者中的地址,而不是它自己的。这一点在逻辑上会有点绕,要理清楚。因而,第一个栈帧中的返回地址没有意义。(也可能根本不会有,我没有仔细研读这里的汇编代码)
  • 实际上,这里要输出的只是进入内核地址空间之后的调用列表,不是一个进程完整的调用列表,所以打印出来的结果中,最后一行也就是第一个被调用的函数一定是 trap.c 中的 usertrap 函数,这个函数是在 uservec 函数中直接 jr 跳转过来的,不是通过调用的形式,因此不会更新 ra 寄存器,因此和上一条得出的结论相同,第一个栈帧中即使存在返回地址,也没有意义。

具体地,对于一个存放了返回地址的栈帧(不存放返回地址的栈帧结构还真不一样),帧指针 p(p - 8) 开始的 \(8\) 个字节是返回地址,(p - 16) 开始的 \(8\) 个字节是上一个栈帧的帧指针。

因此,我们只需要遍历每一个栈帧,输出 (p - 8) 处的返回地址,然后将 \(p\) 更新为 (p - 16) 处的上一个栈帧。

因为在 xv6 中,我们的栈刚好是一个页,而且页的开始地址是和 4KB 对齐的,因此我们一开始的栈指针 sp 初始地址应该是这一页最大的地址加 \(1\)(也就是下一页的开始地址)。

因而,我们在遍历栈帧的时候,就可以知道,当我们的帧指针 p 刚好是一页的首地址时,我们就到达了栈底了,可以退出了。

void backtrace(void) {
  uint64 p = r_fp();
  while (p != PGROUNDUP(p)) {
    uint64 ra = *(uint64*)(p - 8);
    uint64 fp = *(uint64*)(p - 16);
    printf("%p\n", ra);
    p = fp;
  }
}

panicproc_sleep 中添加调用

Implement a backtrace() function in kernel/printf.c. Insert a call to this function in sys_sleep...

Once your backtrace is working, call it from panic in kernel/printf.c so that you see the kernel's backtrace when it panics.

void
panic(char *s)
{
  pr.locking = 0;
  printf("panic: ");
  printf(s);
  printf("\n");
  backtrace();
  panicked = 1; // freeze uart output from other CPUs
  for(;;)
    ;
}
uint64
sys_sleep(void)
{
  int n;
  uint ticks0;

  backtrace();

  if(argint(0, &n) < 0)
    return -1;
  acquire(&tickslock);
  ticks0 = ticks;
  while(ticks - ticks0 < n){
    if(myproc()->killed){
      release(&tickslock);
      return -1;
    }
    sleep(&ticks, &tickslock);
  }
  release(&tickslock);
  return 0;
}

没啥好说的,按题目要求添加即可。

结果展示

./20231116-MIT6-S081-traps/image-20231116212855502

./20231116-MIT6-S081-traps/image-20231116212940787

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.

这个任务大概是要实现一个定时器,这个定时器通过函数 sigalarm(interval, handler) 来设置定时,使得每过 interval 个 tick 就会程序自动调用 handler。一个 tick 是指硬件定时中断的周期。

这个任务要求我们对于系统中断、trapframe 以及中断的处理过程比较熟悉。

让我们跟着 Hints 一步步来。

前期准备

  • You'll need to modify the Makefile to cause alarmtest.c to be compiled as an xv6 user program.

  • The right declarations to put in user/user.h are:

    int sigalarm(int ticks, void (*handler)());
    int sigreturn(void);
    
  • Update user/usys.pl (which generates user/usys.S), kernel/syscall.h, and kernel/syscall.c to allow alarmtest to invoke the sigalarm and sigreturn system calls.

首先,和前几次实验一样,将 alarmtest 加入 Makefile,使得能够被编译且直接通过终端调用。

UPROGS=\
	# ......
	$U/_alarmtest\

然后将上面的 Hints 中给定的 sigalarmsigreturn 的函数原型加入 user/user.h 的末尾,这里就不做演示了。

user/usys.pl 中添加这两个系统调用的入口:

entry("sigalarm");
entry("sigreturn");

kernel/syscall.h 中添加两个系统调用的宏定义符号:

#define SYS_sigalarm    22
#define SYS_sigreturn   23

kernel/syscall.c 中添加两个系统调用的声明:

extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);

static uint64 (*syscalls[])(void) = {
    // .......
[SYS_sigalarm]    sys_sigalarm,
[SYS_sigreturn]   sys_sigreturn,
};

kernel/proc.h 中添加字段

在做这一步前,让我们通览所有的 Hints,来知晓所有需要添加的字段。

  • Your sys_sigalarm() should store the alarm interval and the pointer to the handler function in new fields in the proc structure (in kernel/proc.h).
  • You'll need to keep track of how many ticks have passed since the last call (or are left until the next call) to a process's alarm handler; you'll need a new field in struct proc for this too. You can initialize proc fields in allocproc() in proc.c.
  • Have usertrap save enough state in struct proc when the timer goes off that sigreturn can correctly return to the interrupted user code.
  • Prevent re-entrant calls to the handler----if a handler hasn't returned yet, the kernel shouldn't call it again. test2 tests this.

首先是 alarm_intervalalarm_handler 两个字段,这两个字段的添加比较显然,都是 sigalarm 函数告诉我们的两个参数,一个是定时间隔,一个定时处理函数。

然后,我们还需要添加一个 alarm_tick_count 字段,这个字段告诉我们,距离上一次调用处理函数,已经过去了多少个 tick 了。显然,这个字段的值是用来判断什么时候该调用处理函数的,可以预见的是,在我们即将完成的处理中断的函数中,如果这个值应该会被判断是否和 alarm_interval 相等。

但是还有两个比较重要的字段,可能在一开始不会被考虑到。

显然,在定时器到达间隔后,调用设定好的处理函数,这会打断正常的程序运行顺序,也会改变程序运行时的那些寄存器,如果不加以处理,将会时程序之后的运行过程完全不符合编写者的预期。

因此,我们需要添加一个字段 alarm_trapframe,其类型是 struct trapframe *。我们应该记得,trapframe 是用来在陷阱和中断时,用来保存包括 SEPC 在内的所有用户寄存器,以及必要的内核状态等。这样,在我们处理 sigreturn 调用返回时,就可以将所有的用户状态恢复如初,不影响程序的继续执行,也不影响寄存器内容的一致性了。

此外,我们还需要一个字段 alarm_handling,用来标记这个进程是否正处于处理函数调用状态。只要我们的程序正处于处理函数中,我们就不应该让内核再次调用它。(甚至在我看来,这个时候甚至不应该累加 tick_count

struct proc {
  // ...
  int alarm_interval;
  void (*alarm_handler)();
  int alarm_tick_count;
  int alarm_handling;
  struct trapframe *alarm_trapframe;
};

修改 kernel/proc.c 中的 allocprocfreeproc

allocproc 函数是用来新建一个进程以及为新进程分配资源的,相应的,freeproc 是在进程退出的时候释放资源的。

无论是在 allocproc 还是 freeproc 中,我们新建的字段都应该被重置为 \(0\),除了 alarm_trapframe

trapframe 是需要一定的空间来保存的,因此在 allocproc 中,我们应该为 alarm_trapframe 分配一页空间,在 freeproc 中,我们将这一页空间释放掉。

static struct proc*
allocproc(void)
{
  // ...
  p->alarm_interval = 0;
  p->alarm_handler = 0;
  p->alarm_tick_count = 0;
  p->alarm_handling = 0;
  if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0){
    release(&p->lock);
    return 0;
  }

  return p;
}

static void
freeproc(struct proc *p)
{
  // ...
  p->alarm_interval = 0;
  p->alarm_handler = 0;
  p->alarm_tick_count = 0;
  p->alarm_handling = 0;
  if(p->alarm_trapframe)
    kfree((void*)p->alarm_trapframe);
}

完成 sys_sigalarm

我们应该还记得,sigalarm 这个系统调用,接受两个参数,一个是定时间隔,一个定时处理函数。

sys_sigalarm 函数继续接受这两个参数。回忆一下,所有的系统调用,都需要通过 argintargstrargaddr 这些函数来获取,因此我们可以通过 argint(0, &ticks)argaddr(1, (uint64 *)&handler) 两次调用来获得间隔和处理函数地址。

获得之后,我们将 alarm_interval 设置为相应的间隔,alarm_handler 设置为相应的处理函数。

然后,将 alarm_tick_countalarm_handling 都初始化为 \(0\)。

alarm_trapframe 暂时不需要管,这是在进入处理函数前才需要设置的。

uint64 sys_sigalarm(void) {
  int ticks;
  void (*handler)();
  if (argint(0, &ticks) < 0)
    return -1;
  if (argaddr(1, (uint64 *)&handler) < 0)
    return -1;
  struct proc *p = myproc();
  p->alarm_interval = ticks;
  p->alarm_handler = handler;
  p->alarm_tick_count = 0;
  p->alarm_handling = 0;
  return 0;
}

补全 usertrap

既然是以 tick 为单位的定时,那么我们显然应该在对硬件定时中断的过程中进行定时的判断。

  • Every tick, the hardware clock forces an interrupt, which is handled in usertrap() in kernel/trap.c.

  • You only want to manipulate a process's alarm ticks if there's a timer interrupt; you want something like

    if(which_dev == 2) ...
    

很容易定位到 usertrap 函数中,有一个 else if((which_dev = devintr()) != 0) 的语句。很显然,我们就应该在这个里面补充我们的定时处理。

首先应该判断 p->alarm_interval 是否为 \(0\),如果为 \(0\) 则代表没有设置定时器,因此不应该做任何处理。

接着,还要判断是否正在处于定时处理阶段,如果正在调用处理函数,那么我们不应该继续调用处理函数,甚至不应该去改变 tick_count

如果前面的判断一切顺利,那么我们就要把 p->tick_count 加 \(1\),然后判断是否达到了定时间隔。一旦到达定时间隔,我们就要开始调用处理函数了。

首先,我们要将 p->trapframe 的内容复制到 p->alarm_trapframe 保存,然后将 SEPC 寄存器设置为处理函数的地址,这样在中断返回的时候,就会自动开始执行处理函数中的代码了,最后将 p->alarm_handling 标记为 \(1\) 避免之后重复调用处理函数。

(复习一下,在中断发生的时候,处理器会将原本的 PC 值拷贝到 SEPC 中,而在中断返回的时候,也就是 sret 指令执行的时候,会将 SEPC 的值拷贝进 PC

void
usertrap(void)
{
  // ...
  else if((which_dev = devintr()) != 0){
    // ok
    if (which_dev == 2) {
      if (p->alarm_interval != 0 && !p->alarm_handling) {
        ++p->alarm_tick_count;
        if (p->alarm_tick_count == p->alarm_interval) {
          *(p->alarm_trapframe) = *(p->trapframe);
          p->trapframe->epc = (uint64)(p->alarm_handler);
          p->alarm_handling = 1;
        }
      }
    }
  }
  // ...
  usertrapret();
}

完成 sys_sigreturn

sigreturn 系统调用是用来在处理函数中返回的,只有调用了 sigreturn,处理函数才能正确回到进程原本的执行状态。

那么,首先我们要将我们保存进 alarm_trapframe 中的用户状态存回 p->trapframe 中,以便在中断返回的时候,能够正确将原本的状态写回各个寄存器中。

然后,我们将 alarm_tick_count 清零,重新开始计数。

最后,将 alarm_handling 设置为 \(0\),表示没有正在执行处理函数。

uint64 sys_sigreturn(void) {
  struct proc *p = myproc();
  *(p->trapframe) = *(p->alarm_trapframe);
  p->alarm_tick_count = 0;
  p->alarm_handling = 0;
  return 0;
}

结果展示

alarmtest

./20231116-MIT6-S081-traps/image-20231117113014306

usertests

./20231116-MIT6-S081-traps/image-20231117113047773

./20231116-MIT6-S081-traps/image-20231117113104227

总测试

./20231116-MIT6-S081-traps/image-20231117113659333

标签:kernel,调用,Traps,MIT6,backtrace,alarm,trapframe,printf,S081
From: https://www.cnblogs.com/hankeke303/p/18155279/MIT6-S081-traps

相关文章

  • MIT6.S081 Lab lazy page allocation
    本次实验是有关内存页懒分配的。所谓内存页懒分配,在本实验中,指的是在用户进程使用sbrk()系统调用来增加内存中堆的空间时,我们不直接在物理内存中分配相应的页,而是只是记录了分配到了哪些用户地址,在用户页面表中这些地址默认标记为无效。当进程首次尝试使用任何给定页面的懒惰分......
  • MIT6.S081 Lab syscall
    这一个实验的主要内容就是给xv6添加两个系统调用:trace和sysinfo。Usinggdb(easy)这个部分我就不做了……M1的MacbookAir上的gdb太难安装了,所以暂时用不了gdb调试……Systemcalltracing(moderate)Inthisassignmentyouwilladdasystemcalltracingfe......
  • MIT6.S081 Lab Page Tables
    实验开始前的折腾突然发现2023版的和2020版的实验内容其实还不一样……因为我正在看的视频以及参考资料都是基于2020版的课程,因此我还是决定将之前的实验都迁移到2020版的xv6-lab-2020来。在自己的MacbookAir上折腾了好久……还是没能成功。因此还是用上了我在阿......
  • MIT6.S081 - Lab2: system calls
    Lab2:systemcalls预备知识执行一次系统调用的流程:USERMODEstep1:系统调用声明user/user.h:系统调用函数(如intfork(void))step2:ecall进入内核态user/usys.S(该文件由user/usys.pl生成,后续添加函数可以在这里添加):执行如下命令.globalforkfork:lia7,SYS_f......
  • MIT6824 MapReduce总结
    MapReduce是一个分布式大任务计算框架,旨在可以方便Google内部的将大型任务拆分到集群环境下,以得到并行化的处理速度。在分布式情况下,多台机器协作完成一个大型任务需要考虑很多问题:整个分布式系统中都有哪些角色?可以预见的就是肯定有任务的拆分者负责拆分调度任务,有任务的实际......
  • MIT6.S081 - Lecture3: OS Organization and System Calls
    为什么要使用操作系统使用操作系统的主要原因是为了实现CPU多进程分时复用以及内存隔离如果没有操作系统,应用程序会直接与硬件进行交互,这时应用程序会直接使用CPU,比如假设只有一个CPU核,一个应用程序在这个CPU核上运行,但是同时其他程序也需要运行,因为没有操作系统来帮助......
  • MIT6.S081 - Lab1: Xv6 and Unix utilities
    Part1:sleep实验要求与提示可以参考user/echo.c,user/grep.c和user/rm.c文件如果用户忘记传递参数,sleep应该打印一条错误消息命令行参数传递时为字符串,可以使用atoi函数将字符串转为数字使用系统调用sleep,有关实现sleep系统调用的内核代码参考kernel/sysproc.c(......
  • MIT6.S081 - Lecture1: Introduction and Examples
    课程简介课程目标理解操作系统的设计和实现通过XV6操作系统动手实验,可以扩展或改进操作系统操作系统的目标Abstraction:对硬件进行抽象Multiplex:在多个应用程序之间共用硬件资源Isolation:隔离性,程序出现故障时,不同程序之间不能相互干扰Sharing:实现共享,如数据交互或协......
  • MIT 6.S081入门lab10 mmap
    MIT6.S081入门lab10mmap一、参考资料阅读与总结1.JournalingtheLinuxext2fsFilesystem文件系统可靠性:磁盘崩溃前数据的稳定性;故障模式的可预测性;操作的原子性-论文核心:将日志事务系统加入Linux的文件系统中;事务系统的要求:元数据的更新;事务系统的顺序性;数据块写入磁......
  • MIT 6.S081入门lab8 锁
    #MIT6.S081入门lab8锁一、参考资料阅读与总结1.xv6book书籍阅读(Chapter7:Scheduling:7.5toend)5.sleep与wakeupxv6使用了sleep-wake的机制,实现了进程交互的抽象(序列协调/条件同步机制)这一机制的核心是防止丢失唤醒(生产者还未睡眠时,资源更新并唤醒):如果贸然在睡眠中加......