try~catch 와  __try~__except 의 차이에 대해 물어보시는 분들이 종종 계셔서 한번 정리해보았습니다.


I. 예외(Exception)을 핸들링하는 세 가지 방법


windows 에서 C/C++로 소프트웨어를 개발할 때 예외(Exception)을 핸들링할 수 있는 방법에는 크게 세가지가 있습니다.


1. __try ~ __except

Windows OS에서 제공하는 예외 처리 방법이며, 일반적으로 SEH (Structured Error Handling)이라고 부릅니다.

Access Violation 과 같이 프로그램 상의 오류(버그)로 인해 발생하는 대부분의 예외를 처리 가능합니다.


예를 들어, 다음과 같은 코드가 있다면

  1. VOID        TryExcept()    
  2. {    
  3.     __try     
  4.     {    
  5.         wprintf(L"__try!!\n");    
  6.         *(PINT)NULL = 0;  
  7.     } __except(EXCEPTION_EXECUTE_HANDLER)    
  8.     {    
  9.         wprintf(L"__except!!\n");    
  10.     }    
  11.   
  12.     wprintf(L"Finish Function!\n");    
  13. }    


실행 결과는 다음과 같이 됩니다.

__try

__except!!

Finish Function!

계속하려면 아무 키나 누르십시오 . . .


__try ~ __except 구문의 가장 큰 단점은 소멸자가 존재하는 클래스와 함께 사용할 수 없다는 점인데요.. 이 부분은 이 포스트의 마지막 부분에서 다시 살펴보겠습니다.



2. try ~ catch

표준 C++에서 제공하는 예외 처리 방법이며, 일반적으로 C++ Error Handing이라고 부릅니다.

try ~ catch 구문은 throw 키워드를 이용해 코드상에서 명시적으로 발생시킨 예외를 핸들링합니다.

try ~ catch 구문은 내부적으로는 __try ~ __except 의 예외처리 구조 (SEH) 를 기반으로 구현되어 있습니다.


예를 들어, 다음과 같은 코드를...

  1. #define     BUFLEN  100  
  2.   
  3. DWORD       ExceptionTest0(WCHAR* pszMessage, DWORD cchMessageBuffer)  
  4. {  
  5.     HANDLE      hFile = INVALID_HANDLE_VALUE;  
  6.     WCHAR*      pszMoreMessage = NULL;  
  7.   
  8.     hFile = CreateFile(L"C:\\temp\\result.txt", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);  
  9.     if (hFile == INVALID_HANDLE_VALUE)  
  10.     {  
  11.         wprintf(L"[ERROR] FILE NOT FOUND\n");  
  12.         return ERROR_FILE_NOT_FOUND;  
  13.     }  
  14.   
  15.     pszMoreMessage = new WCHAR[BUFLEN];  
  16.     memset(pszMoreMessage, NULL, sizeof(WCHAR) * BUFLEN);  
  17.     StringCchCopy(pszMoreMessage, BUFLEN, L"My Name is kuaaan!!");  
  18.   
  19.     if (pszMessage == NULL)  
  20.     {  
  21.         wprintf(L"[ERROR] INVALID PARAMETER\n");  
  22.         CloseHandle(hFile);   
  23.         delete pszMoreMessage;  
  24.         return ERROR_INVALID_PARAMETER;  
  25.     }  
  26.   
  27.     if (cchMessageBuffer < 12)  
  28.     {  
  29.         wprintf(L"[ERROR] INSUFFICIENT BUFFER\n");  
  30.         CloseHandle(hFile);         // 정리코드가 중복됨.  
  31.         delete pszMoreMessage;      // 정리코드가 중복됨.  
  32.         return ERROR_INSUFFICIENT_BUFFER;  
  33.     }  
  34.   
  35.     StringCchPrintf(pszMessage, cchMessageBuffer, L"Hello World~! %s", pszMoreMessage);  
  36.       
  37.     return ERROR_SUCCESS;  
  38. }  


