8

後でデバッグに使用できるスタックトレースをログに記録できるアプリケーションがあります。

Windows では、JEDI プロジェクトが提供する優れた JCLDebug ユニットを使用して取得しました。

アプリケーションが OSX で実行されるようになったので、ちょっと問題が発生しました。例外が発生したときに正しいスタック トレースを取得する方法がわかりません。

私は基本を理解しています -

1) 「backtrace」を使用してスタックトレースを取得できます (libSystem.dylib にあります)

2) 結果のバックトレースは、Delphi のリンカーが提供する .map ファイルを使用して行番号に変換できます。

私が残した問題は、どこからバックトレースを呼び出すべきかわからないということです。Delphi が(別のスレッドで)Mach 例外を使用していること、および posix シグナルを使用できないことは知っていますが、これですべて解決できました。

「try...except」ブロックでバックトレースを取得できますが、残念ながら、その時点でスタックはすでに縮小されています。

例外が発生した直後に実行される適切な例外ロガーをインストールするにはどうすればよいですか?

アップデート:

「Honza R の提案に従って、「GetExceptionStackInfoProc」プロシージャを調べました。

この機能により、例外処理プロセスの「内部」に入ることができますが、残念ながら、以前と同じ問題がいくつか残っています。

まず第一に、デスクトップ プラットフォームでは、この関数 'GetExceptionStackInfoProc' は単なる関数ポインターであり、独自の例外情報ハンドラーで割り当てることができます。そのため、すぐに使用できる Delphi は、スタック情報プロバイダーを提供しません。

関数を「GetExceptionStackInfoProc」に割り当て、その中で「バックトレース」を実行すると、スタックトレースを受け取りますが、そのトレースは、例外の原因となったスレッドではなく、例外ハンドラに関連しています。

「GetExceptionStackInfoProc」には「TExceptionRecord」へのポインタが含まれていますが、これに関するドキュメントは非常に限られています。

私は自分の深さを超えているかもしれませんが、正しいスレッドからスタックトレースを取得するにはどうすればよいですか? 独自の「バックトレース」関数を例外ハンドラーに挿入し、そこから標準の例外ハンドラーに戻ることは可能でしょうか?

更新 2

いくつかの詳細。1 つ明確にしておくべきことがあります。この質問は、RTL 内で完全に処理されるソフトウェア例外ではなく、MACH メッセージによって処理される例外に関するものです。

エンバカデロは、これらの機能とともにいくつかのコメントを発表しました -

    System.Internal.MachExceptions.pas -> catch_exception_raise_state_identity

    {
     Now we set up the thread state for the faulting thread so that when we
     return, control will be passed to the exception dispatcher on that thread,
     and this POSIX thread will continue watching for Mach exception messages.
     See the documentation at <code>DispatchMachException()</code> for more
     detail on the parameters loaded in EAX, EDX, and ECX.
    }

    System.Internal.ExcUtils.pas -> SignalConverter

    {
      Here's the tricky part.  We arrived here directly by virtue of our
      signal handler tweaking the execution context with our address.  That
      means there's no return address on the stack.  The unwinder needs to
      have a return address so that it can unwind past this function when
      we raise the Delphi exception.  We will use the faulting instruction
      pointer as a fake return address.  Because of the fencepost conditions
      in the Delphi unwinder, we need to have an address that is strictly
      greater than the actual faulting instruction, so we increment that
      address by one.  This may be in the middle of an instruction, but we
      don't care, because we will never be returning to that address.
      Finally, the way that we get this address onto the stack is important.
      The compiler will generate unwind information for SignalConverter that
      will attempt to undo any stack modifications that are made by this
      function when unwinding past it.  In this particular case, we don't want
      that to happen, so we use some assembly language tricks to get around
      the compiler noticing the stack modification.
    }

私が抱えている問題の原因のようです。

この例外システムが制御を RTL に渡した後にスタックトレースを実行すると、次のようになります。完了)

