나는이 작은 보석 뒤에 숨어있는 매우 불쾌한 벌레를 추적했습니다. C ++ 사양에 따라 서명 된 오버플로는 정의되지 않은 동작이지만 값이 bit-width로 확장 될 때 오버플로가 발생할 때만 발생한다는 것을 알고 sizeof(int)
있습니다. 내가 이해했듯이, 증가하는 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에서 확인하십시오.
답변
정의되지 않은 동작에 대해 불가능한 결과를 얻는 것은 유효한 결과이지만 실제로 코드에는 정의되지 않은 동작이 없습니다. 무슨 일이 일어나고 있는지 컴파일러 는 동작이 정의되지 않았다고 생각 하고 그에 따라 최적화합니다.
경우 c
로 정의 int8_t
하고, int8_t
에 촉진 int
, 다음 c--
뺄셈을 수행하도록되어 c - 1
있는 int
연산과에 결과 다시 변환 int8_t
. 뺄셈은 int
오버플 로 되지 않으며 범위를 벗어난 정수 값을 다른 정수 유형으로 변환하는 것은 유효합니다. 대상 유형이 서명 된 경우 결과는 구현에서 정의되지만 대상 유형에 대해 유효한 값이어야합니다. (대상 유형이 서명되지 않은 경우 결과는 잘 정의되어 있지만 여기에는 적용되지 않습니다.)
답변
컴파일러에는 다른 요구 사항이 있기 때문에 표준에 대한 부적합 이외의 버그가있을 수 있습니다. 컴파일러는 다른 버전과 호환되어야합니다. 또한 다른 컴파일러와 어떤면에서 호환 될 것으로 예상 될 수 있으며 대부분의 사용자 기반이 보유한 동작에 대한 일부 신념을 준수 할 수도 있습니다.
이 경우 적합성 버그로 보입니다. 표현은 c--
조작해야 c
유사한 방법으로 c = c - 1
. 여기 c
에서 오른쪽 의 값 이으로 승격 된 int
다음 빼기가 수행됩니다. 보낸 c
의 범위이고 int8_t
,이 감산이 오버 플로우되지 않으며, 그러나의 범위를 벗어난 값을 생성 할 수있다 int8_t
. 이 값이 할당되면 int8_t
결과가에 다시 맞도록 유형으로 다시 변환 됩니다 c
. 범위를 벗어난 경우 변환에 구현 정의 값이 있습니다. 그러나 범위를 벗어난 값 int8_t
은 유효한 구현 정의 값이 아닙니다. 구현은 8 비트 유형이 갑자기 9 비트 이상을 보유한다고 “정의”할 수 없습니다. 값이 구현 정의된다는 것은 C 표준 범위에있는 무언가가 포화 산술 (DSP에서 일반적) 또는 랩 어라운드 (주류 아키텍처)와 같은 동작을 허용 한다는 것을 의미합니다 .int8_t
가 생성되고 프로그램이 계속됨을 의미합니다.
같은 작은 정수형의 값 조작 할 때 컴파일러는 더 넓은 하부 시스템 유형을 사용 int8_t
또는 char
. 산술을 수행 할 때 작은 정수 유형의 범위를 벗어난 결과를이 넓은 유형에서 안정적으로 캡처 할 수 있습니다. 변수가 8 비트 유형이라는 외부에서 볼 수있는 동작을 유지하려면 더 넓은 결과를 8 비트 범위로 잘라야합니다. 기계 저장 위치 (레지스터)가 8 비트보다 넓고 더 큰 값에 만족하기 때문에 명시 적 코드가 필요합니다. 여기서 컴파일러 는 값 정규화를 무시하고 그대로 전달했습니다 printf
. 변환 지정자 %i
에서는 printf
인수가 원래 온 것을 아무 생각이 없습니다 int8_t
계산; 그것은 단지 함께 일하고 있습니다int
논의.
답변
나는 이것을 댓글에 넣을 수 없으므로 답변으로 게시하고 있습니다.
아주 이상한 이유로 --
운영자가 범인이됩니다.
I 코드가 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 비트에 맞지 않는 값을 얻고 있기 때문에 제가 생각해 낼 수있는 유일한 논리적 결론입니다.
답변
기본 하드웨어가 여전히 32 비트 레지스터를 사용하여 int8_t를 유지하고 있다고 생각합니다. 사양이 오버플로에 대한 동작을 부과하지 않기 때문에 구현시 오버플로를 확인하지 않고 더 큰 값도 저장할 수 있습니다.
volatile
메모리를 강제로 사용하도록 지역 변수를 표시 하고 결과적으로 범위 내에서 예상되는 값을 얻습니다.
답변
어셈블러 코드는 문제를 보여줍니다.
: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와 함께 사용하거나 나머지 EBX 클리어와 함께 BL 만 사용해야합니다. dec 대신 sub를 사용하는 것이 궁금합니다. -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의 푸시 opcode가 작동하는 방식이기 때문입니다.
답변
나는 이것이 코드의 최적화에 의한 것이라고 생각합니다.
for (int32_t i = 0; i <= 300; i++)
printf("c: %i\n", c--);
컴파일러 int32_t i
는 i
및에 모두 변수를 사용합니다 c
. 최적화를 끄거나 직접 캐스트하기 printf("c: %i\n", (int8_t)c--);