반응형


http://blog.naver.com/kimgudtjr/140152402219


[ 예외 ]

 

======= 고전적인 에러 처리 방법 =======

사람은 부정확한 존재이므로 항상 실수를 할 가능성이 있고 프로그래밍도 예외가 아니라고 함

일반 문법적인 예러

ex) if i==3  , siwtch(i) 

와 같은 문법적 에러는 컴파일러가 발견해주지만  논리적인 에러는 발견하기 힘들고 이런 것을 "버그"라고 한다


버그를 완전히 소탕해도 항상 에러를 일으킬 소지를 가지고 있는데 다음과 같은 경우라고 함

---------------------------------------------------------------------------------------------------
○ 존재하지 않는 파일을 열려고 함

○ 포인터가 빈 메모리르 가리키고 있음

○ 리소스나 메모리가 부족하여 작업을 계속 진행할 수 없다

○ 하드 디스크에 물리적인 손상으로 인해 사용할 수 없다. (베드 섹터)
---------------------------------------------------------------------------------------------------

 

고전적인 방법은

---------------------------------------------------------------------------------------------------
파일 핸들 = 파일 오픈(인자)

if(파일 핸들 != INVALiD_HANDLE_VALUE){
 파일 작업 진행
}
else
 에러 메시지 출력
---------------------------------------------------------------------------------------------------

위와 같은 방법은 함수의 리턴문을 비교하여 에러를 피하는 고전적인 방법이라고 함


상식적이고 쉽지만 불편하고 비효율적이라고 함

그래서 Win32에서는 에러만을 전문적으로 처리할 수 있는 매커니즘을 운영체제 차원에서 제공해준다고 함

 

======== 최후 에러 코드 ========


여러 윈도우 API함수들이 에러 발생시 리턴값으로 NULL을 리턴하여 알려주지만

구체적으로 어떤 에러가 발생했는지에 대한 자세한 정보는 알려주지 못한다고 함

그래서 아래와 같은 함수로 구체적인 에러내용을 알 수 있다.


---------------------------------------------------------------------------------------------------
DWORD GetLastError(VOID);
---------------------------------------------------------------------------------------------------

스레드는 최후 에러 코드를 기억하고 있으며 API 함수들은 에러 발생시 어떤 종류의 에러가 발생 했다는 것을 최후 에러 코드에

설정해 놓는다. 그러면 응용 프로그래램에서는 GetLastError 함수로 이 최후 에러 코드를 읽어 발생한 에러의 종류를

조사할 수 있다. 최후 에러 코드는 스레드별로 유지되므로 스위칭이 발생해도 최후 에러 코드는 그대로 유지된다.

그래서 다른 스레드에서 발생한 에러는 GetLastError로 알 수 없다.

최후 에러 코드에 대해 또 한가지 주의할 것은 이 코드는 실패에 대한 기록일 뿐이며 성공에 대해서는  리셋되지 않는 다는 것이다.

예를 들어 함수 A호출 결과 실패 코드 n이 기록되었다고 하자, 이어서 호출된 함수 B가 성공했다고 해서 A의 실패 코드 n이 0으로

리셋되지는 않으므로 이 값으로부터 B가 실패했다고 생각해서는 안된다. 함수 자체의 실패 여부는 리턴값으로 알 수 있으며

GetLastError는 최후 실패한 동작의 이유를 설명할 뿐이다.


함수 호출이 성공한 경우에도 최후 실패 코드가 설정되는 특수한 경우가 몇가지 있는데 성공할 수 있는 유형이 여러 개일 때

어떤 이유로 성공했는지도 GetLastError로 조사할 수 있다. 예를 들어 CrateMutex 함수는 뮤텍스를 생성하되

이미 있는 뮤텍스를 연 경우 최후 실패 코드를 ERROR_ALREADY_EXIST로 설정하여 새로 만든 뮤텍스가 아니라는 것을

리턴한다. 시스템이 정의하는 에러 코드 중 자주 발생하는 것은 다음과 같다.

 

---------------------------------------------------------------------------------------------------
에러 이름     코드   설명
---------------------------------------------------------------------------------------------------
ERROR_SUCCESS     0   성공

ERROR_INVALID_FUNCTION    1   함수가 없음

ERROR_FILE_NOT_FOUND    2   파일을 찾을 수 없음

ERROR_PATH_NOT_FOUND    3   경로를 찾을 수 없음

ERROR_TOO_MANY_OPEN_FILES   4   파일을 여려 수 없음

ERROR_ACCESS_DENIED    5   액세스 권한이 없음

ERROR_INVALID_HANDLE    6   핸들이 무효함

ERROR_NOT_ENOUGH_MEMORY    8   메모리가 부족함

ERROR_WRITE_PROTECT    19   쓰기 금지되어 있음

ERROR_FILE_EXISTS    80   파일이 이미 존재함

ERROR_DISK_FULL     112   디스크에 여유 공간이 없음
---------------------------------------------------------------------------------------------------

자주 발생하는 에러 몇 가지에 대해서만 썼지만  에러 종류는 아주 많으므로 래퍼런스를 참조바란다고 함

에러 코드는 시스템이 정의하여 사용하며 API 함수들이 적절히 설정하지만 사용자가 만든 함수도

에러 코드로 정의할 수 있다. 응용 프로그래램의 에러 코드는 29번 비트를 1로 설정하여 정의하며

