| Рабочим названием платформы .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
