Fr0sT.Brutal 900 Posted August 25, 2022 (edited) Many of us miss call stacks in Delphi and have to use heavy or commercial libs. Luckily Microsoft cares of us and provides necessary API's. Here's the unit // Stack tracing with WinAPI // (c) Fr0sT-Brutal // License MIT unit StackTrace; interface {$IFDEF MSWINDOWS} uses Windows, SysUtils; const DBG_STACK_LENGTH = 32; type TDbgInfoStack = array[0..DBG_STACK_LENGTH - 1] of Pointer; PDbgInfoStack = ^TDbgInfoStack; function RtlCaptureStackBackTrace(FramesToSkip: ULONG; FramesToCapture: ULONG; BackTrace: Pointer; BackTraceHash: PULONG): USHORT; stdcall; external 'kernel32.dll'; procedure GetCallStackOS(var Stack: TDbgInfoStack; FramesToSkip: Integer); function CallStackToStr(const Stack: TDbgInfoStack): string; procedure InstallExceptionCallStack; {$ENDIF} implementation {$IFDEF MSWINDOWS} procedure GetCallStackOS(var Stack: TDbgInfoStack; FramesToSkip: Integer); begin ZeroMemory(@Stack, SizeOf(Stack)); RtlCaptureStackBackTrace(FramesToSkip, Length(Stack), @Stack, nil); end; function CallStackToStr(const Stack: TDbgInfoStack): string; var Ptr: Pointer; begin Result := ''; for Ptr in Stack do if Ptr <> nil then Result := Result + sLineBreak + Format('$%p', [Ptr]) else Break; end; function GetExceptionStackInfo(P: PExceptionRecord): Pointer; begin Result := AllocMem(SizeOf(TDbgInfoStack)); GetCallStackOS(PDbgInfoStack(Result)^, 1); // excluding the very function GetCallStackOS end; function GetStackInfoStringProc(Info: Pointer): string; begin Result := CallStackToStr(PDbgInfoStack(Info)^); end; procedure CleanUpStackInfoProc(Info: Pointer); begin Dispose(PDbgInfoStack(Info)); end; procedure InstallExceptionCallStack; begin Exception.GetExceptionStackInfoProc := GetExceptionStackInfo; Exception.GetStackInfoStringProc := GetStackInfoStringProc; Exception.CleanUpStackInfoProc := CleanUpStackInfoProc; end; procedure UninstallExceptionCallStack; begin Exception.GetExceptionStackInfoProc := nil; Exception.GetStackInfoStringProc := nil; Exception.CleanUpStackInfoProc := nil; end; {$ENDIF} end. test project program Project2; {$APPTYPE CONSOLE} {$R *.res} uses Windows, SysUtils, StackTrace in 'StackTrace.pas'; // Demo subs procedure Nested2; begin Abort; end; procedure Nested1; begin Nested2; end; procedure Nested0; begin Nested1; end; begin try InstallExceptionCallStack; Nested0; except on E: Exception do Writeln(E.ClassName, ': ', E.Message, sLineBreak, E.StackTrace); end; Readln; end. and output Quote EAbort: Operation aborted $0041A5AB $00418A56 $0041A614 $0041A620 $0041A62C $0041C3C5 $7582343D $77D69802 $77D697D5 Edited August 25, 2022 by Fr0sT.Brutal 2 2 Share this post Link to post
Remy Lebeau 1394 Posted August 25, 2022 9 hours ago, Fr0sT.Brutal said: Many of us miss call stacks in Delphi and have to use heavy or commercial libs. Luckily Microsoft cares of us and provides necessary API's. It is a nice start. But from experience, having just the raw stack addresses is still a PITA to debug. I took a similar approach in one of my projects (not using the build-in Exception.StackTrace callbacks, though), but I also output the module name that each address belongs to, and for a DLL module I also worked out the exported function name+offset based on the address. I was planning on adding logic to resolve non-DLL function names using the generated MAP file, but I didn't get that far before the project was discontinued. 1 Share this post Link to post
Fr0sT.Brutal 900 Posted August 26, 2022 13 hours ago, Remy Lebeau said: It is a nice start. But from experience, having just the raw stack addresses is still a PITA to debug. I took a similar approach in one of my projects (not using the build-in Exception.StackTrace callbacks, though), but I also output the module name that each address belongs to, and for a DLL module I also worked out the exported function name+offset based on the address. I was planning on adding logic to resolve non-DLL function names using the generated MAP file, but I didn't get that far before the project was discontinued. You're right for sure, that's why I recently implemented MAP file reading and extracting all the info available for any given address. Besides some tricky aspects, that wasn't too hard. I merged that with built-in stack traces and now I have fully detailed traces with module, function name and LOC. Alas, the code requires some other my routines which are not fully ready for publishing yet (translate & add comments etc). But in case someone is interested I could try to switch to built-in routines 5 Share this post Link to post
SwiftExpat 65 Posted August 26, 2022 3 hours ago, Fr0sT.Brutal said: in case someone is interested I would be interested for sure, any approach to reading the map file is worth posting to save others time and learn the tricky aspects. 1 Share this post Link to post
Fr0sT.Brutal 900 Posted August 29, 2022 function GetExceptionStackInfo(P: PExceptionRecord): Pointer; begin Result := AllocMem(SizeOf(TDbgInfoStack)); // ! Excluding nested functions: // - GetCallStackOS // - GetExceptionStackInfo // - System.SysUtils.Exception.RaisingException GetCallStackOS(PDbgInfoStack(Result)^, 3); end; Fix: remove 2 non-relevant functions being added to call stack Share this post Link to post
Fr0sT.Brutal 900 Posted August 29, 2022 Check the results here https://github.com/Fr0sT-Brutal/Delphi_StackTraces >StackTraceSample.exe Operation aborted (EAbort @ $449D18) Stack trace: StackTraceSample.dpr:16 [StackTraceSample.Nested2] StackTraceSample.dpr:21 [StackTraceSample.Nested1] StackTraceSample.dpr:26 [StackTraceSample.Nested0] StackTraceSample.dpr:~37 [StackTraceSample.StackTraceSample] $7582343D $77D69802 $77D697D5 1 Share this post Link to post
SwiftExpat 65 Posted August 29, 2022 Thanks for the great solution, after a first review here is what I found: It would not compile, ambiguous over load error. I changed the if statement to be more generic than the {$IFNDEF RAD_XE3_UP} {$IF CompilerVersion < 24.0} // System.Pos with offset starting from XE3 function Pos(const SubStr, Str: string; Offset: Integer): Integer; overload; {$ENDIF} I fails to print method name in 11.1, but works in 10.4 & 10.2. I debugged it a little and the map file is loaded, but the lookup seems to fail in GetAddrInfo. line 322 fails the test : if (laIdx = -1) and (paIdx = -1) then Exit(False); Do you have any guess as to the difference in 11.1? Share this post Link to post
Fr0sT.Brutal 900 Posted August 30, 2022 9 hours ago, SwiftExpat said: It would not compile, ambiguous over load error Remnants of initial sources, thanks for pointing this out 9 hours ago, SwiftExpat said: Do you have any guess as to the difference in 11.1? No guess... I'll need your actual stack trace and MAP file itself. Share this post Link to post
SwiftExpat 65 Posted August 30, 2022 2 hours ago, Fr0sT.Brutal said: tack trace and MAP file itself. Stack Trace: Operation aborted (EAbort @ $A53294) Stack trace: $00A53294 $00A532A0 $00A532AC $00A57D8F $75F9FA29 $77EB7A9E $77EB7A6E StackTraceSample.map Share this post Link to post
Fr0sT.Brutal 900 Posted August 30, 2022 IDK. "Nested#" functions should have $004xxxxx addrs ($00401000 + $000D22A4 for StackTraceSample.Nested0). No idea why are you getting $00Axxxxx addrs. Maybe it's code relocation? What happens if you stop at any breakpoint and execute "Go to address" with some of these $00Axxxxx? Share this post Link to post
SwiftExpat 65 Posted August 30, 2022 1 hour ago, Fr0sT.Brutal said: Maybe it's code relocation? Your comment got me to remember ASLR in 11. Removing ASLR makes it work correctly. For anyone else interested, it is the 2nd to last option in Linking. 1 Share this post Link to post
Fr0sT.Brutal 900 Posted August 30, 2022 (edited) 37 minutes ago, SwiftExpat said: Removing ASLR makes it work correctly. Good to know! If you know how to determine current base address, ASLR would be supported as well. The only simple option I see without diving too deep into PE analysis is to compare actual addr of any function (f.ex., @ReadMapFile) with that taken from MAP file. Edited August 30, 2022 by Fr0sT.Brutal Share this post Link to post
SwiftExpat 65 Posted August 30, 2022 46 minutes ago, Fr0sT.Brutal said: compare actual addr of any function (f.ex., @ReadMapFile) with that taken from MAP file. This is what I was thinking as well, just calculate an offset. Not sure it is worth the effort, maybe in the future. A quick google did turn up some results but all seemed to require elevated privileges, which makes sense given what ASLR is trying to provide. Share this post Link to post
Fr0sT.Brutal 900 Posted December 8, 2022 Update: * Change format of stack trace: $Address Unit:LOC [Unit.Routine] * GetCallStackOS, internally removes itself from call stack by increasing FramesToSkip by 1. Skipping other 2 routines removed as they might not appear in some cases ! ReadLineNumbersSection, fix reading LOC-address parts separated by single spaces * GetExceptionStackInfo, adds the exception address itself as 1st stack item so that exception location is also decoded Share this post Link to post
Fr0sT.Brutal 900 Posted December 14, 2022 Update: * Total redesign of reading. Minimized string allocations which allowed to gain x3,5 perf boost on MAP file loading + Demo project: add comments and messages, add sample of random callstack, add demo for GetMethodName https://github.com/Fr0sT-Brutal/Delphi_StackTraces 1 Share this post Link to post
thatlr 1 Posted December 18, 2022 I just want to point to this solution: https://github.com/thatlr/Delphi-StackTrace As it is relying on PDB files, it is not limited to Delphi sources: With the matching PDBs, it will also display function names and source code locations for foreign DLLs. (It could also display function names of Windows DLLs, if tweaking the SymInitialize searchpath argument, as mentioned in the source). Share this post Link to post
pudnivec 1 Posted December 19, 2022 Thank you for the link. It's an inspiring post, but I'll use a slightly different solution for our needs. Share this post Link to post
Rustam Novikov 0 Posted February 6, 2023 I have a question regarding getting stack trace in delphi vcl application using Datasnap technology. I am currently tracking a very nasty EStackOverflow exception in delphi 11.2 patch 1. It occurs very rarely in production environment and The problem is, that I get the stack trace, but I do not see the origin of the exception, because stack trace looks like this: 20230206 10354806 EXCOS EStackOverflow (c00000fd) [] at 011ef4a8 MidasLib.DllGetDataSnapClassObject (5371) stack trace API 011ef701 MidasLib.DllGetDataSnapClassObject (5460) 011ef701 MidasLib.DllGetDataSnapClassObject (5460) 011ef701 MidasLib.DllGetDataSnapClassObject (5460) 011ef701 MidasLib.DllGetDataSnapClassObject (5460) 011ef701 MidasLib.DllGetDataSnapClassObject (5460) 011ef701 MidasLib.DllGetDataSnapClassObject (5460) ... So it keeps going until depth limit is reached. And the start of the exception stack is missing, so I have no idea what caused the exception. Is there any opportunity to get the whole exception stack, or at least first 20-30 rows? Share this post Link to post
Fr0sT.Brutal 900 Posted February 7, 2023 16 hours ago, Rustam Novikov said: Is there any opportunity to get the whole exception stack, or at least first 20-30 rows? Do you mean you get all the 32 entries of call stack filled? Try to enlarge DBG_STACK_LENGTH then. I don't remember why I chose the value of 32, I've re-read MSDN and saw no limitations on stack size. Share this post Link to post