사용자 도구

사이트 도구


kb:seh

문서의 이전 판입니다!


Structured Exception Handling

  • 윈도우즈에서 제공하는 예외 처리 방식이다. (사실 실제 구현은 컴파일러에서 이루어진다.)
  • C++ 예외와는 다른 것이다.
  • 윈도우즈 상에서는 C++ 스탠다드 라이브러리도 내부적으로 SEH를 이용한다.
  • C++ 예외 처리는 당연히 C++에서만 사용할 수 있으나, SEH는 언어 중립적이기 때문에 다른 언어에서도 사용 가능하다.
  • Termination Handling과 Exception Handling으로 분류할 수 있다.

1. Termination Handling

__try
{
    // Guarded code
}
__finally 
{
    // Termination handler
}

finally 블록 안의 안의 코드는 try 블록 안에서 무슨 짓을 하든 반드시 실행된다. try 블록 안에서 return을 해도, goto를 해도, longjump 명령을 직접 호출해도, 결국은 실행된다. 좀 더 자세하게 말하자면, return이나 goto를 하기 직전에 finally 블록 안의 코드가 실행된다. (exit, abort, ExitProcess, TerminateThread 등을 통해 프로세스나 스레드가 종료되는 경우는 예외다. 이런 경우에는 finally 블록의 코드가 실행되지 않는다.)

try 블록 안에서 return, goto, longjump 등을 사용하는 것은 자제하는 것이 좋다. 반환값 문제 때문에 컴파일러가 추가적인 코드를 만들어내기 때문이다. 이 코드의 양은 CPU마다 틀려지는데, 수백에서 수천 사이클까지 걸리는 경우가 있다. 그러므로 자주 사용하는 코드에다가 집어넣어 놓으면 프로그램의 성능이 심각한 수준까지 떨어질 염려가 있다.

코드의 흐름이 자연스레 finally 블록까지 흘러가는 경우(return 등을 사용하지 않는 경우), 오버헤드는 거의 없다. x86 계열의 CPU에서 마이크로소프트 컴파일러를 사용한 경우, 단 하나의 명령어를 실행할 뿐이다.

일반적으로 가장 좋은 방법은 try 블록 안에서 return, continue, break, goto 문 등을 쓰지 않는 것이다. 이들은 try 블록 바깥 쪽으로 빼내줘야 한다. 그래도 어쩔 수 없이 try 블록을 빠져나가고자 한다면 __leave 키워드를 사용하면 된다.

bool some_function(const char* param)
{
    bool bOK = false;
    char* pBuffer = NULL;
    ...
 
    __try
    {
        pBuffer = new char[100];
        if (pBuffer == NULL)
        {
            // return false; // 여기서 false를 리턴할 것이 아니라, __leave를 사용한다.
            __leave;
        }
 
        bOK = true;
    }
    __finally
    {
        if (pBuffer) delete [] pBuffer;
    }
 
    ...
    return bOK;
}

__leave 키워드를 사용하면, 코드의 흐름(instruction pointer)이 try 블록이 끝나는 곳(__finally 바로 앞의 괄호)으로 이동한다. 그 다음 finally 블록 안의 코드가 자연스레 실행된다. 위에서도 언급했듯이 자연스러운 이동은 성능에 영향을 거의 주지 않는다.

2. Exception Handling

__try 
{
    // Guarded code
} 
__except (EXCEPTION_EXECUTE_HANDLER) // Exception filter
{  
    // Exception handling code
}

except 부분은 C++ 예외 처리 구문의 catch와 비슷해 보이지만, 다르다. C++ 예외 처리에서는 이 부분에 예외의 종류 밖에 들어갈 수 없지만, SEH에서는 exception filter라고 불리는 C 구문(expression)이 들어간다. exception filter 구문은 계산을 거친 후, 최종적으로 세 가지 값 중의 하나가 되어야 한다.

  • EXCEPTION_CONTINUE_EXECUTION (-1)
    • 예외를 무시하고, 예외가 발생한 지점에서부터 프로그램을 계속 실행한다.
    • 예를 들어 10 / i 에서 i가 0이라서 예외가 발생한 경우, 예외 처리 필터가 이 값이라면, 다시 10 / i부터 실행한다는 말이다.
  • EXCEPTION_CONTINUE_SEARCH (0)
    • except 블록 안의 코드를 실행하지 않고, 상위에 있는 예외 처리 핸들러에게 예외 처리를 넘긴다.
  • EXCEPTION_EXECUTE_HANDLER (1)
    • except 블록 안의 코드를 실행한 후, except 블록 아래의 코드를 실행한다.

이 세가지 값을 이용해서 예외 처리 코드를 샘플로 만들어 보자면 다음과 같다.

