【问题标题】:Reading CF, PF, ZF, SF, OF读取 CF、PF、ZF、SF、OF
【发布时间】:2016-03-26 05:33:14
【问题描述】:

我正在为我自己的汇编语言编写一个虚拟机,当我执行加法等操作时,我希望能够像在 x86-64 架构中设置的那样设置进位、奇偶校验、零、符号和溢出标志.

注意事项:

  • 我正在使用 Microsoft Visual C++ 2015 和 Intel C++ Compiler 16.0
  • 我正在编译为 Win64 应用程序。
  • 我的虚拟机(当前)只对 8 位整数进行算术运算
  • 我(目前)对任何其他标志(例如 AF)不感兴趣

我目前的解决方案是使用以下函数:

void update_flags(uint16_t input)
{
    Registers::flags.carry = (input > UINT8_MAX);
    Registers::flags.zero = (input == 0);
    Registers::flags.sign = (input < 0);
    Registers::flags.overflow = (int16_t(input) > INT8_MAX || int16_t(input) < INT8_MIN);

    // I am assuming that overflow is handled by trunctation
    uint8_t input8 = uint8_t(input);
    // The parity flag
    int ones = 0;
    for (int i = 0; i < 8; ++i)
        if (input8 & (1 << i) != 0) ++ones;

    Registers::flags.parity = (ones % 2 == 0);
}

另外,我会使用如下:

uint8_t a, b;
update_flags(uint16_t(a) + uint16_t(b));
uint8_t c = a + b;

编辑: 为了澄清,我想知道是否有更有效/更简洁的方法(例如通过直接访问 RFLAGS) 此外,我的代码可能不适用于其他操作(例如乘法)

EDIT 2我现在已将我的代码更新为:

void update_flags(uint32_t result)
{
    Registers::flags.carry = (result > UINT8_MAX);
    Registers::flags.zero = (result == 0);
    Registers::flags.sign = (int32_t(result) < 0);
    Registers::flags.overflow = (int32_t(result) > INT8_MAX || int32_t(result) < INT8_MIN);
    Registers::flags.parity = (_mm_popcnt_u32(uint8_t(result)) % 2 == 0);
}

还有一个问题,我的进位标志代码能正常工作吗?我还希望为减法期间发生的“借用”正确设置它。

注意:我正在虚拟化的汇编语言是我自己设计的,旨在简单并基于 Intel 的 x86-64(即 Intel64)实现,因此我希望这些标志的行为方式大致相同。

【问题讨论】:

  • 您遇到的具体问题是什么?你的代码不起作用吗?
  • input &lt; 0 永远不会为真,因为input 是无符号的,并且 OF 将取决于操作数,而不仅仅是结果。例如,8 位操作0x7f + 0x02 = 0x81 将产生OF = 1,但0x82 + 0xff = 0x81 将产生OF = 0。这段代码是错误的,所以一些正确设置标志的代码比这种方式更整洁。

标签: c++ x86-64 icc


【解决方案1】:

TL:DR:使用惰性标志评估,见下文。


input 是一个奇怪的名字。大多数 ISA 根据操作的结果而不是输入来更新标志。您正在查看 8 位操作的 16 位结果,这是一种有趣的方法。在 C 中,您应该只使用unsigned int,它保证至少是uint16_t。它将在 x86 上编译成更好的代码,其中unsigned 是 32 位。 16 位操作需要一个额外的前缀,并可能导致部分寄存器速度变慢。

这可能有助于解决您提到的 8bx8b->16b mul 问题,具体取决于您希望如何为正在模拟的架构中的 mul 指令定义标志更新。

我认为您的溢出检测不正确。请参阅链接自 标签 wiki 的 this tutorial,了解它是如何完成的。


这可能不会编译成非常快的代码,尤其是奇偶校验标志。您是否需要您正在模拟/设计的 ISA 具有奇偶校验标志?你从来没有说过你在模拟 x86,所以我认为这是你自己设计的一些玩具架构。

一个高效的模拟器(尤其是需要支持奇偶校验标志的)可能会从某种惰性标志评估中受益匪浅。保存一个值,如果需要,您可以从中计算标志,但在您到达读取标志的指令之前实际上不计算任何东西。大多数指令只写入标志而不读取它们,它们只是将uint16_t 结果保存到您的架构状态中。标志读取指令既可以从保存的uint16_t 中仅计算他们需要的标志,也可以计算所有标志并以某种方式存储。


假设您无法让编译器从结果中实际读取PF,您可以尝试_mm_popcnt_u32((uint8_t)x) &amp; 1。或者,将所有位水平异或:

x  = (x&0b00001111) ^ (x>>4)
x  = (x&0b00000011) ^ (x>>2)
PF = (x&0b00000001) ^ (x>>1)   // tweaking this to produce better asm is probably possible

