본문 바로가기

High Level Technique/Window System

SEH (Structured Exception Handling)

SEH (Structured Exception Handling)


__try, __except를 이용하는 방법을 말합니다. try에서 실제 처리할 코드를 넣어주고 except에서 예외가 발생했을 때 처리하는 코드를 넣어줍니다.



보통 코드를 작성을하면 if문을 통해서 NULL일 경우에 대해 GetLastError()을 이용하여 어떤 에러가 발생했는지 화인합니다.


모든 부분에 if문을 통해서 처리를 할 수도 있지만 SEH를 이용할 수도 있습니다.



Termination Handler


Termination Handler는 종료 핸들러라고 하는데 이 핸들러에서 사용되는 키워드는 __try, __finally를 사용합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
 
int main(void)
{
    int a, b;
 
    __try
    {
        printf("Input divide string [a/b]: ");
        scanf("%d %d"&a, &b);
 
        if (b == 0)
        {
            return -1;
        }
    }
    __finally
    {
        printf("Finally block \n");
    }
 
    printf("Result: %d \n", a / b);
 
    return 0;
}
cs


위 코드를 실행하면 결과는 아래와 같습니다.



위 결과는 정상적으로 나눌 수 있는 수를 넣었을 경우이지만 0을 넣는다면 어떻게 나올까요? 예상으로는 b에 0이 들어가면 return -1;을 통해서 실행이 종료되어야 할 것 같습니다.



실행이 바로 종료가 되지 않고 Finally block이 출력되는 것을 볼 수 있습니다. 어떻게 된 것일까요?



컴파일러는 return 문에 의해 반환되는 값을 임시 변수에 저장하고 __finally 블록을 실행한 다음 값을 반환하도록 구성합니다.


__finally 영역은 예외가 발생하건 안하건 항상 실행이 됩니다.


__try 영역을 빠져나오게 할 수 잇는 것은 return, continue, break, goto, 예외 라고 할 수 있습니다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
 
int main(void)
{
    char str[2];
 
    __try
    {
        strcpy(str, "k3y6reak");
        printf("%s \n", str);
    }
    __finally
    {
        printf("finally block \n");
    }
 
    return 0;
}
cs


위 코드를 보면 배열의 길이가 2인데 해당 배열에 k3y6reak라는 문자열을 넣고 있습니다. 당연히 오류가 발생하겠죠?




추가적으로 ExitProcess, ExitThread, ANSI 라이브러리의 exit에 의한 프로세스, 쓰레드 강제종료는 __finally 영역을 실행시키지 않습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
 
int ReadStringAndWrite(void)
{
    FILE* fptr = NULL;
    char* strBufptr = NULL;
 
    __try
    {
        fptr = fopen("string.dat""a+t");
        if (fptr == NULL)
        {
            return -1;
        }
 
        DWORD strlen = 0;
        printf("Input Strig Length (0 to exit): ");
        scanf("%d"&strlen);
 
        if (strlen == 0)
        {
            return 0;
        }
 
        strBufptr = (char*)malloc((strlen + 1)*sizeof(char));
 
        if (strBufptr == NULL)
        {
            return -1;
        }
 
        printf("Input String: ");
        scanf("%s", strBufptr);
 
        fprintf(fptr, "%s\ n", strBufptr);
    }
    __finally
    {
        if (fptr != NULL)
        {
            fclose(fptr);
        }
 
        if (strBufptr != NULL)
        {
            free(strBufptr);
        }
    }
 
    return 1;
}
 
int main(void)
{
    int state = 0;
 
    while (1)
    {
        state = ReadStringAndWrite();
        if (state == -1)
        {
            printf("Some problem occurred! \n");
            break;
        }
 
        if (state == 0)
        {
            printf("Graceful exit! \n");
            break;
        }
    }
 
    return 0;
}
 
 
cs



__try 구문에서 예외가 발생을 하건 안하건 항상 __finally 영역이 실행이 됩니다. __try 영역에서 입력한 수 만큼 동적할당을 했고, ReadStringAndWrite()에서 FILE 포인터를 이용해서 파일을 열었기 때문에 free도 해주어야 하고 파일도 닫아줘야 합니다.


__finally 구문에 항상 실행이 되기 때문에 이 영역에 free, fclose를 넣어준 것입니다.  만약 __try __finally를 사용하지 않았다면 free와 fclose는 중간에 넣어줘야 합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
 