0: MyExceptionBacktracer
1: initunwinder in System.pas
2: RaiseSignalException in System.Internal.ExcUtils.pas 

RaiseSignalExceptionによって呼び出されるため、libc によって提供される関数は、スタックに加えられた変更と互換性がないSignalConverterと考えるようになりました。backtraceそのため、そのポイントを超えてスタックを読み取ることはできませんが、スタックはまだその下に存在しています。

それについて何をすべきか(または私の仮説が正しいかどうか)を知っている人はいますか?

アップデート 3

私はついにOSXで適切なスタックトレースを取得することができました. Honza と Sebastian の両方に感謝します。両方のテクニックを組み合わせることで、うまくいくものを見つけました。

これから恩恵を受ける可能性のある他の人のために、ここに基本的なソースがあります. 100% 正しいかどうかはわかりませんが、改善を提案できる場合は、どうぞ。この手法は、Delphi が障害のあるスレッドのスタックをアンワインドする直前に例外にフックし、事前に発生した可能性のあるスタック フレームの破損を補償します。

unit MyExceptionHandler;

interface

implementation

uses
  SysUtils;

var
  PrevRaiseException: function(Exc: Pointer): LongBool; cdecl;

function backtrace2(base : NativeUInt; buffer : PPointer; size : Integer) : Integer;
var SPMin   : NativeUInt;
begin
  SPMin:=base;
  Result:=0;
  while (size > 0) and (base >= SPMin) and (base <> 0) do begin

    buffer^:=PPointer(base + 4)^;
    base:=PNativeInt(base)^;

    //uncomment to test stacktrace
    //WriteLn(inttohex(NativeUInt(buffer^), 8));

    Inc(Result);
    Inc(buffer);
    Dec(size);

  end;
  if (size > 0) then buffer^:=nil;
end;

procedure UnInstallExceptionHandler; forward;

var
  InRaiseException: Boolean;

function RaiseException(Exc: Pointer): LongBool; cdecl;
var b : NativeUInt;
    c : Integer;
    buff : array[0..7] of Pointer;
begin
  InRaiseException := True;

  asm
    mov b, ebp
  end;

  c:=backtrace2(b - $4 {this is the compiler dependent value}, @buff, Length(buff));
  //... do whatever you want to do with the stacktrace

  Result := PrevRaiseException(Exc);
  InRaiseException := False;
end;

procedure InstallExceptionHandler;
var
  U: TUnwinder;
begin
  GetUnwinder(U);
  Assert(Assigned(U.RaiseException));
  PrevRaiseException := U.RaiseException;
  U.RaiseException := RaiseException;
  SetUnwinder(U);
end;

procedure UnInstallExceptionHandler;
var
  U: TUnwinder;
begin
  GetUnwinder(U);
  U.RaiseException := PrevRaiseException;
  SetUnwinder(U);
end;

initialization
  InstallExceptionHandler;
end.
4

2 に答える 2

8

を使用できます。クラスではGetExceptionStackInfoProc、スタック トレースを保存して取得する必要があります。おそらく、Android でこれを示しているhttps://bitbucket.org/shadow_cs/delphi-arm-backtraceを見ることもできます。CleanUpStackInfoProcGetStackInfoStringProcExceptionGetExceptionStackInfoProcGetStackInfoStringProcStackTraceException

Mac OS X でこれを適切に行うには、 fromlibc backtraceを呼び出すときに Delphi がスタック フレームを破損するため、関数を使用できません。手で修正できる別のベースアドレスからスタックをウォークできる独自の実装を使用する必要があります。GetExceptionStackInfoProcException.RaisingException

GetExceptionStackInfoProc次に、次のようになります (この例では XE5 を使用しました。EBP ベローに追加される値は、使用するコンパイラによって異なる場合があり、この例は Mac OS X でのみテストされており、Windows の実装は異なる場合と異なる場合があります) :

var b : NativeUInt;
    c : Integer;
    buff : array[0..7] of Pointer;
