윈도우즈 시스템 프로그래밍

윈도우즈 시스템 프로그래밍 17장 - 예외 핸들러(Exception Handler)

111-000-111 2021. 8. 14. 20:19

 

 

 

 

윈도우즈 시스템 프로그래밍이라는 책과 해당 책의 저자이신 윤성우님의 강의를 통해 공부한 내용을 정리하는 글입니다.

 

 

 

 


 

 

 

종료 핸들러는 무조건 실행이라는 특징을 지니지만, 예외 핸들러는 예외 상황 시 선별적 실행이라는 특징을 가지고 있다.

 

 

예외 핸들러와 필터(Exception Handler & Filters)

 

__try
{
    // 예외 발생 경계 지역
}
__except (예외처리 방식)
{
    // 예외처리를 위한 코드 지역
}

 

⦁ __try 블록은 예외 상황이 발생 가능한 영역을 묶는데 사용된다.

⦁ __except 블록은 __try 블록에서 발생한 예외 상황을 처리한다.

⦁ 예외처리 방식이라고 써있는 부분은 예외필터라고 한다. 이는 예외처리 매커니즘의 동작방식을 결정하는 영역이다. 이 곳에 올 수 있는 값은 필터 표현식이라고 하며, EXCEPTION_EXECUTE_HANDLER, EXCEPTION_CONTINUE_EXCUTION, EXCEPTION_CONTINUE_SEARCH 가 올 수 있다.

 

 

⦁ SEH_FlowView.cpp

 

#include <stdio.h>
#include <tchar.h>
#include <windows.h>

int _tmain(int argc, TCHAR* argv[])
{
	_tprintf(_T("start point! \n"));

	int* p = NULL;

	__try
	{
		*p = 100;
		_tprintf(_T("value: %d \n"), *p);
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		_tprintf(_T("exception occurred! \n"));
	}

	_tprintf(_T("end point! \n"));
	return 0;
}

 

 

실행결과

 

할당되지 않은 주소에 값을 쓰려고 하므로 예외 상황이 발생한다.

*p = 100 부분에서 예외가 발생하는데, 이때 __except 블록이 실행되어 예외를 처리한다. 그 뒤에 end point!를 출력하는 출력문이 실행된다. 즉, 예외 상황이 발생한 다음 문장은 실행되지 않는다는 것을 확인할 수 있다.

예외필터 EXCEPTION_EXECUTE_HANDLER를 선언할 경우 예외 상황이 발생한 라인 이후의 __try 블록의 구문은 건너뛴다.

이렇게 예외를 처리하면 프로그램이 강제 종료되지 않는다. Windows는 예외가 적절히 처리된 것으로 간주하고 예외필터가 명시한대로 실행을 한다.

 

 

 

예외 핸들러의 활용 사례

 

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <windows.h>

enum{DIV=1,MUL,ADD,MIN,EXIT};

DWORD ShowMenu(void);
BOOL Calculator(void);

int _tmain(int argc, TCHAR* argv[])
{
	BOOL state;

	do {
		state = Calculator();
	} while (state == TRUE);

	return 0;
}

DWORD ShowMenu(void)
{
	DWORD sel;

	_fputts(_T("-----Menu----- \n"), stdout);
	_fputts(_T("num 1: Divide \n"), stdout);
	_fputts(_T("num 2: Multiple \n"), stdout);
	_fputts(_T("num 3: Add \n"), stdout);
	_fputts(_T("num 4 : Minus \n"), stdout);
	_fputts(_T("num 5 : Exit \n"), stdout);
	_fputts(_T("SELECTION >>"), stdout);
	_tscanf(_T("%d"), &sel);

	return sel;
}

BOOL Calculator(void)
{
	DWORD sel;
	int num1, num2, result;

	sel = ShowMenu();
	if (sel == EXIT)
		return FALSE;

	_fputts(_T("Input num1, num2: "), stdout);
	_tscanf(_T("%d %d"), &num1, &num2);

	__try
	{
		switch (sel)
		{
		case DIV:
			result = num1 / num2;
			_tprintf(_T("%d / %d = %d \n\n"), num1, num2, result);
			break;
		
		case MUL:
			result = num1 * num2;
			_tprintf(_T("%d * %d = %d \n\n"), num1, num2, result);
			break;

		case ADD:
			result = num1 + num2;
			_tprintf(_T("%d + %d = %d \n\n"), num1, num2, result);
			break;

		case MIN:
			result = num1 - num2;
			_tprintf(_T("%d - %d = %d \n\n"), num1, num2, result);
		}
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		_tprintf(_T("Wrong number inserted. Try again! \n\n"));
	}
	return TRUE;
}

 

 

 

 

 

 

