MIT6.S081-lab4

MIT6.S081-lab4

注:本篇lab的前置知识在《MIT6.S081-lab3前置》

1. RISC-V assembly

第一个问题

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

我们先来看看main干了什么:

void main(void) {1c:	1141                	addi	sp,sp,-161e:	e406                	sd	ra,8(sp) 20:	e022                	sd	s0,0(sp)22:	0800                	addi	s0,sp,16printf("%d %d\n", f(8)+1, 13);							# 编译器直接算出来了,无需调用f和g函数24:	4635                	li	a2,13              	# printf参数存入寄存器a226:	45b1                	li	a1,12             28:	00001517          	auipc	a0,0x1        	 	# 存入格式格式字符串的大致地址,printf的第一个参数2c:	84850513          	addi	a0,a0,-1976        	# a0 = a0 - 1976,即精确地得到格式字符串地址 "%d %d\n"30:	68c000ef          	jal	6bc <printf>exit(0);34:	4501                	li	a0,0 36:	26e000ef          	jal	2a4 <exit>

综上,a0,a1,a2存放了对应的调用函数所要用的参数。

第二个问题

is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)

对f的调用我发现已经被编译器所优化了,这里直接将一个立即数存入了a1中:

  26:	45b1                	li	a1,12

第三个问题

At what address is the function printf located?

根据汇编代码,我们可以知道,printf位于6bc处,事实上,我们可以在call.asm里面搜索printf,我们可以找到,函数的入口确实是6bc

....
void
printf(const char *fmt, ...)
{6bc:	711d                	addi	sp,sp,-966be:	ec06                	sd	ra,24(sp)6c0:	e822                	sd	s0,16(sp)6c2:	1000                	addi	s0,sp,32.....

第四个问题

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

在main里面,我们很容易发现,根本没有用到ra寄存器,但是其实,ra存储的一般是我们的函数返回的地址,所以在我们调用jal的时候, 会自动将下一条指令的地址存入ra寄存器中,即0x34

第五个问题

Run the following code. What is the output?

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

输出:He110 World,大端模式则需要将i修改为0x72 6c 64 00,我们可以发现就是反转了一下,而另一个数字无需修改,因为这个打印的是16进制表示数字,与大端小端字节序无关。

第六个问题

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

未定义行为,这取决于对应寄存器的值。

2. Backtrace

常爆panic的同学应该会对这个backtrace非常熟悉,他会打印我们函数调用链路的函数返回的地方。这就是我们实验需要实现的东西了,根据lab1,我们可以知道,函数调用的时候,都会把函数返回的地方的地址存储起来,那么我们的目的,就是找到这个存储地址的地方,并且将他打印出来。

难点就在于怎么去找到这个地址,光靠自己去推理,肯定是很困难的,这时候就需要看给我们的hint。

首先,我们向kernel/defs.h中添加static inline uint64 r_fp()这个函数,用来在我们的当前需要编写的backtrace中获取当前的帧指针,以此为基础,来获取之前的函数返回地址。

随后继续往下看:

  • These lecture notes have a picture of the layout of stack frames. Note that the return address lives at a fixed offset (-8) from the frame pointer of a stackframe, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.
  • Your backtrace() will need a way to recognize that it has seen the last stack frame, and should stop. A useful fact is that the memory allocated for each kernel stack consists of a single page-aligned page, so that all the stack frames for a given stack are on the same page. You can use PGROUNDDOWN(fp) (see kernel/riscv.h) to identify the page that a frame pointer refers to.

我们可以通过这个hint知道,我们保存的地址的偏移量是-8,而想要得到上一个帧地址,就需要-16,然后继续以此为-8为偏移量去得到我们的保存的return地址,并且在遇到页的边缘的时候,我们就会停止回溯。

于是,我们的backtrace代码就可以写出来了:

void backtrace(void) {printf("backtrace:\n");uint64 ra, fp = r_fp();// 获取前一个帧指针的位置,位于当前帧指针 fp - 16 的位置// 按照调用约定,fp-8 是返回地址,fp-16 是上一个函数的帧指针uint64 pre_fp = *((uint64*)(fp - 16));// 当上一个帧指针和当前帧指针还在同一个物理页中(即没有越过页边界)时,继续回溯while (PGROUNDDOWN(fp) == PGROUNDDOWN(pre_fp)) {ra = *(uint64 *)(fp - 8);printf("%p\n", (void*)ra);// 更新当前帧指针为上一个帧指针fp = pre_fp;// 继续获取上一个帧的帧指针pre_fp = *((uint64*)(fp - 16));}// 打印最后一个返回地址(最后一个栈帧)ra = *(uint64 *)(fp - 8);printf("%p\n", (void*)ra);
}

除此之外,记得在kernel/defs.h定义我们的backtrace函数,并且将这个函数添加到sys_sleep中。

这样,backtrace就算完成了。

3. Alarm

实验要求是注册一个时间间隔和函数到当前的cpu,到点的时候就会调用这个函数,并且期间要求恢复我们的当前进程的上下文(寄存器)不受影响,简单来讲,就是一个非常tiny的trap。

首先我们阅读hint,这个实验不读hint真的是没法做。

  • You’ll need to modify the Makefile to cause alarmtest.c to be compiled as an xv6 user program.

  • The right declarations to put in user/user.h are:

        int sigalarm(int ticks, void (*handler)());int sigreturn(void);
    
  • Update user/usys.pl (which generates user/usys.S), kernel/syscall.h, and kernel/syscall.c to allow alarmtest to invoke the sigalarm and sigreturn system calls.

  • For now, your sys_sigreturn should just return zero.

  • Your 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).

  • You’ll need to keep track of how many ticks have passed since the last call (or are left until the next call) to a process’s alarm handler; you’ll need a new field in struct proc for this too. You can initialize proc fields in allocproc() in proc.c.

  • Every tick, the hardware clock forces an interrupt, which is handled in usertrap() in kernel/trap.c.

  • You only want to manipulate a process’s alarm ticks if there’s a timer interrupt; you want something like

        if(which_dev == 2) ...
    
  • Only invoke the alarm function if the process has a timer outstanding. Note that the address of the user’s alarm function might be 0 (e.g., in user/alarmtest.asm, periodic is at address 0).

  • You’ll need to modify usertrap() so that when a process’s alarm interval expires, the user process executes the handler function. When a trap on the RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?

  • It will be easier to look at traps with gdb if you tell qemu to use only one CPU, which you can do by running

        make CPUS=1 qemu-gdb
    
  • You’ve succeeded if alarmtest prints “alarm!”.

  • Your solution will require you to save and restore registers—what registers do you need to save and restore to resume the interrupted code correctly? (Hint: it will be many).
  • Have usertrap save enough state in struct proc when the timer goes off that sigreturn can correctly return to the interrupted user code.
  • Prevent re-entrant calls to the handler----if a handler hasn’t returned yet, the kernel shouldn’t call it again. test2 tests this.
  • Make sure to restore a0. sigreturn is a system call, and its return value is stored in a0.

