C - 区分机器代码中的有符号和无符号



我正在看一本教科书,上面写着:

重要的是要注意机器代码如何区分签名 和无符号值。与 C 不同,它不关联数据类型 与每个程序值。相反,它主要使用相同的 (汇编)指令适用于这两种情况,因为算术很多 操作对于无符号和 二进制补码算术。

我不明白这是什么意思,谁能给我举个例子?

例如,以下代码:

int main() {
int i = -1;
if(i < 9)
i++;
unsigned u = -1;  // Wraps around to UINT_MAX value
if(u < 9)
u++;
}

在 x86 GCC 上给出以下输出:

main:
push    rbp
mov     rbp, rsp
mov     DWORD PTR [rbp-4], -1 ; i = -1
cmp     DWORD PTR [rbp-4], 8  ; i comparison
jg      .L2                   ; i comparison
add     DWORD PTR [rbp-4], 1  ; i addition
.L2:
mov     DWORD PTR [rbp-8], -1 ; u = -1
cmp     DWORD PTR [rbp-8], 8  ; u comparison
ja      .L3                   ; u comparison
add     DWORD PTR [rbp-8], 1  ; u addition
.L3:
mov     eax, 0
pop     rbp
ret

注意它是如何对变量iu使用相同的初始化(mov)和递增(add)指令的。这是因为无符号和 2 补码的位模式变化相同。

比较也使用相同的指令cmp,但跳转决策必须不同,因为设置最高位的值在类型上是不同的:有符号的jg(如果更大则跳),无符号的ja(如果高于跳),

则跳。选择什么指令取决于体系结构和编译器。

在英特尔处理器(x86 家族)和其他具有FLAGS的处理器上,您会在这些FLAGS中获得告诉您上一个操作如何工作的位。FLAGS的名称因处理器而异,但总的来说,您在算术方面有两个重要的名称:CFOF.

CF是进位(在其他处理器上通常称为C)。

OF是溢出位(在其他处理器上通常称为V)。

或多或少,CF表示无符号溢出,OF表示签名溢出。当处理器执行ADD操作时,它有一个额外的位,即CF.因此,如果添加两个 64 位数字,则不换行的结果可能需要 65 位。那就是携带。OF标志设置为最高位(即 64 位数字中的位 63),对两个源和目标中的该位使用 3 个逻辑运算。

有一个CF如何使用 4 位寄存器的示例:

R1 = 1010
R2 = 1101
R3 = R1 + R2 = 1 0111
^
+---- carry (CF)

额外的1不适合 R3,因此它被放入CF位。作为旁注,MIPS处理器没有任何FLAGS。由您决定是否生成进位(您可以在两个源和目的地上使用 XOR 等方式执行此操作)。

但是,在 C(和 C++)中,没有对整数类型的溢出进行验证(至少默认情况下不会)。换句话说,除了四个比较运算符(<<=>>=)之外,所有操作都会忽略CFOF标志。

如 @user694733 提供的示例所示,区别在于是使用jg还是ja。16 条跳跃指令中的每一个都将测试各种标志,以了解是否跳跃。这种组合确实是与众不同的地方。

另一个有趣的方面是ADCADD之间的区别 .在一种情况下,您添加进位,而另一种情况则不添加。现在我们有 64 位计算机,它可能没有那么多使用,但是要使用 32 位处理器添加两个 64 位数字,它会将较低的 32 位添加为无符号 32 位数字,然后添加较高的32 位数字(可能是有符号或无符号)加上第一个操作的进位。

假设您在 32 位寄存器(ECX:EAXEDX:EBX)中有两个 64 位数字,您可以像这样添加它们:

ADD EAX, EBX
ADC ECX, EDX

在这里,如果EAX + EBX有无符号溢出,EDX和进位将添加到ECX(进位 - 这意味着正确添加EAXEBX现在应该用 33 位表示,因为结果不适合 32 位,CF标志是第 33 位)。

需要注意的是,英特尔处理器具有:

  • 零位:ZF(无论结果是否为零,)
  • CF在减去(对于SBCSBB、)和
  • 也是用于"十进制数运算">AF位(没有一个头脑正常的人使用)。该AF位告诉您小数操作中存在溢出。类似的东西。我从来没有用过那个。我发现它们的使用太复杂/繁琐了。此外,该位在 amd64 中位于门槛,但设置它的说明已被删除(例如,请参阅DAA)。

