Visual Studio 계열 쉬프트(>>) 연산 버그의 원인

김우승님의 댓글을 보고 ISO/IEC 9899:201x를 찾아 읽어본 결과, 아래 내용은 버그가 아님.
C/C++는 전체 비트수(즉, long int는 32비트이니 32)보다 큰 숫자의 쉬프트는 정의하지 않음!
그냥 '이런 뻘짓을 했는데, 다 헛소리구나'하는 참고용으로만 읽을 것.

visual studio의 쉬프트연산 버그란 글을 보곤 원인이 궁금해졌다.


위 글의 요지는 아래와 같은 연산 결과가 Visual Studio 2005에선 0이 아니라 1이란 거다.

i=0;
i|=1>>(32-i);

확인해보니, 이 버그는 아래와 같은 특성을 보인다.

1. VC++6.0, VS.Net2003, VS2005에서 똑같이 발생함
2. 오직 디버그 모드에서만 나타남. 릴리즈 모드에선 정상적인 결과인 0을 출력함

상식적으로야 1>>(32-0)은 당근빠따 0인데, 왜 1이라고 구라를 치는가 궁금해져서 컴파일 된 어셈블리 코드를 뽑아봤다.


1. 디버그 모드

; 35   :     int i;
; 36   :     i=0;

    mov    DWORD PTR _i$[ebp], 0

; 37   :     i|=1>>(32-i);

    mov    ecx, 32                    ; 00000020H
    sub    ecx, DWORD PTR _i$[ebp]
    mov    eax, 1
    sar    eax, cl
    or    eax, DWORD PTR _i$[ebp]
    mov    DWORD PTR _i$[ebp], eax

그렇다! 바로 저것이 원인인 것이다. (참 쉽죠잉?)

쉬프트 로테이트 연산엔 총 4가지가 있는데, >>는 이 중에서 보통 sar로 컴파일된다.
그리고, VC에선 속도를 향상시키기 위해 쉬프트 횟수 인자를 cl로 넘긴다.

이게 왜 문제가 되냐면 x86 CPU에선 sar에서 cl을 사용할 때 하위 5비트만 사용하기 때문이다.
인텔에서야 컴파일러 만드는 사람이나 어셈블리 프로그래머가 알아서 하길 바랬지만, 컴파일러 만드는 쪽에서 직무유기를 하면 이런 결과가 나오는 것이다.

참고로, x86 어셈블리에선 32비트 ecx 레지스터의 하위 16비트를 cx라고 한다.
그리고, 다시 이 cx 레지스터를 상위/하위 8비트로 나눠 각각 ch, cl이라고 한다.

그럼 다시 코드로 돌아가보자.
ecx의 값이 32-0 == 32이고, eax의 값이 1인데 sar eax, cl을 했다.
이 때 cl의 값은 물론 32이지만, sar 연산을 할 땐 이 중 하위 5비트만 사용되므로 sar eax, 0이 되는 것이다.

따라서 결과는 1 >> 0 == 1.


2. 릴리즈 모드

앞에서도 얘기했듯이, 이 버그는 오직 디버그 모드에서만 나타난다.
릴리즈 모드에선 얼마나 대단한 수를 써서 안 나오나 쳐다봤다.

; 35   :     int i;
; 36   :     i=0;
; 37   :     i|=1>>(32-i);
; 38   :     printf("%d\n",i);

    push    0
    push    OFFSET FLAT:??_C@_03PMGGPEJJ@?$CFd?6?$AA@
    call    _printf

이런! 컴파일 타임에 계산해주고 끝냈기 때문에 문제가 발생하지 않은 것 뿐이다.

더 확인해보니, 인자를 좀 복잡하게 넘기면 릴리즈 모드에서도 똑같은 문제가 발생한다.
다시 말해 릴리즈 모드가 결코 안전하지 않다는 것이다!


컴파일러 만드는 것이 결코 쉬운 일도 아니고, Visual Studio의 C++ 컴파일러가 결코 듣보잡 수준은 절대로 절대로 아니지만, 이런 기본적인 버그는 아무리 생각해도 짜증난다.
C++ 프로그래머가 C++ 세상에서만 고민하면 되지, 어셈블리 세상까지 와서 고민해야 되겠는가!!

Comment 8