8 비트 정수에서 크기가 8 비트보다 큰 값을 어떻게 얻었습니까?


118

나는이 작은 보석 뒤에 숨어있는 매우 불쾌한 벌레를 추적했습니다. 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에서 확인하십시오.


61
"C ++ 사양에 따라 서명 된 오버플로가 정의되지 않았 음을 알고 있습니다." -- 권리. 정확하게 말하면 이 정의되지 않은 것이 아니라 동작 이 있습니다. 물리적으로 불가능한 결과를 얻는 것처럼 보이는 것은 유효한 결과입니다.

@hvd 나는 누군가가 일반적인 C ++ 구현 이이 동작을 일으키는 방법에 대한 설명을 가지고 있다고 확신합니다. 아마도 그것은 정렬과 관련이 있거나 어떻게 printf()변환합니까?
rliu

다른 사람들은 주요 문제를 해결했습니다. 내 의견은 더 일반적이며 진단 접근 방식과 관련이 있습니다. 나는 당신이이 퍼즐을 발견 한 이유의 일부가 그것이 불가능한 신념이라고 믿습니다. 분명히 불가능하지 않습니다. 그러니 그것을 받아들이고 다시보세요
Tim X

@TimX-나는 행동을 관찰하고 분명히 불가능하지 않다는 결론을 도출했습니다. 이 단어의 사용은 정의상 불가능한 9 비트 값을 보유하는 8 비트 정수를 나타냅니다. 이것이 발생했다는 사실은 8 비트 값으로 취급되지 않음을 시사합니다. 다른 사람들이 언급했듯이 이것은 컴파일러 버그 때문입니다. 여기서 불가능 해 보이는 유일한 것은 8 비트 공간에서 9 비트 값이며,이 명백한 불가능 성은 실제로보고 된 것보다 "더 큰"공간으로 설명됩니다.
Unsigned

내 기계에서 방금 테스트했으며 결과는 그대로입니다. 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

답변:


111

이것은 컴파일러 버그입니다.

정의되지 않은 동작에 대해 불가능한 결과를 얻는 것은 유효한 결과이지만 실제로 코드에는 정의되지 않은 동작이 없습니다. 무슨 일이 일어나고 있는지 컴파일러 는 동작이 정의되지 않았다고 생각 하고 그에 따라 최적화합니다.

경우 c로 정의 int8_t하고, int8_t에 촉진 int, 다음 c--뺄셈을 수행하도록되어 c - 1있는 int연산과에 결과 다시 변환 int8_t. 뺄셈은 int오버플 로 되지 않으며 범위를 벗어난 정수 값을 다른 정수 유형으로 변환하는 것은 유효합니다. 대상 유형이 서명 된 경우 결과는 구현에서 정의되지만 대상 유형에 대해 유효한 값이어야합니다. (대상 유형이 서명되지 않은 경우 결과는 잘 정의되어 있지만 여기에는 적용되지 않습니다.)


나는 그것을 "버그"라고 설명하지 않을 것이다. 서명 된 오버플로는 정의되지 않은 동작을 유발하므로 컴파일러는 이것이 발생하지 않을 것이라고 가정하고 루프를 최적화하여 c더 넓은 유형 의 중간 값을 유지 합니다. 아마도 그것이 여기서 일어나는 일입니다.
Mike Seymour 2013

4
@MikeSeymour : 여기서 유일한 오버플로는 (암시 적) 변환입니다. 서명 된 변환의 오버플로에는 정의되지 않은 동작이 없습니다. 단순히 구현 정의 결과를 생성합니다 (또는 구현 정의 신호를 발생하지만 여기에서는 발생하지 않는 것 같습니다). 산술 연산과 변환의 정의 차이는 이상하지만 언어 표준이 정의하는 방식입니다.
Keith Thompson

2
@KeithThompson 그것은 C와 C ++의 차이점입니다. C는 구현 정의 신호를 허용하지만 C ++는 허용하지 않습니다. C ++에서는 "대상 형식이 서명 된 경우 대상 형식 (및 비트 필드 너비)으로 나타낼 수 있으면 값이 변경되지 않고, 그렇지 않으면 값이 구현 정의됩니다."라고만 말합니다.

그렇게되면 g ++ 4.8.0에서 이상한 동작을 재현 할 수 없습니다.
Daniel Landau

2
@DanielLandau 해당 버그의 주석 38 참조 : "4.8.0에서 수정 됨." :)

15

