AI;DR – The investigation described here was done by Claude Code, with the model Claude Opus 5.5 (1M context) at the effort setting “medium”, and this blog post was written mostly by it too. The prompts I gave it are at the end. If you don’t want to read “AI slop”, stop reading now.
While testing some DPI fixes for the GExperts message boxes in Delphi 13.2 32 bit, I closed one of the IDE instances used for the tests and got this:
Application Error Exception EAccessViolation in module coreide370.bpl at 001AF8D9. Access violation at address 7051F8D9 in module 'coreide370.bpl' (offset 1AF8D9). Read of address 000002FC.
Of course the obvious suspect for an access violation on shutdown, in an IDE that had GExperts loaded is GExperts itself. So I set out to find out whether it was. This post is about how that was done without any external debugger, using only a second Delphi 13 instance and GxInspect, the UI inspection server that lets a script (or Claude Code, which did most of the work described here) look into a running IDE and drive it.
Step 1: Which routine is it?
The message gives the offset into coreide370.bpl: $1AF8D9. A BPL is a DLL, and a Delphi package exports practically every routine it contains, with its mangled name. So the export table is a map of the package: the nearest export at or below the offset is the routine the address is in. A small Python script, written by Claude Code without me prompting for that, read the PE export directory and gave:
0x1AF8D9: @Ideinsightcmdstoolbar@TIDEInsightCommandsToolbar@ActiveControlChange$qqrp14System@TObject + 0x11
That is TIDEInsightCommandsToolbar.ActiveControlChange, $11 bytes in: the IDE Insight toolbar’s handler for Screen.OnActiveControlChange. “Read of address 000002FC” means it read a field at offset $2FC of an object reference that was nil.
Step 2: Is GExperts in the chain?
GExperts hooks several events of Screen and Application, and a hook that keeps calling a handler after its owner has gone is a classic source of exactly this kind of crash. GExperts does have a hook class for this very event, TScreenActiveControlChangeHook in GX_EventHook.pas, but a search in the GExperts source code showed that nothing uses it.
That covers GExperts, but not other plugins. GxInspect can answer that for any running IDE: its EVENTS command reports an event’s handler, and follows hook chains (of GExperts and other plugins that use the same pattern) down to the original handler. In a running Delphi 13 it displays this:
> GxInspect EVENTS Screen OnActiveFormChange=TNotifyEventHook(hook TFormChangeManagerInternal.(unnamed method at 6472B8C8), orig (nil)) OnActiveControlChange=TIDEInsightCommandsToolbar.(unnamed method at 7051F8C8)
OnActiveControlChange is the IDE’s own handler, not chained by anybody.
Step 3: The call stack, without a debugger
Knowing the routine is not enough; the interesting part is who called it and when. According to Claude, the usual tool for that is cdb, which can attach to a process non-invasively and print all its stacks. It is part of the Debugging Tools for Windows SDK but was not installed on my computer and I didn’t feel like doing that right now.
But there is a debugger on every Delphi developer’s computer: the IDE itself. The crashed IDE was still running, waiting for its error message to be dismissed, so a second Delphi 13 instance could attach to it. GxInspect has commands for the IDE’s debugger (ATTACH, THREADS, SETTHREAD, STACK, REGISTERS, MEMORY and a few more), so all of this could be done from a script or interactively by Claude Code. There is one small complication: the debugging IDE needs a project open because the debugger takes the target platform from it. Any project will do, it just must be a Win32 project because we are attaching to the Win32 IDE. Without one, ATTACH says OK but attaches to nothing.
> GxInspect --pid=6252 ATTACH 14064 PAUSE OK Attaching to 14064, ask PROCESSES whether it took > GxInspect --pid=6252 SETTHREAD 6692 OK thread 6692 is now the current one > GxInspect --pid=6252 STACK OK thread 6692 has 14 frames :7760101c win32u.NtUserPeekMessage + 0xc :7723cc1e user32.PeekMessageW + 0xde ... :77281775 user32.MessageBoxW + 0x45 rtl.System.SysUtils.ShowException(???,???) [c:\delphi\13\SOURCE\RTL\SYS\System.SysUtils.pas line 24344] rtl.System.SysUtils.ExceptHandler(???,???) [c:\delphi\13\SOURCE\RTL\SYS\System.SysUtils.pas line 25113] rtl.System._ExceptionHandler [c:\delphi\13\SOURCE\RTL\SYS\System.pas line 24165] :71f34a41 ExceptHandler + $5 :77da92e4 ; C:\Windows\System32\ntdll.dll :77d957c6 ntdll.KiUserExceptionDispatcher + 0x26
So the main thread sits in SysUtils.ExceptHandler, the RTL’s last resort. This was not a VCL exception dialog: the exception escaped every try..except, which during shutdown means it happened after the VCL’s message handling had already ended. And the stack walk stops at KiUserExceptionDispatcher. The frames of the crash itself are not shown.
Another thing I didn’t know but Claude did: These stack frames are still there. When an exception happens, Windows saves the complete CPU state of the faulting thread, a CONTEXT record, on the stack together with the EXCEPTION_RECORD, and passes both to KiUserExceptionDispatcher. Everything needed was in the stack memory of the crashed process:
REGISTERSgave the current ESP of the main thread, andMEMORY <esp> 16384dumped 16 KB of stack from there.- In the dump, the
EXCEPTION_RECORDis easy to find: the exception code $C0000005 (access violation), with the faulting address $7051F8D9 three DWORDs later. - The x86
CONTEXTfollows it at +$50. Its Eip (at offset $B8) was $7051F8D9 again, which confirmed the find. It also gave Esp = $00E0FDEC and Esi = 0, the nil the code read through. - Starting at that saved Esp, every DWORD that looks like a code address is a candidate return address.
- The base addresses of the modules came from a 32 bit PowerShell:
(Get-Process -Id 14064).Modules. With them, every candidate turns into module + offset, and the same export lookup as in step 1 names it.
Read from the bottom of the stack up, this is what the IDE was doing:
rtl370 System.Halt0 the program is ending rtl370 System.Classes.TComponent.DestroyComponents Application frees its forms designide370 Deskform.TDesktopForm.Destroy one of the IDE's dockable forms vcl370 Vcl.Forms.TCustomForm.Destroy vcl370 Vcl.Forms.TScrollingWinControl.Destroy vcl370 Vcl.Controls.TWinControl.Destroy vcl370 Vcl.Styles.TStyleEngine.DoRemoveControl vcl370 Vcl.Controls.TControl.Destroy vcl370 Vcl.Forms.TApplication.ControlDestroyed vcl370 Vcl.Forms.TScreen.UpdateLastActive fires Screen.OnActiveControlChange coreide370 TIDEInsightCommandsToolbar.ActiveControlChange reads a field of nil: access violation
A word of caution: scanning the stack for things that look like return addresses can also pick up stale values from earlier calls, so a single line in such a list may be wrong. But this sequence as a whole makes sense, and every step of it is something the VCL really does.
What it means
When the IDE terminates, Application frees its forms. Freeing one of the IDE’s desktop forms changes the active control, so TScreen fires OnActiveControlChange. That event at that time apparently is still wired to the IDE Insight toolbar, which by then has already been partly torn down, and its handler reads through a reference that is nil.
Nothing between Screen and that handler belongs to a plugin, so this looks like a shutdown order problem in the IDE itself, not something GExperts caused. It happened once, and I have not been able to reproduce it. Why it happened in that particular session and not in the others is unknown; that session had shown and closed several modal dialogs and moved one of them between monitors with different scaling, so its focus history was unusual, but nothing links that to the crash.
The recipe is written down now, for the next time this happens. If it does, it will also be the material for a bug report to Embarcadero.
The prompts
These are the prompts I wrote, verbatim, typos included. Claude Code had started that Delphi 13 instance itself, for testing the message boxes.
I just got an access violation when I closed the Delphi 13 instance you opened for the test: --------------------------- Application Error --------------------------- Exception EAccessViolation in module coreide370.bpl at 001AF8D9. Access violation at address 7051F8D9 in module 'coreide370.bpl' (offset 1AF8D9). Read of address 000002FC. --------------------------- OK --------------------------- That was the first dialog, it's still open.
I copied the dialog text via the clipboard (Ctrl+C) to the Claude Code window (right mouse click).
Claude then suggested to use cdb (I should rather say: complained that is is not installed). I told it:
you can run a new instance of Delphi 13 and attach its debugger to the failed instance.
Everything else, from looking up the routine to reading the stack memory, it came up with by itself. A year ago, that would have left me completely gobsmacked, but since then I have found that LLMs, in particular Claude seem to be able to do things I would never have thought possible.
Discussion about this in the corresponding post in the international Delphi Praxis forum.