I recently was asked whether the Delphi exception event filter for WinDBG that I wrote about would also work with x64 Delphi applications. The answer was no, it wouldn’t work, but that made me curious to find out what was different with x64. I knew x64 exception handling was completely different to x86, being table based instead of stack based, but I wasn’t sure how much of this would be reflected in the event filter.
The original post contains the details about how the exception record was accessible at a known location on the stack, and how we could dig in from there.
Before firing up WinDBG, I had a look at System.pas, and found the x64 virtual method table offsets. I have highlighted the key field we want to pull out:
{ Virtual method table entries } {$IF defined(CPUX64)} vmtSelfPtr = -176; vmtIntfTable = -168; vmtAutoTable = -160; vmtInitTable = -152; vmtTypeInfo = -144; vmtFieldTable = -136; vmtMethodTable = -128; vmtDynamicTable = -120; vmtClassName = -112; vmtInstanceSize = -104; vmtParent = -96; vmtEquals = -88 deprecated 'Use VMTOFFSET in asm code'; vmtGetHashCode = -80 deprecated 'Use VMTOFFSET in asm code'; vmtToString = -72 deprecated 'Use VMTOFFSET in asm code'; vmtSafeCallException = -64 deprecated 'Use VMTOFFSET in asm code'; vmtAfterConstruction = -56 deprecated 'Use VMTOFFSET in asm code'; vmtBeforeDestruction = -48 deprecated 'Use VMTOFFSET in asm code'; vmtDispatch = -40 deprecated 'Use VMTOFFSET in asm code'; vmtDefaultHandler = -32 deprecated 'Use VMTOFFSET in asm code'; vmtNewInstance = -24 deprecated 'Use VMTOFFSET in asm code'; vmtFreeInstance = -16 deprecated 'Use VMTOFFSET in asm code'; vmtDestroy = -8 deprecated 'Use VMTOFFSET in asm code'; vmtQueryInterface = 0 deprecated 'Use VMTOFFSET in asm code'; vmtAddRef = 8 deprecated 'Use VMTOFFSET in asm code'; vmtRelease = 16 deprecated 'Use VMTOFFSET in asm code'; vmtCreateObject = 24 deprecated 'Use VMTOFFSET in asm code'; {$ELSE !CPUX64} vmtSelfPtr = -88; vmtIntfTable = -84; vmtAutoTable = -80; vmtInitTable = -76; vmtTypeInfo = -72; vmtFieldTable = -68; vmtMethodTable = -64; vmtDynamicTable = -60; vmtClassName = -56; vmtInstanceSize = -52; vmtParent = -48; vmtEquals = -44 deprecated 'Use VMTOFFSET in asm code'; vmtGetHashCode = -40 deprecated 'Use VMTOFFSET in asm code'; vmtToString = -36 deprecated 'Use VMTOFFSET in asm code'; vmtSafeCallException = -32 deprecated 'Use VMTOFFSET in asm code'; vmtAfterConstruction = -28 deprecated 'Use VMTOFFSET in asm code'; vmtBeforeDestruction = -24 deprecated 'Use VMTOFFSET in asm code'; vmtDispatch = -20 deprecated 'Use VMTOFFSET in asm code'; vmtDefaultHandler = -16 deprecated 'Use VMTOFFSET in asm code'; vmtNewInstance = -12 deprecated 'Use VMTOFFSET in asm code'; vmtFreeInstance = -8 deprecated 'Use VMTOFFSET in asm code'; vmtDestroy = -4 deprecated 'Use VMTOFFSET in asm code'; vmtQueryInterface = 0 deprecated 'Use VMTOFFSET in asm code'; vmtAddRef = 4 deprecated 'Use VMTOFFSET in asm code'; vmtRelease = 8 deprecated 'Use VMTOFFSET in asm code'; vmtCreateObject = 12 deprecated 'Use VMTOFFSET in asm code'; {$IFEND !CPUX64}
I also noted that the exception code for Delphi x64 was the same as x86:
cDelphiException = $0EEDFADE;
Given this, I put together a test x64 application in Delphi that would throw an exception, and loaded it into WinDBG. I enabled the event filter for unknown exceptions, and triggered an exception in the test application. This broke into WinDBG, where I was able to take a look at the raw stack:
(2ad4.2948): Unknown exception - code 0eedfade (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. KERNELBASE!RaiseException+0x39: 000007fe`fd6ccacd 4881c4c8000000 add rsp,0C8h 0:000> dd rbp 00000000`0012eab0 00000008 00000000 00000021 00000000 00000000`0012eac0 0059e1f0 00000000 0059e1f0 00000000 00000000`0012ead0 0eedfade 00000001 00000000 00000000 00000000`0012eae0 0059e1dd 00000000 00000007 00000000 00000000`0012eaf0 0059e1dd 00000000 0256cff0 00000000 00000000`0012eb00 00000000 00000000 00000000 00000000 00000000`0012eb10 00000000 00000000 00000000 00000000 00000000`0012eb20 00000000 00000000 0256cff8 00000000
We can see at rbp+20 is the familiar looking 0EEDFADE value. This is the start of the EXCEPTION_RECORD structure, which I’ve reproduced below from Delphi’s System.pas with a little annotation of my own:
TExceptionRecord = record ExceptionCode: Cardinal; // +0 ExceptionFlags: Cardinal; // +4 ExceptionRecord: PExceptionRecord; // +8 ExceptionAddress: Pointer; // +10 NumberParameters: Cardinal; // +18 case {IsOsException:} Boolean of True: (ExceptionInformation : array [0..14] of NativeUInt); False: (ExceptAddr: Pointer; // +20 ExceptObject: Pointer); // +28 end;
We do have to watch out for member alignment with this structure — because it contains both 4 byte DWORDs and 8 byte pointers, there are 4 bytes of hidden padding after the NumberParameters member, as shown below (this is from MSDN, sorry to switch languages on you!):
typedef struct _EXCEPTION_RECORD64 { DWORD ExceptionCode; DWORD ExceptionFlags; DWORD64 ExceptionRecord; DWORD64 ExceptionAddress; DWORD NumberParameters; DWORD __unusedAlignment; DWORD64 ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD64, *PEXCEPTION_RECORD64;
But what we can see from TExceptionRecord is that at offset 0x28 in the record is a pointer to our ExceptObject. Great! That’s everything we need. We can now put together our x64-modified event filter.
You may remember the x86 event filter:
sxe -c "da poi(poi(poi(ebp+1c))-38)+1 L16;du /c 100 poi(poi(ebp+1c)+4)" 0EEDFADE
So here is the x64 version:
sxe -c "da poi(poi(poi(rbp+48))-70)+1 L16;du /c 100 poi(poi(rbp+48)+8)" 0EEDFADE
And with this filter installed, here is how a Delphi exception is now displayed in WinDBG:
(2ad4.2948): Unknown exception - code 0eedfade (first chance) 00000000`0059e0cf "MyException" 00000000`02573910 "My very own kind of error message" First chance exceptions are reported before any exception handling. This exception may be expected and handled. KERNELBASE!RaiseException+0x39: 000007fe`fd6ccacd 4881c4c8000000 add rsp,0C8h
I’ll dissect the pointer offsets a little more than I did in the previous blog, because they can be a bit confusing:
- rbp+48 is a pointer to the exception object (usually a type that inherits from Exception).
- poi(rbp+48) dereferences that, and at offset 0 right here, we have a pointer to the class type.
- Before we look at the class type, poi(rbp+48)+8 is the first member of the object (don’t forget ancestor classes), which happens to be FMessage from the Exception class. That gives us our message.
- Diving deeper, poi(poi(rbp+48)) is now looking at the class type.
- And we know that the offset of vmtClassName is -112 (-0x70). So poi(poi(poi(rbp+48))-70) gives us the the ShortString class name, of which the first byte is the length.
- So we finish with poi(poi(poi(rbp+48))-70)+1, which lets us look at the string itself.
You will see that to access the exception message, I have opted to look directly at the Exception object rather than use the more direct pointer which is on the stack. I did this to make it easier to see how it might be possible to pull out other members of descendent exception classes, such as ErrorCode from EOSError.
And one final note: looking back on that previous blog, I see that one thing I wrote was a little misleading: the string length of FMessage is indeed available at poi(poi(rbp+48)+8)-4, but the string is null-terminated, so we don’t need to use it — WinDBG understands null-terminated strings. Where this is more of a problem is with the ShortString type, which is not null-terminated. This is why sometimes exception class names displayed using this method will show a few garbage characters afterwards, because we don’t bother about accounting for that; the L16 parameter prevents us dumping memory until we reach a null byte.