这些hint可谓是信息量很大了,简单梳理一下,我们先将需要的系统调用框架先搭好:

makefile

UPROGS=\$U/_cat\$U/_echo\$U/_forktest\$U/_grep\$U/_init\$U/_kill\$U/_ln\$U/_ls\$U/_mkdir\$U/_rm\$U/_sh\$U/_stressfs\$U/_usertests\$U/_grind\$U/_wc\$U/_zombie\// 添加这一行$U/_alarmtest\

user/usys.pl

entry("sigalarm");
entry("sigreturn");

user/user.h

// lab
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);

kernel/syscall.h

#define SYS_sigalarm 22
#define SYS_sigreturn 23

kernel/syscall.c

[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn
// 这部分加在数组里面,做过之前的lab懂得都懂

目前我们大体的框架是弄好了,随后着手去看我们的hint,我们可以知道,如果发生了定时器中断,我们的which_dev就是2,hint告诉了我们这一点,于是,我们可以在这一部分代码块写下我们的中断逻辑,但是这部分应该如何去写呢?我们需要去执行我们之前注册的函数,并且需要保存当前的trapframe,保证之后还能够回到这里,并且还需要去判断计时器的时间,并且做一些加减操作,所以,我们在此之前,还需要对我们的proc结构体进行一些修改:

kernel/proc.h

//为proc结构体添加以下字段uint64 interval;              // 间隔void (*handler)();            // 定时处理的函数uint64 ticks;                 // 上一次调用函数距离的时间struct trapframe *alarm_trapframe;  // 用于恢复 trapframeint alarm_goingoff;           // 是否正在alarm,防止嵌套的中断,导致trapframe丢失

我们既然多了这么多字段,那么必须要在allocproc里面,也为这些字段进行初始化

static struct proc*
allocproc(void)
{//...found://...if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0) {freeproc(p);release(&p->lock);return 0;}p->ticks = 0;p->handler = 0;p->interval = 0;p->alarm_goingoff = 0;//...return p;
}

同时,在释放proc的时候,也需要执行对应的操作:

static void
freeproc(struct proc *p)
{//...// free alarm trapframeif(p->alarm_trapframe)kfree((void*)p->alarm_trapframe);p->alarm_trapframe = 0;//...p->ticks = 0;p->handler = 0;p->interval = 0;p->alarm_goingoff = 0;p->state = UNUSED;
}

随后,我们需要去编写我们的具体的系统调用的逻辑,sigalarm和sigreturn

uint64
sys_sigalarm(void) {int n;uint64 handler;// 获取参数argint(0, &n);argaddr(1, &handler);// 调用下一层return sigalarm(n, (void(*)())(handler));
}uint64
sys_sigreturn(void) {return sigreturn();
}

我们的sigreturn和sigalarm定义在trap.c

int sigalarm(int ticks, void(*handler)()) {// 初始化alarmstruct proc *p = myproc();p->interval = ticks;p->handler = handler;p->ticks = 0;return 0; 
}int sigreturn() {struct proc *p = myproc();// 恢复之前的trapframe,并清除alarm标志位*(p->trapframe) = *(p->alarm_trapframe);p->alarm_goingoff = 0;// 这里返回a0的原因是,当我们执行return的时候,返回值会被保存在a0中// 导致a0被覆盖,所以此时直接返回a0即可,我们在最后会进行分析return p->trapframe->a0;
}

当然,这两个函数还需要在kernel/defs.h中声明,否则会报错!

最后,回到我们的usertrap函数,我们会在这里完成最后的工作

void
usertrap(void)
{//...// give up the CPU if this is a timer interrupt.if(which_dev == 2) {if(p->interval != 0) { // 如果设定了时钟事件if(p->ticks++ == p->interval) {if(!p->alarm_goingoff) { // 确保没有时钟正在运行p->ticks = 0;*(p->alarm_trapframe) = *(p->trapframe);p->trapframe->epc = (uint64)p->handler;p->alarm_goingoff = 1;}}}yield();}usertrapret();
}

我们在which_dev满足等于2的条件的时候,会增加我们的时钟计时,当达到我们的间隔时间,就会保存我们的trapframe,并且修改我们的epc,epc是什么?就是我们返回用户态的时候,会执行的代码的指针,我们将需要执行的函数的地址赋给epc,也就是说,我们接下来就会去执行它,当然,如果需要我们的之前执行的函数能够恢复,也就意味着,我们需要在注册的函数里面主动去调用sigreturn,然后才能恢复到我们原来的用户态的中断的地方,这样,就完成了这个系统调用的闭环。

回到刚刚的问题,为什么要返回a0?

我们可以查看汇编代码来解决这个问题

kernel/kernel.asm

  return p->trapframe->a0;80001c44:	6d3c                	ld	a5,88(a0)      # 加载 p->trapframe 的地址到 a5,偏移 88 字节是 trapframe*
}80001c46:	5ba8                	lw	a0,112(a5)     # 加载 trapframe->a0 的值到 a0,偏移 112 字节是 a0 寄存器的位置80001c48:	60a2                	ld	ra,8(sp)       # 恢复调用者的返回地址(ra)80001c4a:	6402                	ld	s0,0(sp)       # 恢复调用者的帧指针(s0)80001c4c:	0141                	addi	sp,sp,16       # 恢复栈指针(释放本函数栈帧)80001c4e:	8082                	ret              # 返回到调用者,返回值已保存在 a0 中

