delphiexceptiondelphi-5minidumpdbghelp

How to get the EXCEPTION_POINTERS during an EExternal exception?


How do i get the EXCEPTION_POINTERS, i.e. both:

data during an EExternal exception?

Background

When Windows throws an exception, it passes a PEXCEPTION_POINTERS; a pointer to the exception information:

typedef struct _EXCEPTION_POINTERS {
   PEXCEPTION_RECORD ExceptionRecord;
   PCONTEXT          ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

When Delphi throws me an EExternal exception, it only contains half that information, the PEXCEPTION_RECORD only:

EExternal = class(Exception)
public
  ExceptionRecord: PExceptionRecord;
end;

How, during an EExternal exception, do i get both?

Example Usage

i am trying to write a Minidump using MiniDumpWriteDump function from Delphi.

The function has a few optional parameters:

function MiniDumpWriteDump(
    hProcess: THandle; //A handle to the process for which the information is to be generated.
    ProcessID: DWORD; //The identifier of the process for which the information is to be generated.
    hFile: THandle; //A handle to the file in which the information is to be written.
    DumpType: MINIDUMP_TYPE; //The type of information to be generated.
    {in, optional}ExceptionParam: PMinidumpExceptionInformation; //A pointer to a MINIDUMP_EXCEPTION_INFORMATION structure describing the client exception that caused the minidump to be generated.
    {in, optional}UserStreamParam: PMinidumpUserStreamInformation;
    {in, optional}CallbackParam: PMinidumpCallbackInformation): Boolean;

At a basic level i can omit the three optional parameters:

MiniDumpWriteDump(
    GetCurrentProcess(), 
    GetCurrentProcessId(),
    hFileHandle,
    nil,  //PMinidumpExceptionInformation
    nil,
    nil);

and it succeeds. The downside is that the minidump is missing the exception information. That information is (optionally) passed using the 4th miniExceptionInfo parameter:

TMinidumpExceptionInformation = record
    ThreadId: DWORD;
    ExceptionPointers: PExceptionPointers;
    ClientPointers: BOOL;
end;
PMinidumpExceptionInformation = ^TMinidumpExceptionInformation;

This is good, except i need a way to get at the EXCEPTION_POINTERS that is supplied by Windows when an exception happens.

The TExceptionPointers structure contains two members:

EXCEPTION_POINTERS = record
   ExceptionRecord : PExceptionRecord;
   ContextRecord : PContext;
end;

i know that Delphi's EExternal exception is the base of all "Windows" exceptions, and it contains the needed PExceptionRecord:

EExternal = class(Exception)
public
  ExceptionRecord: PExceptionRecord;
end;

But it doesn't contain the associated ContextRecord.

Isn't PEXCEPTION_RECORD good enough?

If i try to pass the EXCEPTION_POINTERS to MiniDumpWriteDump, leaving ContextRecord nil:

procedure TDataModule1.ApplicationEvents1Exception(Sender: TObject; E: Exception);
var
   ei: TExceptionPointers;
begin
   if (E is EExternal) then
   begin
      ei.ExceptionRecord := EExternal(E).ExceptionRecord;
      ei.ContextRecord := nil;
      GenerateDump(@ei);
   end;

   ...
end;

function GenerateDump(exceptionInfo: PExceptionPointers): Boolean;
var
   miniEI: TMinidumpExceptionInformation;
begin
   ...

   miniEI.ThreadID := GetCurrentThreadID();
   miniEI.ExceptionPointers := exceptionInfo;
   miniEI.ClientPointers := True;

   MiniDumpWriteDump(
       GetCurrentProcess(), 
       GetCurrentProcessId(),
       hFileHandle,
       @miniEI,  //PMinidumpExceptionInformation
       nil,
       nil);
end;

Then the function fails with error 0x8007021B

Only part of a ReadProcessMemory or WriteProcessMemory request was completed

What about SetUnhandledExceptionFilter?

Why don't you just use SetUnhandledExceptionFilter and get the pointer you need?

SetUnhandledExceptionFilter(@DebugHelpExceptionFilter);

function DebugHelpExceptionFilter(const ExceptionInfo: TExceptionPointers): Longint; stdcall;
begin
   GenerateDump(@ExceptionInfo);
   Result := 1;  //1 = EXCEPTION_EXECUTE_HANDLER
end;

Problem with that is that the unfiltered exception handler only kicks in if the exception is unfiltered. Because this is Delphi, and because because i handle the exception:

procedure DataModule1.ApplicationEvents1Exception(Sender: TObject; E: Exception);
var
    ei: TExceptionPointers;
begin
    if (E is EExternal) then
    begin
       //If it's EXCEPTION_IN_PAGE_ERROR then we have to terminate *now*
       if EExternal(E).ExceptionRecord.ExceptionCode = EXCEPTION_IN_PAGE_ERROR then
       begin
           ExitProcess(1);
           Exit;
       end;

       //Write minidump
       ...
    end;

    {$IFDEF SaveExceptionsToDatabase}
    SaveExceptionToDatabase(Sender, E);
    {$ENDIF}

    {$IFDEF ShowExceptionForm}
    ShowExceptionForm(Sender, E);
    {$ENDIF}
end;

The application doesn't, nor do i want it to, terminate with a WER fault.

How do i get the EXCEPTION_POINTERS during an EExternal?

Note: You can ignore everything from Background on. It's unnecessarily filler designed to make me look smarter.

Pre-emptive snarky Heffernan comment: You should stop using Delphi 5.

Bonus Reading


Solution

  • Since the Delphi RTL doesn't expose the context pointer directly but only extracts the exception pointer and does so in the bowels of System, the solution is going to be somewhat specific to the version of Delphi you are using.

    It's been a while since I've had Delphi 5 installed, but I do have Delphi 2007 and I believe that the concepts between Delphi 5 and Delphi 2007 have remained largely unchanged as far as this goes.

    With that in mind, here's an example of how it can be done for Delphi 2007:

    program Sample;
    
    {$APPTYPE CONSOLE}
    
    uses
      Windows,
      SysUtils;
    
    
    var
      SaveGetExceptionObject : function(P: PExceptionRecord):Exception;
    
    // we show just the content of the general purpose registers in this example
    procedure DumpContext(Context: PContext);
    begin
      writeln('eip:', IntToHex(Context.Eip, 8));
      writeln('eax:', IntToHex(Context.Eax, 8));
      writeln('ebx:', IntToHex(Context.Ebx, 8));
      writeln('ecx:', IntToHex(Context.Ecx, 8));
      writeln('edx:', IntToHex(Context.Edx, 8));
      writeln('esi:', IntToHex(Context.Esi, 8));
      writeln('edi:', IntToHex(Context.Edi, 8));
      writeln('ebp:', IntToHex(Context.Ebp, 8));
      writeln('esp:', IntToHex(Context.Esp, 8));
    end;
    
    // Below, we redirect the ExceptObjProc ptr to point to here
    // When control reaches here we locate the context ptr on
    // stack, call the dump procedure, and then call the original ptr
    function HookGetExceptionObject(P: PExceptionRecord):Exception;
    var
      Context: PContext;
    begin
      asm
        // This +44 value is likely to differ on a Delphi 5 setup, but probably
        // not by a lot. To figure out what value you should use, set a
        // break-point here, then look in the stack in the CPU window for the
        // P argument value on stack, and the Context pointer should be 8 bytes
        // (2 entries) above that on stack.
        // Note also that the 44 is sensitive to compiler switches, calling
        // conventions, and so on.
        mov eax, [esp+44]
        mov Context, eax
      end;
      DumpContext(Context);
      Result := SaveGetExceptionObject(P);
    end;
    
    var
      dvd, dvs, res: double; // used to force a div-by-zero error
    begin
      dvd := 1; dvs := 0;
      SaveGetExceptionObject := ExceptObjProc;
      ExceptObjProc := @HookGetExceptionObject;
      try
        asm
          // this is just for register context verification
          // - don't do this in production
          mov esi, $BADF00D5;
        end;
        // cause a crash
        res := dvd / dvs;
        writeln(res);
      except
        on E:Exception do begin
          Writeln(E.Classname, ': ', E.Message);
          Readln;
        end;
      end;
    end.