x86 上的有符号和无符号算术实现 [英] signed and unsigned arithmetic implementation on x86

查看:23
本文介绍了x86 上的有符号和无符号算术实现的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

C 语言有 char 和 int 等有符号和无符号类型.我不确定它是如何在程序集级别实现的,因为例如,在我看来,有符号和无符号的乘法会带来不同的结果,所以汇编做两个未签名和签名算术或只有一个,这在某种程度上模拟不同的情况?

解决方案

如果你查看 x86 的各种乘法指令,只查看 32bit 变体而忽略 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 测试(uint32_t a, uint32_t b){返回 a * b;}int32_t 测试(int32_t a,int32_t b){返回 a * b;}

使用 GCC 或其他一些相对合理的编译器,你会得到这样的结果:

test(unsigned int, unsigned int):mov eax, ediimul eax, esi退测试(整数,整数):mov eax, ediimul eax, esi退

(实际 GCC 输出 -O1)

<小时>

所以符号性对于乘法(至少对于您在 C 中使用的那种乘法)和其他一些运算无关紧要,即:

  • 加减法
  • 按位与、或、异或、非
  • 否定
  • 左移
  • 比较平等

x86 不为这些提供单独的签名/未签名版本,因为无论如何都没有区别.

但是对于某些操作来说是有区别的,例如:

  • 除法(idiv vs div)
  • 余数(还有idiv vs div)
  • 右移(sar vs shr​​)(但要注意 C 中的带符号右移)
  • 比较大于/小于

但最后一个是特殊的,x86 也没有单独的有符号和无符号版本,而是有一个操作(cmp,这实际上只是一个非破坏性的 sub) 同时执行这两项操作,并给出多个结果(标志"中的多个位受到影响).稍后实际使用这些标志的指令(分支、条件移动、setcc)然后选择他们关心的标志.例如,

cmp a, bjg 某处

如果 a 是符号大于"b,则将转到 某处.

cmp a, bjb某处

如果 a 是未签名的"b,就会去 某处.

有关标志和分支的更多信息,请参阅汇编 - CMP 之后的 JG/JNLE/JL/JNGE.

<小时>

这不是有符号乘法和无符号乘法相同的正式证明,我只是试图让您深入了解为什么它们应该相同.

考虑 4 位 2 的补码整数.它们各自位的权重是,从 lsb 到 msb,1、2、4 和 -8.当你将这些数字中的两个相乘时,你可以将其中的一个分解成对应于它的位的 4 部分,例如:

0011(分解这个让它有趣)0010---- *0010(来自重量为1的位)0100(来自权重为 2 的位,因此左移 1)---- +0110

2 * 3 = 6 所以一切都检查出来了.这只是大多数人在学校学习的常规长乘法,只有二进制,这使得它更容易,因为您不必乘以十进制数字,您只需乘以 0 或 1,然后进行移位.

无论如何,现在取一个负数.符号位的权重是-8,所以在某一时刻你会做一个部分乘积-8 * something.乘以 8 是左移 3,所以以前的 lsb 现在是 msb,所有其他位都是 0.现在如果你否定它(毕竟它是 -8,而不是 8),什么都不会发生.0 显然没有变化,但 8 也是如此,一般只有 msb 集的数字:

-1000 = ~1000 + 1 = 0111 + 1 = 1000

因此,如果 msb 的权重为 8(如在未签名的情况下)而不是 -8,那么您已经做了同样的事情.

C language has signed and unsigned types like char and int. I am not sure, how it is implemented on assembly level, for example it seems to me that multiplication of signed and unsigned would bring different results, so do assembly do both unsigned and signed arithmetic or only one and this is in some way emulated for the different case?

解决方案

If you look at the various multiplication instructions of x86, looking only at 32bit variants and ignoring BMI2, you will find these:

  • imul r/m32 (32x32->64 signed multiply)
  • imul r32, r/m32 (32x32->32 multiply) *
  • imul r32, r/m32, imm (32x32->32 multiply) *
  • mul r/m32 (32x32->64 unsigned multiply)

Notice that only the "widening" multiply has an unsigned counterpart. The two forms in the middle, marked with an asterisk, are both signed and unsigned multiplication, because for the case where you don't get that extra "upper part", that's the same thing.

The "widening" multiplications have no direct equivalent in C, but compilers can (and often do) use those forms anyway.

For example, if you compile this:

uint32_t test(uint32_t a, uint32_t b)
{
    return a * b;
}

int32_t test(int32_t a, int32_t b)
{
    return a * b;
}

With GCC or some other relatively reasonable compiler, you'd get something like this:

test(unsigned int, unsigned int):
    mov eax, edi
    imul    eax, esi
    ret
test(int, int):
    mov eax, edi
    imul    eax, esi
    ret

(actual GCC output with -O1)


So signedness doesn't matter for multiplication (at least not for the kind of multiplication you use in C) and for some other operations, namely:

  • addition and subtraction
  • bitwise AND, OR, XOR, NOT
  • negation
  • left shift
  • comparing for equality

x86 doesn't offer separate signed/unsigned versions for those, because there's no difference anyway.

But for some operations there is a difference, for example:

  • division (idiv vs div)
  • remainder (also idiv vs div)
  • right shift (sar vs shr) (but beware of signed right shift in C)
  • comparing for bigger than / smaller than

But that last one is special, x86 doesn't have separate versions for signed and unsigned of this either, instead it has one operation (cmp, which is really just a nondestructive sub) that does both at once, and gives several results (multiple bits in "the flags" are affected). Later instructions that actually use those flags (branches, conditional moves, setcc) then choose which flags they care about. So for example,

cmp a, b
jg somewhere

Will go somewhere if a is "signed greater than" b.

cmp a, b
jb somewhere

Would go somewhere if a is "unsigned below" b.

See Assembly - JG/JNLE/JL/JNGE after CMP for more about the flags and branches.


This won't be a formal proof that signed and unsigned multiplication are the same, I'll just try to give you insight into why they should be the same.

Consider 4-bit 2's-complement integers. The weights their individual bits are, from lsb to msb, 1, 2, 4, and -8. When you multiply two of those numbers, you can decompose one of them into 4 parts corresponding to its bits, for example:

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 so everything checks out. That's just regular long multiplication that most people learn in school, only binary, which makes it a lot easier since you don't have to multiply by a decimal digit, you only have to multiply by 0 or 1, and shift.

Anyway, now take a negative number. The weight of the sign bit is -8, so at one point you will make a partial product -8 * something. A multiplication by 8 is shifting left by 3, so the former lsb is now the msb, and all other bits are 0. Now if you negate that (it was -8 after all, not 8), nothing happens. Zero is obviously unchanged, but so is 8, and in general the number with only the msb set:

-1000 = ~1000 + 1 = 0111 + 1 = 1000

So you've done the same thing you would have done if the weight of the msb was 8 (as in the unsigned case) instead of -8.

这篇关于x86 上的有符号和无符号算术实现的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