Получить имя устройства из InitTable — взломать System.pas

Для отладки нашего приложения Delphi XE3 с медленным запуском мы хотели бы регистрировать фазу инициализации всех используемых модулей в нашей системе.

Чтобы выполнить эту задачу, нам нужно имя модуля для каждого вызова инициализации.

Анализируя данные, проходя через InitNames, мы находим внутри первое имя модуля:

InitContext.InitTable^.TypeInfo.UnitNames

но мы не знаем, как получить соответствующее Имя из идентификатора объекта I перед вызовом процедуры Инициализация. В документации по коду говорится, что TypeInfo.UnitNames содержит объединенные имена всех модулей. Но как мне перемещаться между ними? Это не массив и не длинная строка с разделителями.

Код, куда я хотел бы вставить процедуру журнала.

procedure InitUnits;
var
  Count, I: Integer;
  Table: PUnitEntryTable;
  P: Pointer;
begin
  if InitContext.InitTable = nil then
    exit;
  Count := InitContext.InitTable^.UnitCount;
  I := 0;
  Table := InitContext.InitTable^.UnitInfo;
{$IFDEF LINUX}
  Inc(PByte(Table), InitContext.Module^.GOT);
{$ENDIF}
  try
    while I < Count do
    begin

      /////////////////////////////////////
      MyLogCode( 'Unit: ' + Get UnitName here )  
      /////////////////////////////////////

      P := Table^[I].Init;
      Inc(I);
      InitContext.InitCount := I;
      if Assigned(P) and Assigned(Pointer(P^)) then
  begin
{$IF defined(MSWINDOWS)}
    TProc(P)();
{$ELSEIF (defined(POSIX) and defined(CPUX86))}
        CallProc(P, InitContext.Module^.GOT);
{$ELSE}
        TProc(P)();
{$ENDIF}
      end;
    end;
  except
    FinalizeUnits;
    raise;
  end;
end;

Перекомпиляция System.pas будет выполнена с помощью предложенного Арно Бушез решения.


person Hugie    schedule 11.04.2014    source источник
comment
Вы уверены, что хотите это сделать? Вы действительно хотите изменить System.pas?   -  person David Heffernan    schedule 11.04.2014
comment
Я знаю, что это может стать немного утомительным с кучей модулей, но использование CodeSite (подойдет включенная версия Express Edition) с EnterMethod/ExitMethod выглядит прямолинейно и гораздо менее навязчиво для меня.   -  person Uwe Raabe    schedule 11.04.2014
comment
Я бы предложил использовать профилировщик, чтобы найти узкое место в вашем приложении, которое не требует изменения кода. Я только что использовал NQS LineTimer, и он показывает, что у меня есть какой-то длинный код в моей части инициализации некоторого модуля (я просто посчитал до MaxInt).   -  person Stefan Glienke    schedule 11.04.2014
comment
Я написал для нашего приложения простой регистратор, чувствительный к потокам, который я уже использую во всем приложении и хотел бы использовать его и для этого. Он имеет хорошее визуальное представление времени выполнения и времени вызова в виде графической временной шкалы. Недостатком является то, что мне нужно вставить код журнала вручную. Я рассмотрю предложенные вами приложения. Спасибо.   -  person Hugie    schedule 11.04.2014
comment
Мне бы хотелось иметь логгер, чувствительный к потокам, который визуализирует журналы, как профилировщик потоков игрового движка. Например: ImageOfProfilerVis   -  person Hugie    schedule 11.04.2014


Ответы (2)


UnitNames содержит несколько имен единиц. Это короткие строки Паскаля, объединенные в конкатенацию. Однако, как мы увидим, это не те имена, которые вам нужны.

Включите отладчик в InitUnits и оцените:

PAnsiChar(InitContext.InitTable.TypeInfo.UnitNames)

В моем простом тестовом проекте, консольном приложении, которое просто использует SysUtils, вы видите следующее:

#$F'System.SysUtils'#6'System'#$18'System.Internal.ExcUtils'#$F'System.SysConst'
#7'SysInit'#$10'System.Character'#$E'Winapi.Windows'#$E'System.UITypes'
#$C'System.Types'#$10'System.RTLConsts'#$C'Winapi.PsAPI'#$F'Winapi.SHFolder'
#$F'Winapi.ImageHlp‹À'

