Samstag, 26. März 2011

Exception Handling - Inform your users! Part 2

Hello again

In the previous article (Part 1) i explained how you provide some basic information about an unhandled exception to the user and announced that in the next part we will have a look on how to get more detailed information. Now thats exactly what we will do in this article. Ill show two methods to gather more information what happened and why it happened.

Method 1: Do it manually
For this method we will create several helper functions that handle one or more types of exceptions. We start with the following code:
#include <Windows.h>
#include <iostream>

LONG WINAPI UnhandledException(LPEXCEPTION_POINTERS exceptionInfo)
{
 return EXCEPTION_EXECUTE_HANDLER;
}

int main()
{
 SetUnhandledExceptionFilter(UnhandledException);
}

The first function we make will return a name which describes the type of exception that happened. Writing that function is pretty easy if we use macro that helps us:
#define EX_CASE(code) \
 case code:\
  return #code;

LPCSTR GetExceptionName(DWORD code)
{
 switch(code)
 {
 EX_CASE(EXCEPTION_ACCESS_VIOLATION);
 EX_CASE(EXCEPTION_DATATYPE_MISALIGNMENT);
 EX_CASE(EXCEPTION_BREAKPOINT);
 EX_CASE(EXCEPTION_SINGLE_STEP);
 EX_CASE(EXCEPTION_ARRAY_BOUNDS_EXCEEDED);
 EX_CASE(EXCEPTION_FLT_DENORMAL_OPERAND);
 EX_CASE(EXCEPTION_FLT_DIVIDE_BY_ZERO);
 EX_CASE(EXCEPTION_FLT_INEXACT_RESULT);
 EX_CASE(EXCEPTION_FLT_INVALID_OPERATION);
 EX_CASE(EXCEPTION_FLT_OVERFLOW);
 EX_CASE(EXCEPTION_FLT_STACK_CHECK);
 EX_CASE(EXCEPTION_FLT_UNDERFLOW);
 EX_CASE(EXCEPTION_INT_DIVIDE_BY_ZERO);
 EX_CASE(EXCEPTION_INT_OVERFLOW);
 EX_CASE(EXCEPTION_PRIV_INSTRUCTION);
 EX_CASE(EXCEPTION_IN_PAGE_ERROR);
 EX_CASE(EXCEPTION_ILLEGAL_INSTRUCTION);
 EX_CASE(EXCEPTION_NONCONTINUABLE_EXCEPTION);
 EX_CASE(EXCEPTION_STACK_OVERFLOW);
 EX_CASE(EXCEPTION_INVALID_DISPOSITION);
 EX_CASE(EXCEPTION_GUARD_PAGE);
 EX_CASE(EXCEPTION_INVALID_HANDLE);

 case 0xE06D7363:
  return "C++ Exception";
 
 default:
  return "Unknown exception";
 }
}

#undef EX_CASE

The next helper function will take an exception address an return in which module the exception occurred. This is done by looping through the loaded modules and comparing which one is the last with its base address before the exception location. Then we use GetModuleFileName to retrieve the name of the module.
#include <windows.h>
#include <iostream>
#include <psapi.h>

#pragma comment(lib, "psapi.lib")

// Definition of function GetExceptionName....


HMODULE GetExceptionModule(LPVOID address, LPSTR moduleName /* must support MAX_PATH characters */)
{
 HMODULE moduleList[1024];
 DWORD sizeNeeded = 0;
 if(FALSE == EnumProcessModules(GetCurrentProcess(), moduleList, 1024, &sizeNeeded) || sizeNeeded < sizeof(HMODULE))
  return NULL;

 int curModule = -1;
 for(int i = 0; i < (sizeNeeded / sizeof(HMODULE)); ++i)
 {
  if((DWORD)moduleList[i] < (DWORD)address)
  {
   if(curModule == -1)
    curModule = i;
   else
   {
    if((DWORD)moduleList[curModule] < (DWORD)moduleList[i])
     curModule = i;
   }
  }
 }

 if(curModule == -1)
  return NULL;

 if(!GetModuleFileName(moduleList[curModule], moduleName, MAX_PATH))
  return NULL;

 return moduleList[curModule];
}
We can test those two functions now and see what result we get using the following code:
// previous code...

LONG WINAPI UnhandledException(LPEXCEPTION_POINTERS exceptionInfo)
{
 char message[MAX_PATH + 255];
 char module[MAX_PATH];
 char* moduleName = NULL;
 if(GetExceptionModule(exceptionInfo->ExceptionRecord->ExceptionAddress, module))
  moduleName = module;
 else
  moduleName = "Unknown module!";

 sprintf_s(message, 
  "An exception has occured which was not handled!\nCode: %s\nModule: %s", 
  GetExceptionName(exceptionInfo->ExceptionRecord->ExceptionCode),
  moduleName
 );

 MessageBox(0, message, "Error!", MB_OK);
 return EXCEPTION_EXECUTE_HANDLER;
}

int main()
{
 SetUnhandledExceptionFilter(UnhandledException);

 int b = 0;
 int a = 2 / b;
}
The messagebox now should display the name of your executable. The EXCEPTION_RECORD structure inside exceptionInfo contains an array of additional data that can be used to provide more information. Sadly those values are not defined by a standard. Its up to every compiler to do whatever it likes to. There are only 2 codes for which additional information is defined in a standardized way. EXCEPTION_ACCESS_VIOLATION and EXCEPTION_IN_PAGE_ERROR. We will now write a function that handles EXCEPTION_ACCESS_VIOLATION separately and provide a function that handles all the other codes.
void HandleAccessViolation(LPEXCEPTION_POINTERS info)
{
 char message[MAX_PATH + 512];
 char module[MAX_PATH];
 char* moduleName = NULL;
 if(GetExceptionModule(info->ExceptionRecord->ExceptionAddress, module))
  moduleName = module;
 else
  moduleName = "Unknown module!";

 DWORD codeBase = (DWORD)GetModuleHandle(NULL);
 DWORD offset = (DWORD)info->ExceptionRecord->ExceptionAddress - codeBase;

 char* accessType = NULL;
 switch(info->ExceptionRecord->ExceptionInformation[0])
 {
 case 0:
  accessType = "Read";
  break;
 case 1:
  accessType = "Write";
  break;
 case 2:
  accessType = "Execute";
  break;
 default:
  accessType = "Unknown";
  break;
 }

 sprintf_s(message, 
  "An exception has occured which was not handled!\nCode: %s\nModule: %s\n"\
  "The thread %u tried to %s memory at address 0x%08X which is inaccessible!\n"\
  "Offset: 0x%08X\nCodebase: 0x%08X",
  GetExceptionName(info->ExceptionRecord->ExceptionCode),
  moduleName,
  GetCurrentThreadId(),
  accessType,
  info->ExceptionRecord->ExceptionInformation[1],
  offset,
  codeBase
 );

 MessageBox(0, message, "Error!", MB_OK);
}

void HandleCommonException(LPEXCEPTION_POINTERS info)
{
 char message[MAX_PATH + 512];
 char module[MAX_PATH];
 char* moduleName = NULL;
 if(GetExceptionModule(info->ExceptionRecord->ExceptionAddress, module))
  moduleName = module;
 else
  moduleName = "Unknown module!";

 DWORD codeBase = (DWORD)GetModuleHandle(NULL);
 DWORD offset = (DWORD)info->ExceptionRecord->ExceptionAddress - codeBase;

 sprintf_s(message, 
  "An exception has occured which was not handled!\nCode: %s\nModule: %s\n"\
  "Offset: 0x%08X\nCodebase: 0x%08X",
  GetExceptionName(info->ExceptionRecord->ExceptionCode),
  moduleName,
  offset,
  codeBase
 );

 MessageBox(0, message, "Error!", MB_OK);
}

LONG WINAPI UnhandledException(LPEXCEPTION_POINTERS exceptionInfo)
{
 switch(exceptionInfo->ExceptionRecord->ExceptionCode)
 {
 case EXCEPTION_ACCESS_VIOLATION:
  HandleAccessViolation(exceptionInfo);
  break;
 default:
  HandleCommonException(exceptionInfo);
  break;
 }
 return EXCEPTION_EXECUTE_HANDLER;
}
Well, thats it! Now we have handled access violations and also a default handling for other exceptions. Method 2: Let windows do the thing! Windows provides a method to create a dump file from the current execution context. This will include a dump of the memory as well as other information visual studio can use to display all the information necessary. The function is called inside the exception handler. Its very easy so i wont tell much about it. It will create a .dmp file which can be opened with Visual Studio. When you start debugging (using the green arrow) you will get all the information needed. Here is the corresponding code:
#include 
#include 
#include 
#include 

#pragma comment(lib, "dbghelp.lib")

LONG WINAPI UnhandledException(LPEXCEPTION_POINTERS exceptionInfo)
{
 char timeStr[255];
 time_t t = time(NULL);
 tm* tM = localtime(&t);
 strftime(timeStr, 255, "CrashDump_%d_%m_%Y_%H_%M_%S.dmp", tM);
 HANDLE hFile = CreateFile(
  timeStr, 
  GENERIC_WRITE | GENERIC_READ,
  0,
  NULL,
  CREATE_ALWAYS,
  0,
  NULL
 );

 if(hFile != NULL)
 {
  MINIDUMP_EXCEPTION_INFORMATION info = 
  {
   GetCurrentThreadId(),
   exceptionInfo,
   TRUE
  };

  MiniDumpWriteDump(
   GetCurrentProcess(),
   GetCurrentProcessId(),
   hFile,
   MiniDumpWithIndirectlyReferencedMemory,
   &info,
   NULL,
   NULL
  );

  CloseHandle(hFile);
 }

 return EXCEPTION_EXECUTE_HANDLER;
}

int main()
{
 SetUnhandledExceptionFilter(UnhandledException);

 int b = 0;
 int a = 2 / b;
}


Now you have 2 good methods to retreive vital information about errors that happened.

Thanks for reading and bye
Yanick

1 Kommentar: