Вземете име на единица от 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 ще бъде извършено чрез предложеното от Arnaud Bouchez решение.


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 съдържа множество имена на единици. Те са Pascal къси низове, свързани. Въпреки това, както ще видим, те не са имената, от които се нуждаете.

Разбийте програмата за отстраняване на грешки в 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
Имате ли връзки, за да се задълбочите в това? Знам разликата между stack&heap и (afaik), че Delphi или FastMM4 управляват разпределението на heap, за да ускорят процеса. Това, което се опитвате да ми кажете е: Ако Fastmm4 не е инициализиран и не знае за предишното ми разпределение на паметта, ще се натъкна на проблеми като MemoryLeaks или Memory Allocation Overlaps? - person Hugie; 11.04.2014
comment
@Hugie Добре, няма официална документация, която да ви казва как да хакнете RTL. Но можете да го разберете, като погледнете източника. В init proc за системния модул намирате извикване на InitializeMemoryManager. Преди да бъде направено това повикване, RTL купчината не е достъпна за използване. И кодът във вашия въпрос, който изпълнява concat на низ, се случва преди да бъде извикан InitializeMemoryManager. Нещо повече, ако замените мениджъра на паметта с друг (напр. пълен FastMM), тогава трябва да се погрижите да освободите същата купчина, за която сте разпределили. - person David Heffernan; 11.04.2014
comment
Ще откриете, че е малко напред в инициализацията на RTL, преди да бъде разпределена първата RTL куп памет. Според моите изчисления три единици се инициализират преди да се използва RTL купчината. И бих бил изумен, ако вашият клас за регистриране не извърши разпределение на RTL купчина. Така че това обаждане до MyLogCode ще бомбардира. - person David Heffernan; 11.04.2014
comment
Това има смисъл. Добре, текущият ми теоретичен хак би бил да въведа нова функция за обратно извикване преди инициализация. Тази функция, ако е зададена, ще бъде задействана преди инициализацията и ще получи идентификатора и указателя на функцията за инициализация като параметър. И така, при инициализирането на първия ми проектен модул, ще задам тази функция за обратно извикване. В този момент критичните модули трябва да бъдат инициализирани, а модулите за забавяне не трябва да бъдат. По този начин имам обратната връзка за ново извикване за инициализация, имам необходимите данни за по-късните въпроси за 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