Один из слотов в магической структуре компилятора из классовой виртуальной таблицы методов (VMT) является указателем на динамическую таблицу методов (DMT) этого же класса. Класс имеет DMT, только если он объявляет или перекрывает один или несколько динамических методов (или методов-сообщений). DMT содержит 16-разрядный (
word
) Count
, за которым следует массив [0.. Count-1] of Smallint
индексов и массив [0..Count-1] of Pointer
, содержащий адреса кода реализации динамических методов. Обратите внимание, что массивы "встроены" в структуру DMT (в ней нет указателей на эти массивы).Примерным способом представить эту структуру на Pascal-е будет:
type TDMTIndex = Smallint; PDmtIndices = ^TDmtIndices; TDmtIndices = array[0..High(Word)-1] of TDMTIndex; PDmtMethods = ^TDmtMethods; TDmtMethods = array[0..High(Word)-1] of Pointer; PDmt = ^TDmt; TDmt = packed record Count: word; Indicies: TDmtIndices; // на деле - [0..Count-1] Methods : TDmtMethods; // не деле - [0..Count-1] end;Из-за того, что Pascal не поддерживает объявление статических массивов, размер которых варьируется в зависимости от поля, нам придётся выполнить несколько трюков с указателями, чтобы добраться до массива
Methods
. Теперь мы можем обновить объявление нашей записи VMT – мы можем изменить тип поля DynamicTable
с простого Pointer
на более конкретный тип PDmt
:
type PVmt = ^TVmt; TVmt = packed record SelfPtr : TClass; IntfTable : Pointer; AutoTable : Pointer; InitTable : Pointer; TypeInfo : Pointer; FieldTable : Pointer; MethodTable : Pointer; DynamicTable : PDmt; // <-- уточнили ClassName : PShortString; InstanceSize : PLongint; Parent : PClass; SafeCallException : PSafeCallException; AfterConstruction : PAfterConstruction; BeforeDestruction : PBeforeDestruction; Dispatch : PDispatch; DefaultHandler : PDefaultHandler; NewInstance : PNewInstance; FreeInstance : PFreeInstance; Destroy : PDestroy; { UserDefinedVirtuals: array[0..999] of Pointer; } end;
Волшебные подпрограммы компилятора
МодульSystem
содержит несколько волшебных подпрограмм RTL. Кстати, это не я придумал фразы "магия компилятора" и "волшебные подпрограммы". Над объявлениями некоторых специальных типов и подпрограмм, имена которых начинаются с подчёркивания (который становится символом @ после компиляции), из модуля System.pas
вы можете увидеть такой комментарий:
{ Procedures and functions that need compiler magic }В компиляторе жёстко зашита логика по местоположению и использованию этих объявлений, когда он генерирует код для таких возможностей языка как строки, динамические массивы и динамические методы. К ним (объявлениям) нельзя обратиться напрямую из Pascal - только опосредованно, используя языковые возможности, которые они реализуют, либо же явно, но только из BASM. Как мы уже видели несколько раз, для вызова волшебной подпрограммы компилятора из BASM вам нужно использовать синтаксис
CALL System.@MagicName
.Опубликованные в
interface
секции модуля System
подпрограммы, которые работают с диспетчерезацией и поиском динамических методов, выглядят так:
procedure _CallDynaInst; procedure _CallDynaClass; procedure _FindDynaInst; procedure _FindDynaClass;Есть отдельные
Call
и Find
подпрограммы для экземпляров и классов (да, у нас есть динамические методы уровня классов). Подпрограммы CallDyna
принимают параметр Self
(TObject
или TClass
) в регистре EAX
, а 16-битный знаковый Smallint
(индекс) - в регистре SI
. Обе подпрограммы делают явный JMP
прямо на начало кода динамического метода, если они его найдут. Все параметры динамического метода должны располагаться в регистрах EDX
, ECX
и стеке. Вот почему обычно свободно модифицируемый SI
используется для передачи индекса.Обе подпрограммы
FindDyna
не имеют таких ограничений на параметры, поэтому они используют стандартное соглашение вызова (register
), принимая Self
(TObject
или TClass
) в EAX
и индекс в EDX
.Все эти подпрограммы используют общую внутреннюю (не опубликованную в
interface
модуля) функцию GetDynaMethod
, которая, собственно, и выполняет сканирование DMT, проходя по DMT родительских классов при необходимости. Я сумел реконструировать запись TDmt
выше именно анализом её кода. Реализация использует достаточно эффективную инструкцию REPNE SCASW
для быстрого сканирования массива Smallint
в поиске индекса DMT. Совет для отладки
Если вы собираете ваше приложение с отладочной версией RTL (Project Options | Compiler | [X] Use debug DCUs) – включение этой опции является хорошей идеей, если вы используете трейсер исключений вроде JclDebug/JclHookExcept, EurekaLog или madExcept – вы можете попасть во время отладки внутрь процедуры_CallDynaInst
, если вы нажмёте F7 для "входа" в вызов динамического метода. Теперь вы знаете, почему так происходит.
procedure _CallDynaInst; asm ... CALL GetDynaMethod ... JMP ESI ... end;Чтобы быстро перейти к коду самого динамического метода - просто переместите курсор к инструкции
JMP ESI
, нажмите F4 (Run to Cursor), а затем - F7 (Step into). Вот теперь вы находитесь в динамическом методе.Доступ к DMT из Pascal кода
Хотя компилятор и RTL поставляют всю функциональность по диспетчеризации и поиска в DMT, которая может нам понадобится, иногда может быть интересно написать свои собственные процедуры, которые обращаются к этим массивам. С учётом определения типов выше, мы можем написать несколько рабочих (в смысле "worker" - прим.пер.) функций:function GetDmt(AClass: TClass): PDmt; var Vmt: PVmt; begin Vmt := GetVmt(AClass); if Assigned(Vmt) then Result := Vmt.DynamicTable else Result := nil; end; function GetDynamicMethodCount(AClass: TClass): integer; var Dmt: PDmt; begin Dmt := GetDmt(AClass); if Assigned(Dmt) then Result := Dmt.Count else Result := 0; end; function GetDynamicMethodIndex(AClass: TClass; Slot: integer): integer; var Dmt: PDmt; begin Dmt := GetDmt(AClass); if Assigned(Dmt) and (Slot < Dmt.Count) then Result := Dmt.Indicies[Slot] else Result := 0; // или возбуждение исключения end; function GetDynamicMethodProc(AClass: TClass; Slot: integer): Pointer; var Dmt: PDmt; DmtMethods: PDmtMethods; begin Dmt := GetDmt(AClass); if Assigned(Dmt) and (Slot < Dmt.Count) then begin DmtMethods := @Dmt.Indicies[Dmt.Count]; Result := DmtMethods[Slot]; end else Result := nil; // или возбуждение исключения end;Функция
GetDmt
возвращает указатель на DMT по данной ей ссылке на класс (например, от Instance.ClassType
). Три другие процедуры возвращают количество динамических методов в классе и позволяют нам перебрать все индексы и методы в DMT. Учитывая теперь эти вспомогательные функции, мы можем написать программу, которая будет дампить информацию обо всех динамических методах (и методах-сообщений) класса и всех его родительских классов:
procedure DumpDynamicMethods(AClass: TClass); var i : integer; Index: integer; MethodAddr: Pointer; begin while Assigned(AClass) do begin writeln('Dynamic methods in ', AClass.ClassName); for i := 0 to GetDynamicMethodCount(AClass)-1 do begin Index := GetDynamicMethodIndex(AClass, i); MethodAddr := GetDynamicMethodProc(AClass, i); writeln(Format('%d. Index = %2d, MethodAddr = %p', [i, Index, MethodAddr])); end; AClass := AClass.ClassParent; end; end;Мы можем также написать Pascal-эквивалент BASM-го
GetDynaMethod
из System
, который ищет динамический метод по его индексу в DMT:
function FindDynamicMethod(AClass: TClass; DMTIndex: TDMTIndex): Pointer; // Pascal-вариант более быстрой BASM-версии подпрограммы System.GetDynaMethod var Dmt: PDmt; DmtMethods: PDmtMethods; i: integer; begin while Assigned(AClass) do begin Dmt := GetDmt(AClass); if Assigned(Dmt) then for i := 0 to Dmt.Count-1 do if DMTIndex = Dmt.Indicies[i] then begin DmtMethods := @Dmt.Indicies[Dmt.Count]; Result := DmtMethods[i]; Exit; end; // Не в этом классе - поднимаемся по иерархии AClass := AClass.ClassParent; end; Result := nil; end;Ну, разве это не весело? ;)
В качестве глупого примера: мы могли бы использовать эту процедуру, чтобы проверить, имеет ли класс произвольный динамический метод с конкретным (отрицательным) индексом или любой метод обработки сообщений с заданным идентификатором сообщения:
procedure DumpFoundDynamicMethods(AClass: TClass); procedure Dump(DMTIndex: TDMTIndex); var Proc: Pointer; begin Proc := FindDynamicMethod(AClass, DMTIndex); writeln(Format('Dynamic Method Index = %2d, Method = %p', [DMTIndex, Proc])); end; begin Dump(-1); Dump(1); Dump(13); Dump(42); end;
Этот комментарий был удален автором.
ОтветитьУдалитьОчень полезно, даже исчерпывающе!
ОтветитьУдалить