Best Way To Investigate crashes in C [How To Create Stack Trace and Dump]

One of the valuable techniques to investigate crashes in C is generating stack trace and dumps when errors happen. Unfortunately, it is not a built-in feature of the C language. On the contrary, other languages, such as C# (with its CLR runtime) or Java (with its JVM runtime), provide auto-generated stack traces. Therefore, It is easier for the C# or Java developers to eliminate bugs and investigate problems when an exception is raised, or an error occurs.

This article shows how to investigate crashes in C in the Windows platform. We see how to generate stack trace in C and to auto-generate dumps of programs written in C/C++ language when an error occurs.

Structured Exception Handling (SEH)

First, Let’s see what are Structured Exception Handling (SEH) and __try __except construct, a specific extension of Microsoft C/C++ compiler to the C/C++ language.

Structured Exception Handling (SEH) is a technique that allows the programmer to gain control when fatal errors occurred when the program is running before the operating system gain control and brutally terminates the execution of the program. Those fatal errors require the execution of code outside the normal flow of the program and are therefore called exceptions.

Structured Exception Handling (SEH) allows the developer to terminate the program gracefully. Using this feature, The programmer can verify that resources (such as memory and files) are released. The programmer can also log why the program has been terminated and generate a dump file and stack trace. The dump file can be loaded later by the debugger to investigate further the bug that causes the crash.

There are 2 types of exceptions:

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

The __try __except Construct

The __try __except allows to handle the exception

__try __except construct
 __try {
     // guarded-code
 } __except ( execution-expression ) {
     // exception-handler-code
 }

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_CONTINUE_EXECUTION then execution will continue from the point at which the exception occurred 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_EXECUTE_HANDLER then exception-handler-code will be executed.
  • 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.

Find information about the exception

We can find information about the exception using the following macros:

  • 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.
  • GetExceptionCode() – The code which identifies the exception. It can be called inside execution-expression or exception-handler-code

Sample of __try __except construct

The following program denonstrate the __try __except construct.

Sample of __try __except construct
 int main(int argc, char* argv[]) {
     __try {
         int *zz = NULL;
         *zz += 1;  // Raise hardware exception since we dereferencing a NULL pointer
     } __except ( EXCEPTION_EXECUTE_HANDLER ) {
         printf("Exception 1: 0x%08x\n", GetExceptionCode() );
     }
     __try {
         int zz = 0;
         int xx = 10 / zz; // Raise hardware exception since we divide by zero
     } __except ( EXCEPTION_EXECUTE_HANDLER ) {
         printf("Exception 2: 0x%08x\n", GetExceptionCode() );
     }
     __try {
         RaiseException( 0x1 , 0, 0 , NULL ); // Raise software exception with code 0x1
     } __except ( EXCEPTION_EXECUTE_HANDLER ) {
         printf("Exception 3: 0x%08x\n", GetExceptionCode() );
     }
     __try {
         GenerateException();
     } __except ( EXCEPTION_EXECUTE_HANDLER ) {
         printf("Exception 5: 0x%08x\n", GetExceptionCode());
     }
     return 0;
 }

 void GenerateException() {
     __try {
         RaiseException( 0x1000 , 0, 0 , NULL ); // Raise software exception with 0x1000 code
         printf("After Exception 4\n");// This will be displayed since
         //the execution-expression is EXCEPTION_CONTINUE_EXECUTION for exception with 0x1000 code
         RaiseException( 0x2000 , 0, 0 , NULL ); // Raise software exception with 0x2000 code.
         //Since execution-expression is EXCEPTION_CONTINUE_SEARCH for exception with 0x2000,
         //it will search enclosing handler in current function or upward the call stack
         printf("After Exception 5\n"); // This will not be displayed
     } __except ( GetExceptionCode() == 0x1000 ?
         EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_CONTINUE_SEARCH ) {
     }
 }
The output of the program
 Exception 1: 0xc0000005
 Exception 2: 0xc0000094
 Exception 3: 0x00000001
 After Exception 4
 Exception 5: 0x00002000

