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

Сидит девочка за MAC'ом, а рядом грузин за IBM. Вдруг девочка подскакивает и говорит грузину:
- Скажи MAC.
- Ну мак.
- У меня компьютер MAC, а ты - голубой маньяк! Ла-ла-ла-ла ла-ла-ла.
Прошло еще 5 минут. Девочка говорит грузину:
- Дяденька, скажи Система 7.
- Ну Сыстэма сэм.
- У меня система семь, а ты - п#дераст совсем! Ла-ла-ла-ла ла-ла-ла.
И сказал грузин тогда девочке:
- Дэвачка, скажы АйБиЭм савмэстымый компутэр.
- Ну IBM-совместимый компьютер.
- В рот мой х@й тебя е#ал. Ла-ла-ла-ла ла-ла-ла.

Введение

На этот раз я представляю Вам сугубо теоретическое исследование, и все рассматриваемые программы написал сам. Кроме них нам понадобятся Delphi и исходный код VCL (я использовал Delphi 4.0 Client/Server Edition), а также дизассемблер IDA Pro (я пользуюсь v3.8b). Полагаю Вы понимаете Ассемблер и имеете опыт в написании программ на Delphi с применением VCL.

Delphi генерирует огромное количество мёртвого и практически одинакового кода для любого приложения, использующего VCL. Тем не менее множество приложений относительно успешно создаются на Delphi, как же бедным исследователям отделять зёрна от плевел?

Я набросал в несистематическом порядке несколько элементов управления (TEdit, TButton и TBitBtn - именно они чаще всего применяются в диалогах регистрации), и написал примерно такой непритязательный код:


type
  TForm1 = class(TForm)
    Edit1: TEdit;
    Edit2: TEdit;
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    BitBtn1: TBitBtn;
    BitBtn2: TBitBtn;
    BitBtn3: TBitBtn;
    procedure BitBtn1Click(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
    procedure MyClickHandler(Sender: TObject);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.BitBtn1Click(Sender: TObject);
begin
 MessageDlg('BitBtn1Click',mtConfirmation, [mbOk], 0);
 ModalResult := mrOk;
end;

procedure TForm1.MyClickHandler(Sender: TObject);
begin
 MessageDlg('MyClickHandler',mtConfirmation, [mbOk], 0);
 ModalResult := mrCancel;
end;

procedure TForm1.FormShow(Sender: TObject);
begin
 MessageDlg('FormShow',mtConfirmation, [mbOk], 0);
 BitBtn2.OnClick := MyClickHandler;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
 S: String;
begin
 S := Trim(Edit1.Text) + Trim(Edit2.Text);
 Application.MessageBox(PChar(S),'Button1Click',IDOk);
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
 MessageDlg('Button2Click',mtConfirmation, [mbOk], 0);
 Edit1.Enabled := not Edit1.Enabled;
 Button3.Enabled := not Button3.Enabled;
end;

Чтобы мне было легко идентифицировать мой же собственный код, я поместил в каждой функции вызов MessageDlg(). Также здесь не все обработчики назначаются во время проектирования - функция MyClickHandler() назначается обработчиком динамически при показе формы (в методе FormShow()). Компилируем, запускаем - безделица, конечно, но работает... Размер EXE-файла 329728 байт! И это буквально за пять минут! Да я - серьезный программист!

Далее неплохо было бы дизассемблировать полученный файл.

Общее замечание: строки в Delphi в бинарном виде выглядят не как во всех прочих языках - т.е. не оканчиваются нулевым символом, отчего IDA Pro не опознаёт их как строки. Вначале идёт один байт - длина, а далее - сама строка, причём её конец никак более не обозначен. Это верно для так называемых коротких строк, длина которых меньше 256 байт. К несчастью, именно такими строками пользуется механизм поддержки классов.

Надо заметить, что, несмотря на все свои достоинства, IDA Pro не справляется со всеми тонкостями программ, написанных на Delphi - утверждает, что на месте VTBL находится код, не распознаёт строк в стиле Pascal'я и прочие мелочи - так что нас выручит только её интерактивность. И, кстати, не забудьте применить файл сигнатур для VCL 4 - для моего файла IDA Pro опознала аж 2297 библиотечных функций!

Для начала посмотрим, как выглядит стартовая процедура Start() (004444A8h):


push    ebp
 mov     ebp, esp
 add     esp, 0FFFFFFF4h
 mov     eax, offset dword_0_444398
 call    @@InitExe       ; ::`intcls'::InitExe
 mov     eax, ds:off_0_445CDC
 mov     eax, [eax]
 call    @TApplication@Initialize ; TApplication::Initialize
 mov     ecx, ds:off_0_445DAC
 mov     eax, ds:off_0_445CDC
 mov     eax, [eax]
 mov     edx, ds:off_0_443F30
 call    @TApplication@CreateForm ; TApplication::CreateForm
 mov     eax, ds:off_0_445CDC
 mov     eax, [eax]
 call    @TApplication@Run ; TApplication::Run
 call    @@Halt0         ; ::`intcls'::Halt0

Самым многообещающим здесь выглядит вызов метода TApplication::CreateForm(), аргументом ему передаётся некий указатель - на структуру RTTI (Run-Time Type Information, информация о типе времени исполнения) класса нашей формы TForm1. Исследуем ее.

По смещению DWORD от начала структуры RTTI расположен указатель на VTBL. Далее идут 12 нулей (возможно выравнивание по границе, а возможно эти три DWORDа тоже что-нибудь означают). А по смещению 10h в расположен указатель (DWORD) на некую рекурсивную структуру, которую я назвал список наследственности:

смещениетипописание
0BYTEзначение не выяснено
1BYTEдлина N Pascal-строки
2Stringимя класса
N+2DWORDещё один указатель на VTBL
N+6DWORDуказатель на указатель (!) предка этого класса; обычно он указывает на 4 байта дальше себя, но я не берусь этого гарантировать
N+10WORDзначение не выяснено
N+12BYTEдлина Pascal-строки
N+13Stringимя модуля, где определяется этот класс

Путешествуя по этому списку, можно с лёгкостью выяснить генеалогическое дерево класса TForm1:

TForm, файл Forms
TCustomForm, файл Forms
TScrollingWinControl, файл Forms
TWinControl, файл Controls
TControl, файл Controls
TComponent, файл Classes
TPersistent, файл Classes
TObject, файл System

У последнего указатель на предка содержит нулевое значение - видимо, означая конец списка.

Вернёмся к структуре RTTI класса TForm1. По смещению 14h находится указатель на компоненты, которыми владеет данный класс. Это все элементы списка Components во время разработки. Эта структура имеет довольно простой вид:

смещениетипописание
0WORDчисло CompCount различных классов компонентов
2DWORDуказатель на массив указателей на структуры RTTI этих классов. Первым элементом этого массива является WORD - число его элементов, далее расположены указатели на структуры RTTI.

Сразу вслед за ней идут CompCount структур, описывающих эти компоненты:

смещениетипописание
0WORDсмещение в классе, по которому находится указатель на компонент
1WORDзначение не выяснено
2WORDиндекс в массиве структур RTTI - по нему определяется класс компонента
N+2WORDдлина Pascal-строки
N+6Stringимя компонента (например, Edit1)

Самым важным здесь являются смещение на компонент во включающем классе и его тип. Запомним их для компонентов в форме TForm1:

имя компонентаcмещение в классетип компонента
Edit102C4h0 - TEdit
Edit202C8h0 - TEdit
Button102CCh1 - TButton
Button202D0h1 - TButton
Button302D4h1 - TButton
BitBtn102D8h2 - TBitBtn
BitBtn202DCh2 - TBitBtn
BitBtn302E0h2 - TBitBtn

Снова вернёмся к структуре RTTI класса TForm1. По смещению 18h находится указатель на одну из самых полезных структур - на массив обработчиков событий (но только тех, которые заданы во время проектирования!). Первым элементом этого массива идёт WORD, определяющий длину этого массива, а его элементы имеют такие поля:

смещениетипописание
0WORDтип обработчика
2DWORDуказатель на функцию-обработчик
6BYTEдлина Pascal-строки
7Stringимя функции-обработчика

Тип определяет количество и размерность аргументов. Для обработчиков OnClick он равен 13h, для OnShow 0Fh.

Не прошло и получаса, а я уже нашёл свой код. Мы рассмотрим его чуть позже (пока Вы можете назвать найденные функции как в оригинале), а сейчас продолжим рассмотрение структуры RTTI класса. По смещению 24h записывается размер класса (DWORD) - для TForm1 он составляет 02E4h байт. Сравните его с таблицей смещений компонентов. По смещению 28h находится указатель на структуру RTTI класса-предка. У объекта TObject он равен нулю. По смещению 20h находится указатель на Pascal-строку - имя класса. Я повторю всю вышеизложенную информацию в следующей таблице:

смещениетипописание
0DWORDуказатель на VTBL
412 байтзначение не выяснено
10hDWORDуказатель на список наследований
14hDWORDуказатель на компоненты, которыми владеет данный класс
18hDWORDуказатель на массив обработчиков событий
1ChDWORDзначение не выяснено
20hDWORDуказатель на Pascal-строку - имя класса
24hDWORDразмер класса
28hDWORDуказатель на структуру RTTI класса-предка данного класса

По смещению 2Ch идёт таблица методов. Порядок следования методов в ней мне не до конца ясен, однако я уверен, что в ней должны содержаться конструктор и деструктор данного класса.

Настало время рассмотреть обнаруженные нами методы подробнее. Я рассмотрю их в том порядке, в каком их расположила Delphi в массиве обработчиков событий.

BitBtn1Click


BitBtn1Click    proc near
                push    ebx
                mov     ebx, eax
                push    0
loc_0_444149:
                mov     cx, ds:word_0_444168
                mov     dl, 3
                mov     eax, offset aBitbtn1click
                call    @MessageDlg
loc_0_44415C:
                mov     dword ptr [ebx+22Ch], 1
                pop     ebx
                retn
BitBtn1Click    endp

Простой и понятный код. Подспудно выясняется, что закрытие формы осуществляется записью DWORD'а (ModalResult) по смещению 022Ch в экземпляре классе. Обратите внимание на механизм передачи параметров - по умолчанию Delphi использует соглашение вызова register - параметры передаются слева-направо, используя регистры EAX, EDX и ECX, очистку стека производит вызываемая функция. Соответственно, первый (неявный) аргумент для этой функции, представляющий собой указатель на класс, передаётся в регистре EAX.

OnFormShow


OnFormShow      proc near
                push    ebx
                mov     ebx, eax
                push    0
                mov     cx, ds:word_0_4441F4
                mov     dl, 3
                mov     eax, offset aFormshow
                call    @MessageDlg
                mov     eax, [ebx+2DCh]
                mov     [eax+108h], ebx
                mov     dword ptr [eax+104h], offset MyClickHandler
                pop     ebx
                retn
OnFormShow      endp

Здесь тоже можно увидеть кое-что интересное. Во-первых, смещение 02DCh не напоминает Вам о компоненте BitBtn2? Во-вторых, обратите внимание, что здесь присваиваются два указателя. Почему? Потому что мы присваиваем не просто указатель на функцию. Все обработчики являются "of object" - т.е. методами классов. Соответственно, присваивается сначала указатель на экземпляр класса (в данном случае Self) по смещению 0108h, а затем - указатель на нашу функцию MyClickHandler(). Замечу, что больше указатель на эту функцию не встречается. Это сильно затрудняет поиск динамически назначенных обработчиков событий. Нам может помочь только ещё одно обстоятельство - все строковые константы, используемые в функции, Delphi располагает следом за самой функцией.

Button1Click


Button1Click    proc near
var_10          = dword ptr -10h
var_C           = dword ptr -0Ch
var_8           = dword ptr -8
var_4           = dword ptr -4
                push    ebp
                mov     ebp, esp	; фрейм стека для локальных переменных
                xor     ecx, ecx
                push    ecx
                push    ecx
                push    ecx
                push    ecx   ; 4 нуля в стек
                push    ebx
                mov     ebx, eax	; в eax - указатель на экземпляр класса
                xor     eax, eax
                push    ebp
                push    offset loc_0_4442B0
		push 	dword ptr fs:[eax]
                mov     fs:[eax], esp
...
loc_0_4442B0:
		jmp     @@HandleFinally

IDA Pro неправильно опознала аргументы функций - ведь они передаются в регистрах, а не через стек. Кроме того, здесь задействуется механизм обработки исключений. Для передачи управления при исключениях Delphi использует сегментный регистр FS - в FS:[0] помещается текущий указатель стека ESP, предыдущее же значение перед этим помещается в стек. Кроме того, в стек также помещается адрес функции - обработчика блока finally. Также обратите внимание на инициализацию четырёх локальных переменных типа DWORD нулями.


     lea     edx, [ebp+var_C]
     mov     eax, [ebx+2C8h]	; смещение 02C8h не напоминает Вам о Edit2?
     call    @TControl@GetText ; TControl::GetText
     mov     eax, [ebp+var_C]
     lea     edx, [ebp+var_8]
     call    @Trim
     mov     eax, [ebp+var_8]
     push    eax
     lea     edx, [ebp+var_C]
     mov     eax, [ebx+2C4h]	; а 02C4h - о Edit1?
     call    @TControl@GetText ; TControl::GetText
     mov     eax, [ebp+var_C]
     lea     edx, [ebp+var_10]
     call    @Trim
     mov     edx, [ebp+var_10]
     lea     eax, [ebp+var_4]
     pop     ecx
     call    @@LStrCat3      ; ::'intcls'::LStrCat3
     push    1
     mov     eax, [ebp+var_4]
     call    @@LStrToPChar   ; ::'intcls'::LStrToPChar
     mov     edx, eax
     mov     ecx, offset aButton1click
     mov     eax, ds:off_0_445CDC
     mov     eax, [eax]
     call    @TApplication@MessageBox ; TApplication::MessageBox

В общем-то, в этом коде нет ничего примечательного, но можно выяснить, что по адресу 00445CDCh находится указатель на экземпляр класса Application.


                xor     eax, eax
                pop     edx
                pop     ecx
                pop     ecx
                mov     fs:[eax], edx
                push    offset loc_0_4442B7

loc_0_444292:                           ; CODE XREF: CODE:004442B5.j
                lea     eax, [ebp+var_10]
                call    @@LStrClr       ; ::`intcls'::LStrClr
                lea     eax, [ebp+var_C]
                call    @@LStrClr       ; ::`intcls'::LStrClr
                lea     eax, [ebp+var_8]
                mov     edx, 2
                call    @@LStrArrayClr  ; ::`intcls'::LStrArrayClr
                retn
...
offset loc_0_4442B7:
                pop     ebx
                mov     esp, ebp
                pop     ebp
                retn


Рассмотрим восстановление стека подробнее. В стеке в настоящий момент содержится:

  • указатель на finally-функцию
  • EBP - прежнее значение стека
  • EBX
  • ECX = 0
  • ECX = 0
  • ECX = 0
  • ECX = 0
  • оригинальное значение EBP
  • адрес возврата из функции

Хотя перед этим в стек была помещена 1 - её нет в стеке. Почему? Потому что она является последним аргументом функции TApplication::MessageBox(). Но ведь у этой функции всего три аргумента, и они все передаются в регистрах - скажете Вы! Ничего подобного, Вы забыли, что всем методам классов передаётся неявно ещё один аргумент (под номером ноль) - указатель на экземпляр класса. При возврате же вызываемая функция сама производит очистку стека.

Итак, сначала извлекается предыдущее значение FS:[0], указатель на finally-функцию и прежнее значение стека, и восстанавливается значение FS:[0]. Дальше в стек помещается адрес процедуры очистки стека. После инструкции retn стек будет выглядеть так:

  • EBX
  • ECX = 0
  • ECX = 0
  • ECX = 0
  • ECX = 0
  • оригинальное значение EBP
  • адрес возврата из функции

Далее снимается оригинальное значение регистра EBX, стек восстанавливается в первоначальное состояние (которое хранилось всё время выполнения процедуры в регистре EBP). Стек сейчас выглядит так:

  • оригинальное значение EBP
  • адрес возврата функции

Восстанавливается предыдущее значение регистра EBP (указатель стека для вызывающей процедуры) и после инструкции retn мы возвращаемся в вызывающую функцию с полностью восстановленным стеком.

Button2Click


Button2Click    proc near
                push    ebx
                push    esi
                mov     ebx, eax ; в eax - указатель на экземпляр класса
                push    0
                mov     cx, ds:word_0_44431C
                mov     dl, 3
                mov     eax, offset aButton2click_0
                call    @MessageDlg
                mov     esi, [ebx+2C4h] ; смещение на Edit1
                mov     eax, esi
                mov     edx, [eax]
                call    dword ptr [edx+50h] ; вызов TEdit::GetEnabled
                mov     edx, eax	; результат в eax
                xor     dl, 1		; xor boolean с 1 - его же not
                mov     eax, esi
                mov     ecx, [eax]
                call    dword ptr [ecx+60h] ; вызов TEdit::SetEnabled
                mov     esi, [ebx+2D4h] ; смещение на Button3
                mov     eax, esi
                mov     edx, [eax]
                call    dword ptr [edx+50h]
                mov     edx, eax
                xor     dl, 1
                mov     eax, esi
                mov     ecx, [eax]
                call    dword ptr [ecx+60h]
                pop     esi
                pop     ebx
                retn
Button2Click    endp

Эта функция инвертирует свойство Enabled поля ввода и кнопки. Свойство Enabled определено для класса TComponent (общий предок для TEdit и TButton) так:


property Enabled: Boolean read GetEnabled write SetEnabled
  stored IsEnabledStored default True;

Доступ к этому свойству осуществляется через методы GetEnabled & SetEnabled, что мы и видим здесь - через индекс в VTBL.

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