📖
R‘Notes
  • 关于本仓库/网站
  • Note
    • Golang的知识碎片
      • 关于Golang的一些碎片知识
    • LeetCode
      • LCR 121. 寻找目标值 - 二维数组
      • LCR 125. 图书整理 II
      • LCR 127. 跳跃训练
      • LCR 143. 子结构判断
      • LCR 159. 库存管理 III
      • LCR 161. 连续天数的最高销售额
      • LCR 170. 交易逆序对的总数
      • LCR 174. 寻找二叉搜索树中的目标节点
      • LeetCode--1. 两数之和
      • LeetCode--10. 正则表达式匹配
      • LeetCode--1004. 最大连续1的个数 III
      • LeetCode--101. 对称二叉树
      • LeetCode--102. 二叉树的层序遍历
      • LeetCode--1027. 最长等差数列
      • LeetCode--103. 二叉树的锯齿形层序遍历
      • LeetCode--1035. 不相交的线
      • LeetCode--104. 二叉树的最大深度
      • LeetCode--1044. 最长重复子串
      • LeetCode--1049. 最后一块石头的重量 II
      • LeetCode--105. 从前序与中序遍历序列构造二叉树
      • LeetCode--106. 从中序与后序遍历序列构造二叉树
      • LeetCode--110. 平衡二叉树
      • LeetCode--111. 二叉树的最小深度
      • LeetCode--112. 路径总和
      • LeetCode--113. 路径总和 II
      • LeetCode--1137. 第 N 个泰波那契数
      • LeetCode--114. 二叉树展开为链表
      • LeetCode--1143. 最长公共子序列
      • LeetCode--115. 不同的子序列
      • LeetCode--1191. K 次串联后最大子数组之和
      • LeetCode--120. 三角形最小路径和
      • LeetCode--121. 买卖股票的最佳时机
      • LeetCode--1218. 最长定差子序列
      • LeetCode--122. 买卖股票的最佳时机 II
      • LeetCode--1220. 统计元音字母序列的数目
      • LeetCode--123. 买卖股票的最佳时机 III
      • LeetCode--124. 二叉树中的最大路径和
      • LeetCode--125. 验证回文串
      • LeetCode--128. 最长连续序列
      • LeetCode--1289. 下降路径最小和 II
      • LeetCode--129. 求根节点到叶节点数字之和
      • LeetCode--1301. 最大得分的路径数目
      • LeetCode--1312. 让字符串成为回文串的最少插入次数
      • LeetCode--134. 加油站
      • LeetCode--135. 分发糖果
      • LeetCode--136. 只出现一次的数字
      • LeetCode--138. 随机链表的复制
      • LeetCode--139. 单词拆分
      • LeetCode--14. 最长公共前缀
      • LeetCode--141. 环形链表
      • LeetCode--142. 环形链表 II
      • LeetCode--143. 重排链表
      • LeetCode--144. 二叉树的前序遍历
      • LeetCode--145. 二叉树的后序遍历
      • LeetCode--146. LRU 缓存
      • LeetCode--148. 排序链表
      • LeetCode--15. 三数之和
      • LeetCode--151. 反转字符串中的单词
      • LeetCode--152. 最大乘积子数组【DP】
      • LeetCode--153. 寻找旋转排序数组中的最小值
      • LeetCode--155. 最小栈
      • LeetCode--1584. 连接所有点的最小费用,最小生成树模板题
      • LeetCode--1594. 矩阵的最大非负积
      • LeetCode--16. 最接近的三数之和
      • LeetCode--160. 相交链表
      • LeetCode--162. 寻找峰值
      • LeetCode--165. 比较版本号
      • LeetCode--169. 多数元素
      • LeetCode--174. 地下城游戏
      • LeetCode--1774. 最接近目标价格的甜点成本
      • LeetCode--179. 最大数
      • LeetCode--1824. 最少侧跳次数
      • LeetCode--188. 买卖股票的最佳时机 IV
      • LeetCode--189. 轮转数组
      • LeetCode--19. 删除链表的倒数第 N 个结点,关于删除链表会遇见的指针问题
      • LeetCode--1937. 扣分后的最大得分
      • LeetCode--1964. 找出到每个位置为止最长的有效障碍赛跑路线
      • LeetCode--198. 打家劫舍
      • LeetCode--199. 二叉树的右视图
      • LeetCode--2. 两数相加
      • LeetCode--20. 有效的括号
      • LeetCode--200. 岛屿数量
      • LeetCode--206. 反转链表
      • LeetCode--207. 课程表
      • LeetCode--208. 实现 Trie (前缀树)
      • LeetCode--209. 长度最小的子数组
      • LeetCode--21. 合并两个有序链表,关于链表的复习
      • LeetCode--210. 课程表 II
      • LeetCode--213. 打家劫舍 II
      • LeetCode--2140. 解决智力问题
      • LeetCode--215. 数组中的第K个最大元素
      • LeetCode--22. 括号生成
      • LeetCode--221. 最大正方形
      • LeetCode--2218. 从栈中取出 K 个硬币的最大面值和
      • LeetCode--224. 基本计算器
      • LeetCode--225. 用队列实现栈
      • LeetCode--226. 翻转二叉树
      • LeetCode--2266. 统计打字方案数
      • LeetCode--2267. 检查是否有合法括号字符串路径
      • LeetCode--227. 基本计算器 II
      • LeetCode--23. 合并 K 个升序链表【堆和分治】
      • LeetCode--230. 二叉搜索树中第 K 小的元素
      • LeetCode--2304. 网格中的最小路径代价
      • LeetCode--232. 用栈实现队列
      • LeetCode--2320. 统计放置房子的方式数
      • LeetCode--2321. 拼接数组的最大分数
      • LeetCode--2328. 网格图中递增路径的数目
      • LeetCode--233. 数字 1 的个数
      • LeetCode--234. 回文链表
      • LeetCode--236. 二叉树的最近公共祖先
      • LeetCode--239. 滑动窗口最大值,关于单调队列的复习
      • LeetCode--24. 两两交换链表中的节点
      • LeetCode--240. 搜索二维矩阵 II
      • LeetCode--2435. 矩阵中和能被 K 整除的路径
      • LeetCode--2466. 统计构造好字符串的方案数
      • LeetCode--25. K 个一组翻转链表
      • LeetCode--2533. 好二进制字符串的数量
      • LeetCode--256. 粉刷房子
      • LeetCode--2606. 找到最大开销的子字符串
      • LeetCode--265. 粉刷房子 II
      • LeetCode--2684. 矩阵中移动的最大次数
      • LeetCode--2787. 将一个数字表示成幂的和的方案数
      • LeetCode--279. 完全平方数【动态规划】
      • LeetCode--283. 移动零
      • LeetCode--287. 寻找重复数
      • LeetCode--2915. 和为目标值的最长子序列的长度
      • LeetCode--295. 数据流的中位数
      • LeetCode--297. 二叉树的序列化与反序列化
      • LeetCode--3. 无重复字符的最长子串
      • LeetCode--300. 最长递增子序列【DP+二分】
      • LeetCode--3082. 求出所有子序列的能量和
      • LeetCode--309. 买卖股票的最佳时机含冷冻期
      • LeetCode--31. 下一个排列
      • LeetCode--3180. 执行操作可获得的最大总奖励 I
      • LeetCode--3186. 施咒的最大总伤害
      • LeetCode--32. 最长有效括号【栈和dp】
      • LeetCode--322. 零钱兑换
      • LeetCode--328. 奇偶链表
      • LeetCode--329. 矩阵中的最长递增路径
      • LeetCode--33. 搜索旋转排序数组【直接二分】
      • LeetCode--3363. 最多可收集的水果数目
      • LeetCode--337. 打家劫舍 III
      • LeetCode--3393. 统计异或值为给定值的路径数目
      • LeetCode--34. 在排序数组中查找元素的第一个和最后一个位置
      • LeetCode--3418. 机器人可以获得的最大金币数
      • LeetCode--343. 整数拆分
      • LeetCode--347. 前 K 个高频元素
      • LeetCode--347. 前 K 个高频元素Golang中的堆(containerheap)
      • LeetCode--3489. 零数组变换 IV
      • LeetCode--354. 俄罗斯套娃信封问题
      • LeetCode--377. 组合总和 Ⅳ
      • LeetCode--39. 组合总和
      • LeetCode--394. 字符串解码【栈】
      • LeetCode--395. 至少有 K 个重复字符的最长子串
      • LeetCode--4. 寻找两个正序数组的中位数
      • LeetCode--40. 组合总和 II
      • LeetCode--402. 移掉 K 位数字
      • LeetCode--41. 缺失的第一个正数
      • LeetCode--415. 字符串相加
      • LeetCode--416. 分割等和子集_494. 目标和【01背包】
      • LeetCode--42. 接雨水(单调栈和双指针)
      • LeetCode--426. 将二叉搜索树转化为排序的双向链表
      • LeetCode--43. 字符串相乘
      • LeetCode--437. 路径总和 III【前缀和】
      • LeetCode--44. 通配符匹配
      • LeetCode--440. 字典序的第K小数字
      • LeetCode--442. 数组中重复的数据
      • LeetCode--445. 两数相加 II
      • LeetCode--45. 跳跃游戏 II
      • LeetCode--450. 删除二叉搜索树中的节点
      • LeetCode--46. 全排列
      • LeetCode--460. LFU 缓存
      • LeetCode--468. 验证IP地址
      • LeetCode--470. 用 Rand7() 实现 Rand10()
      • LeetCode--474. 一和零
      • LeetCode--48. 旋转图像
      • LeetCode--498. 对角线遍历
      • LeetCode--5. 最长回文子串
      • LeetCode--50. Pow(x, n)
      • LeetCode--509. 斐波那契数
      • LeetCode--516. 最长回文子序列
      • LeetCode--518. 零钱兑换 II
      • LeetCode--529. 扫雷游戏题解C++广搜
      • LeetCode--53. 最大子数组和
      • LeetCode--54. 螺旋矩阵
      • LeetCode--540. 有序数组中的单一元素
      • LeetCode--543. 二叉树的直径
      • LeetCode--55. 跳跃游戏
      • LeetCode--556. 下一个更大元素 III
      • LeetCode--56. 合并区间
      • LeetCode--560. 和为 K 的子数组
      • LeetCode--572. 另一棵树的子树
      • LeetCode--59. 螺旋矩阵 II
      • LeetCode--61. 旋转链表
      • LeetCode--62. 不同路径
      • LeetCode--622. 设计循环队列
      • LeetCode--63. 不同路径 II
      • LeetCode--64. 最小路径和
      • LeetCode--646. 最长数对链
      • LeetCode--662. 二叉树最大宽度
      • LeetCode--673. 最长递增子序列的个数
      • LeetCode--678. 有效的括号字符串
      • LeetCode--679. 24 点游戏
      • LeetCode--69. x 的平方根
      • LeetCode--695. 岛屿的最大面积
      • LeetCode--7. 整数反转
      • LeetCode--70. 爬楼梯
      • LeetCode--704. 二分查找
      • LeetCode--712. 两个字符串的最小ASCII删除和
      • LeetCode--714. 买卖股票的最佳时机含手续费
      • LeetCode--718. 最长重复子数组
      • LeetCode--72. 编辑距离
      • LeetCode--739. 每日温度
      • LeetCode--74. 搜索二维矩阵
      • LeetCode--740. 删除并获得点数
      • LeetCode--746. 使用最小花费爬楼梯
      • LeetCode--75. 颜色分类
      • LeetCode--76. 最小覆盖子串
      • LeetCode--77. 组合
      • LeetCode--78. 子集
      • LeetCode--79. 单词搜索
      • LeetCode--790. 多米诺和托米诺平铺
      • LeetCode--8. 字符串转换整数 (atoi)
      • LeetCode--82. 删除排序链表中的重复元素 II
      • LeetCode--83. 删除排序链表中的重复元素
      • LeetCode--84. 柱状图中最大的矩形【单调栈】
      • LeetCode--85. 最大矩形
      • LeetCode--87. 扰乱字符串
      • LeetCode--879. 盈利计划
      • LeetCode--88. 合并两个有序数组
      • LeetCode--887. 鸡蛋掉落
      • LeetCode--91. 解码方法
      • LeetCode--912. 排序数组
      • LeetCode--918. 环形子数组的最大和
      • LeetCode--92. 反转链表 II
      • LeetCode--93. 复原 IP 地址
      • LeetCode--931. 下降路径最小和
      • LeetCode--94. 二叉树的中序遍历
      • LeetCode--958. 二叉树的完全性检验
      • LeetCode--97. 交错字符串
      • LeetCode--98. 验证二叉搜索树
      • LeetCode--983. 最低票价
      • LeetCode--LCR 140. 训练计划 II
      • NC--311.圆环回原点
      • NC--36进制加法
      • 补充题1. 排序奇升偶降链表
    • Redis
      • Redis基础部分
      • 在用Docker配置Redis哨兵节点的时候出现的错误及其解决
    • SQL学习记录
      • SQL碎片知识
      • 系统
        • MySQL学习笔记1【DQL和DCL】
        • MySQL学习笔记2【函数/约束/多表查询】
        • MySQL学习笔记3【事务】
        • MySQL学习笔记4【存储引擎和索引】
        • MySQL学习笔记5【SQL优化/视图/存储过程/触发器】
        • MySQL学习笔记6【锁】
        • MySQL学习笔记7【InnoDB】
    • x86汇编
      • 学习汇编随手记
    • 微服务相关
      • Nacos与gRPC
      • 【Golangnacos】nacos配置的增删查改,以及服务注册的golang实例及分析
    • 手搓
      • Whalebox(仿Docker)的爆诞
    • 操作系统
      • 操作系统碎片知识
      • MIT6.S081
        • MIT6.S081-lab1
        • MIT6.S081-lab2
        • MIT6.S081-lab3
        • MIT6.S081-lab3前置
        • MIT6.S081-lab4
        • MIT6.S081-lab4前置
        • MIT6.S081-lab5
        • MIT6.S081-lab5前置
        • MIT6.S081-lab7
        • MIT6.S081-lab7前置
        • MIT6.S081-lab8
        • MIT6.S081-lab8前置
        • MIT6.S081-lab9
        • MIT6.S081-环境搭建
    • 消息队列MQ
      • Kafka
    • 算法杂谈
      • 关于二分查找时的边界分类问题
    • 计组笔记
      • 计算机组成原理的学习笔记(1)--概述
      • 计算机组成原理的学习笔记(10)--CPU·其二 组合逻辑控制器和微程序
      • 计算机组成原理的学习笔记(11)--CPU·其三 中断和异常多处理器相关概念
      • 计算机组成原理的学习笔记(12)--总线和IO系统
      • 计算机组成原理的学习笔记(2)--数据的表示和运算·其一
      • 计算机组成原理的学习笔记(3)--数据表示与运算·其二 逻辑门和加减乘
      • 计算机组成原理的学习笔记(4)--数据的表示与运算·其三 补码的乘法以及原码补码的除法
      • 计算机组成原理的学习笔记(4)--数据的表示与运算·其三 补码的乘法以及原码补码的除法
      • 计算机组成原理的学习笔记(6)--存储器·其一 SRAMDRAMROM主存储器的初步认识
      • 计算机组成原理的学习笔记(7)--存储器·其二 容量扩展多模块存储系统外存Cache虚拟存储器
      • 计算机组成原理的学习笔记(8)--指令系统·其一 指令的组成以及数据寻址方式RISK和CISK
      • 计算机组成原理的学习笔记(9)--CPU·其一 CPU的基本概念流水线技术数据通路
由 GitBook 提供支持
在本页
  • 1. RISC-V assembly
  • 2. Backtrace
  • 3. Alarm
  1. Note
  2. 操作系统
  3. MIT6.S081

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,-16
  1e:	e406                	sd	ra,8(sp) 
  20:	e022                	sd	s0,0(sp)
  22:	0800                	addi	s0,sp,16
  printf("%d %d\n", f(8)+1, 13);							# 编译器直接算出来了,无需调用f和g函数
  24:	4635                	li	a2,13              	# printf参数存入寄存器a2
  26:	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,-96
 6be:	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中获取当前的帧指针,以此为基础,来获取之前的函数返回地址。

随后继续往下看:

  • 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;  // 用于恢复 trapframe
  int 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 trapframe
  if(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)()) {
  // 初始化alarm
  struct 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而放弃,这反倒是我最不想看到的,最重要的是从这个实验中学到了多少,所以,在这里,我将自己学到的分享出去,希望能够帮助更多的人。

参考文献:

上一页MIT6.S081-lab3前置下一页MIT6.S081-lab4前置

最后更新于2天前

These 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.

lecture notes
miigon'blog