Minidump and Stack Trace with DumpManager

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

Generate stack trace in C/C++ with DumpManager

We can use the following DumpManager class to generate stack trace and dumps

DumpManager.h
 class DumpManager {
 public:
     DumpManager(char* buildId = "????", char* prefix = "");
     DWORD Add(LPEXCEPTION_POINTERS exception_pointers);
 protected:
     virtual bool  Validate();
     virtual DWORD OnException();

     void WriteMinidump  (LPEXCEPTION_POINTERS exceptionPointers);     // write dump file
     void WriteStackTrace(LPEXCEPTION_POINTERS exceptionPointers);     // write stack trace

     virtual void FindNameStackTrace(char* outname, int size);
     virtual void FindNameDump(char* outname, int size);
     void FindName(char* outname, int size, char* extension);

     char*      _buildId;   // unique string which identifies the current version of the compiled program.
     char*      _prefix;    // the prefix to save the stack trace and dumps

     // exception properties
     HANDLE     _hProcess;  // The process handle where the exception occured
     DWORD      _processId; // The process id where the exception occured
     DWORD      _threadId ; // The thread  id where the exception occured
     SYSTEMTIME _time;      // the time the exception occoured
     DWORD      _code;      // the code of the exception
 };

Using DumpManager.Add

The Add method is responsible for creating the files and updating the exception properties (_time,_code, … ).

DumpManager::Add
 DWORD DumpManager::Add(LPEXCEPTION_POINTERS exceptionPointers) {
     _hProcess  = GetCurrentProcess();
     _processId = GetProcessId(_hProcess);
     _threadId  = GetCurrentThreadId();
     _code      = exceptionPointers->ExceptionRecord->ExceptionCode;
     GetSystemTime(&_time);
     if ( Validate() ) {
         WriteMinidump(exceptionPointers);
         WriteStackTrace(exceptionPointers);
     }
     return OnException();
 }

It should be execution-expression of _try __except construct and should be called with GetExceptionInformation() macro.

DumpManager usage
 DumpManager dumpManager("buildId", "prefix");
 __try {
     // guarded-code
 } __except ( dumpManager.Add( GetExceptionInformation() ) ) {
     // exception-handler-code
 }

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

  • __--_-_-_.dmp
  • __--_-_-__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 be 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 required file is minidump file.

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).

The debugger with loaded dump file
The debugger with loaded dump file
The debugger is ready for investigation. The dump file is loaded and the c stack trace text file is displayed. Also call stack window and source code editor are opend.
The debugger is ready for investigation.
The dump file is loaded and the c stack trace text file is displayed. Also call stack window and source code editor are opend.

We can also see that this exception happened at f3 fuction 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 infomation by viewing the stack trace text file in the text editor.

You can add actions that should be done when exception occurred 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 implemention 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 which desired _code values.

Investigate crashes in C

We learn about Structured Exception Handling(SEH) and how to use it with the __try and __except construct. Next, we see how we can generate a stack trace and dump when fatal errors occur in your program before the operating system gains control and brutally terminates the program’s execution. Next, we see how to build the program to support generating this helpful information. Finally, we see how to use the stack trace files and the dump files to investigate crashes in C in the Windows platform.

Further Reading

In the above DumpManager. WriteStackTrace and DumpManager.WriteMinidump , we used functions located in dbghelp dll

  • In the WriteStackTrace is using StackWalk64 which allows to obtaining a stack trace in portable way.
  • The DumpManager.WriteMinidump use the MiniDumpWriteDump function. 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).

Faq

What Structured Exception Handling (SEH)?

Structured Exception Handling (SEH) is a technique that allows the programmer to gain control when fatal errors occurred when the program is running before the operating system gain control and brutally terminates the execution of the program. Read more

What is __try __except construct in C/C++ Language?

The __try __except construct allows the developer to gain control when fatal errors occurred when the program is running before the operating system gain control and brutally terminates the execution of the program. Read more

Leave a Reply

Your email address will not be published. Required fields are marked *