Delphi – hardcore debugging the intialization of your app, dlls and packages
Posted by Jeroen Pluimers on 2009/10/15
Recently we got involved with a client having a large and complex application that (historically) consists of
One of the biggest problems is debugging the startup sequence.
Somehow, when the Delphi IDE loads DLLs in the initialization sequences of units, it looses its ability symbol tables.
This article describes a few tips on how to debug those, especially where to put breakpoints.
Yes, we have done the obvious things like:
- the EXE, DLLs and BPLs were already in the same directory
- that directory is referenced in the IDE Debug symbol search path for loading the symbols
- turned on all sorts of debug information (Turbo Debugger inside EXE/DLL/BPL, RSM files, MAP files)
- turned off Optimization and turned on Stack frames
- and studied a lot of FAQs and tips from other people
In the end, it appears that when DLL’s with symbols get loaded during unit initialization sections, the Delphi IDE (in this case 2007) loses the Symbol Tables either for the EXE or for the DLL (or both).
In order to work around this:
- We had to know which DLLs were loaded and when
- We rewrote all the DLLs to be ‘demand/delay loaded‘ (i.e. not statically linked, see code sample below)
- We rewrote all the stuff that referenced the DLLs to initialize on demand as well
Here are some tips for setting breakpoints to debug the startup sequence, including some links to relevant documentation, samples or articles:
- Unit System; procedure InitUnits; line “TProc(P)();”
(this line gets called often: for each unit that is being initialized, either because the unit has an initialization section, or it has initialized consts; see an example call stack) .
- Unit SysUtils; procedure SafeLoadLibrary;
(we call this for every DLL that gets loaded; this method saves and restores the FPU control word, especially handy for 3rd party DLLs that think they own the FPU).
- unit System; procedure _InitLib; line “MOV EDX,offset Module”
(this gets called in a DLL when Windows is loading it with LoadLibrary)
- unit SysInit; procedure InitProcessTLS;
(inside a DLL this reserves the Thread Local Storage to be used by this DLL; it usually is the first thing called in a DLL)
- unit System; procedure ExitDll; line “RET 12″ (assembler)
(this gets called in a DLL when Windows is almost finished processing the LoadLibrary for it)
- DLL kernel32.LoadLibraryA and DLL kernel32.LoadLibraryW, CPU code “cmp dword ptr [ebp+$08],$00″, then in the data portion of the CPU viewer, go to the address “ebp+$08″ , then press Ctrl-D to follow “long data”; now you see the module that is going to be loaded
(this gets called by Windows when loading DLLs)
- ntdll.LdrInitializeThunk, CPU code “call dword ptr[ebp+$08]“
(this gets called by Windows just before loading the DLL)
For the demand loading, we used this small unit:
unit BLibrary; interface uses SysUtils, Windows; const NoLibraryHandle = 0; type ELibrary = class(Exception); TLibraryRecord = record Name: string; Handle: HModule; end; /// See also this method in the ComObj unit: /// procedure RegisterComServer(const DLLName: string); function LoadLibrary(var LibraryRecord: TLibraryRecord): Boolean; overload; procedure FreeLibrary(var LibraryRecord: TLibraryRecord); overload; function GetProcAddress(var LibraryRecord: TLibraryRecord; const ProcName: string): FARPROC; overload; implementation function IsInvalidModuleHandle(const Handle: HModule): Boolean; begin Result := (Handle <= HINSTANCE_ERROR); end; function SysErrorMessageLastError(): string; begin Result := SysErrorMessage(GetLastError); end; function LoadLibrary(var LibraryRecord: TLibraryRecord): Boolean; begin Result := False; if IsInvalidModuleHandle(LibraryRecord.Handle) then begin LibraryRecord.Handle := SafeLoadLibrary(LibraryRecord.Name); if IsInvalidModuleHandle(LibraryRecord.Handle) then raise ELibrary.CreateFmt('DLL "%s" nicht geladen wegen: "%s"', [LibraryRecord.Name, SysErrorMessageLastError]); Result := True; end; end; procedure FreeLibrary(var LibraryRecord: TLibraryRecord); var Success: Boolean; begin if not IsInvalidModuleHandle(LibraryRecord.Handle) then begin Success := Windows.FreeLibrary(LibraryRecord.Handle); if not Success then raise ELibrary.CreateFmt('DLL "%s" entladen fehler: "%s"', [LibraryRecord.Name, SysErrorMessageLastError]); LibraryRecord.Handle := NoLibraryHandle; end end; function GetProcAddress(var LibraryRecord: TLibraryRecord; const ProcName: string): FARPROC; begin LoadLibrary(LibraryRecord); Result := Windows.GetProcAddress(LibraryRecord.Handle, PChar(ProcName)); if Result = nil then raise ELibrary.CreateFmt('DLL "%s" fehler beim laden von ohne Eintrittspunkt "%s": "%s"', [LibraryRecord.Name, ProcName, SysErrorMessageLastError]); end; end.
This article is work-in-progress, so you will see updates in the next coming days.