首页 > 其他分享 >[MIT 6.S081] Lab: traps

[MIT 6.S081] Lab: traps

时间:2024-01-30 20:44:40浏览次数:23  
标签:sp proc alarm ticks Lab trapframe void S081 MIT

Lab: traps

RISC-V assembly

在这个任务中我们需要观察 call.asm 汇编。

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 %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>
  exit(0);
  38:	4501                	li	a0,0
  3a:	00000097          	    auipc	ra,0x0
  3e:	27e080e7          	    jalr	638(ra) # 2b8 <exit>

Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
哪些寄存器包含函数的参数?例如,在 main 对 printf 的调用时,哪个寄存器保存了 13

哪个寄存器保存了 13 ,只需要看一下执行 printf 调用中过程的汇编指令即可。由 24: 4635 li a2,13 可知,13 保存在 a2 寄存器中。

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 的调优化,更像是 constexpr),观察 main 中对 printf 的调用,可以看到,对于 f 的调用直接优化成了一条语句 li a2, 13 ,在 f 中将对 g 的调用直接优化为一条执行语句了。

At what address is the function printf located?
printf 的地址位于哪里?

直接使用编辑器搜 <printf>: ,就可以直接看到地址了,位于 0x630 ,或者要用 gdb 看也可以。

What value is in the register ra just after the jalr to printf in main?
mainjalrprintf 之后,寄存器 ra 中的值是什么?

观察一下两句语句:

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

指令 auipc rd, imm 的作用是 rd = pc + (imm << 12) ,对于 imm << 12 ,左移的位置将会补 0 。对于此处来说,pc = 0x30 ,因此 ra = pc + (0x0 << 12) , 即此处 ra = 0x30

指令 jalr 会将接下来要执行的语句地址压入指定的寄存器中,然后跳转到指定的偏移地址,这里也就是将 pc+4 压入 ra 中,即 ra = pc + 4 = 0x34 + 4 = 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?

运行以下代码,它会输出什么?
如果 RISC-V 不是小端而是大端,那 i 要怎么样才可以保持输出一致?

57616 的十六进制就是 0xe110 ,也就是这里会输出 E110

这里其实就是将 i 作为一个 char 数组输出了,由于小端,按照地址增长的方向读取 i 会得到 72 6c 64 00 ,也就是 rld\0 。如果是大端形式,那么按照地址增长方向,i 的高位字节会被先读,因此要反过来,为 0x726c6400

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

在这段代码中,y 会输出什么?为什么会发生这种情况?

这里按照函数参数寄存器来说,"x=%d y=%d" 的指针位于 a0 中,自然 3 位于 a1 中,那么 y 应该是来自于 a2 ,既然没有传递给 a2 ,那么意味着之前调用某个函数时候 a2 中留下来的值将会直接被读取,或者说读了个 a2 寄存器中的脏数据。

Backtrace

在这个任务中要实现的是:打印函数调用栈内的内容。根据 xv6 中栈的形式,不断循环地址,直到结束即可。

xv6 中,内核为每个栈按照内存对齐的格式分配了一个页面,所以只需要判断遍历的时候这个指针是否依然还在一个页面内即可。

接下来就是这个栈的格式,其增长方向是从高地址到低地址的,每一次函数调用,都会产生一个 stack frame ,即栈帧,我们所要做的,就是知道栈顶栈帧的位置,然后通过其中的 fp 遍历,并输出其中的 return address 。对于栈顶栈帧的位置,可以使用所提供的函数 r_fp() 获取到,然后按照地址偏移,返回地址就 -8 ,下一个 fp 就 -16 ,直到遍历到页面结束为止。

void backtrace(void) {
  printf("backtrace:\n");

  uint64 fp = r_fp();

  while (PGROUNDUP(fp) - PGROUNDDOWN(fp) == PGSIZE) {
    uint64 ret_addr = fp - 8;
    uint64 nex_fp_addr = fp - 16;

    printf("%p\n", *(pte_t*)ret_addr);

    fp = *(pte_t*)nex_fp_addr;
  }
}

Alarm

在这个子任务中,所要实现的是一个定时的警报功能,当达到一定时间时,将调用用户指定的函数。

我们需要看一下在 xv6 中一个系统调用大致是什么流程。

  1. 在用户态下,执行 ecall 指令来切换模式,并将 pc 的值保存在 sepc 寄存器中,然后再跳转到 stvec 寄存器指向的指令;
  2. 我们要保证之后回到用户模式的时候,依然是原先的环境,那么就先把 32 个用户寄存器的内容保存在 trapframe page 中;
  3. 现在将调用 usertrap ,然后将 sepc + 4 保存在 p->trapframe 中(这个 trapframe 也是上面寄存器保存的位置),当恢复到用户模式时将执行下一条语句,而不是继续执行 ecall
  4. 通过 syscall() 函数分发到具体的 syscall 函数中;
  5. 等待系统调用执行完毕后,将执行 usertrapret
  6. 最后执行 userret ,将之前备份的寄存器重新赋值回去,并使用 sret 返回和切换模式。

我们目前添加的新系统调用涉及到这个过程,需要按照这个过程去构建整个功能。