C++ Exception Handling을 이용해 다시 작성하면 다음과 같이 됩니다.

  1. DWORD       ExceptionTest1(WCHAR* pszMessage, DWORD cchMessageBuffer)  
  2. {  
  3.     HANDLE      hFile = INVALID_HANDLE_VALUE;  
  4.     DWORD       dwResult = 0xFFFFFFFF;  
  5.     WCHAR*      pszMoreMessage = NULL;  
  6.       
  7.     try  
  8.     {  
  9.         hFile = CreateFile(L"C:\\temp\\result.txt", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);  
  10.         if (hFile == INVALID_HANDLE_VALUE)  
  11.         {  
  12.             throw ERROR_FILE_NOT_FOUND;  
  13.         }  
  14.   
  15.         pszMoreMessage = new WCHAR[BUFLEN];  
  16.         memset(pszMoreMessage, NULL, sizeof(WCHAR) * BUFLEN);  
  17.         StringCchCopy(pszMoreMessage, BUFLEN, L"My Name is kuaaan!!");  
  18.   
  19.         if (pszMessage == NULL)  
  20.         {  
  21.             throw ERROR_INVALID_PARAMETER;  
  22.         }  
  23.   
  24.         if (cchMessageBuffer < 12)  
  25.         {  
  26.             throw ERROR_INSUFFICIENT_BUFFER;  
  27.         }  
  28.   
  29.         StringCchPrintf(pszMessage, cchMessageBuffer, L"Hello World~! %s", pszMoreMessage);  
  30.         dwResult = ERROR_SUCCESS;  
  31.     } catch (LONG& dwExceptionCode)  
  32.     {  
  33.         wprintf(L"[ERROR] ExceptionCode : %d\n", dwExceptionCode);  
  34.         dwResult = dwExceptionCode;  
  35.     }  
  36.   
  37.     CloseHandle(hFile);   
  38.     delete pszMoreMessage;  
  39.   
  40.     return dwResult;  
  41. }  


아무래도 정리 코드가 중복되지 않고 하다보니.. 좀 더 깔끔해보이죠.

catch에서 일치되는 DataType이 없어 예외가 핸들링되지 않는다면... 프로그램은 최종적으로 예외를 발생시키고 비정상 종료되게 됩니다.


3. Unhandled Exception Filter

이것은... 말 그대로 SEH나 C++ 예외핸들링에 의해 Handling되지 않은 예외를 최종적으로 핸들링할 수 있는 Callback 함수를 등록하는 방법입니다.

다음 포스트를 참고하세요.

http://kuaaan.tistory.com/103


보통 __except 구문이나.. Unhandled Exception Handler에는 프로세스 메모리덤프를 작성하거나, 예외 내용이나 콜스택 등의 정보를 로깅하는 코드가 들어가게 됩니다.


UnhandledExceptionHandler 의 가장 큰 단점은.. 예외를 통지받을 수는 있지만 핸들링한 후에 (예외를 발생시킨 코드를  skip하고) 계속해서 실행시키기는 어렵다는 점입니다. 보통은... 메모리덤프나 로그를 작성한 다음 프로세스를 조용히 종료시키거나 재시작시키는 등의 용도로 사용합니다.



IItry~catch 을 __try~__except 처럼 사용하기

그렇다면... C++의 예외 핸들링 (try~catch)으로 Accecc Violation 같은 종류의 예외는 핸들링할 수 없는 걸까요??


다음과 같이 Virual Studio의 컴파일 옵션에서 설정할 수 있습니다.

C/C++ > Code Generation > Enable C++ Exceptions



각 항목의 설정에 따라 바이너리의 동작은 다음과 같이 달라집니다.


Compile Option try~catch 에서 일반 예외 (SEH) 처리
Yes with SEH Exceptions (/EHa) 가능
Yes (/Ehsc, /EHs) 불가 (==> Visual Studio 2010부터 Default 설정)
No 가능


즉, /EHa 로 설정하면 try ~ catch 블럭에서도 SEH와 같은 에러 핸들링이 가능합니다.

