Последние записи
- TChromium (CEF3), сохранение изображений
- Как в Delphi XE обнулить таймер?
- Изменить цвет шрифта TextBox на форме
- Ресайз PNG без потери прозрачности
- Вывод на печать графического файла
- Взаимодействие через командную строку
- Перенести программу из Delphi в Lazarus
- Определить текущую ОС
- Автоматическая смена языка (раскладки клавиатуры)
- Сравнение языков на массивах. Часть 2
Интенсив по Python: Работа с API и фреймворками 24-26 ИЮНЯ 2022. Знаете Python, но хотите расширить свои навыки?
Slurm подготовили для вас особенный продукт! Оставить заявку по ссылке - https://slurm.club/3MeqNEk
Online-курс Java с оплатой после трудоустройства. Каждый выпускник получает предложение о работе
И зарплату на 30% выше ожидаемой, подробнее на сайте академии, ссылка - ttps://clck.ru/fCrQw
21st
Фев
Компилятор домашнего приготовления. Часть 2
Posted by Chas under Журнал, Статьи
Ветер… У вас тоже на улице ветерок? Возможно, он принесет дождь, по крайней мере, разгонит эту изнурительную жару. Но пока этого не случится, людям стоит спрятаться от палящих лучей солнца в помещения с кондиционером и холодильником, в котором охлаждается квасок или живое пиво. Ну, а пока это счастье свежеет от фреонного холода, есть время продолжить разработку своего компилятора…
Виталий Белик
by Stilet www.programmersforum.ru
Краткий экскурс
В прошлый раз нам удалось поковыряться во внутренностях Win32 программы. Даже удалось написать простенький «плагиатор», генерирующий экзешник со строкой, которую мы заранее определили. Можно ли остаться на этом этапе написания ядра компилятора и перейти непосредственно к разработке своего языка? Конечно можно: DOS эмуляторы все еще живы, даже если не в виде NTVDM, который, скорее всего, скоро будет исключен из стандартного комплекта Windows, то в виде популярного DosBox, более приспособленного под работу с DOS программами. Одно только маленькое «но», которое вряд ли подсластит пилюлю работы с DOS, – под него нет удобных инструментов отладки (разве что кроме «Иды», которая IDA). Популярный Turbo Debugger вполне мог бы подойти для отладки, и, собственно, это лучшее средство отлаживать DOS программы, которое было на вооружении программистов до появления Windows.
Теперь же мы, избалованные красивым графическим интерфейсом, смотрим на него совершенно другими глазами. Я уверен, что максимум 1 из 100 пользователей Windows с уверенностью и без тени сомнения может отложить мышку в сторону и станет все делать в консоли только клавиатурой. Отвыкли мы от старого стиля работы с вычислительной техникой. Раньше ведь и мышка-то не везде поддерживалась, а сейчас некоторые ОС могут и не запуститься без нее. Мне удалось застать времена, когда в ходу были DOS 5 на процессорах 80286(386), а 40-мегабайтные жесткие диски приходилось сжимать архиватором типа Stacker. Тогда средства отладки Debug, Soft Ice, Turbo Debugger казались пределом совершенства и удобства (кто еще помнит – пусть сравнит Turbo Pascal 5 и Turbo Pascal 7, в первом мышка не работала, зато второй так жутко тормозил 286-е, что на него не сразу перешли).
Сейчас все эти инструменты ушли на почетную пенсию. И использовать их не то что невыгодно, но даже опасно: операционная система нашего времени может запротестовать, и накроется программа «медным тазиком».
Так что и нам нужно будет сделать еще один шаг вперед*, чтобы писать программы под еще популярную, но уже уступающую свои позиции платформу Win32.
Run on the run
Итак, достаточно лирики. Начнем, пожалуй. С чего начать-то? Как обычно, со стратегии и планирования. Многие исполняемые программы под Win32 хранятся в файлах, описанные PE заголовком, в котором содержится информация для загрузчика: как содержимое файла разместить в памяти, что считать кодом, что считать данными, сколько отвести памяти и прочее. В прошлой статье «Оля» (Olly Debugger) показала нам этого зверя изнутри. Теперь же наша задача – приготовить ядро нашего компилятора, способное формировать эту информацию.
То, что описано в статье, всего лишь начало. Я не хочу читать книги о компиляторах по одной простой причине: я хочу самостоятельно прийти к тому, к чему (возможно) пришли авторы тех книг, именно поэтому я сделал акцент на том, что было бы, если бы компилятор писал студент, которому совершенно неинтересны всякие умные авторы умных книг. Что получится, если иметь фантазию и уметь искать ответы на свои вопросы. Боже упаси меня конкурировать с кем-то или идти по чьей-то протоптанной дорожке.
В статье (этой и последующих) я хочу показать, что любой, даже тот, кто не захочет читать много скучной теории, сможет без проблем добиться желаемого, идя своим оригинальным путем.Я не ссылаюсь и буду стараться избегать ссылок на кого бы то ни было из ветеранов этой области. Для них есть Википедия и прочие порталы. В конце концов, литературы по этому делу много. Главное – не бояться пробовать. Подумаешь, не по правилам. Подумаешь, чего-то не хватает. Подумаешь, непривычно…
Поверив «Оле», мы разделим ядро на блоки. Каждый блок (класс) будет отвечать за свою часть формирования файла, а главный класс будет, управляя этими классами, получать скомпилированные части экзешника, которые поступят непосредственно в файл, так сказать, открытым текстом.
Пока этого нам хватит за глаза. Не будем встревать в работу с импортом-экспортом. Потом, когда пойдет работа с вводом- выводом, дополнительно разберемся с Win API функциями. Отсюда вывод: нам понадобится в общем случае 4–5 классов. Давайте оговорим их.
Вспомним, из каких же частей** состоит экзешник win32:
- MZ заголовок – 64 байта минимум;
- какой-то обработчик события, если программа не запущена под Windows (ну кому он нужен, а? предлагаю обойтись без него);
- непосредственно PE заголовок, имеющий 246 байт в общем случае;
- массив описания секций. Каждая секция размером 32 байта.
The Favels
Пойдем по порядку. Помните, «Оля» показывала нам РЕ заголовок (см. рисунок 1), вспомнили? Вот так и пойдем описывать класс, отвечающий за MZ часть заголовка:
Рис. 1. Начало РЕ заголовка
private // Класс-описатель MZ заголовка
DOS_PartPag, // Размер всего MZ заг-ка до начала PE
DOS_PageCnt,
DOS_ReloCnt,
DOS_HdrSize,
DOS_MinMem,
DOS_MaxMem,
DOS_ReloSS,
DOS_ExeSP,
DOS_ChkSum,
DOS_ExeIP,
DOS_ReloCS,
DOS_TablOff,
DOS_Overlay:word;
OffsetToPE:DWord; // Смещение на начало РЕ заголовка
public
Constructor Create;
Function Compile:String; // Компиляция
end;
Надеюсь, не забыли***, что мы пишем на Delphi? Хоть я и перечислил сюда все поля, можно было этого и не делать. Но если мы все не учтем это, то впоследствии можем сильно запутаться. Из всего этого нам важны два поля: DOS_PartPag и OffsetToPE:DWord. В принципе, это будут константы, т. е. я имею в виду, что мы забьем в эти поля значения при создании экземпляра класса.
Пока не забыл, хочу напомнить, что наш компилятор должен компилировать значения этих полей не так, как мы определим их, а в обратном порядке. Т.е. нам придется вносить их в хранилище компиляции с конца.
Функция Compile будет компилировать эти поля в поток байт. Заранее договоримся, что в качестве хранилища компиляций будут переменные типа String. Поскольку в Delphi этот тип весьма развит (с ним еще с Паскаля было очень удобно обрабатывать массивы байтов), пускай и понимаются они в виде символов. Так что в качестве потока будет String во всей «Алисе» (TAlisaMZHeader).
Поэтому приготовим пару функций, которые помогут нам переворачивать байты числа, превращая их в строку:
Function WordToStr(w: word):String;
begin
Result:= chr(lo(w))+chr(hi(w));
end;
// Функция, переворачивающая 4-х байтное значение
Function DWordToStr(w: dword):String;
var l,h: word; s: string[4];
begin
asm
mov eax,[w]
mov [l],ax
shr eax,16
mov [h],ax
end;
Result:= WordToStr(l)+WordToStr(h);
end;
Первая функция сначала записывает младший байт, а после старший, как и должно быть, а вторая помимо этого еще и разбивает на младшие и старшие части двойное слово. Теперь можем приступить к реализации:
var i:integer;
begin
DOS_PartPag :=$80;
DOS_PageCnt :=$1;
DOS_ReloCnt :=$0;
DOS_HdrSize :=$4;
DOS_MinMem :=$10;
DOS_MaxMem :=$FFFF;
DOS_ReloSS :=$0;
DOS_ExeSP :=$140;
DOS_ChkSum :=$0;
DOS_ExeIP :=$0;
DOS_ReloCS :=$0;
DOS_TablOff :=$40;
DOS_Overlay :=$0;
OffsetToPE :=$40;
end;
ЭЭто наш конструктор. Здесь мы, как и договаривались, заведем константно значения в поля. Опять-таки, не обязательно было так досконально их описывать, но, дабы не запутаться, это стоило сделать. Следующим будет реализация метода компиляции заголовка:
var i:integer;
begin
Result :=‘MZ’;
Result :=Result+WordToStr(DOS_PartPag);
Result :=Result+WordToStr(DOS_PageCnt);
Result :=Result+WordToStr(DOS_ReloCnt);
Result :=Result+WordToStr(DOS_HdrSize);
Result :=Result+WordToStr(DOS_MinMem);
Result :=Result+WordToStr(DOS_MaxMem);
Result :=Result+WordToStr(DOS_ReloSS);
Result :=Result+WordToStr(DOS_ExeSP);
Result :=Result+WordToStr(DOS_ChkSum);
Result :=Result+WordToStr(DOS_ExeIP);
Result :=Result+WordToStr(DOS_ReloCS);
Result :=Result+WordToStr(DOS_TablOff);
Result :=Result+WordToStr(DOS_Overlay);
for i:=1 to 32 do Result :=Result+#0;
Result :=Result+WordToStr(OffsetToPE);
for i:=length(Result) to OffsetToPE–1 do Result:=Result+#0;
end;
Тут, я думаю, тоже все понятно: каждое поле по порядку мы приводим к строке и выкатываем как поток байт в результат метода. Здесь непонятным моментом могут быть разве что циклы.
for i:=1 to 32 do Result:=Result+#0; дополняет MZ заголовок 32 байтами до поля описания смещения на РЕ, а for i:=length(Result) to OffsetToPE–1 do Result:=Result+#0; призван заполнить пустоту в том месте, где у порядочных компиляторов должен быть обработчик события запуска под другую операционку. Мы договорились, что его не будет, так что в принципе второй цикл можно было не писать, но он не помешает. Соответственно обратите внимание: я определил OffsetToPE так, чтобы смещение заголовка РЕ было непосредственно под MZ.
Так, с MZ разобрались. Перейдем к следующему – PE заголовку. Здесь ситуация чуть сложнее. Микрософт постарался и понавыдумывал кучу параметров, в которых легко запутаться, особенно при расчете их значений, так что описание класса будет побольше:
private
FHeader: String;
//******************************************
Machine:word; // Семейство процессоров
NumberOfSections:word; // Кол-во секций, считается
TimeDateStamp:dword; // Время компиляции.
PointerToSymbolTable:dword;
NumberOfSymbols:dword;
SizeOfOptionalHeader:word; // Размер опци-го заголовка
Characteristics:word; // Тип загружаемого файла
MagicNumber:word;
MajorLinkerVersion:byte;
MinorLinkerVersion:byte;
SizeOfCode:dword;
SizeOfInitializedData:dword;
SizeOfUninitializedData:dword;
AddressOfEntryPoint:dword; // Адрес точки начала кода
BaseOfCode:dword;
BaseOfData:dword;
ImageBase:dword; // Базовый адрес
SectionAlignment:dword; // Размер секций в памяти
FileAlignment:dword; // Размер секций в файле
MajorOSVersion:word;
MinorOSVersion:word;
MajorImageVersion:word;
MinorImageVersion:word;
MajorSubsystemVersion:word;
MinorSubsystemVersion:word;
Reserved1:dword;
{ Размер всего образа, размещаемого в памяти
Получается, что это размер=PE+кол-во секций*Размер
секций в памяти, учитывая, что PE тоже выравнивается до
размера секций в памяти. Он считается такой же
равноправной секцией, но служит для описания всех
остальных секций, такой себе Interface
}
SizeOfImage:dword;
SizeOfHeaders:dword; // Общий размер всех заголовков
{ Контрольная сумма. Вообще ходят неоднозначные слухи о
ее надобности. Некоторые HEX редакторы в ней нуждаются,
но для запуска скомпилированной программы ее никто не
проверяет. Мы не будем ее рассчитывать, но в принципе
на будущее необходимо знать, что это за поле
}
CheckSum:dword;
Subsystem:word; // Тип приложения,
// консолька, оконка или ДЛЛ
DLLCharacteristics:word;
SizeOfStackReserve:dword; // память, требуемая для стека
// объем памяти, отводимой в
// стеке немедленно после
// загрузки
SizeOfStackCommit:dword;
SizeOfHeapReserve:dword; // макс.возможный размер кучи
SizeOfHeapCommit:dword;
LoaderFlags:dword;
NumberOfRvaAndSizes:dword;
Export_Table_address:dword;
Export_Table_size:dword;
Import_Table_address,
Import_Table_size,
Resource_Table_address,
Resource_Table_size,
Exception_Table_address,
Exception_Table_size,
Certificate_File_pointer,
Certificate_Table_size,
Relocation_Table_address,
Relocation_Table_size,
Debug_Data_address,
Debug_Data_size,
Architecture_Data_address,
Architecture_Data_size,
Global_Ptr_address,
Must_be_0,
TLS_Table_address,
TLS_Table_size,
Load_Config_Table_address,
Load_Config_Table_size,
Bound_Import_Table_address,
Bound_Import_Table_size,
Import_Address_Table_address,
Import_Address_Table_size,
Delay_Import_Descriptor_address,
Delay_Import_Descriptor_size,
COM_Runtime_Header_address,
Import_Address_Table_size2,
Reserved2,
Reserved3:d word;
//******************************************
public
Function Header:String;
Constructor Create;
end;
Из всей этой «лесопосадки» нам понадобятся только несколько полей (я к ним в листинге 5 комментарии приписал). Все остальные, поверив «Оле», проинициализируем константными значениями.
Здесь же в классе присутствует функция Header, которая сформирует выходную строку, содержащую скомпилированный РЕ заголовок. Посмотрим на код, в котором будет описана инициализация этих полей:
begin
//******************************************
Machine :=$014C; // IMAGE_FILE_MACHINE_I386
//NumberOfSections :=$2;
TimeDateStamp :=$0;
PointerToSymbolTable :=$0;
NumberOfSymbols :=$0;
SizeOfOptionalHeader :=$E0;
Characteristics :=$010E;
MagicNumber :=$010B;
MajorLinkerVersion :=$0;
MinorLinkerVersion :=$0;
SizeOfCode :=$0;
SizeOfInitializedData :=$0;
SizeOfUninitializedData :=$0;
//AddressOfEntryPoint :=$0;
BaseOfCode :=$0;
BaseOfData :=$0;
ImageBase :=$400000;
//SectionAlignment :=$0;
//FileAlignment :=$200
MajorOSVersion :=$1;
MinorOSVersion :=$0;
MajorImageVersion :=$0;
MinorImageVersion :=$0;
MajorSubsystemVersion :=$3;
MinorSubsystemVersion :=$A;
Reserved1:=0;
//SizeOfImage :=$3000;
SizeOfHeaders :=$200;
//CheckSum :=$ 7FAB
Subsystem :=$3;// IMAGE_SUBSYSTEM_WINDOWS_CUI
DLLCharacteristics :=$0;
SizeOfStackReserve :=$1000;
SizeOfStackCommit :=$1000;
SizeOfHeapReserve :=$10000;
SizeOfHeapCommit :=$0;
LoaderFlags :=$0;
NumberOfRvaAndSizes :=$10;
Export_Table_address:=0;
Export_Table_size:=0;
Import_Table_address:=0;
Import_Table_size:=0;
Resource_Table_address:=0;
Resource_Table_size:=0;
Exception_Table_address:=0;
Exception_Table_size:=0;
Certificate_File_pointer:=0;
Certificate_Table_size:=0;
Relocation_Table_address:=0;
Relocation_Table_size:=0;
Debug_Data_address:=0;
Debug_Data_size:=0;
Architecture_Data_address:=0;
Architecture_Data_size:=0;
Global_Ptr_address:=0;
Must_be_0:=0;
TLS_Table_address:=0;
TLS_Table_size:=0;
Load_Config_Table_address:=0;
Load_Config_Table_size:=0;
Bound_Import_Table_address:=0;
Bound_Import_Table_size:=0;
Import_Address_Table_address:=0;
Import_Address_Table_size:=0;
Delay_Import_Descriptor_address:=0;
Delay_Import_Descriptor_size:=0;
COM_Runtime_Header_address:=0;
Import_Address_Table_size:=0;
Reserved2:=0;
Reserved3:=0;
//******************************************
end;
Обратите внимание, я закомментировал те поля, значения которых придется рассчитывать. И не забудем описать реализацию компиляции этого класса в выходную строку:
begin
Result :=‘PE’#0#0;
Result :=Result+ WordToStr(Machine);
Result :=Result+ WordToStr(NumberOfSections);
Result :=Result+ DWordToStr(TimeDateStamp);
Result :=Result+ DWordToStr(PointerToSymbolTable);
Result :=Result+ DWordToStr(NumberOfSymbols);
Result :=Result+ WordToStr(SizeOfOptionalHeader);
Result :=Result+ WordToStr(Characteristics);
Result :=Result+ WordToStr(MagicNumber);
Result :=Result+ chr(MajorLinkerVersion);
Result :=Result+ chr(MinorLinkerVersion);
Result :=Result+ DWordToStr(SizeOfCode);
Result :=Result+ DWordToStr(SizeOfInitializedData);
Result :=Result+ DWordToStr(SizeOfUninitializedData);
Result :=Result+ DWordToStr(AddressOfEntryPoint);
Result :=Result+ DWordToStr(BaseOfCode);
Result :=Result+ DWordToStr(BaseOfData);
Result :=Result+ DWordToStr(ImageBase);
Result :=Result+ DWordToStr(SectionAlignment);
Result :=Result+ DWordToStr(FileAlignment);
Result :=Result+ WordToStr(MajorOSVersion);
Result :=Result+ WordToStr(MinorOSVersion);
Result :=Result+ WordToStr(MajorImageVersion);
Result :=Result+ WordToStr(MinorImageVersion);
Result :=Result+ WordToStr(MajorSubsystemVersion);
Result :=Result+ WordToStr(MinorSubsystemVersion);
Result :=Result+ DWordToStr(Reserved1);
Result :=Result+ DWordToStr(SizeOfImage);
Result :=Result+ DWordToStr(SizeOfHeaders);
Result :=Result+ DWordToStr(CheckSum);
Result :=Result+ WordToStr(Subsystem);
Result :=Result+ WordToStr(DLLCharacteristics);
Result :=Result+ DWordToStr(SizeOfStackReserve);
Result :=Result+ DWordToStr(SizeOfStackCommit);
Result :=Result+ DWordToStr(SizeOfHeapReserve);
Result :=Result+ DWordToStr(SizeOfHeapCommit);
Result :=Result+ DWordToStr(LoaderFlags);
Result :=Result+ DWordToStr(NumberOfRvaAndSizes);
Result :=Result+ DWordToStr(Export_Table_address);
Result :=Result+ DWordToStr(Export_Table_size);
Result :=Result+ DWordToStr(Import_Table_address);
Result :=Result+ DWordToStr(Import_Table_size);
Result :=Result+ DWordToStr(Resource_Table_address);
Result :=Result+ DWordToStr(Resource_Table_size);
Result :=Result+ DWordToStr(Exception_Table_address);
Result :=Result+ DWordToStr(Exception_Table_size);
Result :=Result+ DWordToStr(Certificate_File_pointer);
Result :=Result+ DWordToStr(Certificate_Table_size);
Result :=Result+ DWordToStr(Relocation_Table_address);
Result :=Result+ DWordToStr(Relocation_Table_size);
Result :=Result+ DWordToStr(Debug_Data_address);
Result :=Result+ DWordToStr(Debug_Data_size);
Result :=Result+ DWordToStr(Architecture_Data_address);
Result :=Result+ DWordToStr(Architecture_Data_size);
Result :=Result+ DWordToStr(Global_Ptr_address);
Result :=Result+ DWordToStr(Must_be_0);
Result :=Result+ DWordToStr(TLS_Table_address);
Result :=Result+ DWordToStr(TLS_Table_size);
Result :=Result+ DWordToStr(Load_Config_Table_address);
Result :=Result+ DWordToStr(Load_Config_Table_size);
Result :=Result+ DWordToStr(Bound_Import_Table_address);
Result :=Result+ DWordToStr(Bound_Import_Table_size);
Result :=Result+ DWordToStr(Import_Address_Table_address);
Result :=Result+ DWordToStr(Import_Address_Table_size);
Result :=Result+ DWordToStr(Delay_Import_Descriptor_address);
Result :=Result+ DWordToStr(Delay_Import_Descriptor_size);
Result :=Result+ DWordToStr(COM_Runtime_Header_address);
Result :=Result+ DWordToStr(Import_Address_Table_size2);
Result :=Result+ DWordToStr(Reserved2);
Result :=Result+ DWordToStr(Reserved3);
end;
Здоровый код, не так ли? Мне лично неприятно иметь дело с такой щепетильностью. Будь программизм Микрософта продуманнее, они могли бы все эти поля сделать одного размера, тогда и нам было бы попроще: работа с массивом однородных элементов все-таки не требует большой писанины кода. Ну, да ладно. Скажу по секрету: это описание будет единственным огромным, далее рутинного кода будет не так много.
Еще нам понадобится класс, описывающий секции и управляющий ими. То есть нам нужно сосредоточить в нем не только описание секций, но и саму ее реализацию. Описание будет в виде списка полей плюс поле содержимого секции, которое будет представлено в виде строки:
private
Name:string[8]; // (0h) Имя секции
{ (8h) Размер выделяемой под секцию памяти. Фактически это
размер данных, которые компилируются. Если речь идет о
коде, то это размер чистого кода в байтах без выравнивания
до размера секции
}
VirtualSize, // (0Ch) Адреc, с которого начнется
// секция относительно базы
VirtualAddress,
SizeOfRawData, // (10h) Размер секции в файле.
// (14h) Смешение в файле, с которого
// начинается секция
PointerToRawData,
PointerToRelocations,
PointerToLineNumbers:dword;
NumberOfRelocations,
NumberOfLineNumbers:word;
Characteristics:dword; // Доступы секции
public
Data:String; // Это поле для чистых данных или кода
// Функция будет возвращать
// скомпилированное описание заголовка
// секции
Function Header:String;
end;
Здесь, как видим, все попроще. Тоже описываются все поля, чтобы не запутаться, но только некоторые для нас будут важны – те, которым сопутствуют комментарии. Напомню их:
- Поле имени. Оно важно только для программиста, чтобы удобно было различать и группировать содержимое программы. Фактически нам понадобится только две-три секции: главная секция кода (для точности она будет идти всегда первой) и одна секция данных. Вполне возможно, что потом понадобится выделить еще одну секцию для описания функций, но время покажет: если функции небольшие, их вполне можно будет оставить вместе с главным кодом, не выделяя лишней секции (вообще можно натолкать все в одну, но выглядеть это будет некрасиво: неудобно для анализирования).
- VirtualSize – размер выделяемой под секцию памяти. Получается, что это размер чистого кода, столько байт, сколько накомпилировано в опкодах или (если речь идет о секции данных) столько инициализированных байт, сколько мы впихнем. Если, предположим, программа будет состоять из двух операторов Mov Eax, некое значение, то это поле будет содержать число десять. Потому что размер этой операции – 5 байт (учитывая, что оперируем переменной DWord) * 2, ибо их два. А поскольку у нас все содержимое секции будет храниться в виде строки, высчитать это поле труда не составит – функцию Length в Delphi еще никто не отменял.
- VirtualAddress. Адрес, с которого будет начинаться секция. Первая наша секция будет начинаться с 1000h – это будет секция кода, следующие секции будут соответственно 2000h, 3000h и т. д. Это не займет много места: как показывает практика, для двух секций (код и данные) хватит 2 кбайт.
Так вот давайте посмотрим на свойства, которые нам покажет Windows на рисунке 1. Ничего не смущает? Откуда там аж 4 кбайта, если мы заказывали всего две секции+заголовок? Это что за особенности NTFS? Кстати, нужно взять на заметку, что секцию можно описать таким образом, что ее реализация отсутствует в файле. Это так называемая секция неинициализированных данных. Вот смотрите, на рисунке 3 наглядно показано в шаблоне для MASM из пакета MASM Builder. Видите секцию «.data?»? Тело этой секции в файл не компилируется, но ее описание в РЕ заголовке присутствует, говоря загрузчику: «Эй, приятель, зарезервируй мне в памяти секцию. Не ищи ее в файле, она нужна будет только для работы и не содержит начальных значений». Загрузчик послушно кивает и резервирует в памяти местечко для пикничка на обочине, не выискивая в файле, чем бы эту полянку наполнить. И даже в этом случае компилятор инициализирует VirtualSize для неинициализированных секций. Ну, компилятору Ассемблера хорошо, он заранее знает, какого размера будут переменные, даже не инициализированные. Мы условимся следующим образом: пусть наш компилятор сам определяет, инициализированная секция или нет. Если в ней все ячейки будут пустыми, то резервировать в файле для нее место не надо. Поскольку реализация секции также будет представлять строку, проверка будет проста: если строка пуста, то и формировать тело секции в файл не стоит, но раз уж мы ее заказали – загрузчик должен зарезервировать место в памяти. Потом, экспериментируя, можно даже будет вообще обойтись одной-единственной секцией – кода, куда можно впихнуть и данные, но тогда придется менять поле доступа (characteristics) для этой секции.
Кстати, что интересно. Я до сегодняшнего дня не сталкивался с программами менее мегабайта, редко писал на Ассемблере и еще реже обращал внимание на файлы, которые потом компилировались. Теперь же, когда начал вникать в то, как работают компиляторы, обнаружил интересный и незнакомый мне эффект. Помните, в предыдущей статье мы компилировали в Ассемблере программу-пустышку, чтобы принести ее в жертву во имя Ее Величества Науки?
Рис. 2. Свойства файла-пустышки
- SizeOfRawData. Поле, говорящее, сколько байт в файле займет секция (при неинициализированной секции это значение равно 0).
- PointerToRawData. Поле, указывающее смещение, по которому находится начало секции. Фактически оно считается по формуле fileAlignment*(Номер секции+1). +1, потому что РЕ заголовок мы тоже считаем секцией. Для неинициализированной секции имеет значение 0.
- Characteristics. Это поле доступов. Указывает, что можно делать с этой секцией: только читать, писать и читать – или только выполнять. Значения мы подсмотрим в «Оле» и установим их стандартно. Пока что нам хватит только двух значений: чтения и выполнения (для секции кода – $60000020) и чтения и записи (для секции данных – $C0000040).
- Здесь же опишем поле _Data, которое будет хранить собственно тело секции. Для секции кода здесь будут опкоды команд, для секций данных – данные. Компилировать описание секции будет функция «Header».
Ничего не забыли? Вроде нет. В таком случае перейдем к реализации методов этого класса. А собственно, у нас только одна функция, ее и реализуем:
begin
Result:=»;
// Если тело ее пусто, будем считать, что это
// неинициализированная секция, и она не нуждается в
// компиляции в файл.
if _Data=» then begin
SizeOfRawData:=0;
PointerToRawData:=0;
VirtualSize:=2;
end;
VirtualSize:=length(_Data);
if VirtualSize=0 then VirtualSize:=1;
// В остальном действия такие же – переворачиваем значение и
// конвертируем в строку
Result :=Result+ Name;
Result :=Result+ DWordToStr(VirtualSize);
Result :=Result+ DWordToStr(VirtualAddress);
Result :=Result+ DWordToStr(SizeOfRawData);
Result :=Result+ DWordToStr(PointerToRawData);
Result :=Result+ DWordToStr(PointerToRelocations);
Result :=Result+ DWordToStr(PointerToLineNumbers);
Result :=Result+ WordToStr(NumberOfRelocations);
Result :=Result+ WordToStr(NumberOfLineNumbers);
Result :=Result+ DWordToStr(Characteristics);
end;
Рис. 3. Пример секции неинициализированных данных
Видите, все просто. Банально поле за полем выдаем в строку поток компилированных данных по порядку, не забывая переворачивать положение байт в поле.
Master of puppets
У нас есть заготовленные детали механизма. Простые шестеренки, неплохо бы собрать из них редуктор. Именно этим и займемся – напишем главный класс, который будет компилировать, используя вышеописанные классы-шестеренки и формировать выходной файл. Определимся, что туда должно входить:
- Поля классов для РЕ, MZ и массив секций.
- Функции компиляции описаний всех этих секций и тел секций.
- Метод создания секций.
- Функции расчета размера скомпилированного заголовка, расчета выравнивания секции в файле. Хоть мы и не будем писать огромные программы, но неплохо бы подумать на перспективу о расчете размера выравниваний согласно содержимому секции. А вдруг данных в секции больше, чем 512 байт (это минимальное значение для инициализированной секции или РЕ заголовка)? Или количество секций, например, пять, а их описание раздует заголовок в размер поболее 512 (372+40*5 = 572. Здесь 372 – минимальный размер РЕ заголовка, 40 – размер описания секций, 5 – их количество)? Значит, придется резервировать для секций уже не 200h (512 в десятиричной).
Определились? Приступим. Смотрим описание класса:
Private
// Наши компиляторы заголовков
PE:TAlisaPEHeader;
MZ:TAlisaMZHeader;
Sections:TObjectList;
// Это переменная-коллектор скомпилированного файла
_Data:String;
// Функции, компилирующие описания заголовков, и их реализации
Function CompilePE:string;
Function CompileSectionsHeader(i:integer):string;
// Вспомогательные функции
// Функция вычисления, сколько нужно для стандартного РЕ
// заголовка без обработчика запуска программы не из-под
// Windows
Function LengthHeader:Integer;
// Функция расчета выравнивания в файле согласно максимальному
// размеру тел секций
Function CalcFileAlignment:Integer;
// Функция, проставляющая для каждой секции рассчитанное
// выравнивание секций в файле
procedure FixAlignSectionInFile;
public
// По умолчанию мы обязательно будем иметь как минимум одну
// секцию – секцию кода. Она же будет в списке первой и
// начинаться будет по смещению 1000h
_Code:TAlisaSection;
// Функция, создающая новую секцию.
Function NewSection(AName:string;Type_:Dword):TAlisaSection;
// будет поступать в файл как есть.
Function Compile:String;
Constructor Create;
Destructor Free;
end;
Начнем по порядку реализовывать каждое звено цепочки. Первым пойдет конструктор. Здесь мы проинициализируем основные поля заголовка и создадим секцию кода:
begin
MZ:=TAlisaMZHeader.Create;
pe:=TAlisaPEHeader.Create;
//******************************************
pe.AddressOfEntryPoint:=$1000;
pe.ImageBase:=$400000;
pe.SectionAlignment:=$1000;
{ TODO –oUser –cConsole Main : С этим параметром аккуратнее }
pe.FileAlignment:=$200;
pe.SizeOfHeaders:=pe.FileAlignment;
pe.Subsystem:=3;
//******************************************
Sections:=TObjectList.Create;
_Code:=NewSection(‘Код’,codeSec);
end;
Это стандартные настройки для основных параметров нашего компилятора, позаимствованные из компиляции FASM. Их описания я давал выше. Конструкторы MZ и РЕ мы уже описали и реализовали выше, поэтому перейдем к реализации метода создания секции:
begin
// создадим секцию и внесем ее в список секций
Result:=TAlisaSection.Create;
Sections.Add(Result);
// Назовем ее и обязательно дополним до 8 байт
Result.Name:=AName;
while length(Result.Name)<8 do Result.Name:=Result.Name+#0;
// Определим параметры секции. РЕ заголовок хоть и
// считается секцией, но в список их не заносится. Здесь
// находятся только настоящие секции
Result.Characteristics:=Type_;
Result.VirtualAddress:=PE.SectionAlignment*Sections.Count;
Result.VirtualSize:=1;
Result.SizeOfRawData:=PE.FileAlignment;
end;
Теперь посмотрим***** реализацию метода компиляции, чтобы располагать последовательностью вызовов методов:
var i,csp:integer; HeaderSum,CheckSum:dword;s,e:string;
begin
// Посчитаем количество секций
PE.NumberOfSections:=Sections.Count;
// и выясним общий размер заголовка с описаниями секций
pe.SizeOfImage:=(PE.NumberOfSections+1)*pe.SectionAlignment;
// Проведем расчет выравнивания секций в файле
pe.FileAlignment:=CalcFileAlignment;
// Поправим на основе расчета поля секций
FixAlignSectionInFile;
// Скомпилируем заголовки
_Data:=CompilePE;
// Теперь присоединим тела секций
for i:=0 to Sections.Count–1 do
_Data:=_Data+CompileSection(i);
// Теперь нужно посчитать контрольную сумму
//CheckSumMappedFile(@s[1],length(s),@HeaderSum,@CheckSum);
Result:=_Data;
end;
В листинге 4 приведена функция расчета длины заголовка в байтах. Помните, мы условились, что не будем использовать обработчик события запуска под не Windows систему? Поэтому можем смело говорить, что OffsetToPE является крайней точкой MZ заголовка, за которым, не теряя ни одного байта, начнется РЕ заголовок. Кто-то скажет: «А зачем нужно было избавляться от этого обработчика, если все равно выравнять до FileAlignment придется? В чем тут потери?» Верно, можно было и не отказываться от него, но, во-первых, лень писать еще и код этого обработчика, а во-вторых, при анализе так быстрее можно найти части заголовка. Давайте сравним вид «Оли» с обработчиком и без него, начиная с точки смещения на РЕ часть. Видите? На рисунках 4 и 5 разница очевидна. На 2-м мы сразу добрались до РЕ, а на 1-м его еще не видать, хотя на размер РЕ экзешника это не повлияло. Ладно, идем далее. Рассмотрим расчет выравнивания тел секций в файле:
begin
// Это длина MZ+PE без описания секций
Result:=MZ.OffsetToPE+248;
//Это общая чистая длина PE с секциями
Result:=Result+Sections.Count*40;
end;
Рис. 4. Вид с мусором обработчика
Рис. 5. Вид без мусора обработчика
var ls,i,l: integer;
begin
// Получаем размер РЕ
l:= LengthHeader;
// Если этот размер не делится нацело на 512, значит, его
// лучше выравнять считая, что появилась еще какая-то секция
if (l mod $200)<>0 then l:=(l div $200+1)*$200;
// И соответственно точно так же проанализировать тела
// секций. Вдруг какая-то из них больше минималки, тогда
// равнять будем по ней
for i:=0 to Sections.count–1 do begin
ls:=length(TAlisaSection(Sections[i])._Data);
if ls>l then
if (ls mod $200)<>0 then l:=(ls div $200+1)*$200;
end;
Result:= l;
end;
И проставление полей в секциях согласно расчету:
var i:integer;
begin
for i:=0 to Sections.Count–1 do
if TAlisaSection(Sections[i]).SizeOfRawData<>0 then
TAlisaSection(Sections[i]).PointerToRawData:=pe.FileAlignment*(i+1)
else
TAlisaSection(Sections[i]).PointerToRawData:=0;
end;
Следующий метод призван компилировать РЕ заголовок (все поля должны быть рассчитаны корректно до вызова этой функции):
var i:integer;
begin
// Компилируем РЕ заголовок
Result:= MZ.Compile+PE.Header;
// Компилируем описания секций
for i:=0 to Sections.Count–1 do begin
Result:=Result+TAlisaSection3(Sections[i]).Header;
end;
// выравниваем скомпилированное по минимальному размеру
// тела секции в файле.
while length(result)<pe.FileAlignment do Result:=Result+#0;
end;
Здесь всплыла функция Header (метод класса TAlisaPEHeader), описанный выше. Идем далее. Компиляция тел секций:
begin
Result:=»;
if (i>=0)and(i<Sections.Count) then
with TAlisaSection(Sections[i]) do begin
Result:=_Data;
if Result<>» then
while length(result)<pe.FileAlignment do
Result:=Result+#0;
end;
end;
Здесь тела секций выравниваются до FileAlignment, дополняясь нулями. Вот оно – ядро компилятора, формирующее интерьер файла- экзешника. Далее содержимое поля _Data пойдет непосредственно в файл. А что – попробуем? Поместим эти классы в отдельный модуль, назовем <Alisa.pas>. На всякий случай напомню, что описание классов вешается в раздел interface, а реализация – в раздел implementation. Также в модуле понадобятся использование модулей types и contnrs. Я не описал деструктор TAlisaCompiler.Free, но он не так важен. В нем просто нужно освобождать задействованные в классе объекты – это вы можете написать сами.
Создадим также консольный проект, где напишем собственно программу, вызывающую компилятор и передающую ему опкоды команд. Пока что придется заглядывать в книгу и help по Ассемблеру, чтобы определить, какие опкоды нам нужны. Допустим, задача «поместить значение переменной в регистр» выглядит в реализации так (см. рисунок 6). Видите опкод? А100204000 в 16-тиричном виде. Вот так мы его и опишем в строку (обратите внимание, «Оля» показывает нам, как нужно правильно написать опкод в уже перевернутом виде: сначала младшие, потом старшие байты), не забыв про retn (C3h) в листинге 19.
Рис. 6. Вид реализации задания
Кстати, кто-то спросит: «Откуда мы знаем адрес переменной? Не будем же мы заставлять в коде указывать адрес?» Нет, конечно, но проверить нам наше творчество как-то надо, а мы помним, что у нас секция данных идет после кода и размер секций в памяти 1000h, плюс база 400000h, вот и получается, что 400000h+размер секции кода дадут 402000h. А поскольку у нас только одна переменная (для проверки хватит), она лежит в самом начале секции данных – вот вам и адрес (см. листинг 19).
{$APPTYPE CONSOLE}
uses
SysUtils,
Unit1 in ‘Unit1.pas’;
var a: TAlisaCompiler;
d: TAlisaSection;
f: file of byte;s:string;
begin
// Зарядим компилятор
a:=TAlisaCompiler.Create;
// Создадим секцию данных
d:=a.NewSection(‘Данные’,dataSec);
// Положим в секцию данных число 5
d._Data:=#5;
// положим в секцию кода опкод команды, помещающей в регистр
// EAX число из секции данных и выход из программы
a._Code._Data:=#$A1#$00#$20#$40#$00#$C3;
// Скомпилируем
s:=a.Compile;
{ TODO –oUser –cConsole Main : Insert code here }
// Сохраним в файл
AssignFile(f,‘file.exe’); rewrite(f);
BlockWrite(f,s[1],length(s));
CloseFile(f);
a:= nil;
end.
Рис. 7. Результат компиляции
Набрали? Сохранили проект? Жмем <F9>. И пытаемся открыть полученный file.exe в отладчике или дизассемблере («Оля» рулит, так что покажу в ней).
Посмотрим, что получилось (см. выше рисунок 7). Нажмите <F8>. «Оля» выполнит команду, поместив в EAX число из памяти.
Close to seven
О! А вот и дождик пошел. Посвежело, исчезла изнуряющая жара. Это хорошо. И у нас все получилось удачно. Согласитесь, приятно собственноручно создавать что-то. Чувствуешь себя на порядок выше. Жаль, что многие начинающие хакеры этого не понимают. Хоть и хорошо знают систему, но кидают все силы на то, чтобы ее поломать, но «ломать не строить». Есть, конечно, люди, взламывающие не ради развлечения, а чтобы выяснить слабые места, но таких гораздо меньше. Теперь можно подумать о том, как дальше все эти наработки использовать. И воплотить все свои замыслы в код. Но это уже совсем другая история…
Все упомянутые в статье исходные коды и тестовый проект приведены в виде ресурсов в теме «Журнал клуба программистов. Седьмой выпуск» или непосредственно в архиве с журналом.
Статья из седьмого выпуска журнала «ПРОграммист».
Обсудить на форуме — Компилятор домашнего приготовления. Часть 2
Похожие статьи
Купить рекламу на сайте за 1000 руб
пишите сюда - alarforum@yandex.ru
Да и по любым другим вопросам пишите на почту
пеллетные котлы
Пеллетный котел Emtas
Наши форумы по программированию:
- Форум Web программирование (веб)
- Delphi форумы
- Форумы C (Си)
- Форум .NET Frameworks (точка нет фреймворки)
- Форум Java (джава)
- Форум низкоуровневое программирование
- Форум VBA (вба)
- Форум OpenGL
- Форум DirectX
- Форум CAD проектирование
- Форум по операционным системам
- Форум Software (Софт)
- Форум Hardware (Компьютерное железо)