Первый символ — это длина строки. Они объединены одно за другим, всего InitContext.InitTable.TypeInfo.UnitCount имен. Для моего простого проекта InitContext.InitTable.TypeInfo.UnitCount оценивается как 13.

Однако эти имена не соответствуют инициализируемым модулям. В моем тестовом проекте InitContext.InitTable^.UnitCount имеет значение 18, и юниты инициализируются в совершенно другом порядке, чем указано выше. Я уверен, вы знаете, что SysInit всегда на первом месте. Как видно из вышеизложенного, он находится в середине списка. Таким образом, хотя InitContext.InitTable.TypeInfo.UnitNames дает вам список определенных модулей, он не имеет отношения ни к модулям, требующим инициализации, ни к порядку инициализации.

Итак, как я это прочитал, UnitNames не может вам здесь помочь. Я считаю, что вам нужно будет использовать подробный файл карты, чтобы расшифровать это. Вам нужно найти имя функции Table^[I].Init. Если бы вы использовали, например, madExcept, это было бы легко сделать.

Конечно, вы не сможете выполнить поиск внутри InitUnits. Вы столкнулись с ситуацией курицы и яйца. Возможно, вам потребуется инициализировать по крайней мере некоторые единицы, прежде чем вы начнете вести журнал.

Например, похоже, что вы пытаетесь выделить строковую переменную. Это не удастся, потому что распределитель кучи RTL не был инициализирован. Ваш код ведения журнала не может выполнять какое-либо динамическое выделение с использованием кучи RTL, если вы ожидаете вызвать его до инициализации RTL.


Мне все это кажется чрезмерным. Если бы я был тобой, я бы:

  1. Идентифицируйте единицы по индексу при регистрации. То есть запишите значение I.
  2. Используйте результаты своего профилирования, чтобы выяснить, какие индексы являются проблемными.
  3. В отладчике используйте условную точку останова для останова при вызове TProc(P)(), связанного с индексом, который вы определили на предыдущем шаге.
  4. Войдите в P, чтобы узнать, к какому отряду он прикреплен.
person David Heffernan    schedule 11.04.2014
comment
Да, мы используем madExcept, я забыл об этом. Я попробую сохранить указатель и отобразить его позже при запуске нашей формы анализатора. Ваше окончательное решение — это то, о чем я тоже думал, но его нельзя использовать на компьютере, не предназначенном для разработки, в реальной среде, например, на клиентском компьютере. - person Hugie; 11.04.2014
comment
Порядок инициализации на клиентской машине будет таким же, как и на машине разработчика. Если вы знаете, какой индекс идентифицирует единицу, то у вас есть то, что вам нужно. - person David Heffernan; 11.04.2014
comment
Ты прав. Но чтобы избежать ручного определения наиболее важных идентификаторов для каждой сборки или после больших изменений кода, я бы предпочел автоматическое решение, такое как madExcept. - person Hugie; 11.04.2014
comment
Возможно, более серьезной проблемой для вас является то, что вы должны избегать выделения кучи RTL до тех пор, пока RTL не будет инициализирован. Вы понимаете этот вопрос? - person David Heffernan; 11.04.2014
comment
У вас есть ссылки, чтобы копнуть глубже? Я знаю разницу между стеком и кучей и (на самом деле) тем, что Delphi или FastMM4 управляют распределением кучи для ускорения процесса. Вы пытаетесь сказать мне следующее: если Fastmm4 ist не инициализирован и не знает о моем предыдущем выделении кучи, я столкнусь с такими проблемами, как MemoryLeaks или перекрытия распределения памяти? - person Hugie; 11.04.2014
comment
@Hugie Ну, официальной документации, в которой рассказывается, как взломать RTL, нет. Но вы можете решить это, посмотрев на источник. В процедуре инициализации модуля System вы найдете вызов InitializeMemoryManager. До выполнения этого вызова куча RTL недоступна для использования. И код в вашем вопросе, который выполняет преобразование строк, происходит до вызова InitializeMemoryManager. Более того, если вы замените диспетчер памяти другим (например, полным FastMM), вам нужно позаботиться об освобождении той же кучи, на которой вы выделили. - person David Heffernan; 11.04.2014
comment
Вы обнаружите, что инициализация RTL проходит немного раньше, чем выделяется первая память кучи RTL. По моим подсчетам, перед использованием кучи RTL инициализируются три блока. И я был бы поражен, если бы ваш класс ведения журнала не выполнял выделение кучи RTL. Так что этот звонок MyLogCode будет бомбой. - person David Heffernan; 11.04.2014
comment
В этом есть смысл. Хорошо, моим текущим теоретическим приемом будет введение новой функции обратного вызова перед инициализацией. Эта функция, если она назначена, будет запущена перед инициализацией и получит идентификатор и указатель функции инициализации в качестве параметра. Итак, при инициализации моего первого модуля проекта я установлю эту функцию обратного вызова. В этот момент критические юниты должны быть инициализированы, а замедляющие юниты — нет. При этом у меня есть обратная связь нового вызова Initialization, есть необходимые данные для более поздних вопросов madExcept и не провоцируют кучи ошибок. - person Hugie; 11.04.2014