我怀疑任何主要编译器都可以窥视优化对结果的一堆检查到LAHF + SETO alPUSHF。编译器可以导入using a flag condition to detect integer overflow to implement saturating addition, for example。但是让它弄清楚你想要所有的标志,并实际使用LAHF 而不是一系列setcc 指令,可能是不可能的。编译器需要一个模式识别器才能使用LAHF,并且可能没有人实现它,因为用例非常罕见。

没有 C/C++ 方法可以直接访问操作的标志结果,这使得 C 成为实现此类功能的糟糕选择。 IDK 如果任何其他语言确实有标记结果,除了 asm。

我希望您可以通过在 asm 中编写部分仿真来获得很多性能,但这将是特定于平台的。更重要的是,它的工作量更大。

【讨论】:

  • 谢谢你,我会调查 _mm_popcnt_u32。并且参数“输入”的名称是一个糟糕的选择。有几个后续问题:我的溢出检查有什么问题?我基于英特尔架构手册所说的“溢出标志 - 如果整数结果太大为正数或太小负数(不包括符号位)以适合目标操作数;否则清除。此标志表示有符号整数(二进制补码)算术的溢出条件。”还有你的 XOR'ing 东西,它是如何工作的?
  • 懒惰的评估最终可能会降低效率,而且考虑到我的架构设计,肯定会更加复杂。
  • @Isaac:奇偶校验所有位的异或。 XOR 是无进位相加。我建议的方法类似于 SIMD 水平和,但按位并使用 XOR。
  • 如果您认真编写模拟器,this paper 将介绍 Bochs 和其他高性能模拟器如何处理标志。
  • 比保存结果和执行标志的惰性评估更快的是根本不存储任何东西! - 这就是 JIT 仿真器 (wikiwand.com/en/Just-in-time_compilation) 经常做的事情,他们进行活性(数据流)分析 (wikiwand.com/en/Live_variable_analysis) 以确定可能需要哪些标志,并且只对这些标志进行惰性评估。例如,x86 ADD 后跟 CMP,由 ADD 生成的标志被 CMP 覆盖......所以 ADD 不需要对标志做任何事情。这是 google.com/patents/US4951195 的 28 年专利
【解决方案2】:

我似乎已经解决了这个问题,方法是将更新标志的参数拆分为一个无符号和有符号的结果,如下所示:

void update_flags(int16_t unsigned_result, int16_t signed_result)
{
    Registers::flags.zero = unsigned_result == 0;
    Registers::flags.sign = signed_result < 0;
    Registers::flags.carry = unsigned_result < 0 || unsigned_result > UINT8_MAX;
    Registers::flags.overflow = signed_result < INT8_MIN || signed_result > INT8_MAX
}

对于加法(应该为有符号和无符号输入产生正确的结果),我会执行以下操作:

int8_t a, b;
int16_t signed_result = int16_t(a) + int16_t(b);
int16_t unsigned_result = int16_t(uint8_t(a)) + int16_t(uint8_t(b));
update_flags(unsigned_result, signed_result);
int8_t c = a + b;

有符号乘法我会做以下事情:

int8_t a, b;
int16_t result = int16_t(a) * int16_t(b);
update_flags(result, result);
int8_t c = a * b;

其他更新标志的操作以此类推

注意:我在这里假设 int16_t(a) 符号扩展,int16_t(uint8_t(a)) 零扩展。

我还决定不使用奇偶校验标志,如果我稍后改变主意,我的 _mm_popcnt_u32 解决方案应该会起作用..

附:感谢所有回复的人,非常有帮助。另外,如果有人能在我的代码中发现任何错误,我们将不胜感激。

【讨论】:

  • 这不是您在问题中的第一句话所说的“我希望能够设置进位、奇偶校验、零、符号和溢出标志,因为它们是在 x86-64 架构中设置的,当我执行加法等操作。”
  • @amdn:我发现这个问题也不清楚。鉴于这句话,我不确定 OP 是否正在发明新的 ISA。所以我不确定我完全省略 PF 的建议是否有用(因为在 C 中计算成本很高)。
  • 考虑这个 C 函数:int8_t div( int8_t a, int8_t b, int8_t c ) { int8_t sum = b + c; if (sum == 0) return 0; else return a / sum; } 现在,编译器应该为你的 ISA 作为目标的这个函数生成什么?
  • @PeterCordes:通常溢出可能发生在转换期间(实现定义的行为)或评估期间(未定义的行为)。在这种情况下,我们首先进行了两次转换(将 int8_t 加法操作数提升到 int),显然那里没有溢出。然后执行求和,不会溢出,因为两个 8 位有符号量之和适合 9 位,并且int 保证至少为 16 位。最后一次转换是将 sum(一个 int)的结果分配给 int8_t - 该转换可能会溢出,并且行为是由实现定义的。
  • @amdn:哎呀,我忘记了整数提升到int,即使两个操作数是相同的类型,并且略过了你之前评论的那部分,xD。同意你的结论。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多