Lab4: traps
Lab4: traps
文章目录
- Lab4: traps
- RISC-V assembly (easy)
- Backtrace(moderate) —— 打印每个栈的返回地址
- 第一步
- 第二步
- 第三步
- Alarm(Hard) —— 每个n个时钟周期,内核强制调用用户函数fn
- 第一步
- 第二步
RISC-V assembly (easy)
阅读***user/call.asm***中生成可读的汇编版,回答一下问题
- 哪些寄存器保存函数的参数?例如,在
main
对printf
的调用中,哪个寄存器保存13?main
的汇编代码中对函数f
的调用在哪里?对g
的调用在哪里(提示:编译器可能会将函数内联)printf
函数位于哪个地址?- 在
main
中printf
的jalr
之后的寄存器ra
中有什么值?- 运行以下代码。
unsigned int i = 0x00646c72; printf("H%x Wo%s", 57616, &i);
程序的输出是什么?这是将字节映射到字符的ASCII码表。
输出取决于RISC-V小端存储的事实。如果RISC-V是大端存储,为了得到相同的输出,你会把
i
设置成什么?是否需要将57616
更改为其他值?这里有一个小端和大端存储的描述和一个更异想天开的描述。
- 在下面的代码中,“
y=
”之后将打印什么(注:答案不是一个特定的值)?为什么会发生这种情况?printf("x=%d y=%d", 3);
/* call.c */
#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);
}
- 哪些寄存器保存函数的参数?例如,在
main
对printf
的调用中,哪个寄存器保存13?
a0保存寄存器返回值 0,a1寄存器保存12 (编译器计算好了), a2寄存器保存13
main
的汇编代码中对函数f
的调用在哪里?对g
的调用在哪里(提示:编译器可能会将函数内联)
编译器直接用计算结果替换了函数调用过程,因为始终未出现两函数地址而出现了计算结果
printf
函数位于哪个地址?
0000000000000630 <printf>:
34: 608080e7 jalr 1536(ra) # 630 <printf>
auipc的作用是把立即数左移12位,低12位补0,和pc相加赋给指定寄存器。这里立即数是0,指定寄存器是ra,即ra=pc=0x30=48。jalr作用是跳转到立即数+指定寄存器处并且把ra的值+8。因此jalr会跳转1536+48=1594=0x630处,观察汇编代码可知确实在0000000000000630处。
- 在
main
中printf
的jalr
之后的寄存器ra
中有什么值?
0x30+8=0x38
- 运行以下代码。
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
程序的输出是什么?输出取决于RISC-V小端存储的事实。如果RISC-V是大端存储,为了得到相同的输出,你会把i
设置成什么?是否需要将57616
更改为其他值?
输出为HE110 World
因为riscv为小端存储,从&i开始字节分别为0x72,0x6c,0x64, 0x00.分别对应’r’,‘l’,‘d’,'0’的ascii码,0x00作为字符串结束标志。
57616=0xE110
i应该设置为0x726c6400
- 运行printf(“x=%d y=%d”, 3);在y=后面输出什么?为什么会这样?
输出y=1。取决于寄存器a2(第3个参数)的值。
Backtrace(moderate) —— 打印每个栈的返回地址
回溯(Backtrace)通常对于调试很有用:它是一个存放于栈上用于指示错误发生位置的函数调用列表。
在***kernel/printf.c***中实现名为
backtrace()
的函数。在sys_sleep
中插入一个对此函数的调用,然后运行bttest
,它将会调用sys_sleep
。你的输出应该如下所示:backtrace: 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
你应该看到类似下面的输出:
kernel/sysproc.c:74 kernel/syscall.c:224 kernel/trap.c:85
编译器向每一个栈帧中放置一个帧指针(frame pointer)保存调用者帧指针的地址。你的
backtrace
应当使用这些帧指针来遍历栈,并在每个栈帧中打印保存的返回地址。
提示
- 在***kernel/defs.h***中添加
backtrace
的原型,那样你就能在sys_sleep
中引用backtrace
- GCC编译器将当前正在执行的函数的帧指针保存在
s0
寄存器,将下面的函数添加到***kernel/riscv.h***
static inline uint64
r_fp()
{uint64 x;asm volatile("mv %0, s0" : "=r" (x) );return x;
}
并在backtrace
中调用此函数来读取当前的帧指针。这个函数使用内联汇编来读取s0
- 这个课堂笔记中有张栈帧布局图。注意返回地址位于栈帧帧指针的固定偏移(-8)位置,并且保存的帧指针位于帧指针的固定偏移(-16)位置
- XV6在内核中以页面对齐的地址为每个栈分配一个页面。你可以通过
PGROUNDDOWN(fp)
和PGROUNDUP(fp)
(参见***kernel/riscv.h***)来计算栈页面的顶部和底部地址。这些数字对于backtrace
终止循环是有帮助的。
一旦你的backtrace
能够运行,就在***kernel/printf.c***的panic
中调用它,那样你就可以在panic
发生时看到内核的backtrace
。
第一步
按照提示,在kernel/defs.h添加定义,在kernel/riscv.h增加r_fp()
第二步
第五课的图,理解这个图就理解了这个函数怎么写,xv6里一个栈帧大小是16个字节
backtrace
这一个lab可以说是为了能够深刻理解stack machine
机制的设计的,每当调用函数时,首先需要将返回地址,之前的栈帧地址入栈,而栈空间的地址也是从高地址往低地址增长的,所以当前的栈帧的偏移8个字节即为return address
,我们需要每次打印出返回地址,同时偏移16个字节则为前一个栈帧的地址,我们依次往前寻找,直到当前的栈帧的起始地址为PGROUNDUP(fp)
:
***kernel/printf.c***中实现名为backtrace()
的函数
void
backtrace(void)
{uint64 sp = r_fp();// 找到栈底结束uint64 bottom = PGROUNDUP(fp);uint64 return_addr;printf("backtrace:\n");while (sp < bottom){return_addr = *(uint64*)(sp-8);sp = *(uint64*)(sp-16);printf("%p\n", return_addr);}
}
第三步
在kernel/sysproc.c的sys_sleep()函数中调用backtrace()
uint64
sys_sleep(void)
{int n;uint ticks0;if(argint(0, &n) < 0)return -1;acquire(&tickslock);ticks0 = ticks;while(ticks - ticks0 < n){if(myproc()->killed){release(&tickslock);return -1;}sleep(&ticks, &tickslock);}release(&tickslock);backtrace();return 0;
}
Alarm(Hard) —— 每个n个时钟周期,内核强制调用用户函数fn
在这个练习中你将向XV6添加一个特性,在进程使用CPU的时间内,XV6定期向进程发出警报。**这对于那些希望限制CPU时间消耗的受计算限制的进程,或者对于那些计算的同时执行某些周期性操作的进程可能很有用。更普遍的来说,你将实现用户级中断/故障处理程序的一种初级形式。**例如,你可以在应用程序中使用类似的一些东西处理页面故障。如果你的解决方案通过了
alarmtest
和usertests
就是正确的。
你应当添加一个新的sigalarm(interval, handler)
系统调用,如果一个程序调用了sigalarm(n, fn)
,那么每当程序消耗了CPU时间达到n个“滴答”,内核应当使应用程序函数fn
被调用。当fn
返回时,应用应当在它离开的地方恢复执行。在XV6中,一个滴答是一段相当任意的时间单元,取决于硬件计时器生成中断的频率。如果一个程序调用了sigalarm(0, 0)
,系统应当停止生成周期性的报警调用。
你将在XV6的存储库中找到名为***user/alarmtest.c***的文件。将其添加到***Makefile***。注意:你必须添加了sigalarm
和sigreturn
系统调用后才能正确编译(往下看)。
alarmtest
在test0
中调用了sigalarm(2, periodic)
来要求内核**每隔两个滴答强制调用periodic()
,然后旋转一段时间。你可以在*user/alarmtest.asm***中看到alarmtest
的汇编代码,这或许会便于调试。当alarmtest
产生如下输出并且usertests
也能正常运行时,你的方案就是正确的:
$ 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
$ usertests
...
ALL TESTS PASSED
$
当你完成后,你的方案也许仅有几行代码,但如何正确运行是一个棘手的问题。我们将使用原始存储库中的***alarmtest.c***版本测试您的代码。你可以修改***alarmtest.c***来帮助调试,但是要确保原来的alarmtest
显示所有的测试都通过了。
test0: invoke handler(调用处理程序)
首先修改内核以跳转到用户空间中的报警处理程序,这将导致test0
打印“alarm!”。不用担心输出“alarm!”之后会发生什么;如果您的程序在打印“alarm!”后崩溃,对于目前来说也是正常的。以下是一些提示:
- 您需要修改***Makefile***以使***alarmtest.c***被编译为xv6用户程序。
- 放入***user/user.h***的正确声明是:
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
- 更新***user/usys.pl***(此文件生成***user/usys.S***)、***kernel/syscall.h***和***kernel/syscall.c***以允许
alarmtest
调用sigalarm
和sigreurn
系统调用。 - 目前来说,你的
sys_sigreturn
系统调用返回应该是零。 - 你的
sys_sigalarm()
应该将报警间隔和指向处理程序函数的指针存储在struct proc
的新字段中(位于***kernel/proc.h***)。 - 你也需要在
struct proc
新增一个新字段。用于跟踪自上一次调用(或直到下一次调用)到进程的报警处理程序间经历了多少滴答;您可以在***proc.c***的allocproc()
中初始化proc
字段。 - 每一个滴答声,硬件时钟就会强制一个中断,这个中断在***kernel/trap.c***中的
usertrap()
中处理。 - 如果产生了计时器中断,您只想操纵进程的报警滴答;你需要写类似下面的代码
if(which_dev == 2) ...
- 仅当进程有未完成的计时器时才调用报警函数。请注意,用户报警函数的地址可能是0(例如,在***user/alarmtest.asm***中,
periodic
位于地址0)。 - 您需要修改
usertrap()
,以便当进程的报警间隔期满时,用户进程执行处理程序函数。当RISC-V上的陷阱返回到用户空间时,什么决定了用户空间代码恢复执行的指令地址? - 如果您告诉qemu只使用一个CPU,那么使用gdb查看陷阱会更容易,这可以通过运行
make CPUS=1 qemu-gdb
- 如果
alarmtest
打印“alarm!”,则您已成功。
test1/test2(): resume interrupted code(恢复被中断的代码)
alarmtest
打印“alarm!”后,很可能会在test0
或test1
中崩溃,或者alarmtest
(最后)打印“test1 failed”,或者alarmtest
未打印“test1 passed”就退出。要解决此问题,必须确保完成报警处理程序后返回到用户程序最初被计时器中断的指令执行。必须确保寄存器内容恢复到中断时的值,以便用户程序在报警后可以不受干扰地继续运行。最后,您应该在每次报警计数器关闭后“重新配置”它,以便周期性地调用处理程序。
作为一个起始点,我们为您做了一个设计决策:用户报警处理程序需要在完成后调用sigreurn
系统调用。请查看***alarmtest.c**中的periodic
作为示例。这意味着您可以将代码添加到usertrap
和sys_sigreurn
中,这两个代码协同工作,以使用户进程在处理完警报后正确恢复。
提示:
- 您的解决方案将要求您保存和恢复寄存器——您需要保存和恢复哪些寄存器才能正确恢复中断的代码?(提示:会有很多)
- 当计时器关闭时,让
usertrap
在struct proc
中保存足够的状态,以使sigreurn
可以正确返回中断的用户代码。 - 防止对处理程序的重复调用——如果处理程序还没有返回,内核就不应该再次调用它。
test2
测试这个。 - 一旦通过
test0
、test1
和test2
,就运行usertests
以确保没有破坏内核的任何其他部分。
第一步
1、2、3、4按照提示完成即可
5、6 添加:报警间隔和指向处理程序函数的指针(int ticks, void (*handler)() )、经历了多少滴答的字段
并在***proc.c***的allocproc()
中初始化proc
字段。
// alarmvoid (*handler)(); // alarm函数int alarm_interval; // 报警间隔int total_ticks; // 判断走过滴答数 要初始化的字段
7、8
如果产生了计时器中断,您只想操纵进程的报警滴答;你需要写类似下面的代码
// give up the CPU if this is a timer interrupt.if(which_dev == 2) {p->total_ticks++;yield(); // Give up the CPU for one scheduling round.}
第二步
9、10
修改usertrap()
,使得当进程的报警间隔期满时,用户进程执行处理程序函数。
添加一下32个寄存器,参考lec 6的内容
// 需要保存的历史寄存器用uint64 his_epc; uint64 his_ra;uint64 his_sp;uint64 his_gp;uint64 his_tp;uint64 his_t0;uint64 his_t1;uint64 his_t2;uint64 his_t3;uint64 his_t4;uint64 his_t5;uint64 his_t6;uint64 his_a0;uint64 his_a1;uint64 his_a2;uint64 his_a3;uint64 his_a4;uint64 his_a5;uint64 his_a6;uint64 his_a7;uint64 his_s0;uint64 his_s1;uint64 his_s2;uint64 his_s3;uint64 his_s4;uint64 his_s5;uint64 his_s6;uint64 his_s7;uint64 his_s8;uint64 his_s9;uint64 his_s10;uint64 his_s11;
sys_alarm的实现
uint64
sys_sigalarm(void)
{int interval;uint64 handler;struct proc* p = myproc();if (argint(0, &interval) < 0)return -1;if (argaddr(1, &handler) < 0)return -1;p->alarm_interval = interval;p->handler = (void(*)())handler;return 0;
}
sys_sigreturn的实验
uint64
sys_sigreturn(void)
{// 重载寄存器struct proc *p = myproc();p->trapframe->epc = p->his_epc; p->trapframe->ra = p->his_ra; p->trapframe->sp = p->his_sp; p->trapframe->gp = p->his_gp; p->trapframe->tp = p->his_tp; p->trapframe->a0 = p->his_a0; p->trapframe->a1 = p->his_a1; p->trapframe->a2 = p->his_a2; p->trapframe->a3 = p->his_a3; p->trapframe->a4 = p->his_a4; p->trapframe->a5 = p->his_a5; p->trapframe->a6 = p->his_a6; p->trapframe->a7 = p->his_a7; p->trapframe->t0 = p->his_t0; p->trapframe->t1 = p->his_t1; p->trapframe->t2 = p->his_t2; p->trapframe->t3 = p->his_t3; p->trapframe->t4 = p->his_t4; p->trapframe->t5 = p->his_t5; p->trapframe->t6 = p->his_t6;p->trapframe->s0 = p->his_s0;p->trapframe->s1 = p->his_s1;p->trapframe->s2 = p->his_s2;p->trapframe->s3 = p->his_s3;p->trapframe->s4 = p->his_s4;p->trapframe->s5 = p->his_s5;p->trapframe->s6 = p->his_s6;p->trapframe->s7 = p->his_s7;p->trapframe->s8 = p->his_s8;p->trapframe->s9 = p->his_s9;p->trapframe->s10 = p->his_s10;p->trapframe->s11 = p->his_s11;p->is_handler_in = 1;return 0;
}
然后就是修改usertrap完成最后要求
if(which_dev == 2) {p->total_ticks++;if (p->is_handler_in){p->is_handler_in = 0;// 保存当前trampframe上的用户寄存器// 用于alarm函数调用完之后p->his_epc = p->trapframe->epc; p->his_ra = p->trapframe->ra;p->his_sp = p->trapframe->sp;p->his_gp = p->trapframe->gp;p->his_tp = p->trapframe->tp;p->his_t0 = p->trapframe->t0;p->his_t1 = p->trapframe->t1;p->his_t2 = p->trapframe->t2;p->his_t3 = p->trapframe->t3;p->his_t4 = p->trapframe->t4;p->his_t5 = p->trapframe->t5;p->his_t6 = p->trapframe->t6;p->his_a0 = p->trapframe->a0;p->his_a1 = p->trapframe->a1;p->his_a2 = p->trapframe->a2;p->his_a3 = p->trapframe->a3;p->his_a4 = p->trapframe->a4;p->his_a5 = p->trapframe->a5;p->his_a6 = p->trapframe->a6;p->his_a7 = p->trapframe->a7;p->his_s0 = p->trapframe->s0;p->his_s1 = p->trapframe->s1;p->his_s2 = p->trapframe->s2;p->his_s3 = p->trapframe->s3;p->his_s4 = p->trapframe->s4;p->his_s5 = p->trapframe->s5;p->his_s6 = p->trapframe->s6;p->his_s7 = p->trapframe->s7;p->his_s8 = p->trapframe->s8;p->his_s9 = p->trapframe->s9;p->his_s10 = p->trapframe->s10;p->his_s11 = p->trapframe->s11;// 将当前PC值改为alarm函数的地址// 当函数从内核返回时调用handlerp->trapframe->epc = (uint64)p->handler;p->total_ticks = 0;} else {p->total_ticks -= 1;}yield(); // Give up the CPU for one scheduling round.}
Lab4: traps相关推荐
- MIT 6.S081 Lab4 traps
#Lab4: traps #Source #My Code #Motivation #Backtrace (moderate) #Motivation #Solution #S0 - RISC-V 栈 ...
- MIT6.S081学习总结-lab4:traps
lab4 是traps相关 Backtrace 添加一个backtrace函数,sys_sleep调用这个函数后可以打印出函数调用栈 实现: kernel/riscv.h里添加函数来获取frame p ...
- 6.S081 lab4: traps
1. 写在前面 感觉这个lab给我最大的收获就是:不加参数的cat,用ctrl+D退出(笑).之前用cat的时候忘了写参数然后就陷入cat的循环里去了,不知道怎么退出,就只好强行关掉shell.另 ...
- 6.S081 Lab4 Traps
第一部分 RISC-V assembly 阅读汇编 相关的C代码: #include "kernel/param.h" #include "kernel/types.h& ...
- MIT6.S081 2021
MIT6.S081 2021 环境配置 Xv6 and Unix utilities vscode格式化头文件排序问题 以地址空间的视角看待变量 其他 代码参考 system calls trace ...
- mit 6.s081
简介 xv6-book chapter1 Operating system interfaces chapter2 Operating system organization Code:startin ...
- XV6实验(2020)
XV6实验记录(2020) 环境搭建 参考连接 Lab guidance (mit.edu) 6.S081 / Fall 2020 (mit.edu) xv6 book中文版 Lab1:Xv6 and ...
- switch分解试验部分-LAB4:VLAN VTP设置
LAB4:VLAN VTP设置 一.实验目的 1.掌握交换机VTP的应用及配置 二.实验内容 拓扑图: 需求: 1.简化VLAN的配置 2.减少VLAN的泛洪 3.确保VLAN信息的安全 三.实验配置 ...
- xv6---Lab4 traps
参考: Lab: Traps 关于寄存器s0和堆栈 https://pdos.csail.mit.edu/6.828/2020/lec/l-riscv-slides.pdf RISC-V assemb ...
- Mining of Massive Dataset----PageRank的两种问题spider traps和dead ends
PageRank的两种问题 spider traps(蛛网陷阱) 在几个网页的节点之间跳转,经过一段很长的时间之后,只能在节点n来回跳转(也就是说不嫩访问到其他的网页,只能点击访问节点n这个网页). ...
最新文章
- mysql如何下载连接到visual_Visual Studio 2015 Community连接到Mysql
- leetcode算法题--替换空格
- 张莉python 玩转数据答案_大学mooc2020年用Python玩转数据课后答案
- php mysql连续签到跨月_PHP连续签到功能实现方法详解
- [BZOJ3093][Fdu校赛2012] A Famous Game(不等概率)
- Python operator.le()函数与示例
- java上传csv错误信息_java处理csv文件上传示例详解
- data.name.toLowerCase() is not a function问题
- python gevent asyncio_python用from gevent import monkey; monkey.patch_all()之后报ssl等错误
- 泛泰A870S官方4.4.2系统S0218210 内核版本号信息
- 编程 单引号 双引号_我的25个最喜欢的编程引号也很有趣
- node设置跨域白名单
- 在同个工程中使用 Swift 和 Objective-C(Swift 2.0更新)-b
- 计算机管理-共享打印,打印机局域网共享怎么设置?最简单稳定的方法:一键共享...
- [境内法规]中国人民银行关于印发《反洗钱现场检查管理办法(试行)》的通知—银发〔2007〕175号
- 北理工计算机学院沈建斌,中国高校计算机大赛-团体程序设计天梯赛全国总决赛获奖.doc...
- 这次跟大家聊聊技术,也聊聊人生
- 华为交换机配置consol密码及vty密码
- 奈奎斯特采样定理之我见
- Widget中的一些基本概念