当前位置: 首页 > 知识库问答 >
问题:

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

熊哲圣
2023-03-14

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

必须注意机器代码如何区分有符号和无符号值。与C不同,它不将数据类型与每个程序值相关联。相反,它在这两种情况下大多使用相同的(汇编)指令,因为许多算术运算对于无符号和二补码算术具有相同的位级行为。

我不明白这意味着什么,有人能给我举个例子吗?

共有3个答案

丁文轩
2023-03-14

二进制补码的美妙之处在于加法(作为减法的结果,因为它使用加法器,也是二进制补码美妙之处的一部分)。加法操作本身不关心有符号和无符号,相同的位模式加在一起产生相同的结果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

嗯……相同的位中的相同位出来,但我作为程序员对这些位的解释方式差异很大。

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

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

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

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

这就是计算机如何进行数学运算,反转进位和第二个操作数。有些人反转执行,因为当加法器用作减法器时,1 on执行意味着没有借用,但是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或溢出标志都有基于有符号的用例。取决于指令集,当由减法器产生时,进位标志本身可以具有两种定义,并且由于比较通常是用减法器来完成的,所以进位定义关系到随后如何使用标志。

大于或小于,同时使用减法来确定如何使用它们,并且结果本身不受标志性的影响。

取一些4位正数。

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…

如果N==V是大于或等于有符号的,那么在你经常盯着它或者只是欺骗并查看ARMs文档中的有符号之前,不一定会有什么明显的东西。对于N!=它是一个带符号的小于。不需要检查进位。特别是有符号位模式问题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;

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

但事实上,如果结果与操作数大小相同,那么有符号与无符号的乘法并不重要,如果结果是操作数大小的两倍,那么需要单独的有符号乘法指令/操作和无符号乘法。您当然可以将nn=2n与nn=n指令级联/合成,正如您将在某些指令集中看到的那样。

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

左移位从abcd移位一bcd0开始,移位二cd00以此类推。不是很有趣。右移位虽然希望有单独的算术和逻辑右移位,其中算术msbit被复制为位中的移位,逻辑a零移位算术abcd aabc aaab aaaa,逻辑abcd 0abc 00ab 000a 0000

但是我们在c中没有两种右移,但是直接做加法和减法的时候,位就是位,二进制补码的妙处。进行减法比较时,有符号和无符号所用的标志是不同的。对于许多比较,请参考较旧的ARM架构参考手册,我想他们称之为armv5,尽管它可以追溯到armv4和armv6。

有一个称为“条件字段”的部分和一个表格,它非常好地显示了至少ARM标志的标志组合,包括无符号的这个和那个,有符号的这个和那个,以及不关心有符号性(等于、不等于等)的标志组合。

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

除法不是那么容易显示的,你要做长除法等。我将把那个留给读者。

并非所有的文档都像我在ARMs文档中提到的表格一样好。其他处理器文档可能会或可能不会将无符号与有符号进行比较,他们可能会说,如果大于则跳转,您可能需要通过实验来了解这意味着什么。现在,所有这些你可能已经明白了,例如,如果无符号或相等,你不需要分支。这只是意味着分支,如果不低于,那么你可以

cmp r0,r1
or 
cmp r1,r0

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

尽管如此,处理器从未区分有符号和无符号。这些概念只对程序员有意义,处理器非常愚蠢。位是位,处理器不知道这些位是否是地址,如果它们是字符串中的字符或浮点数(用定点软浮点库实现),则它们是变量,这些解释只对程序员有意义,而不是处理器。处理器不“区分无符号和有符号的机器代码”,程序员必须正确放置对程序员有意义的位,然后选择正确的指令和指令序列来执行程序员想要执行的任务。寄存器中的一些32位数字仅是一个地址,当这些位被用于通过加载或存储来寻址某个东西时,一旦在一个时钟周期内对它们进行采样并将其传送到地址总线,它们就是一个地址。当你在程序中增加指针时,它们不是地址,它们只是你向其中添加一些其他位的位。你当然可以构建一个类似MIPS的指令集,没有标志,只有N位到N位的乘法,只有当两个寄存器相等或不相等时,才有一个跳转,指令不大于或小于类型指令,并且仍然能够像指令集一样生成有用的程序,这些指令集与未签名此标志和签名该标志、未签名此指令和签名该指令的指令集一样。

不太流行,但有时会在学校讨论,也许有一个真正的指令集或许多这样做的是非二进制补码解决方案,这很大程度上意味着符号和幅度一个符号位和一个无符号值,因此3是0011,而-3是1011,对于一个四位寄存器,在进行带符号数学运算时会消耗一个符号位。然后像二进制补码一样,你必须坐下来,拿着纸和笔,按照小学的方式完成数学运算,然后用逻辑实现这些运算。这会导致单独的无符号和有符号加法吗?二进制补码4位寄存器对于符号幅度,我们可以做0 - 15和-8到7,我们可以声明无符号是0-15,但有符号是-7到7。这是给读者的一个练习,问题/引文与二进制补码有关。