test0: invoke handler

test0 只需要按部就班实现即可。

首先是添加系统调用的流程,添加 Makefile ,把两个系统调用的函数声明添加到 user/user.h 中,然后修改 user/user.S, 让它能够生成调用系统调用的入口代码。接着转到 kernel 部分,在 kernel/sycall.h 中添加两个系统调用的编号,在 kernel/syscall.c 中让 syscall() 函数能够调用到指定的函数,在 kernel/sysproc.c 中定义以下两个系统调用。

uint64 sys_sigalarm(void) {

}

uint64 sys_sigreturn(void) {
  return 0;
}

接着我们需要去实现计时的功能,在 kernel/proc.h 中添加这几个成员,作用分别是:记录已经过了几个时钟周期,一次报警所需要的时钟周期,报警所调用的函数指针。

struct proc {
  ...
  uint64 alarm_pass_ticks;
  uint64 alarm_ticks_period;
  void (*alarm_handler_fn)();
};

接着还是构造和析构的问题,不过这三个并不涉及资源分配,只需要初始化时候清为 0 ,析构的时候也是即可。

static struct proc*
allocproc(void)
{
  ...
  p->alarm_pass_ticks = 0;
  p->alarm_ticks_period = 0;
  p->alarm_handler_fn = 0;

  return p;
}

static void
freeproc(struct proc *p)
{
  ...
  p->alarm_pass_ticks = 0;
  p->alarm_ticks_period = 0;
  p->alarm_handler_fn = 0;
}

这只是构造和析构的问题,我们需要看一下怎样去传递所需要的参数,回到 sys_sigalarm 函数,我们将两个参数传递给两个指定的成员。

uint64 sys_sigalarm(void) {
  int ticks;
  uint64 fn_addr;

  if (argint(0, &ticks) < 0 || argaddr(1, &fn_addr) < 0)
    return -1;

  struct proc* cur_proc = myproc();
  cur_proc->alarm_ticks_period = ticks;
  cur_proc->alarm_handler_fn = (void(*)())fn_addr;

  return 0;
}

然后去 kernel/trap.c 中的 usertrap 函数,看向判断时钟中断的位置,即 which_dev == 2 这个地方,我们要在这边处理一下这个如何记时。按照手册提示,每个时钟周期,cpu 都会产生一次时钟中断,将控制权转交给内核,因此在这里,我们只需要判断一下目前是否需要产生警报,需要的话就开始累计 +1 即可。

当达到指定的周期数时,我们就需要在返回用户模式的时候执行的是指定的代码,因此将 trapframe->epc 赋值为指定的地址,这样返回时拷贝后,sretsepc 内的内容给 pc ,执行的代码就是它了。

void
usertrap(void)
{
  ...

  if (which_dev == 2) {
    if (p->alarm_ticks_period != 0) {
      p->alarm_pass_ticks ++;
      if (p->alarm_pass_ticks == p->alarm_ticks_period) {
        p->trapframe->epc = (uint64)p->alarm_handler_fn;
        p->alarm_pass_ticks = 0;
      }
    }
    yield();
  }

  usertrapret();
}

test1/test2(): resume interrupted code

如果注意到的话,在 test0 中其实有一个问题:一旦开始报警,那么是不是没办法返回到原先的环境呢?这个子任务就是要解决这问题的。

首先,我们需要再一次保存寄存器,第一次保存寄存器是为了在内核模式下执行 c 代码而不会将原本的用户模式环境更改,这一次保存寄存器则是要让在之前调用 alarm 的函数环境能得到保留,使得之后返回时候和之前环境一样。因此,我们还需在 struct proc 中添加两个成员,is_alarmed 代表是否已经在警报模式中,alarm_trapframe 则是保存调用 alarm 时候的 trapframe

struct proc {
  int is_alarmed;
  uint64 alarm_pass_ticks;
  uint64 alarm_ticks_period;
  void (*alarm_handler_fn)();
  struct trapframe *alarm_trapframe;
};

接着还是构造和析构的问题,is_alarmed 直接初始化为 0 即可,alarm_trapframe 像保存模式切换下的 trapframe 一样给一段内存即可。

static struct proc*
allocproc(void)
{
  ...
  if ((p->alarm_trapframe = (struct trapframe*)kalloc()) == 0) {
    release(&p->lock);
    return 0;
  }
  p->is_alarmed = 0;
  p->alarm_pass_ticks = 0;
  p->alarm_ticks_period = 0;
  p->alarm_handler_fn = 0;

  return p;
}

static void
freeproc(void)
{
  ...
  if (p->alarm_trapframe)
    kfree((void *)p->alarm_trapframe);
  p->is_alarmed = 0;
  p->alarm_trapframe = 0;
  p->alarm_handler_fn = 0;
  p->alarm_pass_ticks = 0;
  p->alarm_pass_ticks = 0;
 ...
}

然后因为我们要在被调用的时候就保存 trampframe ,当然是在刚刚的 which_dev == 2 那边,因为那边涉及到寄存器的状态更改了。我们需要在 sepc 更改之前将其保存一下。

