MIT6.S081 - Lab4(Trap)实验笔记

本篇是 MIT6.S081 操作系统课程 Lab4 的实验笔记,Lab4 的两道题难度并不高,更重要的是 Lec5 和 Lec6 课程中,对 xv6 在 RISC-V 上的 Trap 流程进行学习。

在开始实验之前,你需要:

  1. 观看 Lecture 5 课程录播视频:RISC-V Calling Convention and Stack Frames
  2. 阅读 《xv6 book》 第四章:陷阱指令和系统调用
  3. 观看 Lecture 6 课程录播视频:Isolation & System Call Entry/Exit

RISC-V assembly (easy)

代码切换到 traps 分支,仓库中有个 user/call.c 文件

c 复制代码
int g(int x) {
  return x+3;
}

int f(int x) {
  return g(x);
}

void main(void) {
  printf("%d %d\n", f(8)+1, 13);
  exit(0);
}

执行 make fs.img 以后,它会被编译为 call.asm 汇编文件。

asm 复制代码
user/_call:     file format elf64-littleriscv

Disassembly of section .text:

0000000000000000 <g>:

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>

阅读 call.asm 中的 g()f()main() 三个函数代码的,回答以下问题。

Q:哪些寄存器包含传递给函数的参数?例如,在 main() 调用 printf() 时,哪个寄存器中包含 13?

a0 ~ a7 共 8 个参数寄存器,13 保存在 a2 RISC-V 函数参数优先使用寄存器传递,返回值可以放在 a0a1 寄存器,如果函数参数超过寄存器梳理,额外的参数会被存放在栈上。

Q:在 main() 函数的汇编代码中, f() 函数的调用在哪里? g() 函数的调用又在哪里?(提示:编译器可能会内联函数。)

正如提示所言,汇编里面没有对 f()g() 的调用(没有看到 calljalr 指令跳转到这些函数) 由于 f()g() 函数都很简单( f() 调用 g()g() 只是返回参数 +3),编译器进行了函数内联优化,直接把计算结果 f(8)+1 优化成了常量 12。

Q:函数 printf() 位于什么地址?

printf() 函数位于地址 0x630

  1. auipc ra,0x0 - 将当前 PC(程序计数器)的值加上立即数(这里是0)存入 ra 寄存器
  2. jalr 1536(ra) - 跳转到 ra + 1536 的地址,并将返回地址保存在 ra

Q:在 jalrprintf()main() 之后,寄存器 ra 中的值是什么?

ra 寄存器中保存的值是 0x38 ,也就是 jalr 指令后面那条指令的地址( exit(0) 的第一条指令的地址)

Q:运行以下代码,输出是什么?如果 RISC-V 是大端序,你需要将 i 设置为什么值才能得到相同的输出? 57616 的值是否需要改变?

c 复制代码
   unsigned int i = 0x00646c72;
   printf("H%x Wo%s", 57616, &i);

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

输出是: HE110 World 。在大端序下, i 应该设置为 0x726c6400,57616 的值不需要改变,因为:

  1. 这个数字是直接用于十六进制打印( %x )
  2. 数字的表示方式不受字节序的影响,输出仍然是 E110

Q:在下面的代码中,在 'y=' 之后将打印什么?(注意:答案不是一个具体的值。)为什么会这样?

c 复制代码
   	printf("x=%d y=%d", 3);
  • x= 后会打印 3,y= 后会打印一个随机值,具体是什么值取决于上一次 a2 寄存器保存的是什么

Backtrace (moderate)

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() 函数并在 kernel/printf.c 中插入对该函数的调用,然后运行 sys_sleepsys_sleep 会调用 bttest 。你的输出应如下所示:

makefile 复制代码
backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

标的中等难度,实际做起来挺简单的,思路:

  • 每个函数调用都会在栈上创建一个栈帧(stack frame)
  • 栈帧中包含:
    • 返回地址(return address):在帧指针-8的位置
    • 上一个栈帧的帧指针(frame pointer):在帧指针-16的位置

首先打开 kernel/riscv.h 文件,在底部追加获取帧指针的函数

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

在 kernel/defs.h 中添加函数声明

c 复制代码
// printf.c
...
void            printfinit(void);
void            backtrace(void);  // 新增

接着,kernel/printf.c 实现 backtrace() 函数

c 复制代码
void
backtrace(void)
{
  printf("backtrace:\n");
  
  uint64 fp = r_fp();
  uint64 top = PGROUNDUP(fp);
  uint64 bottom = PGROUNDDOWN(fp);
  
  // 遍历栈帧
  while(fp >= bottom && fp < top) {
    uint64 ra = *(uint64*)(fp - 8);  // 返回地址在 fp-8
    printf("%p\n", ra);
    fp = *(uint64*)(fp - 16);  // 上一个帧指针在 fp-16
  }
}

最后一步, kernel/sysproc.c 的 sys_sleep 中增加调用

csharp 复制代码
uint64
sys_sleep(void)
{
  int n;
  uint ticks0;
  
  backtrace();  // 添加调用
  
  ...
}

搞定,主要工作在 backtrace() 函数中,挺简单的,运行 bttest 查看结果

makefile 复制代码
$ bttest
backtrace:
0x0000000080002ce4
0x0000000080002bbe
0x00000000800028a8

测试通过,完整的代码在:github.com/yibaoshan/x...

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.

写个定时器程序,实现两个系统调用:

  1. sigalarm(interval, handler) - 设置定时器,每经过 interval 个时钟周期就调用一次 handler() 函数
  2. sigreturn() - handler() 函数执行完后返回原来的执行位置