SetLastError 함수로 최후 에러 코드를 설정하면 된다.  마이크로소프트가 정의한 에러 비트는 29번이 0으로 되어

있어 사용자가 정의한 커스텀 에러 코드와는 구분되게 해 놓았다. 이렇게 에러 코드를 정의하면

시스템의 최후 에러 코드를 응요요 프로글ㄻ이 사용할 수도 있다.

 

만약 에러 발생 사실만 사용자에게 알리려면 다음 함수로 에러 메시지 문자열을 구할 수 있다.

---------------------------------------------------------------------------------------------------
DWORD FormatMessage(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageld, DWORD dwLanguageld, LPTSTR lpBuffer, DWORD nSize, val_list *Arguments);
---------------------------------------------------------------------------------------------------

에러 메시지 문자열은 시스템에 정의되어 있기도 하지만 응용 프로그래램이 리소스에 메시지 테이플 형태로 정의할 수도 있고

함수로 즉시 조립할 수도 있따.또한 이 함수는 메시지버퍼를 내부에서  할당하기도 하고 서식 문자열도 지원하기 때문에

굉장히 복잡하고 사용방법도 어렵다.

자세한 사용방법은 래퍼런스를 참고하되 시스템 메시지 문자열만 얻고자 한다면 다음과 같이 단순화 하여 사용하면 된다.

---------------------------------------------------------------------------------------------------
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,NULL,에러코드,0,버퍼,버퍼길이,NULL);
---------------------------------------------------------------------------------------------------

세 번째 인수에 GetLastError로 조사한 에러 코드, 다섯 번째 인수에 에러 메시지를 돌려받을 버퍼,

여섯번째 인수에 버퍼 길이를 전달하면 시스템이 정의하는 에러 메시지 문자열을 구할 수 있다.

 

예)
---------------------------------------------------------------------------------------------------
#include <windows.h>
#include <stdio.h>

int main(){

 HANDLE hFile;

 hFile = CreateFile("c:\\NotExistFile.txt",GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

 if(hFile != INVALID_HANDLE_VALUE)
  CloseHandle(hFile);
 

 DWORD code; 
 code = GetLastError();
 char errMes[1024] = {0};

 FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,NULL,code,0,errMes,1024,NULL);

 
 printf("%s\n",errMes);

 return 0;
}

---------------------------------------------------------------------------------------------------

퀵 와치 윈도우에서 @err 라는 예약된 변수명을 확임함으로써 최후 에러 코드를 조사할 수 있다.

@err, hr 형식으로 문자열 형태의 에러 메시지를 읽을 수도 있다.

 


============================================================================================================

[ 예외의 정의 ]

예외(Exception)란 정상적인 코드 흐름의 외부 코드를 실해앟는 일종의 이벤트이다. 예외는 하드웨어에 의해

발생할 수도 있고 소프트웨어에 의해 발생할 수도 있다. 운영체제가 발생했을 때 예외를 처리하는 핸들러를 호출하여

프로그래램이 예외에 대처할 수 있도록 한다. 예외는 여러 가지 요인에 의해 발생ㅇ하는데 예외를 근본적으로 막을 수 있는 방법은 어벖다.

왜냐하면 프로그램이 CPU내부에 갇혀서 실행되는 것이 아니라 사용자와 상호작용하고 주변기기 등의 외부적인 환경과 어울려서

동작하기 때문이다. 운영체제가 예외 처리 메커니즘을 제공함으로써 다음과 같은 이점이 생겼다.

---------------------------------------------------------------------------------------------------
○ 운영체제 자체가 예외 처리를 사용함으로써 어떠한 상황에서도 시스템이 마비되는 사태를 막을 수 있도록

견고해졌다. 물론 아직까지 완벽하지 않지만 이런 견고성은 점점 더 확벽에 가까워질 것이다.


○ 예외 처리를 사용함으로써 응용 프로그램의 신뢰성이 훨씬 더 높아졌다. 사용자가 아무리 프로그램을 잘못 다루더라도

프로그램이 갑자기 이상 동작을 하지는 않는다.


○ 프로그램 고유의 작업과 오류를 처리하는 작업이 분리되었다.그래서 프로그래머는 작업에만 신경을 집중하고 나머지 에러는

예외로 처리하면 된다.
---------------------------------------------------------------------------------------------------


구체적으로 응용 프로그래램이 예외를 처리하는 예를 보기 위해 잠시 타갯ㄱ기를 테스트해 보자.

플로피 드라이브에 디스켓을 넣지 않고 A드라이브를 선택하면 탐색기는 에러메시지를 보여준다.

혹은 파일을 끌어서 다른 디렉토리에 떨어뜨리면 복사가 되는게 정상이지만

ROM과 같은 곳에 끌어다 놓으면 복사할 수 없다는 에러 메시지를 보여준다.

모름지기 프로그래램은 이렇게 짜야한다고 함...  사용자가 어떤 실수를 하더라도, 주변기기가 어떤 상황이더라도

프로그램은 이를 처리할 수 있어야 한다.  어떻게 하는가 하면 발생 가능한 예외에 대해 운영체제가 지원하는

방법대로 예외 핸들러를 작성하는 것이다.

예외 처리는 운영체제가 작업 지원하는 메커니즘이다. 하지만 예외가 제대로 동작하도록 코드를 생성하는 책임은