В общем, лучше НЕ полагаться на порядок конструкторов классов, секций инициализации, деструкторов классов и секций финализации.

В большом (1.2M LOC) проекте невозможно сделать вывод, какой порядок будет использовать Delphi.

Лучше всего провести рефакторинг кода, чтобы избежать таких зависимостей, или использовать побочный эффект шаблона Singleton (инициализировать при первой необходимости).

Тем не менее, если вам действительно нужно вывести заказ, вы можете вручную зарегистрировать все

  • Записи раздела инициализации

  • Записи раздела завершения

  • конструкторы классов

  • деструкторы классов

Один из безопасных способов сделать это — использовать Windows API — OutputDebugString().

Вы можете использовать следующий блок, чтобы помочь вам с ведением журнала

unit unt_Debug;
{$SCOPEDENUMS ON}

interface

uses
  // System
  System.SysUtils
  {$IFDEF MACOS}
  ,FMX.Types
  {$ELSE}
  ,Winapi.Windows
  {$ENDIF};

{$WARN SYMBOL_PLATFORM OFF}
/// <remarks>
/// Output debug string. Output debug string can be seen in Delphi
/// View|Debug Windows|Event Log or with 3-rd party programs such as
/// dbgview.exe from SysInternals (www.sysinternals.com)
/// </remarks>
procedure ODS(const Text: string);

procedure ReportClassConstructorCall(const C: TClass);

procedure ReportClassDestructorCall(const C: TClass);

procedure ReportInitializationSection(const UnitName: string);

procedure ReportFinalizationSection(const UnitName: string);

implementation

procedure ReportClassConstructorCall(const C: TClass);
begin
  ODS(Format('%0:s class constructor invoked.', [C.ClassName]));
end;

procedure ReportClassDestructorCall(const C: TClass);
begin
  ODS(Format('%0:s class destructor invoked.', [C.ClassName]));
end;

procedure ReportInitializationSection(const UnitName: string);
begin
  ODS(Format('Unit %0:s - entering Initialization section.', [UnitName]));
end;

procedure ReportFinalizationSection(const UnitName: string);
begin
  ODS(Format('Unit %0:s - entering Finalization section.', [UnitName]));
end;

procedure ODS(const Text: string);
begin
  {$IFDEF DEBUG}
  {$IFDEF MACOS}
  // http://stackoverflow.com/questions/12405447/outputdebugstring-with-delphi-for-macosunit unt_Debug;
   Log.d(Text);
  {$ENDIF}
  {$IFDEF LINUX}
  __write(stderr, AText, Length(AText));
  __write(stderr, EOL, Length(EOL));
  {$ENDIF}
  {$IFDEF MSWINDOWS}
  OutputDebugString(PWideChar(Text));
  {$ENDIF}
  {$ENDIF}
end;

{$WARN SYMBOL_PLATFORM ON}

end.

Его можно использовать, например, так:

unit Sample;

interface

type
  TSample = class
  public
    class constructor Create;
    class destructor Destroy;
  end;

implementation

uses
  unt_Debug;

class constructor TSample.Create;
begin
  ReportClassConstructorCall(TSample);
  // Do stuff
end;

class destructor TSample.Destroy;
begin
  ReportClassDestructorCall(TSample);
  // Do stuff
end;

initialization

  ReportInitializationSection(TSample.UnitName);
  // do stuff

finalization

  ReportFinalizationSection(TSample.UnitName);
  // do stuff

end.
person Gad D Lord    schedule 01.01.2016