2011-03-09 2 views
6

64 비트 포인터 뺄셈, 부호있는 정수 언더 플로 및 가능한 컴파일러 버그?

물론 두 포인터의 차이를 지정하면 (std::upper_bound에서 검색된 배열의 시작 부분을 뺀 값) ptrdiff_t가 아닌 int로 변환하는 것은 64 비트 환경에서는 잘못이지만, 결과적으로 발생하는 특별한 나쁜 동작은 매우 예상치 못한 결과였습니다. 나는 [indexBegin, indexEnd]의 배열이 2GB보다 크면 그 차이가 int를 오버플로 할 때 실패 할 것으로 예상된다. 실제로 일어난 것은 indexBegin 및 indexEnd가 2^31의 양면에 값 (즉, indexBegin = 0x7fffffe0, indexEnd = 0x80000010)을 가진 경우의 충돌이었습니다. 추가 조사를 통해 다음과 같은 x86-64 어셈블리 코드가 생성되었습니다 (MSVC++ 2005에서 최적화에 의해 생성됨).

; (inlined code of std::upper_bound, which leaves indexBegin in rbx, 
; the result of upper_bound in r9, block at *(r12+0x28), and data at 
; *(r12+0x40), immediately precedes this point) 
movsxd rcx, r9d     ; movsxd?! 
movsxd rax, ebx     ; movsxd?! 
sub  rcx, rax 
lea  rdx, [rcx+rdi-1] 
movsxd rax, dword ptr [r12+28h] 
imul  rdx, rax 
mov  rax, qword ptr [r12+40h] 
mov  rcx, byte ptr[rdx+rax] 

이 코드는 을 부호있는 32 비트 값으로 뺄셈되는 포인터 x 64로 부호 확장합니다 비트 레지스터를 빼고 그 결과를 다른 부호 확장 32 비트 값으로 곱한 다음 해당 계산의 64 비트 결과로 다른 배열을 인덱싱합니다. 내가 할 수도있는 것처럼 시도해 보라. 나는 이것이 어떤 이론 하에서 올바른지 알 수 없다. 포인터가 64 비트 값으로 감산되었거나 imul 바로 다음에 edx가 rdx로 확장 된 (또는 최종 mov 참조 된 rax + edx가 있었지만 다른 명령을 사용할 수 있다고 생각하지 않습니다. x86-64), 모든 것이 괜찮을 것입니다 (명목상 위험하지만, [indexBegin, indexEnd]가 2GB에 근접하지 않음을 알고 있습니다).

질문은 다소 학문적입니다. 실제 버그는 포인터 차이를 잡기 위해 64 비트 형식을 사용하는 것만으로 쉽게 해결할 수 있지만 컴파일러 버그이거나 허용 할 수없는 언어 사양의 일부입니다 컴파일러는 뺄셈의 피연산자가 결과 유형에 개별적으로 적합하다고 가정해야합니까?

EDIT : 컴파일러가 수행 한 작업이 괜찮을 것이라고 생각할 수있는 유일한 상황은 정수 언더 플로가 발생하지 않는다고 가정 할 수 있는지의 여부입니다 (따라서 두 숫자를 빼고 결과를 a signed int이면 컴파일러는 더 큰 부호있는 정수 타입을 실제로 사용할 수 있습니다.이 경우에는 틀린 것으로 나타납니다). 언어 사양에서 허용됩니까?

+0

을 당신이 합법적 CODEGEN 있다고 생각 여기 버그. VS2010에서 여전히 발생하는 것 같습니다. 당신은 http://connect.microsoft.com에 이러한 세부 사항을 게시해야합니다 –

답변

1

조금 늦었 습니다만, 궁금한 사항은 마지막에 답변하지 않은 것으로 보입니다. EDIT.

예, 오버플로는 정의되지 않은 동작입니다. 그리고 예, UB는 직관력이 떨어질 수 있습니다.특히 UB는 이미 실행 된 코드에 영향을주는 것처럼 보일 수 있습니다.

실제로 컴파일러는 오버플로가 없다는 가정하에 작업 할 수 있습니다. 고전적인 예제는 if (x+1<x)이며 컴파일러가 if (false)으로 대체 할 수있는 오버 플로우에 대한 오보 테스트입니다.

그렇습니다. 32 비트 변수가 실제로 64 비트 레지스터에 저장 될 때 "오버플로"동작을 실제로 혼란스럽게 할 수 있으므로 오버플로에 사용할 공간이 있습니다. 그 레지스터는 당신이 현명하게 정의되지 않은 행동와 C++ 프로그램의 결과를 추론 할 수없는 방법을 보여줍니다 값 1<<32을 저장할 수있는 (!) 효과적으로 값 MAX_INT+1int

+0

그래, 내가이 질문을했을 때 서명 된 오버플로가 UB라는 것을 몰랐다. 그것이 (그리고 당신이 지적한 바와 같이 고전적인 것을 감안할 때),이 예제에 대해 특별한 것은 없습니다. –

1

는 C++ 변환은 다음과 같이 진행됩니다

  1. 귀하의 경우 (대상 유형에 포인터로 동일한 크기의 부호없는 정수에 정수를 부호없는 정수에서
  2. 변환 변환)

이제 컴파일러에서 정수 빼기를 확인합니다. 서명을 유지하는 한 그것이 적합하다고 보는 어떤 방법으로도 이것을 수행 할 수 있습니다. 따라서 Visual-C++은 64 비트 레지스터를 사용하여이를 수행하기로 결정했습니다.

왼쪽 값에 할당하기 전에 오른 쪽을 unsigned int로 캐스팅하여이 작동 순서를 확인할 수 있습니다. 이것은 당신이 예상했던 나쁜 행동을 초래할 것입니다.

+0

하지만 그들을 빼기 전에 int 포인터를 캐스팅하지 않을거야 - 안 int diff = ptr1 - ptr2' 포인터의 빼기 부호없는 타입을 가지고 있고,이 경우에는 64 비트 값이 될 것이므로 int에 대입되기 전에 빼야한다)? –

+0

@Jonathan : int 타입이 잘못되었습니다. 그게 ptrdiff_t와 size_t가 발명 된 이유입니다 ... – 0xC0000022L

+0

@STATUS_ACCESS_DENIED - 조나단은 질문에서 정확히 말합니다. – Seth

관련 문제