前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一个在关键路径上面隐藏了11个月的BUG:DragonOS进程切换查错

一个在关键路径上面隐藏了11个月的BUG:DragonOS进程切换查错

作者头像
灯珑LoGin
发布2023-10-18 10:46:53
1660
发布2023-10-18 10:46:53
举报
文章被收录于专栏:龙进的专栏

前言的前面

DragonOS是一个从0开始研发内核及用户态环境的,独立自主的,面向服务器领域的开源操作系统,提供Linux兼容性。

官网:https://DragonOS.org

代码仓库:https://github.com/fslongjin/DragonOS

前言

在写DragonOS的时候,我总是遇到一些神奇的BUG,包括但不限于:

  • 加一行printk(“”);,代码就能正常运行
  • 读写几个无关的变量,代码就能跑了
  • 加一层函数调用,把某个函数wrap一下,代码就能运行
  • 10月份的时候,我和同学调试IDR的源代码,有个单元测试用例就是无法通过。并且,出错的位置总是不相同。将测试用例的数据规模减小之后,就不会报错。
  • XHCI驱动程序在初始化的时候,随机性报错,系统重启后即有概率正常初始化。

上面这些bug,每次碰到,都摸不着头脑,觉得真的是个玄学问题,一直不知道怎么解决他们,根本找不到方向。直到最近,在使用Rust重构CFS调度器的时候,突然间意识到了,上面这些现象,都是来自于进程切换的代码,产生了错误。

先说结论,BUG的产生来自两个方面:

  • 未定义行为的内联汇编代码
  • 切换进程前,存在未完全保存执行现场的调用路径。(也就是说,有时候保存了,有时候没有保存)

我是怎么发现这个bug的?

首先,我使用Rust重构了CFS调度器,这个逻辑不复杂,很快就实现了。

由于原先的C语言版本的代码,调用了这两个宏来进行进程切换:switch_mm()和switch_proc(),分别用来切换页表以及进程上下文。

参见DragonOS-0.1.2的cfs.c的第84、86行:

http://opengrok.ringotek.cn/xref/DragonOS-0.1.2/kernel/src/sched/cfs.c#84

这两个宏主要是汇编代码,长下面这样:

http://opengrok.ringotek.cn/xref/DragonOS-0.1.2/kernel/src/process/process.h?fi=process_switch_mm#process_switch_mm

http://opengrok.ringotek.cn/xref/DragonOS-0.1.2/kernel/src/process/process.h?fi=switch_proc#switch_proc

简单介绍一下这两个宏的作用:

  • process_switch_mm这个宏,主要作用是,将下一个进程的基地址加载到页表基址寄存器CR3中。
  • switch_proc这个宏,首先保存了rbp寄存器(当前栈帧基址)和rsp寄存器(当前栈指针),把他们保存到当前进程的线程结构体中。然后切换到下一个进程的内核栈,同时获取为当前进程的设置一个返回地址(就是switch_proc_ret_addr所在的地址),存到当前进程的线程结构体内的rip成员变量中。并且,往下一个进程的内核栈内,压入下一个进程的返回地址(next->thread->rip),接着,跳转到__switch_to这个函数(注意不是call,而是jmp,因此这里是不会压栈的),进行其他的工作,当__switch_to函数返回时,处理器将会弹出63行压入的“下一个进程的RIP”,这样就完成了进程切换。

后面的实验证明,错误具有两处,其中一处正是发生在switch_proc宏的内联汇编代码之中。

回到重构CFS的话题,我想在Rust代码中,实现切换进程的动作。由于内联汇编的编写有点麻烦,那么最简单、最直接的办法,自然是在C里面加一个函数,把switch_proc和switch_mm这两个宏封装一下,接着直接在Rust里面调用这个C函数即可。

因此,我把这两个宏封装了一下,封装成这样:

http://opengrok.ringotek.cn/xref/DragonOS/kernel/src/sched/core.c?r=d4f3de93#9

注意,我为了避免歧义,在这里把原本的switch_proc()宏,改名为switch_to().在下文中,将用switch_to来代指前文的switch_proc宏。

然后,再在Rust的代码之中,调用了这个函数。本来我以为这样就万事大吉了,但是一运行,处理器就在进程调度的时候,产生了General Protection异常,并且出错的地方,是位于__switch_to函数的ret指令处。(switch to函数里面切换了fs、gs寄存器)

指向这个异常产生的原因有很多,查询Intel开发手册的Volume3A的第6.15章节中,关于General Protection的产生的原因的描述后,大概是这样:

由于文档中,大量的描述是关于那几个段选寄存器的,并且__switch_to函数里面切换了fs、gs寄存器,因此我对进程切换前后,cs、ds、es、fs、gs、ss几个段选寄存器的值,以及将要被换入的值,进行了详细的检查。发现他们的值都是正确的,权限也都是正确的。

Debug陷入了僵局。

解决BUG

我反复思考:为什么这两个宏单独使用就可以运行,独立成函数就不行了呢?是不是因为由于编译器指令重排序优化问题,或者是处理器乱序执行问题导致的?我加了内存屏障,依然无法解决。

BUG的原因之一:未完全保存指执行现场的上下文

在这个时候,我检查发现:在中断结束时调用的sched(),由于进入中断的时候,保存了上下文。除了这种情况以外,其他时候,直接调用sched(),我们并没有对进程当前的执行现场作保存!在这个时候,我联想到之前那些奇怪的BUG,就是文章开头所说的那些。我把他们结合起来思考,突然顿悟:那些玄学的bug的产生,正是因为发生进程调度,而执行现场没有被保存,在进程被重新调度时,由于执行现场的数据缺失,导致其报错!随机性出错的现象,正是因为调度时机不确定导致的!

因此,我对这个问题提出了解决方案:调度器必须在中断上下文中运行,以保证执行现场被完整保存。为了支持那些需要立即调度的场景(与时钟中断触发的调度相对应),我为DragonOS新增了一个系统调用:sys_sched().而原先的sched()函数,功能则改为“发起一个SYS_SCHED系统调用”。这个系统调用就是利用了进入系统调用之前,会由中断处理机制先把执行现场保存了的特点,从而解决了进程的执行现场没有被保存的问题。

具体的代码如图所示:

http://opengrok.ringotek.cn/xref/DragonOS/kernel/src/arch/x86_64/sched.rs?r=d4f3de93#6

http://opengrok.ringotek.cn/xref/DragonOS/kernel/src/sched/core.rs?r=d4f3de93#78

经过上面的修改,所有的能够运行进程调度器,切换进程的路径,都保存了进程的上下文。我想着,这样问题应该就解决了吧?结果一运行,仍然是报错的,还是那个熟悉的General Protection异常。

这个时候,我重新审视了一下上面的代码,经过一个小时的思考,我确认我上面找的确实就是一个BUG,仍然报错肯定是因为还有未发现的bug。

BUG的原因之二:switch_to宏的内联汇编,是未定义行为的代码

我重新思考了很久,我坚信问题一定存在于switch_to和__switch_to这两个地方。但是,在进入这两个地方的前后,寄存器值,以及即将换入的值,我都没有发现异常。我盯着switch_to()宏的代码看了很久,发现它就是有点不对劲!

http://opengrok.ringotek.cn/xref/DragonOS-0.1.2/kernel/src/process/process.h?fi=switch_proc#switch_proc

在这串汇编里面,我修改了rax寄存器的值,并且rax不存在于内联汇编的输入、输出部分,也没有在损坏部分声明。GCC编译器并不知道我在这串汇编里面改了rax寄存器!那么,这段代码的行为就是未定义行为,因为编译器可能会利用rax来存一些临时数据,而我这样就破坏了它。因此,直接在损坏部分(下图第70行加上”rax”寄存器,再运行,bug就解决了!

http://opengrok.ringotek.cn/xref/DragonOS/kernel/src/process/process.h?r=d4f3de93#54

后续测试

为了验证是否像我想的那样,IDR中的大数据测试用例无法通过,且随机性assert failed的现象,是由于进程切换时的BUG导致的,我重新运行了IDR的所有测试用例,都直接通过了。

小结

这个BUG前前后后花了我5天时间去调试,如果算上之前调试实时调度器、IDR、XHCI以及其他模块的时候,由于玄学问题花费的时间,那么总耗时可能达到了将近一个月。真的是,未定义行为的代码,以及未保存上下文这个bug,浪费了我、小伙伴的很多时间。

这个bug,经过了codeQL、cppcheck、ControlFlag、腾讯云的代码检查服务的检测,都没法查出来,真的藏的够深的。或许是因为,那些工具都是为检查应用软件而研发的吧。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年12月31日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言的前面
  • 前言
  • 我是怎么发现这个bug的?
  • 解决BUG
    • BUG的原因之一:未完全保存指执行现场的上下文
      • BUG的原因之二:switch_to宏的内联汇编,是未定义行为的代码
      • 后续测试
      • 小结
      相关产品与服务
      云服务器
      云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档