컴파일러에게 있다. 비록 운영체제가 예외 처리를 위한 준비를 완벽하게 하고 있더라도 응용 프로그래램이 이러한 예외 처리 서비스를

받을 수 있도록 작성되어 있지 않다면 완벽한 예외 처리를 불가능하다. 응용 프로그램이 예외 처리를 하도록 코드를 생성하는 것은 물론

컴파일러의 책임이다. 컴파일러는 운영체제가 예외 처리에 사용하는 정보를 응용 프로그램에 삽입해 두어야 하며 예외 발생시

운영체제가 호출할 수 있는 콜백함수를 작성해야 한다. 또한 예외 발생시 응용 프로그램이 비정상적으로 종료되지 않고 예외에

능동적으로 대처하도록 해야 할 책임도 컴파일러에게 있다.

그래서 예외 처리 루틴은 사용하는 컴파일러마다 다를 수밖에 없으며 Win32 API에 예외에 대한 명확한 규정이 없다.

일반적으로 많이 사용되는 예외처리 루틴은 두 가지 형식이 있다. 첫 번째는 C 형이며 두 번째는 C++ 형이다.

이 중 C 형을 구조화된 예외 처리(Structured Exception Handling)라고 하며 간단히 줄여 SEH라고 한다.

SEH 구문도 컴파일러마다 다른데 여기서는 비주얼 C++ 을 기준으로 설명한다.

C++형은 언어 차원에 제공되는 예외 처리 방식이며 C++ 국제 표준에 정의되어 있다.


============================================================================================================

[ 구조화된 예외 처리 ]

 

======== 예외 핸들러 ========

예외 핸들러는 1) 보호 구역, 2) 예외 필터, 3) 예외 핸들러의 세가지 요소로 구성된다.

문법은 다음과 같다.

---------------------------------------------------------------------------------------------------------

__try{
 보호구역
}
__except(예외 필터){
 예외 핸들러 
}
---------------------------------------------------------------------------------------------------------

___try와 __except( 밑줄이 두 개임에 유의할 것)는 키워드이며 {} 는 생략할 수 없다.

보호구역은 예외처리의 대상이 되는 코드이며 이 구역에서 예외가 발생하면 예외 필터와 예외 핸들러에 의해 보호된다.

예외가 발생할 만한 소지가 있는 코드를 보호구역으로 설정해 놓으면 된다.  예를 들어 다음 코드는 잠재적으로 예외가 발생할

소지가 있다.

---------------------------------------------------------------------------------------------------------
strcpy(str, "Test");
---------------------------------------------------------------------------------------------------------

str이 지정하는 메모리 번지로 "Test"라는 문자열을 복사하는 코드인데 만약 str이 NULL이거나 유효하지 않은 메모리를

가리키고 있다면 예외가 발생할 것이다. 이 예외를 처리하지 않고 그대로 두면 프로그래램음  에러 메시지 박스를 출력하며

종료되어 버릴 것이다.


<그림생략 p.2005>

참고로 strcpy는 무조건 지정한 포인터에 복사하므로 예외가 잘 발생하지만 lstrcpy는 메모리를 먼저 점검하는

내부적인 예외 처리를 하기 때문에 테스트용으로 쓸 수 없다. 이 코드는 다음과 같이 보호 구역으로 지정해야 한다.

---------------------------------------------------------------------------------------------------------
__try{
 strcpy(str, "Test"); 
}
---------------------------------------------------------------------------------------------------------

보호 구역 바로 다음에 __except와 함께 예외 필터가 온다. 예외 필터는 보호구역에서 예외 발생시

시스템이 예외를 처리하는 방법을 지정한다. 필터식의 결과에 따라 시스템은 예외를 핸들러에 넘기거나 다시 실행한다.

필터식은 다음 세 가지 중 한가지여야 하며 이 값에 따라 시스템이 예외를 처리하는 방식이 달라진다.


---------------------------------------------------------------------------------------------------------
값    상수   설명
---------------------------------------------------------------------------------------------------------
EXCEPTION_EXECUTE_HANDLER 1   예외 핸들러로 제어를 넘기고 예외 핸들러 다음 코드를 실행한다.

EXCEPTION_CONTINUE_SEARCH 0   예외 핸들러를 계속해서 찾는다.

EXCEPTION_CONTINUE_EXECUTION -1   예외가 발생한 지점을 다시 실행한다.
---------------------------------------------------------------------------------------------------------

예외 필터는 단순한 평가식이거나 아니면 별도의 함수로 빼낼 수 있다.  물론 필터 함수는 위 세가지 값중

한가지를 리턴해야 한다. 각 값은 매크로 상수가 정의되어 있기는 하지만 너무 길기 때문에 때로는

1,0,-1의 상수가 곧바로 사용되기도 한다. 예외 필터 각 값의 의미와 사용 예를 알아보자.

 

======== EXCEPTION_EXECUTE_HANDLER ========

예외 핸들러는 예외 필터식이 EXCEPTION_EXECUTE_HANDLER 일 때 실행되는 코드이다. 다음과 같이 코드를 작성했다고 해보자.

---------------------------------------------------------------------------------------------------------
void func(){
 TCHAR *str;
 __try{
  strcpy(str,"Test");
 }
 __except(EXCEPTION_EXECUTE_HANDLER){
  MessageBox(NULL,"str pointer is invalid", "Exception",MB_OK);
 }
}
---------------------------------------------------------------------------------------------------------