二进制补码的美妙之处在于加法(结果减法,因为它使用了加法器,这又是二元补码之美的一部分)。 添加操作本身不关心有符号与无符号,相同的位模式加在一起会产生相同的结果0xFE + 0x01 = 0xFF,-2 + 1 = 1 也是 126 + 1 = 127。 相同的输入位相同的结果模式。

二人补充仅对百分比有帮助。 不是全部。 加/减,但不一定乘除。 按位当然位是位。 但是(右)转变希望有所不同,但 C 能提供吗?

比较非常敏感。 相等和不相等,零和不零这些是单标志测试,将起作用。 但是,无符号小于和签名小于不是使用/测试的同一组标志。 小于和大于(带或不带相等)与无符号的工作方式不同。 同样,有符号溢出和无符号溢出(通常称为进位)的计算方式也不同。 当操作数为减法时,某些指令集进位是反转的,但并非总是如此,因此为了进行比较,您需要知道它是减法的借用位还是总是只是执行未修改。

乘法和可能的除法是"视情况而定"。 N 位乘以 N 位等于 N 位结果有符号和无符号都可以工作,但 N 位乘以 N 位等于 2*Nbit(唯一真正有用的硬件乘法)需要有符号和无符号版本才能让硬件/指令完成所有工作,否则如果您没有两种风格,则必须将操作数分解为多个部分。 一个简单的纸笔小学将说明原因,留给读者弄清楚。

您根本不需要我们,您可以轻松提供自己的示例,并从编译器输出中查看何时存在差异,何时没有差异。

int32_t fun0 ( int32_t a, int32_t b ) { return a+b; }
int32_t fun1 ( int32_t a, int32_t b ) { return a*b; }
int32_t fun2 ( int32_t a, int32_t b ) { return a^b; }
uint32_t fun3 ( uint32_t a, uint32_t b ) { return a+b; }
uint32_t fun4 ( uint32_t a, uint32_t b ) { return a*b; }
uint32_t fun5 ( uint32_t a, uint32_t b ) { return a^b; }
uint32_t fun6 ( uint64_t a, uint64_t b ) { return a+b; }
uint32_t fun7 ( uint64_t a, uint64_t b ) { return a*b; }
uint32_t fun8 ( uint64_t a, uint64_t b ) { return a^b; }
uint64_t fun9 ( uint64_t a, uint64_t b ) { return a*b; }
int64_t fun10 ( int64_t a, int64_t b ) { return a*b; }
uint64_t fun11 ( uint32_t a, uint32_t b ) { return a*b; }
int64_t fun12 ( int32_t a, int32_t b ) { return a*b; }
int32_t comp0 ( int32_t a, int32_t b ) { return a<b; }
uint32_t comp1 ( uint32_t a, uint32_t b ) { return a<b; }

加上其他运算符和组合。

编辑

好的,真正的答案...而不是让你做工作。

我想添加 -2 和 +1

11111110
+ 00000001
============

完成它

00000000
11111110
+ 00000001
============
11111111

-2 + 1 = -1

那么127+1呢

00000000
11111110
+ 00000001
============
11111111

嗯......相同的位在相同的位中,但是作为程序员,我如何解释这些位差异很大。

您可以根据需要尝试任意数量的合法值(不会溢出结果的值),您将看到加法结果不知道也不关心已签名的操作者与未签名的操作者。 部分美的二人相辅相成。

减法只是逻辑上的加法,有些人可能已经学会了"反转加一"想知道你反转 00000000 11111111 1 00000001所以11111111是 -1 的位模式是什么。 但是加法如何真正与两个操作数一起工作,如上所示,你真的需要一个三位加法器三个位输入和两个位输出结果并执行,所以有一个进位,两个操作数位一个结果和执行。 如果我们也回到小学怎么办...

-32 - 3 = (-32) + (-3)应用反转并将 1 加到 -3,我们得到 (-32) + (~3) + 1

1
11100000
+   11111100
==============

