一般的调试流程其实很简单:发现问题,稳定复现,确定临界条件,定位问题,修复问题,核查结果。迭代这个过程,形成一个闭环

老实说,OS的实验代码,开箱体验极差,程序跳来跳去,进了Lab4后还要考虑内核态切换,很难靠肉眼完成上述闭环。debug愉悦指数为负。

所以在几周的探索后,我大概总结整理了一些调试经验,主要是如何在当前体系下利用或构建调试工具,改善调试体验。

我们的口号是:没有蛀牙

抛砖引玉。

从0开始:现在我们有什么

现在我们手里有:

  • 一坨新鲜的系统代码。
  • 上学期积累的老练的MIPS 32汇编开发与理论基础,虽然这个系统用的是MIPS 2
  • 对高级语言特别是面向对象语言的美好眷恋
  • 全宇宙最牛逼的编程语言:C,以及全宇宙最牛逼的C tool chain:GCC

除此之外我们还有:

  • 凑活能用的printf
  • 助教为我们封装好的panicassert,她们是海滩上最抢眼的宝贝儿
  • 用户态的一些函数,但说真的它们不太好用
  • 一颗勇敢的心

Level 0:静态调试

也就是所谓的“瞪眼法”。能瞪出来的Bug请直接瞪出来。

瞪眼法的升级版是小黄鸭调试法,进一步升级版是面向室友调试,终极形态是面向男朋友/女朋友/老妈调试

别漏了这一步,有时候瞪是最快的,但这也要求你对OS实验到底要干什么有完全清晰的把控。

记得调VIM配色,记得买珍视明。

Level 1: 断言与防御性编程

assert这个东西可能我们已经在各种check里面见过了。它的实现在include/mm.h中(很奇怪的位置),在我们的代码中是以宏的形式实现的:

#define assert(x) do {if(!(x)) panic("...%s", #x); } while(0)

作者十分老练,这里的trick是用拼接运算将参数x作为一个字符串传入panic,这样输出时我们就可以看到断言内的具体内容。

现在assert的功能就是这样的:如果传入的语句x布尔值为假,就陷入panic(顺带一提,panic其实就是在输出信息后陷入死循环)。熟悉其他语言的断言机制,比如python,的同学应该会感到十分亲切

这个东西有什么用呢?我们可以在代码中合适的位置加入断言,来显示地检查某段运行逻辑是否如我们的预期。

举个例子,page_insert后,插入的页面所对应的物理地址应该等于虚拟地址va所映射的物理地址(这正是这个函数的功能),因此在函数的末尾我们可以添加一句断言:

assert(va2pa(pgdir, va) == page2pa(pp));

当程序运行到这里时,如果没有出现错误,那么这句assert不会产生任何作用,如果出现了各种情况导致这两个物理地址不一致,那么程序就会自陷终止并输出信息。当我们调试时,如果看到了它自陷,我们就能知道这里的代码一定存在逻辑漏洞(或者,大灾难),在它引起更隐晦的错误前将其修复。

当然assert也不是完全没有副作用:它依旧会占用指令运行周期数,这个问题我们之后再讨论。

Level 2:把printf撒的满屋子都是

然后因为忘了删调试语句,喜提评测0/100

Level 3:更好用的输出调试信息:封装一个debug()

printf的问题在于,开关并不方便。很难做到只做少量的修改将所有调试信息全部关闭。因此这里开始我们需要自己造轮子

稍微优雅一些的做法是,声明一个调试宏或一个全局变量,来控制全局的调试开关

宏的写法类似于:

# ifndef __DEBUG__
# define __DEBUG__
# endif ... # ifdef __DEBUG__
printf("SOME DEBUG INFO\n");
# endif

全局变量的写法则是

extern int is_debug_mode = 1;

...

if (is_debug_mode) {
printf("SOME DEBUG INFO\n");
}

宏的一个优势在于,如果我们关掉了调试,整个调试代码块不会被编译,执行的语句更少

直接用变量则当然更可读,更美观,并且可以更轻松地实现调试信息分级

无论哪一种,每次输出的时候敲三行代码太复杂了,因此终极形式是包装一个debug函数