这道题也不是很难,代码量比上一题多一点,主要考察系统调用的实现 (Lab2已经做过了)、用户态和内核态的切换如何保存和回复进程的上下文 、以及 处理时钟中断 这几点。

首先,按照实验要求,在 user/user.h 中添加两个系统调用

c 复制代码
// system calls
int fork(void);
...
int uptime(void);
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);                       

在 kernel/sysproc.c 底部实现系统调用

c 复制代码
uint64
sys_sigalarm(void)
{
  int interval; // 定时器的间隔
  uint64 handler; // 处理函数的地址

  if(argint(0, &interval) < 0)
    return -1;
  if(argaddr(1, &handler) < 0)
    return -1;
    
  struct proc *p = myproc();

  // 初始化定时器
  p->alarm_interval = interval;
  p->alarm_handler = (void(*)())handler;
  p->ticks_count = 0;
  p->alarm_on = 0;
  
  return 0;
}

uint64
sys_sigreturn(void)
{
  struct proc *p = myproc();
  *p->trapframe = *p->alarm_trapframe; // 恢复保存的上下文!!!
  p->alarm_on = 0; // 清除执行标志,允许下一次定时器触发
  return 0;
}

接着需要为 xv6 增加两个系统调用,参考 lab2 的过程,我这里写到一起了

c 复制代码
user/usys.pl 添加系统调用入口
entry("fork");
...
entry("sigalarm"); 
entry("sigreturn");

kernel/syscall.h 中添加系统调用号
#define SYS_sigalarm  22 
#define SYS_sigreturn  23

kernel/syscall.c
...
extern uint64 sys_uptime(void);
extern uint64 sys_sigalarm(void); 
extern uint64 sys_sigreturn(void);

static uint64 (*syscalls[])(void) = {
[SYS_fork]    sys_fork,
...
[SYS_close]   sys_close,
[SYS_sigalarm]  sys_sigalarm, 
[SYS_sigreturn] sys_sigreturn,
};

然后我们来修改调用过程,先为进程增加定时器相关字段, kernel/proc.h

c 复制代码
struct proc {
  struct spinlock lock;
  ...
  char name[16];               // Process name (debugging)

  int alarm_interval;         // 记录定时器触发间隔(以时钟周期为单位)
  void (*alarm_handler)();    // 定时器处理函数的函数指针
  int ticks_count;           // 当前已经过的时钟周期计数器
  struct trapframe *alarm_trapframe; // 用于保存中断前进程的上下文信息
  int alarm_on;              // 定时器是否已经在运行了
};

字段的含义看注释

接着来处理时钟中断逻辑 kernel/trap.c

scss 复制代码
void
usertrap(void)
{
   ...
  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    struct proc *p = myproc();

    // 如果设置了定时器
    if(p->alarm_interval > 0) {
      p->ticks_count++;

      // 是否达到触发条件:计数达到了设定的间隔 + 处理函数当前未在执行
      if(p->ticks_count >= p->alarm_interval && !p->alarm_on) {
        p->ticks_count = 0;
        p->alarm_on = 1;
        
        // 首次调用时分配保存上下文的空间
        if(p->alarm_trapframe == 0)
          p->alarm_trapframe = kalloc();
        // 保存当前进程的完整上下文
        *p->alarm_trapframe = *p->trapframe;
        
        // 修改程序计数器指向处理函数
        // 当中断返回时会跳转到 handler 函数执行
        p->trapframe->epc = (uint64)p->alarm_handler;
      }
    }
    yield();
  }

  usertrapret();
}

最后,修改 Makefile,添加 alarmtest 测试程序

makefile 复制代码
UPROGS=\
	...
	$U/_zombie\
	$U/_alarmtest\    # 新增

执行 alarmtest 运行测试程序

perl 复制代码
$ alarmtest
test0 start
.....alarm!
test0 passed
test1 start
.alarm!
alarm!
alarm!
.alarm!
alarm!
alarm!
.alarm!
.alarm!
alarm!
alarm!
test1 passed
test2 start
..alarm!
test2 passed

测试通过,完整代码在:github.com/yibaoshan/x...

参考资料

相关推荐
Caron_xcb14 分钟前
大数据——解决Matplotlib 字体不足问题(Linux\mac\windows)
大数据·linux·matplotlib
水水沝淼㵘39 分钟前
嵌入式开发学习日志(数据结构--顺序结构单链表)Day19
linux·服务器·c语言·数据结构·学习·算法·排序算法
愚润求学1 小时前
【Linux】基础 IO(一)
linux·运维·服务器·开发语言·c++·笔记
大白的编程日记.1 小时前
【Linux学习笔记】理解一切皆文件实现原理和文件缓冲区
linux·笔记·学习
孞㐑¥1 小时前
Linux之进程控制
linux·开发语言·c++·经验分享·笔记
一丝晨光2 小时前
数值溢出保护?数值溢出应该是多少?Swift如何让整数计算溢出不抛出异常?类型最大值和最小值?
java·javascript·c++·rust·go·c·swift
愚润求学2 小时前
【Linux】简单设计libc库
linux·运维·开发语言·c++·笔记
chennalC#c.h.JA Ptho2 小时前
Bodhi linux 系统详解
linux·经验分享·笔记·系统架构·系统安全
敲上瘾3 小时前
MySQL数据库表的约束
linux·数据库·sql·mysql·数据库开发·数据库架构·数据库系统
u0109362653 小时前
Linux电源管理(五),发热管理(thermal),温度控制 (结合设备树 和ACPI Source Language(ASL)分析)
linux