我无法理解一些指令,例如
sub
指令,根据手册将其定义为 AddWithCarry
操作,其中进位设置为 1
的硬编码值:
bits(datasize) result;
bits(datasize) operand1 = if n == 31 then SP[]<datasize-1:0> else X[n, datasize];
bits(datasize) operand2;
operand2 = NOT(imm);
(result, -) = AddWithCarry(operand1, operand2, '1');
if d == 31 then
SP[] = ZeroExtend(result, 64);
else
X[d, datasize] = result;
AddWithCarry
操作定义如下:
(bits(N), bits(4)) AddWithCarry(bits(N) x, bits(N) y, bit carry_in)
integer unsigned_sum = UInt(x) + UInt(y) + UInt(carry_in);
integer signed_sum = SInt(x) + SInt(y) + UInt(carry_in);
bits(N) result = unsigned_sum<N-1:0>; // same value as signed_sum<N-1:0>
bit n = result<N-1>;
bit z = if IsZero(result) then '1' else '0';
bit c = if UInt(result) == unsigned_sum then '0' else '1';
bit v = if SInt(result) == signed_sum then '0' else '1';
return (result, n:z:c:v);
是否始终将
1
作为进位传递并根据 AddWithCarry
的定义进行查找,使减法运算也从所有操作中减去 1
?
我知道当我们写这样的东西时:
sub sp, sp, #0x20
我们实际上只从
sp
中减去 32 个字节,那么这个操作中的进位位是怎么回事?
您可能已经看到这样的解释:大多数 ALU 将
a-b
当作 a + (~b + 1)
,因为它们已经有一个二进制加法器,并且按位 NOT 在硬件上非常便宜。 -b = ~b + 1
(补码恒等式)。 CPU 如何进行减法? 就是一个很好的例子,还有 Wikipedia 的 binary Adder–subtractor 文章。
其中一些解释忽略的是
+ 1
部分是通过二进制加法器的进位完成的(如 adc
),因此它仍然只是一个加法,这对于性能和获得有符号的有用标志结果都很重要o溢出和未签名的结转。
这就是这里发生的事情。请注意
operand2 = NOT(imm);
,而不是 NEG
或 -imm
。需要额外的 +1
进位才能得到正确的答案。
在 ARM 和 AARch64 中,减法输出的进位标志是 ALU 加减法器的原始输出,它以这种方式进行减法。这使其成为非借用标志,如果
x - y
,则在 x < y
之后为 false。
其他一些 ISA,尤其是 x86,会反转 ALU 输出的进位标志,因此它是借位标志。这就是为什么 x86 的
adc
减法版本是 sbb
(带借位减法,它还必须在输入上反转 CF,以获得无借位情况下的 1
进位),而 ARM/AArch64 则相反sbc
(带进位减法,直接输入 C 以获得无借位的 1
,否则 0
使输出在有借位时降低 1,通常来自不太重要的块)一个 bigint。)
无论哪种方式,x86
sbb
/ ARM sbc
都是软件将 64 位 ALU 链接到更宽的加法器/减法器中的一种方法,就像行进位加法器中全加法器之间的正常进位传播一样。 (在每个 64 位块内,ALU 可能会做一些有趣的事情,例如进位前瞻或进位选择,以将延迟保持在足够少的门延迟以适应一个时钟周期,但通常不值得尝试在软件中这样做.)
还有相关:
add
指令以天真的方式进行操作,您会得到错误的标志结果。您需要使用一个 adc
。)一开始这也让我很困惑。关键是前面的那行:它不是您所期望的
operand2 = -imm
,而是 operand2 = NOT(imm)
,即按位非(补码)。在二进制补码算术中,您可以轻松检查NOT(imm) = -imm - 1
。因此,进位设置为 1 可以有效地计算 x + (-imm - 1) + 1
,这确实是 x - imm
。
这是伪代码语言设置方式的产物:它们的整数类型是能够表示任何数字的纯数学整数,并且算术运算符仅在此类类型上定义。但这里的操作数是
bits(n)
类型,只是一个位串,它们仅定义逻辑运算符。所以写 operand2 = -imm
的格式不正确。他们必须说类似 operand2 = (-SInt(imm))<n-1:0>
之类的话,这会更令人困惑。
它也可能反映了简单 ALU 实现加法指令的一种方式。您不需要单独的 ADD、SUB、ADC 等单元,而只需要一个可以进行加法进位的单元。因此ADC会将该单元的进位输入连接到NZCV寄存器的实际C位; ADD 会将其接地。 SUB 将使第一个输入通过反相器,并将进位输入连接到 VDD。 SBC 对反相器执行相同的操作,并将进位输入连接到 C 标志。 (请注意,这会导致减法将 C 标志视为真正的进位,而不是 x86,其中 SUB 反转进位标志的含义,使其表现得像借位。)