Files Delphi

Title: Advanced exception handler to find the exception source file name, call stack and all other related information
Question: This article describes how to replace the standard Delphi exception handlers with advanced ones in order to get more control over all errors occured during run-time execution. It is possible to collect very detailed information about all occured exceptions and save this information for later use without any interference with end-user. Such feature gives you the ultimate power and ability to track all exceptions down to the source file name, function name and code line number where exception was raised so you can identify the possible bugs or problems in much more convenient and effective way.
Answer:
We provide the following downloads for your convenience:
1. ExWatcher.zip - an archive with ExWatcher component
2. 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:
1. 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.
2. OnDelphiException - handles the exceptions caused by the "raise Exception" operation performs.
Within this handler you can access the Exception class caused this situation.
3. 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.
4. 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
1. Compile the ExWatcherDemo project with the following options:
Compiler: Stack Frames - Enabled;
Linker: Map File - Public.
2. 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