How to get the EXCEPTION_POINTERS during an EExternal exception?
Asked Answered
B

1

9

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

  • PEXCEPTION_RECORD and
  • PCONTEXT

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

Brittabrittain answered 13/2, 2013 at 15:34 Comment(3)
Post-emptive Heffernan comment: I doubt that it's any easier in later versions. And you don't have to worry about x64.Sealed
FWIW madExcept gives you easy access to the context recordSealed
madExcept gets hold of the context by rather nefarious means. It hooks ExceptObjProc. Which turns out to be quite tricky. Because the hooking process needs to read some registers before calling a Pascal function. I could probably work out how to do it using ME as a template. But it would take ages. Personally, I'd just use ME. Once you have ME I'd guess you don't need minidumps anymore. The ME diagnostics will be easier to use.Sealed
I
7

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.
Immune answered 13/2, 2013 at 21:39 Comment(4)
+1 Well done. I hadn't appreciated how easy it is to hook ExceptObjProc.Sealed
Excellent! And i was hoping there was a Win32 or RTL GetExceptionContextBrittabrittain
@IanBoyd The context has to be captured at the point at which the exception is raised. The OS does that for you. And passes it to you. And then the RTL ignores it. So I think that this hooking approach is the only viable solution. The reason I thought it was harder is that ME does more than this. It hooks the ExceptObjProc mechanism and allows the user still to use that mechanism. Rather nifty.Sealed
Rather than have HookGetExceptionObject() rely on a per-compiler offset of the ESP register to find the CONTEXT record, I find it easier to use AddVectoredExceptionHandler() instead. The vectored handler is called before HookGetExceptionObject() and can save the CONTEXT pointer into a thread-local variable where HookGetExceptionObject() can then access it.Wade

© 2022 - 2024 — McMap. All rights reserved.