#define num_of_gate 7
 
 
LONG gTotalCount = 0;
 
HANDLE hMutex;
 
void IncreaseCount()
{
    __try
    {
        WaitForSingleObject(hMutex, INFINITE);
        gTotalCount++;
    }
    __finally
    {
        ReleaseMutex(hMutex);
    }
}
 
unsigned int WINAPI ThreadProc(LPVOID lpParam)
{
    for (int i = 0; i < 100000; i++)
    {
        IncreaseCount();
    }
 
    return 0;
}
 
int _tmain(void)
{
    DWORD dwThreadId[num_of_gate];
    HANDLE hThread[num_of_gate];
 
    hMutex = CreateMutex(NULL, FALSE, NULL);
 
    if (hMutex == NULL)
    {
        printf("Create mutex error: %d \n", GetLastError());
    }
 
    for (int i = 0; i < num_of_gate; i++)
    {
        hThread[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, NULL, CREATE_SUSPENDED, (unsigned*)&dwThreadId[i]);
    
        if (hThread[i] == NULL)
        {
            printf("Thread creation Fault! \n");
            return -1;
        }
    }
 
    for (int i = 0; i < num_of_gate; i++)
    {
        ResumeThread(hThread[i]);
    }
 
    WaitForMultipleObjects(num_of_gate, hThread, TRUE, INFINITE);
    
    printf("total count: %d ", gTotalCount);
 
    for (int i = 0; i < num_of_gate; i++)
    {
        CloseHandle(hThread[i]);
    }
 
    CloseHandle(hMutex);
 
    return 0;
}
cs



위 코드를 보면 IncreaseCount()에서 __try, __finally를 이용해 코드를 작성했습니다. 실행 과정중에 문제가 발생하여 쓰레드가 종료되어야만 해도 __finally 구문에서 ReleaseMutex()를 통해서 반환하기 때문에 뮤텍스의 소유권에 대한 문제는 발생하지 않게 됩니다.





Exception Handler


Exception Handler는 예외 핸들러라고 합니다. Termination Handler는 __finally를 통해서 무조건 실행이 되도록 했습니다. 하지만 Exception Handler는 예외상황 발생 시 선별 실행을 하게됩니다.


 마찬가지로 __try 구문을 사용하지만 __finally 대신에 __except 구문을 사용합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <Windows.h>
 
int main(void)
{
    printf("Start Point! \n");
 
    int *= NULL;
 
    __try
    {
        *= 100;
        printf("value: %d \n"*p);
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        printf("exception occurred! \n");
    }
 
    printf("End point! \n");
 
    return 0;
}
cs


위 코드를 보면 int *p가 NULL로 초기화 되어있습니다. 그런데 12라인을 보면 없는 곳에 값을 100을 집어 넣고 있기 때문에 에러가 발생합니다. 에러가 발생했기 때문에 __except 구문으로 넘어가게 됩니다.


여기서 __finally와 다르게 __except 구문에 인자가 들어가게 되는데 EXCEPTION_EXECUTE_HANDLER, EXCEPTION_CONTINUE_EXECUTION, EXCEPTION_CONTINUE_SEARCH 이렇게 3가지가 존재하며 대표적인 것은 EXCEPTION_EXECUTE_HANDLER 입니다.


실행결과를 먼저 보도록 하겠습니다.


출력 결과를 보면 exception occurred가 출력된 것을 볼 수 있습니다. __try 구문에서 value가 출력이 되지 않았죠.


에러가 발생하면 __except 구문으로 바로 넘어가게 되는 것입니다.


__try, __except 구문 없이 코드를 작성하면 프로그램이 종료가 됩니다.


마찬가지로 이러한 구문은 중첩으로 사용이 가능하다.



그렇다면 에러가 발생한다는 것은 코드가 간단하기 때문에 쉽게 알 수 있다. 하지만 그렇지 않은 경우에는 어떻게 알 수 있을까?


어떤 에러가 발생했는지 알기 위해서는 GetExceptionCode()를 이용하면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <Windows.h>
 