실행 결과

 

 

 

나눗셈에서 두 번째 피연산자가 0이 전달되었을 경우 예외가 발생하여 식을 출력하는 출력문이 필요없어지므로 건너뛰어야 마땅하다. 이럴때 예외필터 EXCEPTION_EXECUTE_HANDLER가 유용하게 사용될 수 있다.

 

 

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <windows.h>

enum{DIV=1, MUL, ADD, MIN, EXIT};

DWORD ShowMenu(void);
BOOL Calculator(void);

void Divide(int, int);
void Multiple(int, int);
void Add(int, int);
void Min(int, int);

int _tmain(int argc, TCHAR* argv[])
{
	BOOL state;

	do {
		state = Calculator();
	} while (state == TRUE);

	return 0;
}

DWORD ShowMenu(void)
{
	DWORD sel;
	_fputts(_T("-----Menu----- \n"), stdout);
	_fputts(_T("num1 : Divide \n"), stdout);
	_fputts(_T("num2 : Multiple \n"), stdout);
	_fputts(_T("num 3: Add \n"), stdout);
	_fputts(_T("num 4 : Minus \n"), stdout);
	_fputts(_T("num 5 : Exit \n"), stdout);
	_fputts(_T("SELECTION >>"), stdout);
	_tscanf(_T("%d"), &sel);

	return sel;
}

BOOL Calculator(void)
{
	DWORD sel;
	int num1, num2, result;

	sel = ShowMenu();
	if (sel == EXIT)
		return FALSE;

	_fputts(_T("Input num1 num2 : "), stdout);
	_tscanf(_T("%d %d"), &num1, &num2);

	__try
	{
		switch (sel)
		{
		case DIV:
			Divide(num1, num2);
			break;
		case MUL:
			Multiple(num1, num2);
			break;
		case ADD:
			Add(num1, num2);
			break;
		case MIN:
			Min(num1, num2);
			break;
		}
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		_tprintf(_T("Wrong number inserted. try again! \n"));
	}

	return TRUE;
}

void Divide(int a, int b)
{
	_tprintf(_T("%d / %d = %d \n\n"), a, b, a / b);
}
void Multiple(int a, int b)
{
	_tprintf(_T("%d * %d = %d \n\n"), a, b, a * b);
}
void Add(int a, int b)
{
	_tprintf(_T("%d + %d = %d \n\n"), a, b, a + b);
}
void Min(int a, int b)
{
	_tprintf(_T("%d - %d = %d \n\n"), a, b, a - b);
}

 

 

실행 결과

 

 

Divde 함수에서 발생한 예외를 처리할 구문이 존재하지 않으므로 이를 호출한 Calculator 함수로 예외의 처리가 넘어간다. 만일 Calculator 함수 내에도 예외처리 구문이 존재하지 않으면 _tmain 함수로 예외처리가 이동된다. 그리고 _tmain 함수 내에서도 예외를 처리하지 못한다면 프로그램은 종료된다.

 

 

 

핸들러의 중복

 

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <windows.h>

enum { DIV = 1, MUL, ADD, MIN, EXIT };

DWORD ShowMenu(void);
BOOL Calculator(void);

void Divide(int, int);
void Multiple(int, int);
void Add(int, int);
void Min(int, int);

int _tmain(int argc, TCHAR* argv[])
{
	BOOL state;

	do {
		state = Calculator();
	} while (state == TRUE);

	return 0;
}

DWORD ShowMenu(void)
{
	DWORD sel;
	_fputts(_T("-----Menu----- \n"), stdout);
	_fputts(_T("num1 : Divide \n"), stdout);
	_fputts(_T("num2 : Multiple \n"), stdout);
	_fputts(_T("num 3: Add \n"), stdout);
	_fputts(_T("num 4 : Minus \n"), stdout);
	_fputts(_T("num 5 : Exit \n"), stdout);
	_fputts(_T("SELECTION >>"), stdout);
	_tscanf(_T("%d"), &sel);

	return sel;
}

BOOL Calculator(void)
{
	DWORD sel;
	int num1, num2, result;

	sel = ShowMenu();
	if (sel == EXIT)
		return FALSE;

	_fputts(_T("Input num1 num2: "), stdout);
	_tscanf(_T("%d %d"), &num1, &num2);

	__try
	{
		__try
		{
			switch (sel)
			{
			case DIV:
				Divide(num1, num2);
				break;

			case MUL:
				Multiple(num1, num2);
				break;

			case ADD:
				Add(num1, num2);
				break;

			case MIN:
				Min(num1, num2);
				break;
			}
		}
		__finally
		{
			_tprintf(_T("End Operation \n\n"));
		}
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		_tprintf(_T("Wrong number inserted. try again! \n\n"));
	}
	return TRUE;
}