这就是计算机进行数学运算的方式,反转进位和第二个操作数。 SOME 反转执行,因为当加法器用作减法器时,执行时为 1 表示没有借用,但 0 表示发生了借用。 所以有些指令集会反转执行,有些不会。 这对于本主题非常重要。

同样,执行位是根据操作数的 msbit 的添加和进位到该位置计算的,它是该添加的执行。

abcxxxxxx
dxxxxxxx
+ exxxxxxx
============
f 

a 执行是添加位 B+D+E 时的执行。 当这是加法运算并且操作数被视为无符号值时,这也称为无符号溢出标志。 但是有符号的溢出标志是由 b 和 a 相等还是不相等决定的。

这在什么情况下发生。

bde af
000 00
001 01
010 01
011 10 <--
100 01 <--
101 10
110 10
111 11

所以你可以读到,进位不等于对MSBIT执行有一个签名的溢出。 同时,您可以说如果操作数的msbit相等并且结果的msbit不等于那些操作数位,则有符号溢出为真。 如果您生成一个有符号数字表及其结果以及哪个溢出将开始变得清晰,则不必执行 8 位 x 8 位 256 * 256 组合,采用 3 位或 4 位数字合成您自己的加法例程,或 3 或 4 位和较少数量的组合就足够了。

因此,虽然加法和减法本身就结果位而言不知道无符号标志,如果您有使用它们的处理器 C 或携带标志,V 或溢出标志具有基于签名的用例。 根据指令集的不同,当由减法生成时,进位标志本身可以有两个定义,并且由于比较通常是用减法完成的,因此进位定义对如何使用标志很重要。

大于或小于时使用减法来确定它们的使用方式,并且结果本身不受符号性的影响,标志的解释方式非常大。

取一些四位正数。

1101 - 1100 (13 - 12)
1100 - 1100 (12 - 12)
1011 - 1100 (11 - 12)
11111
1101
+ 0011
=======
0001
carry out 1, zero flag 0, v = 0, n = 0
11111
1100
+ 0011
========
0000
carry out 1, zero flag 1, v = 0, n = 0
00111
1011
+ 0011
========
1111
carry out 0, zero flag 0, v = 0, n = 1

(n 是结果的 MSBIT,符号位 1 表示有符号负数,零表示有符号正数)

cz
10 greater than but not equal
11 equal
00 less than but not equal

相同的位模式

1101 - 1100 (-3 - -4)
1100 - 1100 (-4 - -4)
1011 - 1100 (-5 - -4)
cz
10 greater than but not equal
11 equal
00 less than but not equal

到目前为止,没有任何变化。

但是如果我检查所有组合

#include <stdio.h>
int main ( void )
{
unsigned int ra;
unsigned int rb;
unsigned int rc;
unsigned int rx;
unsigned int v;
unsigned int n;
int sa,sb;
for(ra=0;ra<0x10;ra++)
for(rb=0;rb<0x10;rb++)
{
for(rx=8;rx;rx>>=1) if(rx&ra) printf("1"); else printf("0");
printf(" - ");
for(rx=8;rx;rx>>=1) if(rx&rb) printf("1"); else printf("0");
rc=ra-rb;
printf(" = ");
for(rx=8;rx;rx>>=1) if(rx&rb) printf("1"); else printf("0");
printf(" c=%u",(rc>>4)&1);
printf(" n=%u",(rc>>3)&1);
n=(rc>>3)&1;
if((rc&0xF)==0) printf(" z=1"); else printf(" z=0");
v=0;
if((ra&8)==(rb&8))
{
if((ra&8)==(rc&8)) v=1;
}
printf(" v=%u",v);
printf(" (%2u - %2u)",ra,rb);
sa=ra;
if(sa&8) sa|=0xFFFFFFF0;
sb=rb;
if(sb&8) sb|=0xFFFFFFF0;
printf(" (%+2d - %+2d)",sa,sb);
if(rc&0x10) printf(" C ");
if(n==v) printf(" NV ");
printf("n");
}
}

您可以在输出中找到显示问题的片段。

