Рабочим названием платформы .NET было |
Структура программных компонентов
Пример генерации PE-файла
В приложении A приведен исходный код программы pegen, демонстрирующей генерацию PE-файла. Эта программа создает сборку hello.exe, работа которой заключается в дублировании строки, введенной пользователем с клавиатуры. Несмотря на то, что генерируемая сборка столь примитивна, программа pegen может служить основой для разработки реального генератора исполняемых файлов .NET.
Программа pegen написана на языке C и состоит из двух частей:
- модуль генерации PE-файла, оформленный как отдельная библиотека;
- главный модуль, использующий модуль генерации для создания простейшей сборки .NET.
В модуле генерации определена функция make_file, которая принимает блок входных параметров и дескриптор выходного файла:
void make_file (FILE* file, PINPUT_PARAMETERS inP) { make_headers (file, inP); // 1 этап make_text_section (file, inP); // 2 этап make_cli_section (file, inP); // 3 этап make_reloc_section (file, inP); // 4 этап };
Как видно из приведенного листинга, эта функция вызывает еще четыре функции, поскольку процесс генерации PE файла разбит на четыре этапа.
Блок входных параметров описывается структурой INPUT_PARAMETERS:
unsigned long Type;
Тип исполняемого файла: exe или dll. Поле может принимать значения:
EXE_TYPE - выходной файл-exe;
DLL_TYPE - выходной файл-dll.
unsigned char* metadata;
Это поле содержит указатель на область памяти, где находятся метаданные в бинарном виде.
unsigned char* cilcode;
Указатель на область памяти, где лежит CIL-код методов в бинарном виде.
unsigned long SizeOfMetadata;
Размер метаданных.
unsigned long SizeOfCilCode;
Размер CIL-кода методов.
unsigned long ImageBase;
Базовый адрес загрузки.
unsigned long FileAlignment;
Выравнивание секций в файле.
unsigned long EntryPointToken;
Точка входа в сборку (токен метаданных, соответствующий некоторому статическому методу).
unsigned short Subsystem;
Тип подсистемы Console или Windows GUI. Поле может принимать значения:
IMAGE_SUBSYSTEM_WINDOWS_GUI - подсистема Windows GUI;
IMAGE_SUBSYSTEM_WINDOWS_CUI - подсистема Windows CUI.
Этих входных данных достаточно для генерации сборки .NET.
Подробно рассмотрим каждый этап выполнения программы.
Этап 1. Заполнение заголовка PE-файла
Первый этап включает заполнение структуры HEADERS. Всю работу на этом этапе выполняет функция make_headers, принимающая блок входных параметров и файловый дескриптор. Прототип функции:
void make_headers (FILE* file, PINPUT_PARAMETERS inP);
Структура HEADERS включает в себя заголовок MS-DOS, сигнатуру PE, заголовок PE, дополнительный заголовок PE, директории данных и заголовки секций. Формат структур IMAGE_DATA_DIRECTORY и IMAGE_SECTION_HEADER, которые входят в структуру HEADERS, можно найти дальше:
struct HEADERS { char ms_dos_header[128]; // заголовок MS-DOS unsigned long signature; // сигнатура PE struct _IMAGE_FILE_HEADER { // заголовок PE unsigned short Machine; unsigned short NumberOfSections; unsigned long TimeDateStamp; unsigned long PointerToSymbolTable; unsigned long NumberOfSymbols; unsigned short OptionalHeaderSize; unsigned short Characteristics; }PeHdr; struct _IMAGE_OPTIONAL_HEADER { //Дополнительный заголовок PE unsigned short Magic; unsigned char LMajor; unsigned char LMinor; unsigned long CodeSize; unsigned long SizeOfInitializedData; unsigned long SizeOfUninitializedData; unsigned long EntryPointRVA; unsigned long BaseOfCode; unsigned long BaseOfData; unsigned long ImageBase; unsigned long SectionAlignment; unsigned long FileAlignment; unsigned short OSMajor; unsigned short OSMinor; unsigned short UserMajor; unsigned short UserMinor; unsigned short SubsysMajor; unsigned short SubsysMinor; unsigned long Reserved; unsigned long ImageSize; unsigned long HeaderSize; unsigned long FileCheckSum; unsigned short Subsystem; unsigned short DllFlags; unsigned long StackReserveSize; unsigned long StackCommitSize; unsigned long HeapReserveSize; unsigned long HeapCommitSize; unsigned long LoaderFlags; unsigned long NumberOfDataDirectories; }OptHdr; // Поле не используется в сборках. Заполняется нулями struct IMAGE_DATA_DIRECTORY STUB1; // Директория импорта struct IMAGE_DATA_DIRECTORY IMPORT_DIRECTORY; // Поле не используется в сборках. Заполняется нулями struct IMAGE_DATA_DIRECTORY STUB2[3]; // Директория релокации struct IMAGE_DATA_DIRECTORY BASE_RELOC_DIRECTORY; // Поле не используется в сборках. Заполняется нулями struct IMAGE_DATA_DIRECTORY STUB3[6]; // Директория таблицы адресов импорта struct IMAGE_DATA_DIRECTORY IAT_DIRECTORY; // Поле не используется в сборках. Заполняется нулями struct IMAGE_DATA_DIRECTORY STUB4; // Директория заголовка CLI struct IMAGE_DATA_DIRECTORY CLI_DIRECTORY; // Поле не используется в сборках. Заполняется нулями struct IMAGE_DATA_DIRECTORY STUB5; // Заголовок .text секции struct IMAGE_SECTION_HEADER TEXT_SECTION; // Заголовок .cli секции struct IMAGE_SECTION_HEADER CLI_SECTION; // Заголовок .reloc секции struct IMAGE_SECTION_HEADER RELOC_SECTION; }; struct IMAGE_DATA_DIRECTORY { // Директория данных unsigned long RVA; unsigned long Size; }; struct IMAGE_SECTION_HEADER { // Заголовок секции unsigned char Name[8]; unsigned long VirtualSize; unsigned long VirtualAddress; unsigned long SizeOfRawData; unsigned long PointerToRawData; unsigned long PointerToRelocations; unsigned long PointerToLinenumbers; unsigned short NumberOfRelocations; unsigned short NumberOfLinenumbers; unsigned long Characteristics; };
В свою очередь функция make_headers вызывает функцию make_headers_const, которая заполняет поля-константы, одинаковые во всех сборках.
Для нашего учебного примера выберем расположение секции в файле, указанное на рис. 2.6.
Как можно заметить, сгенерированная сборка .NET состоит из 3 секций:
- Секция ".text" (содержит тела методов и метаданные);
- Секция ".cli" (содержит точку входа, заголовок cli, таблицу импорта);
- Секция ".reloc" (секция релокаций).
Следовательно, после дополнительного заголовка в структуре HEADERS будут находиться 3 заголовка секций.
Для сборок .NET необходимы 4 директории данных:
- Директория импорта;
- Директория релокации;
- Директория заголовка CLI;
- Директория таблицы адресов импорта.
На основе блока входных параметров вычисляется расположение секций в памяти. Вычисления осуществляются внутри набора макросов (см. таблицу 2.1).
Код функции align:
#include <stdlib.h> unsigned long align(unsigned long x, unsigned long alignment) { div_t t = div(x,alignment); return t.rem == 0 ? x : (t.quot+1)*alignment; };
В заключение структура HEADERS пишется в начало выходного файла, причем записывается количество байт, равное значению макроса SIZE_OF_HEADERS(params), который объявлен следующим образом:
#define SIZEOF_HEADERS(params) \ align(sizeof(struct HEADERS), params->FileAlignment)
Обычно размер структуры HEADERS не кратен i nP->FileAlignment, следовательно разница дописывается нулями.
Этап 2. Генерация секции ".text"
Функция, выполняющая работу на этом этапе - make_text_section. Прототип функции:
void make_text_section (FILE* file, PINPUT_PARAMETERS inP);
В секции ".text" находятся метаданные и тела методов. Сначала в памяти выделяется массив, кратный выравниванию в файле. Размер массива задается макросом SIZEOF_TEXT(params), который определен следующим образом:
#define SIZEOF_TEXT(params) \ align(params->SizeOfMetadata+params->SizeOfCilCode, \ params->FileAlignment)
Макрос принимает в качестве аргумента блок входных параметров.
В выделенную память записываются метаданные из массива metadata и тела методов из массива cilcode, адреса которых передаются в функцию через поля inP->metadata и inP->cilcode блока входных параметров. Затем этот массив записывается в выходной файл сразу после заголовка HEADERS. Если размер метаданных и CIL-кода не кратен inP->FileAlignment, то разница дописывается нулями.
Этап 3. Генерация секции ".cli"
Всю работу на этом этапе выполняет функция make_cli_section. Прототип функции:
void make_cli_section (FILE* file, PINPUT_PARAMETERS inP);
В секции ".cli" содержится структура CLI_SECTION_IMAGE, в которой находится точка входа в приложение, заголовок CLI, таблица импорта и таблица адресов импорта:
struct CLI_SECTION_IMAGE { struct _JMP_STUB { // Точка входа unsigned short JmpInstruction; unsigned long JmpAddress; }JMP_STUB; struct _CLI_HEADER { // Заголовок CLI unsigned long cb; unsigned short MajorRuntimeVersion; unsigned short MinorRuntimeVersion; struct IMAGE_DATA_DIRECTORY MetaData; unsigned long Flags; unsigned long EntryPointToken; struct IMAGE_DATA_DIRECTORY NotUsed[6]; }CLI_HEADER; struct _IMPORT_TABLE { // Import Address Table unsigned long HintNameTableRVA2; unsigned long zero2; // Вход в таблицу импорта unsigned long ImportLookupTableRVA; unsigned long TimeDateStamp; unsigned long ForwarderChain; unsigned long NameRVA; unsigned long ImportAddressTableRVA; unsigned char zero[20]; // Import Lookup Table unsigned long HintNameTableRVA1; unsigned long zero1; // Hint/Name Table unsigned short Hint; char Name[12]; // Dll name ("mscoree.dll") char DllName[12]; }IMPORT_TABLE; };
Поле JmpAddress заполняется значением выражения:
RVA_OF_CLI(inP) + OFFSETOF(struct CLI_SECTION_IMAGE,IMPORT_TABLE.Hint) + inP->ImageBase;
Заметим, что
#define OFFSETOF(s,m) (size_t)&(((s *)0)->m)
Таким образом, к абсолютному адресу секции ".cli" прибавляется смещение поля Hint в структуре CLI_SECTION_IMAGE.
Сразу за точкой входа находится заголовок CLI, который служит для определения положения метаданных в PE-файле. В заголовке находится информация об RVA и размере метаданных, а также информация о версии CLR, для которой предназначена сборка и токен метаданных, указывающий на точку входа в сборку. У DLL токен точки входа равен 0, т.к. DLL не может сама выполнять какие-либо действия.
В конце работы функции структура CLI_SECTION_IMAGE пишется в выходной файл, сразу после секции ".text". Записывается количество байт, равное значению макроса SIZEOF_CLI, который имеет следующий вид:
#define SIZEOF_CLI(params) \ align(sizeof(struct CLI_SECTION_IMAGE), params->FileAlignment)
Если структура CLI_SECTION_IMAGE не кратна inP->FileAlignment, то разница дописывается нулями.
Этап 4. Генерация секции ".reloc"
Функция, ответственная за этот этап - make_reloc_section. Прототип данной функции:
void make_reloc_section (FILE* file, PINPUT_PARAMETERS inP);
Заключительная секция релокации содержит исправления для единственного абсолютного адреса в сборке, который находится в точке входа jmp dword ptr ds:[x] в секции ".cli". Адрес x надо исправить, если сборка грузится по адресу, отличному от базового. Сгенерированная секция ".reloc" содержит единственную структуру RELOC_SECTION, в которой есть все необходимые поля для исправления.
Поле PageRVA содержит адрес страницы, в которой надо произвести исправление. Заполняется значением макроса RVA_OF_CLI. Поле BlockSize заполняется значением макроса SIZEOF_RELOC_NOTALIGNED, который определен так:
#define SIZEOF_RELOC_NOTALIGNED sizeof(struct RELOC_SECTION).
В сборках .NET в качестве типа исправления используется значение 3. Смещение адреса x на странице равно 2, т.к. расположение секций в памяти выровнено по страницам:
struct RELOC_SECTION { unsigned long PageRVA; // адрес страницы unsigned long BlockSize; // размер блока unsigned short TypeOffset; // тип исправления и // смещение на странице unsigned short Padding; // завершающие нули };
Структура записывается в конец файла после секции ".cli". Чтобы размер файла был кратен inP->FileAlignment, в него дописывается определенное количество нулей.
Метаданные и методы
Если описать метаданные и методы сгенерированной сборки на CIL с использованием синтаксиса ILASM, то получится следующая IL-программа:
.assembly extern mscorlib { .ver 1:0:5000:0 } .assembly arith { .hash algorithm 0x00008004 .ver 1:0:1:1 } .module arith.exe // MVID: {86612D1B-0333-4F08-A88A-857326D72DDF} .imagebase 0x11000000 .subsystem 0x00000003 .file alignment 4096 .corflags 0x00000001 // Image base: 0x02ef0000 .method public static void calc() cil managed { .entrypoint // Code size 21 (0x15) .maxstack 8 IL_0000: ldstr "Hello" IL_0005: call void [mscorlib]System.Console::WriteLine(string) IL_000a: call string [mscorlib]System.Console::ReadLine() IL_000f: call void [mscorlib]System.Console::WriteLine(string) IL_0014: ret }
Метаданные, используемые при генерации сборки, находятся в массиве metadata, который в программе описан следующим образом (полное описание не приводится из-за его большого размера, полностью листинг массива metadata приводится в исходных текстах учебного примера):
unsigned char metadata[] = { 0x42, 0x53, 0x4A, 0x42, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, . . . . . . . . . . . . . . . . . . . . . . . . 0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
В такой же форме в программе находится CIL-код методов:
unsigned char cilcode[] = { 0x56, 0x72, 0x01, 0x00, 0x00, 0x70, 0x28, 0x02, 0x00, 0x00, 0x0A, 0x28, 0x01, 0x00, 0x00, 0x0A, 0x28, 0x02, 0x00, 0x00, 0x0A, 0x2A };
Пример работы программы
Итак, попробуем запустить нашу программу, набрав в консоли pegen.exe (так будет называться наша программа):
C:\>Pegen.exe
Если все прошло успешно, то на экране мы увидим сообщение об успешной генерации сборки hello.exe:
File: hello.exe generated
Запустим сгенерированную сборку:
C:\>hello.exe
Программа распечатает на экране строку "Hello" и попросит ввести произвольный текст. Введем, например, строку:
Hello Programm
В результате программа распечатает на экране строку, введенную ранее, и закончит свою работу:
Hello Programm