void _debug(char * file, int line, char *fmt, ...){
if (is_debug_mode) printf("SAY SOMETHING I'M GIVING UPON YOU.\n");
} # define debug(...) _debug(__FILE__, __LINE__, __VA__ARGS__)

这样我们就能像使用printf一样使用debug(支持可变参数),同时还能输出更多调试信息,比如这一句在哪个文件的哪一行

然后在init中控制debug的全局开闭就好

Level 3.5 汇编调试:相信聪明的MIPS一定帮我们搞定了

上面的各种方法都是针对C语言程序的,汇编则不太好办。大体上有两个思路:

  1. 为某一部分特定信息的输出单独封装一个函数,并在汇编中JAL跳转链接调用,比如上机时我们写过的output_ov_info(我怎么可能记得它叫什么,反正差不多就这个东西)。这个东西使用场景太杂乱,不展开了
  2. 加断点,断点是好文明。

MIPS为我们提供了BREAK和一系列Trap指令,这里先只讨论Break的使用,Trap是类似的。

BREAK的作用是,抛出一个bp断点异常并使CPU切入内核态处理异常。bp的cause编码是9,为了让我们的小系统可以处理这个异常我们需要仿照课上的ov为其添加一个handler

  1. 修改lib/trap.c,声明外链接extern handle_bp();,然后将其绑定在9号异常上
  2. lib/genex.S中添加handle_bp的处理程序。推荐使用BUILD_HANDLER bp break_handler cli将异常处理移交给一个C语言函数break_handler(struct Trapframe *)完成

然后我们在异常处理中输出各种需要的信息,末尾死循环即可(也就是,panic)。最重要的信息或许是pc。

如果想要更好的体验,添加一个读入字符syscall(参考lab1某次课上),让我们可以向系统输入字符,然后把break的死循环换成等待读入。这么做的时候记得处理中断屏蔽

回过头看C程序的调试,用类似的技巧也可以实现断点调试。如果需要可以再封装一个bp()配合debug()使用。


再回过头看Trap(TEQ、TNE等一系列条件内陷语句),我们可以用类似的方法为其添加异常处理程序,这些指令抛出的异常都是TRAP(13),因此我们只需要把处理程序挂在13号异常上就可以令其运作。

Trap很像MIPS版的assert,当满足某个条件时让内核自陷。因此在汇编代码中善用这些指令也能起到尽早发现并规避错误的作用。


或许我们可以进一步挖掘MIPS的片上调试,但目前我还没有遇到这种粒度的需求。

Level 4

更多奇技淫巧,我自己还在探索与试用,如果体验不错的话之后再更新。

stay tuned

注意事项

  • 自己实现的函数,声明尽量放在单独的头文件中,定义尽量放在单独的源文件中,所有东西尽可能封装,做到可插拔。万一课上把自己实现的某个关键文件替换掉导致CE(目前我还没遇到),沉着冷静处理,比如更换函数声明的位置
  • 少造轮子,多看代码
  • 舒服的才是好的,按自己的使用习惯改造自己的调试工具与调试流程
  • 不要尝试学我的代码风格,我已经没有救了,你还有

