코드를 작성하다 보면 에러가 나는 경우가 있습니다. 이 경우 실시간 디버깅을 하지 않고 run time상에서 실행이 어디까지 되었는지를 알 수 있도록 디버깅용 변수(tag) 하나를 선언해서 이용하는 경우도 있습니다.
int tag = 0; void foo() { int i, j, k; tag = 1; scanf_s("%d", &i); tag = 2; scanf_s("%d", &j); tag = 3; k = i / j; // 예외 발생 가능성이 존재 tag = 4; printf("k=%d\n", k); tag = 5; }
위 foo라는 함수는 2개의 숫자(i, j)을 입력받아서 나눈 값을 k에 넣고 화면에 출력하는 함수입니다. 여기에서 j의 값이 0이라면 예외가 발생하겠고, 이 경우 tag 변수에는 3의 값이 들어 갈 것입니다.
이상한 현상이 발생하여 테스트 코드를 작성해 보았습니다.
#include <stdio.h> volatile int tag = 0; class MyObj { public: virtual void foo() { int i, j, k; tag = 1; scanf_s("%d", &i); tag = 2; scanf_s("%d", &j); tag = 3; k = i / j; tag = 4; printf("k=%d\n", k); tag = 5; } }; int main() { __try { MyObj obj; obj.foo(); } __except (1) { } printf("tag=%d\n", tag); return 0; }
상기 코드를 빌드한 이후 i, j 값을 각각 8과 0을 입력하여 예외가 발생하도록 하였습니다. 그 결과 다음과 같은 결과가 나옵니다. 빌드 환경은 Microsoft Visual Studio 2012 32bit Release Compile - Optimization Maximize Speed (/O2) 입니다.
8 0 tag=2
Debug 모드에서는 정상적으로 "tag=3"으로 출력되는데 Release Mode에서는 tag의 값이 2로 나오더군요. 왜 이런 현상이 나타나는 걸까요?
소스 코드 및 실행 파일 첨부합니다.
참고로 개인적으로 몇년동안 유지 보수하는 VDream 라이브러리가 있고, Core 모듈이 좀 있어서 UnitTest 모듈을 만들어 놨습니다.
UnitTest를 매일 돌려 보는 건 아니구요(귀차니즘), 오랜만에 UnitTest를 돌려 보았는데 이러한 현상이 나타 나서 테스트를 해 보게 되었습니다(아래는 실제 코드).
https://github.com/gilgil1973/vdream90/blob/master/app/unittest/threadtest.cpp
Visual Studio 2005나 Visual Studio 2008에서는 UnitTest fail이 나지 않다가 Visual Studio 2012에 와서 이런 현상이 나타 나네요.
class ExceptionThread : public VThread { protected: virtual void run() { tag = 100; int i = 5; tag = 200; int j = 0; tag = 300; int k = i / j; // occur exception tag = 400; printf("i=%d j=%d k=%d\n", i, j, k); tag = 500; } }; TEST( Thread, exceptionTest ) { ExceptionThread thread; thread.open(); LOG_INFO("Exception will occur. Don't worry. :)"); bool res = thread.wait(); EXPECT_TRUE( res == true ); EXPECT_EQ( thread.tag , 300 ); }
분석에 착오가 있었습니다.
virtual이 아닌 경우 어셈 파일(main_non_virtual.asm)을 다시 분석해 보았습니다. 이 경우 foo 함수는 main 함수에 inline으로 가면서 새로운 코드가 실행이 되는군요.
main_non_virtual.asm파일에서 "tag = 3"으로 검색해 보면 2개가 나오는데, 실제 실행이 될 때에는 처음 검색이 된 코드가 아닌 두번째 검색된 코드(아래 부분)가 실행이 됩니다.
여기에서는 나누기보다 "tag = 3"이 먼저 실행되는 것을 확인할 수 있습니다.
; 11 : tag = 1; mov DWORD PTR ?tag@@3HC, 1 ; tag ; 12 : scanf_s("%d", &i); lea eax, DWORD PTR _i$3[ebp] push eax push OFFSET ??_C@_02DPKJAMEF@?$CFd?$AA@ mov esi, DWORD PTR __imp__scanf_s call esi ; 13 : tag = 2; mov DWORD PTR ?tag@@3HC, 2 ; tag ; 14 : scanf_s("%d", &j); lea eax, DWORD PTR _j$2[ebp] push eax push OFFSET ??_C@_02DPKJAMEF@?$CFd?$AA@ call esi ; 15 : tag = 3; mov DWORD PTR ?tag@@3HC, 3 ; tag ; 16 : k = i / j; mov eax, DWORD PTR _i$3[ebp] cdq idiv DWORD PTR _j$2[ebp] ; 17 : tag = 4; mov DWORD PTR ?tag@@3HC, 4 ; tag ; 18 : printf("k=%d\n", k); push eax push OFFSET ??_C@_05FHGCAJII@k?$DN?$CFd?6?$AA@ mov esi, DWORD PTR __imp__printf call esi add esp, 24 ; 00000018H ; 19 : tag = 5; mov DWORD PTR ?tag@@3HC, 5 ; tag
[결론]
코드 실행와 최적화를 위해서 코드의 실행 순서가 뒤바뀔 수 있다(Compiler dependent한 사항임). 예를 들어
tag = 3; k = i / j;
상기 코드에서 2개의 실행은 순서가 바뀌어 컴파일 될 수 있다.
MS 사이트에 관련 내용을 submit하였습니다.
https://connect.microsoft.com/VisualStudio/feedback/details/832508/variable-assignment-order-bug
foo라는 함수가 virtual 일 때과 그렇지 않을 때 실행 결과가 달라 지는군요. 어셈 코드상으로는 다른 부분이 없는 것 같습니다.
virtual void foo () 일 경우 실행결과 ( main_virtual.asm )
void foo () 일 경우(virtual을 생략함) 실행 결과 ( main_non_virtual.asm )
아래는 어셈 코드입니다(virtual을 붙이던지 안 붙이던지 똑같이 생겼습니다).