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
fin the assembly code for main? Where is the call tog? (Hint: the compiler may inline functions.)
对f的调用我发现已经被编译器所优化了,这里直接将一个立即数存入了a1中:
26: 45b1 li a1,12
第三个问题
At what address is the function
printflocated?
根据汇编代码,我们可以知道,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
rajust after thejalrtoprintfinmain?
在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 usePGROUNDDOWN(fp)(seekernel/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.cto 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
alarmtestto invoke the sigalarm and sigreturn system calls.For now, your
sys_sigreturnshould just return zero.Your
sys_sigalarm()should store the alarm interval and the pointer to the handler function in new fields in theprocstructure (inkernel/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 procfor this too. You can initializeprocfields inallocproc()inproc.c.Every tick, the hardware clock forces an interrupt, which is handled in
usertrap()inkernel/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,
periodicis 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-gdbYou’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
usertrapsave enough state instruct procwhen the timer goes off thatsigreturncan 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.
test2tests this.- Make sure to restore a0.
sigreturnis 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
相关文章:
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干了什么: …...
精通 Spring Cache + Redis:避坑指南与最佳实践
Spring Cache 以其优雅的注解方式,极大地简化了 Java 应用中缓存逻辑的实现。结合高性能的内存数据库 Redis,我们可以轻松构建出响应迅速、扩展性强的应用程序。然而,在享受便捷的同时,一些常见的“坑”和被忽视的最佳实践可能会悄…...
[SpringBoot]快速入门搭建springboot
默认有spring基础,不会一行代码一行代码那么细致地讲。 SpringBoot的作用 Spring Boot是为了简化Spring应用的创建、运行、调试、部署等而出现的。就像我们整个SSM框架时,就常常会碰到版本导致包名对不上、Bean非法参数类型的一系列问题(原出…...
理解.NET Core中的配置Configuration
什么是配置 .NET中的配置,本质上就是key-value键值对,并且key和value都是字符串类型。 在.NET中提供了多种配置提供程序来对不同的配置进行读取、写入、重载等操作,这里我们以为.NET 的源码项目为例,来看下.NET中的配置主要是有…...
C++面试八股文:智能指针
一、了解哪些智能指针? 回答:智能指针是用于管理动态分配的内存,行为类似于指针,但又具有自动管理内存的能力,所以称为智能指针。 首先说一下 auto_ptr和unique_ptr,它们都是独占式指针,同一时…...
nohup命令使用说明
文章目录 如何在后台运行程序呢?如何正常运行代码重定向呢?nohup: ignoring input 如何在后台运行程序呢? 使用nohup命令即可, nohup python dataset/ReferESpatialDataset.py >>dataset_20250417.log 2>&1 &n…...
MYSQL “Too Many Connections“ 错误解决
1.查询当前连接数 show status like "Threads_connected"; 2.查询数据库最大连接数 show variables like "max_connections" 3.查询所有活动连接 show processlist; 4.根据查询结果观察是否有长时间未被释放的连接 参数解释 : 字段说明id连接的唯一…...
Linux `init 6` 相关命令的完整使用指南
Linux init 6 相关命令的完整使用指南—目录 一、init 系统简介二、init 6 的含义与作用三、不同 Init 系统下的 init 6 行为1. SysVinit(如 CentOS 6、Debian 7)2. systemd(如 CentOS 7、Ubuntu 16.04)3. Upstart(如 …...
【外研在线-注册/登录安全分析报告】
前言 由于网站注册入口容易被黑客攻击,存在如下安全问题: 暴力破解密码,造成用户信息泄露短信盗刷的安全问题,影响业务及导致用户投诉带来经济损失,尤其是后付费客户,风险巨大,造成亏损无底洞…...
【NLP 63、大模型应用 —— Agent】
人与人最大的差距就是勇气和执行力,也是唯一的差距 —— 25.4.16 一、Agent 相关工作 二、Agent 特点 核心特征: 1.专有场景(针对某个垂直领域) 2.保留记忆(以一个特定顺序做一些特定任务,记忆当前任务的前…...
React 打包
路由懒加载 原本的加载方式 #使用lazy()函数声明的路由页面 使用Suspense组件进行加载 使用CDN优化...
2025.4.14-2025.4.20学习周报
目录 摘要Abstract1. 文献阅读1.1 模型架构1.2 实验分析1.3 代码实践 总结 摘要 在本周阅读的论文中,作者提出了一种名为MGSFformer的空气质量预测模型。模型通过残差去冗余模块可以有效解耦多粒度数据间的信息重叠;时空注意力模块采用并行建模策略&…...
Spring 微服务解决了单体架构的哪些痛点?
1. 部署困难 (Deployment Difficulty & Risk) 单体痛点: 整体部署: 对单体应用的任何微小修改(哪怕只是一行代码),都需要重新构建、测试和部署整个庞大的应用程序。部署频率低: 由于部署过程复杂且风险高,发布周期通常很长&a…...
【1】云原生,kubernetes 与 Docker 的关系
Kubernetes?K8s? Kubernetes经常被写作K8s。其中的数字8替代了K和s中的8个字母——这一点倒是方便了发推,也方便了像我这样懒惰的人。 什么是云原生? 云原生: 它是一种构建和运行应用程序的方法,它包含&am…...
Kubernetes控制平面组件:APIServer 限流机制详解
云原生学习路线导航页(持续更新中) kubernetes学习系列快捷链接 Kubernetes架构原则和对象设计(一)Kubernetes架构原则和对象设计(二)Kubernetes架构原则和对象设计(三)Kubernetes控…...
springboot全局异常捕获处理
一、需求 实际项目中,经常抛出各种异常,不能直接抛出异常给前端,这样用户体验相当不好,用户看不懂你的Exception,对于一些sql异常,直接抛到页面上也不安全。所以有没有好的办法解决这些问题呢,当然有了&am…...
Flask(1): 在windows系统上部署项目1
1 前言 学习python也有段时间了,最近一个小项目要部署,正好把过程写下来。 在程序的结构上我选择了w/s模式,相比于c/s模式,无需考虑客户端的升级;框架我选择了flask,就是冲着轻量级去的,就是插件…...
【文献阅读】EndoNet A Deep Architecture for Recognition Tasks on Laparoscopic Videos
关于数据集的整理 Cholec80 胆囊切除手术视频数据集介绍 https://zhuanlan.zhihu.com/p/700024359 数据集信息 Cholec80 数据集 是一个针对内窥镜引导 下的胆囊切除手术视频流程识别数据集。数据集提供了每段视频中总共7种手术动作及总共7种手术工具的标注,标…...
基于springboot的个人财务管理系统的设计与实现
博主介绍:java高级开发,从事互联网行业六年,熟悉各种主流语言,精通java、python、php、爬虫、web开发,已经做了六年的毕业设计程序开发,开发过上千套毕业设计程序,没有什么华丽的语言࿰…...
Linux系统编程---孤儿进程与僵尸进程
1、前言 在上一篇博客文章已经对Linux系统编程内容进行了较为详细的梳理,本文将在上一篇的基础上,继续梳理Linux系统编程中关于孤儿进程和僵尸进程的知识脉络。如有疑问的博客朋友可以通过下面的博文链接进行参考学习。 Linux系统编程---多进程-CSDN博客…...
简单使用MCP
简单使用MCP 1 简介 模型上下文协议(Model Context Protocol,MCP)是由Anthropic(产品是Claude)推出的开放协议,它规范了应用程序如何向LLM提供上下文。MCP可帮助你在LLM之上构建代理和复杂的工作流。 从…...
Semaphore的核心机制
在 Java 中,Semaphore 通过 许可计数器 和 同步队列 的机制实现并发线程数的限制。以下是其核心实现原理和步骤的详细分析: 一、核心机制 许可计数器(Permits) • 初始化时指定的许可数(如 new Semaphore(3)࿰…...
计算机视觉与深度学习 | RNN原理,公式,代码,应用
RNN(循环神经网络)详解 一、原理 RNN(Recurrent Neural Network)是一种处理序列数据的神经网络,其核心思想是通过循环连接(隐藏状态)捕捉序列中的时序信息。每个时间步的隐藏状态 ( h_t ) 不仅依赖当前输入 ( x_t ),还依赖前一时间步的隐藏状态 ( h_{t-1} ),从而实现…...
Keil MDK 编译问题:last line of file ends without a newline
问题与处理策略 问题描述 ..\..\User\main.c(38): warning: #1-D: last line of file ends without a newline} ..\..\User\main.c: 1 warning, 0 errors问题原因 这是文件末尾缺少换行符警告 处理策略 在文件(main.c)的最后一行按回车键添加一个空…...
MySQL:9.表的内连和外连
9.表的内连和外连 表的连接分为内连和外连 9.1 内连接 内连接实际上就是利用where子句对两种表形成的笛卡儿积进行筛选,之前查询都是内连 接,也是在开发过程中使用的最多的连接查询。 语法: select 字段 from 表1 inner join 表2 on 连接…...
C++栈操作集合
数组 #include <bits/stdc.h> using namespace std;class sss{ private:int a[1000];int curr -1; public:void push(int);void pop();int top();bool empyt();int size(); };int main() {sss n;while(true){int a;cout<<"1.添加\n2.删除-\n3.显示栈顶\n4.储…...
在阿里云和树莓派上编写一个守护进程程序
目录 一、阿里云邮件守护进程 1. 安装必要库 2. 创建邮件发送脚本 mail_daemon.py 3. 设置后台运行 二、树莓派串口守护进程 1. 启用树莓派串口 2. 安装依赖库 3. 创建串口输出脚本 serial_daemon.py 4. 设置开机自启 5. 使用串口助手接收 一、阿里云邮件守护进程 1.…...
每日一题——最小测试用例集覆盖问题
最小测试用例集覆盖问题(C语言实现) 问题描述 假设我们有一系列测试用例,每个测试用例会覆盖若干个代码模块。 我们使用一个二维数组来表示这些测试用例的覆盖情况: 如果某个测试用例 i 能覆盖代码模块 j,则数组中…...
LangChain 单智能体模式示例【纯代码】
# LangChain 单智能体模式示例import os from typing import Anyfrom langchain.agents import AgentType, initialize_agent, Tool from langchain_openai import ChatOpenAI from langchain.tools import BaseTool from langchain_experimental.tools.python.tool import Pyt…...
基于前端技术的QR码API开发实战:从原理到部署
前言 QR码(Quick Response Code)是一种二维码,于1994年开发。它能快速存储和识别数据,包含黑白方块图案,常用于扫描获取信息。QR码具有高容错性和快速读取的优点,广泛应用于广告、支付、物流等领域。通过扫…...