컴파일러에는 다른 요구 사항이 있기 때문에 표준에 대한 부적합 이외의 버그가있을 수 있습니다. 컴파일러는 다른 버전과 호환되어야합니다. 또한 다른 컴파일러와 어떤면에서 호환 될 것으로 예상 될 수 있으며 대부분의 사용자 기반이 보유한 동작에 대한 일부 신념을 준수 할 수도 있습니다.

이 경우 적합성 버그로 보입니다. 표현은 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 논의.


이것은 명쾌한 설명입니다.
David Healy

컴파일러는 최적화 프로그램을 끈 상태에서 좋은 코드를 생성합니다. 따라서 "규칙"및 "정의"를 사용한 설명은 적용되지 않습니다. 옵티마이 저의 버그입니다.

14

나는 이것을 댓글에 넣을 수 없으므로 답변으로 게시하고 있습니다.

아주 이상한 이유로 --운영자가 범인이됩니다.

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 비트에 맞지 않는 값을 얻고 있기 때문에 제가 생각해 낼 수있는 유일한 논리적 결론입니다.


4
이 때문에 적분 프로모션의 c = c - 1방법 c = (int8_t) ((int)c - 1. 범위 int를 벗어난 것을로 변환하면 int8_t동작이 정의되었지만 구현 정의 결과가 있습니다. 사실 c--똑같은 변환을 수행해야 하지 않습니까?

12

기본 하드웨어가 여전히 32 비트 레지스터를 사용하여 int8_t를 유지하고 있다고 생각합니다. 사양이 오버플로에 대한 동작을 부과하지 않기 때문에 구현시 오버플로를 확인하지 않고 더 큰 값도 저장할 수 있습니다.


volatile메모리를 강제로 사용하도록 지역 변수를 표시 하고 결과적으로 범위 내에서 예상되는 값을 얻습니다.


1
오 와우. 컴파일 된 어셈블리가 가능한 경우 레지스터에 지역 변수를 저장한다는 것을 잊었습니다. 이것은 형식 값을 printf신경 쓰지 않는 것과 함께 가장 가능성이 높은 대답처럼 보입니다 sizeof.
rliu

3
@roliu 실행 g ++ -O2 -S code.cpp, 그러면 어셈블리가 표시됩니다. 또한 printf ()는 가변 인수 함수이므로 순위가 int보다 작은 인수는 int로 승격됩니다.
nos

@nos하고 싶습니다. 내 컴퓨터에서 archlinux를 실행하기 위해 UEFI 부트 로더 (특히 rEFInd)를 설치할 수 없었기 때문에 실제로 오랫동안 GNU 도구로 코딩하지 않았습니다. 나는 그것을 얻을 것이다 ... 결국. 지금은 VS의 C # 일 뿐이며 C를 기억하려고 노력하고 있습니다. C ++를 배우려고합니다. :)
rliu

@rollu 가상 머신에서 실행합니다. 예 : VirtualBox
nos

@nos 주제를 탈선하고 싶지는 않지만 그래, 할 수 있습니다. BIOS 부트 로더로 리눅스를 설치할 수도 있습니다. 나는 고집스럽고 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와 함께 사용하거나 나머지 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

그래서 이것은 옵티마이 저의 버그입니다.


4

%hhd대신 사용%i ! 문제를 해결해야합니다.

여러분이 보는 것은 printf에게 32 비트 숫자를 인쇄하도록 지시 한 다음 (아마도 8 비트) 숫자를 스택에 푸시하는 것과 결합 된 컴파일러 최적화의 결과입니다. 이것은 x86의 푸시 opcode가 작동하는 방식이기 때문입니다.


1
.NET을 사용하여 시스템에서 원래 동작을 재현 할 수 g++ -O3있습니다. 변경 %i하는 %hhd것도 변경되지 않습니다.
Keith Thompson

3

나는 이것이 코드의 최적화에 의한 것이라고 생각합니다.

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

컴파일러 int32_t ii및에 모두 변수를 사용합니다 c. 최적화를 끄거나 직접 캐스트하기 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 bits. 다음 32 bits과 같이 사용할 때 모두 활용int

[컴파일러 버그]


0

나는 당신의 루프가 int i가 300이되고 c가 -300이 될 때까지 갈 것이기 때문에 일어난 것이라고 생각합니다. 그리고 마지막 가치는

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

'c'는 8 비트 값이므로 -300만큼 큰 숫자를 보유 할 수 없습니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.