我如何从一个8位整数中得到一个大于8位的值?


118

我找到了隐藏在这个小宝石后面的极其讨厌的错误。我知道,按照C ++规范,带符号的溢出是未定义的行为,但是只有当值扩展到bit-width时才发生溢出sizeof(int)。据我了解,增加a char永远不会是未定义的行为sizeof(char) < sizeof(int)。但这并不能解释如何c获得不可能的价值。作为8位整数,如何c保存大于其位宽的值?

// Compiled with gcc-4.7.2
#include <cstdio>
#include <stdint.h>
#include <climits>

int main()
{
   int8_t c = 0;
   printf("SCHAR_MIN: %i\n", SCHAR_MIN);
   printf("SCHAR_MAX: %i\n", SCHAR_MAX);

   for (int32_t i = 0; i <= 300; i++)
      printf("c: %i\n", c--);

   printf("c: %i\n", c);

   return 0;
}

输出量

SCHAR_MIN: -128
SCHAR_MAX: 127
c: 0
c: -1
c: -2
c: -3
...
c: -127
c: -128  // <= The next value should still be an 8-bit value.
c: -129  // <= What? That's more than 8 bits!
c: -130  // <= Uh...
c: -131
...
c: -297
c: -298  // <= Getting ridiculous now.
c: -299
c: -300
c: -45   // <= ..........

在ideone上检查一下。


61
“我知道根据C ++规范,未定义符号溢出。” - 对。确切地说,不仅是未定义的行为也是。看来获得身体上不可能的结果是有效的结果。

@hvd我确定有人对常见的C ++实现如何导致此行为有一个解释。也许与对齐方式或printf()转换方式有关?

其他人已经解决了主要问题。我的评论较为笼统,涉及诊断方法。我相信您之所以会发现这种困惑的部分原因是,它坚决相信这是可行的。显然,这并非不可能,因此请接受并再次
-Tim X

@TimX-我观察了这种行为,显然得出了这样的结论,那就是不可能的。我对这个词的使用是指一个8位整数,该整数具有9位值,根据定义,这是不可能的。发生这种情况的事实表明,它没有被视为8位值。正如其他人已经提到的,这是由于编译器错误。这里唯一看似不可能的是8位空间中的9位值,而这种明显的不可能是由实际上比所报告的“大”空间来解释的。
2013年

我刚刚在机器上对其进行了测试,结果就是应该的结果。c:-120 c:-121 c:-122 c:-123 c:-124 c:-125 c:-126 c:-127 c:-128 c:127 c:126 c:125 c:124 c: 123 c:122 c:121 c:120 c:119 c:118 c:117我的环境是:Ubuntu-12.10 gcc-4.7.2
VELVETDETH

Answers:


111

这是一个编译器错误。

尽管针对未定义行为获得不可能的结果是有效的结果,但是您的代码中实际上没有未定义行为。发生的情况是,编译器认为行为未定义,因此进行了优化。

如果c定义为int8_t,并int8_t提升为intc--则应c - 1int算术中执行减法并将结果转换回int8_t。in的减法int不会溢出,并且将超出范围的整数值转换为另一种整数类型是有效的。如果目标类型是带符号的,则结果是实现定义的,但它必须是目标类型的有效值。(如果目标类型是无符号的,则结果是明确定义的,但这不适用于此。)


我不会将其描述为“错误”。由于带符号的溢出会导致未定义的行为,因此编译器完全有权假设它不会发生,并优化循环以将的中间值保持c在较宽的类型中。据推测,这就是这里发生的事情。
Mike Seymour

4
@MikeSeymour:这里唯一的溢出是在(隐式)转换上。签名转换上的溢出没有未定义的行为;它仅产生实现定义的结果(或引发实现定义的信号,但这似乎没有发生)。算术运算和转换之间的定义性差异很奇怪,但这就是语言标准对其进行定义的方式。
基思·汤普森

2
@KeithThompson这是C和C ++之间的区别:C允许实现定义的信号,而C ++不允许。C ++只是说:“如果目标类型是带符号的,则可以用目标类型(和位域宽度)表示该值,则该值不变;否则,该值是实现定义的。”

碰巧的是,我无法在g ++ 4.8.0上重现奇怪的行为。
丹尼尔·兰道

2
@DanielLandau请参阅该错误中的注释38:“已修复4.8.0”。:)

15

编译器可能存在除不符合标准之外的错误,因为还有其他要求。编译器应与其自身的其他版本兼容。还可以期望它在某种程度上与其他编译器兼容,并且也符合其大多数用户群对行为的某些信念。

在这种情况下,它似乎是一致性错误。该表达式c--c以类似于的方式进行操作c = c - 1。在此,将c右边的值提升为type int,然后进行减法。由于c处于的范围内int8_t,因此该减法不会溢出,但是可能会产生超出的范围的值int8_t。分配此值后,转换会转换回该类型,int8_t因此结果将放回到c。在超出范围的情况下,转换具有实现定义的值。 但是超出范围的int8_t值不是有效的实现定义的值。一个实现不能“定义”一个8位类型突然拥有9位或更多位。 对于要由实现定义的值,意味着将int8_t产生范围内的某个值,然后程序继续。因此,C标准允许诸如饱和算法(在DSP上常见)或环绕(主流体系结构)之类的行为。

