繁体   English   中英

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

[英]how does c compiler handle unsigned and signed integer? Why the assembly code for unsigned and signed arithmetic operation are the same?

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

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

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

添加更多信息:

学习了一些指令之后,其实有一些指令是区分signed和unsigned的,比如setg,seta等。 此外,CF 和OF 分别适用于无符号和。 但是大多数整数算术指令对待无符号和有符号是一样的,例如

int s = a + b

unsigned s = a + b

生成相同的指令。

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

PS我正在使用x86和gcc

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

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

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

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

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

  • 加一
  • 减一
  • 与零比较

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

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

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

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

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

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

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

无符号 s = a + b

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

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

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

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

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

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

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

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

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

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

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 的结果的进一步指令。

以下反汇编说明了编译器如何为有符号比较和无符号比较生成不同的指令。 注意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的补码表示的算术指令。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM