2016年6月27日

在Simics上运行Wind River Simics找到一个内核1-2-3错误!

由雅各布Engblom

jakob-for-wr-jive

我必须承认我喜欢错误和奇怪的和意外的软件行为。它们为本应枯燥的软件开发世界提供了喜剧和戏剧。有些bug是微不足道的,一旦它们被解决了,谈论起来就会很尴尬。有些虫子就像神话中的雪人一样难以捉摸,永远也找不到。有些漏洞给了我们英雄般的故事,在故事中,一个勇敢的程序员通过出色的灵感和纯粹的决心杀死了龙。这就是其中一个错误的故事——我们在Linux内核中发现的一个非常棘手的错误。我们通过使用它找到了它风河系统公司西米奇* (Simics)运行Simics本身。然而,只需要触发和复制错误一个网络,两个分开的机器,至少三个处理器核心(标题中的1-2-3)。这种类型的bug需要真正的并发才能触发,而不仅仅是在单个处理器上进行多任务处理。找到bug需要在用户空间和Linux内核代码中走很长的路。

背景

为了在基于IA的主机上快速运行Intel®架构(IA)目标,Simics使用了Intel®虚拟化技术(Intel®VT-x)。这在Simics中称为VMP,它要求您在主机上安装一个名为VMXMON的Linux内核模块。VMXMON提供使用Intel VT-x所需的内核级硬件访问,以加速用户级Simics进程中代码的执行。

这个bug始于对VMXMON的一行更改。引入这个更改是为了修复之前的一个问题。修复是经过严格审查的,正确的,必要的。在部署前的测试中,我们发现VMXMON的新版本不仅解决了之前的问题,而且绝对可靠。

错误点击

然而,当我们在Simics与另一个模拟器集成的设置中部署新版本的VMXMON时,我们开始看到失败。新的、修改过的VMXMON、与以前相同的主机内核和与以前相同版本的相同外部模拟器的组合——这导致了一个以前从未发生过的错误。在新的VMXMON中有一些东西揭示了一个Bug -考虑到变化的性质,这被认为是非常令人惊讶的。导致这个问题的设置是这样的(但不是完全的,因为我们很快就会发现):

sim1

Simics和外部模拟器通过主机上的文件进行通信。在两个模拟器中使用mmap打开此文件。当我们使用较新的VMXMON时,我们开始看到某些类型的模拟器工作负载经常崩溃。每次的错误都是相同的:主机Linux操作系统在网络文件系统(NFS)驱动程序中的断言上停止。断言声明一个虚拟内存页面同时被认为是干净的和脏的,因此,文件系统的状态从根本上被破坏了,除了断言之外没有其他事情可做。

因此,在Simics和其他模拟器上运行的测试最终导致主机崩溃,这不是期望的行为。这个断言来自NFS,这表明这个设置比我们最初想象的要复杂得多。实际上,该文件存储在NFS文件服务器上,而不是本地。如上所示,当整个设置都是本地设置时,错误实际上并没有发生。我们需要这个设置来触发它,添加一个网络文件服务器:

sim2

内核assert提供了一个堆栈跟踪,显示了检测到不一致状态的位置,并且断言触发。然而,这显然与问题的根本原因很少。真正的问题是找到首先创建不一致状态的代码。显然,这是一个完美的模拟反向调试案例,因为它涉及从检测到错误的点回来,到状态改变的点。能够从错误向后工作是识别和解决复杂错误的巨大优势。

复制错误

为了让错误启动并在Simics上运行以进行分析,我们首先简化了设置。这帮助我们避免在设置中包含相当复杂的外部模拟器。幸运的是,复制这个错误是相当容易的。关键是让Simics和外部程序共享位于NFS服务器上的文件。简化复制设置提供了一些关于bug的必要先决条件的额外见解:

  • NFS服务器必须位于远程机器上。
  • 共享文件必须发生更改,同时还必须有同步请求,以确保更改被写入磁盘并与其他进程通信。
  • 主机Linux必须是某个版本。事实证明,这个bug在最近的Linux内核版本中并没有触发,但是在SUSE Linux Enterprise Server 11* (SUSE 11)上运行时(SUSE 11是基于2.6系列Linux内核的),就像在测试服务器上使用的那样。

此设置可靠地复制了与原始错误相同的内核堆栈跟踪。在相同的条件下触发相同的断言,因此我们将其引入Simics进行调试。Simics设置如下所示,在一个Simics会话中,网络上有两台机器。请注意,在Simics中有一个正在运行的Simics,以及强制出现错误所需的测试程序。还要注意,NFS服务器运行在第二台机器上。

