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

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

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

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

添加更多信息:

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

int s = a + b 

 unsigned s = a + b 

生成相同的指令。

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

PS我正在使用x86和gcc

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

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

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

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

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

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

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

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

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

unsigned s = a + b

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

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

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

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

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

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

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

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

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

真实世界仅使用有符号整数,因为发现了负数。 这就是在编译器中默认将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,则结果错误。 在运行它们之前,他应该对数字进行签名扩展。

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

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

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

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