begin
  asm
    mov b, ebp
  end;
  c:=backtrace2(b - $14 {this is the compiler dependent value}, @buff, Length(buff));
  //... do whatever you want to do with the stacktrace
end;

関数は次のようになりbacktrace2ます (スタック ウォーキング中に AV が発生しないようにするため、実装には停止条件やその他の検証が含まれていないことに注意してください)。

function backtrace2(base : NativeUInt; buffer : PPointer; size : Integer) : Integer;
var SPMin   : NativeUInt;
begin
  SPMin:=base;
  Result:=0;
  while (size > 0) and (base >= SPMin) and (base <> 0) do begin
    buffer^:=PPointer(base + 4)^;
    base:=PNativeInt(base)^;
    Inc(Result);

    Inc(buffer);
    Dec(size);
  end;
  if (size > 0) then buffer^:=nil;
end;
于 2014-04-13T13:29:23.307 に答える
1

Exception Unwinder に引っかかる可能性があります。次に、例外が発生した場所で backtrace を呼び出すことができます。これが例です。ユニット SBMapFiles は、マップファイルの読み取りに使用するものです。例外コール スタックを取得する必要はありません。

unit MyExceptionHandler;

interface

implementation

uses
  Posix.Base, SysUtils, SBMapFiles;

function backtrace(result: PNativeUInt; size: Integer): Integer; cdecl; external libc name '_backtrace';
function _NSGetExecutablePath(buf: PAnsiChar; BufSize: PCardinal): Integer; cdecl; external libc name '__NSGetExecutablePath';

var
  PrevRaiseException: function(Exc: Pointer): LongBool; cdecl;
  MapFile: TSBMapFile;

const
  MaxDepth = 20;
  SkipFrames = 3;

procedure ShowCurrentStack;
var
  StackLog: PNativeUInt; //array[0..10] of Pointer;
  Cnt: Integer;
  I: Integer;
begin
  {$POINTERMATH ON}
  GetMem(StackLog, SizeOf(Pointer) * MaxDepth);
  try
    Cnt := backtrace(StackLog, MaxDepth);

    for I := SkipFrames to Cnt - 1 do
    begin
      if StackLog[I] = $BE00EF00 then
      begin
        WriteLn('---');
        Break;
      end;
      WriteLn(IntToHex(StackLog[I], 8), ' ', MapFile.GetFunctionName(StackLog[I]));
    end;

   finally
    FreeMem(StackLog);
   end;
  {$POINTERMATH OFF}
end;

procedure InstallExceptionHandler; forward;
procedure UnInstallExceptionHandler; forward;

var
  InRaiseException: Boolean;

function RaiseException(Exc: Pointer): LongBool; cdecl;
begin
  InRaiseException := True;
  ShowCurrentStack;

  Result := PrevRaiseException(Exc);
  InRaiseException := False;
end;

procedure InstallExceptionHandler;
var
  U: TUnwinder;
begin
  GetUnwinder(U);
  Assert(Assigned(U.RaiseException));
  PrevRaiseException := U.RaiseException;
  U.RaiseException := RaiseException;
  SetUnwinder(U);
end;

procedure UnInstallExceptionHandler;
var
  U: TUnwinder;
begin
  GetUnwinder(U);
  U.RaiseException := PrevRaiseException;
  SetUnwinder(U);
end;

procedure LoadMapFile;
var
  FileName: array[0..255] of AnsiChar;
  Len: Integer;
begin
  if MapFile = nil then
  begin
    MapFile := TSBMapFile.Create;
    Len := Length(FileName);
    _NSGetExecutablePath(@FileName[0], @Len);
    if FileExists(ChangeFileExt(FileName, '.map')) then
      MapFile.LoadFromFile(ChangeFileExt(FileName, '.map'));
  end;
end;

initialization
  LoadMapFile;
  InstallExceptionHandler;
end.
于 2014-04-21T21:17:22.583 に答える