try~catch 와 __try~__except 의 차이에 대해 물어보시는 분들이 종종 계셔서 한번 정리해보았습니다.
I. 예외(Exception)을 핸들링하는 세 가지 방법
windows 에서 C/C++로 소프트웨어를 개발할 때 예외(Exception)을 핸들링할 수 있는 방법에는 크게 세가지가 있습니다.
1. __try ~ __except
Windows OS에서 제공하는 예외 처리 방법이며, 일반적으로 SEH (Structured Error Handling)이라고 부릅니다.
Access Violation 과 같이 프로그램 상의 오류(버그)로 인해 발생하는 대부분의 예외를 처리 가능합니다.
예를 들어, 다음과 같은 코드가 있다면
실행 결과는 다음과 같이 됩니다.
__try
__except!!
Finish Function!
계속하려면 아무 키나 누르십시오 . . .
__try ~ __except 구문의 가장 큰 단점은 소멸자가 존재하는 클래스와 함께 사용할 수 없다는 점인데요.. 이 부분은 이 포스트의 마지막 부분에서 다시 살펴보겠습니다.
2. try ~ catch
표준 C++에서 제공하는 예외 처리 방법이며, 일반적으로 C++ Error Handing이라고 부릅니다.
try ~ catch 구문은 throw 키워드를 이용해 코드상에서 명시적으로 발생시킨 예외를 핸들링합니다.
try ~ catch 구문은 내부적으로는 __try ~ __except 의 예외처리 구조 (SEH) 를 기반으로 구현되어 있습니다.
예를 들어, 다음과 같은 코드를...
C++ Exception Handling을 이용해 다시 작성하면 다음과 같이 됩니다.
아무래도 정리 코드가 중복되지 않고 하다보니.. 좀 더 깔끔해보이죠.
catch에서 일치되는 DataType이 없어 예외가 핸들링되지 않는다면... 프로그램은 최종적으로 예외를 발생시키고 비정상 종료되게 됩니다.
3. Unhandled Exception Filter
이것은... 말 그대로 SEH나 C++ 예외핸들링에 의해 Handling되지 않은 예외를 최종적으로 핸들링할 수 있는 Callback 함수를 등록하는 방법입니다.
다음 포스트를 참고하세요.
보통 __except 구문이나.. Unhandled Exception Handler에는 프로세스 메모리덤프를 작성하거나, 예외 내용이나 콜스택 등의 정보를 로깅하는 코드가 들어가게 됩니다.
UnhandledExceptionHandler 의 가장 큰 단점은.. 예외를 통지받을 수는 있지만 핸들링한 후에 (예외를 발생시킨 코드를 skip하고) 계속해서 실행시키기는 어렵다는 점입니다. 보통은... 메모리덤프나 로그를 작성한 다음 프로세스를 조용히 종료시키거나 재시작시키는 등의 용도로 사용합니다.
II. try~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와 같은 에러 핸들링이 가능합니다.
샘플을 보면 다음과 같습니다.
위의 코드를 "/EHa" 옵션으로 빌드하여 실행시키면 다음과 같이 됩니다.
try
before exception
catch!!
계속하려면 아무 키나 누르십시오 . . .
이걸 "/EHsc" 옵션으로 빌드하여 실행시키면... 이렇게 되죠.
예를 들어 다음과 같은 코드는 빌드 오류를 발생시킵니다.
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 | 가능 (대신, 소멸자 호출이 보장되지 않음 ) |
예외 발생시 핸들링을 하는 대신 소멸자 호출을 포기할지는 개발자 개개인의 선호도에 따라 선택할 문제겠지만...
만약 저 소멸자 내에 메모리를 해제하는 코드가 들어있었다면?? 그냥 메모리 릭이 좀 발생하고 말았을 것입니다. 그런데 소멸자 내에 CriticalSection을 Unlock하는 코드가 들어있었다면....?? 그렇다면 예외 발생이 결과적으로 Dead Lock으로 귀결될 수도 있는 일입니다. 이건 받아들이기 어렵죠. ^^;;
대신 다음과 같은 식으로는 가능합니다. (모든 스레드에 대해 아래와 같은 방식으로 사용)
위와 같이 코드를 작성하여 "/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일까요? 제 생각엔 이것은 아마 성능과 관련된 이유 때문일 것 같습니다.
요약하자면.... 만약 다음과 같은 기능들을 원한다면....
- 복수의 워커스레드로 구성된 스레드풀을 사용하면서,
- 소멸자가 있는 클래스(예 : 스마트포인터, 자동 CriticalSection 등)를 자유롭게 사용하고,
- 워커스레드에서 예외 발생시 메모리 덤프를 작성하는 등의 예외 핸들링을 수행.
- 예외 핸들링 후 프로세스를 종료하지 않고 새로운 Job을 계속 실행
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@#$!@#%해서요. ^^)
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
'C++ > Debug' 카테고리의 다른 글
x64 디버깅 강좌 (1) - x64 Stack 개요 (4) | 2015.08.02 |
---|---|
windbg의 !locks 확장 명령이 동작하지 않는 경우... (0) | 2014.10.06 |
WinDbg 교육자료 (6) | 2014.03.04 |
Debugging Tips (14) 64비트 OS에서 작성된 메모리덤프에서 32비트(Wow64) 프로세스 분석하기 (0) | 2013.09.16 |
Heap32Next 와 RtlAllocateHeap을 동시에 호출하면 DeadLock이 발생함 (2) | 2013.09.10 |