try 블록 안에서 예외가 발생한 경우, 그 예외가 ACCESS_VIOLATION일 경우에는, exception 블록 안의 코드를 실행하고, 다른 예외인 경우에는 상위에 있는 예외 처리 핸들러에게 통제를 넘기는 코드.

(GetExceptionCode 함수는 try 블록 안에서 발생한 예외의 종류를 반환하는 함수다. 자세한 것은 MSDN을 참고하도록.)

__try
{
    ...
    // compute something
}
__except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? 
	EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
    cerr << "Access violation happened!" << endl;
}

try 블록 안에서 발생한 예외에 대한 정보를 검사하는 필터 함수를 만든 후, 이를 exception filter 부분에서 실행해서 어떤 행동을 해야할 지를 판단하는 코드

(GetExceptionInformation 함수는 예외와 예외가 발생한 시점에서의 시스템에 대한 정보를 반환하는 함수다. 자세한 것은 MSDN을 참고하도록.)

DWORD decide_what_to_do(PEXCEPTION_POINTERS pExceptions)
{
    // check exceptions
    ...
 
    return EXCEPTION_CONTINUE_EXECUTION;
}
 
__try
{
    ...
    // compute something
}
__except(decide_what_to_do(GetExceptionInformation()))
{
    ...
}

Exception Handling의 경우, Termination Handling과는 달리, try 블록 안에 return, goto 등이 있어도 코드 크기나 성능에 페널티가 없다.

3. Stack Unwind

3.1. Local Unwind

try-finally 구문에서 try 블록 내부에 return, goto 등의 premature exit가 있을 경우 발생한다. finally 블록 실행 후, 값을 리턴해야하기 때문에, 컴파일러는 임시 변수를 생성해서, 리턴값을 저장한 후, finally 블록을 실행하고, 저장해둔 값을 리턴한다. 위에서도 나와있듯이 이런 코드가 꽤 비용이 크기 때문에 극구 피해야한다.

3.2. Global Unwind

Global unwind는 try-except 구문에서 exception filter가 EXCEPTION_EXECUTE_HANDLER 값인 경우 발생한다. 동작을 요약하자면, EXCEPTION_EXECUTE_HANDLER로 판정된 try 블록 안의 모든 finally 블록을 제일 안쪽의 것부터 실행한 다음, 원래 try 블록의 except 블록 부분을 실행한다가 되겠다.

다음은 Programming Applications from Microsoft Windows에서 가져온 예제이다. 주석에 있는 번호는 실행 순서를 나타낸다.

void function1()
{
    // 1. 여기서 뭔가를 처리
    ...
 
    __try
    {
        // 2. 다른 함수를 호출
        function2();
 
        // 이 부분의 코드는 실행되지 않는다.
        ...
    }
    __except ( /* 6. 필터 구문을 검사 */ EXCEPTION_EXECUTE_HANDLER)
    {
        // 8. Unwind가 일어난 후에, 예외 처리 핸들러가 실행된다.
        MessageBox(...);
    }
}
 
void function2()
{
    // 3. 여기서 뭔가를 처리
    ...
 
    __try
    {
        // 4. 뭔가에다 락을 건다.
        EnterCriticalSection(&C);
 
        // 5. 여기서 예외가 발생한다.
        int some = 10 / 0;
    }
    __finally 
    {
        // 7. 상위 함수의 exception filter 부분의 값이
        // EXCEPTION_EXECUTE_HANDLER이기 때문에 Global unwind가 일어난다.
 
        // 락을 푼다.
        LeaveCriticalSection(&C);
    }
 
    // 이 부분의 코드는 실행되지 않는다.
}

왜 실제로 예외가 발생한 finally 부분이 먼저 실행되지 않느냐고 할 수 있는데, 예외가 발생한 경우, finally 보다는 except를 먼저 찾아서 처리한다고 생각하면 된다. 왜 그런지는 아직까지 잘 이해가 가지 않는다. 어쨌든 중요한 것은 finally 부분은 반드시 실행이 된다는 점이다.

4. Unhandled Exception

예외가 발생해서 그걸 처리하는 try-except 부분을 차례대로 검사했는데, 모두가 EXCEPTION_CONTINUE_SEARCH를 반환한 경우를 Unhandled Exception이라고 한다. 즉 해당하는 예외를 처리할 핸들러가 하나도 없다는 말이다.

윈도우즈 상에서 모든 스레드는 Kernal32.dll에 있는 BaseProcessStart와 BaseThreadStart 함수를 통해 실행된다. 첫번째는 프로세스의 메인 스레드를 위한 것이고, 두번째는 추가적인 스레드를 위한 것이지만, 결국 똑같은 넘이다.

VOID BaseProcessStart(PPROCESS_START_ROUTINE pfnStartAddr)
{
    __try {
        ExitThread((pfnStartAddr)());
    }
    __except(UnhandledExceptionFilter(GetExceptionInformation())) {
        ExitProcess(GetExceptionCode());
    }
}
 
