全网最最实用--基于Mac ARM 芯片实现操作系统MIT 6.S081-lab4

实验四 陷阱中断

一、代码理解

在开始编码之前,请阅读xv6 书的第 4 章和相关源文件:
kernel/trampoline.S:涉及从用户空间切换到内核空间并返回的程序集
kernel/trap.c:处理所有中断的代码

1.用户空间和内核空间的切换

用户模式到内核模式的切换(uservec)
当用户程序发生异常或系统调用时,控制权会跳到 uservec 标签下的代码,此代码段的主要目的是保存用户状态并为内核模式执行做准备。
csrrw a0, sscratch, a0:交换寄存器 a0 和 sscratch 的值。sscratch 是一个特殊的 CSR(Control and Status Register),用于暂存某些系统信息,在这里它存储了用户空间的陷阱帧地址(TRAPFRAME)。
sd ra, 40(a0) 到 sd t6, 280(a0):这些指令将寄存器的值保存到 TRAPFRAME。这包括通用寄存器和特殊寄存器,如返回地址 ra,堆栈指针 sp,全局指针 gp 等。
csrr t0, sscratch 和 sd t0, 112(a0):取出 sscratch 中的值(用户的 a0),并存储到 TRAPFRAME 中相应的位置。
ld sp, 8(a0):恢复内核栈指针。
ld tp, 32(a0):加载当前硬件线程ID。
ld t0, 16(a0) 和 jr t0:加载并跳转到内核的异常处理函数 usertrap()。
ld t1, 0(a0) 和 csrw satp, t1:从 TRAPFRAME 加载内核页表的地址,并设置当前的页表寄存器 satp。
sfence.vma zero, zero:执行虚拟内存地址转换(VMA)的同步操作,确保页表变更生效。


内核模式到用户模式的切换(userret)
执行完内核操作后,需要通过 userret 来安全返回用户模式。
csrw satp, a1:设置页表寄存器 satp 为用户页表,a1 为入口参数之一,含用户页表信息。
sfence.vma zero, zero:同步虚拟内存地址转换。
ld t0, 112(a0) 和 csrw sscratch, t0:将用户的 a0 寄存器值从 TRAPFRAME 加载到 sscratch,以便后续恢复。
从 ld ra, 40(a0) 到 ld t6, 280(a0):从 TRAPFRAME 恢复用户的寄存器值。
csrrw a0, sscratch, a0:恢复用户的 a0 寄存器的原值。
sret:使用 sret 指令从内核模式返回到用户模式。sret 会加载 sepc(保存着将要返回到的用户程序的指令地址)和 sstatus(处理器状态)寄存器。

2.中断相关