str 포인터는 지역변수로 선언되기만 했으며 메모리가 할당되어 있지 않으므로 이 포인터에 문자열을 복사하는 것은

예외를 발생시킨다. 예외의 예를 보이기 위해 일부러 이런 코드를 작성하였다.

strcpy(str,"Text") 문이 보호 구역에 들어 있으므로 이 문장 실행중에 에러가 발생하면 예외 필터가 평가된다.

예외 필터식이 EXCEPTION_EXECUTE_HANDLER 이므로 예외 핸들러가 호출될 것이다. 예외 핸들러에서는

메시지 박스로 정중하게 사용자에게 예외가 발생한 이유를 알려주고 있다.

(독자는 메시지 박스가 바로 나오지 않고...  다른 에러가 발생하고 이것을 무시나 다시시도를 해야 메시지 박스가 나왔다.

왜그런지 아시는분.. 알려주세요....)


예외 처리를 하지 않으면 이 응용프로그래램은 Access Violation 에러를 내고 종료되어 버리겠지만

예외 처리를 함으로써 에러 메시지만 출력하고 실행을 계속할 수 있게 된다.

 

======== EXCEPTION_CONTINUE_EXECUTION ========

예외 필터식이 EXCEPTION_CONTINUE_EXECUTION 일 때는 예외가 발생한 문장을 다시 실행한다.

생각해 볼때 예외 필터란 예외가 발생했을 때 평가되는데 이 평가식에서 다시 예외가 발생한 지점으로

제어를 돌려 보내면 또 다른 예외가 발생할 것이다. 무한 루프에 빠지지 않으려면

EXCEPTION_CONTINUE_EXECUTION 이전에 뭔가 조치를 취해서 예외 상황이 다시 발생하지 않도록 해야 한다.

다음 예를 보자.
---------------------------------------------------------------------------------------------------------
void func(){

 int Div = 0,i;

 __try{
  i = 2/Div;
 }
 __except(Div=1,EXCEPTION_CONTINUE_EXECUTION){
 }
}
---------------------------------------------------------------------------------------------------------

보호 구역에서 0으로 나누기 예외가 발생하도록 해 두었다. 이때 예외 필터가 평가되며 예외 필터가

Div=1,EXCEPTION_CONTINUE_EXECUTION 을 리턴하므로 다시 나눗셈을 하는 보호 구역으로 돌아갈것이다.

그러나 예외 필터에서 그 전에 Div=1로 변경하기 때문에 예외 상황이 해결되었으며 i=2/Div는 정상적으로

실행될 수 있다. 이 경우 예외 핸들러는 절대로 실행될 수 어벖으므로 작성하지 않아도 된다.

EXCEPTION_CONTINUE_EXECUTION 의 의미는 예외 상황을 해결했으니까 이제 코드를 다시 실행해도 된다는

의미이다. 단 예외 필터에서는 이 값을 돌려보내기 전에 예외의 원인을 조사해서 해결해야 한다.

위 예제에서는 제수를 1로 바꿔 예외를 제거하였다.


그런데 예외 필터는 하나의 식이기 때문에 복문을 사용할 수 없다는 제약이 있다. 세가지 값중에

하나의 값으로 평가되어야 하며 코드를 가지도록 되어 있지는 않다. 하지만 C의 콤마 연산자와

삼항 연산자를 사용하면 약간의 코드를 구겨넣는 방법이 있다. 앞에서 보인 콤마 연산자가 그 좋은 예이다.

콤마 연산자는 왼쪽에서 오른쪽으로 식을 평가해 나가다가 제일 오른쪽 평가식의 결과를 리턴한다.


C의 기본 연산자중 하나이지만 잘 사용되지 않아 생소하게 느끼는 사람이 많을 것이다.

삼항 연산자를 사용하는 예를 보자. 다음은 0으로 나누기 예외와 액세스 위반 예외가 동시에 발생하는 코드이다.

---------------------------------------------------------------------------------------------------------
void func(){
 
 int Div = 0,i;

 TCHAR *str;
 __try{
  i = 2/Div;
  strcpy(str,"Test");
 }
 __except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? Div=1,EXCEPTION_CONTINUE_EXECUTION:EXCEPTION_EXECUTE_HANDLER){
  MessageBox(NULL,"str pointer is invalid", "Exception", MB_OK);
 }


}
---------------------------------------------------------------------------------------------------------

예외 필터에 사용된 GetExceptionCode() 함수는 발생한 예외으 종류를 조사하는데 잠시 후에 별도로 알아 볼 것이다.


이 코드의 실행 순서는... 생략... (상식적으로 생각해보면 알수도 있을 것이라고 믿음 p.2007)


예외 필터가 좀 더 복잡해진다면 별도의 필터 함수를 작성한 후 예외 필터에서는 함수를 호출하면 된다.

필터 함수에는 반드시 0,-1,1 중 한 갑을 리턴해야 한다. 앞의 예제를 다음과 같이 바꾸어도 결과는 동일하다.

 

---------------------------------------------------------------------------------------------------------
int Div = 0;
DWORD ExcFilter(DWORD exCode){
 
 if(exCode == EXCEPTION_INT_DIVIDE_BY_ZERO){
  Div = 1;
  return EXCEPTION_CONTINUE_EXECUTION;
 }
 else{
  return EXCEPTION_EXECUTE_HANDLER;
 }
  
}

