(x86) 虚拟机通常如何处理标志?

How do (x86) virtual machines generally handle flags?

对于业余项目,我正在尝试编写一个半可编程的 x86 虚拟机。

我了解格式,所以大部分设计都比较简单,但是在用操作数执行指令后,标志经常会改变。检查每个潜在位的效率非常低,所以我考虑将标志寄存器弹出到 VM 中,对其进行 ANDing,然后设置 VM 的标志寄存器。然而,这仍然是很多的开销。

这有点自以为是,但我是否遗漏了什么?

如果您希望您的模拟器按原样模拟处理器,那么是的,您需要准确地模拟标志。

这意味着清除需要清除的位(用AND),设置需要设置的位(用OR),并在需要时复制/计算位(即Z标志需要测试结果是否为零,进位需要知道你有没有溢出等)

没有办法解决。

这就像解码 R/M mod 字节一样。您无法绕过必须加载该字节、检查模式以确定这是寄存器还是内存访问,并相应地应用它们...

实际上,这意味着您的模拟器将是 "much slower"(除非您使用 3Ghz 现代处理器模拟旧的 10Mhz 处理器,无论如何您有时间执行 300 个指令周期......所以你应该没问题。)

如果您有兴趣,我写了一个 6502 emulator 并用 Apple 2 ROM 对其进行了测试。我不得不添加睡眠以使其不在 100Mhz 或更高频率下 运行...(该处理器最初是 运行ning 1Mhz...)

您似乎是在询问关于模拟 x86,而不是虚拟化它。由于 modern x86 hardware supports virtualization,其中 CPU 运行 的访客代码是本机的,并且仅针对某些特权指令陷入管理程序,这就是术语 "virtualization" 通常的意思。


惰性标志评估是典型的。无需实际计算所有标志,只需保存最后一条设置标志的指令的操作数。然后,如果确实有东西读取标志,请找出标志值需要是什么。

这意味着您实际上不必在每次写入(几乎每条指令)时计算 PF 和 AF,只需在每次读取它们时计算(主要是 PUSHF 或中断,几乎没有任何代码读取 PF (除了 FP 分支,它表示 NaN))。在每个整数指令之后计算 PF 在纯 C 中是昂贵的,因为它需要对结果的低 8 位进行 popcount。 (而且我认为 C 编译器通常无法识别该模式并自己使用 setp,更不用说 pushflahf 来存储多个标志,如果将 x86 模拟器编译为 运行 在 x86 主机上。它们有时会识别人口计数模式并发出 popcnt 指令,但是,当目标主机 CPU 具有该功能时(例如 -march=nehalem)) .

BOCHS 使用此技术,并在这个简短的 pdf 的惰性标志部分中详细描述了实现:How Bochs Works Under the Hood 2nd edition。他们保存结果,以便他们可以导出 ZF、SF 和 PF,以及从高 2 位导出 CF 和 OF,从第 3 位导出 AF。有了这个,他们永远不需要重播一条指令来计算它的标志结果。

有些指令没有写入所有标志(即部分标志更新),并且可能来自像 BSF 这样基于 输入 而不是输出。


进一步阅读

This paper on emulators.com 提供了很多关于如何有效地保存足够的状态来重建标志的细节。它有一个“2.1 Lazy Arthmetic Flags for CPU Emulation”。

其中一位作者是 Darek Mihocka(长期的模拟器作者,现在显然在英特尔工作)。他写了很多关于使非 JIT 仿真器 运行 快速和 CPU 性能一般的东西,其中大部分发布在他的网站上,http://www.emulators.com/. E.g. this article 关于避免分支预测错误模拟器的解释器循环分派到实现每个操作码的函数是非常有趣的。 Darek 也是我之前 link 编辑的那篇关于 BOCHS 内部结构的文章的合著者。

惰性标志评估的 google 命中也可能是相关的:https://silviocesare.wordpress.com/2009/03/08/lazy-eflags-evaluation-and-other-emulator-optimisations/

上次出现类似 x86 标志的模拟时,我的惰性标志答案中的 有一些有趣的东西:例如@Raymond Chen 建议 link 到 Mihocka & Troeger 论文,@amdn 指出 JIT 动态翻译可以产生比解释更快的仿真。