C语言有带符号和无符号类型,如char和int。 我不确定在汇编级别上它是如何实现的,例如,我认为有符号数和无符号数的乘积会产生不同的结果,所以汇编是否同时进行有符号和无符号算术运算,还是只进行其中一种,并且对于不同的情况进行模拟?
C语言有带符号和无符号类型,如char和int。 我不确定在汇编级别上它是如何实现的,例如,我认为有符号数和无符号数的乘积会产生不同的结果,所以汇编是否同时进行有符号和无符号算术运算,还是只进行其中一种,并且对于不同的情况进行模拟?
如果您查看x86的各种乘法指令,仅考虑32位变体并忽略BMI2,则会发现以下内容:
imul r/m32
(32x32->64有符号乘法)imul r32,r/m32
(32x32->32乘法)*imul r32,r/m32,imm
(32x32->32乘法)*mul r/m32
(32x32->64无符号乘法)请注意,只有“扩展”乘法有一个无符号的对应项。 中间带星号的两种形式都是有符号和无符号乘法,因为在您不获取额外的“上部分”的情况下,那是一样的。
“扩展”乘法在C语言中没有直接的等价物,但编译器可以(而且经常会)使用这些形式。
例如,如果您编译以下内容:
uint32_t test(uint32_t a, uint32_t b)
{
return a * b;
}
int32_t test(int32_t a, int32_t b)
{
return a * b;
}
使用GCC或其他相对合理的编译器,您将得到以下结果:
test(unsigned int, unsigned int):
mov eax, edi
imul eax, esi
ret
test(int, int):
mov eax, edi
imul eax, esi
ret
(带有 -O1 的实际 GCC 输出)
因此,在一些操作中,符号并不重要(至少对于在 C 中使用的乘法而言),包括:
x86 对于这些操作没有单独的有符号和无符号版本,因为它们本身就没有区别。
但是,对于某些操作,存在区别,例如:
idiv
vs div
)idiv
vs div
)sar
vs shr
)(但要注意 C 中的有符号右移)但最后一个是特殊情况,x86 没有针对有符号和无符号的单独版本,而是拥有一种操作(cmp
,实际上只是非破坏性的 sub
),可以同时执行两种比较,并且生成多个结果("标志位" 中的多个位受到影响)。稍后使用这些标志位的指令(分支、条件移动、setcc
)可以选择它们关心的标志位。例如,
cmp a, b
jg somewhere
如果a
被"大于符号"签名,就会去somewhere
。
cmp a, b
jb somewhere
如果 a
是 "无符号小于" b
,则会去某个地方。
有关标志和分支,请参见Assembly - JG/JNLE/JL/JNGE after CMP。
这不是有关有符号和无符号乘法相同的正式证明,我只是试图让你了解为什么它们应该是相同的。
考虑4位2补码整数。它们各自的位权从最低有效位到最高有效位依次为:1、2、4和-8。当您将其中两个数字相乘时,您可以将其中一个数字分解成与其位对应的4个部分,例如:
0011 (decompose this one to keep it interesting)
0010
---- *
0010 (from the bit with weight 1)
0100 (from the bit with weight 2, so shifted left 1)
---- +
0110
2 * 3 = 6,所以所有东西都很对。这只是大多数人在学校里学习的常规长乘法,只不过是二进制,这使得它更容易,因为您不必乘以十进制数字,而只需乘以0或1,然后进行移位。
无论如何,现在取一个负数。符号位的权重为-8,因此在某一点上,您将进行部分乘积-8 * something
。乘以8相当于左移3位,因此以前的lsb现在成为msb,所有其他位都为0。现在如果你取反它(毕竟它是-8,而不是8),什么也不会发生。显然,零不变,但是8也不变,在一般情况下,只有msb设置的数字也是如此:
-1000 = ~1000 + 1 = 0111 + 1 = 1000
所以,您所做的事情与无符号情况下最高有效位的权重为8时相同,而不是-8。
uint64_t res = a * b
,但这会导致狭窄的结果,然后扩展。或者你可以写uint64_t res = a * (uint64_t)b
,得到宽的结果,但这时你真的有一个64x64->64的乘法,其中操作数的高位恰好为零(但在某些情况下,编译器可能会使用32x32->64 mul来实现这一点)。虽然不完全相同,但也差不多了。至于汇编是否支持(无)符号算术运算,本答案(和其他答案)的重点是表明它们基本上是相同的东西。 - harolduint64_t
和double
之间的转换是不支持的,但有符号版本是可以的。我想现在想到的就是这些了,除了矢量指令以外,所有东西都有一个有符号和无符号版本或者不需要两者都有。 - haroldrdx
中吗?对于任何实现64x64到128的人来说,这非常重要,否则获取高位字需要进行大量计算。 - Z bosonC
规范只是指定了有符号和无符号类型的行为,但并没有指定如何实现这种行为。如果处理器支持所需操作,则使用这些操作进行实现,否则编译器编写者选择使用可用的有限指令集来实现该行为。这就是为什么大多数编译器编写者选择使用2s补码表示负数的原因,因为在2s补码上进行加法/减法与无符号数相同。在X86上,有不同的指令用于有符号/无符号乘法。 - Mohit Jaincmp
和sub
。我们知道cmp
被视为非破坏性的sub
,所以让我们聚焦于sub
。sub
指令时,例如:sub eax, ebx
eax: 0b0001
ebx: 0b1111
ebx:15(dec) - eax: 1(dec) = 14(dec) = 0b1110 (two's complement)
ebx: -1(dec) - eax: 1(dec) = -2(dec) = 0b1110 (two's complement)
0b1110
。
但一个是正数:14(十进制),另一个是负数:-2(十进制),那么我们的问题来了:CPU如何判断哪个是哪个?
答案是CPU将同时评估两者,来自于:http://x86.renejeschke.de/html/file_module_x86_id_308.html
它会对有符号和无符号整数操作数的结果进行评估,并设置OF和CF标志以分别指示有符号或无符号结果中的溢出。SF标志指示有符号结果的符号。
对于这个特定的例子,当CPU看到结果:0b1110
时,如果将其解释为负数,则它将设置SF标志为1
,因为它是-2(十进制)
。
然后,它取决于接下来的指令是否需要使用SF标志,或者是否简单地忽略它。
0b1111
。通常 0x
表示十六进制。 - Peter Cordes
<stdint.h>
。 - Basile Starynkevitch