void Divide(int a, int b)
{
	_tprintf(_T("%d / %d = %d \n\n"), a, b, a / b);
}
void Multiple(int a, int b)
{
	_tprintf(_T("%d * %d = %d \n\n"), a, b, a * b);
}
void Add(int a, int b)
{
	_tprintf(_T("%d + %d = %d \n\n"), a, b, a + b);
}
void Min(int a, int b)
{
	_tprintf(_T("%d - %d = %d \n\n"), a, b, a - b);
}

 

 

실행 결과

 

 

 

핸들러는 위와 같이 중첩해서 사용이 가능하다.

End operation을 출력하는 문장을 __finally 블록으로 감싸지 않으면 예외 발생시에 해당 문자열을 확인할 수 없다.

 

 

 

 

정의되어 있는 예외의 종류와 예외를 구분하는 방법

 

 

예외가 발생했을 때 예외의 종류를 구분하기 위하여 GetExceptioncode 함수를 호출하면 된다.

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <windows.h>

int _tmain(int argc, TCHAR* argv[])
{
	int* p = NULL;
	int sel = 0;
	int num = 0;
	while (1)
	{
		_tprintf(_T("1 for memory access exception \n"));
		_tprintf(_T("2 for divdie by 0 exception \n"));
		_tprintf(_T("Select exception type[3 for exit]"));

		_tscanf(_T("%d"), &sel);

		if (sel == 3)
			break;

		__try
		{
			if (sel == 1)
			{
				*p = 100;
				_tprintf(_T("value : %d \n"), *p);
			}
			else
			{
				int n = 0;
				n = 7 / num;
			}
		}
		__except (EXCEPTION_EXECUTE_HANDLER)
		{
			DWORD exptType = GetExceptionCode();

			switch (exptType)
			{
			case EXCEPTION_ACCESS_VIOLATION:
				_tprintf(_T("Access violation \n\n"));
				break;

			case EXCEPTION_INT_DIVIDE_BY_ZERO:
				_tprintf(_T("Divide by zero \n\n"));
				break;
			}
		}
	}

	return 0;
}

 

 

실행 결과

 

 

 

GetExceptionCode 함수는 __except 블록 내에서나 예외 필터 표현식을 지정하는 위치에서만 호출 가능하다.

 

 

 

EXCEPTION_CONTINUE_EXECUTION & EXCEPTION_CONTINUE_SEARCH

 

 

EXCEPTION_CONTINUE_EXECUTION은 위 예제와 같은 상황에서 나눗셈 연산 시 0이 입력된 것으로 인해 예외 상황이 야기되었으므로 해당 숫자만 다시 입력받는 상황을 가능하게 해준다.

 

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <windows.h>

enum { DIV = 1, MUL, ADD, MIN, EXIT };

DWORD ShowMenu(void);
BOOL Calculator(void);
DWORD FilterFunction(DWORD exptType);

int _tmain(int argc, TCHAR* argv[])
{
	BOOL state;

	do {
		state = Calculator();
	} while (state == TRUE);

	return 0;
}

DWORD ShowMenu(void)
{
	DWORD sel;
	_fputts(_T("-----Menu----- \n"), stdout);
	_fputts(_T("num1 : Divide \n"), stdout);
	_fputts(_T("num2 : Multiple \n"), stdout);
	_fputts(_T("num 3: Add \n"), stdout);
	_fputts(_T("num 4 : Minus \n"), stdout);
	_fputts(_T("num 5 : Exit \n"), stdout);
	_fputts(_T("SELECTION >>"), stdout);
	_tscanf(_T("%d"), &sel);

	return sel;
}

int num1, num2, result;

BOOL Calculator(void)
{
	DWORD sel;

	sel = ShowMenu();
	if (sel == EXIT)
		return FALSE;

	_fputts(_T("Input num1 num2: "), stdout);
	_tscanf(_T("%d %d"), &num1, &num2);

	__try
	{
		switch (sel)
		{
		case DIV:
			result = num1 / num2;
			_tprintf(_T("%d/%d=%d \n\n"), num1, num2, result);
			break;
		case MUL:
			result = num1 * num2;
			_tprintf(_T("%d * %d = %d \n\n"), num1, num2, result);
			break;
		case ADD:
			result = num1 + num2;
			_tprintf(_T("%d + %d = %d \n\n"), num1, num2, result);
			break;
		case MIN:
			result = num1 - num2;
			_tprintf(_T("%d - %d = %d \n\n"), num1, num2, result);
			break;
		}
	}
	__except (FilterFunction(GetExceptionCode()))
	{
		_tprintf(_T("__except block... \n\n"));
	}
	return TRUE;
}

