Uns*_*ned 118 c++ gcc undefined-behavior
我找到了一个隐藏在这个小宝石后面的非常讨厌的小虫.我知道根据C++规范,有符号溢出是未定义的行为,但只有当值扩展到位宽时发生溢出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;
}
Run Code Online (Sandbox Code Playgroud)
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 // <= ..........
Run Code Online (Sandbox Code Playgroud)
小智 111
虽然对于未定义的行为获得不可能的结果是有效的结果,但实际上代码中没有未定义的行为.发生的事情是编译器认为行为未定义,并相应地进行优化.
如果c被定义为int8_t,并且int8_t提升为int,则c--应该c - 1在int算术中执行减法并将结果转换回int8_t.减法int不会溢出,并且将超出范围的积分值转换为另一个整数类型是有效的.如果目标类型已签名,则结果是实现定义的,但它必须是目标类型的有效值.(如果目标类型是无符号的,则结果是明确定义的,但这不适用于此.)
Kaz*_*Kaz 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原样.在转换说明%i在printf不知道的是,争论最初来自int8_t计算; 它只是在int争论.
Moh*_*oun 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
Run Code Online (Sandbox Code Playgroud)
怪异的?我不知道很多关于什么样的编译器像表达式i++或i--.它可能int会将回报价值提升到并传递给它.这是我能想出的唯一合乎逻辑的结论,因为你实际上得到的值不能达到8位.
Zol*_*tán 12
我猜底层硬件仍然使用32位寄存器来保存int8_t.由于规范没有强加溢出行为,因此实现不会检查溢出并允许存储更大的值.
如果标记局部变量,volatile则强制为其使用内存,从而获得该范围内的预期值.
小智 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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
然后它仅使用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
Run Code Online (Sandbox Code Playgroud)
所以这是优化器中的一个错误.