int main(void)
{
    printf("Start Point! \n");
 
    int *= NULL;
 
    __try
    {
        *= 100;
        printf("value: %d \n"*p);
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        DWORD exptType = GetExceptionCode();
 
        switch (exptType)
        {
        case EXCEPTION_ACCESS_VIOLATION:
            printf("Access violation \n");
            break;
        case EXCEPTION_INT_DIVIDE_BY_ZERO:
            printf("Divide by zero \n");
            break;
        }
 
        printf("exception occurred! \n");
    }
 
    printf("End point! \n");
 
    return 0;
}
cs



앞의 코드와 똑같다. 추가된 것은 GetExceptionCode()인데 이 함수의 반환 값에 따라서 switch case 구문으로 확인할 수 있다.






앞서 __except()에 인자 값이 3가지가 존재한다고 했는데 남은 두가지에 대해서 알아보도록 하겠습니다.



가령 나누기를 할때 0으로 나눈다면 에러가 발생하게 되는데 에러가 발생했다고 모든 과정을 다시 하도록 처리하는건 뭔가 비효율적이라고 생각됩니다. 0으로 나눴을 때 에러가 발생하면 0이 아닌 다른 수를 입력 받도록만 하면 좋겠죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <Windows.h>
 
int num1, num2;
int result;
 
