6.S081|Lab4-Traps
本文最后更新于 119 天前,其中的信息可能已经有所发展或是发生改变。

RISC-V assembly

以下内容摘自知乎用户rocketeerli

首先,执行 make fs.img 指令,进行编译。然后查看生成的 user/call.asm 文件,其中的 main 函数如下:

Pasted image 20241015000959.png

这部分没有需要写的代码,主要根据这个编译生成的代码,回答几个问题。

这里直接按照中文翻译了。

问题一

Q: 哪些寄存器存储了函数调用的参数?举个例子,main 调用 printf 的时候,13 被存在了哪个寄存器中? A: A1. a0-a7, a2.

根据第 45 行代码,可以看到 13 被放到了 a2 寄存器中。猜测是 a0-a7 寄存器保存参数。

问题二

Q: main 中调用函数 f 对应的汇编代码在哪?对 g 的调用呢? (提示:编译器有可能会内联(inline)一些函数) A: nowhere, compiler optimization by inline function.

其实是没有这样的代码。 g(x) 被内联到 f(x) 中,然后 f(x) 又被进一步内联到 main() 中。所以看到的不是函数跳转,而是优化后的内联函数。

问题三

Q: printf 函数所在的地址是? A: 0x0000000000000630 (ra=pc=0x30, 1536(ra)=0x0000000000000630).

其实,直接在 user/call.asm 代码中一直找,就能找到 printf 函数的地址。

也可以通过计算得到,首先将当前程序计数器的值赋给 ra 寄存器。auipc ra, 0x0,是指将当前立即数向右移动12位,然后加上 pc 寄存器的值,赋给 ra 寄存器,由于立即数为 0,因此 ra 的值即为 pc 的值。当前指令在0x30处,因此 pc = 0x30。1528(ra) 是指 1528 加上 ra 寄存器的值,1536 转为16进制再加上0x30 即为 0x0000000000000628。刚好是 printf 的地址。

问题四

Q: 在 main 中 jalr 跳转到 printf 之后,ra 的值是什么? A: 0x38(ra=pc+4).

jalr 指令会将 pc + 4 赋给当前寄存器,刚好是其下一条指令的地址。

问题五

Q: 运行下面的代码

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

输出是什么? 如果 RISC-V 是大端序的,要实现同样的效果,需要将 i 设置为什么?需要将 57616 修改为别的值吗?

A: He110 World, 0x726c6400, no change for 57616.

%x 表示以十六进制数形式输出整数,57616 的16进制表示就是 e110,与大小端序无关。 %s 是输出字符串,以整数 i 所在的开始地址,按照字符的格式读取字符,直到读取到 '\0' 为止。当是小端序表示的时候,内存中存放的数是:72 6c 64 00,刚好对应rld。当是大端序的时候,则反过来了,因此需要将 i 以16进制数的方式逆转一下。

问题六

Q: 在下面的代码中,'y=' 之后会答应什么? (note: 答案不是一个具体的值) 为什么?

printf("x=%d y=%d", 3);

A: print the value of a2 register.

printf 接收到了两个参数,但实际需要三个参数,最后一个参数是放在 a2 寄存器中,由于没有输入第三个参数,因此 a2 寄存器中目前有啥就输出啥。

Backtrace (moderate)

这个实验是让我们完成一个函数调用跟踪

首先我们阅读题目获取信息,题目让我们先将backtrace()这个函数的定义加到kernel/defs.h里面,以便于后面sys_sleep()函数获取他。

接着,根据hint,我们知道当前的帧指针被保存在s0里面,我们想要获取就得使用内联汇编函数,不过题目已经给出,我们按照要求将其加入到kernel/riscv.h即可。

//kernel/riscv.h
...

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

...

xv6会为每个进程分配一页的栈,其中包含着许多栈帧(stack frame),每个frame里面都会包含此函数的一些信息。如下图所示

高地址  
  