0000 - 0110 = 0110 c=1 n=1 z=0 v=0 ( 0 -  6) (+0 - +6) C 
0000 - 0111 = 0111 c=1 n=1 z=0 v=0 ( 0 -  7) (+0 - +7) C 
0000 - 1000 = 1000 c=1 n=1 z=0 v=0 ( 0 -  8) (+0 - -8) C 
0000 - 1001 = 1001 c=1 n=0 z=0 v=0 ( 0 -  9) (+0 - -7) C  NV 
0000 - 1010 = 1010 c=1 n=0 z=0 v=0 ( 0 - 10) (+0 - -6) C  NV 
0000 - 1011 = 1011 c=1 n=0 z=0 v=0 ( 0 - 11) (+0 - -5) C  NV 

对于无符号 0 小于 6,7,8,9...,因此设置执行表示大于。 但是有符号 0 的相同位模式小于 6 和 7 但大于 -8 -7 -6 ...

直到你经常盯着它看它或者只是作弊并查看 ARM 文档是否有签名,如果 N == V 它是一个大于或等于的签名,这不一定是显而易见的。 对于 N != V,它是一个符号小于。 不需要检查执行。 特别是有符号位模式问题 0000 和 1000 不像其他位模式那样适用于进位。

嗯,我之前在其他问题中写过这一切。 无论如何,乘法既关心又不关心无符号和签名。

使用计算器0xF * 0xF = 0xE1。 最大的 4 位数字乘以最大的 4 位数字给出 8 位数字,我们需要两倍的位来覆盖所有位模式。

1111
*       1111
=================    
1111
1111
1111
+    1111
=================
11100001

因此,我们看到结果至少为 2n-1 位的加法,如果您最终得到最后一个位的进位,那么您最终会得到 2n 位。

但是,什么是 -1 * -1? 它等于 1 吧? 我们缺少什么?

无符号隐含

00001111
*       1111
=================    
00001111
00001111
00001111
+00001111
=================
00011100001

但签名的标志被延长

11111111
*       1111
=================    
11111111
11111111
11111111
+11111111
=================
00000000001

所以用乘法符号很重要?

0xC * 0x3 = 0xF4或0x24。

#include <stdio.h>
int main ( void )
{
unsigned int ra;
unsigned int rb;
unsigned int rc;
unsigned int rx;
int sa;
int sb;
int sc;
for(ra=0;ra<0x10;ra++)
for(rb=0;rb<0x10;rb++)
{
sa=ra;
if(ra&8) sa|=0xFFFFFFF0;
sb=rb;
if(rb&8) sb|=0xFFFFFFF0;
rc=ra*rb;
sc=sa*sb;
if((rc&0xF)!=(sc&0xF))
{
for(rx=8;rx;rx>>1) if(rx&ra) printf("1"); else printf("0");
printf(" ");
for(rx=8;rx;rx>>1) if(rx&rb) printf("1"); else printf("0");
printf("n");
}
}
}

并且没有输出。 不出所料。 位 ABCD * 1111

abcd
1111
===============
aaaaabcd
aaaaabcd
aaaaabcd
aaaaabcd
================

每个操作数的四个位,如果我只关心较低的四个位

abcd
1111
===============
abcd
bcd
cd
d
================

就结果而言,操作数符号如何扩展并不重要

现在知道 n 位乘以 n 位等于 n 位溢出的可能组合的很大一部分,在您想要有用的任何代码中做这样的事情并没有多大帮助。

int a,b,c;
c = a * b;

除了较小的数字外,不是很有用。

但现实情况是,如果结果与操作数的大小相同,则乘法,则有符号与无符号无关紧要,如果结果是操作数大小的两倍,那么您需要一个单独的有符号乘法指令/运算和一个无符号。 您当然可以使用 n n=n 指令级联/合成n n=2n,正如您将在某些指令集中看到的那样。

按位操作数、异或或,以及,这些是按位的,它们不关心/不能关心符号。

从 ABCD 开始向左移动 移位一个 BCD0,换位两个 CD00,依此类推。 不是很有趣。 右移,尽管希望有单独的算术和逻辑右移,其中算术 msbit 被复制为位移,逻辑 a 零移位在算术 abcd aabc aaab aaaa,逻辑 abcd 0abc 00ab 000a 0000