void
usertrap(void)
{
  ...
  if(which_dev == 2) {
    if (p->alarm_ticks_period != 0) {
      p->alarm_pass_ticks ++;
      // jump to handler_fn
      if (!p->is_alarmed && p->alarm_pass_ticks == p->alarm_ticks_period) {
        p->is_alarmed = 1;
        p->alarm_pass_ticks = 0;
        *p->alarm_trapframe = *p->trapframe;
        p->trapframe->epc = (uint64)p->alarm_handler_fn;
      }
    }
    yield();
  }

  usertrapret();
}

最后,在 sys_sigreturn 中,我们要将代码执行的环境重新回归到之前的函数,也就是直接把这个 alarm_handler_fntrapframe 就好了。

uint64 sys_sigreturn(void) {
  struct proc* cur_proc = myproc();

  if (cur_proc->is_alarmed) {
    cur_proc->is_alarmed = 0;
    *cur_proc->trapframe = *cur_proc->alarm_trapframe;
    cur_proc->alarm_pass_ticks = 0;
  }
  
  return 0;
}

Grade

标签:sp,proc,alarm,ticks,Lab,trapframe,void,S081,MIT
From: https://www.cnblogs.com/FlandreScarlet/p/17997930

相关文章

  • GitLab 首席产品官谈 DevSecOps 在 AI 时代的四大趋势
    内容来源:about.gitlab.com作者:GitLab首席产品官DavidDeSantoDevSecOps中的AI变革已经到来,你做好准备了吗?利用AI来加速创新并提高客户价值对于在AI驱动的市场中保持竞争力至关重要。AI在软件研发领域中的作用来到了关键时刻——这将迫使组织及其DevSecOps领导......
  • Gitlab 权鉴相关
    解决思路1、首先登录gitlab服务器执行以下命令:sudogitlab-railsconsole-eproduction 2、通过命令查找,确定用户为“root”#以下这两个命令都可以,随便输入一个user=User.where(id:1).firstuser=User.where(name:"root").first 3、将root用户密码重置为admin123......
  • Walrus 实用教程|Walrus + Gitlab,打通CI/CD 自动化交付!
    Walrusfile是Walrus0.5版本推出的新功能,用户可以通过一个非常简洁的YAML描述应用或基础设施资源的部署配置,然后通过WalrusCLI执行walrusapply或在WalrusUI上进行import,将Walrusfile提交给Walrusserver,由Walrusserver完成对应用或基础设施资源的部署/配置/......
  • 手摸手教你 docker+jenkins+gitlab 部署你的前端项目
    手摸手教你docker+jenkins+gitlab部署你的前端项目 :https://blog.csdn.net/wx17343624830/article/details/128137274?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-128137274-blog-132392231.235^v43^control&spm=1001.2......
  • gitlab-runner helm 安装问题记录
      首先说说安装步骤1、安装版本gitlab-runner-0.36.1.tgz  cat./gitlab-runner/values.yaml|grep-v"#"|sed-e'/^$/d'imagePullPolicy:IfNotPresentgitlabUrl:http://101.43.196.155:32080/runnerRegistrationToken:"Woq_Drxy-SSy1kQzJBZT&......
  • java用多线程批次查询大量数据(Callable返回数据)方式
    我看到有的数据库是一万条数据和八万条数据还有十几万条,几百万的数据,然后我就想拿这些数据测试一下,发现如果用java和数据库查询就连一万多条的数据查询出来就要10s左右,感觉太慢了。然后网上都说各种加索引,加索引貌似是有查询条件时在某个字段加索引比较快一些,但是毕竟是人家的库不......
  • Jenkins + Gitlab 前后端项目自动化构建部署
    Jenkins+Gitlab前后端项目自动化构建部署:https://blog.csdn.net/IT_ZRS/article/details/115032509?spm=1001.2014.3001.5501Docker+Jenkins+Gitlab自动化构建部署:https://blog.csdn.net/IT_ZRS/article/details/117533847?spm=1001.2101.3001.6650.1&utm_medium=distrib......
  • javax.annotation.Nullable找不到
    您需要包括一个存在该类的罐子。您可以在这里找到它如果使用Maven,则可以添加以下依赖项声明:<dependency><groupId>com.google.code.findbugs</groupId><artifactId>jsr305</artifactId><version>3.0.2</version></dependency>对于Gradle:dependencies......
  • labview静态和动态装载子Vi
    学习《Labview的编程经验》的笔记静态装载子Vi静态装载子Vi:运行一个vi时,将这个vi的子vi全部加载到内存。对于小型程序,影响不大。对于大型程序(子vi过多),会有两个问题,一是占用内存过大问题,二是程序运行时的启动速度过慢问题。动态装载子Vi动态装载子Vi:程序启动时,不载入这些子V......
  • Git取消add 、 commit、push的命令
    1.撤销已经add,但是没有commit的问题gitresetHEAD2.撤销已经commit,但是没有push到远端的文件(仅撤销commit保留add操作)撤销上一次的提交gitreset--softHEAD^ windows系统使用提示more,需要多加一个^(windows当中^才是换行符?)gitreset--softHEAD^^按照输入的数字撤销输入......