Stack  
                   .  
                   .  
      +->          .  
      |   +-----------------+   |  
      |   | 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 ------+ <-- 如果是 x86,那么 bp 指针会指向这里  
          | saved registers |  
  $sp --> | local variables |  
          +-----------------+  
  
低地址(增长方向)

所以我们只需要不断的找栈帧内保存的上一个frame pointer的数据就可以实现函数调用顺序的打印了。

请注意,在RISCV中,返回地址位于距当前stack frame的帧指针的固定偏移量 (-8) 处,而保存的帧指针位于距当前stack frame的帧指针的固定偏移量 (-16) 处。

Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address.一连串的函数调用最多放在一个页中。那么如果我们在递归打印的时候,可以通过宏定义的PGROUNDDOWNPGROUNDUP来限定页的范围,超出了这一页的范围,就可以说明已经是最底层的函数,可以停止了。

由以上信息不难写出

void
backtrace()
{
  uint64 fp = r_fp();
  uint64 top = PGROUNDUP(fp);
  uint64 bottom = PGROUNDDOWN(fp);
   printf("backtrace:\n");
  for (;bottom < fp && fp < top;) {
    printf("%p\n", *(uint64 *)(fp - 8));
    fp = *(uint64*)(fp -16);
  }
}

Alarm (hard)

这个task是实现一个用户级的timer?(不知道这么叫是否合适),大概意思就是设定两个参数,间隔n和函数handler,代表着每间隔n个tick,就会调用一次handler()函数。

在写代码之前,我们先按照要求添加这两个系统调用,具体过程和lab syscalls一样,这里不再赘述。

我们这次主要要修改的地方在kernel/sysproc.c(实现两个系统调用的地方),kernel/trap.c(利用hardware clock配合实现的系统调用完成功能),kernel/proc.h(为proc定义新的有用的数据)里面。

根据我个人写实验总结的习惯,我还是会将我的思考路线尽可能还原出来。

test0

我们首先来看test0,首先hint告诉我们先不用管sys_sigreturn(),直接让其return 0即可,我们按照提示加代码就行了

//kernel/sysproc.c
int
sys_sigreturn(void)
{
return 0;
}

接着,hint提示我们,在sys_sigalarm()函数中,应该像proc结构体里面存放间隔和指向handler()的指针。

虽然现在还不知道有什么用,但是在kernel/proc.h的proc结构体里面加入如下两行。

//kernel/proc.h
struct proc {
...
int nticks // 用来存放间隔
uint64 addFunc // 用来存放函数指针
...
}

接着在sys_sigalarm()里面增加存储过程

//kernel/sysproc.c
int
sys_sigalarm(void)
{
struct proc* p = myproc();

int ticks;
uint64 addFunc; // 函数指针也是地址,所以我们定义uint64用来接收

argint(0, &ticks);
argaddr(1, &addFunc);

p->nticks = ticks;
p->addFunc = addFunc;


return 0;
}

这样就好了。不过我们还得在kernel/proc.callocproc()里面添加这两个值的初始化。都将其设置为0即可


static struct proc*
allocproc(void)
{
...

found:
...
p->nticks = 0;
p->addFunc = 0;
...
}

riscv 的硬件(其实我不太确定是哪个硬件)会每过一个时钟周期都产生一个时钟中断,而 trap.c 会处理这个中断。以此我们可以通过不断的进入usertrap()来确定我们到底过去了多少个ticks,现在是不是可以慢慢理解为什么要像proc结构体里面加入nticks这个数据了?猜测是为了检测是否进入了usertrap()这么多次,所以我们还需要往结构体里面加一个sinceticks的参数,用于记录在上一次调用handler过后,我们经历了多少个ticks,如果经历sinceticks = nticks,那我们就继续调用handler

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()的代码一起理解,就可以发现这是干什么的。变量 int which_dev = 0; 在 usertrap() 函数中的作用是用于记录设备中断的类型,帮助确定当前陷阱是否由设备中断引起,以及具体是由哪个设备触发的。

