скрыть

скрыть

  Форум  

Delphi FAQ - Часто задаваемые вопросы

| Базы данных | Графика и Игры | Интернет и Сети | Компоненты и Классы | Мультимедиа |
| ОС и Железо | Программа и Интерфейс | Рабочий стол | Синтаксис | Технологии | Файловая система |



Google  
 

Исследование кода, генерируемого 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.






Copyright © 2004-2016 "Delphi Sources". Delphi World FAQ




Группа ВКонтакте   Ссылка на Twitter   Группа на Facebook