我如何从8位整数获得大于8位的值?
我追踪了这个小gem背后的一个非常讨厌的虫子。 我知道,根据C ++规范,有符号溢出是未定义的行为,但只有当值扩展到位宽sizeof(int)
时发生溢出。 据我了解,只要sizeof(char) < sizeof(int)
,增加一个char
不应该是未定义的行为。 但是这并不能解释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。
这是一个编译器错误。
尽pipe为未定义的行为获取不可能的结果是一个有效的结果,但在代码中实际上没有未定义的行为。 发生什么事是编译器认为行为是不确定的,并相应地进行优化。
如果c
被定义为int8_t
,并且int8_t
int
,那么c--
应该在int
算术中执行减法c - 1
并将结果转换回int8_t
。 int
中的相减不会溢出,并且将超出范围的整数值转换为另一个整数types是有效的。 如果目标types已签名,则结果是实现定义的,但是它必须是目标types的有效值。 (如果目标types是无符号的,结果是明确的,但在这里不适用。)
一个编译器可能会有不符合标准的错误,因为还有其他的要求。 编译器应该与其他版本兼容。 它也可能在某些方面与其他编译器兼容,也符合一些关于大多数用户群所持有的行为的信念。
在这种情况下,它似乎是一个符合性错误。 expression式c--
应该以类似于c = c - 1
的方式操作c
。 在这里,右边的c
的值被提升为int
types,然后进行相减。 由于c
在int8_t
的范围内, int8_t
这个减法不会溢出,但是可能会产生一个超出int8_t
范围的int8_t
。 当这个值被赋值时,转换返回到int8_t
types,所以结果适合c
。 在超出范围的情况下,转换具有实现定义的值。 但是, int8_t
范围int8_t
值不是有效的实现定义的值。 一个实现不能“定义”8位types突然保持9位或更多位。 对于实现定义的值意味着int8_t
范围内的int8_t
被生成,并且程序继续。 因此,C标准允许诸如饱和算术(在DSP上常见)或环绕(主stream架构)等行为。
编译器在处理int8_t
或char
等小整数types的值时使用更宽的底层机器types。 当执行算术运算时,在这个更宽的types中可以可靠地捕获超出小整数types范围的结果。 为了保持variables是8位types的外部可见行为,更宽的结果必须被截断为8位范围。 由于机器存储位置(寄存器)的宽度大于8位,所以需要使用显式代码,并且对于较大的值来说是满意的。 在这里,编译器忽略了规范化的值,只是简单地把它传递给printf
。 printf
的转换说明符%i
不知道该参数最初来自int8_t
计算; 它只是与一个int
参数一起工作。
我不能评论这个,所以我把它作为答案。
出于某种非常奇怪的原因,这个--
操作员碰巧是罪魁祸首。
我testing了在Ideone上发布的代码,并用c = c - 1
replace了c--
值保持在[-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--
这样的expression式。 这很可能会将返回值提升为一个int
并传递给它。 这是我能想出的唯一合乎逻辑的结论,因为你实际上正在获得不能适应8位的价值。
我猜测底层硬件仍然使用32位寄存器来保存int8_t。 由于规范没有强制执行溢出行为,因此实现不会检查溢出,并允许存储更大的值。
如果将局部variables标记为volatile
variables,则强制使用内存,从而获得范围内的期望值。
汇编代码揭示了这个问题:
: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
所以这是优化器中的一个错误。
使用%hhd
而不是%i
! 应该解决你的问题。
你看到的是编译器优化的结果,你告诉printf打印一个32位的数字,然后将一个(应该是8位)的数字推到堆栈上,这实际上是指针大小,因为这是x86中的推式操作码的工作原理。
我认为这是通过优化代码来完成的:
for (int32_t i = 0; i <= 300; i++) printf("c: %i\n", c--);
编译器为i
和c
使用int32_t i
variables。 closures优化或直接投射printf("c: %i\n", (int8_t)c--);
c
本身被定义为int8_t
,但是当操作++
或者--
over int8_t
它首先被隐式地转换为int
, 而操作的结果 是c的内部值用printf来打印,后者碰巧是int
。
查看整个循环后 c
的实际值 ,特别是在最后一次递减之后
-301 + 256 = -45 (since it revolved entire 8 bit range once)
其正确的价值类似的行为-128 + 1 = 127
c
开始使用int
大小的内存,但打印时只用8 bits
打印为int8_t
。 作为int
使用时,全部使用32 bits
[编译器错误]
我认为这是因为你的循环会一直持续下去,直到int变成300,c变成-300。 最后一个值是因为
printf("c: %i\n", c);