Блог → Общая структура PE-файла. Часть 1

Что такое PE-файлы? Так называют исполняемые файлы, откомпилированные для работы в 32-разрядных операционных системах семейства Windows, и таким образом, к РЕ-файлам относятся, прежде всего, exe-шники и dll-ки, хотя, разумеется, одними или этот список не ограничивается. Часто аббревиатуру "PE" представляют как "Portable Executable", и в данном случае буквальный перевод слова "portable" как "портативный", вероятно, неприменим. Скорее тут подойдёт слово "портируемый", и в данном случае речь идёт о возможности переноса программ, откомпилированных для Windows, на другие платформы. Мы же в дальнейшем ограничимся названием "PE-файл".

Думаю, знать структуру РЕ-файла обязан каждый программист, работающий под Windows. Данные, которые могут находиться в нём, в совокупности представляют собой целую энциклопедию. Но для того, чтобы прочитать эту энциклопедию, нужно научиться читать на языке РЕ-файла.

В этой статье я постараюсь показать взаимодействие данных, из которых состоит PE-файл, а также рассказать о том, как эти данные можно извлечь из глубин, самых что ни есть недр, исполняемого файла. Надеюсь, этот рассказ поможет программистам под Windows в решении некоторых проблем, возникающих перед ними. Итак, допустим, что PE-файл полностью (за исключением, разумеется, непосредственно кода и определенных программистом данных) состоит из структур, документированных в MSDN и/или заголовочных файлах, поставляемых с любым SDK. Постараемся определить, что же это за структуры.

Совершенно естественно, что любой исполняемый файл начинается с заголовка DOS. Я прошелся контекстным поиском по заголовочным файлам, которые поставляются вместе с Visual C++, и в файле winnt.h обнаружил структуру IMAGE_DOS_HEADER. Естественно, эта структура и определяет состав и назначение полей DOS-заголовка исполняемого файла. Описание этой структуры я привожу ниже:

typedef Struct _IMAGE_DOS_HEADER {

// DOS .EXE header
WORD e_magic;
// Magic number
WORD e_cblp;
// Bytes on last page of file
WORD e_cp;
// Pages in file
WORD e_crlc;
// Relocations
WORD e_cparhdr;
// Size of header in paragraphs
WORD e_minalloc;
// Minimum extra paragraphs needed
WORD e_maxalloc;
// Maximum extra paragraphs needed
WORD e_ss;
// Initial (relative) SS value
WORD e_sp;
// Initial SP value
WORD e_csum;
// Checksum
WORD e_ip;
// Initial IP value
WORD e_cs;
// Initial (relative) CS value
WORD elfarlc;
// File address of relocation table
WORD e_ovno;
// Overlay number
WORD e_res[4];
// Reserved words
WORD e_oemid;
// OEM identifier (for e_oeminfo)
WORD e_oeminfo;
// OEM information; e_oemid specific
WORD e_res2[10);
// Reserved words
LONG elfanew;
// File address of new exe header
} IMAGE_DOS_HEADER,

* PIMAGE_DOS_HEADER;


Этот заголовок подробно описан во множестве источников, поэтому я не буду останавливаться на описании его полей. Но, однако, нам необходимо найти Windows-заголовок. Как это сделать? Достаточно просто, необходимо только обратить внимание на последнее поле заголовка DOS: elfanew - это и есть смещение Windows-заголовка относительно начала исполняемого файла. Мэтт Питрек в одной из статей, правда, упомянул, что для того, чтобы поле elfanew можно было бы интерпретировать как смещение Windows-заголовка, его значение должно быть больше или равно 0x40. Примем это утверждение к сведению!

Итак, смещение Windows-заголовка мы узнали. Теперь попытаемся определить его структуру. Пройдемся по файлу winnt.h в надежде увидеть что-нибудь интересное. В самом ближайшем будущем надежды оправдываются - мы находим структуру IMAGE_NT_HEADERS. Вообще-то, мы найдем две структуры - IMAGE_NT_HEADERS64 и IMAGE_NT_HEADERS32. Однако, при использовании макроса IMAGE_NT_HEADERS в зависимости от используемой системы (64-или 32-битовой) выбирается нужная структура. Так делается во многих местах файла winnt.h, поэтому я больше не буду останавливаться на этом механизме и буду опускать суффиксы "64" и "32". Естественно, смысл при этом останется неизменным. Смотрим, что представляет собой эта структура:

typedef Struct IMAGE_NT_HEADERS {

DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32,

*PIMAGE_NT_HEADERS32;


Итак... Мы видим, что Windows-заголовок, в свою очередь, состоит из сигнатуры, обязательной и необязательной частей. Как мы вскоре увидим, название "необязательный" (optional) в данном случае явно неудачно, скорее всего, это "отголосок" самых первых, внутренних релизов 32-разрядных Windows. Определение сигнатуры мы также найдем в файле winnt.h:

define IMAGE_NT_SIGNATURE
0x00004550 // PE00


Теперь, думаю, стало понятно, почему исполняемый файл в 32-разрядной Windows часто называют PE-файлом. Несложно найти и описание обязательной части заголовка:

typedef struct _IMAGE_FILE_HEADER {

WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER,

* PIMAGE_FILE_HEADER;


Что может нас здесь заинтересовать? Поле Machine определяет, для какого компьютера откомпилирована данная программа. Значения, которые может принимать это поле, можно найти в том же файле winnt.h. Поищите, уважаемый читатель, строки, начинающиеся с "#define IMAGE_FILE_MACHINE_". Нашли?

Поле второе - NumberOfSection. Возможно, исполняемый файл состоит из каких-то секций или разделов? Состоит-то он состоит, но в этой статье мы их рассматривать не будем. Собственно, в дальнейшем при анализе содержимого PE-файла мы вспомним только одно поле, входящее в состав "обязательного" заголовка, это и будет число секций.

Следующие три поля, определяющие время создания файла, а также указатель на таблицу символов и число символов, мы пропустим - в данном случае они никакой интересной информации не несут. Очередное поле SizeOfOptionalHeader хранит, как видно из его названия, размер необязательного заголовка файла. Лично мне не совсем понятно назначение этого поля. Оно постоянно и равно 224. Это подтверждает и следующий макрос, также находящийся в файле winnt.h:

define IMAGE_SIZEOF_NT_
OPTIONAL_HEADBR 224


Возможно, в более ранних версиях Win32 размер необязательного файла не был фиксированным, и это поле оставлено для того, чтобы старые программы, которые были написаны ещё с учётом этого факта, остались бы совместимыми с новыми версиями операционной системы.

И, наконец, мы добрались до поля характеристик. Если вы, читатель, найдете строку "#define IMAGE_FILE_RELOCS_STRIPPED", то узнаете, какие значения может принимать это поле. Оно используется как логическая шкала, каждый бит которой имеет свое назначение. Назначение этих полей станет ясно из комментариев, приведенных в файле winnt.h.

Ну что же, с "обязательной" частью заголовка покончили, теперь ищем описание "необязательной" части:

typedef Struct _IMAGE_OPTIONAL_HEADER {

WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//NT additional fields.
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD Checksum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRBCTORY
DataDirectory [IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER,

*PIMAGE_OPTIONAL_HEADER;


Ого, ничего себе заголовочек! К сожалению, я просто не имею возможности описать здесь назначение всех полей этого заголовка. Замечу только, что в подавляющем большинстве случаев смещения данных указываются относительно начала уже загруженного в память файла, поэтому при просмотре файла на диске многие значения могут показаться несколько странными. На этом пока всё, но продолжение следует!