DWORD FilterFunction(DWORD exptType)
{
	switch (exptType)
	{
	case EXCEPTION_ACCESS_VIOLATION:
		_tprintf(_T("Access violation \n\n"));
		return EXCEPTION_EXECUTE_HANDLER;

	case EXCEPTION_INT_DIVIDE_BY_ZERO:
		_tprintf(_T("Wrong number inserted. \n"));
		_tprintf(_T("Input second number again: "));
		_tscanf(_T("%d"), &num2);
		return EXCEPTION_CONTINUE_EXECUTION;

	default:
		return EXCEPTION_EXECUTE_HANDLER;
	}
}

 

 

 

 

EXCEPTION_CONTINUE_EXECUTION을 사용하여 0으로 나누는 상황에 인자를 다시 받는 상황을 만들었다.

 

 

 

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <windows.h>

enum { DIV = 1, MUL, ADD, MIN, EXIT };

DWORD ShowMenu(void);
BOOL Calculator(void);
DWORD FilterFunction(DWORD exptType);

int _tmain(int argc, TCHAR* argv[])
{
	BOOL state;

	__try
	{
		do {
			state = Calculator();
		} while (state == TRUE);
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{
		_tprintf(_T("End of execution! \n"));
	}

	return 0;
}

DWORD ShowMenu(void)
{
	DWORD sel;
	_fputts(_T("-----Menu----- \n"), stdout);
	_fputts(_T("num1 : Divide \n"), stdout);
	_fputts(_T("num2 : Multiple \n"), stdout);
	_fputts(_T("num 3: Add \n"), stdout);
	_fputts(_T("num 4 : Minus \n"), stdout);
	_fputts(_T("num 5 : Exit \n"), stdout);
	_fputts(_T("SELECTION >>"), stdout);
	_tscanf(_T("%d"), &sel);

	return sel;
}

int num1, num2, result;

BOOL Calculator(void)
{
	DWORD sel;

	sel = ShowMenu();
	if (sel == EXIT)
		return FALSE;
	_fputts(_T("Input num1 num2: "), stdout);
	_tscanf(_T("%d %d"), &num1, &num2);

	__try
	{
		switch (sel)
		{
		case DIV:
			result = num1 / num2;
			_tprintf(_T("%d/%d=%d \n\n"), num1, num2, result);
			break;
		case MUL:
			result = num1 * num2;
			_tprintf(_T("%d * %d = %d \n\n"), num1, num2, result);
			break;
		case ADD:
			result = num1 + num2;
			_tprintf(_T("%d + %d = %d \n\n"), num1, num2, result);
			break;
		case MIN:
			result = num1 - num2;
			_tprintf(_T("%d - %d = %d \n\n"), num1, num2, result);
			break;
		}
	}
	__except (FilterFunction(GetExceptionCode()))
	{
		_tprintf(_T("__except block... \n\n"));
	}

	return TRUE;
}

DWORD FilterFunction(DWORD exptType)
{
	switch (exptType)
	{
	case EXCEPTION_ACCESS_VIOLATION:
		_tprintf(_T("Access violation \n\n"));
		return EXCEPTION_EXECUTE_HANDLER;

	case EXCEPTION_INT_DIVIDE_BY_ZERO:
		_tprintf(_T("Worng number inserted. \n"));
		_tprintf(_T("Input second number again: "));
		_tscanf(_T("%d"), &num2);
		return EXCEPTION_CONTINUE_SEARCH;

	defalut:
		return EXCEPTION_EXECUTE_HANDLER;
	}
}

 

 

실행결과

 

 

 

EXCEPTION_CONTINUE_SEARCH는 다른 곳에 있는 예외 핸들러를 통해서 예외를 처리하라는 의미이다.

함수가 호출된 순서를 바탕으로 예외 핸들러를 찾게 된다. Calculator를 호출한 영역은 _tmain이므로 _tmain 함수의 Calculator 호출 문이 있는 위치에서 예외 핸들러를 찾는다.

실행 결과를 확인해보면, _tmain함수에 있는 예외 핸들러가 실행된 것을 알 수 있다. 그러므로 재입력을 받은 숫자는 의미가 없다.