c编译器如何处理无符号和有符号整数?为什么无符号和有符号算术运算的汇编代码是相同的?

时间:2013-10-19 08:47:35

标签: c assembly

我正在读这本书:CS-APPe2。 C具有unsigned和signed int类型,并且在大多数体系结构中使用二进制补码算法来实现有符号值;但在学习了一些汇编代码后,我发现很少有指令区分无符号和有符号。所以我的问题是:

  1. 编译器是否有责任区分签名和 无符号?如果是的话,它是如何做到的?

  2. 谁实现了二进制补码算法 - CPU还是编译器?

  3. 添加更多信息:

    在学习了更多指令之后,实际上有一些指令区分了有符号和无符号,例如setg,seta等。此外,CF和OF分别适用于无符号和。但是大多数整数算术指令都处理无符号并且签名相同,例如

    int s = a + b
    

    unsigned s = a + b
    

    生成相同的指令。

    因此,在执行ADD s d时,CPU应该对待s& d unsigned还是签名?或者它是无关紧要的,因为两个结果的位模式是相同的,编译器的任务是将基础位模式结果转换为无符号或有符号?

    P.S我正在使用x86和gcc

6 个答案:

答案 0 :(得分:8)

在许多情况下,在有符号和无符号操作之间的机器级别上没有区别,并且它仅仅是对位模式的解释的问题。例如,考虑以下4位字操作:

Binary Add  Unsigned   2's comp
----------  --------   --------
  0011          3         3
+ 1011       + 11       - 5
-------     --------   --------
  1110         14        -2  
-------     --------   --------

二进制模式对于有符号和无符号操作是相同的。请注意,减法仅仅是添加负值。当执行SUB操作时,右手操作数是2的补码(反转位和增量)然后添加(负责的ALU电路是加法器);不是你理解的指令级别,而是在逻辑级别,虽然可以实现没有SUB指令的机器,但仍然执行减法,尽管是在两个指令中而不是一个。

根据类型的不同,有些操作需要不同的指令,编译器通常负责生成适当的代码 - 架构变体可能适用。

答案 1 :(得分:2)

这很容易。加法和减法等操作不需要对二进制补码算法中的带符号类型进行任何调整。只需进行一次心智实验,并使用以下数学运算来想象算法:

  • 增加一个
  • 递减一个
  • 与零比较

添加只是从一个堆中逐个获取项目并将它们放到另一个堆中,直到第一个堆为空。减法一次从它们两个中取出,直到减去的一个为空。在模块化算术中,您只需将最小值视为最大值加一,它就可以工作。两个补码只是一个模运算,其中最小值为负。

如果您想看到任何差异,我建议您尝试一些与溢出无关的操作。一个例子是比较(a < b)。

  

是否有责任区分签名和   无符号?如果是的话,它是如何做到的?

通过在需要时生成不同的装配。

  

谁实现了两个补码算法 - CPU还是编译器?

这是一个棘手的问题。两个补码可能是在计算机中使用负整数的最自然的方式。对于溢出的两个补码,大多数操作与具有溢出的无符号整数的操作相同。符号可以从一个位中提取。比较可以通过减法(符号无关),符号位提取和比较为零来概念性地完成。

它的CPU算术功能允许编译器以二进制补码产生计算。

  

无符号s = a + b

请注意,此处计算的方式加上不取决于结果的类型。 Insead它取决于等号右边的变量类型。

  

因此,当执行ADD时,CPU应该对待s&amp; d unsigned还是signed?

CPU指令不了解类型,仅供编译器使用。此外,添加两个无符号数字和添加两个有符号数字之间没有区别。对同一操作有两条指令是愚蠢的。

答案 2 :(得分:1)

对于大多数算术/逻辑运算,无需区分有符号和无符号整数。通常只需要在打印,零/符号扩展或比较值时考虑标志。事实上,CPU对值的类型一无所知。一个4字节的值只是一系列的比特,它没有任何意义,除非用户指出它是一个浮点数,一个4个字符的数组,一个unsigned int或signed int等。例如,当打印一个char变量时,根据指示的类型和输出属性,它将打印出字符,无符号整数或有符号整数。程序员有责任向编译器显示如何处理该值,然后编译器将发出处理该值所需的正确指令。

答案 3 :(得分:1)

关于你的第一个问题已经说了很多,但我想谈谈你的第二个问题:

  

谁实现二进制补码算法 - CPU或者   编译?

C标准不要求负数具有二进制补码,它根本不定义硬件如何表示负数。编译器的任务是将C代码转换为执行代码请求的CPU指令。因此,如果你的CPU使用二进制补码运算,C编译器是否会为二进制补码运算创建代码完全取决于事实。编译器必须知道CPU的工作方式并相应地创建代码。所以这个问题的正确答案是:CPU。

如果你的CPU使用了一个补码表示,那么该CPU的C编译器会发出一个补码指令。另一方面,C编译器可以模拟对不知道负数的CPU上的负数的支持。由于二进制补码允许您忽略许多操作中的数字是否已签名或未签名,因此这并不难。在那种情况下,编译器将实现二进制补码算法。这也可以在具有负数表示的CPU上完成,但为什么编译器应该这样做而不只是使用CPU理解的本机形式?所以除非必须这样做,否则不会这样做。