void func(){
 int i;
 TCHAR *str;
 
 __try{
  i = 2/Div;
  strcpy(str,"Test");
 }
 __except( ExcFilter(GetExceptionCode()) )
 {
  MessageBox(NULL,"str pointer is invalid","Exception",MB_OK);
 }
}

---------------------------------------------------------------------------------------------------------

ExcFilter 라는 필터 함수는 만들고 이 함수에서 예외 코드를 조사한 후 적절한 처리를 하고 평가식의 결과를 리턴했으며

예외 필터에서는 이 함수를 불러 필터식의 평가를 전담시켰다. 단 이때 GetExeceptionCode는 예외 필터 바깥에서는 호출할 수 없는

내재 함수이기 때문에 예외 필터에서 호출한 후 필터 함수로 결과를 넘기는 방식을 사용해야 한다.

 


======== EXCEPTION_CONTINUE_SEARCH ========

예외 평가식이 이 값이면 지금 실행되고 있는 코드 이전의 try 블록이 위쪽 try 블록에 대응되는 except 문의 예외 필터를 평가하도록 한다.

(아 먼소리냐.. 예제보면 이해됨..)

이 값은 함수 호출 등에 의해 예외 처리가 중첩되어 있는 경우에만 사용할 수 있다. 예를 들어 보호 구역 내에 또 다른 보호 구역이 있을 경우

안쪽 보호 구역에서 이 값을 사용한다. 이 값을 사용하는 예를 보이기 위해 예제를 하나 만들어 보자. 다음 예제는 0으로 나누는 연산을 하는 함수를

만들고 이 함수 호출문이 보호 구역에 들어 있다.


---------------------------------------------------------------------------------------------------------
int Div = 0;

int DivInt(int a, int b){

 int result;
 result = a/b;
 result result;

}

void func(){
 int i;

 __try{
  i = DivInt(2,Div);
 }
 __except(EXCEPTION_EXECUTE_HANDLER){
  MessageBox(NULL,"Divide by zero", "Exception",MB_OK);
 }
}
---------------------------------------------------------------------------------------------------------

DivInt 함수 실행중에 예외가 발생하면 func 함수의 예외 필터가 평가될 것이며 예외 필터식의 결과

EXCEPTION_EXECUTE_HANDLER 이므로 예외 핸들러가 실행될 것이다. 그런데 만약 DivInt 함수 내부에서

또다른 예외 처리를 해야 할 필요가 있다고 하자. 다음처럼 말이다.


---------------------------------------------------------------------------------------------------------

int Div = 0;

int DivInt(int a, int b){

 int result;

 
 __try{
  result = a/b;
  strcpy(str,"Test");
 }
 __except( GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_CONTINUE_SEARCH:EXCEPTION_EXECUTE_HANDLER){

  MessageBox(NULL,"str pointer is invalid","Exception",MB_OK);

 }
 result result;
}

void func(){
 int i;

 __try{
  i = DivInt(2,Div);
 }
 __except(EXCEPTION_EXECUTE_HANDLER){
  MessageBox(NULL,"Divide by zero", "Exception",MB_OK);
 }
}

---------------------------------------------------------------------------------------------------------


이 경우 DivInt에서는 0으로 나누기 예외와 액세스 위반 예외가 동시에 발생하는데 액세스 위반 예외는 DivInt 내부에서

처리할 수 있지만 0으로 나누기 예외는 이 함수를 호출한 func 함수에서 처리하고자 한다. 이때 사용되는 예외 평가값이

바로 EXCEPTION_CONTINUE_SEARCH 이다. 평가식이 이 값이면 이전의 try 블록과 대응되는 예외 필터를 평가하여 그 결과에 따른다.

 

==============================================================================================================================

[ 내재 함수 ]

예외 처리는 __try, __except 키워드에 의해 지원되닌다. 이 키워드 외에 예외 처리를 보조하는 몇 가지 함수가 있다.

앞에서 본 GetExceptionCode 등의 함수가 이에 해당한다. 근데 이 함수들은 다른 API 함수들과는 본질적으로

다른 함수들이다. 함수의 본체가 정의되어 있지 않으며 컴파일 결과 함수 호출문이 삽입되지 않고 직접 이 함수의 코드가 삽입된다.

동작 방식으로 볼 대 인라인 함수와 유사하지만 그것과도 다르다.  함수의 형태를 띠고 있지만 사실은 컴파일러가 직접처리하는 키워드이다.

이런 함수를 내재 함수(Intrinsic Function)라고 한다. 이름만 함수고 사용한는 방법도 함수와 동일하지만 사실은 함수가 아니다. 무늬만 함수이다.


함수와 구체적으로 어떤 점이 다른가 하면 내재 함수는 사용할 수 있는 곳이 정해져 있다. 예를 들어 GetExceptionCode는 예외 필터에서만

사용되어야지 예외 필터 바깥에서는 사용할 수 없다. 왜냐하면 예외 처리 코드 이외의 부분에서는 이 함수가 필요할 리도 없고 이 함수가

정보를 구할 방법도 어벖기 때문이다. 예외가 발생해야 어떤 예외가 발생했는지 알 수 있지 않겠는가.

 

======== GetExceptionCode ========

