如何计算 x86 程序集中的辅助标志状态

How to calculate the Auxiliary Flag status in x86 Assembly

x86 Assembly 中的 Auxiliary Flag 是如何计算的?

我能找到的大多数 解释说,如果第 3 位到第 4 位有进位,则辅助标志设置为“1”。

Wiki :

It indicates when a carry or borrow has been generated out of the least significant four bits of the accumulator register following the execution of an arithmetic instruction.

示例:

mov al,-14 *(1111 0010)
mov bl,-130 (0111 1110)
sub al,bl (1111 0010 – 0111 1110)

* 括号显示存储的二进制模式。

结果:1111 0010 – 0111 1110 将使用二进制补码计算为 1111 0010 + 1000 0010,得到结果 0111 0100 + OF

在给定的示例中设置了 AF (=1)。我不明白这是为什么,因为我看不到从第 3 位到第 4 位有进位。添加 0010 + 0010,最低有效半字节,等于 0100,没有进位。累加器寄存器的低四位,由0010变为0100('2'变为'4'),低半字节到高半字节没有进位?

谁能解释一下我的想法哪里出了问题?

我怀疑 'negatives' 的丰富性在某些时候让我失望了,因为我在调试器中尝试了几个不同的例子,它们都按照我的期望行事,除了这个例子.

x86 CPUs 上的 sub instruction 是自第一个芯片 8086 以来的 "real" 指令,即它不是某种汇编程序的便利,它被翻译为否定 + 添加,但是它有自己的二进制操作码,CPU 本身会知道它应该产生减法结果。

该指令有 Intel 的定义,它如何影响标志,并且在这种情况下修改了标志 "as if" 计算真正的减法。当您专注于编程算法或审查某些代码的正确性时,这就是您需要知道的全部内容。芯片本身是否 实现 它作为附加,并且有一些额外的晶体管将标志转换为 "subtraction" 变体,是 "implementation detail",只要你想知道只是结果,不重要

当您调整特定代码的性能时,实现细节变得很重要,然后考虑芯片的内部架构和特定操作码的实现可能会让您想到如何重写特定代码 unintuitive/non-human方式,通常甚至比 "naive" 版本有更多的指令,但由于更好地利用芯片的内部实现,性能会更好。

但是结果定义明确,不能通过某些实现细节改变,那就是 "bug in CPU",就像第一个奔腾芯片确实计算了某些除法的错误结果。

也就是说,汇编指令的定义已经像其他语言一样泄露了实现细节,因为汇编指令在设计时处于路径 "what is simple to create in HW transistors" 和路径 "what makes some programming sense" 的中间,而其他高级编程语言更偏向 "what makes sense",只是不情愿地从 HW 实现中强加了一些繁琐的限制,例如变量类型的特定位大小的值范围。

所以对实现以及为什么某些东西按原样定义感到好奇(例如为什么 dec xxx 不更新 CF 标志,而否则它只是 sub xxx,1)通常会给出您对如何在汇编中更有效地编写某些任务、芯片如何进化以及哪些任务比其他任务更容易计算有了新的见解。

但首先是基础知识。 sub 指令像计算减法一样更新标志,并且 sub 指令不知道它正在处理的值的任何上下文,它得到的只是值的二进制模式,在你的案例:1111_0010 – 0111_1110 这是在带符号的 8 位数学“-14 - +126”中解释的(-130 不适合 8 位,因此它被截断为 +126,好的汇编程序将发出 warning/error那里),或者当用无符号 8b 数学“242 - 126”解释时。在有符号数学的情况下,结果应该是 -140,它被截断(发生溢出,OF=1)到 8b 值 +116,在无符号数学的情况下,结果是 +116,没有无符号溢出(carry/borrow CF= 0).

减法本身是按位定义的,即

         1111_0010
       – 0111_1110
       ___________
result:  0111_0100
borrow:  0111_1100
              ^ this borrow goes to AF
         ^ the last borrow goes to CF
         ^ the last result bit goes to SF
  All zero result bits sets ZF=1
  PF is calculated from only low 8 bits of result (even with 32b registers!)
  where PF=1 means there was even number of set bits, like here 4.

你可以从右到左,逐位减法,即0-0=0、1-1=0、0-1=1+b、0-2=0+b等..(其中 +b 表示需要 "borrow",即第一个操作数借用 +2(下一位为 +1)以使结果有效位值为 +0 或 +1)

顺便说一句,OF 究竟是如何在位级别上设置的有点棘手,SO 上有一些很好的 Q+A,你可以搜索它,但是从数学的角度来看,如果结果是 "truncated" 在带符号的解释中(如本例所示),然后设置 OF。这就是它的定义方式(并且实现符合该定义)。

如您所见,所有标志都按定义设置,sub 甚至不知道第一个参数是 -14 还是 +242,因为那不是'如果不更改位级别的任何内容,该指令只会从另一个位模式中减去一个位模式,并按照定义、完成设置所有标志。位模式代表什么,以及如何解释标志结果,这取决于以下指令(代码逻辑),但与 sub 本身无关。

仍然有可能通过 CPU 内部的加法实现减法(虽然不太可能,但实现减法并不困难),需要更多额外的标志处理来修复标志,但这取决于特定的芯片实现。

请注意,现代 x86 是一个相当复杂的野兽,首先将经典 x86 指令转换为微代码操作,在可能的情况下对它们重新排序以避免停顿(比如等待内存芯片的值),有时执行多个微指令并行操作(一次最多 3 个操作 IIRC),并使用动态 renamed/mapped 到原件的一百多个物理寄存器(如代码中的 al, bl ),即如果你将复制这 3 行asm 本身两次,现代 x86 CPU 实际上可能会与两个不同的物理 "al" 寄存器并行执行它,然后下一个代码要求 "al" 中的结果将从后面一个,第一个明显被第二个给舍弃了sub。但是所有这些都被定义+创建以产生可观察的结果 "as if the classic 8086 did sequentially run each instruction separately over real single physical AL register",至少在单核意义上(在 multi-core/thread 设置中有额外的指令允许程序员 serialize/finalize 结果某些代码点,然后另一个 core/thread 可能会检查它们以一致的方式查看它们)。

所以只要你只是学习 x86 汇编基础知识,你真的不需要知道现代 x86 中有一些微体系结构 CPU 将你的机器代码翻译成不同的代码(这不是直接的可供程序员使用,因此没有 "modern x86 micro-assembly" 可以直接编写这些微操作的地方,您只能生成常规的 x86 机器代码并让 CPU 自行处理内部实现。