我们可以看见,我们会将返回的代码赋给a0,但是即便如此,我们的a5也会被覆盖,所以最好的办法还是自己用汇编来实现这些上下文的切换。

那么最后,我们的alarm实验就完成了。

== Test backtrace test == 
$ make qemu-gdb
backtrace test: OK (2.6s) 
== Test running alarmtest == 
$ make qemu-gdb
(4.8s) 
== Test   alarmtest: test0 == alarmtest: test0: OK 
== Test   alarmtest: test1 == alarmtest: test1: OK 
== Test   alarmtest: test2 == alarmtest: test2: OK 
== Test   alarmtest: test3 == alarmtest: test3: OK 
== Test usertests == 
$ make qemu-gdb
usertests: OK (151.6s) 

即便之前读过了系统调用陷入的一系列代码,通过写这个lab4的实验,也是比较困难的,但也能学到一些东西的,虽然中途确实看了别人的代码,但是总归是写出来的,重要的不是看了别人的多少的代码,我倒是觉得这并不可耻,在一些无聊的地方卡住好几个小时没有一点进展,而因为秉持着学术诚信最后却因为一些bug而放弃,这反倒是我最不想看到的,最重要的是从这个实验中学到了多少,所以,在这里,我将自己学到的分享出去,希望能够帮助更多的人。