在处理小整数类型(例如int8_t或)的值时,编译器使用的基础机器类型范围更广char。当执行算术运算时,可以在这种较宽的类型中可靠地捕获超出小整数类型范围的结果。为了保留变量为8位类型的外部可见行为,必须将较宽的结果截断为8位范围。这样做需要使用显式代码,因为机器存储位置(寄存器)的宽度大于8位,并且对较大的值感到满意。在这里,编译器忽略了对值进行规范化,只是将其printf原样传递给了它。在转换说明%iprintf不知道的是,争论最初来自说法。int8_t计算; 它只是与一个int


这是一个清晰的解释。
大卫·希利

在关闭优化器的情况下,编译器会生成良好的代码。因此,使用“规则”和“定义”的说明不适用。这是优化程序中的错误。

14

我无法在评论中显示此内容,因此将其发布为答案。

由于某些非常奇怪的原因,--操作员恰好是罪魁祸首。

我测试了Ideone上发布的代码,并替换c--c = c - 1,并将值保持在[-128 ... 127]范围内:

c: -123
c: -124
c: -125
c: -126
c: -127
c: -128 // about to overflow
c: 127  // woop
c: 126
c: 125
c: 124
c: 123
c: 122

怪异的家伙?我不太了解编译器对诸如i++或的表达式的处理方式i--。可能int会将返回值提升为并传递给它。这是我能得出的唯一合乎逻辑的结论,因为实际上您正在获取的值不能适合8位。


4
由于积分促销,c = c - 1手段c = (int8_t) ((int)c - 1。将超出范围的转换intint8_t已定义的行为,但是将实现定义的结果转换为。实际上,难道不 c--应该执行相同的转换吗?

12

我想底层硬件仍在使用32位寄存器来保存该int8_t。由于该规范没有强加溢出行为,因此该实现不检查溢出,也允许存储较大的值。


如果将局部变量标记为volatile强制使用内存,并因此获得该范围内的期望值。


1
哇哦 我忘记了编译后的程序集将在可能的情况下将局部变量存储在寄存器中。这似乎是最可能的答案,printf而不关心sizeof格式值。
rliu

3
@roliu运行g ++ -O2 -S code.cpp,您将看到程序集。此外,printf()是变量参数函数,因此等级小于int的参数将被提升为int。

@nos我想。我无法安装UEFI引导加载程序(尤其是rEFInd)来在计算机上运行archlinux,因此很长时间以来我实际上并未使用GNU工具进行编码。我会解决的...最终。目前,它只是VS中的C#,并试图记住C /学习一些C ++ :)
rliu 2013年

@rollu运行它在虚拟机中,例如VirtualBox的

@nos不想破坏话题,但是是的,我可以。我也可以只使用BIOS引导加载程序安装linux。我只是固执己见,如果我无法使用UEFI引导加载程序运行它,那么我可能根本无法运行它:P。
rliu

11

汇编代码揭示了问题:

:loop
mov esi, ebx
xor eax, eax
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
sub ebx, 1
call    printf
cmp ebx, -301
jne loop

mov esi, -45
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
xor eax, eax
call    printf

EBX应与FF后递减相加,或者仅BL与剩余的EBX清除一起使用。很好奇它使用sub而不是dec。-45完全神秘。这是300和255 = 44的按位求逆。-45 =〜44。某处有连接。

使用c = c-1时,它需要做很多工作。

mov eax, ebx
mov edi, OFFSET FLAT:.LC2   ;"c: %i\n"
add ebx, 1
not eax
movsx   ebp, al                 ;uses only the lower 8 bits
xor eax, eax
mov esi, ebp

然后,它仅使用RAX的较低部分,因此限制为-128至127。编译器选项“ -g -O2”。

没有优化,它会生成正确的代码:

movzx   eax, BYTE PTR [rbp-1]
sub eax, 1
mov BYTE PTR [rbp-1], al
movsx   edx, BYTE PTR [rbp-1]
mov eax, OFFSET FLAT:.LC2   ;"c: %i\n"
mov esi, edx

因此,这是优化程序中的错误。


4

使用%hhd代替%i!应该解决你的问题。

您会看到编译器优化的结果,同时告诉您printf打印32位数字,然后将一个(据说是8位)数字推入堆栈,该数字实际上是指针大小的,因为这就是x86中推入操作码的工作方式。


1
我可以使用来在系统上重现原始行为g++ -O3。更改%i%hhd不会更改任何内容。
基思·汤普森

3

我认为这是通过优化代码来完成的:

for (int32_t i = 0; i <= 300; i++)
      printf("c: %i\n", c--);

编译器将int32_t i变量同时用于ic。关闭优化或直接投放 printf("c: %i\n", (int8_t)c--);


然后关闭优化。或执行以下操作:(int8_t)(c & 0x0000ffff)--
Vsevolod

1

c本身被定义为int8_t,但是当操作++--结束运算时,int8_t它首先被隐式转换成运算int结果,c的内部值则是用printf打印的,恰好是int

实际值c整个循环后,尤其是去年之后递减

-301 + 256 = -45 (since it revolved entire 8 bit range once)

与行为类似的正确值 -128 + 1 = 127

c开始使用int尺寸记忆,但打印int8_t时仅使用本身打印8 bits32 bits用作时全部使用int

[编译器错误]


0

我认为这是因为您的循环将一直进行到int变为300且c变为-300为止。最后一个值是因为

printf("c: %i\n", c);

'c'是一个8位值,因此它不可能拥有最大-300的数字。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.