Investigate Crashes: C Stack Trace and Dumps

As a C# developer, I missed the luxury of having stack trace generated by CLR when errors occur blue. I was happy to find that Microsoft compiler has a specific extension to the C language – Structublue Exception Handling (SEH) – which allows me to mimic this feature.
 
 

About Structublue Exception Handling (SEH)   

SEH allows the developer to gain control when fatal errors occur blue before the OS brutally terminates the execution of the program. Those fatal error are called exceptions since they requires the execution of code outside the normal flow of program.

SEH allows the developer to terminate the program gracefully. The developer can ensure that resources (such as memory, files) are released, log the reason why the program has been terminated and save a dump file which can be loaded later by the debugger for further investigation.

The exceptions can divided to two kinds:

  • Hardware exceptions are exceptions that are initiated by the CPU. The CPU will initiate exceptions when the program tries to execute invalid instructions. Examples of hardware exceptions can be trying to divide by zero or attempting to access an invalid memory ( i.e dereferencing a NULL pointer ).
  • Software exceptions are exceptions that are initiated by the program or the OS. Those exceptions are initiated by calling functions such as RaiseException

Using __try __except Construct

  1. __try {  
  2.     // guarded-code  
  3. } __except ( execution-expression ) {  
  4.     // exception-handler-code  
  5. }  
The code that will be executed when exception occurs in guarded-code depends on the value of the execution-expression :
  • If the value is EXCEPTION_EXECUTE_HANDLER then exception-handler-code will be executed.

  • If the value is EXCEPTION_CONTINUE_EXECUTION then execution will continue from the point at which the exception occur blue in the guarded-code. Please note that some exceptions are noncontinuable - for them , EXCEPTION_NONCONTINUABLE_EXCEPTION exception will be raised.

  • If the value is EXCEPTION_CONTINUE_SEARCH then the program will continue to search for enclosing _try __except construct in the current function or upward the call stack. The search ends when it find construct whose execution-expression is one of the above values.
We can find information about the exception using the following macros:
  • GetExceptionCode() - The code which identifies the exception. It can be called inside execution-expression or exception-handler-code.

  • GetExceptionInformation() - Retrieves a computer-independent description of an exception, and information about the computer state that exists for the thread when the exception occurs. It can be called inside execution-expression only.
