Advanced exception handler to find the exception source file name, call stack and all other related information

By: Clever Components

Abstract: Advanced exception handler to find the exception source file name, call stack and all other related information

We provide the following downloads for your convenience:
  • ExWatcher.zip - an archive with ExWatcher component

  • Demo.zip - an archive with source code for the sample program

  • Abstract

    When your application raises an exception it provides some textual (usually by showing an exception dialog with some description in it) information about exception occurred. Usually this information is not enough for analysing and locating the problem. When your program fails you definitely must find out where and why the problem occurred. So you need the most detailed information about all procedure calls (their names, source lines, CPU registers and so on) led the application failure.



    If you are using safe-call procedures in your program (e.g. in case of using ActiveX controls), Delphi does not set-up COM error info for any system exception - usually all you get is just the E_FAIL result code. In this article we also provided a simple method for handling such errors too.

    Write your own exception handler

    The Microsoft Operation Systems family has very handy engine which will help us to do a job - the Structured Exception Handling (SEH). It allows you to register your own exception handler which will be called by OS if any exception raised. If you want to write down the call stack or collect the values of the CPU registers, you will find that it is not possible because of the standard Delphi exception handles declared in the System.pas unit hides all desired information. To work around this problem you have to replace the standard Delphi handlers with your own ones. In order to replace a handler, we have to look through an exception handler sequence pointed by FS register for Delphi handler (for example, the _ExceptHandler procedure is the second item within the exception handler list). The easiest way to do that is to write few code lines in assembler:

    
    mov eax, fs:[0] 
    mov eax, [eax].TExcFrame.desc
    
    Now eax points to the first code byte of the code of the desired exception handler. Next step we saving an address of old (Delphi original) handler:

    
    mov ecx, cHackSize ; 11 bytes 
    mov esi, eax ; eax points to handler being copied 
    mov edi, edx ; edx points to place where copied bytes will be stored 
    rep movsb 
    ...
    
    Finally we replacing first bytes with the JMP command:

    
    VirtualQuery(pProc, mbi, sizeof(mbi)); 
    VirtualProtect(mbi.BaseAddress, mbi.RegionSize, PAGE_EXECUTE_READWRITE, OldProtect); 
    ... 
    mov eax, pProc ; pProc points to handler being replaced 
    mov ebx, pNewProc ; pNewProc points to a new handler 
    mov byte ptr [eax], $E9 ;jmp instruction 
    sub ebx, eax 
    sub ebx, 5 ;sizeof(jmp xxx) 
    mov dword ptr [eax + 1], ebx 
    ...
    
    So, we placed the 'jmp pNewProc' command over the old handler code.
    Because the code segment is write-protected you should use the VirtualProtect function to erase and then restore this protection.
    Next step will implement the desired exceptions handlers. They can look like:

    
    procedure clHandleAnyException; 
    asm 
       lea eax, pOldHandleAnyExceptionCode 
       push eax 
       push [ebp + 8] 
       push [ebp + 16] 
       call clExWatcherProcImpl 
       pop eax 
       jmp eax 
    end; 
    
    procedure clExWatcherProcImpl(ExcContext: PContext; ExcRecord: PExceptionRecord); stdcall; 
    begin 
       if (ExcRecord^.ExceptionFlags and cUnwindInProgress = 0) then 
       begin 
          if (ExcRecord^.ExceptionCode <> cDelphiException) then 
          begin 
             DoSystemException(..); 
          end else 
          begin 
             DoDelphiException(..); 
          end; 
       end; 
    end;
    
    By checking the values of the TExceptionRecord structure passed in the ExcRecord variable you can distinguish between an operation system and Delphi exception. Also you can find out if the stack unwinding is in progress. After that, your handlers will be called if any exceptions occur. Because Delphi has different handlers for usual procedures and safecall procedures you can have four handlers which will make things easier:

  • OnSystemException - handles the exceptions like "Access Violation" fired by the operation system.

  • Within this handler you will be able to read the CPU registers, get the call stack info and of course find out what kind of exception has just occurred.
  • OnDelphiException - handles the exceptions caused by the "raise Exception" operation performs.

  • Within this handler you can access the Exception class caused this situation.
  • OnDelphiSafeCallException - handles the exceptions caused by "raise Exception" inside any safe-call procedure, e.g. while an ActiveX / COM object method calls.

  • Within this handler you can check an instance of a successor of the Object class whose method has raised this exception.
  • OnSystemSafeCallException - handles the exceptions like "Access Violation" occured inside safe-call procedures.

  • Within this handler you have an opportunity to redirect an exception to the SafeCallException method of the Object class which is used to set the COM error info. If you let this method handle exceptions, the clients of your COM component will be able to find the real problem not just the "Unspecified error" message.

    Working with procedure call stack

    As you probably noticed the information about pointer within the code where exception occurred is not enough to locate and fix the problem caused this exception. By using the TContext structure provided by the SEH engine you can get full call stack, including any operation system calls. To resolve the procedure and function addresses with their source code names you have to use the debug information produced during the compilation of the program.
    There is number of different methods which can help you to walk through the stack frames and resolve procedure names. In this article we used the DbgHelp library shipped by Microsoft. This library allows you to work with system debug symbols information. Unfortunately, Delphi could not produce the debug information in the Microsoft's format, so you have to use other utilities (map2dbg, by Lucian Wischik) which can produce such symbols and also give us other advantages such as ability to access and display the stack of your application and the system stack simultaneously - Delphi's standard debugger cannot to do this. The other way would be writting your own engine to parse Delphi debug symbols and provide user-like appearence of the call stack. This method is used by MemCheck for example. Let's go ahead and study how to unwind procedure call stack using the DbgHelp library.
    The DbgHelp functions work with the TContext exception structure, so in order to extract a function name by using its memory allocation address you can use the following code:

    
    var 
       dwDisplacement: DWORD; 
       buffer: array[0..$1FF] of BYTE; 
       pSymbol: PIMAGEHLP_SYMBOL; 
    begin 
       pSymbol := PIMAGEHLP_SYMBOL(@buffer); 
       pSymbol.SizeOfStruct := sizeof(IMAGEHLP_SYMBOL); 
       pSymbol.MaxNameLength := sizeof(buffer) - sizeof(IMAGEHLP_SYMBOL) + 1; 
       ... 
       if (SymGetSymFromAddr(GetCurrentProcess(), DWORD(pFunc), dwDisplacement, pSymbol)) then 
       begin 
          Result := PChar(@pSymbol.Name); 
       end; 
    end;
    
    Where the pFunc variable points to the address you want to get the symbolic name for.
    The application's debug symbols are loaded automatically by the DbgHelp library during its initialization.
    The following code extracts the next call stack entry for the given context. You can call this code recursively to walk through the whole stack from exception point to any deepness you wish.

    
    while (0 <> StackWalk(IMAGE_FILE_MACHINE_I386, GetCurrentProcess(), GetCurrentThread(), 
       @FStack, pContext, nil, SymFunctionTableAccess, SymGetModuleBase, nil)) then 
    begin 
       frame := TclStackFrame.Create(); 
       frame.CallAddress := Pointer(FStack.AddrPC.Offset); 
    end;
    
    The pContext variable is an instance of the TContext structure describes the thread state and the FStack variable is an instance of the STACKFRAME structure which allows you to iterate through the whole stack.
    For your convenience we implemented all methods for working with stack in the TclStackTracer class. You can find full source code at ExWatcher.zip. There is a simple component with both the events for each exception type and the functions for stack walking. You can use this component in order to collect the exception information and receive it from the application users, e.g. via e-mail or using uploading via internet. Please see our Automatic bug report sending from your application article for more details.

    How to use this Demo program

  • Compile the ExWatcherDemo project with the following options:

  • Compiler: Stack Frames - Enabled;
    Linker: Map File - Public.
  • Run the map2dbg.exe utility in order to produce the file with debug info. Use the command line with the following parameters: map2dbg.exe ExWatcherDemo.exe.


  • Note! You should always distribute your programs (in our case ExWatcherDemo.exe) together with their *.dbg files (in our case ExWatcherDemo.dbg).

    Conclusion

    The map2dbg utility converts Delphi map files to Windows native debug files. The current version of this program converts only information related to procedure names and does not convert the source code line and source file name. If you want to retrieve the information about source code line and source file name you have to use some other programs to produce proper dbg file. Another way would be modifying the source code of the map2dbg program in order to implement the source line information extraction and conversion.
    You can learn more about map2dbg (Ms-dbg) utility provided by Lucian Wischik at http://www.wischik.com/lu/programmer/ web page.
    This code is constantly improved and your comments and suggestions are always welcome.

    Please write us at info@clevercomponents.com

    With best regards,
    Sergey S and Alex P
    Clever Components team.
    www.clevercomponents.com

    Server Response from: ETNASC01