12

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

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

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

添加更多信息:

在学习了一些指令之后,实际上其中有一些区分有符号和无符号的,例如setg,seta等。此外,CF 和 OF 分别适用于 unsigned 和。但是大多数整数算术指令将无符号和有符号视为相同,例如

int s = a + b

unsigned s = a + b

生成相同的指令。

那么在执行的时候ADD s d,CPU应该把s&d处理成无符号的还是有符号的呢?或者它是不相关的,因为两个结果的位模式相同,编译器的任务是将底层位模式结果转换为无符号或有符号?

PS我正在使用x86和gcc

4

6 回答 6

8

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

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

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

根据类型的不同,有些操作确实需要不同的指令,一般来说,生成适当的代码是编译器的责任——可能会应用架构变化。

于 2013-10-19T09:18:48.717 回答
3

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

  • 加一
  • 减一
  • 与零比较

加法只是从一个堆中一个一个地取出项目并将它们放入另一个堆,直到第一个为空。减法是同时从它们两个中取出,直到减去的那个是空的。在模运算中,您只需将最小值视为最大值加一即可。二进制补码只是一种模算术,其中最小值为负数。

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

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

通过在需要时生成不同的程序集。

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

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

不过,正是 CPU 的算术功能允许编译器以二进制补码形式进行计算。

无符号 s = a + b

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

那么在执行ADD sd的时候,CPU应该把s&d处理成unsigned还是signed呢?

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

于 2013-10-19T13:09:48.540 回答
1

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

于 2013-10-20T12:08:22.727 回答
1

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

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

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

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

于 2016-10-26T18:03:45.417 回答
0

这也困扰了我很久。在处理其默认值和隐式指令时,我不知道编译器如何作为程序工作。但是我对答案的搜索使我得出以下结论:

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

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

编译器的工作原理是一个非常高级的学习。我承认目前这超出了我的理解范围。这种接受帮助我继续我的课程。

在 x86 架构中:

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

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

当程序员计划将 add 或 sub 指令的结果读取为有符号数时,他应注意 OF 标志。如果设置为 1,则结果错误。他应该在数字之间进行操作之前对数字进行签名扩展。

于 2013-10-21T14:53:43.787 回答
0

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

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

它选择与无符号减法相同的 SUB 的原因是它只是按预期产生 0xFFFFFFFD。无需要求 CPU 为有符号减法提供特殊的 SUB。说第二个操作数被 2 的补码取反,由此推断出 CPU 在 SUB 中实现 2 的补码的结论是不公平的。因为在减法中从高位借位恰好与 2 的补码反转相同,并且 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 的结果。

以下反汇编说明了编译器如何为有符号比较和无符号比较生成不同的指令。Noticecmp包括一个 internal sub,并且jle基于 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的补码表示。

于 2018-12-04T11:10:01.030 回答