参考文献:

miigon’blog

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

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

相关文章

一文总结通信电路中LC谐振回路中各公式以及对深入解读品质因数Q

目录 前言 一、基本公式总结 1.并联谐振回路 2.串联谐振回路 二、浅谈品质因数 1.衡量谐振回路能量存储与能量损耗之比的无量纲参数&#xff0c;用于描述谐振电路的频率选择性 2.当受到振荡驱动力时&#xff0c;谐振腔的中心频率与其带宽的比值 3.为什么谐振时电容上的…

Linux:文件系统

一.认识硬件–磁盘 1. 物理结构 1.2 存储结构 ❓如何定位⼀个扇区呢&#xff1f; 可以先定位磁头&#xff08;header&#xff09;——》确定磁头要访问哪⼀个柱⾯(磁道)&#xff08;cylinder&#xff09;——》 定位⼀个扇区(sector)。 柱⾯&#xff08;cylinder&#xff09…

数字孪生废气处理工艺流程

图扑数字孪生废气处理工艺流程系统。通过精准 3D 建模&#xff0c;对废气收集、预处理、净化、排放等全流程进行 1:1 数字化复刻&#xff0c;实时呈现设备运行参数、污染物浓度变化等关键数据。 借助图扑可视化界面&#xff0c;管理者可直观掌握废气处理各环节状态&#xff0c…

Scratch——第18课 列表接龙问题

在四级的考级中&#xff0c;接龙的题目虽然在CIE中只出现过两次&#xff0c;但是这类题目对字符串的知识点考察相对全面。 一、接龙游戏的判断方法 接龙的内容对应的字符数 ? 已接龙内容的字符数 满足条件>接龙内容的第一个字符数 ? 上一项接龙的最后一个字符 满足条件…

webgl入门实例-向量在图形学中的核心作用

在图形学中&#xff0c;向量是描述几何、光照、运动等核心概念的基础工具。以下是向量在图形学中的关键应用和深入解析&#xff1a; 1. 向量的核心作用 几何表示&#xff1a;描述点、方向、法线、切线等。空间变换&#xff1a;平移、旋转、缩放等操作依赖向量运算。光照计算&a…

Redis 是如何保证线程安全的?

Redis 是如何保证线程安全的&#xff1f; Redis 是一个高性能的键值数据库&#xff0c;广泛应用于缓存、消息队列、实时分析等场景。由于其性能优势&#xff0c;Redis 已经成为许多系统的核心组件之一。然而&#xff0c;很多开发者在使用 Redis 时&#xff0c;常常会问&#x…

Img2img-turbo 在2080Ti上的测试笔记

1. 介绍 [img2img-turbo]是[pytorch-CycleGAN-and-pix2pix]推荐的更新的图像变换的代码实现&#xff1b; 2. 配置信息 Conda环境名称&#xff1a;img2img-turbo 3. 问题描述 当前在我们尝试使用了官方推荐的训练命令在2080Ti上进行训练&#xff0c; 3.1 出现了 CUDA out …

代码随想录算法训练营第三十五天|416. 分割等和子集、698.划分为k个相等的子集、473.火柴拼正方形

今日题目 416. 分割等和子集 题目链接&#xff1a;416. 分割等和子集 - 力扣&#xff08;LeetCode&#xff09; 思考&#xff1a;本题要将数组分为两个子数组&#xff0c;且两个子数组和相等&#xff0c;因此首先可以想到的条件就是数组可分为两个&#xff0c;这要求数组元素数…

纯CSS实现自动滚动到底部

<!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>自动滚动到底部</title><style>*…

【新人系列】Golang 入门(十五):类型断言