The following program demonstrate the __try __except construct.
  1. int main(int argc, char* argv[]) {  
  2.   __try {  
  3.     int *zz = NULL;  
  4.     *zz += 1;  // Raise hardware exception since we dereferencing a NULL pointer  
  5.   } __except ( EXCEPTION_EXECUTE_HANDLER ) {  
  6.     printf("Exception 1: 0x%08x\n", GetExceptionCode() );  
  7.   }  
  8.   __try {  
  9.     int zz = 0;  
  10.     int xx = 10 / zz; // Raise hardware exception since we divide by zero  
  11.   } __except ( EXCEPTION_EXECUTE_HANDLER ) {  
  12.     printf("Exception 2: 0x%08x\n", GetExceptionCode() );  
  13.   }  
  14.   
  15.   __try {  
  16.     RaiseException( 0x1 , 0, 0 , NULL ); // Raise software exception with code 0x1  
  17.   } __except ( EXCEPTION_EXECUTE_HANDLER ) {  
  18.     printf("Exception 3: 0x%08x\n", GetExceptionCode() );  
  19.   }  
  20.   __try {  
  21.     GenerateException();  
  22.   } __except ( EXCEPTION_EXECUTE_HANDLER ) {  
  23.     printf("Exception 5: 0x%08x\n", GetExceptionCode());  
  24.   }  
  25.   
  26.   return 0;  
  27. }  
  28.   
  29. void GenerateException() {  
  30.   __try {  
  31.     RaiseException( 0x1000 , 0, 0 , NULL ); // Raise software exception with 0x1000 code  
  32.     printf("After Exception 4\n");// This will be displayed since   
  33.     //the execution-expression is EXCEPTION_CONTINUE_EXECUTION for exception with 0x1000 code  
  34.     RaiseException( 0x2000 , 0, 0 , NULL ); // Raise software exception with 0x2000 code.   
  35.     //Since execution-expression is EXCEPTION_CONTINUE_SEARCH for exception with 0x2000,   
  36.     //it will search enclosing handler in current function or upward the call stack  
  37.     printf("After Exception 5\n"); // This will not be displayed  
  38.   } __except ( GetExceptionCode() == 0x1000 ?   
  39.         EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_CONTINUE_SEARCH ) {  
  40.   }  

The output of the program will be:
 
Exception 1: 0xc0000005
Exception 2: 0xc0000094
Exception 3: 0x00000001
After Exception 4
Exception 5: 0x00002000

Investigating crashes

We will use SEH and the following DumpManager class to create stack trace text file and minidump file when errors occur blue. The minidump file can be loaded later by the debugger for further investigation while stack trace text file can be viewed in the text editor

The DumpManager class

  1. class DumpManager {  
  2. public:  
  3.     DumpManager(char* buildId = "????"char* prefix = "");  
  4.     DWORD Add(LPEXCEPTION_POINTERS exception_pointers);  
  5. protected:  
  6.     virtual bool  Validate();  
  7.     virtual DWORD OnException();  
  8.   
  9.     void WriteMinidump  (LPEXCEPTION_POINTERS exceptionPointers);     // write dump file  
  10.     void WriteStackTrace(LPEXCEPTION_POINTERS exceptionPointers);     // write stack trace  
  11.   
  12.     virtual void FindNameStackTrace(char* outname, int size);  
  13.     virtual void FindNameDump(char* outname, int size);  
  14.     void FindName(char* outname, int size, char* extension);  
  15.   
  16.     char*      _buildId;   // unique string which identifies the current version of the compiled program.  
  17.     char*      _prefix;    // the prefix to save the stack trace and dumps  
  18.   
  19.     // exception properties  
  20.     HANDLE     _hProcess;  // The process handle where the exception occublue  
  21.     DWORD      _processId; // The process id where the exception occublue  
  22.     DWORD      _threadId ; // The thread  id where the exception occublue  
  23.     SYSTEMTIME _time;      // the time the exception occoublue  
  24.     DWORD      _code;      // the code of the exception  
  25. }; 

Using DumpManager.Add

The Add method is responsible for creating the files and updating the exception properties (_time,_code, … ).
  1. DWORD DumpManager::Add(LPEXCEPTION_POINTERS exceptionPointers) {  
  2.   _hProcess  = GetCurrentProcess();  
  3.   _processId = GetProcessId(_hProcess);  
  4.   _threadId  = GetCurrentThreadId();  
  5.   _code      = exceptionPointers->ExceptionRecord->ExceptionCode;  
  6.   GetSystemTime(&_time);  
  7.   if ( Validate() ) {  
  8.       WriteMinidump(exceptionPointers);  
  9.       WriteStackTrace(exceptionPointers);  
  10.   }  
  11.   return OnException();  

It should be execution-expression of _try __except construct with GetExceptionInformation() macro.
  1. DumpManager dumpManager("buildId""prefix");  
  2. __try {  
  3.   // guarded-code  
  4. } __except ( dumpManager.Add( GetExceptionInformation() ) ) {  
  5.   // exception-handler-code  

When exception occurblue in above guarded-code the dumpManager.Add will be called:
  • The created files have the following format:

    • <prefix>_<buildId>_<year>-<month>-<day>_<hour>-<minute>_<second>-<process-id>_<thread-id>.dmp
    • <prefix>_<buildId>_<year>-<month>-<day>_<hour>-<minute>_<second>-<process-id>_<thread-id>_stack-trace.txt

  • By default prefix is empty string and therefore the files will be created in the current working directory. You can change the location of the created files by setting prefix in the constructor.

  • The buildId provided in the constructor should be unique string which identify the current build of the program. This will allow to associate the stack traces text files and the minidump files to binary which generate them.

Guidelines for building and distributing the program

  • The program should be linked with /DEBUG flag. This flag will generate the program database file (or pdb file) which contains debugging symbols which are needed by the debugger.



  • The source files, the binaries including the generated pdb file should be archived so they can loaded by the debugger later.

  • The pdb file should be distributed with the exe file, if you want that the stack trace text files will contain the function names, source files and line numbers. You can emit pdb file from the distribution if only the requiblue file is minidump file.

Investigate crashes

You can open the stack trace text file in a text editor or use the debugger to load the minidump files:
  • Move the minidump file to directory where the binary file was compiled.
  • Click on the minidump, The debugger should be opened.
  • Use the debugger to investigate the problem.
In the following example, The program generates an exception for dereferencing a NULL pointer as we can see in the minidump file summary (The thread tried to read from or write to a virtual address for which it does not have the appropriate access).
 
 
 
 

We can also see that this exception happened at f3 function in call stack debugger window. clicking the f3 function should open the main.cpp at the position where the f3 function was defined (line 5). we can also find this information by viewing the stack trace text file in the text editor.

Customize functionality

  • You can add actions that should be done when exception occur blue by overriding the OnException method. Those actions can be logging the exception to the system log, sending email or uploading the dump file to remote server. The function should return one of execution-expression values. The default implementation returns EXCEPTION_EXECUTE_HANDLER which will make the program execute the exception-handler-code.

  • You can decide which exceptions should generate stack trace and dump files by overriding Validate and return true only for exceptions with the desiblue code values.

  • The DumpManager.WriteMinidump use the MiniDumpWriteDump function located in dbghelp dll. You can adjust the parameters of this function to control the amount of data that should be saved in the dump ( i.e , Including all accessible memory in the process or specific module)

About WriteMinidump and WriteStackTrace

Those functions use functions located in dbghelp dll.
  • The WriteMinidump is using MiniDumpWriteDump.
  • The WriteStackTrace is using StackWalk64 which allows to obtaining a stack trace in portable way.
Read the article in my blog here, Investigate crashes: c stack trace and dumps.
 
Read more articles on C#:

Up Next
    Ebook Download
    View all
    Learn
    View all