Delphi World - это проект, являющийся сборником статей и малодокументированных возможностей  по программированию в среде Delphi. Здесь вы найдёте работы по следующим категориям: delphi, delfi, borland, bds, дельфи, делфи, дэльфи, дэлфи, programming, example, программирование, исходные коды, code, исходники, source, sources, сорцы, сорсы, soft, programs, программы, and, how, delphiworld, базы данных, графика, игры, интернет, сети, компоненты, классы, мультимедиа, ос, железо, программа, интерфейс, рабочий стол, синтаксис, технологии, файловая система...
Исследование кода, генерируемого Delphi 3

Оформил: DeeCo
Автор: http://www.cracklab.narod.ru

Часть 3. Интерфейсы и published свойства Итак, мы уже знаем, как найти VTBL. Но в каком порядке хранятся в ней методы ? Ответ можно получить, посмотрев на ассемблерный листинг и сравнив его с исходным кодом VCL. И выяснится, что новые методы дописыватся в конец VTBL, по мере произведения новых классов. Я проследил генеалогию классов до TWinControl и вот что у меня получилось (цифра означает смещение в VTBL):
  • TObject
    Виртуальные методы этого класса расположены в VTBL по отрицательным индексам. Смотрите моё описание RTTI в предыдущей статье
  • TPersistent
    • 0x00 AssignTo
    • 0x01 DefineProperties
    • 0x02 Assign
  • TComponent
    В нём, помимо всего прочего, реализуется также интерфейсы IUnknown & IDispatch, поэтому объекты-производные от него могут быть серверами OLE-Automation
    • 0x03 Loaded
    • 0x04 Notification
    • 0x05 ReadState
    • 0x06 SetName
    • 0x07 UpdateRegistry
    • 0x08 ValidateRename
    • 0x09 WriteState
    • 0x0A QueryInterface
    • 0x0B Create(AOwner: TComponent)
  • TControl
    Его производные классы могут быть помещены на форму во время проектрирования и умеют отображать себя ( так называемые "видимые" компоненты )
    • 0x0C UpdateLastResize
    • 0x0D CanResize
    • 0x0E CanAutoResize
    • 0x0F ConstrainedResize
    • 0x10 GetClientOrigin
    • 0x11 GetClientRect
    • 0x12 GetDeviceContext
    • 0x13 GetDragImages
    • 0x14 GetEnabled
    • 0x15 GetFloating
    • 0x16 GetFloatingDockSiteClass
    • 0x17 SetDragMode
    • 0x18 SetEnabled - полезный метод, особенно для всяких кнопок в диалогах регистрации серийных номеров...
    • 0x19 SetParent
    • 0x1A SetParentBiDiMode
    • 0x1B SetBiDiMode
    • 0x1C WndProc - адрес оконной процедуры. Если она не находит обработчика у себя, вызывается метод TObject::Dispatch. И уже последний метод вызывает dynamic функцию по индексу, равному номеру сообщения Windows.
    • 0x1D InitiateAction
    • 0x1E Invalidate
    • 0x1F Repaint - адрес функции отрисовки компонента
    • 0x20 SetBounds
    • 0x21 Update
  • TWinControl
    Его производные классы имеют собственное окно
    • 0x22 AdjustClientRect
    • 0x23 AlignControls
    • 0x24 CreateHandle
    • 0x25 CreateParams
    • 0x26 CreateWindowHandle
    • 0x27 CreateWnd
    • 0x28 DestroyWindowHandle
    • 0x29 DestroyWnd
    • 0x2A GetControlExtents
    • 0x2B PaintWindow
    • 0x2C ShowControl
    • 0x2D SetFocus
А где же хранятся методы интерфейсов, спросите Вы ? Хороший вопрос, учитывая, что классы Delphi могут иметь только одного предка, но в то же самое время реализовывать несколько интерфейсов. Чтобы выяснить это, я написал ещё одну тестовую программу, на сей раз из нескольких файлов.
Unit1.pas - главная форма приложения.
interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, Project1_TLB;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

uses
  Unit2;

procedure TForm1.Button1Click(Sender: TObject);
var
  My_Object: TRP_Server;
  My_Interface: IRP_Server;
