Delphi-получить полную трассировку стека на OSX

у меня есть приложение, которое может регистрировать stacktrace, который позже может использоваться для отладки.

в Windows я получил, используя отличный блок JCLDebug, предоставленный проектом JEDI.

теперь, когда мое приложение работает на OSX, я попал в небольшую заминку - я не знаю, как получить правильный stacktrace при возникновении исключения.

у меня есть основы вниз -

1) я могу получить stacktrace, используя "backtrace" (найдено в libSystem.dylib нужна)

2) результирующий backtrace может быть преобразован в номера строк с помощью.файл карты, предоставленный компоновщиком Delphi

проблема, с которой я остаюсь, - я не знаю, откуда позвонить в backtrace. Я знаю, что Delphi использует исключения Mach (в отдельном потоке), и что я не могу использовать сигналы posix, но это все, что мне удалось разобраться.

Я могу получить backtrace в " try...кроме блока, но, к сожалению, к этому моменту стек уже закончили.

как я могу установить правильный регистратор исключений, который будет работать сразу после возникновения исключения?

обновление:

согласно предложению 'Honza R, я взглянул на процедуру "GetExceptionStackInfoProc".

эта функция позволяет мне "внутри" процесса обработки исключений, но, к сожалению, оставляет меня с некоторыми из тех же проблем, которые я имел ранее.

прежде всего-на рабочем столе платформы, эта функция "GetExceptionStackInfoProc" - это просто указатель функции, который вы можете назначьте свой собственный обработчик информации об исключениях. Таким образом, из коробки Delphi не предоставляет никакого поставщика информации о стеке.

если я назначаю функцию "GetExceptionStackInfoProc" , а затем запускаю "backtrace" внутри нее, я получаю stacktrace, но эта трассировка относится к обработчику исключений, а не к потоку, который вызвал исключение.

'GetExceptionStackInfoProc' содержит указатель на "TExceptionRecord", но на это есть очень ограниченная документация.

Я могу выйти за пределы моей глубины, но как я могу получить stacktrace из правильной нити? Можно ли было бы ввести мою собственную функцию "backtrace" в обработчик исключений, а затем вернуться к стандартному обработчику исключений оттуда?

обновление 2

некоторые более подробности. Одно дело прояснить - этот вопрос касается исключений, которые обрабатываются сообщениями MACH, а не исключениями программного обеспечения, которые обрабатываются полностью в RTL.

Embarcadero выложил некоторые комментарии вместе с этими функциями -

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

которые, похоже, ответственны за проблему, которую я имею.

когда я делаю stacktrace после того, как эта система исключений передала управление RTL, это выглядит так - (имея в виду, размотчик стека была заменена программой backtrace. Backtrace передаст управление на разматыватель, как только он будет завершен)

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

С RaiseSignalException называется SignalConverter, Я убежден, что backtrace функция, предоставляемая libc, несовместима с изменениями, внесенными в стек. Таким образом, он не способен читать стек за пределами этой точки, но стек все еще присутствует под ним.

кто-нибудь знает, что с этим делать (или моя гипотеза правильно)?

обновление 3

мне наконец удалось получить правильные stacktraces на OSX. Огромное спасибо и Хонзе, и Себастьяну. Объединив обе их техники, я нашел то, что работает.

для всех, кто может извлечь выгоду из этого, вот основной источник. Имейте в виду, что я не совсем уверен, что это 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 -  {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.

2 ответов


можно использовать GetExceptionStackInfoProc, CleanUpStackInfoProc и GetStackInfoStringProc на Exception class вам нужно сохранить трассировку стека в GetExceptionStackInfoProc а затем получить его с GetStackInfoStringProc который будет вызван RTL, если вы используете StackTrace свойства Exception. Может быть, вы также могли бы взглянуть на https://bitbucket.org/shadow_cs/delphi-arm-backtrace что демонстрирует это на Android.

чтобы сделать это правильно на Mac OS X libc backtrace функция не может использоваться, потому что Delphi повредит стек рамка при вызове GetExceptionStackInfoProc С Exception.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 -  {this is the compiler dependent value}, @buff, Length(buff));
  //... do whatever you want to do with the stacktrace
end;

и backtrace2 функция будет выглядеть как это (обратите внимание, что условия остановки и другие проверки отсутствуют в реализации, чтобы гарантировать, что AVs не вызваны во время ходьбы стека):

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;

вы можете подключить себя к Разматывателю исключений. Затем вы можете вызвать backtrace, где происходит исключение. Вот пример. Блок SBMapFiles-это то, что я использую для чтения mapfiles. Не требуется получать стек вызовов исключений.

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.