void
usertrap(void)
{
  int which_dev = 0; // 初始化设备标识变量,用于记录哪种设备引起的中断

  // 检查是否从用户模式进入,SSTATUS_SPP 是状态寄存器 sstatus 的一个标志位,表示前一个模式
  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode"); // 如果不是从用户模式进入,则触发内核恐慌

  // 设置中断向量寄存器 stvec 指向 kernelvec,用于后续的异常和中断处理
  w_stvec((uint64)kernelvec);

  // 获取当前进程的进程控制块
  struct proc *p = myproc();
  
  // 将当前的程序计数器值(sepc)保存到该进程的陷阱帧中
  p->trapframe->epc = r_sepc();
  
  // 检查引起陷阱的原因是否为系统调用(scause == 8)
  if(r_scause() == 8){
    // 系统调用处理

    // 如果进程已被标记为结束,直接退出
    if(p->killed)
      exit(-1);

    // 将程序计数器增加4,跳过当前的ecall指令到下一条指令
    p->trapframe->epc += 4;

    // 开启中断处理,因为中断会改变 sstatus 等寄存器
    intr_on();

    // 处理系统调用
    syscall();
  } 
  else if((which_dev = devintr()) != 0){
    // 如果是设备中断,devintr() 处理后返回非零,代表处理成功
    // 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);

  // 如果是定时器中断(which_dev == 2),让出 CPU
  if(which_dev == 2)
    yield();

  // 准备返回到用户模式,包括恢复用户状态并跳转到适当的地址
  usertrapret();
}

二、RISC-V assembly

1.题目描述

您的 xv6 存储库中 有一个文件user/call.cmake fs.img 。编译它并在 user/call.asm中生成该程序的可读汇编版本。阅读 call.asm 中函数g、f和main的代码。回答以下问题。

2.解答

哪些寄存器包含函数的参数?例如,在 main 调用printf时,哪个寄存器保存 13 ?
答:a0-a7。a2。
在 main 的汇编代码中,对函数f 的调用在哪里?对g 的调用在哪里?(提示:编译器可能会内联函数。)
答:没有,被内联了。
普通函数调用:
系统首先要跳跃到该函数的入口地址,执行函数体,执行完成后,再返回到函数调用的地方,函数始终只有一个拷贝

内联函数调用:
内联函数不需要寻址的过程,当执行到内联函数时,此函数展开(很类似宏的使用),如果在 N处调用了此内联函数,则此函数就会有N个代码段的拷贝。
函数printf位于什么地址?
答:0x630
auipc ra, 0x0        # 设置 ra = 当前 PC 高 20 位 + 立即数 0x0
jalr 1536(ra)        # 跳转到 ra + 1536 (也就是 0x30 + 0x600 = 0x630)

在main中的jalr到printf 之后, 寄存器ra中的值是什么?
答:当在 main 函数中使用 jalr 指令跳转到 printf 之后,ra 寄存器(通常用作返回地址寄存器)将被设置为 jalr 指令后的下一条指令的地址。jalr 指令在程序中的地址是 0x34,并且这是一条 4 字节的指令,那么 ra 的值将是 0x38。
运行以下代码。
	unsigned int i = 0x00646c72;
	printf("H%x Wo%s", 57616, &i); 
输出是什么? 这是将字节映射到字符的 ASCII 表。输出取决于 RISC-V 是小端字节序的事实。
如果 RISC-V 是大端字节序,你会设置什么i来产生相同的输出?你需要更改 57616为不同的值吗?
答: "HE110 World"。
	i = 0x726c6400。



在下面的代码中,后面会打印什么 'y='?(注意:答案不是一个具体的值。)为什么会发生这种情况?
	printf("x=%dy=%d", 3);
答:由于缺少第二个参数,printf 会尝试读取堆栈上预期的第二个整数。这种情况下,由于未定义行为,printf 可能会读取到堆栈上的其他值,这个值可能是一个任意数,或者是程序运行时的垃圾值。

三、Backtrace

1.题目描述

在kernel/printf.c中 实现backtrace()函数。在sys_sleep中插入对此函数的调用,然后运行,调用sys_sleep。您的输出应如下所示: bttest

回溯:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898
  
bttest退出 qemu 后。在您的终端中:地址可能略有不同,但如果您运行addr2line -e kernel/kernel(或riscv64-unknown-elf-addr2line -e kernel/kernel)并剪切并粘贴上述地址,如下所示:
    $addr2line -e kernel/kernel
    0x0000000080002de2
    0x0000000080002f4a
    0x0000000080002bfc
    Ctrl-D
  
你应该看到类似这样的内容:
    内核/sysproc.c:74
    内核/系统调用.c:224
    内核/trap.c:85

2.解答

在 riscv.h 中给定获取当前 fp(frame pointer)寄存器的方法:
fp 指向当前地址的开始地址,sp 指向当前地址的结束地址。

// riscv.h
static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x));
  return x;
}
/*这行代码使用内联汇编语法来执行一个汇编指令:

asm 关键字用于嵌入汇编代码。
volatile 关键字表示这段汇编代码不应该被编译器优化掉。
"mv %0, s0" 是一条 RISC-V 汇编指令,mv 是 move 的缩写,表示将 s0 寄存器的值移动到 %0 指定的位置。%0 是一个占位符,表示输出操作数。
: "=r" (x) 部分指定了输出操作数。"=r" 表示将结果存储到一个寄存器中,(x) 表示将结果存储到变量 x 中。*/

该函数返回当前的帧指针(frame pointer, fp),通常存储在 RISC-V 架构中的 s0 寄存器中。帧指针用于标识当前函数的栈帧。

// printf.c

void backtrace() {
  uint64 fp = r_fp();  // 获取当前帧指针
  if (fp == 0) {  // 检查帧指针是否有效
    printf("无有效帧指针。\n");
    return;
  }
  while (fp != PGROUNDUP(fp) && fp) {  // 检查是否到达栈底或帧指针非零
    uint64 ra = *(uint64*)(fp - 8);  // 获取返回地址
    printf("%p\n", ra);  // 打印返回地址
    uint64 next_fp = *(uint64*)(fp - 16);  // 获取前一个帧指针
    if (next_fp <= fp) {  // 检查栈是否向下增长(防止栈损坏或异常的帧指针)
      printf("检测到栈损坏或已到栈底。\n");
      break;
    }
    fp = next_fp;  // 更新当前帧指针为前一个帧指针
  }
}

四、Alarm

1.题目描述

在本练习中,您将向 xv6 添加一项功能,该功能会在进程使用 CPU 时间时定期向其发出警报。这可能对计算受限的进程很有用,这些进程希望限制它们占用的 CPU 时间,或者对想要计算但也想要采取一些定期操作的进程很有用。
更一般地说,您将实现一种原始形式的用户级中断/故障处理程序;例如,您可以使用类似的东西来处理应用程序中的页面错误。如果您的解决方案通过了警报测试和用户测试,则说明它是正确的。

