понедельник, 18 июля 2011 г.

Хак №9: структура таблицы динамических методов

Это перевод Hack #9: Dynamic method table structure. Автор: Hallvard Vassbotn.

Один из слотов в магической структуре компилятора из классовой виртуальной таблицы методов (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;

Вывод

Хотя методы сообщений являются очень элегантным решением проблемы обработки произвольных сообщений Windows (без необходимости поддерживать громоздкие case), вам следует избегать динамических методов в других ситуациях. Теперь у вас должно быть твёрдое понимание того, что такое динамические методы, как они работают, и почему вы должны их избегать.

2 комментария:

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.