<구조적 예외처리(SEH) 기법>
[공부했던 것을 되짚어보며]
이전 내용에서는 컴퓨터 구조에 대한 마지막 내용을 다뤘었습니다.
가상 메모리와 관련된 내용이 주를 이뤘었는데, 이번 글에서는 다루지 않습니다.
이번 글에서는 예외 상황과 그에 대한 처리를 다루게 됩니다.
그리고 다음 글은 파일의 I/O와 디렉터리를 제어하는 내용을 다룰 것입니다.
이 두 내용은 참고 사항 정도의 내용이므로 완벽하게 기억하실 필요는 없다고 생각합니다.
사용해야 한다면 필요할 때 찾아보시는 정도로 참고를 하면 됩니다.
"Windows에서는 이런 기능을 제공하고 있다"로 이해하고 넘어가시길 바랍니다.
[SEH(Structured Exception Handling)]
아마 C++이나 JAVA를 배우면서 try-catch, throw에 대한 문법을 배우신 적이 있을 겁니다.
이걸 빠르게 다루는 책이 있는가 하면 되게 뒤에서 다루는 경우들도 있죠.
사실 우리가 공부했던 내용들에서도 예외처리를 하긴 했었습니다.
단지 위와 같은 문법을 적용했던 것이 아니라 간단한 if문을 통해 처리를 했었죠.
그리고 나중에 가면 코드에서 예외처리를 아예 안하는 경우가 점점 늘게됩니다.
'어차피 문제 없이 돌아가는 코드니까' 라고 생각해서 생략하는 것이지, 원론적으로는 하는게 맞습니다.
[예외처리의 필요성]
사실 예외처리를 하지 않는 이유는 앞에서도 말했던 이유가 꽤 큰 비중을 차지합니다.
'어차피 여기서 문제가 생길 일은 거의 없을텐데 뭐'
그리고 또 하나의 이유는 코드의 가독성 부분입니다.
예외처리를 너무 빡빡하게 하면 코드가 길어지면서 코드를 보기가 어려워집니다.
간단한 예로 파일의 스트림을 개방하는 예를 들어보겠습니다.
FILE* fileptr = fopen("test","r");
if(fileptr == NULL)
{
// 예외 처리
}
char* dataBuf = (char*)malloc(sizeof(char) * 100);
if(dataBuf == NULL)
{
// 예외 처리
}
int numOfRead = fread(dataBuf, 1, 10, fileptr);
if(numOfRead != 10)
{
// 예외 처리
// 추가로 정말 파일의 끝에 도달한 것인지, EOF를 받은 것인지도 확인해야 함(feof)
}
보다시피 파일을 하나 개방하고 읽어들이는 것과 메모리의 동적할당만 해도 이와 같은 예외 처리를 하게 됩니다.
스트림이 개방되고 난 이후에도 읽어들인 파일이 제대로 읽어들여졌는지를 확인해야 할 필요가 있습니다.
이처럼 if문을 통해서 예외처리를 하게 되지만 알고리즘에서도 if문을 사용하는 경우는 흔합니다.
위의 예시는 굉장히 간단한 예시지만, 코드가 길어지게 될 경우에는 문제가 될 수 있습니다.
if문이 예외처리에 사용되는 것인지, 알고리즘의 로직처리에 사용되는지 분간하기가 어려울 수 있기 때문입니다.
그래서 예외를 처리하는 코드만 따로 떼어놓고 볼 수 있다면 참 좋을 것 같습니다.
아래의 예시와 같이 말이죠.
// 프로그램의 실제 흐름
{
FILE* fileptr = fopen("test","r");
char* dataBuf = (char*)malloc(sizeof(char) * 100);
int numOfRead = fread(dataBuf, 1, 10, fileptr);
}
// 위의 흐름에 따른 예외 처리 영역
if(fileptr == NULL)
{
// 예외 처리
}
if(dataBuf == NULL)
{
// 예외 처리
}
if(numOfRead != 10)
{
// 예외 처리
}
위처럼 따로 떼어내서 볼 수 있다면 코드를 분석하는 데 굉장히 수월해질 수 있습니다.
프로그램의 실제 흐름만 보고 싶다면 프로그램의 흐름 부분만, 예외 처리와 관련된 부분은 예외 처리 부분만.
이처럼 떼어놓고 볼 수 있도록 Windows에서는 구조적 예외처리 메커니즘(SEH)을 제공하고 있습니다.
[예외(예외상황)와 에러(혹은 오류)의 차이점]
우리가 프로그램 코드를 짜다보면 '에러가 났다'라는 말을 입에 달고 삽니다.
그런데 예외는 뭐고, 에러는 도대체 뭘까요?
여기서 에러의 종류와 예외는 어디서 발생할 수 있는지에 대해서 정리를 하겠습니다.
https://sevenshards.tistory.com/34
여기서 프로그램의 실행 과정을 설명하면서도 간단하게 언급했던 부분이 있으니 추가로 읽어보시면 좋을 것 같습니다.
1. 컴파일 에러(Compile time Error)
이 에러는 컴파일 중에 발생할 수 있는 에러입니다.
우리가 코드를 다 작성하고 나서 빌드를 하려고 하면 컴파일 에러가 나죠.
이 에러는 컴파일러가 이해할 수 없는 코드, 다시 말해서 문법적인 오류에 의해 발생하는 에러입니다.
그래서 빌드가 되지 않고, 실행파일도 생성되질 않습니다.
그렇기 때문에 이 에러는 반드시 해결하고 넘어가야 하는 에러입니다.
2. 링킹 에러(Linking Error)
컴파일 에러가 없었다고 하면 그 다음에 발생할 수 있는 에러가 바로 링킹 에러입니다.
빌드는 '컴파일 + 링킹'이 같이 수행되는 부분입니다.
코드를 다 작성했고, 빌드를 하려고 했는데 LNK2019와 같은 에러를 보신 적이 있을겁니다.
링커가 함수와 라이브러리, 변수를 매핑하는데 뭔가가 맞지 않아서 이해할 수가 없다고 하는 것입니다.
이를테면 헤더 파일에서는 함수명을 ABC라고 선언했습니다.
그런데 실제 정의를 하는 부분에서는 ABD라고 선언하는 경우나 선언과 다른 식으로 정의를 할 때 일어납니다.
또는 파일의 위치가 맞지 않게 있을 경우에 발생할 수 있는 에러입니다.
마찬가지로 빌드가 되지 않고 실행파일도 생성되지 않기 때문에 이 에러도 해결을 하고 넘어가야 합니다.
3. 런타임 에러(Run-time Error) - 일반적으로는 예외가 대부분이다
이제 앞에 있는 에러들은 다 해결이 되어서 실행파일까지 생성이 됐습니다.
컴파일러에 의한 문법적인 문제나 링커에 의해서 발생하는 에러는 없는 상황인 것이죠.
런타임 에러는 말 그대로 프로그램이 실행되면서 일어날 수 있는 에러를 말합니다.
그리고 일반적으로 예외는 런타임 에러와 관련이 있습니다.
위에 예시로 들었던 코드를 예로 들겠습니다.
FILE* fileptr = fopen("test","r");
if(fileptr == NULL)
{
// 예외 처리
}
char* dataBuf = (char*)malloc(sizeof(char) * 100);
if(dataBuf == NULL)
{
// 예외 처리
}
int numOfRead = fread(dataBuf, 1, 10, fileptr);
if(numOfRead != 10)
{
// 예외 처리
// 추가로 정말 파일의 끝에 도달한 것인지, EOF를 받은 것인지도 확인해야 함(feof)
}
프로그래머가 위의 코드를 작성하면서 test.txt라는 파일이 있을 것이라고 예측을 하고 코드를 작성합니다.
그리고 할당한 메모리 해제도 필요가 없을 것이라고 예측을 합니다.
그런데 test.txt라는 파일을 read모드로 개방하려고 하는데 파일이 없다면?
할당한 메모리의 영역을 예상치 못한 흐름이나 예상하지 못한 데이터의 입력으로 문제가 된다면?
이는 에러이기도 하지만 동시에 '예외'라고 볼 수 있습니다.
프로그래머가 생각하지 못한 부분에서, 의도치 않게 에러가 발생한 것이기 때문입니다.
int* p = NULL;
*p = 100;
또 다른 예로는 위와 같이 NULL 포인터를 참조하는 경우입니다.
메모리를 잘못 참조하게 되는 것이죠.
이는 프로그래머가 적절하게 초기화를 하지 않아서 발생하는 메모리 참조 오류입니다.
그런데 이걸 하드웨어의 관점에서 본다면 잘못된 주소에 접근하는 예외적인 상황이 되기도 합니다.
그 이외에도 0으로 나누기라던가 무한 루프에 빠지는 경우, 존재하지 않는 메모리 위치에 접근하는 경우 등 다양합니다.
이처럼 런타임 에러의 대부분은 개발자의 잘못된 설계에 의해서 발생하게 됩니다.
그리고 하드웨어의 입장에서는 '예외적인 상황'이 발생한 것이고요.
이제 런타임 에러에 대해서 간단하게 결론을 내리면 다음과 같습니다.
"프로그램 실행 도중에 발생하는 대부분의 에러는 '예외'라고 인식하자"
우리가 지금까지 프로그램을 작성하면서 처리 불가능한 문제가 발생하면 프로그램을 종료시켰습니다.
그런데 문제가 발생했다고 해서 무작정 프로그램을 종료시키는 것도 문제가 됩니다.
그래서 내/외부적인 요소에 의해서 발생하는 문제는 구조적 예외처리(이하 SEH) 기법으로 해결할 수 있어야 합니다.
프로그램 실행 시 예측 가능한 대부분의 문제점은 '예외'로 간주하고, 이를 처리할 수 있도록 구현해야 합니다.
4. 논리적 에러(Logical Error)
마지막으로 실행 중에 발생하는 에러와는 또 다른 에러입니다.
프로그램 실행에 있어서 아무런 문제가 없는 경우지만, 결과가 예상과 다르게 나오는 경우입니다.
이런 경우는 프로그래머가 작성한 로직에 문제가 있는 것으로, 예외와는 무관한 경우입니다.
[하드웨어 예외와 소프트웨어 예외]
앞에서 프로그램 실행 중에 예측 가능한 모든 문제점은 예외로 인식하기로 했습니다.
그런데 예외라고 다 똑같은 예외는 아닙니다.
실제로 SEH에서 다루는 예외는 크게 두 가지입니다.
하나는 하드웨어 예외, 하나는 소프트웨어 예외입니다.
1. 하드웨어 예외 - CPU
하드웨어 예외란 '하드웨어에서 인식하고 알려주는 예외'를 뜻합니다.
물론, 예외가 발생하는 근본적인 원인은 실행 중인 프로그램에서 발생하게 됩니다.
이것을 감지하고 예외상황이 발생했음을 알리는 주체가 하드웨어라는 뜻입니다.
간단한 예로 정수를 0으로 나누는 경우가 프로그램 내에서 발생했다고 가정하겠습니다.
CPU는 0으로 나누라는 연산 요청을 받고 문제가 발생했음을 인지합니다.
그래서 CPU에서는 OS에 예외상황이 발생했다고 신호를 전달하게 됩니다.
OS는 CPU의 신호를 받게되고 SEH 메커니즘에 의해 예외상황이 처리되도록 일을 진행하게 됩니다.
하드웨어 예외는 CPU가 사전에 정의해놓은 예외 상황들이 있습니다.
그래서 예외가 발생하게 되면 소프트웨어 측에 예외의 처리 결정권을 넘기게 됩니다.
소프트웨어 쪽에서는 예외를 처리하는 코드(Handler)를 마련하고 있습니다.
만약 CPU에서 미리 정의한 예외가 발생하게 되면 예외에 맞는 핸들러가 동작하는 방식입니다.
다시 말해서 CPU는 예외를 전달하기만 할 뿐, 예외 처리의 주체는 아닙니다.
또한 '사전에 정의해놓은' 예외 상황들이 있기 때문에 인위적으로 예외의 종류를 늘리거나 바꿀 수 없습니다.
2. 소프트웨어 예외 - OS, App
이름만 보면 아시겠지만, 예외상황이 발생했음을 알리는 주체가 소프트웨어일 경우입니다.
여기서 OS 역시 소프트웨어이기 때문에 OS까지 포함이 됩니다.
다만 하드웨어 예외와는 달리 프로그래머가 직접 정의할 수 있는 예외입니다.
하드웨어 예외는 이미 결정이 되어있는 예외 사항들이기 때문에 별도의 정의를 할 수 없습니다.
[종료 핸들러(Termination Handler)]
[SEH를 설명하기에 앞서]
앞에서는 에러와 예외, 그리고 에러의 종류와 예외의 종류 등을 다루었습니다.
실질적인 SEH에 대한 이야기는 여기서부터 시작을 하게 됩니다.
분명히 SEH는 발생하는 예외의 처리를 다루는 데에 유용합니다.
그리고 코드의 가독성을 높이는 데에도 도움을 주고요.
하지만 성능의 저하를 유발하는 원인이 될 수도 있습니다.
아마 C++이나 JAVA를 공부하면서 예외처리를 공부하신 분들이라면 Stack Unwinding이라는 개념을 아실겁니다.
이 때문에 성능의 저하를 유발하게 되며, 실제로 예외가 발생하는 경우에는 더 큰 성능 저하가 발생하게 됩니다.
(내용이 궁금하신 분들은 따로 찾아보시길 바랍니다.)
그래서 서버 프로그래밍을 할 경우에는 가장 중요한 것이 성능입니다.
성능 부분이나 시스템의 호환성에 대해서 아주 민감하기 때문에 SEH를 잘 사용하지 않게 됩니다.
성능 저하가 있으니 SEH를 쓰는 것은 결국 도움이 안된다는 것은 아닙니다.
가장 중요한 것은 SEH의 장점을 파악하고 적재적소에 활용하는 것입니다.
우선 SEH 메커니즘에서는 기능적 특성에 따라 두 가지로 나뉘게 됩니다.
하나는 종료 핸들러, 또 하나는 예외 핸들러입니다.
우선은 종료 핸들러에 대해서 알아보도록 하겠습니다.
[종료 핸들러의 기본 구성과 동작 원리]
종료 핸들러는 SEH에서 제공하는 핸들러 중 하나입니다.
이는 예외 핸들러의 확장이라고도 볼 수 있는데, 이후에 다를 예외 핸들러에 비해서는 사용하기가 간편합니다.
그래서 예외 핸들러보다 먼저 설명을 하게 되었습니다.
종료 핸들러에서 사용하는 키워드는 try-catch와 비슷한 try-finally 구조를 가집니다.
어떻게 생긴 녀석인지 코드를 통해서 한 번 보도록 하겠습니다.
[__try__finally.cpp]
/*
* Windows System Programming - 구조적 예외처리(Structured Exception Handling, SEH)
* 파일명: __try__finally.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-11
* 이전 버전 작성 일자:
* 버전 내용: 기본적인 종료 핸들러 예제
* 이전 버전 내용:
*/
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int _tmain(int argc, TCHAR* argv[])
{
int a, b;
// __try ~ __finally (둘이 하나를 이룬다)
// try 블록
// 예외발생 가능 구역 + 예외처리 이후 실행위치(얼마나 건너뛸 것인가)를 고려
// try 블록에서 빠져나오게 되는 대표적인 상황 = return, continue, break, goto, "예외상황"
__try
{
_tprintf(TEXT("Input Num to Divide [ a / b ]: "));
_tscanf(TEXT("%d / %d"), &a, &b);
if (b == 0)
return -1;
}
// finally 블록
// try 블록을 한 줄이라도 실행하게 되면 반드시 finally 블록을 실행
// finally 블록이 실행될 때 return문에 의해 반환되는 값은 컴파일러가 만드는 임시변수에 저장
// 그리고 finally 블록을 실행하고 난 뒤에 return이 수행된다
__finally
{
_tprintf(TEXT("__finally blocked\n"));
}
_tprintf(TEXT("result: %d\n"), a / b);
return 0;
}
[exception_finally.cpp]
/*
* Windows System Programming - 구조적 예외처리(Structured Exception Handling, SEH)
* 파일명: exception_finally.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-11
* 이전 버전 작성 일자:
* 버전 내용: 종료 핸들러의 동작 원리 이해
* 이전 버전 내용:
*/
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int _tmain(int argc, TCHAR* argv[])
{
TCHAR str[2];
__try
{
_tcscpy(str, TEXT("abcdefghijklmnopqrstuvwxyz")); // 예외 발생 (메모리 참조 오류)
_tprintf(TEXT("%s\n"), str);
}
// finally 블록은 오류가 발생해도 실행된다
// 예외적으로 ExitProcess나 ExitThread나 ANSI C의 exit 함수에 의한 강제 종료 발생시 실행되지 않는다.
__finally
{
_tprintf(TEXT("__finally blocked\n"));
}
return 0;
}
대부분의 설명은 주석에 달아놓았기 때문에 이해하시는 데에는 어려울 것이 없을 것이라고 봅니다.
여기서 주요 포인트는 "try 블록을 한 줄이라도 실행하게 되면 반드시 finally 블록을 실행한다"라는 것입니다.
다시 말해서 예외가 발생을 하던 안하던 __finally 블록은 무조건 실행이 된다는 것입니다.
물론 여기에는 또 예외가 있습니다.
ExitProcess나 ExitThread, exit와 같은 강제 종료는 __finally 블록이 실행되지 않는다는 점만 알아두시면 되겠습니다.
[종료 핸들러 활용 사례 연구 1]
이제 종료 핸들러라는 놈이 어떻게 돌아가는 놈인지, 그리고 어떻게 쓰는 것인지 대략적으로는 이해가 가셨을겁니다.
그렇다면 이걸 써먹을 수 있는 경우가 과연 어떤 경우일까요?
'파일 개방'과 관련된 경우에 사용할 수 있습니다.
[TerminationHandlerEx1.cpp]
/*
* Windows System Programming - 구조적 예외처리(Structured Exception Handling, SEH)
* 파일명: TerminationHandlerEx1.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-11
* 이전 버전 작성 일자:
* 버전 내용: 종료 핸들러의 활용 예제(1)
* 이전 버전 내용:
*/
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int ReadStringAndWrite(void);
int _tmain(int argc, TCHAR* argv[])
{
int state = 0;
while (1)
{
state = ReadStringAndWrite();
if (state == -1)
{
_tprintf(TEXT("Some Exception Occurred!\n"));
break;
}
if (state == 0)
{
_tprintf(TEXT("Jobs Done!\n"));
break;
}
}
return 0;
}
int ReadStringAndWrite(void)
{
FILE* fPtr = NULL;
TCHAR* strBufPtr = NULL;
__try
{
// 파일 개방
fPtr = _tfopen(TEXT("string.dat"), TEXT("a+t"));
if (fPtr == NULL)
return -1;
DWORD strLen = 0;
_tprintf(TEXT("Input String Length(0 to exit): "));
_tscanf(TEXT("%d"), &strLen);
if (strLen == 0)
return 0;
// 입력받은 문자열을 저장하기 위한 메모리 동적 할당
strBufPtr = (TCHAR*)malloc(sizeof(TCHAR) * strLen + 1);
if (strBufPtr == NULL)
return -1;
_tprintf(TEXT("Input String:"));
_tscanf(TEXT("%s"), strBufPtr);
_ftprintf(fPtr, TEXT("%s\n"), strBufPtr);
}
__finally
{
// 예외 발생 시 개방했던 파일을 닫는다
if (fPtr != NULL)
fclose(fPtr);
// 예외 발생 시 할당한 메모리를 해제한다
if (strBufPtr != NULL)
free(strBufPtr);
}
return 1;
}
보다시피 개방한 파일은 반드시 닫아줘야 하고, 메모리를 할당하면 그 메모리를 해제해줘야 합니다.
그리고 예외가 발생하더라도 위의 과정을 거쳐줘야 안정적인 프로그램이 될 수 있습니다.
[종료 핸들러 활용 사례 연구 2]
이번에는 쓰레드 동기화에서 사용했던 뮤텍스를 가지고 예를 들어보겠습니다.
앞서 뮤텍스에 대한 개념을 공부하면서 뮤텍스를 소유한 쓰레드가 반드시 그 뮤텍스를 반환해야 합니다.
그런데 예외의 발생으로 인해서 반환하지 못하는 일은 없어야겠죠.
마찬가지로 예제 코드를 통해 간단하게 확인해보겠습니다.
[TerminationHandlerEx2.cpp]
/*
* Windows System Programming - 구조적 예외처리(Structured Exception Handling, SEH)
* 파일명: TerminationHandlerEx2.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-11
* 이전 버전 작성 일자:
* 버전 내용: 종료 핸들러의 활용 예제(2) - 쓰레드의 동기화(뮤텍스의 소유 및 반환)
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <process.h>
#define NUM_OF_GATE 7
// 전역 변수로 사용. -> 쓰레드는 공유 가능
LONG gTotalCount = 0;
// Mutex 오브젝트를 선언
// 커널에 의해 생성되는 오브젝트이므로 HANDLE값을 가지게 된다.
HANDLE hMutex;
void IncreaseCount()
{
__try
{
// Signaled -> Non-Signaled
WaitForSingleObject(hMutex, INFINITE); // EnterCriticalSection(&hCriticalSection);
gTotalCount++; // 임계 영역(Critical Section)
}
__finally
{
// Non-Signaled -> Signaled
ReleaseMutex(hMutex); // LeaveCriticalSection(&hCriticalSection);
}
}
unsigned int WINAPI ThreadProc(LPVOID lpParam)
{
for (DWORD i = 0; i < 1000; i++)
{
IncreaseCount();
}
return 0;
}
int _tmain(int argc, TCHAR argv[])
{
DWORD dwThreadID[NUM_OF_GATE];
HANDLE hThread[NUM_OF_GATE];
// InitializeCriticalSection(&hCriticalSection);
// 뮤텍스를 생성
hMutex = CreateMutex(
NULL, // 디폴트 보안 속성 (상속)
FALSE, // 누구나 소유할 수 있도록 함 -> 선점형 / TRUE로 설정할 경우 뮤텍스를 생성하는 쓰레드가 우선적으로 소유하게 된다
NULL // 이름을 정하지 않음
);
for (DWORD i = 0; i < NUM_OF_GATE; i++)
{
hThread[i] = (HANDLE)_beginthreadex(
NULL, // 보안(상속) 속성, 안하므로 NULL
0, // 쓰레드의 스택 크기, 0으로 주면 디폴트값(1M)
ThreadProc, // 쓰레드의 main이 될 함수
NULL, // 쓰레드에 전달할 인자, 없으므로 NULL
CREATE_SUSPENDED, // 생성 시 즉시 실행 여부를 인자로 전달, CREATE_SUSPENDED를 전달했으므로 바로 실행하지 않는다
(unsigned*)&dwThreadID[i] // 쓰레드의 ID값을 저장할 주소값을 인자로 전달
);
// 쓰레드의 생성이 실패했을 경우
if (hThread[i] == NULL)
{
_tprintf(TEXT("Thread Creation Fault\n"));
return -1;
}
}
// 모든 쓰레드를 실행 상태로 바꾸는 부분
// 쓰레드 생성 또한 시간이 걸리는 작업이므로 동시 실행을 위해서 SUSPENDED로 생성
// ResumeThread 함수를 통해 모든 쓰레드를 실행 상태로 바꾼다.
for (DWORD i = 0; i < NUM_OF_GATE; i++)
{
ResumeThread(hThread[i]);
}
// 모든 쓰레드의 작업이 끝나기를 기다리는 부분
WaitForMultipleObjects(
NUM_OF_GATE, // 관찰할 오브젝트의 갯수
hThread, // 관찰할 오브젝트를 담고 있는 배열의 주소
TRUE, // 모든 오브젝트가 Signaled 상태가 되면 값을 반환
INFINITE // 무한정으로 대기
);
_tprintf(TEXT("total count: %d\n"), gTotalCount);
// 생성한 쓰레드의 커널 오브젝트 Usage Count를 감소 (리소스 해제 요청)
for (DWORD i = 0; i < NUM_OF_GATE; i++)
CloseHandle(hThread[i]);
// 뮤텍스는 커널 오브젝트이므로 핸들을 반환 및 Usage Count 1 감소
CloseHandle(hMutex); // DeleteCriticalSection(&hCriticalSection);
return 0;
}
이전에 사용했던 예제에 SEH의 종료 핸들러를 간단하게 추가를 했습니다.
보다시피 굉장히 간단하게 예시를 들기 위해 만들어져 있는 코드입니다.
실제로는 WaitForSingleObject의 반환값을 참조해서 구현하는 것이 좋습니다.
여기서는 WAIT_ABANDONED의 개념을 적용할 수가 있을 것 같습니다.
[예외 핸들러(Exception Handler)]
앞서 소개했던 종료 핸들러는 "예외가 발생하건 말건 무조건 실행!" 이라는 성격의 핸들러였습니다.
반대로 예외 핸들러는 "예외가 발생하면 이 조건에 맞춰서 실행"이라는 성격을 지니게 됩니다.
그래서 종료 핸들러를 예외 핸들러의 확장이라는 말을 했던 것이기도 합니다.
예외 핸들러는 일종의 약속이라고 보시면 쉽게 이해하실 것 같습니다.
많은 생각을 할 필요가 없고, 그냥 이렇게 하면 이렇게 실행된다고 이해하면 됩니다.
종료 핸들러에 비해 경우가 많지만, 억지로 외우실 필요도 없습니다.
[예외 핸들러와 필터(Exception Handler & Filters)]
종료 핸들러때와는 달리 try-except의 구성을 지니게 됩니다.
그리고 필터라는 것이 있게 되는데, 예제 코드를 통해서 설명하도록 하겠습니다.
[SEH_FlowView.cpp]
/*
* Windows System Programming - 구조적 예외처리(Structured Exception Handling, SEH)
* 파일명: SEH_FlowView.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-11
* 이전 버전 작성 일자:
* 버전 내용: 예외적 핸들러 기본 예제
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int _tmain(int argc, TCHAR* argv[])
{
_tprintf(TEXT("Start!\n"));
int* p = NULL;
__try
{
*p = 100; // 예외 발생 (메모리 참조 오류)
_tprintf(TEXT("value: %d\n"), *p); // 실행되지 않는다
}
// 예외 핸들러 (__except)
// 예외 필터 EXCEPTION_EXECUTE_HANDLER
// 예외 발생 이후 __try블록의 남은 부분을 건너뛰고 __except 블록을 실행하고 나서 그 이후를 실행
__except (EXCEPTION_EXECUTE_HANDLER)
{
_tprintf(TEXT("Exception Occurred!\n"));
}
_tprintf(TEXT("End!\n"));
return 0;
}
예제 자체를 이해하시는 데에는 큰 어려움이 없을 것입니다.
위 코드에서 사용된 EXCEPTION_EXECUTE_HANDLER가 바로 예외 필터입니다.
__except 블록에서 예외 필터를 적용하면 예외 처리는 필터를 지정해준 방식대로 동작하게 됩니다.
EXCEPTION_EXECUTE_HANDLER 필터의 특징은 __try 블록 내에서 예외 발생 지점 이후로는 다 건너뛰게 됩니다.
그리고 예외 처리 블록을 수행한 다음의 코드를 수행합니다.
또한 프로그램이 강제종료가 되지 않습니다.
Windows에서는 적절하게 예외를 처리했다고 판단하기 때문에 처리 이후의 코드를 계속 수행하게 됩니다.
[예외 핸들러의 활용 사례 연구]
앞서 소개했던 예외 필터는 EXCEPTION_EXECUTE_HANDLER라는 필터였습니다.
이것 외에도 두 가지가 더 있습니다.
일단은 EXCEPTION_EXECUTE_HANDLER라는 필터를 적용한 예를 보겠습니다.
[SEH_Calculator.cpp]
/*
* Windows System Programming - 구조적 예외처리(Structured Exception Handling, SEH)
* 파일명: SEH_Calculator_One.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-11
* 이전 버전 작성 일자:
* 버전 내용: 예외 필터 EXCEPTION_EXECUTE_HANDLER 활용 예제
* 이전 버전 내용:
*/
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
enum{DIV=1, MUL, ADD, SUB, QUIT};
DWORD ShowMenu();
BOOL Calculator();
int _tmain(int argc, TCHAR* argv[])
{
BOOL state;
do {
state = Calculator();
} while (state == TRUE);
return 0;
}
DWORD ShowMenu()
{
DWORD sel;
_fputts(TEXT("-----Menu-----\n"), stdout);
_fputts(TEXT("Num 1: Divide\n"), stdout);
_fputts(TEXT("Num 2: Multiply\n"), stdout);
_fputts(TEXT("Num 3: Add\n"), stdout);
_fputts(TEXT("Num 4: Subtract\n"), stdout);
_fputts(TEXT("Num 5: Quit\n"), stdout);
_fputts(TEXT("Choose One:\n"), stdout);
_tscanf_s(TEXT("%d"), &sel);
return sel;
}
BOOL Calculator()
{
DWORD sel;
int num1, num2, result;
sel = ShowMenu();
if (sel == QUIT)
return FALSE;
_fputts(TEXT("Input num1, num2: "), stdout);
_tscanf_s(TEXT("%d %d"), &num1, &num2);
__try
{
switch (sel)
{
case DIV:
result = num1 / num2; // 0으로 나눌 경우 DIV/0 예외가 발생
_tprintf(TEXT("%d / %d = %d\n"), num1, num2, result); // 예외 발생 시 이 부분은 실행되지 않음
break;
case MUL:
result = num1 * num2;
_tprintf(TEXT("%d * %d = %d\n"), num1, num2, result);
break;
case ADD:
result = num1 + num2;
_tprintf(TEXT("%d + %d = %d\n"), num1, num2, result);
break;
case SUB:
result = num1 - num2;
_tprintf(TEXT("%d - %d = %d\n"), num1, num2, result);
break;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
_fputts(TEXT("Wrong Number Inserted. Try Again!\n"), stdout);
}
return TRUE;
}
코드 자체는 어려울 것이 없으므로 크게 설명할 부분이 없습니다.
여기서 예외가 발생할 수 있는 지점은 나눗셈을 수행하는 부분입니다.
나눗셈에서 정수를 0으로 나누면 예외가 발생하게 됩니다.
여기서 필터는 EXCEPTION_EXECUTE_HANDLER로 되어있으므로 해당 지점 이후를 다 건너뛰게 됩니다.
이 예제를 통해서 생각해볼 것이 있는데, try 블록은 단순히 예외가 발생할 수 있는 블록이 아닙니다.
더 나아가서 보면 예외 처리 이후에 얼마나 건너뛸 것인가도 고려를 해야한다는 것을 알아야 합니다.
[처리되지 않은 예외의 이동 - Stack Unwinding]
이제 SEH가 성능의 저하를 유발할 수도 있다는 원인을 제공하는 현상을 한 번 보도록 하겠습니다.
[SEH_Calculator_Two.cpp]
/*
* Windows System Programming - 구조적 예외처리(Structured Exception Handling, SEH)
* 파일명: SEH_Calculator_Two.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-11
* 이전 버전 작성 일자:
* 버전 내용: 처리되지 않은 예외를 처리하는 과정
* 이전 버전 내용:
*/
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
enum { DIV = 1, MUL, ADD, SUB, QUIT };
DWORD ShowMenu();
BOOL Calculator();
void Divide(int, int);
void Multiply(int, int);
void Add(int, int);
void Subtract(int, int);
int _tmain(int argc, TCHAR* argv[])
{
BOOL state;
do {
state = Calculator();
} while (state == TRUE);
return 0;
}
DWORD ShowMenu()
{
DWORD sel;
_fputts(TEXT("-----Menu-----\n"), stdout);
_fputts(TEXT("Num 1: Divide\n"), stdout);
_fputts(TEXT("Num 2: Multiply\n"), stdout);
_fputts(TEXT("Num 3: Add\n"), stdout);
_fputts(TEXT("Num 4: Subtract\n"), stdout);
_fputts(TEXT("Num 5: Quit\n"), stdout);
_fputts(TEXT("Choose One:\n"), stdout);
_tscanf_s(TEXT("%d"), &sel);
return sel;
}
BOOL Calculator()
{
DWORD sel;
int num1, num2, result;
sel = ShowMenu();
if (sel == QUIT)
return FALSE;
_fputts(TEXT("Input num1, num2: "), stdout);
_tscanf_s(TEXT("%d %d"), &num1, &num2);
__try
{
switch (sel)
{
case DIV:
// 여기서 함수를 호출, 예외가 발생할 경우 호출했던 함수로 복귀
// try의 나머지 부분의 실행을 생략하고 예외처리(__except 블록으로)
Divide(num1, num2);
break;
case MUL:
Multiply(num1, num2);
break;
case ADD:
Add(num1, num2);
break;
case SUB:
Subtract(num1, num2);
break;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
_fputts(TEXT("Wrong Number Inserted. Try Again!\n"), stdout);
}
return TRUE;
}
void Divide(int a, int b)
{
_tprintf(TEXT("%d / %d = %d\n"), a, b, a / b); // 예외는 여기서 발생, 하지만 처리는 여기서 하지 않는다.
}
void Multiply(int a, int b)
{
_tprintf(TEXT("%d * %d = %d\n"), a, b, a * b);
}
void Add(int a, int b)
{
_tprintf(TEXT("%d + %d = %d\n"), a, b, a + b);
}
void Subtract(int a, int b)
{
_tprintf(TEXT("%d - %d = %d\n"), a, b, a - b);
}
마찬가지로 코드는 굉장히 단순하기 때문에 별도의 설명을 드리지 않겠습니다.
다만 예외가 발생하는 부분에 대해서 지금부터 설명할 Stack Unwinding에 대한 부분을 설명하고자 합니다.
보다시피 예외가 발생할 수 있는 곳은 나눗셈이 수행되는 Divide 함수입니다.
그런데 코드에서는 Divide 함수에서는 보이지 않습니다.
실제로 예외처리를 하는 위치는 Caculator 함수에 있습니다.
그래서 예외가 발생하게 되는 경우에는 Divide를 호출한 함수인 Calculator로 예외처리를 위해 이동하게 됩니다.
여기서 포인트는 현재 스택에 들어있는 Divide 함수의 스택 프레임이 반환되게 됩니다.
만약 Calculator에도 예외처리가 되어있지 않았다면, Calculator 함수 역시 자신을 호출한 함수로 이동하게 됩니다.
앞선 경우와 마찬가지로 Calculator 함수의 스택 프레임도 반환되게 됩니다.
위의 두 경우 모두 정확하게는 자신을 호출한 위치로 되돌아가게 됩니다.
마지막으로 _tmain에서도 예외처리가 되어있지 않다고 하면 최종적으로는 OS까지 이르게 됩니다.
OS에서는 예외가 발생한 프로세스를 종료하게 되면서 예외처리가 끝나게 됩니다.
그리고 이와 같은 일련의 과정을 '스택 풀기(Stack Unwinding)'이라고 합니다.
Stack Unwinding에 대한 연산을 추가로 수행하기 때문에 당연히 성능의 저하가 일어날 수 있습니다.
그래서 가급적이면 예외가 발생한 곳에서 처리를 하는 것이 중요합니다.
그렇다고 예외가 발생할 수 있는 곳마다 SEH를 적용하는 것도 문제가 됩니다.
과도한 SEH를 사용하는 것 역시 성능의 저하를 초래하게 됩니다.
앞서 말했던 '적재적소에 활용 하는 것이 중요하다'라는 말이 여기서 비롯된 것입니다.
if 수준에서 예외 처리가 가능하다면 if로, 그게 아니라 SEH를 사용하는 것이 더 낫겠다 싶을 때는 SEH를.
그래서 정말로 SEH가 필요한 상황이 아니라면 사용하지 않는 것도 고려해야 합니다.
[핸들러의 중복]
앞에서는 사용을 자제하라는 차원에서 설명을 하게 되었지만, 그래도 아직 설명해야 할 내용이 남아있습니다.
이 부분은 문법적 차원의 내용이기 때문에 예제코드를 보시면 핸들러의 중복이 무엇인지 바로 이해하실 수 있습니다.
[SEH_Calculator_Three.cpp]
/*
* Windows System Programming - 구조적 예외처리(Structured Exception Handling, SEH)
* 파일명: SEH_Calculator_Three.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-11
* 이전 버전 작성 일자:
* 버전 내용: 핸들러의 중첩 예제(종료 핸들러, 예외 핸들러)
* 이전 버전 내용:
*/
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
enum { DIV = 1, MUL, ADD, SUB, QUIT };
DWORD ShowMenu();
BOOL Calculator();
void Divide(int, int);
void Multiply(int, int);
void Add(int, int);
void Subtract(int, int);
int _tmain(int argc, TCHAR* argv[])
{
BOOL state;
do {
state = Calculator();
} while (state == TRUE);
return 0;
}
DWORD ShowMenu()
{
DWORD sel;
_fputts(TEXT("-----Menu-----\n"), stdout);
_fputts(TEXT("Num 1: Divide\n"), stdout);
_fputts(TEXT("Num 2: Multiply\n"), stdout);
_fputts(TEXT("Num 3: Add\n"), stdout);
_fputts(TEXT("Num 4: Subtract\n"), stdout);
_fputts(TEXT("Num 5: Quit\n"), stdout);
_fputts(TEXT("Choose One:\n"), stdout);
_tscanf_s(TEXT("%d"), &sel);
return sel;
}
BOOL Calculator()
{
DWORD sel;
int num1, num2, result;
sel = ShowMenu();
if (sel == QUIT)
return FALSE;
_fputts(TEXT("Input num1, num2: "), stdout);
_tscanf_s(TEXT("%d %d"), &num1, &num2);
__try // 예외 핸들러의 try 블록
{
__try // 종료 핸들러의 try 블록 (중첩)
{
switch (sel)
{
case DIV:
// 여기서 함수를 호출, 예외가 발생할 경우 호출했던 함수로 복귀
// try의 나머지 부분의 실행을 생략하고 예외처리(__except 블록으로)
Divide(num1, num2);
break;
case MUL:
Multiply(num1, num2);
break;
case ADD:
Add(num1, num2);
break;
case SUB:
Subtract(num1, num2);
break;
}
}
// 종료 핸들러 (try를 블록을 실행하고 빠져나오게 되면 무조건 실행)
// 만약 이 부분이 finally로 감싸져있지 않았다면 예외가 발생하는 경우 종료 핸들러 부분은 볼 수가 없다.
__finally
{
_fputts(TEXT("End Operation!\n\n"), stdout);
}
}
__except (EXCEPTION_EXECUTE_HANDLER) // 예외 핸들러 (예외가 발생할 경우에 실행)
{
_fputts(TEXT("Wrong Number Inserted. Try Again!\n"), stdout);
}
return TRUE;
}
void Divide(int a, int b)
{
_tprintf(TEXT("%d / %d = %d\n"), a, b, a / b); // 예외는 여기서 발생, 하지만 처리는 여기서 하지 않는다.
}
void Multiply(int a, int b)
{
_tprintf(TEXT("%d * %d = %d\n"), a, b, a * b);
}
void Add(int a, int b)
{
_tprintf(TEXT("%d + %d = %d\n"), a, b, a + b);
}
void Subtract(int a, int b)
{
_tprintf(TEXT("%d - %d = %d\n"), a, b, a - b);
}
보다시피 예외 핸들러 안에 종료 핸들러를 중첩시켜 놓은 형태의 코드입니다.
굳이 설명을 더 드리지 않아도 충분히 이해하실 수 있을 것이라고 봅니다.
[정의되어 있는 예외의 종류와 예외를 구분하는 방법]
예외의 종류는 우리가 생각하는 것보다 다양하게 구성이 되어있습니다.
예외이기 때문에 다 똑같은 예외기는 하지만, 발생하는 상황은 제각각 다릅니다.
이를테면 정수를 0으로 나눴을 때 발생하는 예외와 널 포인터 참조와 같은 메모리 참조의 예외가 있습니다.
이와 같은 예외를 구분하는 방법은 GetExceptionCode()라는 함수를 통해 확인할 수 있습니다.
[GetExceptionCode.cpp]
/*
* Windows System Programming - 구조적 예외처리(Structured Exception Handling, SEH)
* 파일명: GetExceptionCode.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-11
* 이전 버전 작성 일자:
* 버전 내용: 사전 정의 되어 있는 예외의 구분 방법 예제
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int _tmain(int argc, TCHAR argv[])
{
int* p = NULL;
int sel = 0;
while (1)
{
_tprintf(TEXT("1 for Memory Access Exception\n"));
_tprintf(TEXT("2 for Divide by 0 Exception\n"));
_tprintf(TEXT("Select Exception Type [3 for exit]: "));
_tscanf_s(TEXT("%d"), &sel);
if (sel == 3)
break;
__try
{
if (sel == 1)
{
*p = 100; // 예외 발생 -> 메모리 참조 예외
_tprintf(TEXT("value: %d"), *p);
}
else // sel == 2
{
int n = 0;
n = 7 / n; // 예외 발생 -> 0으로 나누는 경우
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
DWORD exptType = GetExceptionCode(); // 예외 발생 코드를 받아오는 함수(사실상 매크로)
switch (exptType)
{
case EXCEPTION_ACCESS_VIOLATION: // 메모리 참조 예외
_tprintf(TEXT("Memory Access Violated\n"));
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO: // 0으로 나누는 경우
_tprintf(TEXT("Divide by Zero\n"));
break;
}
}
}
return 0;
}
참고로 GetExceptionCode()는 함수가 아니라 매크로입니다.
예외 코드에 대해서는 MS에서 다 정리를 해뒀으니 필요할 때 참고하시면 될 것 같습니다.
그리고 GetExceptionCode는 "__except 블록 내" 또는 "예외 필터 표현식 지정 위치"에서만 사용이 가능합니다.
[EXCEPTION_CONTINUE_EXECUTION과 EXCEPTION_CONTINUE_SEARCH]
앞서 예외 필터 표현식에는 EXCEPTION_EXECUTE_HANDLER 이외에 두 가지가 더 있다고 했습니다.
여기서 이 두 가지에 대해 소개를 하려고 합니다.
먼저 EXCEPTION_CONTINUE_EXECUTION은 예외가 발생하면 except 블록으로 들어와 예외처리를 합니다.
그리고 예외가 발생했던 지점으로 돌아가서 프로그램을 이어서 실행하게 됩니다.
[SEH_Calculator_ContinueExecution.cpp]
/*
* Windows System Programming - 구조적 예외처리(Structured Exception Handling, SEH)
* 파일명: SEH_Calculator_ContinueExecution.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-11
* 이전 버전 작성 일자:
* 버전 내용: 예외 필터 EXCEPTION_CONTINUE_EXECUTION 활용 예제
* 이전 버전 내용:
*/
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
enum { DIV = 1, MUL, ADD, SUB, QUIT };
DWORD ShowMenu();
BOOL Calculator();
DWORD FilterFunction(DWORD exptType);
int _tmain(int argc, TCHAR* argv[])
{
BOOL state;
do {
state = Calculator();
} while (state == TRUE);
return 0;
}
DWORD ShowMenu()
{
DWORD sel;
_fputts(TEXT("-----Menu-----\n"), stdout);
_fputts(TEXT("Num 1: Divide\n"), stdout);
_fputts(TEXT("Num 2: Multiply\n"), stdout);
_fputts(TEXT("Num 3: Add\n"), stdout);
_fputts(TEXT("Num 4: Subtract\n"), stdout);
_fputts(TEXT("Num 5: Quit\n"), stdout);
_fputts(TEXT("Choose One:\n"), stdout);
_tscanf_s(TEXT("%d"), &sel);
return sel;
}
int num1, num2, result;
BOOL Calculator()
{
DWORD sel;
sel = ShowMenu();
if (sel == QUIT)
return FALSE;
_fputts(TEXT("Input num1, num2: "), stdout);
_tscanf_s(TEXT("%d %d"), &num1, &num2);
__try
{
switch (sel)
{
case DIV:
result = num1 / num2; // 0으로 나눌 경우 DIV/0 예외가 발생
_tprintf(TEXT("%d / %d = %d\n"), num1, num2, result); // 예외 발생 시 이 부분은 실행되지 않음
break;
case MUL:
result = num1 * num2;
_tprintf(TEXT("%d * %d = %d\n"), num1, num2, result);
break;
case ADD:
result = num1 + num2;
_tprintf(TEXT("%d + %d = %d\n"), num1, num2, result);
break;
case SUB:
result = num1 - num2;
_tprintf(TEXT("%d - %d = %d\n"), num1, num2, result);
break;
}
}
// 예외 필터 부분에 함수 호출문이 와도 상관이 없다.
// 단, 반환값은 예외필터 표현식이어야 한다.
__except (FilterFunction(GetExceptionCode()))
{
_fputts(TEXT("Wrong Number Inserted. Try Again!\n"), stdout);
}
return TRUE;
}
DWORD FilterFunction(DWORD exptType)
{
switch (exptType)
{
case EXCEPTION_ACCESS_VIOLATION:
_tprintf(TEXT("Access violation\n"));
return EXCEPTION_EXECUTE_HANDLER;
case EXCEPTION_INT_DIVIDE_BY_ZERO:
_tprintf(TEXT("Wrong Number Inserted.\n"));
_tprintf(TEXT("Input Second Number Again:"));
_tscanf_s(TEXT("%d"), &num2);
return EXCEPTION_CONTINUE_EXECUTION; // 숫자만 다시 입력 받아서 예외가 발생한 지점에서 이어서 실행
default:
return EXCEPTION_EXECUTE_HANDLER;
}
}
이전의 EXCEPTION_EXECUTE_HANDLER와는 달리 분모에 사용될 정수를 다시 입력받게 됩니다.
그리고 0이 아닌 정수를 입력받으면 그 위치로 다시 돌아가서 나눗셈을 수행하게 됩니다.
그래서 EXCEPTION_EXECUTE_HANDLER와 EXCEPTION_CONTINUE_EXECUTION까지는 유용하게 쓸 수 있습니다.
이제 마지막으로 남은 예외 필터인 EXCEPTION_CONTINUE_SEARCH입니다.
이 예외 필터는 조금 적용하기 까다로운 수준을 넘어서 이걸 쓸 일이 있을까라는 의구심도 드는 필터입니다.
이름 그대로 '찾는 것을 계속하라'라는 의미가 들어가있습니다.
이게 무슨 소리냐 하면 예외 처리를 현 위치가 아닌 다른 곳에서 하겠다는 것입니다.
[SEH_Calculator_ContinueSearch.cpp]
/*
* Windows System Programming - 구조적 예외처리(Structured Exception Handling, SEH)
* 파일명: SEH_Calculator_ContinueSearch.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-11
* 이전 버전 작성 일자:
* 버전 내용: 예외 필터 EXCEPTION_CONTINUE_SEARCH 활용 예제
* 이전 버전 내용:
*/
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
enum { DIV = 1, MUL, ADD, SUB, QUIT };
DWORD ShowMenu();
BOOL Calculator();
DWORD FilterFunction(DWORD exptType);
int _tmain(int argc, TCHAR* argv[])
{
BOOL state;
__try
{
do {
state = Calculator();
} while (state == TRUE);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
_tprintf(TEXT("End of Execution\n"));
}
return 0;
}
DWORD ShowMenu()
{
DWORD sel;
_fputts(TEXT("-----Menu-----\n"), stdout);
_fputts(TEXT("Num 1: Divide\n"), stdout);
_fputts(TEXT("Num 2: Multiply\n"), stdout);
_fputts(TEXT("Num 3: Add\n"), stdout);
_fputts(TEXT("Num 4: Subtract\n"), stdout);
_fputts(TEXT("Num 5: Quit\n"), stdout);
_fputts(TEXT("Choose One:\n"), stdout);
_tscanf_s(TEXT("%d"), &sel);
return sel;
}
int num1, num2, result;
BOOL Calculator()
{
DWORD sel;
sel = ShowMenu();
if (sel == QUIT)
return FALSE;
_fputts(TEXT("Input num1, num2: "), stdout);
_tscanf_s(TEXT("%d %d"), &num1, &num2);
__try
{
switch (sel)
{
case DIV:
result = num1 / num2; // 0으로 나눌 경우 DIV/0 예외가 발생
_tprintf(TEXT("%d / %d = %d\n"), num1, num2, result); // 예외 발생 시 이 부분은 실행되지 않음
break;
case MUL:
result = num1 * num2;
_tprintf(TEXT("%d * %d = %d\n"), num1, num2, result);
break;
case ADD:
result = num1 + num2;
_tprintf(TEXT("%d + %d = %d\n"), num1, num2, result);
break;
case SUB:
result = num1 - num2;
_tprintf(TEXT("%d - %d = %d\n"), num1, num2, result);
break;
}
}
// 예외 필터 부분에 함수 호출문이 와도 상관이 없다.
// 단, 반환값은 예외필터 표현식이어야 한다.
__except (FilterFunction(GetExceptionCode()))
{
_fputts(TEXT("__except blocked...\n"), stdout);
}
return TRUE;
}
DWORD FilterFunction(DWORD exptType)
{
switch (exptType)
{
case EXCEPTION_ACCESS_VIOLATION:
_tprintf(TEXT("Access violation\n"));
return EXCEPTION_EXECUTE_HANDLER;
case EXCEPTION_INT_DIVIDE_BY_ZERO:
_tprintf(TEXT("Wrong Number Inserted.\n"));
_tprintf(TEXT("Input Second Number Again:"));
_tscanf_s(TEXT("%d"), &num2);
// 다른 곳에 있는 예외 핸들러를 통해서 예외를 처리하라! -> main으로 가서 처리
// SEH_Calculator_Two에서 예외 처리를 찾아가는 과정과 동일하게 함수가 호출된, 스택에 쌓인 순서대로 찾는다.
// 그래서 실제로 실행하면 입력을 다시 받더라도 main 함수쪽의 예외처리가 실행됨
return EXCEPTION_CONTINUE_SEARCH;
default:
return EXCEPTION_EXECUTE_HANDLER;
}
}
보다시피 코드 자체는 어렵지가 않습니다.
다만 EXCEPTION_CONTINUE_SEARCH라는 놈이 조금 특이하게 동작한다는 것만 이해하시면 됩니다.
예외는 Caculator에 있는 나눗셈 부분에서 발생할 수 있습니다.
그래서 나눗셈 부분에서 0으로 나누는 예외를 발생시키게 되면 EXCEPTION_CONTINUE_SEARCH가 필터가 됩니다.
이후에는 해당 영역에서 예외처리를 하지 않고 Caculator를 호출했던 main 함수로 돌아가게 됩니다.
main 함수에서도 예외처리 영역이 있기 때문에 이 곳에서 예외처리를 하게 됩니다.
이걸 설명하는 저도 그렇지만, 아마 여러분들도 느끼시기에 이걸 어디다 갖다 쓸까라는 생각이 많이 드실겁니다.
앞에 있는 둘은 적어도 이렇게 쓰면 되겠다 하는 부분들이 있습니다.
그런데 이건 아무리 생각을 해봐도 어디에다가 써야할지 감이 안올만큼 좀 애매한 필터입니다.
책의 저자도 "이건 가급적이면 쓸 일은 없을 것이다"고 했고요.
그러니까 이런게 있다는 것만 기억을 해두셨다가 언젠가 이걸 쓸 일이 있겠다 싶으면 그 때 쓰시면 될 것 같습니다.
[소프트웨어 기반의 개발자 정의 예외]
앞에서 다뤘던 것들은 모두 하드웨어 예외에 대한 처리를 예로 들었습니다.
다시 말해서 사전에 정의된 예외만 가지고 예외 처리를 했던 것입니다.
이번에 다룰 소프트웨어 예외는 개발자가 직접 예외상황에 대해 정의하고 추가하는 것입니다.
[소프트웨어 예외(Software Exceptions)의 발생]
하드웨어 예외는 앞서 말했듯이 하드웨어가 설계될 당시부터 모든 것이 결정되어 나옵니다.
그래서 하드웨어 예외를 추가시키려고 해봐야 할 수가 없습니다.
반대로 소프트웨어 예외는 소프트웨어가 설계되면서 결정됩니다.
다시 말하면 개발자가 설계를 하는 과정에서 예외를 얼마든지 추가를 할 수 있다는 말입니다.
우리가 CPU를 사면 그걸 그대로 갖다 써야하지만 프로그램은 우리 입맛대로 만들 수 있는 것처럼요.
RaiseException이라는 함수를 이용하면 소프트웨어 예외를 직접 발생시킬 수 있습니다.
https://learn.microsoft.com/ko-kr/windows/win32/api/errhandlingapi/nf-errhandlingapi-raiseexception
우선 첫 번째 인자인 dwExceptionCode에 대해서만 설명하겠니다.
보다시피 위 그림처럼 32비트, 4바이트의 데이터를 위와 같은 구조로 채워넣어야 합니다.
인덱스를 기준으로 31, 30번째 비트에는 예외의 심각도 수준을 채워넣습니다.
00(Success, 성공) / 01(Informational, 예외 알림) / 10 (Warning, 예외 경고) / 11(Error, 강도 높은 에러)
심각도 수준은 절대적인 기준이 없기 때문에 필요에 따라서 정하면 됩니다.
29번째 비트에는 예외를 정의한 주체가 누구인지에 대한 정보를 담습니다.
MS사에서 정의한 예외라면 0, 사용자가 정의한 예외면 1로 채워넣도록 약속이 되어있습니다.
28번째 비트는 시스템에 의해서 예약되어 있는 비트이므로 반드시 0으로 초기화합니다.
그리고 16~27번 인덱스의 비트까지는 예외발생 환경 정보(Facility) 정보를 담게 됩니다.
사실 뭔지 잘 모르는 것들이 많고 실제로 이것보다도 더 많은 것으로 알고 있습니다.
마찬가지로 이런 것들이 있다 정도로만 알고 계시면 됩니다.
필요할 때 찾아서 적절하게 사용하기만 하면 됩니다.
마지막으로 0~15번 인덱스까지가 예외의 종류를 구분하는 용도가 됩니다.
그래서 우리가 직접 정의를 해야되는 부분이 바로 이 곳입니다.
2바이트니까 최대 $2^8$가지의 소프트웨어 예외를 정의할 수 있습니다.
이제 사용 예시 코드를 한 번 보겠습니다.
[RaiseException.cpp]
/*
* Windows System Programming - 구조적 예외처리(Structured Exception Handling, SEH)
* 파일명: RaiseException.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-11
* 이전 버전 작성 일자:
* 버전 내용: 소프트웨어 예외(사용자 정의 예외) 생성 예제
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
void SoftwareException();
int _tmain(int argc, TCHAR* argv[])
{
SoftwareException();
_tprintf(TEXT("End of the _tmain\n"));
return 0;
}
void SoftwareException()
{
DWORD DefinedException = 0x00;
// 심각도(31, 30번 비트. 11로 지정할 경우 심각한 에러)
DefinedException |= 0x01 << 31;
DefinedException |= 0x01 << 30;
// MS에서 정의한 예외 또는 사용자 정의 예외 (29번 비트, 1일 경우 사용자 지정 예외)
DefinedException |= 0x01 << 29;
// 예약(Reserved)된 비트, 반드시 0이어야 하며 굳이 이 부분은 안넣어도 됨
DefinedException |= 0x00 << 28;
// 예외 발생 환경 정보(16~27)
DefinedException |= 0x00 << 16;
// 예외 종류를 구분하기 위한 코드 (0~15, 실제로 정의할 내용)
DefinedException |= 0x08;
__try
{
_tprintf(TEXT("Send: exception code: 0x%X \n"), DefinedException);
RaiseException(
DefinedException, // 발생 시킬 예외의 형태
0, // 예외 발생 이후의 실행 방식에 제한을 둘 때 지정하는 인자
NULL, // 추가 정보의 개수, 없다면 NULL
NULL // 추가 정보를 전달하기 위한 인자, 없다면 NULL
);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
DWORD exptType = GetExceptionCode();
_tprintf(TEXT("Recv: exception code : 0x%X\n"), exptType);
}
}
위 예제에서는 일일이 비트마다 값을 입력해줬는데, 이를 실제로 표현하면 0xE0000008L입니다.
따라서 앞으로 예외를 정의할 때에는 매크로를 사용해서 다음과 같이 정의하는 것이 일반적입니다.
#define STATUS_DEFAULT_USER_DEFINED_EXPT ((DWORD)0xE0000008L)
그리고 GetExceptionCode 함수는 예외 코드 외에도 부가적인 정보를 얻을 수 있는 함수입니다.
이 함수로 개발자 라이브러리에서 발생한 예외인지 아닌지를 판단할 때는 다음과 같은 코드를 사용해볼 수 있습니다.
__except(EXCEPTION_EXECUTE_HANDLER)
{
DWORD exptType = GetExceptionCode();
if(exptType & (0x01 << 29))
{
// 개발자가 발생시킨 예외
}
else
{
// MS Windows에서 정의한 예외
}
}
다음으로는 두 번째 인자인 dwExceptionFlags에 대해서입니다.
여기에는 0 또는 EXCEPTION_NONCONTINUABLE 둘 중 하나만 올 수 있습니다.
0은 별도의 설정을 하지 않는다는 것을 의미합니다.
EXCEPTION_NONCONTINUABLE은 예외가 발생한 지점에서부터의 실행을 하지 않겠다는 것을 의미합니다.
마찬가지로 예제코드를 통해서 사용한 예를 보겠습니다.
[EXCEPTION_NONCONTINUABLE.cpp]
/*
* Windows System Programming - 구조적 예외처리(Structured Exception Handling, SEH)
* 파일명: EXCEPTION_NONCONTINUABLE.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-11
* 이전 버전 작성 일자:
* 버전 내용: RaiseException 함수의 두 번째 인자로 EXCEPTION_NONCONTINUABLE를 사용한 예시
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
// 이전과 같이 비트에 일일이 예외를 정의하는 것보다 이와 같은 방식이 보편적이다.
#define TEST_EXCEPTION ((DWORD) 0xE0000008L)
void SoftwareException();
int _tmain(int argc, TCHAR* argv[])
{
SoftwareException();
_tprintf(TEXT("End of the _tmain\n"));
return 0;
}
void SoftwareException()
{
__try
{
RaiseException(
TEST_EXCEPTION, // 발생 시킬 예외의 형태
//0,
EXCEPTION_NONCONTINUABLE, // 이걸 인자로 설정할 경우 예외 필터의 EXCEPTION_CONTINUE_EXECUTION가 동작하지 않는다.
NULL, // 추가 정보의 개수, 없다면 NULL
NULL // 추가 정보를 전달하기 위한 인자, 없다면 NULL
);
_tprintf(TEXT("Working!\n"));
}
__except (EXCEPTION_CONTINUE_EXECUTION) // 예외 발생 이후에도 이어서 실행하는 예외 필터
{
DWORD exptType = GetExceptionCode();
if (exptType == TEST_EXCEPTION)
_tprintf(TEXT("TEST EXCEPTION Occured\n"));
}
}
여기서 플래그를 EXCEPTION_NONCONTINUABLE을 설정했습니다.
만약 플래그가 0으로 되어있다면 예외 발생 이후에도 __try 블록의 남아있는 부분들을 이어서 실행하게 됩니다.
반면에 플래그를 설정하게 되면 EXCEPTION_CONTINUE_EXECUTION 필터를 사용해도 더 실행되지 않고 종료됩니다.
예외가 발생한 지점 이후에 계속 실행되면 안되는 경우에는 다음과 같은 플래그를 사용하는 것도 고려해볼 수 있습니다.
[GetExceptionInformation]
이제 마지막으로 GetExceptionInformation이라는 함수를 설명하겠습니다.
그리고 이 함수를 통해 앞서 설명하지 않았던 3, 4번째 인자가 어떻게 사용되는지를 설명하고자 합니다.
앞서 GetExceptionCode()를 통해 반환받는 정보보다 더 많은 정보가 필요할 때가 있습니다.
이럴 때 사용하는 것이 GetExceptionInformation()입니다.
해당 함수가 호출될 수 있는 부분은 예외필터 표현식을 지정하는 부분에서 사용 가능합니다.
GetExceptionInformation()이 호출되면 반환되는 값은 EXCEPTION_POINTERS라는 구조체를 반환하게 됩니다.
https://learn.microsoft.com/ko-kr/windows/win32/api/winnt/ns-winnt-exception_pointers
해당 구조체에서 EXCEPTION_RECORD는 프로세서(CPU)에 비종속적인 예외 자체에 대한 정보가 들어가게 됩니다.
그리고 CONTEXT에는 레지스터를 비롯한 프로세서(CPU)에 종속적인 데이터가 들어갑니다.
따라서 CONTEXT 부분은 시스템이 내부적으로 연산하는 과정에서 참조하게 됩니다.
https://learn.microsoft.com/ko-kr/windows/win32/api/winnt/ns-winnt-context
별도로 관심있으신 분들은 따로 확인해보시면 될 것 같습니다.
현재 필요로 하는 정보는 EXCEPTION_RECORD에 있으므로 해당 구조체를 통해 예외 정보를 가져오면 됩니다.
해당 예외 정보를 가져오는 예제코드를 보이면서 이번 글을 마치도록 하겠습니다.
[RaiseExceptionParam.cpp]
/*
* Windows System Programming - 구조적 예외처리(Structured Exception Handling, SEH)
* 파일명: RaiseExceptionParam.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-11
* 이전 버전 작성 일자:
* 버전 내용: RaiseException 함수의 3, 4번째 인자 사용과 GetExceptionInformation()을 통한 예외의 상세 정보를 얻는 예제
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
// 사용자(개발자) 정의 예외
#define STATUS_DEFAULT_USER_DEFINED_EXPT ((DWORD) 0xE0000008L)
void SoftwareException();
DWORD FilterFunction(LPEXCEPTION_POINTERS exptPtr);
int _tmain(int argc, TCHAR* argv[])
{
SoftwareException();
_tprintf(TEXT("End of the _tmain\n"));
return 0;
}
void SoftwareException()
{
CONST TCHAR* exptString[] = {
TEXT("Exception String One"),
TEXT("Exception String Two"),
TEXT("Exception String Three")
};
__try
{
RaiseException(
STATUS_DEFAULT_USER_DEFINED_EXPT, // 발생 시킬 예외의 형태
0, // 예외 발생 이후의 실행 방식에 제한을 둘 때 지정하는 인자
3, // 추가 정보의 개수, 없다면 NULL
(ULONG_PTR*)exptString // 추가 정보를 전달하기 위한 인자, 없다면 NULL
);
_tprintf(TEXT("Working!\n"));
}
// 예외 필터의 자리에는 함수가 와도 된다.
// 단, 반환값은 예외필터 정규 표현식일것.
__except (FilterFunction(GetExceptionInformation()))
{
DWORD exptType = GetExceptionCode();
_tprintf(TEXT("Recv: exception code : 0x%X\n"), exptType);
}
}
DWORD FilterFunction(LPEXCEPTION_POINTERS exptPtr)
{
PEXCEPTION_RECORD exptRecord = exptPtr->ExceptionRecord;
switch (exptRecord->ExceptionCode)
{
case STATUS_DEFAULT_USER_DEFINED_EXPT:
_tprintf(TEXT("Exception code: 0x%X\n"), exptRecord->ExceptionCode);
_tprintf(TEXT("Exception flags: %d\n"), exptRecord->ExceptionFlags);
_tprintf(TEXT("Exception param num: %d\n"), exptRecord->NumberParameters);
_tprintf(TEXT("String One: %s\n"), exptRecord->ExceptionInformation[0]);
_tprintf(TEXT("String Two: %s\n"), exptRecord->ExceptionInformation[1]);
_tprintf(TEXT("String Three: %s\n"), exptRecord->ExceptionInformation[2]);
break;
default:
break;
}
return EXCEPTION_EXECUTE_HANDLER;
}