6.s081/6.1810(Fall 2022)Lab4: Traps

文章目录

  • 前言
  • 其他篇章
  • 参考链接
  • 0. 环境搭建
  • 1. RISC-V assembly (easy)
    • 1.0 简介
    • 1.1 Q1
    • 1.2 Q2
    • 1.3 Q3
    • 1.4 Q4
    • 1.5 Q5
    • 1.6 Q6
  • 2. Backtrace (moderate)
    • 2.1 简单分析
    • 2.2 实现
    • 2.3 测试
  • 3. Alarm (hard)
    • 3.1 简单分析
    • 3.2 test0: invoke handler
      • 3.2.1 添加调用
      • 3.2.2 获取参数
      • 3.2.3 处理中断
      • 3.2.4 测试
    • 3.3 test1/test2()/test3(): resume interrupted code
      • 3.3.1 恢复上下文与防止多次调用
      • 3.3.2 恢复寄存器a0的值
      • 3.3.3 测试
    • 3.4 测试
  • 4. 最后测试

前言

这个Lab我觉得比较难,主要是涉及到底层、汇编,这方面我确实接触得少。

其他篇章

环境搭建
Lab1: Utilities
Lab2: System calls
Lab3: Page tables
Lab4: Traps

参考链接

官网链接
xv6手册链接,这个挺重要的,建议做lab之前最好读一读。
xv6手册中文版,这是几位先辈们的辛勤奉献来的呀!再习惯英文文档阅读我还是更喜欢中文一点,开源无敌!
risc-v 指令集
个人代码仓库
官方文档

0. 环境搭建

和Lab3步骤一致,不多解释。**记得make clean!!!**上个lab因为没弄这个卡了一个多小时,难绷。
在这里插入图片描述

1. RISC-V assembly (easy)

1.0 简介

在这里插入图片描述
这个Task是一个热身Task,带你熟悉一下RISC-V汇编的,它告诉我们执行make fs.img会生成一个user/call.c的汇编程序user/call.asm,我们来看看吧:

make fs.img && vi user/call.asm

顺便看一下原型代码,很基础(碎碎念:void main不规范啊!):

#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"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);
}

然后题目叫把答案存在answers-traps.txt下,下面来看看这几个问题:

1.1 Q1

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

我们来看一看printf的调用,可以看到,printf的三个参数分别被压入了a0 - a2寄存器,具体到13这个数,是被保存在了a2里。

在这里插入图片描述
另外,这堂课的Lecture中给出了RISC-V各个寄存器的作用,可以看到,a0 - a7寄存器是用于保存函数的参数的,其中头两个寄存器还用于保存函数返回值。
在这里插入图片描述

1.2 Q2

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,但是汇编代码并没有展示给我们8 + 3 + 1的过程,也没有调用函数时的各种压栈。
在这里插入图片描述
结合题目的提示以及我们的常识,我们可以知道这个地方直接被编译器优化了,优化后内联展开了f和g的调用,无疑节省了开销,因此,没有调用函数f和g的汇编代码。

1.3 Q3

At what address is the function printf located?

还是那段代码,这里写的就是printf的地址,这是十六进制的,因此是0x642
在这里插入图片描述
不过我们来看一看这个0x642是怎么算出来的:
在这里插入图片描述
查阅文档我们可以看见,auiqc指令是将传入的立即数左移12位后加上当前的pc存入ra,因此49行程序执行后ra的值为0x30,然后看一看jalr指令的文档:
在这里插入图片描述
可以看到,这个指令的作用就是跳到指定地址去,在这里就是跳到0x30 + 1554 = 0x30 + 0x612 = 0x642。

1.4 Q4

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

从刚刚发的文档就可以看见,jalr会把PC指针往下偏一个,也就是0x34 + 4 = 0x38,因此这时ra中的值为0x38。

1.5 Q5

在这里插入图片描述
这个问题叫我们运行一下这两行代码,问运行结果,结果是HE110 World,是不是很神奇?必须值得指出的一点是,以%s打印unsigned *的行为是未定义的!不过这里也可以分析一下它的成因,其中57616转化为16进制即为0xE110,因此前面没什么好说的,而i的值如果每两个16进制位转化为一个char的话就会变为'\0'dlr,同时注意到题目给出了一个提示,告诉我们RISC-V是小端序的,即从低位开始看,因此就打出来了rld,题目还问如果是大端序机器i应该输入多少,那么只需要反转一下就行了,也就是0x726c6400,而57616的值当然不需要改变,因为这与端序无关。