DWORD ExceptionFilter(DWORD exceptType)
{
 
    if (exceptType == EXCEPTION_INT_DIVIDE_BY_ZERO)
    {
        printf("Wrong number inserted \n");
        printf("Input second number again : ");
        scanf("%d"&num2);
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    
}
 
BOOL Divide()
{
    printf("Input num1, num2: ");
    scanf("%d %d"&num1, &num2);
 
    __try
    {
        result = num1 / num2;
        printf("%d / %d = %d \n", num1, num2, result);
        
    }
    __except (ExceptionFilter(GetExceptionCode()))
    {
        printf("Except block \n");
    }
 
    return TRUE;
}
 
 
int main(void)
{
    BOOL state;
 
    do
    {
        state = Divide();
    } while (state);
 
    return 0;
}
cs


위 코드를 보면  0으로 나눌 때 ExceptionFilter()에서 GetExceptionCode()의 값을 받아 num2에 대해서만 다시 값을 받고 EXCEPTIOIN_CONTINUE_EXECUTION을 반환합니다.


그러면 __except을 실행하지 않고 에러가 발생한 부분으로 이동해서 다시 실행을 하게 됩니다.


EXCEPTION_CONTINUE_SEARCH는 예외가 처리되어야 하는 위치를 별도로 지정하기 위한 용도로 사용됩니다. 책에서 EXCEPTION_CONTINUE_SEARCH는 반드시 써야만 할 필요를 못느낀다고 합니다. 이정도로 하고 넘어가도록 하겠습니다.





Software Exceptions


소프트웨어가 개발될 때 예외들이 결정되기 때문에 추가가 가능하다. RaiseException()을 통해서 추가를 할 수 있다. 이 함수는 예외발생을 알리기 위한 용도로 사용된다.

이 함수가 호출이 되면 SEH가 작동하면서 예외가 처리가 된다.


RaiseException()의 인자 값 중 dwExceptionCode가 있다. 예외의 종류를 지정할 때 사용되는데 GetExceptionCode 함수를 호출 할 때 얻는 값을 지정하면 안된다.

무작정 정의해서도 안된다.




위 그림은 dwExceptionCode 구성이다. 


31, 30번째 비트에는 예외의 심각도 수준을 타나낸다.


00: Success (성공)

01: Informational (예외의 알림)

10: Warning (예외에 대한 경고)

11: Error (강도 높은 오류 상황)


절대 값은 아니며 적정 값에 따른다.



29번째 비트는 예외를 정의한 주체에 대한 정보를 담는다. Microsoft가 정의한 예외는 0, 사용자가 정의한 예외는 1로 약속되어 있다.


28번째 비트는 시스템에 의해서 예약되어 잇는 비트이며 0으로 초기화되어 있다.


16~27비트는 예외발생 환경 정보를 담는다. 예외 발생 환경정보는 아래와 같다.

 Facility Code

Value 

Facility Code 

Value 

FACILITY_NULL

0

FACILITY_CONTROL

10 

FACILITY_RPC

FACILITY_CERT

11

FACILITY_DISPATCH

2

FACILITY_INTERNET

12

FACILITY_STORAGE

3

FACILITY_MEDIASERVER

13

FACILITY_ITF

4

FACILITY_MSMQ

14

FACILITY_WIN32

7

FACILITY_SETUPAPI

15

FACILITY_WINDOWS

8

FACILITY_SCARD

16

FACILITY_SECURITY

9

FACILITY_COMPLUS

17


0~15는 예외를 구분하는 용도로 사용된다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
 
void SoftwareException(void);
 
int main(void)
{
    SoftwareException();
    printf("End \n");
 
    return 0;
}
 
void SoftwareException(void)
{
    DWORD DefinedException = 0x0;
 
    //Severity
    DefinedException |= 0x01 << 31;
    DefinedException |= 0x01 << 30;
 
    //MS or Customer
    DefinedException |= 0x01 << 29;
 
    //Reserved, Must be 0
    DefinedException |= 0x00 << 28;
 
    //Facility code
    DefinedException |= 0x00 << 16;
 
    //Exception code
    DefinedException |= 0x08;
 
    __try
    {
        printf("Send: Exception Code: 0x%x \n", DefinedException);
        RaiseException(DefinedException, 0, NULL, NULL);
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        DWORD exptType = GetExceptionCode();
        printf("Recv: Exception code: 0x%x \n", exptType);
    }
}
cs


DefinedException은 dwExceptionCode의 값입니다. 이 값을 RaiseException을 이용하여 값을 넘겨주면 됩니다.


추가적으로 Windows 운영체제에서 발생시킨 예외인지 개발자 라이브러리에서 발생한 예외인지 알고자한다면 __except()에서 아래와 같이 수정해주면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__except (EXCEPTION_EXECUTE_HANDLER)
    {
        DWORD exptType = GetExceptionCode();
 
        if (exptType & (0x01 << 29))
        {
            //개발자
        }
        else
        {
            //Windows
        }
        
        printf("Recv: Exception code: 0x%x \n", exptType);
    }
cs


RaiseException의 두번째 인자인 dwExceptionFlags는 0아니면 EXCEPTION_NONCONTINUABLE 둘 중 하나를 선택해서 사용할 수 있다.


0은 특별한 설정을 하지 않는 것이고, EXCEPTIOn_noNCONTINUABLE은 예외가 발생한 지점부터 실행을 막는다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
 
void SoftwareException(void);
 
int main(void)
{
    SoftwareException();
    printf("End \n");
 
    return 0;
}
 
void SoftwareException(void)
{
    DWORD DefinedException = 0x0;
 
    //Severity
    DefinedException |= 0x01 << 31;
    DefinedException |= 0x01 << 30;
 
    //MS or Customer
    DefinedException |= 0x01 << 29;
 
    //Reserved, Must be 0
    DefinedException |= 0x00 << 28;
 
    //Facility code
    DefinedException |= 0x00 << 16;
 
    //Exception code
    DefinedException |= 0x08;
 
    __try
    {
        printf("Send: Exception Code: 0x%x \n", DefinedException);
        RaiseException(DefinedException, EXCEPTION_NONCONTINUABLE, NULL, NULL);
    }
    __except (EXCEPTION_CONTINUE_EXECUTION)
    {
        DWORD exptType = GetExceptionCode();
 
        if (exptType == DefinedException)
        {
            printf("Recv: Exception code: 0x%x \n", exptType);
        }
        
        
    }
}
cs


위와 같이 코드를 수정하고 실행을 해보면 실행이 중지되는 것을 볼 수 있다.




GetExceptionInformation


GetExceptionCode함수보다 더 많은 정보를 얻고 싶을 때 GetExceptionInformation 함수를 사용한다. 이 함수는 예외 필터 표현식을 지정하는 부분에서 사용이 가능하다.


GetExceptionInformation 함수가 호출되면 EXCEPTION_POINTERS 구조체 변수의 주소값이 반환된다.


EXCEPTION_POINTERS 구조체는 ExceptionRecord, ContextRecord 두개의 구조체 포인터가 있다.


ExceptionRecord는 프로세서(CPU) 비종속적 예외 관련데이터가 있고, ContextRecord는 프로세서 종속적 데이터가 있다.



'High Level Technique > Window System' 카테고리의 다른 글

메모리 관리  (0) 2017.01.10
비동기 I/O (Asynchronous I/O)  (0) 2017.01.09
캐쉬와 가상메모리  (0) 2017.01.05
쓰레드 풀 (Thread Pool)  (0) 2017.01.04
생산자/소비자 모델  (1) 2017.01.04