샘플을 보면 다음과 같습니다.

  1. VOID        DoException()     
  2. {       
  3.     wprintf(L"before exception\n");  
  4.   
  5.     *(PINT)NULL = 0;    
  6.   
  7.     wprintf(L"after exception\n");  
  8. }    
  9.   
  10. VOID        TryCatch()    
  11. {  
  12.     try   
  13.     {  
  14.         wprintf(L"try\n");  
  15.           
  16.         DoException();    
  17.   
  18.         wprintf(L"After Exception\n");  
  19.     } catch(...)  
  20.     {  
  21.         wprintf(L"catch!!\n");  
  22.     }  
  23. }  


위의 코드를 "/EHa" 옵션으로 빌드하여 실행시키면 다음과 같이 됩니다.

try

before exception

catch!!

계속하려면 아무 키나 누르십시오 . . .


이걸 "/EHsc" 옵션으로 빌드하여 실행시키면... 이렇게 되죠.



^^;;;;


III. __try ~ __except 구문과 소멸자를 가진 클래스를 함께 사용하기

반면에 __try 구문의 가장 큰 단점은... __try를 사용하는 함수에서는 소멸자가 정의된 클래스를 사용할 수 없다는 점입니다.

예를 들어 다음과 같은 코드는 빌드 오류를 발생시킵니다.

  1. class CDummy  
  2. {  
  3. public:  
  4.     ~CDummy()  
  5.     {  
  6.         wprintf(L"~Dummy!\n");  
  7.     }  
  8. };  
  9.   
  10. VOID        TryExcept()    
  11. {    
  12.     CDummy Dummy;  // 빌드 오류!!  
  13.   
  14.     __try     
  15.     {    
  16.     } __except(EXCEPTION_EXECUTE_HANDLER)    
  17.     {    
  18.     }    
  19. }    


1>------ Build started: Project: TestProject, Configuration: Release Win32 ------

1>Build started 2014-08-11 오후 2:44:19.

1>TestProject.cpp(98): warning C4509: nonstandard extension used: 'TryExcept' uses SEH and 'Dummy' has destructor

1>          TestProject.cpp(85) : see declaration of 'Dummy'

1>TestProject.cpp(98): error C2712: Cannot use __try in functions that require object unwinding

1>

1>Time Elapsed 00:00:00.57

========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========


그래서 저처럼 소멸자를 이용해 장난치는 걸 좋아하는 개발자들은... __try를 그닥 좋아하지 않습니다. ^^;;;


그런데 SEH 관련 빌드 옵션을 정리한 다음 테이블을 보시면... /EH 옵션을 주지 않을 경우 위의 코드가 빌드될 수 있다는 것을 알 수 있습니다. 대신... 예외가 발생할 경우 소멸자 호출이 되지 않는다는군요... 


Compile Option__try ~ __except 와 소멸자를 가진 객체를 함께 사용
Yes with SEH Exceptions (/EHa)불가 (빌드 오류)
Yes (/Ehsc, /EHs)불가 (빌드 오류)
No

가능 (대신, 소멸자 호출이 보장되지 않음 )
Compile warning 발생


예외 발생시 핸들링을 하는 대신 소멸자 호출을 포기할지는 개발자 개개인의 선호도에 따라 선택할 문제겠지만... 

만약 저 소멸자 내에 메모리를 해제하는 코드가 들어있었다면?? 그냥 메모리 릭이 좀 발생하고 말았을 것입니다. 그런데 소멸자 내에 CriticalSection을 Unlock하는 코드가 들어있었다면....?? 그렇다면 예외 발생이 결과적으로 Dead Lock으로 귀결될 수도 있는 일입니다. 이건 받아들이기 어렵죠. ^^;;


