我正在读一本教科书,上面写着:
请务必注意机器码如何区分带符号的 和无符号值。与C语言不同,它不关联数据类型 每个程序的值。相反,它大多使用相同的 两种情况下的(汇编)指令,因为许多算术运算 无符号和无符号操作具有相同的位级行为 二进制补码算法。
我不明白这是什么意思,有人可以给我一个例子吗?
答案 0 :(得分:4)
例如,此代码:
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
请注意,对于变量mov
和add
,它如何对初始化(i
)和递增(u
)使用相同的指令。这是因为无符号和2的补码的位模式变化相同。
比较也使用相同的指令cmp
,但是跳转决定必须有所不同,因为设置最高位的值在以下类型上不同:jg
(带符号的跳转,如果更大,则跳转)和ja
(如果存在则跳转)(未签名)。
选择哪种指令取决于体系结构和编译器。
答案 1 :(得分:1)
检出Two's Complement及其算术运算,它是二进制的带符号数字。
二进制补码是表示带符号的最常见方法 计算机上的整数。在此方案中,如果二进制数字010(2) 编码带符号的整数2(10),然后是其二进制补码110(2), 编码反码:-2(10)。换句话说,要扭转任何迹象 在此方案中,您可以取整数的二进制补码 二进制表示形式。
这样,就有可能在正负二进制值之间进行算术运算。
Two的补码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)
答案 2 :(得分:1)
在Intel处理器(x86家族)和其他带有FLAGS的处理器上,您会在那些FLAGS中得到一些位,告诉您上次操作的工作方式。 FLAGS的名称在处理器之间略有不同,但是就算术而言,您通常有两个重要的含义:CF
和OF
。
CF
是进位位(在其他处理器上通常称为C
)。
OF
是溢出位(在其他处理器上通常称为V
)。
或多或少,CF
代表无符号溢出,OF
代表有符号溢出。处理器执行ADD
操作时,它有一个额外的位,即CF
。因此,如果您添加两个64位数字,则不进行换行的结果可能需要65位。那是进位。通过对两个源和目标中的该位进行3次逻辑运算,将OF
标志设置为最高位(因此64位数字中的位63)。
有一个C如何与4位寄存器一起工作的示例:
R1 = 1010
R2 = 1101
R3 = R1 + R2 = 1 0111
多余的1
不适合R3,因此将其放在CF
位中。附带说明,MIPS处理器没有任何标志。由您决定是否生成进位(可以在两个源和目标上使用XOR等操作)。
但是,在C(和C ++)中,没有验证整数类型上的溢出(至少默认情况下没有。)因此,CF
和OF
标志被忽略除了四个比较运算符(<
,<=
,>
,>=
)以外的所有操作。
如@ user694733所示示例所示,区别在于将使用jg
还是ja
。 16条跳转指令中的每条指令都会测试各种标志,以了解是否跳转。两者的结合才是真正的区别。
另一个有趣的方面是ADC
和ADD
之间的区别。在一种情况下,您加上进位,而另一种则不。现在我们有64位计算机,可能不会使用太多了,但是要用32位处理器添加两个64位数字,它会将低32位添加为无符号32位,然后添加高32位数字(可能是带符号的或无符号的)。
假设您在32位寄存器(CX:AX和DX:BX)中有两个64位数字,则可以这样添加它们:
ADD AX, BX
ADC CX, DX
如果CX
发生无符号溢出(进位-表示将AX + BX
和AX
正确地加进去,则在此处将进位加到BX
上现在应该用33位表示,因为结果不适合32位,CF
标志是该33位)。
请注意,英特尔处理器具有:
CF
,SBC
和)时称为SBB
借入和AF
位用于“十进制数运算” (在他们的头脑中没有人使用。)AF
位告诉您十进制运算中的溢出。这样的事情。我从没用过。我发现它们的使用太复杂/繁琐。答案 3 :(得分:0)
二进制补码的美是加法的结果(由于使用加法器而导致减法,这又是二进制补码的美的一部分)。加法运算本身并不关心有符号和无符号,将相加在一起的相同位模式会产生相同的结果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
hmmm ...在相同的位中输出相同的位,但是我作为程序员解释这些位的方式差异很大。
您可以尝试任意数量的合法值(不会溢出结果的值),您将看到加法结果不知道也不关心签署者与未签署者。完美互补的一部分。
减法只是逻辑上的加法,有些人可能已经学会了“求反”,想知道位模式11111111是您反转00000000并加1 00000001,所以11111111是-1。但是加法如何真正与两个操作数一起工作,如上所示,您确实需要一个三位加法器将三个位加进来,然后将两位输出结果并执行,所以有一个进位,两个结果操作数位并执行。如果我们也回到小学怎么办...
-32-3 =(-32)+(-3)应用求反并将其加到-3上,我们得到(-32)+(〜3)+1
1
11100000
+ 11111100
==============
这就是计算机如何执行该数学运算,将进位和第二个操作数求反。之所以将进位取反,是因为当加法器用作减法器时,执行进位为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不等于那些操作数位,则有符号溢出为true。如果生成带符号数及其结果的表,并且溢出的表将变得很清楚,则不必进行8位乘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对吗?我们缺少什么?
unsigned隐含零
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,就像在某些指令集中看到的那样。
按位运算数,xor或,并且,它们是按位运算符,它们不关心符号。
左移开始于abcd移一bcd0,移两个cd00,依此类推。不太有趣。右移虽然希望具有单独的算术和逻辑右移,其中算术msbit被复制为位的移位,而逻辑零则在算术abcd aabc aaab aaaa中进行逻辑移位,逻辑abcd 0abc 00ab 000a 0000
但是在C中我们没有两种右移。但是当直接进行加减运算时,位就是位,二进制之美互补。在进行比较(即减法)比较时,对于许多比较,有符号和无符号使用的标志是不同的,请获取旧的ARM体系结构参考手册,我认为他们将其称为armv5,即使它返回到armv4直到armv6。
有一个称为“条件字段”的部分和一个表,至少可以很好地显示ARM标志的未签名的this和that,签名的this和that和不关心签名的标志组合(相等,不相等,等等)什么也没说。
理解/记住,某些指令集不仅可以将减法中的进位位和第二个操作数取反,而且还可以将进位位取反。因此,如果在有符号的东西上使用了进位,则将其反转。在上面我尝试使用术语执行而不是进位标志的操作中,对于其他一些指令集,进位标志将被反转,并且无符号的大于和小于表翻转。
划分不太容易显示,您必须进行长划分等。我将其留给读者。
并非所有文档都像我在ARM文档中所引用的表格一样好。其他处理器文档可能会(也可能不会)使未签名与未签名成正比,如果大于,它们可能只是说跳而已,您可能必须实验性地弄清楚这是什么意思。现在您已经了解了所有这些,例如,如果未签名或相等,则不需要分支。那只是意味着分支不少于
cmp r0,r1
or
cmp r1,r0
并仅在带进位的情况下使用分支来覆盖无符号小于,无符号小于或等于,无符号大于,无符号大于或等于的情况。尽管您可能会因为尝试在指令中节省一些位而使某些程序员这样做感到不安。
说完所有这些,处理器就永远不会区分符号和未符号。这些概念仅对程序员有意义,处理器非常愚蠢。位是位,处理器不知道这些位是否是地址,如果它们是变量(如果它们是字符串中的字符),浮点数(由固定点的软浮点库实现),则这些解释仅是对程序员而不是处理器有意义。处理器不会“区分未签名的机器代码和已签名的机器代码”,程序员必须正确放置对程序员有意义的位,然后选择正确的指令和指令序列来执行程序员想要执行的任务。寄存器中的某个32位数字只是当这些位用于加载或存储寻址的地址时,一次采样一个时钟周期以将其传送到地址总线后,它们就是一个地址,在此之前和之后他们只是位。当您在程序中递增该指针时,它们不是地址,它们只是您要添加其他一些位的位。您当然可以构建没有标志的MIPS指令集,并且仅N位到N位相乘,仅当两个寄存器相等或不相等的指令不大于或小于类型指令时才有跳转,并且仍然能够执行有用的程序,如指令集,这些指令使这些东西变得多余,请对该标志取消签名并对其进行签名,对该指令取消签名并对该命令进行签名。
一个不太流行但是有时在学校里谈论过的东西,也许有一个真正的指令集,或者很多这样做的人都是非二进制补码解决方案,这几乎意味着符号和大小是符号位和无符号值,所以+对于一个四位寄存器,当执行有符号数学运算时,它会将一位烧为符号,因此3是0011,-3是1011。然后,您必须像铅笔一样坐下来,用铅笔和纸坐下来,进行数学运算,上学的方式,然后以逻辑方式实施。这是否会导致单独的未签名和已签名的添加?二进制补码4位寄存器,我们可以对符号幅度进行0-15和-8至+7的运算,我们可以声明unsigned为0-15,但有符号为-7至+7。作为读者的练习,问题/引用与二进制补码有关。