//kernel/trap.c
int which_dev = 0;

  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);

  struct proc *p = myproc();
  
  // save user program counter.
  p->trapframe->epc = r_sepc();
  
  if(r_scause() == 8){
    // system call

    if(p->killed)
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;

    // an interrupt will change sstatus &c registers,
    // so don't enable until done with those registers.
    intr_on();

    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
  }

  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
	  yield();

  usertrapret();

于是综合以上信息,我们可以在usertrap()里面写出如下逻辑

// give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    if (p->alarmlock == 0 && p->nticks > 0) {
      p->sinceticks ++;
      if (p->sinceticks == p->nticks) {
      p->sinceticks = 0;
      // 调用handler,但是此时我们还不知道怎么调用
      }
    }

现在有点困惑,如何调用handler呢?我一开始想的是在里面调用sys_sigreturn(),因为这个函数名听起来像是返回东西的函数(?)但是继续阅读hint便可以发现

When a trap on the RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?

恍然大悟,在trap的时候,系统会保存发生trap的pc位置,当trap执行usertrapret()返回的时候,便会将保存下来的pc赋给pc寄存器,这样就可以正确的返回了。但是我们现在并不想回到发生trap的位置,而是想进入handler函数处理。RFTM之后,我们发现sepc这个寄存器保存着要返回的pc信息。

一开始我是这么写的

w_stepc(p->addFunc);

发现根本没用,感觉很奇怪,但是转念又想,这个操作应该是在usertrapret()函数里面被完成的(不要问我为什么,想一想就知道了),于是过去查看代码,发现这段代码之后恍然大悟。原来是通过读取trapframe里面的值来设置sepc

//kernel/trap.c
// set S Exception Program Counter to the saved user pc.
  w_sepc(p->trapframe->epc);

知道了这一点之后,我们只需要修改p->trapframe->epc就可以了

思考一个问题,为什么usertrapret()会从p->trapframe->epc恢复epc寄存器?这说明了什么?

我们来看一下以下代码

//kernel/trap.c/usertrap()
if(r_scause() == 8){
    // system call

    if(p->killed)
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;

    // an interrupt will change sstatus &c registers,
    // so don't enable until done with those registers.
    intr_on();

    syscall();
  }

注意,进入trap的时候系统中断会被关闭,防止在执行trap的时候又有中断进入,影响一些寄存器等数据的保存。

当需要强制执行陷阱时,RISC-V硬件对所有陷阱类型(计时器中断除外)执行以下操作:

  • 如果陷阱是设备中断,并且状态SIE位被清空,则不执行以下任何操作。

    1. 清除SIE以禁用中断。

    2. 将pc复制到sepc。

    3. 将当前模式(用户或管理)保存在状态的SPP位中。

    4. 设置scause以反映产生陷阱的原因。

    5. 将模式设置为管理模式。

    6. 将stvec复制到pc。

    7. 在新的pc上开始执行。

由于syscall()这个操作可能会非常耗时,所以我们在执行syscall之前就显性的打开了系统中断,但是如果又有别的程序调用了系统调用怎么办?所以我们就需要将一些关键的寄存器保存下来,于是变有了以上的设置。将数据保存到进程的trapframe页里面。

所以现在我们的usertrap()里面是这样的

// give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    if (p->alarmlock == 0 && p->nticks > 0) {
      p->sinceticks ++;
      if (p->sinceticks == p->nticks) {
      p->sinceticks = 0;
      p->trapframe->epc = p->addFunc; // 这里卡了好久,以知不知道怎么执行,之前写成了w_stepc(p->addFunc),知道看了usertrapret()的代码才发现问题所在
      }
    }
    
  }

这样的话,应该就可以通过test0了。

test1/2:resume interrupted code

这两个test要求我们在完成handler之后,恢复我们之前代码中断时的状态。这就要求着我们储存一些必要的信息以便于恢复的时候用。