✍ 个人博客&#xff1a;https://blog.csdn.net/Newin2020?typeblog &#x1f4dd; 专栏地址&#xff1a;https://blog.csdn.net/newin2020/category_12898955.html &#x1f4e3; 专栏定位&#xff1a;为 0 基础刚入门 Golang 的小伙伴提供详细的讲解&#xff0c;也欢迎大佬们…

AI大模型发展现状与MCP协议诞生的技术演进

1. 大模型能力边界与用户痛点&#xff08;2023年&#xff09; 代表模型&#xff1a;GPT-4&#xff08;OpenAI&#xff09;、Claude 3&#xff08;Anthropic&#xff09;、通义千问&#xff08;阿里云&#xff09;等展现出强大的生成能力&#xff0c;但存在明显局限&#xff1a…

深入理解Linux中的线程控制:多线程编程的实战技巧

个人主页&#xff1a;chian-ocean 文章专栏-Linux 前言&#xff1a; POSIX线程&#xff08;Pthreads&#xff09; 是一种在 POSIX 标准下定义的线程库&#xff0c;它为多线程编程提供了统一的接口&#xff0c;主要用于 UNIX 和类 UNIX 系统&#xff08;如 Linux、MacOS 和 BS…

(mac)Grafana监控系统之监控Linux的Redis

Grafana安装-CSDN博客 普罗米修斯Prometheus监控安装&#xff08;mac&#xff09;-CSDN博客 1.Redis_exporter安装 直接下载 wget https://github.com/oliver006/redis_exporter/releases/download/v1.0.3/redis_exporter-v1.0.3.linux-amd64.tar.gz 解压 tar -xvf redis_…

鸿蒙应用元服务开发-Account Kit未成年人模式订阅和处理用户信息变更

一、概述 通过订阅用户信息变更&#xff0c;您可以接收有关用户及其账户的重要更新。当用户取消元服务的授权信息、注销华为账号时&#xff0c;华为账号服务器会发送通知到元服务&#xff0c;元服务可以根据通知消息进行自身业务处理。 二、用户信息变更事件介绍 三、订阅用…

buildroot构建根文件系统报错(已解决大部分问题)

title: buildroot构建根文件系统报错(set FORCE_UNSAFE_CONFIGURE1) author: cbus categories: 小知识 tags:小知识 abbrlink: 53691 date: 2025-04-20 08:03:00 错误1 set FORCE_UNSAFE_CONFIGURE1 在使用buildroot构建根文件系统时&#xff0c;一切按照文档的配置&#xff0…

7.QT-常用控件-QWidget|font|toolTip|focusPolicy|styleSheet(C++)

font API说明font()获取当前widget的字体信息.返回QFont对象.setFont(const QFont& font)设置当前widget的字体信息. 属性说明family字体家族.⽐如"楷体",“宋体”,"微软雅⿊"等.pointSize字体⼤⼩weight字体粗细.以数值⽅式表⽰粗细程度取值范围为[…

通过面向目标的奖励弥合人与机器人的灵活性差距

24年10月来自纽约大学的论文“Bridging the Human to Robot Dexterity Gap through Object-Oriented Rewards”。 直接通过人类视频训练机器人是机器人技术和计算机视觉领域的一个新兴领域。尽管双指机械手在双指夹持器方面取得了显著进展&#xff0c;但以这种方式让多指机械手…

C++入门篇(下)

目录 1、引用 1.1 引用概念 1.2 引用特性 1.3 常引用 1.4 使用场景 1.4.1 引用做参数 1.4.2 引用做返回值 1.5 引用和指针的区别 2、内联函数 2.1 概念 2.2 特性 3、auto关键字 4、基于范围的for循环 5、指针空值nullptr 5.1 C98 中的指针空值处理 5.2 C11 …

Multi-Query Attention (MQA) PyTorch 实现

和多头注意力机制的唯一区别&#xff1a;K、V在不同的head之间实现了复用&#xff0c;而对于不同的头&#xff0c;Q依然不同。 因此这里的代码和标准多头注意力的实现也是几乎完全一样&#xff1a; import torch import torch.nn as nn import torch.nn.functional as Fclass…

visual studio无法跳转到函数定义、变量定义、跳转函数位置不准问题解决

参考&#xff1a;https://blog.csdn.net/snakehacker/article/details/135438353 程序有时会出现大部分函数都不能准确的从头文件中正确定位到函数定位,这是因为数据库错乱造成的,可以通过重构数据库来解决,操作方法如下&#xff1a; 菜单栏&#xff1a;工具——选项 文本编辑…