이 함수는 예외 필터와 예외 핸들러에서만 사용할 수 있으며 지금 발생한 예외의 종ㄹ를 조사한다.

인수는 취하지 않으며 리턴값으롱 ㅖ외의 종류를 알려준다. 발생가능한 예외의 종류는 다음과 같다.


---------------------------------------------------------------------------------------------------------
예외    설명
---------------------------------------------------------------------------------------------------------
EXCEPTION_ACCESS_VIOLATION 허가되지 않은 메모리 영역을 읽거나 쓰려고 시도하였다. 
    가장 일반적으로 많이 발생하는 예외

EXCEPTION_BREAKPOINT  중단점을 만났다.

EXCEPTION_DATATYPE_MISALIGNMENT 정렬을 지원하지 앟는 하드웨어에 잘못된 정렬된 값을 읽거나 쓰려고 하였다.
    예를 들어 16비트 값은 반드시 2바이트 경계로 정렬되어야 하고 32비트값은 반드시
    4바이트 경계로 정렬되어야 한다.

EXCEPTION_SINGLE_STEP  단계 실행을 위한 신호이다.

EXCEPTION_ARRAY_BOUNDS_EXCEEDED 배열의 범위를 초과하여 배열 요소를 읽거나 쓰려고 하였다. 단 이예외는 
    하드웨어 배열 범위 점검을 하는 경우에만 발생한다.

EXCEPTION_FLT_DENORMAL_OPERAND 부동 소수점 연산의 피 연산자중 하나가 잘못되었다.
    예를 들어 너무 작은 값이기 때문에 표준 부동 소수점 형식으로 표현할 수 없는 값이다.

EXCEPTION_FLT_DIVIDE_BY_ZERO 실수를 0으로 나누었다.

EXCEPTION_FLT_INEXACT_RESULT 부동 소수점 연산의 결과 정수부를 정확하게 표현할 수 없다.

EXCEPTION_FLT_OVERFLOW  연산의 결과가 허용된 범위보다 더 크다.

EXCEPTION_FLT_STACK_CHECK 연산의 결과 스택이 오버 플로우 되었거나 언더 플로우 되었다.

EXCEPTION_FLT_UNDERFLOW  연산의 결과가 허용된 범위보다 더 작다.

EXCEPTION_INT_DIVIDE_BY_ZERO 0으로 나누기 정수 연산을 하였다.

EXCEPTION_INT_OVERFLOW  성수 연산의 결과가 허용된 범위보다 더 크다.

EXCEPTION_INT_PRIV_INSTRUCTION 현재 기계 모드에서 사용할 수 없는 명령을 실행하려고 하였다.

EXCEPTION_NONCONTINUABLE_EXCEPTION 실행을 계속할 수 없는 예외를 다시 실행하려고 하였다.
---------------------------------------------------------------------------------------------------------

 

======== GetExceptionInformation ========

이 함수는 예외가 발생했을 때 예외에 대한 상세한 정보와 예외 발생시의 기계 상태에 대한 정보를 조사한다.

예외에 대한 정보를 조사하는 것이기 때문에 예외 필터밖에서는 이 함수를 사용할 수 없다.

원형은 다음과 같다.

---------------------------------------------------------------------------------------------------------
LPEXCEPTION_POINTERS GetExceptionInformation(VOID)
---------------------------------------------------------------------------------------------------------

인수는 취하지 않으며 리턴값 하나를 돌려주는데 이 값은 복잡한 정보를 가진 구조체의 포인터이다.

예외가 발생하면 운영체제는 이 구조체를 작성하여 스택에 넣어주며 사용자는 예외 필터에서

GetExceptionInformation 함수를 호출하여 이 구조체를 읽을 수 있다.