首先我想肯定要保存epc,因为这是我们回到中断代码的必要条件,如果回不到之前的代码,那一切都无从谈起了。除此之外,我们还需要恢复寄存器的状态,因为我们知道,跳转到别的函数的时候有些寄存器是会丢失的,所以我们必须把它存起来,也就是我们说的恢复中断时的状态。

所以我们在proc.h里面加上一个trapframe结构体(后面我会解释为什么定义成struct trapframe,而不是和trapframe一样定义成指针),用于恢复寄存器

...
uint64 alarmepc // 用于备份epc
struct trapframe alarmframe // 用于备份trapframe
...

然后继续在usertrap()sys_sigreturn()这两个函数里面完成备份和恢复的逻辑

//usertrap()
// give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    if (p->alarmlock == 0 && p->nticks > 0) {
      p->sinceticks ++;
      if (p->sinceticks == p->nticks) {
      p->sinceticks = 0;
      p->alarmepc = (uint64)r_sepc;
      p->alarmframe = *p->trapframe;
      p->trapframe->epc = p->addFunc; // 这里卡了好久,以知不知道怎么执行,之前写成了w_stepc(p->addFunc),知道看了usertrapret()的代码才发现问题所在
      }
    }
  }
//sys_sigreturn()
uint64
sys_sigreturn(void) {

struct proc* p = myproc();

w_sepc(p->alarmepc);
*p->trapframe = p->alarmframe;
return 0

但是到这里,我们发现还是不能通过测试,照理来说应该已经没问题了呀?

试想这样一个情况,如果 handler 执行的特别慢,自从上次调用 handler 已经过去了规定的时钟周期,但是 handler 还没执行好,这个时候我们又去改一遍 epc,这个 handler 又从头开始执行了,那着不就出大问题了,因为我们每次都会去改 epc,然后就永远执行不完 handler 了。

测试程序里就包括了这个情况:

void  
slow_handler()  
{  
  count++;  
  printf("alarm!\n");  
  if (count > 1) {  
    printf("test2 failed: alarm handler called more than once\n");  
    exit(1);  
  }  
  for (int i = 0; i < 1000*500000; i++) { // 超慢的 handler  
    asm volatile("nop"); // avoid compiler optimizing away loop  
  }  
  sigalarm(0, 0);  
  sigreturn();  
}

所以我们需要在 struct proc 里再加一个属性,就是 alarmlock。如果这个属性为 1,就表示,handler 程序正在执行,这个时候就算又过了 tick 个时钟周期,我们也不能去改 epc 让 handler 重复执行。

//usertrap()
// give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    if (p->alarmlock == 0 && p->nticks > 0) {
      p->sinceticks ++;
      if (p->sinceticks == p->nticks) {
      p->sinceticks = 0;
      p->alarmepc = (uint64)r_sepc;
      p->alarmframe = *p->trapframe;
	  p->alarmlock = 1; // 加锁
      p->trapframe->epc = p->addFunc; // 这里卡了好久,以知不知道怎么执行,之前写成了w_stepc(p->addFunc),知道看了usertrapret()的代码才发现问题所在
      }
    }
  }
//sys_sigreturn()
uint64
sys_sigreturn(void) {

struct proc* p = myproc();

w_sepc(p->alarmepc);
*p->trapframe = p->alarmframe;
p->alarmlock = 0; // 释放锁
return 0

但是接下来我将要说一下为什么不把alarmframe定义成结构体指针的原因。

首先来看为什么trapframe定义成了指针,因为在系统初始化分配空间的时候,allocproc()这个函数就给trapframe分配了一段空间,所以我们只需要用指针引用即可,但是如果我们也定义成了指针,还没有分配空间的话,就会出错。如果我们不用指针,就是直接在结构体proc里面给alarmframe分配空间,这样这段空间就会随着结构体的释放而释放,不需要我们手动freeproc()的。

allocproc()的这段代码代码给trapframe分配了空间,并将proc结构体里的指针指向这段空间。

// Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