【问题标题】:how does c compiler handle unsigned and signed integer? Why the assembly code for unsigned and signed arithmetic operation are the same?c 编译器如何处理无符号和有符号整数?为什么无符号和有符号算术运算的汇编代码相同?
【发布时间】:2021-07-30 22:35:41
【问题描述】:

我正在看书: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

【问题讨论】:

  • "C 有无符号和有符号 int 类型,并使用补码算法来实现有符号值。我们都知道。" - 你们都知道错误。 2 的补码很流行,但并不普遍。 C 标准允许实现对有符号整数使用 1 的补码或符号+幅度表示。
  • 关于问题:1.是的,通过查看变量的类型并在必要时发出不同的程序集; 2. CPU。
  • 低层中无符号数和有符号数之间的差异只有在此类数字被扩展或截断(sext/zext 操作等)时才会显现出来。 1 的补数的算法对于有符号和无符号是相同的。
  • 除了SK-logic:看看除法和比较,你会发现有符号和无符号之间的区别。从 CPU 的角度来看,任何数据都只是一组位或字节。
  • @SK-logic:答案多于评论——我建议你这样发布——尤其是如果你能想出一些可行的例子。

标签: c assembly


【解决方案1】:

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

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

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

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

【讨论】:

  • “当执行 SUB 操作时,右边的操作数被补码然后相加”——我想你错过了一个修正。 补码通常指的是按位补码(~x),对于二进制补码,这与取反不是同一个操作。
  • @clifford,我添加了一些例子,你能阅读我的问题并再次帮助回答吗?
【解决方案2】:

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

  • 加一
  • 减一
  • 与零比较

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

如果您想看看有什么不同,我建议您尝试不安全的溢出操作。一个例子是比较 (a < b)。

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

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

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

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

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

无符号 s = a + b

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

那么在执行ADD s d时,CPU应该把s&d处理成无符号还是有符号的呢?

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

【讨论】:

  • 我想你的意思是说:“CPU 指令不知道类型”。
  • @paval,所以在s = a + b中,将a + b的位结果转换为s的类型,即无符号或有符号是编译器的任务。例如。假设a + b的位结果为1001(假设整数为4位),所以如果s是无符号的,编译器将1001视为9,否则视为-7。所有这些转换都是由编译器完成的,而不是 CPU。我说的对吗?
  • 谢谢@BryanOlivier。
  • @user1446907 不。编译器不会也不可能解释运行时值,因为它当时没有运行。它仅决定将使用哪些指令对这些值执行操作。只有文字(直接写入源文件的数字)由编译器解释,但那些自然默认为带符号的。
  • 已修复:“不过,CPU 的算术特性允许编译器以二进制补码形式进行计算。”这是错误的,也使事情变得混乱。
【解决方案3】:

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

【讨论】:

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

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

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

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

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

【讨论】:

  • 根本有点夸大了。 C 确实指定实现将对有符号整数使用 2 的补码、1 的补码或符号/幅度编码。 (但对象表示允许包含填充。)我认为这可以保证位的位置值处于正常顺序(模字节序),如果您要使用 unsigned char 查看对象表示(这是有保证的)是没有填充的普通二进制文件)。也就是说,我们可以排除符号位在中间,其他位在两边。或任何其他深奥的编码。
  • @PeterCordes 见第二段。 C标准说C中的数字可以是,或者,或者,但它没有说“C不能在不是这种情况的硬件上运行”。如果您的硬件使用不同的表示形式,它仍然可以支持 C 编译器,但是该编译器必须选择您提到的一种表示形式进行存储和计算,要么在软件中执行它们,要么转换为本机格式,进行计算并转换回所选的存储格式,可能更快。在这种极少数情况下,实现会定义它,这是正确的。
【解决方案5】:

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

自从发现负数以来,现实世界只使用有符号整数。这就是编译器默认将 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,则结果错误。他应该在数字之间进行操作之前对数字进行签名扩展。

【讨论】:

  • 我很想给 +1(不是你真的需要它),我只需要一些易于理解但可靠的来源支持的信息(例如关于 sub 和更正) .
  • @PavelŠimerda 我从这本书中了解了子指令:amazon.com/x86-PC-Assembly-Language-Interfacing/product-reviews/… 这本书很旧,它是关于在模拟器中的 Windows 中进行汇编编程的。
  • 是的,CF 会告诉您无符号减法是否已包装,但它可以包装到现在不在有符号 2 的补码的-2^31 .. 2^31-1 范围内。例如sub eax, ecx with eax = 0, ecx = 0xffffffff` 将保留 EAX=1, CF=1。 -0xffffffff = -4294967295 需要 33 位来表示 2 的补码,而 +1 只是这些位中的低 32 位。
  • 当然,在某些情况下,无符号输入的减法会留下正确的 2 的补码有符号结果,但您必须检查更多的 CF 才能确定是否发生了这种情况。
【解决方案6】:

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 包含一个内部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的补码表示。

【讨论】:

    猜你喜欢
    • 2013-10-28
    • 2014-02-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-19
    • 1970-01-01
    • 2014-10-04
    • 1970-01-01
    相关资源
    最近更新 更多