대신 다음과 같은 식으로는 가능합니다. (모든 스레드에 대해 아래와 같은 방식으로 사용)

  1. class CDummy  
  2. {  
  3. public:  
  4.     CDummy()  
  5.     {  
  6.         wprintf(L"Dummy!\n");  
  7.     }  
  8.   
  9.     ~CDummy()  
  10.     {  
  11.         wprintf(L"~Dummy!\n");  
  12.     }  
  13. };  
  14.   
  15. VOID        StartThread()  
  16. {  
  17.     CDummy Dummy;  
  18.   
  19.     wprintf(L"Use Dummy Class!!\n");  
  20.   
  21.     *(PINT)NULL = 0;  
  22. }  
  23.   
  24. VOID        TryExcept()    
  25. {    
  26.     __try     
  27.     {    
  28.         StartThread();    
  29.     } __except(EXCEPTION_EXECUTE_HANDLER)    
  30.     {    
  31.         wprintf(L"__except!!\n");    
  32.     }    
  33.   
  34.     wprintf(L"Finish Function!\n");    
  35. }    


위와 같이 코드를 작성하여 "/EHa" 옵션으로 빌드하면... 빌드도 가능하고 다음과 같이 동작도 정확하게 이루어집니다. 

( 뭐 어차피 /EHa 옵션을 주고 빌드할 거라면 try ~ catch(...)을 사용하면 되니 굳이 __try~__except를 사용할 필요는 없겠지만요... 굳이 사용한다면... 그렇다는 겁니다.. ㅎㅎ )


Dummy!

Use Dummy Class!!

~Dummy!

__except!!

Finish Function!

계속하려면 아무 키나 누르십시오 . . .


(참고로... 위의 코드를 /EHsc 옵션으로 빌드하면 다음과 같이 소멸자가 정상적으로 호출되지 않습니다.)

before exception

Dummy!

__except!!

Finish Function!

계속하려면 아무 키나 누르십시오 . . .



즉.. 특별한 이유가 없다면 그냥 /EHa 를 쓰면 된다는 거죠.

그렇다면 VisualStudio 2010은 왜 디폴트 설정이 /EHa 가 아닌 /EHsc일까요? 제 생각엔 이것은 아마 성능과 관련된 이유 때문일 것 같습니다.


요약하자면.... 만약 다음과 같은 기능들을 원한다면.... 

  1. 복수의 워커스레드로 구성된 스레드풀을 사용하면서,
  2. 소멸자가 있는 클래스(예 : 스마트포인터, 자동 CriticalSection 등)를 자유롭게 사용하고, 
  3. 워커스레드에서 예외 발생시 메모리 덤프를 작성하는 등의 예외 핸들링을 수행.
  4. 예외 핸들링 후 프로세스를 종료하지 않고 새로운 Job을 계속 실행
아래과 같이 코드를 작성한 후 "/EHa" 옵션을 주어 빌드하면 되겠군요. ^^

  1. VOID        StartWork()     
  2. {       
  3.     wprintf(L"Begin WorkThread\n");  
  4.   
  5.     // ToDo...  
  6.   
  7.     wprintf(L"End WorkThread\n");  
  8. }    
  9.   
  10. UINT    CALLBACK        WorkThreadEntryFunc(PVOID pParam)    
  11. {    
  12.     while (TRUE)  
  13.     {  
  14.         __try     
  15.         {    
  16.             StartWork();    
  17.         } __except(EXCEPTION_EXECUTE_HANDLER)    
  18.         {    
  19.             wprintf(L"Exception!!\n");    
  20.             MyWriteProcessMemoryDump();  
  21.         }    
  22.     }  
  23.   
  24.     wprintf(L"Finish Function!\n");    
  25.     return 0;  
  26. }    
 
뭐 대충 아래와 같이 try~catch를 사용해도 큰 차이는 없을 것 같습니다. 
  1. VOID        StartWork()     
  2. {       
  3.     wprintf(L"Begin WorkThread\n");  
  4.   
  5.     // ToDo...  
  6.   
  7.     wprintf(L"End WorkThread\n");  
  8. }    
  9.   
  10. UINT    CALLBACK        WorkThreadEntryFunc(PVOID pParam)    
  11. {    
  12.     while (TRUE)  
  13.     {  
  14.         try     
  15.         {    
  16.             StartWork();    
  17.         } catch (...)   
  18.         {    
  19.             wprintf(L"Exception!!\n");    
  20.             MyWriteProcessMemoryDump();  
  21.         }    
  22.     }  
  23.   
  24.     wprintf(L"Finish Function!\n");    
  25.     return 0;  
  26. }    