VOID BaseThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam)
{
    __try {
        ExitThread((pfnStartAddr)(pvParam));
    }
    __except(UnhandledExceptionFilter(GetExceptionInformation())) {
        ExitProcess(GetExceptionCode());
    }
}

결국 Unhandled Exception이 발생한 경우, 시스템에서 이를 잡아서 UnhandledExceptionFilter 함수를 실행한다는 것을 알 수 있다. 이 함수는 사용자들에게 친숙한 다음 대화창을 표시한다.

여기서 “확인”을 누르게 되면 EXCEPTION_EXECUTE_HANDLER를 반환하고, global unwind를 일으켜, 결국 ExitProcess를 호출하게 한다. 이것이 확인을 누르면 프로그램이 종료되는 이유다. “취소”를 누른 경우, 시스템은 JIT(Just In Time) 디버거를 로드하게 된다.

모든 스레드의 주 루프를 손수 만든 try-except (EXCEPTION_EXECUTE_HANDLER) 구문으로 감싸면, 예외가 위의 함수까지 가지 않게 된다. 이는 위의 대화창을 표시하지 않고, 다른 행동을 할 수 있다는 말이다. 그러나 모든 스레드를 이런 식으로 감싸는 것보다는 SetUnhandledExceptionFilter 함수를 이용하는 것이 낫다.

LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
  LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);
 
// TOP_LEVEL_EXCEPTION_FILTER 함수의 형식
LONG UnhandledExceptionFilter(
    STRUCT _EXCEPTION_POINTERS* ExceptionInfo
);

이 함수를 사용하는 예는 MiniDump 페이지를 참고하기 바란다.

5. First and Second Chance Exception

예외 핸들러를 설치한 경우, 예외가 발생하면 핸들러에서 그것을 잡아서 처리한 후, 실행을 계속한다는 개념은 예외 처리의 기본이다. 그런데 실행 중인 프로그램에 디버거가 붙어있는 경우에는 약간 다르다. 디버거가 붙어있는 상태에서의 예외 처리는 다음과 같은 순서로 이루어진다.

  1. 디버거에서 첫번째로 예외가 발생한다. –> First Chance Exception
  2. 프로그램 내부의 예외 핸들러에서 예외가 발생한다.
  3. 디버거에서 두번째로 예외가 발생한다. –> Second Chance Exception

물론 첫번째로 디버거에서 예외가 발생했을 때, 프로그램 쪽으로 예외를 넘겨주지 않으면 거기서 끝이다. 또한 프로그램 내부에서 예외 핸들링을 제대로 하지 않은 경우에도 한번으로 끝이다. 이 경우는 원래 프로그램이 크래쉬되는 상황이라는 것은 두말할 나위 없다. 더 자세한 내용은 INFO: First and Second Chance Exception Handling를 참고하시라.

6. Vectored Exception Handling

VEH는 XP 이상의 윈도우즈에서 사용할 수 있는 예외 처리 방식으로서, __try/__catch 구문 대신에 AddVectoredExceptionHandler, RemoveVectoredExceptionHandler 함수를 이용한다. VEH 함수는 예외가 발생한 경우, 시스템이 SEH 함수를 찾기 위해 스택을 언와인드하기 전에 호출된다.

PVOID AddVectoredExceptionHandler(
  ULONG FirstHandler,
  PVECTORED_EXCEPTION_HANDLER VectoredHandler
);
 
ULONG RemoveVectoredExceptionHandler(
  PVOID VectoredHandlerHandle
);
 
LONG CALLBACK VectoredHandler(
  PEXCEPTION_POINTERS ExceptionInfo
);

핸들러는 여러개 존재할 수 있으며, 이들 간의 실행 순서는 AddVectoredExceptionHandler 함수의 첫번째 인자인 FirstHandler 값으로 조정할 수 있다. 0이 아닌 값일 경우, 첫번째로 실행하게 되고, 0일 경우 마지막으로 실행하게 된다. 같은 값으로 2개의 핸들러를 추가한 경우, 먼저 추가한 핸들러부터 실행하게 된다.

VEH 핸들러의 리턴값은 2가지 중에 하나여야한다.

  • EXCEPTION_CONTINUE_EXECUTION – SEH를 포함해 더 이상의 핸들러는 실행되지 않는다. 실행은 예외가 발생한 곳부터 다시 시작된다.
  • EXCEPTION_CONTINUE_SEARCH – 다음 VEH 함수를 실행한다. 없다면 스택 언와인드를 수행한다.

7. 링크

kb/seh.1415360690.txt.gz · 마지막으로 수정됨: 2014/11/12 13:07 (바깥 편집)