2.解答

要在xv6中实现sigalarm(interval, handler)sigreturn系统调用,您需要进行几个步骤。以下是详细的实现步骤,包括添加必要的代码和更新Makefile以编译alarmtest

1. 修改struct proc

首先,在struct proc中添加与闹钟相关的字段。这些字段用于跟踪闹钟的状态和处理程序。

// 在 proc.h 中
struct proc {
  // ......
  int alarm_interval;          // 闹钟间隔(0表示禁用)
  void (*alarm_handler)();     // 闹钟处理程序
  int alarm_ticks;             // 距离下一次闹钟响起的时钟周期数
  struct trapframe *alarm_trapframe;  // 运行闹钟处理程序前的陷阱帧副本
  int alarm_goingoff;          // 当前是否有正在执行且尚未返回的闹钟处理程序(防止重入)
};
2. 添加sigalarm系统调用

在xv6中,系统调用通常包括以下几个步骤:定义系统调用号、实现系统调用处理程序、在系统调用表中注册系统调用,并为用户提供一个接口。

a. 定义系统调用号

kernel/syscall.h中添加SYS_sigalarmSYS_sigreturn的定义:

#define SYS_sigalarm 22
#define SYS_sigreturn 23
b. 实现系统调用处理程序

kernel/sysproc.c中添加sys_sigalarmsys_sigreturn的实现:

// 在 kernel/sysproc.c 中
extern 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->alarm_ticks = interval;
  p->alarm_goingoff = 0;

  return 0;
}

extern uint64 sys_sigreturn(void) {
  struct proc *p = myproc();
  if (p->alarm_trapframe) {
    memmove(p->trapframe, p->alarm_trapframe, sizeof(struct trapframe));
    kfree(p->alarm_trapframe);
    p->alarm_trapframe = 0;
  }
  return 0;
}
c. 在系统调用表中注册系统调用

kernel/syscall.c中添加sys_sigalarmsys_sigreturn

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

// 在 syscalls 数组中添加
[SYS_sigalarm]   sys_sigalarm,
[SYS_sigreturn]  sys_sigreturn,
d. 为用户提供接口

user/user.h中声明用户接口:

int sigalarm(int interval, void (*handler)());
int sigreturn(void);

user/usys.pl中添加系统调用接口:

entry("sigalarm");
entry("sigreturn");
3. 实现时钟中断处理

修改时钟中断处理程序,在kernel/trap.c中添加代码,以处理闹钟:

// 在 kernel/trap.c 中
void
usertrap(void)
{
  // existing code...
  
  if (which_dev == 2) {
    struct proc *p = myproc();
    if (p->alarm_interval != 0 && !p->alarm_goingoff) {
      if (--p->alarm_ticks == 0) {
        p->alarm_ticks = p->alarm_interval;
        p->alarm_goingoff = 1;
        if (p->alarm_trapframe == 0) {
          p->alarm_trapframe = (struct trapframe *)kalloc();
          memmove(p->alarm_trapframe, p->trapframe, sizeof(struct trapframe));
        }
        p->trapframe->epc = (uint64)p->alarm_handler;
      }
    }
  }

  // existing code...
}
4. 编译alarmtest

user/Makefile中添加alarmtest

UPROGS=\
  _alarmtest\
  ...
5. 编写用户代码alarmtest

创建user/alarmtest.c,实现测试闹钟功能:

#include "kernel/types.h"
#include "user/user.h"

void periodic() {
  printf("Alarm!\n");
  sigreturn();
}

int main() {
  sigalarm(2, periodic);
  while(1) {
    printf("Main loop\n");
    sleep(1);
  }
  exit(0);
}
6. 编译和运行

编译xv6并运行alarmtest以验证实现效果。

make qemu

五、心得体会

关于我这个gdb时好时坏,但是要想深入了解这个系统,只能进行断点调试。
听课对我来说无济于事,毕竟基础知识还是有点的。
之前的学习路径是讲义 ➡️ 讲座 ➡️ 实验;现在可以讲义 ➡️ 实验了。

最开始我对这个抱有太多期望,我觉得“太好了,学完这个我肯定会拥有很多!”;后来发现我高估了我自己的学习能力“学习道路磕磕绊绊不说,还有很多其他惊世骇俗的想法诱惑着我”。

我肯定不会被过去的自己约束,但是,这个课程是我当时还算有勇气的想法,如果我因为更好的想法而砍掉目前的课程,那我害怕我的每个想法都会无疾而终。

这门课程我应该会选择这两周all in 尽快更完,fighting!

  • 8
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值