Опубликован: 28.06.2006 | Уровень: специалист | Доступ: платный | ВУЗ: Московский государственный технический университет им. Н.Э. Баумана
Лекция 3:

Структура программных компонентов

Пример генерации PE-файла

В приложении A приведен исходный код программы pegen, демонстрирующей генерацию PE-файла. Эта программа создает сборку hello.exe, работа которой заключается в дублировании строки, введенной пользователем с клавиатуры. Несмотря на то, что генерируемая сборка столь примитивна, программа pegen может служить основой для разработки реального генератора исполняемых файлов .NET.

Программа pegen написана на языке C и состоит из двух частей:

  1. модуль генерации PE-файла, оформленный как отдельная библиотека;
  2. главный модуль, использующий модуль генерации для создания простейшей сборки .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.

Схематичное расположение секций и заголовков

Рис. 2.6. Схематичное расположение секций и заголовков

Как можно заметить, сгенерированная сборка .NET состоит из 3 секций:

  1. Секция ".text" (содержит тела методов и метаданные);
  2. Секция ".cli" (содержит точку входа, заголовок cli, таблицу импорта);
  3. Секция ".reloc" (секция релокаций).

Следовательно, после дополнительного заголовка в структуре HEADERS будут находиться 3 заголовка секций.

Для сборок .NET необходимы 4 директории данных:

  1. Директория импорта;
  2. Директория релокации;
  3. Директория заголовка CLI;
  4. Директория таблицы адресов импорта.

На основе блока входных параметров вычисляется расположение секций в памяти. Вычисления осуществляются внутри набора макросов (см. таблицу 2.1).

Таблица 2.1. Описание макросов

Макрос:

RVA_OF_TEXT

Описание:

RVA секции ".text"

Подстановка:

align (sizeof(struct (HEADERS), SECTION_ALIGNMENT)

Код функции align приведен в конце таблицы.

align - округляет первый аргумент в большую сторону до числа, кратного значению SECTION_ALIGNMENT.

SECTION_ALIGNMENT - фиксированное выравнивание секций 0x2000

Макрос:

RVA_OF_CLI (params)

Описание:

RVA секции ".cli". Принимает в качестве аргумента блок входных параметров ( INPUT_PARAMETERS )

Подстановка:

RVA_OF_TEXT + align(params->SizeOfMetadata, SECTION_ALIGNMENT)

Макрос:

RVA_OF_RELOC (params)

Описание:

RVA секции ".reloc". Принимает в качестве аргумента блок входных параметров ( INPUT_PARAMETERS )

Подстановка:

RVA_OF_CLI (params) + SIZEOF_CLI_M

В свою очередь макрос SIZEOF_CLI_M определен как:

align (sizeof(struct (CLI_SECTION_IMAGE), SECTION_ALIGNMENT)

Формат и назначение CLI_SECTION_IMAGE описано далее, в "Этапе 3".

Код функции 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
Анастасия Булинкова
Анастасия Булинкова
Рабочим названием платформы .NET было
Bogdan Drumov
Bogdan Drumov
Молдова, Республика
Azamat Nurmanbetov
Azamat Nurmanbetov
Киргизия, Bishkek