答案 4 :(得分:0)

这也困扰了我很长一段时间。在处理默认值和隐式指令时,我不知道编译器如何作为程序工作。但是我寻找答案让我得出以下结论:

真实世界仅使用有符号整数,因为发现了负数。这就是在编译器中默认将int视为有符号整数的原因。我完全忽略了无符号数运算,因为它没用。

CPU没有签名和无符号整数的线索。它只知道位 - 0和1.你如何解释它的输出取决于你作为汇编程序员。这使得汇编编程变得乏味。处理整数(有符号和无符号)涉及大量的标志检查。这就是开发高级语言的原因。编译器带走了所有的痛苦。

编译器如何工作是一项非常先进的学习。我接受了目前这超出了我的理解。这种接受帮助我继续前进。

在x86架构中:

add和sub instruction指令修改eflags寄存器中的标志。然后,这些标志可以与adc和sbb指令结合使用,以更高的精度构建算术。在这种情况下,我们将数字的大小移动到ecx寄存器中。循环指令的执行次数与字节数的大小相同。

子指令采用减数的2的补码,将其加到minuend,反转进位。这是在硬件中完成的(在电路中实现)。子指令'激活'不同的电路。使用子指令后,程序员或编译器检查CF.如果为0,则结果为正,并且目标具有正确的结果。如果为1,则结果为负,并且目标具有结果的2的补码。通常,结果保留为2的补码并作为带符号的数字读取,但NOT和INC指令可用于更改它。 NOT指令执行操作数的1的补码,然后操作数递增以得到2的补码。

当程序员计划将添加或子指令的结果作为带符号的数字读取时,他应该看OF标志。如果设置为1,则结果错误。他应该在运行之前对数字进行签名扩展。

答案 5 :(得分:0)

2的补码只是十进制数和二进制数之间的映射。

编译器通过将文字数字转换为相应的二进制代码(例如,将-3转换为0xFFFFFFFD(在反汇编中可以看到)),并生成与2的补码表示形式一致的机器代码,来实现此映射。例如,当它尝试执行0-3时,应选择一个以0x00000000和0x000000003作为参数来产生0xFFFFFFFD的指令。

选择SUB的原因与无符号减法相同,因为它只是产生预期的0xFFFFFFFD。无需要求CPU为签名减法提供特殊的SUB。 说第二个操作数被2的补码取反,由此得出CPU在SUB中实现2的补码的结论是不公平的。因为borrowing from higher bit in subtraction happens to be the same as 2's complement inversing和SUB也用于无符号减法,所以根本不需要在SUB中包含2的补余的概念。

以下反汇编说明了有符号减法使用与无符号减法相同的SUB的事实。

//int32_3 = -3;
010B2365  mov         dword ptr [int32_3],0FFFFFFFDh  
//int32_1 = 0, int32_2 = 3;
010B236C  mov         dword ptr [int32_1],0  
010B2373  mov         dword ptr [int32_2],3  
//uint32_1 = 0, uint32_2 = 3;
010B237A  mov         dword ptr [uint32_1],0  
010B2384  mov         dword ptr [uint32_2],3  
//int32_3 = int32_1 - int32_2;
010B238E  mov         eax,dword ptr [int32_1]  
010B2391  sub         eax,dword ptr [int32_2]  
010B2394  mov         dword ptr [int32_3],eax  
//uint32_3 = uint32_1 - uint32_2;
010B2397  mov         eax,dword ptr [uint32_1]  
010B239D  sub         eax,dword ptr [uint32_2]  
010B23A3  mov         dword ptr [uint32_3],eax  

CPU会在CF和OF标志中保留其他信息,以便进一步的指令根据分配了结果的变量类型以不同的方式使用SUB的结果。

以下反汇编说明了编译器如何为有符号比较和无符号比较生成不同的指令。注意cmp包括内部subjle基于OF标志,jbe基于CF标志。

//if (int32_3  > 1)  int32_3 = 0;
010B23A9  cmp         dword ptr [int32_3],1  
010B23AD  jle         main+76h (010B23B6h)  
010B23AF  mov         dword ptr [int32_3],0  
//if (uint32_3 > 1) uint32_3 = 0;
010B23B6  cmp         dword ptr [uint32_3],1  
010B23BD  jbe         main+89h (010B23C9h)  
010B23BF  mov         dword ptr [uint32_3],0 

OF放弃了CPU实现2的补码的事实,因为OF的设置方式是当中间二进制数0x10000000或0x0FFFFFFF被超过时。 2的补码表示将0x10000000映射到-268435456,将0x0FFFFFFF映射到268435455,这是32位整数的上限和下限。因此,此OF标志是专为2的补码设计的,因为其他表示形式可能会选择将其他二进制数字映射到上限和下限。

总结: 1.编译器通过实现相应的表示形式(映射)并生成指令,使指令的结果符合编译器对有符号和无符号整数的表示形式,从而区分有符号和无符号算术。 2.编译器实现2的补码表示法,CPU也实现它来支持编译器生成算术指令,其结果符合2的补码表示法。