---------------------------------------------------------------------------------------------------------
typedef struct _EXCEPTION_POINTERS {
    PEXCEPTION_RECORD ExceptionRecord;
    PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
---------------------------------------------------------------------------------------------------------

이 구조체는 또 다른 두 개의 구조체를 멤버로 가지고 있다. EXCEPTION_RECORD 멤버는 예외 자체에 대한 정보를

가지며 프로그램이 실행되는 플랫폼에는 독립적이다.  ContextRecord 멤버는 예외가 발생한 당시의 기계 상태,

즉 레지스터의 값들을 가지는데 레지스터 구성이 플랫폼마다 다르므로 이 구조체도 플렛폼에 따라 달라진다.

ExceptionRecord 구조체는 다음과 같이 선언되어 있다.


---------------------------------------------------------------------------------------------------------
typedef struct _EXCEPTION_RECORD {
    DWORD    ExceptionCode;
    DWORD ExceptionFlags;
    struct _EXCEPTION_RECORD *ExceptionRecord;
    PVOID ExceptionAddress;
    DWORD NumberParameters;
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

---------------------------------------------------------------------------------------------------------

모두 6개의 멤버를 가진다. 이 구조체의 멤버를 읽으므로써 예외의 종류, 발생 위치, 계속 여부 등의 정보를 얻을 수 있다.

---------------------------------------------------------------------------------------------------------
멤버    설명
---------------------------------------------------------------------------------------------------------
ExceptionCode   어떤 종류의 예외인지를 설명한다. GetExceptionCode 함수의 리턴값과 동일하다.

ExceptionFlags   예외가 계속 실행될 수 있는 종류인가 그렇지 않은가를 나타낸다. 0이면 예외는 계속 진행될 수 있다는 뜻이며
    EXCEPTION_NONCONTINUABLE이면 계속 진행될 수 없는 예외라는 뜻이다.
    진행해서는 안 되는 예외를 계속 진행하려고 해서는 안 된다.


ExceptionRecord   예외 처리 루틴이 중첩되어 있을 겨웅 다른 예외에 대한 정보를 가진다.  예외 처리를 하는 중에도
    예외가 발생할 수 있는데 이때는 예외끼리 발생 순서에 따라 상호 연결된다. 
    마치 연겨려 리스트의 노드가 서로 연결되는 것과 같다.

ExceptionAddress  예외가 발생한 번지이다.


NumberParameters  ExceptionInformation 배열의 크기이다. 대개의 경우 0이다.


ExceptionInformation  예외를 설명하는 추가적인 32비트값의 배열이다. 대부부느이 예외를 이 배열에 값을 정의하지 않으나
    소프트웨어 예외에서는 이 값을 사용할 수도 있다.
    EXCEPTION_ACCESS_VIOLATION 예외에서는 배열의 첫 번재 요소는 예외를 일으킨 여산의 종류를 지정한다.
    이 값이 0이면 읽을 수 없는 데이터를 읽으려고 한 것이며 이 값이 1이면 쓸 수 없는 데이터를 쓰려고 시도한 것이다.
    두 번째 요소는 읽기/쓰기를 시도한 가상 메모리상의 번지를 지정한다.
    
---------------------------------------------------------------------------------------------------------


CONTEXT 구조체는 플랫폼에 따라 달라지며 플랫폼의  종류가 많기 때문에 여기에 그 리스트를 보이지 않겠다.

이 구조체를 보고 싶은 사람은 winnt.h를 직접 열어서 보기 바라되 열어보면 괜히 봤다는 생각이 들지도 모르겠다.

Intel CPU의 CONTEXT 구조체에는 Intel CPU의 레지스터 값들을 담는 멤버들이 있다. 이 값을

읽으면 예외가 발생했을 때의 CPU 상태를 그대로 덤프해 볼 수 있을 것이다. 단 그렇게 하면 예외처리의

일부분이 플랫폼에 종속되어 버린다.

==============================================================================================================================

[ 종료 핸들러 ]

종료 핸들러(Termination handler)는 어떠한 상황에서도 특정 부분이 반드시 실행될 것을 보장하는 예외 처리 구조이다.

예외 핸들러와 마찬가지로 컴파일러에 의해 제공되며 다음과 같은 구조를 가진다.

---------------------------------------------------------------------------------------------------------
__try{
 보호구역
}
__finally{
 종료 핸들러
}
---------------------------------------------------------------------------------------------------------

여기서 보호 구역은 종료 처리가 제대로 이루어지지 않을 롹률이 있는 문장이 포함된다.

예를 들어 메모리를 할당한다거나 파일을 오픈한다거나 크리티컬 섹셕으로 들어간다거나 하는 등의 문장이다.

이런 문장들은 반드시 짝을 이루는 해제 문장이 따라 오는데 해제 문장이 종료 핸들러에 배치된다.

이 구조를 사용하면 보호 구역에서 어떠한 원인으로 종료 처리를 하지 못할 상황이 발생하더라도 종료 핸들러에 의해

반드시 종료 처리됨을 보장받을 수 있다.

예외 처리와 다른 점은  예외 핸들러는 예외가 발생하지 않으면 핸들러가 전혀 실행될 필요가 어벖지만

종료 핸들러는 정상적이나 예외시나 항상 실행된다. 종료 핸들러는 사용하는 간단한 실제 예를 보자.

---------------------------------------------------------------------------------------------------------
void func(){
 TCHAR *str;
 str = (TCHAR*)malloc(20);

 if(1/*어떤 조건*/){
  return;
 } 
 free(str);

}
---------------------------------------------------------------------------------------------------------

이 예에서는 메모리를 할당한 후 이 메모리를 사용하고 그리고 해제한다. 코드가 정상적으로 실행된다면

아무 문제가 없겠으나 만약 어떤 조건에 의해 함수가 중간에 리턴되어야 할 상황이 발생한다면

free 함수가 실행되지 못하기 때문에 할당된 메모리가 그대로 할당된 채로 있게 된다.

결과적으로 메모리 누수를 유발하며 시스템 리소스의 부족과 속도 저하로 이어진다.

이런 문장이 확실하게 동작하려면 다음과 같이 종료 핸들러로 싸야 한다.

---------------------------------------------------------------------------------------------------------
void func(){
 TCHAR *str;

 __try{
  str = (TCHAR*)malloc(20);

  if(1/*어떤 조건*/){
   return;
  } 
 }
 __finally{
  free(str);
 }

}
---------------------------------------------------------------------------------------------------------

메모리르 할당하는 부분이 보호 구역으로 설정되어 있으며 메모리를 해제하는 부분이 종료 핸들러로 지정되어 있다.

그래서 함수가 중간에 리턴하거나 혹은 goto 문에 의해 제어가 다른 곳으로 옮겨가더라도 __finally 블록은 반드시

실행된다. 독자중에 조건을 완벽하게 테스트하여 반드시 free까지 가도록 하면 된다거나 리턴하기 전에

free를 실행하면 되지 않느냐고 반문할 사람이 있을 것이다. 하지만 실제 예에서는 항상 그것이 가능하지 않기 때문에

문제가 되는 것이다. 여기서 /*어떤 조건*/이 라고 되어 있는 부분은 실제로는 무척 복잡한 문장이 될 수 있다.

예외를 일으킬 가능성이 있는 문장 일 수도 있고 또는 함수 호출문이라 함수 내부에서 무슨 일이 일어날지 알 수 없는 경우 등등

여러 가지 경우를 가정해 볼 수 있다.

컴파일러는 종료 핸들러가 있으면 어떤 경우라도 종료 핸들러가 호출될 수 있게 코드를 작성한다.

설사 함수 중간에 return 문이 있더라도 함수의 종료를 미루고 종료 핸들러를 호출한 후 리턴하으로써

함수 실행 중 반드시 종료 핸들러를 거쳐가도록 코드를 컴파일 하기 때문에 종료 핸들러를 사용하면 어떠한 경우라도 정상적인

종료 처리가 된다고 보장할 수 있다. 단 스레드나 프로세스가 ExitThread, ExitProcess에 의해 강제로 종료될 때는

예외이다. 이때는 "나 죽었어요"하는 시점이기 때문에 종료 처리고 뭐고 뒤도 안볼아본다.


종료 핸들러는 정상적인 코드 흐름에서도 실행되며 보호 구역에서 예외가 발생하여 일찍종료되었을 때도 실행된다.

만약 이 두 경우를 구분하려면 AbnormalTernimation 함수를 사용한다. 이 함수는 __finally 블록 내에서만 실행될 수 있는

내재 함수이다. 만약 종료 핸들러가 정상적인 흐름을 따라 실행된다면 이 함수는 FALSE를 리턴하고 보호 구역에서 바로

종료핸들러로 넘어 왔다면  이 함수는 TRUE를 리턴한다. 사실 이 함수가 사용되는 실례를 찾기 어렵지만 억지로라도 예를 들어보자.

비정상적인 종료 처리를 할 경우 메시지를 출력하고 싶다면 다음과 같이 한다.

---------------------------------------------------------------------------------------------------------
__finally{
 if(AnormalTernimation() == TRUE)
  MessageBox(NULL,"뭔가 이상하네요. 이 프로그램 쓰지마세요","어라",MB_OK);
 free(str);
}
---------------------------------------------------------------------------------------------------------


종료 핸들러는 다음과 같은 용도로도 사용된다. 만약 한 함수에서 여러 가지 자원을 할당한 후 함수 종료 전에

해제해야 한다고 해 보자. 그러면 코드는 아래와 같이 될 것이다.

<코드 1>
---------------------------------------------------------------------------------------------------------
if(크리티컬 섹션으로 들어감 == 성공){

 if(핸들 열기 == 성공){
  if(메모리 할당 == 성공){
   if(오브젝트 생성 == 성공){
   
    오브젝트 해제
   } 
  
   메모리 해제 
  }

 핸들 닫기
 }


 크리티컬 섹션 나옴
}
---------------------------------------------------------------------------------------------------------

전통적인 방법이긴 하지만 뭔가 복잡해 보이고 지저분해 보인다.


<코드 2>
---------------------------------------------------------------------------------------------------------
if(크리티컬 섹션으로 들어감 == 실패){
 return;
}

if(핸들 열기 == 실패){
 크리티컬 섹션 나옴
 return;
}

if(메모리 할당  == 실패){
 핸들 닫기
 크리티컬 섹션 나옴
 return;
}

if(오브젝트 생성 == 실패){
 메모리 해제
 핸들 닫기
 크리티컬 섹션 나옴
 return;

하고 싶은 일 
오브젝트 해제
---------------------------------------------------------------------------------------------------------

참고로 위의 코드는 그냐양 웃자고 보인 것인데 결과는 동일하지만 0점짜리 코드다.

위의 처럼 짜면 안된다.

<코드 1>은 그나마 네 가지 할당을 해서 그렇지 만약 10개 정도의 자원을 할당한다면

정말 복잡해 보일 것이며 브레이스의 if else의  짝을 찾기 어려울 것이다.  종료 핸들러를 사용한다면

이런 상황은 훨씬 더 깔끔하게 처리할 수 있다.

---------------------------------------------------------------------------------------------------------
__try{
 크리티컬 섹션으로 들어감
 핸들열기
 메모리 할당
 오브젝트 생성
 하고 싶은 일
}
__finally{
 오브젝트 파괴
 메모리 해제
 핸들 닫기
 크리티컬 섹션 나옴
}
---------------------------------------------------------------------------------------------------------

한마디로 모든 종료 처리를 종료 핸드러로 모아놓은 것이다. 이렇게 되면 중간에 한 자원이 할당에 실패하더라도

자원은 제대로 해제된다. 단 __finally 에서는 무조건 자원을 해제해서는 안 되며 할당된 자원에 대해서만 해제를 하도록 조건 점검을 해 보아야 한다.

종료 핸들러를 이런 목적에 사용하는 것을 지원하기 위해 비주얼 C++은 __leave 라는 키워드를 제공한다. 이 문장이 사용되면

__try 블록 내에서 곧바로 __finally 블록으로 제어를 옮겨 종료 처리를 한다. __leave를 쉽게 표현하면 goto __finally 이다.

 

반응형

+ Recent posts