글수 69
구조적 예외 처리, SEH란게... 어디선가 많이 들어보긴 했어도 뭐하는 건지는 잘 모르겠더라구요.
그래서 본격적으로 연구 좀 해봤습니다.
다행히 MSJ에 착한 문서가 있어서 많이 도움이 되었습니다.
먼저 SEH이란 뭘까요?
여러분께서 잘 아시는 C++의 예외 처리 기능도 SEH의 한 가지입니다.
어디선가 throw 하고, 어디선가 catch해서 처리해주는 거죠.
간단히 말해 예외가 발생할 가능성이 있는 코드와 예외 처리부가 따로 분리되어 있는, 그런 (문법적으로 혹은 논리적으로) 구조적인 처리기를 가지고 있는 것을 대략 SEH라고 합니다.
이 때, 순수하게 C++ 예외가 발생하는 경우엔 문제가 없을 지도 모릅니다.
이미 C++ 표준에 try..catch가 있으니까요...
그런데 예외에는 C++ 예외만 있는 것이 아닙니다.
그 중 가장 유명한 것이 Win32 예외 중 Access Violation이나 Integer Divide by Zero 같은 것들이겠지요.
이러한 예외들도 잡아낼 수 있습니다.
가끔 어떤 프로그램을 보면 그런 오류를 잡아내서 오류 보고와 비슷한 창을 띄운다거나, 아니면 윈도 기본 예외 오류창이 아닌 자체적인 예외 오류 창을 띄워서 죽여(?)주지요.
그것도 아니면 게임 같은 경우엔 오류란 말 없이 그냥 소리 없이 꺼지도록 해줄 수도 있습니다.
일단 먼저 이러한 예외를 잡아내는 방법을 살펴봅시다.
아래 구문은 (아마도) Visual C++에서만 지원하는 컴파일러 확장 구문입니다.
[code]
// compile with /EHa
#include <stdio.h>
#include <windows.h>
int filter(EXCEPTION_POINTERS *ep)
{
// Access Violation이면 처리기로 이동하고,
// 그 외의 경우면 다른 처리기를 찾습니다(예외를 rethrow합니다.).
if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
printf("Access Violation: %s on memory 0x%p at 0x%p\n",
(ep->ExceptionRecord->ExceptionInformation[0] ? "write" : "read"),
ep->ExceptionRecord->ExceptionInformation[1],
ep->ExceptionRecord->ExceptionAddress);
return EXCEPTION_EXECUTE_HANDLER;
}
else
return EXCEPTION_CONTINUE_SEARCH;
}
void main()
{
__try {
int *p;
printf("작업을 시작합니다.\n");
p = 0;
*p = 30; // 여기서 반드시 뻗는다.
}
__except (filter(GetExceptionInformation()))
{
printf("예외가 발생했습니다.\n");
}
printf("작업이 끝났습니다.\n");
}
[/code]
헐렁헐렁한 코드인데 이해하시는데는 큰 문제가 없을 것입니다.
먼저 __try..__except 라는 SEH 처리 구문을 썼습니다.
사용 방법은 대체적으로 C++의 try..catch와 비슷합니다만, 항상 __except는 하나만 나와야 합니다.
그리고 괄호 부분은 필터 함수가 들어갈 부분입니다.
괄호 안은 문장이 아니라 식인데, 여기에 무슨 값이 들어있는가에 따라 동작이 달라집니다. (그래서 filter 함수의 반환값이 void가 아니라 int입니다.)
위에서처럼 EXCEPTION_EXECUTE_HANDLER를 반환하면 예외 처리기쪽 (__except의 블럭 부분)으로 제어를 옮겨서 거기를 실행합니다.
EXCEPTION_CONTINUE_SEARCH를 하면 예외를 처리하지 않고 다른 예외 처리기를 찾습니다.
try..catch와의 주요한 차이점은, catch 블럭이 여러 개 나와서 원하는 여러 종류의 예외들을 잡아낼 수 있는데 반해, __try..__except는 __except가 하나밖에 없으므로 일단 필터 함수에서 원하는 예외 종류를 골라내서 처리할지 말지를 결정하는 구조입니다.
물론 C++ 예외와 마찬가지로 Win32 예외도 스택 되감기 같은 것을 지원합니다.
둘의 큰 차이점은, C++ 예외는 언제 던져지는지 예상이 가능하지만, Win32 예외는 그렇지 않다는 것입니다.
아무리 프로그램을 잘 만들어도 언제 잘못된 메모리에 접근할지 알기 힘들고, 언제 0으로 나눌지 엄밀히 assertion하지 않는 이상 모르는 일이죠.
throw의 경우 문서화도 가능하고 한정 구문도 있어서 알기 쉽지만 Win32 예외는 그렇지 않습니다.
__try..__except 외에도 __try..__finally 란 것도 지원합니다.
둘은 항상 짝이 있으므로, 이런 식으로 섞어 써야 합니다.
[code]
// compile with /EHa
#include <stdio.h>
void main()
{
__try {
__try {
int i = 0;
printf("작업을 시작합니다.\n");
i = 30 / i; // 여기서 반드시 뻗는다.
}
__finally {
printf("이곳은 반드시 실행됩니다.\n");
}
}
__except (EXCEPTION_EXECUTE_HANDLER) // 무조건 예외 처리
{
printf("예외가 발생했습니다.\n");
}
printf("작업이 끝났습니다.\n");
}
[/code]
이런 식으로, Win32 예외를 받아 처리할 수 있습니다.
그런데, C++ 예외도 실은 Win32 예외와 똑같은 방식으로 처리합니다.
그러나 마치 malloc과 new의 관계처럼, 기능이 비슷하더라도 반드시 짝이 맞는 것들을 써야 하듯이, C++의 try와 __try는 같은 함수에서 같이 쓸 수 없습니다.
SEH을 위해서 스택 프레임에 기반을 둔 처리기를 사용하는데, 함수 안에서 try나 __try가 나오면 Visual C++가 함수 프롤로그 코드에 SEH을 설치하기 때문에 그렇습니다. 둘은 동작은 비슷하지만 호환되지는 않습니다.
따라서 try를 한 함수는 SEH의 오버헤드가 곁들어지고(?), 스택 프레임을 반드시 써야 하기 때문에 인라인이 먹히지 않습니다.
try가 나오는 순간 함수 프롤로그에 SEH가 설치되며, throw 된 정보에서 Type-information을 가지고 있다가 일치하는 모든 catch 블럭의 테이블을 검색해서 적절한 곳으로 점프하기 위해 약간 시간이 소요됩니다. 적어도 VC++ 구현은 그렇습니다. (델파이의 경우를 보면 매 try 블럭이 나올 때마다 블럭 단위로 새로 SEH를 설치하더군요...)
어쨌든 예외가 함수 단위로 처리되므로, throw 지정자가 가능한 것도 말이 된다고 할 수 있습니다.
throw 지정자가 사용되면 마치 함수 본문 전체를 try 블럭으로 감싼 것과 같고, 그리고 지정한 타입의 catch를 각각 만들어 예외가 발생하면 기본 처리(그냥 죽어버리는)를 하는 것입니다.
물론 VC++ 2005에서도 끝내 구현을 안 한 부분이지만요.
쓸데없이 길어지고 있습니다만 핵심은 이것입니다.
1. C++ 예외처럼 Win32 예외도 구조적으로 처리가 가능하다.
2. 그리고 처리하는 방법이 원천적으로 같다.
3. try와 __try는 같이 쓸 수 없다.
3번을 보면 아주 불만스럽지요.
저 둘을 한 번에 잡을 수는 없을까요?
있습니다. 2번에 나온 대로, 둘을 처리하는 방법이 같으므로 (적어도 VC++ 에서는!) 한 가지만 써도 둘 다 처리할 수가 있습니다.
그럼 먼저 __try를 써서 둘 다 처리해봅시다.
[code]
// compile with /EHa
#include <stdio.h>
#include <windows.h>
#define CPP_EXCEPTION_MAGIC (0xe06d7363)
void main()
{
DWORD dwException;
__try {
printf("작업을 시작합니다.\n");
// 죽어봅시다.
throw 1;
printf("이 메시지는 안 보입니다.\n");
}
__except (dwException = GetExceptionCode(), EXCEPTION_EXECUTE_HANDLER)
{
if (dwException == CPP_EXCEPTION_MAGIC)
printf("C++ 예외가 throw 되었습니다.\n");
else
printf("예외가 발생했습니다.\n");
}
printf("작업이 끝났습니다.\n");
}
[/code]
VC++의 경우에는 항상 C++ 예외를 저기 define한 코드로 보냅니다.
0xe06d7363에서 뒤 3바이트(0x6d7363)는 문자열로 "msc"라네요.
하여간, 근본적으로 C++ 예외도 예외이므로 이렇게 잡을 수 있습니다.
하지만 GetExceptionInformation을 이용해서 내부적으로 같이 들어온 C++ 예외에 대한 정보(개체의 타입 정보 같은 것)을 꺼내와야 하는데 문서화되어 있지 않아 그럴 수가 없지요.
이런 경우 응당 try를 써야 합니다.
[code]
// compile with /EHa
#include <stdio.h>
void main()
{
try {
printf("작업을 시작합니다.\n");
// 죽어봅시다.
*(int *)0 = 0;
printf("이 메시지는 안 보입니다.\n");
}
catch (...)
{
printf("아무 예외나 다 잡았습니다.\n");
}
printf("작업이 끝났습니다.\n");
}
[/code]
아무거나 다 잡는 catch(...)를 쓰면 C++ 예외가 아니더라도 잡을 수 있습니다.
하지만 이 경우 Win32 예외인지 C++ 예외인지 구분할 방법조차 없습니다.
프로그램이 멋대로 죽는 것만 막아줄 뿐이죠.
그럴 때를 위해 다음과 같은 함수를 제공합니다.
_set_se_translator
이 함수는 다음과 같이 사용합니다.
[code]
// compile with /EHa
#include <stdio.h>
#include <eh.h>
#include <windows.h>
class Win32Exception {
public:
Win32Exception(DWORD dwCode): m_Code(dwCode) { }
DWORD GetCode() { return m_Code; }
private:
DWORD m_Code;
};
void translatorFunction(UINT uCode, EXCEPTION_POINTERS *ep)
{
// throw로 발생한게 아닌 예외가 발생하면 이 함수가 호출됩니다.
// 여기서 throw 해서 C++ 예외로 바꿀 수 있습니다.
// 따라서 이 함수의 반환 값은 의미 없으므로 void입니다.
throw Win32Exception(uCode);
}
void main()
{
// 이 함수가 핵심입니다.
_set_se_translator(translatorFunction);
try {
printf("작업을 시작합니다.\n");
// 죽어봅시다.
*(int *)0 = 0;
printf("이 메시지는 안 보입니다.\n");
}
catch (Win32Exception &e)
{
printf("Win32 예외(0x%X)를 잡았습니다.\n", e.GetCode());
}
catch (...)
{
printf("나머지 C++ 예외를 다 잡았습니다.\n");
}
printf("작업이 끝났습니다.\n");
}
[/code]
translator란 이름에서 알 수 있듯이, C++ 예외가 아닌 예외를 C++ 예외로 바꿔주는데 쓸 수 있는 함수입니다.
이것 말고도 API 중에 SetUnhandledExceptionFilter 같은, 비슷한 역할의 함수도 있습니다.
닷넷에도 비슷한 기능이 있습니다. (처리기를 지정하는건 아니지만 처리 방법을 지정할 수 있네요.)
이러한 함수들을 자주 쓸 일을 없겠지만, 가끔 다른 프로그램들이 이러한 예외도 직접 잘 처리하는 것을 보면 부럽기도 하지요.
기왕 죽는 판에 기분 나쁜 Windows 오류 보고 창보다는 직접 만든 안내창이라도 보여주는 것도 괜찮지 않을까요?
여튼 이 글이 쓸모 있었으면 좋겠네요. 잘못된 부분이나 이상한 부분 있으면 지적 부탁드립니다.
그래서 본격적으로 연구 좀 해봤습니다.
다행히 MSJ에 착한 문서가 있어서 많이 도움이 되었습니다.
먼저 SEH이란 뭘까요?
여러분께서 잘 아시는 C++의 예외 처리 기능도 SEH의 한 가지입니다.
어디선가 throw 하고, 어디선가 catch해서 처리해주는 거죠.
간단히 말해 예외가 발생할 가능성이 있는 코드와 예외 처리부가 따로 분리되어 있는, 그런 (문법적으로 혹은 논리적으로) 구조적인 처리기를 가지고 있는 것을 대략 SEH라고 합니다.
이 때, 순수하게 C++ 예외가 발생하는 경우엔 문제가 없을 지도 모릅니다.
이미 C++ 표준에 try..catch가 있으니까요...
그런데 예외에는 C++ 예외만 있는 것이 아닙니다.
그 중 가장 유명한 것이 Win32 예외 중 Access Violation이나 Integer Divide by Zero 같은 것들이겠지요.
이러한 예외들도 잡아낼 수 있습니다.
가끔 어떤 프로그램을 보면 그런 오류를 잡아내서 오류 보고와 비슷한 창을 띄운다거나, 아니면 윈도 기본 예외 오류창이 아닌 자체적인 예외 오류 창을 띄워서 죽여(?)주지요.
그것도 아니면 게임 같은 경우엔 오류란 말 없이 그냥 소리 없이 꺼지도록 해줄 수도 있습니다.
일단 먼저 이러한 예외를 잡아내는 방법을 살펴봅시다.
아래 구문은 (아마도) Visual C++에서만 지원하는 컴파일러 확장 구문입니다.
[code]
// compile with /EHa
#include <stdio.h>
#include <windows.h>
int filter(EXCEPTION_POINTERS *ep)
{
// Access Violation이면 처리기로 이동하고,
// 그 외의 경우면 다른 처리기를 찾습니다(예외를 rethrow합니다.).
if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
printf("Access Violation: %s on memory 0x%p at 0x%p\n",
(ep->ExceptionRecord->ExceptionInformation[0] ? "write" : "read"),
ep->ExceptionRecord->ExceptionInformation[1],
ep->ExceptionRecord->ExceptionAddress);
return EXCEPTION_EXECUTE_HANDLER;
}
else
return EXCEPTION_CONTINUE_SEARCH;
}
void main()
{
__try {
int *p;
printf("작업을 시작합니다.\n");
p = 0;
*p = 30; // 여기서 반드시 뻗는다.
}
__except (filter(GetExceptionInformation()))
{
printf("예외가 발생했습니다.\n");
}
printf("작업이 끝났습니다.\n");
}
[/code]
헐렁헐렁한 코드인데 이해하시는데는 큰 문제가 없을 것입니다.
먼저 __try..__except 라는 SEH 처리 구문을 썼습니다.
사용 방법은 대체적으로 C++의 try..catch와 비슷합니다만, 항상 __except는 하나만 나와야 합니다.
그리고 괄호 부분은 필터 함수가 들어갈 부분입니다.
괄호 안은 문장이 아니라 식인데, 여기에 무슨 값이 들어있는가에 따라 동작이 달라집니다. (그래서 filter 함수의 반환값이 void가 아니라 int입니다.)
위에서처럼 EXCEPTION_EXECUTE_HANDLER를 반환하면 예외 처리기쪽 (__except의 블럭 부분)으로 제어를 옮겨서 거기를 실행합니다.
EXCEPTION_CONTINUE_SEARCH를 하면 예외를 처리하지 않고 다른 예외 처리기를 찾습니다.
try..catch와의 주요한 차이점은, catch 블럭이 여러 개 나와서 원하는 여러 종류의 예외들을 잡아낼 수 있는데 반해, __try..__except는 __except가 하나밖에 없으므로 일단 필터 함수에서 원하는 예외 종류를 골라내서 처리할지 말지를 결정하는 구조입니다.
물론 C++ 예외와 마찬가지로 Win32 예외도 스택 되감기 같은 것을 지원합니다.
둘의 큰 차이점은, C++ 예외는 언제 던져지는지 예상이 가능하지만, Win32 예외는 그렇지 않다는 것입니다.
아무리 프로그램을 잘 만들어도 언제 잘못된 메모리에 접근할지 알기 힘들고, 언제 0으로 나눌지 엄밀히 assertion하지 않는 이상 모르는 일이죠.
throw의 경우 문서화도 가능하고 한정 구문도 있어서 알기 쉽지만 Win32 예외는 그렇지 않습니다.
__try..__except 외에도 __try..__finally 란 것도 지원합니다.
둘은 항상 짝이 있으므로, 이런 식으로 섞어 써야 합니다.
[code]
// compile with /EHa
#include <stdio.h>
void main()
{
__try {
__try {
int i = 0;
printf("작업을 시작합니다.\n");
i = 30 / i; // 여기서 반드시 뻗는다.
}
__finally {
printf("이곳은 반드시 실행됩니다.\n");
}
}
__except (EXCEPTION_EXECUTE_HANDLER) // 무조건 예외 처리
{
printf("예외가 발생했습니다.\n");
}
printf("작업이 끝났습니다.\n");
}
[/code]
이런 식으로, Win32 예외를 받아 처리할 수 있습니다.
그런데, C++ 예외도 실은 Win32 예외와 똑같은 방식으로 처리합니다.
그러나 마치 malloc과 new의 관계처럼, 기능이 비슷하더라도 반드시 짝이 맞는 것들을 써야 하듯이, C++의 try와 __try는 같은 함수에서 같이 쓸 수 없습니다.
SEH을 위해서 스택 프레임에 기반을 둔 처리기를 사용하는데, 함수 안에서 try나 __try가 나오면 Visual C++가 함수 프롤로그 코드에 SEH을 설치하기 때문에 그렇습니다. 둘은 동작은 비슷하지만 호환되지는 않습니다.
따라서 try를 한 함수는 SEH의 오버헤드가 곁들어지고(?), 스택 프레임을 반드시 써야 하기 때문에 인라인이 먹히지 않습니다.
try가 나오는 순간 함수 프롤로그에 SEH가 설치되며, throw 된 정보에서 Type-information을 가지고 있다가 일치하는 모든 catch 블럭의 테이블을 검색해서 적절한 곳으로 점프하기 위해 약간 시간이 소요됩니다. 적어도 VC++ 구현은 그렇습니다. (델파이의 경우를 보면 매 try 블럭이 나올 때마다 블럭 단위로 새로 SEH를 설치하더군요...)
어쨌든 예외가 함수 단위로 처리되므로, throw 지정자가 가능한 것도 말이 된다고 할 수 있습니다.
throw 지정자가 사용되면 마치 함수 본문 전체를 try 블럭으로 감싼 것과 같고, 그리고 지정한 타입의 catch를 각각 만들어 예외가 발생하면 기본 처리(그냥 죽어버리는)를 하는 것입니다.
물론 VC++ 2005에서도 끝내 구현을 안 한 부분이지만요.
쓸데없이 길어지고 있습니다만 핵심은 이것입니다.
1. C++ 예외처럼 Win32 예외도 구조적으로 처리가 가능하다.
2. 그리고 처리하는 방법이 원천적으로 같다.
3. try와 __try는 같이 쓸 수 없다.
3번을 보면 아주 불만스럽지요.
저 둘을 한 번에 잡을 수는 없을까요?
있습니다. 2번에 나온 대로, 둘을 처리하는 방법이 같으므로 (적어도 VC++ 에서는!) 한 가지만 써도 둘 다 처리할 수가 있습니다.
그럼 먼저 __try를 써서 둘 다 처리해봅시다.
[code]
// compile with /EHa
#include <stdio.h>
#include <windows.h>
#define CPP_EXCEPTION_MAGIC (0xe06d7363)
void main()
{
DWORD dwException;
__try {
printf("작업을 시작합니다.\n");
// 죽어봅시다.
throw 1;
printf("이 메시지는 안 보입니다.\n");
}
__except (dwException = GetExceptionCode(), EXCEPTION_EXECUTE_HANDLER)
{
if (dwException == CPP_EXCEPTION_MAGIC)
printf("C++ 예외가 throw 되었습니다.\n");
else
printf("예외가 발생했습니다.\n");
}
printf("작업이 끝났습니다.\n");
}
[/code]
VC++의 경우에는 항상 C++ 예외를 저기 define한 코드로 보냅니다.
0xe06d7363에서 뒤 3바이트(0x6d7363)는 문자열로 "msc"라네요.
하여간, 근본적으로 C++ 예외도 예외이므로 이렇게 잡을 수 있습니다.
하지만 GetExceptionInformation을 이용해서 내부적으로 같이 들어온 C++ 예외에 대한 정보(개체의 타입 정보 같은 것)을 꺼내와야 하는데 문서화되어 있지 않아 그럴 수가 없지요.
이런 경우 응당 try를 써야 합니다.
[code]
// compile with /EHa
#include <stdio.h>
void main()
{
try {
printf("작업을 시작합니다.\n");
// 죽어봅시다.
*(int *)0 = 0;
printf("이 메시지는 안 보입니다.\n");
}
catch (...)
{
printf("아무 예외나 다 잡았습니다.\n");
}
printf("작업이 끝났습니다.\n");
}
[/code]
아무거나 다 잡는 catch(...)를 쓰면 C++ 예외가 아니더라도 잡을 수 있습니다.
하지만 이 경우 Win32 예외인지 C++ 예외인지 구분할 방법조차 없습니다.
프로그램이 멋대로 죽는 것만 막아줄 뿐이죠.
그럴 때를 위해 다음과 같은 함수를 제공합니다.
_set_se_translator
이 함수는 다음과 같이 사용합니다.
[code]
// compile with /EHa
#include <stdio.h>
#include <eh.h>
#include <windows.h>
class Win32Exception {
public:
Win32Exception(DWORD dwCode): m_Code(dwCode) { }
DWORD GetCode() { return m_Code; }
private:
DWORD m_Code;
};
void translatorFunction(UINT uCode, EXCEPTION_POINTERS *ep)
{
// throw로 발생한게 아닌 예외가 발생하면 이 함수가 호출됩니다.
// 여기서 throw 해서 C++ 예외로 바꿀 수 있습니다.
// 따라서 이 함수의 반환 값은 의미 없으므로 void입니다.
throw Win32Exception(uCode);
}
void main()
{
// 이 함수가 핵심입니다.
_set_se_translator(translatorFunction);
try {
printf("작업을 시작합니다.\n");
// 죽어봅시다.
*(int *)0 = 0;
printf("이 메시지는 안 보입니다.\n");
}
catch (Win32Exception &e)
{
printf("Win32 예외(0x%X)를 잡았습니다.\n", e.GetCode());
}
catch (...)
{
printf("나머지 C++ 예외를 다 잡았습니다.\n");
}
printf("작업이 끝났습니다.\n");
}
[/code]
translator란 이름에서 알 수 있듯이, C++ 예외가 아닌 예외를 C++ 예외로 바꿔주는데 쓸 수 있는 함수입니다.
이것 말고도 API 중에 SetUnhandledExceptionFilter 같은, 비슷한 역할의 함수도 있습니다.
닷넷에도 비슷한 기능이 있습니다. (처리기를 지정하는건 아니지만 처리 방법을 지정할 수 있네요.)
이러한 함수들을 자주 쓸 일을 없겠지만, 가끔 다른 프로그램들이 이러한 예외도 직접 잘 처리하는 것을 보면 부럽기도 하지요.
기왕 죽는 판에 기분 나쁜 Windows 오류 보고 창보다는 직접 만든 안내창이라도 보여주는 것도 괜찮지 않을까요?
여튼 이 글이 쓸모 있었으면 좋겠네요. 잘못된 부분이나 이상한 부분 있으면 지적 부탁드립니다.
