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


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

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

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

添加更多信息:

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

int s = a + b

unsigned s = a + b

生成相同的指令。

所以在执行时 ADD s d,如果CPU处理s&d未签名或签名?或者它是无关紧要的,因为两个结果的位模式是相同的,编译器的任务是将基础位模式结果转换为unsigned或signed?

P.S我正在使用x86和gcc


1306
2017-10-19 08:47


起源

“C有无符号和有符号的int类型,并使用二进制补码算法来实现有符号值。我们都知道。” - 你们都错了。 2的补充很受欢迎,但并不普及。 C标准允许实现对有符号整数使用1的补码或符号+幅度表示。
至于问题:1。是的,通过查看变量的类型并在必要时发出不同的组件; 2. CPU。
较低级别的无符号和有符号数字之间的差异仅在这些数字被扩展或截断时才会显现(sext / zext操作和相似)。有符号和无符号的1的补数的算术是相同的。 - SK-logic
除了SK逻辑:看看除法和比较,你会发现有符号和无符号之间的差异。从cpu的角度来看,任何数据都只是一组位或字节。 - Bryan Olivier
@ SK-logic:这个答案比评论更多 - 我建议你这样发布 - 特别是如果你能提出一些有用的例子。 - Clifford


答案:


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

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

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

如果您想看到任何差异,我建议您尝试在溢出方面不安全的操作。一个例子是比较(a < b)。

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

通过在需要时生成不同的组件。

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

这是一个很难的问题。两个补码可能是在计算机中使用负整数的最自然的方式。对溢出的二进制补码的大多数运算与有溢出的无符号整数的运算相同。符号可以从一个位中提取。比较可以通过减法(符号无关),符号位提取和零比较在概念上完成。

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

unsigned s = a + b

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

因此,当执行ADD时,CPU是否应该对s&d进行无符号或签名?

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


2
2017-10-19 13:09



我想你应该说:“CPU指令 别 了解类型“。 - Bryan Olivier
@paval,所以 s = a + b,转换比特结果是编译器的任务 a + b 到s的类型,即未签名或签名。例如。说有点结果 a + b 是1001(假设整数是4位),所以如果s是无符号的,编译器将1001视为9,否则,它被视为-7。所有这些转换都是由编译器完成的,而不是CPU。我对吗? - tomwang1013
谢谢@BryanOlivier。 - Pavel Šimerda
@ user1446907不。编译器不会也不可能解释运行时值,因为它当时没有运行。它仅决定将使用哪些指令对这些值执行操作。只有文字(直接写入源文件的数字)由编译器解释,但那些自然默认为签名。 - Pavel Šimerda
修正:“这是CPU的算术功能,允许编译器以二进制补码产生计算。”这是错误的并且混淆了这个问题。 - Pavel Šimerda


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

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

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

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


8
2017-10-19 09:18



“当执行SUB操作时,右手操作数被补充然后添加” - 我认为你错过了那里的一个修正。 补充 通常是指按位补码(~x),对于两个补码,这与否定它的操作不同。
@clifford,我添加一些例子,你能阅读我的问题并帮助再次回答吗? - tomwang1013
@hvd:的确,谢谢。 - Clifford


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


1
2017-10-20 12:08



我几乎得到它:虽然 a + b 输出相同的位结果,我们可以告诉编译器将其视为未签名或签名 s = a + b 通过s的类型。或者通过打印声明: print("%xxx", a + b)。 - tomwang1013
对,就是那样。但是传递给printf的格式与格式不同可能会调用未定义的行为。应首先将其类型化为所需类型,但这仅仅是类型转换,位模式仍未更改 - phuclv
@ user1446907:这在技术上是错误的。你没有告诉编译器如何对待它(它从类型中知道它 a + b),你告诉它生成一个指令序列,将它从表达式转换为变量类型。 - Pavel Šimerda
@PavelŠimerda谢谢,你的解释更加简洁和有意义,尽管我还有很多问题。我需要了解更多。 - tomwang1013
@ user1446907:请问。它也帮助我理清我的知识。 - Pavel Šimerda


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

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

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

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

在x86架构中:

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

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

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


0
2017-10-21 14:53



我很想给出一个+1(不是你实际需要的),我只需要一些信息(例如关于sub和更正),这些信息需要易于理解但可靠的来源支持。 - Pavel Šimerda
@PavelŠimerda我从本书中学到了子指令: amazon.com/x86-PC-Assembly-Language-Interfacing/product-reviews/...  这本书很旧,它是关于模拟器中的窗口中的汇编编程。 - KawaiKx


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

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

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

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


0
2017-10-26 18:03