但是我们在 C 中没有两种移位。 但是直接做加减法的时候,位就是位,二分之美互补。 在进行减法比较时,对于许多比较,使用的标志对于有符号和无符号是不同的,请获取较旧的 ARM 架构参考手册,我认为他们称之为 armv5,即使它可以追溯到 armv4 和 armv6。

有一个名为"条件字段"的部分和一个表格,这非常好地显示了至少对于 ARM 标志,无符号的这个和那个、有符号的这个和那个以及那些不关心符号的标志组合(相等、不等于等)不会说什么。

理解/记住,某些指令集不仅反转进位和减法的第二个操作数,而且还会反转执行位。 因此,如果在签名的东西上使用进位,那么它就会反转。 我在上面做过的事情,我试图使用术语 carry out 而不是 carry 标志,对于其他一些指令集,carry 标志将被反转,并且无符号大于和小于表翻转。

分裂不是那么容易表现出来的,你必须做长除法等等。 我会把这个留给读者。

并非所有文档都像我在 ARM 文档中引用的表格一样好。 其他处理器文档可能会也可能不会使未签名与签名,他们可能只是说跳跃,如果大于,您可能需要通过实验弄清楚这意味着什么。 现在你现在所有这些你可能已经发现你不需要一个分支,例如,如果无符号或相等。 这只是意味着分支,如果不是少于这样,你可以

cmp r0,r1
or 
cmp r1,r0

并且只需使用分支,如果携带以覆盖无符号小于,无符号小于或等于,无符号大于或等于大小写。 尽管您可能会因为试图在指令中保存一些位而使某些程序员感到不安。

说了这么多,处理器从不区分有符号和无符号。 这些概念只对程序员有意义,处理器非常愚蠢。 位就是位,处理器不知道这些位是否是地址,如果它们是字符串中的字符,则为浮点数(由定点软浮点库实现),这些解释仅对程序员有意义,而不是处理器。 处理器不会"区分无符号和已登录的机器代码",程序员必须正确放置对程序员有意义的位,然后选择正确的指令和指令序列来执行程序员想要执行的任务。 寄存器中的一些32位数字只是一个地址,当这些位用于寻址具有负载或存储的东西时,一旦它们被采样以传递到地址总线的一个时钟周期,它们就是一个地址,在此之前和之后它们只是一个位。 当您在程序中增加该指针时,它们不是一个地址,它们只是您添加其他一些位的位。 你当然可以构建一个没有标志的MIPS指令集,只有N位到N位乘法,只有在两个寄存器相等或不相等的指令不大于或小于类型指令的情况下才能跳转,并且仍然能够制作有用的程序,就像指令集一样,这些指令集与那些东西过火 无符号 这个标志 并签名那个标志, 未签署此指令并签署该指令。

一个不太流行但有时在学校谈论的,也许有一个真正的指令集或许多这样做的指令集是一个非二补码解决方案,这几乎意味着符号和幅度一个符号位和一个无符号值,所以 +3 是 0011 和 -3 是 1011 对于四位寄存器,在做有符号数学时燃烧一位符号。 然后,你和二进制补码一样,必须坐下来拿着铅笔和纸,完成数学运算,小学风格,然后在逻辑上实现这些运算。 这是否会导致单独的未签名和已签名添加? 二进制补码 4 位寄存器我们可以做 0-15 和 -8 到 +7 对于符号幅度,我们可以声明无符号是 0 - 15,但有符号是 -7 到 +7。 对于读者来说,问题/引用与二进制补语有关。

查看 Two 的补码及其算术运算,它是二进制的有符号数字。

二的补码是表示有符号的最常见方法 计算机上的整数。在该方案中,如果二进制数010(2) 编码有符号整数 2(10),然后编码其 2 的补码 110(2), 编码反向:-2(10)。换句话说,要反转任何 在这个方案中的整数,你可以取其二的补码 二进制表示形式。

这样就可以在正二进制值和负二进制值之间进行算术运算。

Two's Complement Python 代码片段:

def twos_complement(input_value, num_bits):
'''Calculates a two's complement integer from the given input value's bits'''
mask = 2**(num_bits - 1)
return -(input_value & mask) + (input_value & ~mask)

最新更新