IV. 일반적인 예외(SEH)를 C++ 예외로 변환하기 (_set_se_translator)

음... 위의 코드가 다 좋긴 한데요... catch(...) 내에서는 예외 정보 (예외코드나 컨텍스트 정보 같은거...)를 얻을수 없다는 아쉬움이 있죠. 왜냐하면... GetExceptionCode() 나 GetExceptionInformation() 와 같은 api들을 catch블럭 안에서 사용할 수 없기 때문이죠. ^^;;


그래서... SEH 예외가 발생했을 때 이것을 C++ 예외로 변환하는 방법이 있습니다. 

Handles Win32 exceptions (C structured exceptions) as C++ typed exceptions.

일단 저기다가... 콜백을 등록해 놓으면 __try~__except 에 예외가 탐지되었을 때 콜백이 옵니다. 그럼 그 콜백 안에서 적당히 C++ 예외를 생성해서 다시 throw해주면 되는 거죠... ^^


아래는 샘플코드입니다. (msdn에 있는 샘플코드를 제가 약간 수정했습니다... 그 샘플코드가 약간... 1@#$!@#%해서요. ^^)

  1. #include <stdio.h>  
  2. #include <windows.h>  
  3. #include <eh.h>  
  4.   
  5. void SEFunc();  
  6. void trans_func( unsigned int, EXCEPTION_POINTERS* );  
  7.   
  8. class SE_Exception  
  9. {  
  10. private:  
  11.     unsigned int nSE;  
  12.     PEXCEPTION_POINTERS     m_pExceptionPointers;  
  13. public:  
  14.     SE_Exception() {}  
  15.     SE_Exception( unsigned int n , PEXCEPTION_POINTERS      pExceptionPointers) : nSE( n ),  m_pExceptionPointers ( pExceptionPointers ) {}  
  16.     ~SE_Exception() {}  
  17.     unsigned int getSeNumber() { return nSE; }  
  18.     PEXCEPTION_POINTERS getExceptionPointers() { return m_pExceptionPointers; }  
  19. };  
  20.   
  21. VOID        Test()  
  22. {  
  23.     try  
  24.     {  
  25.         SEFunc();  
  26.     }  
  27.     catch( SE_Exception e )  
  28.     {  
  29.         printf( "Caught a __try exception with SE_Exception. ErrorCode : %x, Eip : %p\n", e.getSeNumber(), e.getExceptionPointers()->ContextRecord->Eip);  
  30.     }  
  31. }  
  32.   
  33. int main( void )  
  34. {  
  35.     _set_se_translator( trans_func );  
  36.       
  37.     while(TRUE)  
  38.     {  
  39.         __try  
  40.         {  
  41.             Test();  
  42.         }  
  43.         __finally  
  44.         {  
  45.             printf( "In finally\n" );  
  46.         }  
  47.   
  48.         Sleep(1000);  
  49.     }  
  50. }  
  51.   
  52. void SEFunc()  
  53. {  
  54.     int x, y=0;  
  55.     x = 5 / y;  
  56. }  
  57.   
  58. void trans_func( unsigned int u, EXCEPTION_POINTERS* pExp )  
  59. {  
  60.     printf( "In trans_func. : Exception is detected!!! (ExceptionCode %x)\n", u );  
  61.     throw SE_Exception(u, pExp);  
  62. }  


In trans_func. : Exception is detected!!! (ExceptionCode c0000094)

Caught a __try exception with SE_Exception. ErrorCode : c0000094, Eip : 001E1E63

In finally


In trans_func. : Exception is detected!!! (ExceptionCode c0000094)

Caught a __try exception with SE_Exception. ErrorCode : c0000094, Eip : 001E1E63

In finally


In trans_func. : Exception is detected!!! (ExceptionCode c0000094)

Caught a __try exception with SE_Exception. ErrorCode : c0000094, Eip : 001E1E63

In finally





Posted by kuaaan
,


사랑합니다. 편안히 잠드소서