BUAA OS实验调试指南:从看懂到看开的更多相关文章

  1. 4张图看懂delphi 10生成ipa和在iPhone虚拟器上调试(教程)

    4张图看懂delphi 10生成ipa和在iPhone虚拟器上调试(教程) (2016-02-01 03:21:06) 转载▼ 标签: delphi ios delphi10 教程 编程 分类: 编程 ...

  2. 新手也能看懂的 SpringBoot 异步编程指南

    本文已经收录自 springboot-guide : https://github.com/Snailclimb/springboot-guide (Spring Boot 核心知识点整理. 基于 S ...

  3. linux内核调试指南

    linux内核调试指南 一些前言 作者前言 知识从哪里来 为什么撰写本文档 为什么需要汇编级调试 ***第一部分:基础知识*** 总纲:内核世界的陷阱 源码阅读的陷阱 代码调试的陷阱 原理理解的陷阱 ...

  4. Linux Kernel - Debug Guide (Linux内核调试指南 )

    http://blog.csdn.net/blizmax6/article/details/6747601 linux内核调试指南 一些前言 作者前言 知识从哪里来 为什么撰写本文档 为什么需要汇编级 ...

  5. 小刻也能看懂的Unraid系统使用手册:基础篇

    小刻也能看懂的Unraid系统使用手册 基础篇 Unraid系统简介 Unraid 的本体其实是 Linux,它主要安装在 NAS 和 All in One 服务器上,经常可以在 Linus 的视频里 ...

  6. PS2鼠标+LCD12864实验(调试未成功)

    此试验我一人调试许久都未成功,但发送ff时,读出来的数据确是对的,一开始让我窃喜,但发送f4时,读出来的数据确是错的,哎让苦恼啊,能力有限,只能先暂时就这样吧,那位什么还要贴出来呢,有两个原因: 1. ...

  7. 看懂SqlServer查询计划【转】

    原文链接:http://www.cnblogs.com/fish-li/archive/2011/06/06/2073626.html 开始 SQL Server 查找记录的方法 SQL Server ...

  8. 【转载】看懂SqlServer查询计划

    看懂SqlServer查询计划 阅读目录 开始 SQL Server 查找记录的方法 SQL Server Join 方式 更具体执行过程 索引统计信息:查询计划的选择依据 优化视图查询 推荐阅读-M ...

  9. 看懂SqlServer查询计划

    看懂SqlServer查询计划 阅读目录 开始 SQL Server 查找记录的方法 SQL Server Join 方式 更具体执行过程 索引统计信息:查询计划的选择依据 优化视图查询 推荐阅读-M ...

随机推荐

  1. MySQL在线DDL工具 gh-ost

    一.简介 gh-ost基于 golang 语言,是 github 开源的一个 DDL 工具,是 GitHub's Online Schema Transmogrifier/Transfigurator ...

  2. Windows + Jenkins + .NetFramework + SVN 持续部署

    Windows + Jenkins + .NetFramework + SVN 持续部署 环境准备 服务端环境 安装 Windows 服务器 1.阿里云购买临时服务器 阿里云:https://www. ...

  3. 你说,怎么把Bean塞到Spring容器?

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 小傅哥,你是怎么学习的? 有很多初学编程或者码了几年CRUD砖的小伙伴问我,该怎么学 ...

  4. Android学习之简易版的新闻应用

    •准备工作 新建一个项目,命名为 FragmentBestProject,并选择 Empty Activity: 并将项目的模式结构改为 Project 模式: •进入主题 首先,准备好一个新闻实体类 ...

  5. 201871030116-李小龙 实验三 结对项目—《D{0-1}KP 实例数据集算法实验平台》项目报告

    项目 内容 课程班级博客链接 https://edu.cnblogs.com/campus/xbsf/2018CST 这个作业要求链接 https://www.cnblogs.com/nwnu-dai ...

  6. C#中SQLite的使用及工具类

    目录 SQLite简介 存储类 亲和类型 引用System.Data.SQLite.dll 软件包分类 使用本机库预加载 常用部署包 工具类 参考资料 SQLite简介 SQLite是一款轻型的数据库 ...

  7. UnitTwoSummary

    目录 一.设计策略与程序分析 第一次作业 第二次作业 第三次作业 二.可扩展性检查与分析 三.bug 四.总结与反思 一.设计策略与程序分析 第一次作业 设计思路 输入,调度器,电梯分别设置成三个线程 ...

  8. Dynamic Programming 动态规划入门笔记

    算法导论笔记 programming 指的是一种表格法,并非编写计算机程序 动态规划与分治方法相似,都是通过组合子问题的解来求解原问题.但是分治法将问题划分为互不相交的子问题.而动态规划是应用与子问题 ...

  9. 这一次,彻底搞懂 Go Cond

    hi,大家好,我是 haohongfan. 本篇文章会从源码角度去深入剖析下 sync.Cond.Go 日常开发中 sync.Cond 可能是我们用的较少的控制并发的手段,因为大部分场景下都被 Cha ...

  10. 使用yamllint 检查yaml语法

    安装node 之后npm install -g yaml-lint 使用方法 yamllint confluence.yaml