본문 바로가기
디버그

[펌] Debugging Tips (2) - Access Violation 핸들링하기

by WeZZ 2012. 2. 1.

map 파일과 cod 파일을 사용해 분석하는 방법 을 익혔다고 해도 정작 Exception이 발생한 주소를 모른다면 소용없다.

그래서... Memory Fault 등이 터졌을 때 이것을 로그에 기록한 후 죽도록 구현한다면 디버깅에 매우 도움이 될 것이다.
Memory Fault 와 같은 Critical Exception을 핸들링하는 방법은 크게 두가지가 있다.
이런 방법들을 잘 이용하면 Memory Fault가 발생했을 때 죽지 않고 계속 실행하게 하는 것도 가능할 것이다.

1. SEH (Standard Exception Handling) : __try ~ __except 혹은 __try ~ __finally 구문
__try는 C++의 try ~ catch 구문과는 다르다.
(SEH에 관한 자세한 내용은 http://serious-code.net/moin.cgi/SEH 를 참고.)
__try를 사용하면 Memory Fault 를 포함한 모든 Exception을 핸들링할 수 있다. 코드는 대략 다음과 같이 된다.

  1. DWORD MakeLog(LPEXCEPTION_POINTERS lpException)
  2. {
  3. printf("Exception : 0x%08X\r\n",
  4. lpException->ExceptionRecord->ExceptionCode);
  5. printf("Exception Address : 0x%08p\r\n",
  6. lpException->ExceptionRecord->ExceptionAddress);
  7. return EXCEPTION_EXECUTE_HANDLER;
  8. }
  9. int _tmain(int argc, _TCHAR* argv[])
  10. {
  11. __try {
  12. printf ("Before Exception\r\n");
  13. *(LPINT)NULL = 1; // Exception
  14. // StartWork(); // 진짜 EntryPoint를 여기서 호출해줄 수도 있다.
  15. printf ("After Exception\r\n");
  16. } __except (MakeLog(GetExceptionInformation()))
  17. {
  18. //SetLastError(ERROR_...);
  19. return FALSE // 실패!!
  20. }
  21. return TRUE; // 성공했음.
  22. }

실행 결과는 다음과 같이 된다.

Before Exception
Exception : 0xC0000005
Exception Address : 0x00401047

로그고 뭐고 필요 없고 무조건 죽지만 않으면 된다면 다음과 같이..
  1. int _tmain(int argc, _TCHAR* argv[])
  2. {
  3. __try {
  4. printf ("Before Exception\r\n");
  5. *(LPINT)NULL = 1; // Exception
  6. printf ("After Exception\r\n");
  7. } __except (EXCEPTION_EXECUTE_HANDLER)
  8. {
  9. //SetLastError(ERROR_...);
  10. return FALSE // 실패!!
  11. }
  12. return TRUE; // 성공했음.
  13. }

위의 __except 부분의 상수를
-. EXCEPTION_EXECUTE_HANDLER 는 예외처리루틴을 실행하라는 뜻이다. 여기서 말하는 예외처리루틴이란
__except 블럭 내의 코드를 말한다.
-. EXCEPTION_CONTINUE_SEARCH 로 설정하면 try~catch 에서 예외를 던지는 것과 같은 효과가 된다.
즉, 바깥 함수에 다시 __try ~ 구문이 기다리고 있지 않다면 프로그램은 예외를 내게 된다는 의미.
-. EXCEPTION_CONTINUE_EXECUTION 로 설정하면 예외가 발생한 라인부터 다시 실행을 시도한다.
만약 예외가 해결되지 않은 상황에서 다시 실행을 시도한다면?? 무한루프가 발생한다.

만약 Access Violation만 핸들링하고 싶다면 아래와 같이 하면 된다.
(※ EXCEPTION_EXECUTE_HANDLER의 Define값은 1이다)
  1. int _tmain(int argc, _TCHAR* argv[])
  2. {
  3. __try {
  4. printf ("Before Exception\r\n");
  5. *(LPINT)NULL = 1; // Exception
  6. printf ("After Exception\r\n");
  7. } __except (GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION)
  8. {
  9. //SetLastError(ERROR_...);
  10. return FALSE // 실패!!
  11. }
  12. return TRUE; // 성공했음.
  13. }


SEH를 사용하는 방법의 장점이라면...
- 구조화된 예외처리를 수행하기 때문에 코드가 깔끔해지는다
- 원하는 함수에만 한정하여 적용할 수 있다.
단점이라면...
- 퍼포먼스가 무진장 떨어진다.
- 소멸자가 정의된 클래스의 인스턴스를 __try 블럭 안에서 생성하게 되면 컴파일 에러가 발생하며,
try ~ catch 구문과 함께 사용할 수 없고... 등등의 제한이 있다.



2. SetUnhandledExceptionFilter API를 사용하는 방법

SetUnhandledExceptionFilter 함수를 사용해 Unhandled Exception이 발생할 때 호출될 Exception Handler를 등록할 수 있다. 일단 Exception Handler가 등록되면 Exception이 발생했을 때 Callback된다.

아래의 코드는 상기 샘플코드와 동일한 결과를 나타낸다.
실제로 사용해보면.. __try 구문보다 여러모로 훨씬 편하고 좋다.
단, 이 방법을 사용할 경우 로그를 기록하고 죽게 할수는 있지만, 예외를 핸들링해서 게속 동작하게 할 수는 없는 것 같다.
  1. LONG CALLBACK UnhandledExceptionHandler(EXCEPTION_POINTERS *lpExceptionInfo)
  2. {
  3. printf("Exception : 0x%08X\r\n",
  4. lpExceptionInfo->ExceptionRecord->ExceptionCode);
  5. printf("Exception Address : 0x%08p\r\n",
  6. lpExceptionInfo->ExceptionRecord->ExceptionAddress);
  7. return EXCEPTION_EXECUTE_HANDLER;
  8. }
  9. int _tmain(int argc, _TCHAR* argv[])
  10. {
  11. SetUnhandledExceptionFilter(UnhandledExceptionHandler);
  12. printf ("Before Exception\r\n");
  13. *(LPINT)NULL = 1;
  14. printf ("After Exception\r\n");
  15. return FALSE;
  16. }


    출처 : http://kuaaan.tistory.com/103