1.6 Q6

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

依旧需要指出的一点是,printf中后续传入的参数少于format(就是第一个参数)中所需要的数量的行为是未定义的!不过我们依旧在这里研究一下编译器行为学。

实际结果应该是个不确定的值,Q1的时候我们就看见了,对于printf传参,会把入参依此压入从a0开始的寄存器,这里的printf本来需要三个参数,也就是要到a2寄存器,因此这里会输出a2中的值——这个值当然是不确定的、或者说依赖于之前的调用的。

2. Backtrace (moderate)

2.1 简单分析

这个Task叫Backtrace,回溯,经常刷算法题什么的应该对这个词很有印象。这里的backtrace在哲学上和算法中的backtrace类似,不过在具体上又有所不同。在这里题目就告诉你,在出错的时候,backtrace是对debug很有用的,它可以打印出函数的栈帧(stack frame),涉及到这方面确实也非常有用,比如说C++都在p0881r7把栈踪库加入C++23的标准库stacktrace了。
在这里插入图片描述
XV6中stack占据一个页的大小,
在这里插入图片描述
而函数的调用过程,实际上就是往这个栈里面压栈的过程,可以看到这个图里的解说,压栈是从高地址往低地址压的,每次调用都会在现有的基础上往下添加一个stack frame,我们有一个帧指针fp(frame pointer),指向了这个帧的头部,有一个sp,指向了这个帧的底部,其中帧里的内容,第一项是return address,代表帧应当返回的地址,具体一点的话,就是我们调用这个函数的原来的函数的地址。而我们要做的就是使用帧指针遍历这个栈并打印出每个栈帧保存的return address,这样就可以看出调用链,便于我们调试。
在这里插入图片描述

2.2 实现

在这里插入图片描述
搞清楚了需求就可以开始写实现了,实现的逻辑其实很简单,我们只需要使用fp从stack的头部遍历到stack页的尾部即可。具体而言,像我们上文说过的那样,fp指向一个栈帧的头部,栈帧的第一个位置放的是需要我们打印的return addr值,第二个位置放的是前一个帧的fp值,根据文档中的it actually points to the the address of the saved return address on the stack plus 8可以知道,在这个机器上每个位置的偏移量为8,因此这两个位置分别为fp - 8fp - 16,这实际上就是指针偏移一次和两次的结果,因此我们可以更直观地写为fp[-1](因为stack向下生长所以是减)。

题目告诉我们要实现在kernel/printf.c中,此外,hint还提示我们可以在kernel/riscv.h中添加这样一段代码以获取当前fp的值

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

此外还可以使用kernel/riscv.h中的PGROUNDDOWN(fp) 来获取页末的fp,整体遍历就好像遍历链表一样,就此我们就可以很容易写出如下代码:

void 
backtrace(void)
{for (uint64* fp = (uint64*)r_fp(), *bottom = (uint64*)PGROUNDDOWN((uint64)fp);fp > bottom ; fp = (uint64*)fp[-2]){printf("%p\n", fp[-1]); // 获取并打印返回地址}
}

由于我们这个函数是要被调用的,因此我们还需要在老地方暴露接口:
在这里插入图片描述
然后按照要求,在sys_sleep中调用backtrace
在这里插入图片描述

2.3 测试

推送后跑一下bttest
在这里插入图片描述

./grade-lab-traps backtrace
在这里插入图片描述

3. Alarm (hard)

3.1 简单分析

这个lab是要求我们实现一个feature,用途简单来说就是隔一段时间发出一次中断,调用一次函数,达到周期实现函数的效果。
在这里插入图片描述

3.2 test0: invoke handler

3.2.1 添加调用

我们首先按照要求,增加sigalarm调用与sigreturn 调用,添加方法比较常规:

  • 为Makefile添加文件
	$U/_alarmtest\	
  • 在user.h中暴露接口
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
  • 在usys.pl中添加脚本
entry("sigalarm");
entry("sigreturn");
  • 在syscall.h中注册编号
#define SYS_sigalarm    22
#define SYS_sigreturn   23
  • 在syscall.c中添加映射
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);
...[SYS_sigalarm]    sys_sigalarm,
[SYS_sigreturn]   sys_sigreturn,
  • 在sysproc.c中添加两个syscall的框架