begin
  My_Object := nil;
  My_Interface := nil;
  try
    My_Object := TRP_Server.Create;
    My_Interface := My_Object;
    My_Interface.RP_Prop := PChar('??????');
    MessageDlg(Format('My Method1: %d, string is %s, refcount is %d',
      [My_Interface.Method1(1), My_Interface.RP_Prop, My_Object.RefCount]),
      mtConfirmation, [mbOk], 0);
  finally
    if My_Interface <> nil then
      My_Interface := nil;
    (* ??? ?? ????????? - My_Object ??? ?? ?????????? ????? *)
    MessageDlg(Format('refcount is %d', [My_Object.RefCount]),
      mtConfirmation, [mbOk], 0);
  end;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  RP_IO: IRP_Server;
begin
  try
    RP_IO := CoRP_Server.Create;
    RP_IO.RP_Prop := 'Yet one string';
    MessageDlg(Format('String is %s, Method1 return %d',
      [RP_IO.RP_Prop, RP_IO.Method1(123)]), mtConfirmation, [mbOk], 0);
  except
    on e: Exception do
      MessageDlg(Format('Exception occured: %s, reason %s',
        [e.ClassName, e.Message]), mtError, [mbOk], 0);
  end;
end;
Projcet1_TLB.pas - файл, автоматически сгенерированный Delphi для классов, являющихся серверами OLE-Automation
unit Project1_TLB;
...
interface

uses Windows, ActiveX, Classes, Graphics, OleCtrls, StdVCL;

// *********************************************************************//
// GUIDS declared in the TypeLibrary. Following prefixes are used:      //
//   Type Libraries     : LIBID_xxxx                                    //
//   CoClasses          : CLASS_xxxx                                    //
//   DISPInterfaces     : DIID_xxxx                                     //
//   Non-DISP interfaces: IID_xxxx                                      //
// *********************************************************************//
const
  LIBID_Project1: TGUID = '{198C3180-6073-11D3-908D-00104BB6F968}';
  IID_IRP_Server: TGUID = '{198C3181-6073-11D3-908D-00104BB6F968}';
  CLASS_RP_Server: TGUID = '{198C3183-6073-11D3-908D-00104BB6F968}';
type

  // *********************************************************************//
  // Forward declaration of interfaces defined in Type Library            //
  // *********************************************************************//
  IRP_Server = interface;
  IRP_ServerDisp = dispinterface;

  // *********************************************************************//
  // Declaration of CoClasses defined in Type Library                     //
  // (NOTE: Here we map each CoClass to its Default Interface)            //
  // *********************************************************************//
  RP_Server = IRP_Server;

  // *********************************************************************//
  // Interface: IRP_Server
  // Flags:     (4416) Dual OleAutomation Dispatchable
  // GUID:      {198C3181-6073-11D3-908D-00104BB6F968}
  // *********************************************************************//
  IRP_Server = interface(IDispatch)
    ['{198C3181-6073-11D3-908D-00104BB6F968}']
    function Method1(a: Integer): Integer; safecall;
    function Get_RP_Prop: PChar; safecall;
    procedure Set_RP_Prop(Value: PChar); safecall;
    property RP_Prop: PChar read Get_RP_Prop write Set_RP_Prop;
  end;

  // *********************************************************************//
  // DispIntf:  IRP_ServerDisp
  // Flags:     (4416) Dual OleAutomation Dispatchable
  // GUID:      {198C3181-6073-11D3-908D-00104BB6F968}
  // *********************************************************************//
  IRP_ServerDisp = dispinterface
    ['{198C3181-6073-11D3-908D-00104BB6F968}']
    function Method1(a: Integer): Integer; dispid 1;
    property RP_Prop: {??PChar} OleVariant dispid 2;
  end;

  CoRP_Server = class
    class function Create: IRP_Server;
    class function CreateRemote(const MachineName: string): IRP_Server;
  end;

implementation

uses ComObj;

class function CoRP_Server.Create: IRP_Server;
begin
  Result := CreateComObject(CLASS_RP_Server) as IRP_Server;
end;

class function CoRP_Server.CreateRemote(const MachineName: string): IRP_Server;
begin
  Result := CreateRemoteComObject(MachineName, CLASS_RP_Server) as IRP_Server;