黄修永
2023-03-14

在英特尔处理器(x86系列)和其他具有< code >标志的处理器上,您可以在这些< code >标志中获得一些位,告诉您上一次操作是如何进行的。处理器之间的< code >标志的名称略有不同,但一般来说,在算术方面有两个重要的标志:< code>CF和< code>OF。

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处理器没有任何标志。由您决定是否生成进位(您可以在两个源和目的地上使用 XOR 等方式执行此操作)。

但是,在C(和C)中,整数类型没有溢出验证(至少默认情况下没有。)换句话说,除了四个比较运算符(

如@user694733给出的示例所示,区别在于是否将使用jgja。16个跳转指令中的每一个都将测试各种标志以知道是否跳转。这种组合确实有所不同。

另一个有趣的方面是< code>ADC和< code>ADD之间的区别。在一种情况下,你与进位相加,而在另一种情况下,你不加进位。现在我们有了64位计算机,它可能不会用得太多,但要用32位处理器将两个64位数相加,它会将低32位作为无符号32位数相加,然后将高32位数(有符号或无符号,视情况而定)加上第一次运算的进位。

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

ADD EAX, EBX
ADC ECX, EDX

这里的EDX和进位被添加到ECX,如果EAX EBX有一个未签名的溢出(进位——这意味着正确添加EAXEBX现在应该用33位表示,因为结果不适合32位,CF标志是第33位)。

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

  • A Zero bit:ZF(无论结果是否为零,)
  • CF减去时称为“借用”(对于SBCSBB,)和
  • 还有AF位,它用于“十进制数操作”(没有正常人使用)。AF位告诉您十进制操作中存在溢出。类似的东西。我从未使用过那个。我发现它们的使用太复杂/麻烦了。此外,该位在amd64中仍然存在,但设置它的指令已被删除(例如,参见DAA)。
皮煜
2023-03-14

例如,此代码:

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

请注意它如何对变量< code>i和< code>u使用相同的初始化(< code>mov)和增量(< code>add)指令。这是因为对于无符号和二进制补码,位模式变化相同。

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

选择什么指令取决于架构和编译器。

 类似资料:
  • 我在看的书:CS-app 2。c有无符号和有符号的int类型,并且在大多数架构中使用二进制补码算法来实现有符号值;但是学了一些汇编代码之后,发现很少有指令区分无符号和有符号。所以我的问题是: > 区分有符号和无符号是编译器的责任吗?如果是,它是如何做到的? 谁实现两个补码算法——CPU还是编译器? 添加更多信息: 在学习了更多的指令后,实际上有一些指令区分有符号和无符号,例如setg、seta等。

  • 有符号和无符号变量在按位运算上有区别吗?< br >例如,在处理无符号数字时:< br> 将得到00000101。 但当处理带符号的数字时会发生什么?

  • 问题内容: 关于Java支持哪种无符号数据类型,我有些困惑。 我已经读过了,但我不理解它的非常复杂的解释(至少对我而言)。 问题答案: Java仅支持带符号的类型(除外),因为它假定对于初学者来说,一种类型比每种大小都具有两种类型更容易理解。在C语言中,它被认为是错误的根源,因此不包括对无符号类型的支持。 因此设计师选择了四种尺寸 ,8位 ,16位 ,32位 ,64位。 并保持一致性,他们都签署就

  • 精明的加法学专家会注意到,它只能加到62位。我在编译器和芯片设计方面的经历告诉我,保留位值黄金。所以我们有两个(设置为零)。 那么这是否意味着: 问题一: ~表示36位移位,包含10位类型Id和其余36位本地Id: #00000000000000000000# ShardID 3429的 二进制=1101 0110 0101 因此(hashedValue>>46)=00000 0 110 1 01

  • 问题内容: 如何将手机号码分为国家代码,区号和本地号码?例如,拆分后为+919567123456 国家区号= 91 区号= 9567 当地号码= 123456 问题答案: 用简单的算法解析电话号码是不可能的,您需要使用每个国家/地区的规则填充的数据表-因为每个国家/地区对电话号码的定界都不同。 国家/地区代码非常简单,只需使用Wikipedia中“ 国家/地区调用代码”文章中的数据,并构建一个包含

  • C语言有符号和无符号类型,如char和int。我不确定它是如何在程序集级别实现的,例如,在我看来,有符号和无符号的乘法会带来不同的结果,那么程序集是同时做无符号和有符号的算术,还是只做一个,这在某种程度上是针对不同情况模拟的?