uint64
sys_sigalarm(void)
{return 0;
}uint64
sys_sigreturn(void)
{return 0;
}

3.2.2 获取参数

sys_sigalarm接受两个参数,分别是ticks和handle,从语义学的角度来看就知道前者是间隔的时间,后者是需要处理的函数,以类似于回调函数的形式传入。同时HintYour 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). 告诉我们这俩参数还应该存在proc结构体里,同时我们还应当维护一个变量,用于已经经历的时长,因此我们需要先在proc里开三个字段,然后在实现里获取参数:

// proc.h
struct proc {
...int alarm_past;              // 系统调用alarm的过去时间int alarm_ticks;             // 系统调用alarm的间隔void (*alarm_handler)();     // 系统调用alarm的待执行的函数
...
// sysproc.c
uint64
sys_sigalarm(void)
{struct proc *p = myproc();// 从用户空间获取参数并存储到procargint(0, &p->alarm_ticks);argaddr(1, (uint64*)&p->alarm_handler);p->alarm_past = 0; // 重置alarm_pastreturn 0;
}

同时,我们应该在proc.c下的allocproc中将它们初始化为0。

static struct proc*
allocproc(void)
{
...p->alarm_past = 0;p->alarm_ticks = 0;p->alarm_handler = (void*)0;

3.2.3 处理中断

Hint提示我们时间中断时会在trap.cusertrap()中执行,并且我们只用修改if(which_dev == 2) ...控制的域,这里就存在一个问题:可以去直接在中断中调用handle吗?答案当然是不行的,我们知道,中断是内核态处理的东西,而handle是一个用户态的地址,用户态和内核态的内存空间是不共享的!因此我们需要找到一种方法去调用这个回调函数。

根据学习我们可以知道,trap时系统会把上下文全给存到proc下那个trapframe类型的指针指向的地方,而trapframe类型中又依赖epc去保存先前的用户程序的地址,因此我们这里假如粗暴地把epc重定向到我们的handle,就可以达成test0的调用中断处理函数的目的了:

  // give up the CPU if this is a timer interrupt.if (which_dev == 2) {if (p->alarm_ticks && ++p->alarm_past == p->alarm_ticks) {p->alarm_past = 0;p->trapframe->epc = (uint64)p->alarm_handler;}yield();}

3.2.4 测试

输入alarmtest,观察到test0 passed即可。
在这里插入图片描述

3.3 test1/test2()/test3(): resume interrupted code

3.3.1 恢复上下文与防止多次调用

在上一个test中,我们会发现,虽然test0通过了,但是test1挂掉了,究其原因其实很简单——trap时有个保存上下文(context)与恢复上下文的过程,但是我们刚刚直接暴力覆盖掉了保存的上下文,在恢复上下文的时候自然就会出现错误,因此,很容易想到我们可以使用一个中间变量去存取被覆盖掉的值,然后调用完handle后再放回去。但是这就有一个问题了——哪些值、或者说寄存器被覆盖了?我们显式覆盖的值自然只有epc一个,但是在handle的执行过程中,我们同样可能发生trap,导致trapframe的值被搞得一团糟,因此我们应该保存整个tramframe,此外,还有一个问题是我们的handle可能需要运行一段时间,若是在这段时间里handle再次被调用,很明显它就将占据全部的时间片段,为了避免这一点,我们应当设置一个值来标记当前hadle是否正在被执行。搞清楚了这些就比较简单了:

  • 首先在 proc中定义两个field:
  int alarm_on;                     // 系统调用alarm是否开启int alarm_past;                   // 系统调用alarm的过去时间int alarm_ticks;                  // 系统调用alarm的间隔void (*alarm_handler)();          // 系统调用alarm的待执行的函数struct trapframe* pre_trapframe; // 保存上一次的trapframe

在这里插入图片描述

  • 在alloc中模仿上文分配内存以及为alarm_on置零:
    在这里插入图片描述
  • 别忘了free:
    在这里插入图片描述
  • 检查一下trapframe,发现里面并没有什么需要深拷贝的东西,因此直接在trap中赋值:
  // give up the CPU if this is a timer interrupt.if (which_dev == 2) {if (!p->alarm_on && p->alarm_ticks && ++p->alarm_past == p->alarm_ticks) {p->alarm_on = 1;*p->pre_trapframe = *p->trapframe;p->alarm_past = 0;p->trapframe->epc = (uint64)p->alarm_handler;}yield();}

然后再sys_sigreturn中恢复上下文:

uint64
sys_sigreturn(void)
{struct proc *p = myproc();if (p->alarm_on){*p->trapframe = *p->pre_trapframe;p->alarm_past = 0;p->alarm_on = 0;}return 0;
}

3.3.2 恢复寄存器a0的值

这个test3是6.1810,也就是22Fall才有的,之前都没有,说的是最后还存在一个问题,我们在task1学习过,函数的返回值会被保存在寄存器a0、a1中,而这些系统调用,包括sys_sigreturn都是有返回值的,这就会导致进行了系统调用后a0的值会被污染,因此我们直接返回原来的a0值即可:
在这里插入图片描述
话说总觉得这么写不太对,因为我查了一下Linux的sigreturn是返回-1的,这里明显不是返回-1,希望谁来给我解解惑。

3.3.3 测试

alarmtest后完美运行:
在这里插入图片描述

3.4 测试

跑一下./grade-lab-traps alarm,通过:
在这里插入图片描述

4. 最后测试

按照惯例加入time.txt,然后make grade

因为我在task1写了中文,直接显示编码错误,懒得折腾了,直接把测试脚本这里给注释了,反正它也只能读个单词数量(
在这里插入图片描述
在这里插入图片描述
顺利通过(话说这个usertests是啥玩意啊,花这么多时间)
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/78959.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

服务器硬件、部署LNMP动态网站、部署wordpress、配置web与数据库服务分离、配置额外的web服务器

day01 day01项目实战目标单机安装基于LNMP结构的WordPress网站基本环境准备配置nginx配置数据库服务部署wordpressweb与数据库服务分离准备数据库服务器迁移数据库配置额外的web服务器 项目实战目标 主机名IP地址client01192.168.88.10/24web1192.168.88.11/24web2192.168.88…

Baumer工业相机堡盟工业相机如何通过BGAPISDK获取相机接口数据吞吐量(C++)

Baumer工业相机堡盟工业相机如何通过BGAPISDK里函数来获取相机当前数据吞吐量(C) Baumer工业相机Baumer工业相机的数据吞吐量的技术背景CameraExplorer如何查看相机吞吐量信息在BGAPI SDK里通过函数获取相机接口吞吐量 Baumer工业相机通过BGAPI SDK获取数…

STM32 CubeMX USB_(HID 鼠标和键盘)

STM32 CubeMX STM32 CubeMX USB_HID(HID 鼠标和键盘) STM32 CubeMX前言 《鼠标》一、STM32 CubeMX 设置USB时钟设置USB使能UBS功能选择 二、代码部分添加代码鼠标发送给PC的数据解析实验效果 《键盘》STM32 CubeMX 设置(同上)代码…

睡眠助手/白噪音/助眠夜曲微信小程序源码下载 附教程

睡眠助手/白噪音/助眠夜曲微信小程序源码 附教程 支持分享海报 支持暗黑模式 包含了音频数据 最近很火的助眠小程序,前端vue,可以打包H5,APP,小程序 后台可以设置流量主广告,非常不错的源码 代码完整 完美运营 搭配无…

Django实现音乐网站 ⑸

使用Python Django框架制作一个音乐网站, 本篇主要是配置媒体资源设置。 目录 配置介绍 设置媒体资源 创建媒体资源目录 修改settings.py 注册媒体资源路由 总结 配置介绍 静态资源是指项目配置的js/css/image等系统常用文件。对于一些经常变动的资源&#x…

LLM reasoners 入门实验 24点游戏

LLM reasoners Ber666/llm-reasoners 实验过程 实验样例24games,examples/tot_game24,在inference.py中配置使用代理和open ai的api key。 首先安装依赖 git clone https://github.com/Ber666/llm-reasoners cd llm-reasoners pip install -e .然后…

UltraToolBars Crack,动画菜单和多种显示样式

UltraToolBars Crack,动画菜单和多种显示样式 创建模仿Microsoft Office 2000外观的健壮应用程序。 UltraToolBars包括11个用于创建可自定义工具栏的界面增强控件,包括:个性化菜单、弹出型工具栏、集成选项卡控件等。PictureRegion技术使表单和组件能够采…

C# Blazor 学习笔记(0.1):如何开始Blazor和vs基本设置

文章目录 前言资源推荐环境如何开始Blazor个人推荐设置注释快捷键热重载设置 前言 Blazor简单来说就是微软提供的.NET 前端框架。使用 WebAssembly的“云浏览器”,集成了Vue,React,Angular等知名前端框架的特点。 资源推荐 微软官方文档 Blazor入门基础视频合集 …

张驰课堂:6西格玛绿带培训费用、内容及实施要点分析

6西格玛绿带培训是一种质量管理培训,旨在帮助企业提高质量和效率。这种培训通常由咨询公司或培训机构提供,费用因提供者而异。以下是一些有关6西格玛绿带培训费用和培训内容的信息。 6西格玛绿带培训费用取决于培训机构,还有培训的持续时间和…

【vue】初步使用element-ui框架

cd到当前项目,运行 cnpm i element-ui -S 接下来就是在项目里面引用了,打开src目录下的main.js //导入vue.js import Vue from vue //导入下面2个组件 import ElementUI from element-ui import element-ui/lib/theme-chalk/index.css //导入App.vue根组…

Nginx可视化Nginx-gui

Github:GitHub - onlyGuo/nginx-gui: Nginx GUI Manager 运行方式支持docker、window 下载后压缩,直接运行startup.bat 默认账号密码:admin/admin

gin框架学习

文章目录 配置go环境实现一个简单的web响应服务验证功能gin增加页面以及传递数据 配置go环境 去go官网下载对应的版本 go下载地址 tar -C /usr/local -xzf go1.4.linux-amd64.tar.gz 我们可以编辑 ~/.bash_profile 或者 /etc/profile,并将以下命令添加该文件的末…

Ae 效果:CC Composite

通道/CC Composite Channel/CC Composite CC Composite(CC 合成) 效果主要用途在于它可以帮助你更方便地在一个图层上将不同的效果和原始图像进行合成。 如果在你的工作流程中需要用到同一个原始图层的多个副本,比如用于制作镜像、反射、阴影…

深入理解数据库事务(超详细)

一、事务的介绍 事务是一组操作的集合,事务会把所有操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。 二、事务的基本操作 2.1 事务操作方式一 例子: 转账场景(张三向李四转…

高性能MySQL实战(一):表结构

大家好,我是 方圆。最近因需求改动新增了一些数据库表,但是在定义表结构时,具体列属性的选择有些不知其所以然,索引的添加也有遗漏和不规范的地方,所以我打算为创建一个高性能表的过程以实战的形式写一个专题&#xff…

Element-UI简介

目录 安装 常用组件 Container 布局容器 Button 按钮 MessageBox 弹框 Form 表单验证 element-ui是一个前端的ui框架,封装了很多已经写好的ui组件,例如表单组件,布局组件,表格组件.......是一套桌面端组件。 Element - 网站…

华为PMS API client token auth failed

对接华为pms时出现问题,提示华为PMS API client token auth failed 主要是权限的问题,创建项目的时候选择N/A

11. 使用tomcat中碰到的一些问题

文章目录 问题一:Tomcat的startup.bat启动后出现乱码问题二:一闪而退之端口占用问题三:非端口问题的一闪而退问题四:服务器的乱码和跨域问题问题五: 在tomcat\webapps\下创建文件夹为什么tomcat重启就会丢失问题六:Tom…

无脑入门pytorch系列(一)—— nn.embedding

本系列教程适用于没有任何pytorch的同学(简单的python语法还是要的),从代码的表层出发挖掘代码的深层含义,理解具体的意思和内涵。pytorch的很多函数看着非常简单,但是其中包含了很多内容,不了解其中的意思…

Android中级——RemoteView

RemoteView RemoteView的应用NotificationWidgetPendingIntent RemoteViews内部机制模拟RemoteViews RemoteView的应用 Notification 如下开启一个系统的通知栏,点击后跳转到某网页 public class MainActivity extends AppCompatActivity {private static final …