end;
Меня всегда интересовало, как же это так Delphi позволят иметь код, запускаемый при инициализации и деинициализации модуля ? Просмотрев исходный код в файле Rtl/Sys/System.pas ( я рекомендую иметь исходные тексты, поставляемые вместе с Delphi при исследовании написанных на ней программ ) и сравнив его с ассемблерным листингом, выясняется, что это легко и непринуждённо. Итак, существуют несколько довольно простых структур:
PackageUnitEntry = record
  Init, FInit: procedure;
end;

{ Compiler generated table to be processed sequentially to init & finit all package units }
{ Init: 0..Max-1; Final: Last Initialized..0                                              }
UnitEntryTable = array[0..9999999] of PackageUnitEntry;
PUnitEntryTable = ^UnitEntryTable;

PackageInfoTable = record
  UnitCount: Integer; { number of entries in UnitInfo array; always > 0 }
  UnitInfo: PUnitEntryTable;
end;

PackageInfo = ^PackageInfoTable;
При startupе указатель на PackageInfoTable передаётся единственным аргументом функции InitExe:
start        proc near
             push    ebp
             mov     ebp, esp
             add     esp, 0FFFFFFF4h
             mov     eax, offset dword_0_445424
             call    @@InitExe       ; ::`intcls'::InitExe
По адресу 0x445424 хранится DWORD 0x29 и указатель на таблицу структур PackageUnitEntry, где, в частности, на предпоследнем месте содержатся и адреса моих процедур инициализации и деинициализации.

Delphi помещает список реализуемых классом интерфейсов в отдельную структуру, указатель на которую помещает в RTTI по смещению 0x4. Сама эта структура описана во всё том же Rtl/Sys/System.pas:

PGUID = ^TGUID;
TGUID = record
  D1: LongWord;
  D2: Word;
  D3: Word;
  D4: array[0..7] of Byte;
end;

PInterfaceEntry = ^TInterfaceEntry;
TInterfaceEntry = record
  IID: TGUID;
  VTable: Pointer;
  IOffset: Integer;
  ImplGetter: Integer;
end;

PInterfaceTable = ^TInterfaceTable;
TInterfaceTable = record
  EntryCount: Integer;
  Entries: array[0..9999] of TInterfaceEntry;
end;
Указатель на TInterfaceTable и помещается в RTTI по смещению 0x4 ( если класс реализует какие-либо интерфейсы ). TGUID - это обычная структура UID, используемая в OLE, VTable - указатель на VTBL интерфейса, IOffset - смещение в данном классе на экземпляр, содержащий данные данного интерфейса. Когда вызывается метод интерфейса, он вызывается обычно от указателя на интерфейс, а не на класс, реализующий этот интерфейс. Мы же пишем методы нашего класса, которые ожидают видеть в качестве нулевого аргумента указатель на экземпляр нашего класса. Поэтому Delphi автоматически генерирует для VTable код, настраивающий свой нулевой аргумент соответствующим образом. Например, для моего класса TRP_Server значение поля IOffset составляет 0x34. Функции же, содержащиеся в VTable, выглядят так:
loc_0_444B39: ; функция, вызываемая по интерфейсу
                add     dword ptr [esp+4], 0FFFFFFCCh
                jmp     MyMethod1	; вызов функции в классе
Напомню, что все методы интерфейсов должны объявляться как safecall - параметры передаются как в C, справо налево, но очистку стека производит вызываемая процедура. Поэтому в [esp+4] содержится нулевой параметр функции - указатель на экземпляр интерфейса - класса IRP_Server. Затем вызывается метод класса TRP_Server, которому должен нулевым параметром передаваться указатель на экземпляр TRP_Server - поэтому происходит настройка этого параметра, 0x0FFFFFFCC = -0x34.

Самый же значимый резльтат всех этий ковыряний в коде - мне удалось обнаружить в RTTI полное описание всех published свойств ! Из системы помощи Delphi: ( файл del4op.hlp, перевод мой ):

Published члены имеют такую же видимость, как public члены. Разница заключается в том, что для published членов генерируется информация о типе времени исполнения (RTTI). RTTI позволяет приложению динамически обращаться к полям и свойствам объектов и отыскивать их методы. Delphi использует RTTI для доступа к значениям свойств при сохранении и загрузке файлов форм (.DFM), для показа свойств в Object Inspector и для присваивания некоторых методов (называемых обработчиками событий) определённым свойствам (называемых событиями) Published свойства ограничены по типу данных. Они могут иметь типы Ordinal, string, класса, интерфейса и указателя на метод класса. Также могут быть использованы наборы (set), если верхний и нижний пределы их базового типа имеют порядковые значения между 0 и 31 (другими словами, набор должен помещаться в байте, слове или двойном слове ). Также можно иметь published свойство любого вещественного типа (за исключением Real48). Свойство-массив не может быть published. Все методы могут быть published, но класс не может иметь два или более перегруженных метода с одинаковыми именами. Члены класса могут быть published, только если они являются классом или интерфейсом. Класс не может содержать published свойств, если он не скомпилирован с ключом {$M+} или является производным от класса, скомпилированного с этим ключом. Подавляющее большинство классов с published свойствами являются производными от класса TPersistent, который уже скомпилирован с ключом {$M+}, так что Вам редко потребуется использовать эту директиву.
Что сиё может означать для reverse engeneerов ? Значение вышесказанного трудно переоценить - мы можем извлечь из RTTI названия, типы и местоположение в классе всех published свойств любого класса ! Если вспомнить, что такие свойства, как Enable, Text, Caption, Color, Font и многие другие для таких компонентов, как TEdit,TButton,TForm и проч., обычно изменяющиеся, предположим, в диалоге регистрации в зависимости от правильности-неправильности серийного номера, имеют как раз тип published... Поскольку все формы Delphi и компоненты в них имеют published свойства, моя фантазия рисует мне куда более сочную и красочную картину...
Одна из главных структур, применяющихся для идентификации published свойств - TPropInfo

TPropInfo = packed record
  PropType: PPTypeInfo;
  GetProc: Pointer;
  SetProc: Pointer;
  StoredProc: Pointer;
  Index: Integer;
  Default: Longint;
  NameIndex: SmallInt;
  Name: ShortString;
end;
После структуры наследования ( по смещению 10h в RTTI ) расположен WORD - количество расположенных следом за ним структур TPropInfo, по одной на каждое published свойство. В этой структуре поля имеют следующие значения:
  • PropType - указатель на структуру, описывающую тип данного свойства. Структуры, содержащиеся в TypeInfo, довольно сложные, так что я не буду объяснять, как именно они работают, Вам достаточно знать, что мой IDC script потрошит её в 99 % случаев. Они описаны в файле vcl/typeinfo.pas.
  • GetProc,SetProc,StoredProc - поля, указывающие на методы Get ( извлечение свойства ), Set ( изменение свойства ) и Stored ( признак сохранения значения свойства ). Для всех них есть недокументрированные правила:
    • Если старший байт этих полей равен 0xFF, то в остальных байтах находится смещение в экземпляре класса, по которому находятся данные, представляющие данное свойство. В таком случае все манипуляции со свойством производятся напрямую.
    • Если старший байт равен 0xFE, то в остальных байтах содержится смещение в VTBL класса, т.е. все манипуляции со свойством производятся через виртуальную функцию.
    • Если значение поля равно 0x80000000 - метод не определён ( скажем, метод Set для read-only published свойств )
    • Значение 1 для поля StoredProc означает обязательное сохранение значения свойства.
    • Все остальные значения полей рассматриваются как ссылка на метод класса.
  • Index - значение не выяснено. Есть подозрение, что это поле связано со свойствами типа массив и подчиняется тем же правилам, что и предыдущие три поля. Во время тестирования мне не встретилось ни одного поля Index со значением, отличным от 0x80000000
  • Default - значение свойства по умолчанию
  • NameIndex - порядковый номер published свойства в данном классе, отсчёт ведётся почему-то с 2.
  • Name - Имя свойства, pascal-style строка

Как видите, можно узнать о published-свойствах практически всё, включая адрес, на который нужно ставить точку останова.
Я изменил свой IDC script для анализа RTTI классов Delphi 4, чтобы он поддерживал все обнаруженные структуры.

Желаю удачи...

Проект Delphi World © Выпуск 2002 - 2004
Автор проекта: ___Nikolay