Интерфейсы и published свойства |
Previous Top Next |
Итак, мы уже знаем, как найти 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 - главная форма приложения.
Code: |
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; |
Unit2.pas - объект - сервер OLE-Automation
Code: |
interface
uses ComObj, ActiveX, Project1_TLB, Dialogs, SysUtils;
type TRP_Server = class(TAutoObject, IRP_Server) private MyString: String; protected function Get_RP_Prop: PChar; safecall; function Method1(a: Integer): Integer; safecall; procedure Set_RP_Prop(Value: PChar); safecall; { Protected declarations } public destructor Destroy; override; end;
implementation
uses ComServ;
Destructor TRP_Server.Destroy; begin MessageDlg('Destroy',mtConfirmation,[mbOk],0); Inherited Destroy; end;
function TRP_Server.Get_RP_Prop: PChar; begin if MyString <> '' then Result := PChar(MyString) else Result := PChar(''); end;
function TRP_Server.Method1(a: Integer): Integer; begin MessageDlg(Format('My Method1: %d', [a]),mtConfirmation,[mbOk],0); if MyString <> '' then Result := Length(MyString) else Result := 0; end;
procedure TRP_Server.Set_RP_Prop(Value: PChar); begin if Value <> nil then MyString := Value else MyString := ''; end;
initialization TAutoObjectFactory.Create(ComServer, TRP_Server, Class_RP_Server, ciMultiInstance, tmApartment); MessageDlg('Initializtion part',mtConfirmation,[mbOk],0); end. |
Projcet1_TLB.pas - файл, автоматически сгенерированный Delphi для классов, являющихся серверами OLE-Automation
Code: |
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 при исследовании написанных на ней программ ) и сравнив его с ассемблерным листингом, выясняется, что это легко и непринуждённо. Итак, существуют несколько довольно простых структур:
Code: |
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:
Code: |
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:
Code: |
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
Code: |
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, чтобы он поддерживал все обнаруженные структуры.
Желаю удачи...
©Drkb::04339
Взято с http://delphiworld.narod.ru