Jump to content
Fr0sT.Brutal

Exception call stacks on Windows with only a few LOCs

Recommended Posts

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 by Fr0sT.Brutal
  • Like 2
  • Thanks 2

Share this post


Link to post
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.

  • Like 1

Share this post


Link to post
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

  • Like 5

Share this post


Link to post
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.

  • Like 1

Share this post


Link to post
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

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

 

  • Thanks 1

Share this post


Link to post

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
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
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

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
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.

image.thumb.png.88b1b4488d81d086b41e15d56c9d7173.png

  • Like 1

Share this post


Link to post
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 by Fr0sT.Brutal

Share this post


Link to post
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

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

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

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

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
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

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×