sim3

当在Simics中重新运行设置时,我们发现还需要另一个重要因素来触发错误:运行Simics和外部模拟器的机器必须至少有两个处理器核。如果所涉及的软件线程之间没有真正的并发性,就不会触发bug。这是一个错误的例子,该错误不会触发,并且可以使用串行化单核上多线程程序执行的通用策略进行(反向)调试。这种序列化是大多数反向调试器所做的,因为除非您有一个完整的系统模拟器,否则很难确定地重放并发工作负载的执行。有关反向调试方法的更多信息,请参见http://jakob.engbloms.se/archives/1564

在Simics中触发漏洞很容易,只需要运行几次。在每次运行中,过程是:

  • 在运行开始之前使用Simics检查点。
  • 设置书签以启用反向执行。
  • 运行测试直到内核崩溃——这发生得很快。

分析

此时,附加了Simics调试器以允许对崩溃的系统进行分析。在物理机器上调试它几乎是不可能的,因为内核会因为这个错误而崩溃。如果机器支持连接这样的设备,那么连接到服务器的硬件调试器可能会很有用。然而,即使使用调试端口,这种设置也无法反向调试回问题的根源。

一旦附加了Simics调试器并检查了机器的状态,崩溃所涉及的内核数据就变成了由NFS文件系统和内核维护的内存页的元数据。每个页面有两组标志。Linux内核本身为所有页面维护了一组标志,用于跟踪某个页面是否脏了,或者是否被写回磁盘。此外,在使用NFS时,NFS维护它自己的标志,包括一个单独的NFS“干净/脏”标志。在出现问题的情况下,NFS标志被设置为clean,但内核标志指示一个脏页。当内核检测到这个干净/脏的不一致时触发assert。

要找到这种变化的根本原因,很简单:

  • 在页的NFS标志上设置断点。
  • 反向执行以找到最后一个写入器。

这揭示了Linux内核中线程之间的竞争。

NFS clean标志是从一个内核执行线程设置的,作为完成页的NFS回写的一部分。线程将清除页面上内核的dirty标志,并设置回写标志。一旦回写完成,线程将回写标志设置为NFS clean并清除回写标志。回写完成所花的时间留下了一个打开的窗口,在这个窗口中,不同的线程可以修改元数据并创建不一致。这就是这里发生的事情。

与此同时,在用户线程的ioctl调用的上下文中运行,VMXMON驱动程序将页面标记为dirty,并且这些页面也被锁定了——这导致内核代码采取了不寻常的路径。最后,另一个内核线程正在寻找要回写的脏页。这个线程发现了一个页面,NFS已经将其标记为干净(等待回写完成),但同时VMXMON已将其标记为脏的。找到这样一个页面后,它会触发assert,导致内核崩溃。

如果我们回过头来考虑一下这里发生了什么,我们会发现这个错误是一场涉及三个不同并发线程的竞赛,以及一些非常不幸的时机,再加上mmap和页面锁定的不寻常组合。不幸的是,在软件中也会发生这种情况:在使用中健壮且经过验证的软件组件仍然会由于无法预见或新奇的情况而失败。

SUSE收到了这个问题的通知,并为正在使用的特定的旧版本Linux创建了一个补丁。在较新的Linux内核中,这个错误不会发生。更新的Linux内核代码经历了广泛的修改,这似乎避免了竞争。

结论

如果我们把这个bug作为一个样本来看,它显然不是一个简单的“喜剧救济”bug。相反,这显然是我们正在尝试的一个严重错误,是时候进行一些严肃的调试了。这是一个英勇的任务,在内核的荒地和漫长的网络路径中追踪龙虫。最后,这条龙被发现了,被杀死了。

在这样做的时候,拥有正确的工具是至关重要的,我们很幸运能够利用Simics。Simics的两个特性是不可缺少的:1)允许调试崩溃的操作系统的调试器。2)能够在一个网络和完整的软件栈中重复和逆转包含多台机器、多处理器核的整个系统的执行。

找到并修复这个错误是Simics全系统虚拟平台和工具独特调试能力的一个很好的例子。这也是一个例子,说明模拟实际的硬件并发性以揭示错误是多么重要。如果任务被强制在单个处理器上执行多任务,有些错误就不会触发。

*其他名称和品牌可能被声称为他人的财产。

以前的中国电信开放基础设施挑战和钛云
下一个风河螺旋底盘、物联网和联网汽车