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.
Amazing ! You rock !! Thank you very much 😀
Thanks Adrien 🙂 I may have another blog post coming up about converting Delphi’s TDS debug format into something WinDBG understands. But this one will take me a little longer to write!
Hi Marc,
I’m working on a 64-bit Delphi application problem and am having the usual challenges with working Delphi in WinDbg / DebugDiag / etc. regarding the incomplete .PDB’s the open source symbol file converters emit.
(Since Microsoft has finally published the.PDB format, maybe someone will write a .PDB converter for 64-bit Delphi that matches the functionality I see when doing dump analysis of a Visual C++ or C# apps.)
I used to successfully use your 32-bit version of the search command for Delphi’s SEH 0EEEFADE exceptions. And while working the first large Delphi app dump I’ve been sent in years, I recently found this post of the your 64-bit version but have not seen any search hits at all. That includes trying it with a dump of a 64-bit, Delphi v10.2.3 VCL forms test harness that intentionally throws different exception including an unhandled A/V exception. I tested with ProcDump64 attached to the app to externally monitor the exceptions thrown by the app.
Since VCL forms do have a TApplication OnException default handler, I could see the C0000005 exceptions in ProcDump’s output both using and after commenting out a standard Delphi Exception handler for it; still with no sign of a 0EEEFADE in either ProcDump’s log or in a resulting dump . By adding a “raise E;” inside the on E.Exception handler for the C5 to re-raise the exception, I finally saw the 0EEEFADE show up in ProcDump’s monitoring output.
But when I opened the resulting dump and tried your new, for-64-bit SXE command from WinDbg’s command line, I got zero hits.
Have you tried this on v10.2.x? If not, do you have any idea why I’m getting no hits after seeing that exact SEH exception show up in ProcDump’s output? (I accept the idea that the TApplication.OnException handler may be getting in the way).
Here’s an upvote on a better .TDS to .PDB converter (or, even better, an .RSM to .PDB converter) that will include non-name-mangled 64-bit names and good enough line number info to be displayed by WinDbg. It should be a bit easier now that Microsoft has posted public documentation of the .PTB format.
Thanks for your excellent blog!
-Bob
Thanks for reading! I have unfortunately done very little Delphi x64 debugging recently so I don’t really have any ideas beyond what you’ve already explored, sorry.
Hi Bob.
Have you gotten any further on any of the converter to .PDB front?
I have been using MAP2PDB to get the linker map into PDB but that does not allow me to get private symbol information only source and line numbers. It is great to see a stack with source and line but for some reason the Delphi proper code still does not have source and line information, just function name plus offset.
Stack with MAP2PDB symbols:
0:053> bp ntdll!NtCreateThreadEx “k;g”
0:053> g
# RetAddr : Call Site
00 00007fff`919fff2f : ntdll!NtCreateThreadEx
01 00007fff`9340afdd : KERNELBASE!CreateRemoteThreadEx+0x29f
02 00000000`00412bab : KERNEL32!CreateThreadStub+0x3d
03 00000000`00546f99 : PRTG_Server!BeginThread+0x7b
04 00000000`00546e34 : PRTG_Server!TThread.Create+0xf9
05 00000000`008c4350 : PRTG_Server!TThread.Create+0x44
06 00000000`008c49df : PRTG_Server!TIdThread.Create+0xa0 [IdThread.pas @ 447]
07 00000000`008c59d5 : PRTG_Server!TIdThreadWithTask.Create+0x4f [IdThread.pas @ 602]
08 00000000`008c6cb9 : PRTG_Server!TIdSchedulerOfThread.NewThread+0xa5 [IdSchedulerOfThread.pas @ 198]
09 00000000`008c6c15 : PRTG_Server!TIdSchedulerOfThreadDefault.NewThread+0x9 [IdSchedulerOfThreadDefault.pas @ 140]
0a 00000000`008cb09e : PRTG_Server!TIdSchedulerOfThreadDefault.AcquireYarn+0x15 [IdSchedulerOfThreadDefault.pas @ 89]
0b 00000000`008c404b : PRTG_Server!TIdListenerThread.Run+0x4e [IdCustomTCPServer.pas @ 999]
0c 00000000`00546c73 : PRTG_Server!TIdThread.Execute+0xdb [IdThread.pas @ 361]
0d 00000000`00412afd : PRTG_Server!ThreadProc+0x43
0e 00007fff`93404ed0 : PRTG_Server!ThreadWrapper+0x3d
0f 00007fff`93e6e39b : KERNEL32!BaseThreadInitThunk+0x10
10 00000000`00000000 : ntdll!RtlUserThreadStart+0x2b
Notice that frames 3-5 and 0d-0e are Delphi’s core code and do not have source and line info displayed.
Appreciate any info,
Osiris