Исследование переносимого формата исполнимых файлов "сверху вниз"

Randy Kath
Microsoft Developer Network Technology Group

Резюме

 

Операционная система Windows NT™ версии 3.1 представила новый формат исполнимых файлов, называемый Portable Executable (PE) - переносимый формат исполнимых файлов. Спецификация этого формата ( довольно несвязная ) была опубликована и включена в состав Microsoft Developer Network CD ( Specs and Strategy, Specifications, Windows NT File Format Specifications ). Однако только одна эта спецификация не дает достаточно информации для понимания разработчиками формата PE файлов, и не делает такое понимание легко достижимым или вообще возможным. Данная статья предназначена для устранения этой проблемы. В ней Вы найдете подробное объяснение всего формата PE файлов, вместе с описаниями всех необходимых структур и примерами исходного кода, которые демонстрируют использование излагаемой информации.

Все примеры исходного кода, включенные в эту статью, взяты из динамической библиотеки PEFILE.DLL. Я написал эту DLL просто чтобы просматривать всю важную информацию, содержащуюся в PE-файле. DLL и ее исходный код, а также пример приложения PEFile прилагаются; Вы свободны использовать их в своих собственных приложениях. Вы также можете взять мой исходный код для любых Ваших применений. В конце статьи, Вы можете найти краткий перечень экспортируемых из этой библиотеки функций и объяснения по их использованию. Я полагаю, что Вы гораздо легче поймете формат PE-файлов при наличии этих функций.

 

Введение

 

Недавнее дополнение к семейству операционных систем Windows™ - Microsoft® Windows NT™ привнесла с собою множество изменений в среду разработки и еще больше изменений - в сами приложения. Одно из наиболее значительных изменений - переносимый формат исполнимых файлов - Portable Executable (PE) file format. Новый формат произведен от спецификации COFF ( общий формат объектных файлов - Common Object File Format ), который распространен на многих операционных системах семейства UNIX®. В то же время, для сохранения совместимости с предыдущими версиями MS-DOS® и Windows, PE формат также сохранил старый знакомый MZ заголовок DOSа.

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

Большинство определений отдельных компонентов взято из файла заголовков WINNT.H, включенного в комплект разработчика Microsoft Win32™ Software Development Kit - SDK для Windows NT. В этом файле Вы можете найти определения типов структур для каждого из заголовков файла и структур данных, представляющих различные компоненты PE-файла. Однако, для некоторых компонентов WINNT.H не предоставляет достаточной информации, для таких компонентов я определил собственные структуры, которые могут быть использованы для манипуляций с PE-файлами. Вы найдете определения этих структур в другом файле заголовков - PEFILE.H, написанным при создании библиотеки PEFILE.DLL. Также прилагаются все файлы, необходимые для построения этой библиотеки ( исходный код ).

В дополнение к исходному коду PEFILE.DLL, также прилагается отдельное 32-битное приложение EXEVIEW.EXE. Этот пример был создан для двух целей:
1) мне нужен был пример, с помощью которого я мог бы проверить функции PEFILE.DLL, которые в некоторых случаях требуют множества видов файла одновременно - отсюда поддержка множества видов.
2) Большинство работы по исследованию формата PE-файлов требует инструмента для интерактивного просмотра данных. Например, чтобы понять структуру таблицы импортируемых функций, я должен был видеть заголовок сегмента .idata, опциональный заголовок и собственно тело сегмента .idata, и все одновременно. EXEVIEW.EXE - замечательный инструмент для просмотра подобной информации.

Итак, начнем без дальнейших промедлений

 

Структура PE файла

 

Формат PE-файлов организован в виде линейного потока данных. Он начинается с заголовка MS-DOS, программы реального режима, и сигнатуры PE файла. Далее следуют заголовок PE-файла и опциональный заголовок. После них идут заголовки всех сегментов, за которыми следуют тела этих сегментов. И ближе к концу файла расположены различные области данных, включая информацию о переадресации, таблицу символов, информацию о номерах строк и данные в таблице строк. Все их легче представить графически, как показано на рисунке 1.

 
[PEF2034B  6737 bytes ]
Figure 1. Структура переносимого формата исполнимых файлов

Каждый компонент PE-файла описывается ниже в том порядке, в котором он находится в PE-файле, начиная с заголовка MS-DOS. Большинство описаний основано на коде примеров, демонстрирующих получение информации из файла. Весь код примеров заимствован из файла PEFILE.C, файла исходного кода для библиотеки PEFILE.DLL. Каждый их таких примеров пользуется такой новой выдающейся возможностью Windows NT, как файлы, отображенные в память ( memory-mapped files ). Файлы, отображенные в память, позволяют использовать простые манипуляции с указателями для доступа к данным в таком файле. Каждый из примеров использует файл, отображенный в память, для доступа к данным в PE-файле.

Примечание Смотрите секцию в конце статьи для описания использования библиотеки PEFILE.DLL.

 

Заголовок Реального режима/MS-DOS

 

Как уже упоминалось выше, первый компонент в PE-файле - заголовок MS-DOS. Заголовок MS-DOS не нов для формата PE-файлов. Это тот же самый заголовок MS-DOS, который используется начиная с MS-DOS версии 2. Главная причина сохранения его нетронутым в самом начале файла - это то, что когда Вы попытаетесь загрузить PE-файл в Windows версии 3.1 или более ранней, или под MS DOS версии 2.0 или более поздней, операционная система сможет прочесть файл и понять, что он не совместим с имеющейся операционной системой. Другими словами, когда Вы попытаетесь запустить исполнимый файл от Windows NT под MS-DOS версии 6.0, Вы получите сообщение: "This program cannot be run in DOS mode." ( "Эта программа не может быть запущена в DOS" ). Если бы заголовок MS-DOS не был включен как первая часть PE файла, операционная система могла бы просто сбойнуть при попытке запуска такого файла, и предложить что-нибудь абсолютно бесполезное, наподобие:
"The name specified is not recognized as an internal or external command, operable program or batch file."
( "Указанное имя не является внешней или внутренней командой, исполнимой программой или командным файлом" )

Заголовок MS-DOS занимает первые 64 байта PE файла. Структура, определяющая его содержимое, описана ниже:

WINNT.H

typedef struct _IMAGE_DOS_HEADER {  // DOS .EXE заголовок
    USHORT e_magic;         // магическое число
    USHORT e_cblp;          // количество байт на последней странице файла
    USHORT e_cp;            // количество страниц в файле
    USHORT e_crlc;          // Relocations
    USHORT e_cparhdr;       // размер заголовка в параграфах
    USHORT e_minalloc;      // Minimum extra paragraphs needed
    USHORT e_maxalloc;      // Maximum extra paragraphs needed
    USHORT e_ss;            // начальное ( относительное ) значение регистра SS
    USHORT e_sp;            // начальное значение регистра SP
    USHORT e_csum;          // контрольная сумма
    USHORT e_ip;            // начальное значение регистра IP
    USHORT e_cs;            // начальное ( относительное ) значение регистра CS
    USHORT e_lfarlc;        // адрес в файле на таблицу переадресации
    USHORT e_ovno;          // количество оверлеев
    USHORT e_res[4];        // Зарезервировано
    USHORT e_oemid;         // OEM identifier (for e_oeminfo)
    USHORT e_oeminfo;       // OEM information; e_oemid specific
    USHORT e_res2[10];      // Зарезервировано
    LONG   e_lfanew;        // адрес в файле нового .exe заголовка
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
  

Первое поле, e_magic, этот так называемое магическое число. Это поле используется для идентификации типа файла, совместимого с MS-DOS. Все MS-DOS совместимые исполнимые файлы имеют сигнатуру 0x54AD, представляющую в ASCII-символах строку MZ. По этой причине на заголовок MS-DOS часто ссылаются также как на MZ-заголовок. Большинство других полей имеет значение для операционной системы MS-DOS, для Windows NT же есть только одно важное поле в этой структуре. Это последнее поле, e_lfanew, 4х байтовое смещение в файле, указывающее расположение заголовока PE файла. Мы должны использовать это смещение, чтобы найти заголовок PE файла. Для PE файлов, заголовок PE файла расположен сразу же за заголовком MS-DOS и только только тело программы Реального режима расположена между ними.

 

Программа Реального режима

 

Программа реального режима - это программа, запускаемая MS-DOS после загрузки исполнимого файла. Эта же программа запускается и для обычных исполнимых файлов MS-DOS. Для следующего поколения операционных систем, включая Windows, OS/2®, и Windows NT, сюда помещается маленькая программа, запускаемая вместо настоящего приложения при попытке его запуска из-под MS-DOS. Программа обычно печатает строку текста, например:
"This program requires Microsoft Windows v3.1 or greater." ( "Эта программа требует Microsoft Windows v3.1 или позднее" )
Конечно, создатели приложений имеют возможность поместить любую программу, какую захотят, например, Вы можете часто увидеть что-то наподобие:
"You can't run a Windows NT application on OS/2, it's simply not possible." ( "Вы не можете запустить приложение Windows NT под OS/2, это просто невозможно " )

Когда линкуется приложение для Windows версии 3.1, линковщик подставляет в приложение программу-затычку по умолчанию, называемую WINSTUB.EXE. Вы можете задать линковщику свою собственную MS-DOS-совместимую программу вместо WINSTUB, указав ее с помощью оператора STUB в файле определения проекта ( .DEF файле ). Приложения, разработанные для Windows NT, имеют такую же возможность через опцию линковщика -STUB: при линковке исполнимого файла.

 

Заголовок PE файла

 

Заголовок PE файла расположен по смещению, заданному полем e_lfanew заголовка MS-DOS. Поле e_lfanew содержит простое смещение в файле, поэтому для определения адреса заголовка PE файла мы должны добавить к базовому адресу файла, отображенного в память, это смещение - получим адрес в нашем отображенном в память файле. Например, следующий макрос используется в файле заголовка PEFILE.H:

PEFILE.H

#define NTSIGNATURE(a) ((LPVOID)((BYTE *)a +    \
                        ((PIMAGE_DOS_HEADER)a)->e_lfanew))
 

Я обнаружил, что при манипуляциях с PE файлами, есть несколько мест в файле, на которые мне нужно ссылаться очень часто. Так как эти места можно представить в виде смещений в файле, легче реализовать их вычисление в виде макросов, потому что они дают намного лучшую производительность, чем функции.

Заметьте, что вместо получения смещения заголовка PE файла, этот макрос вычисляет местоположение сигнатуры PE файла. Начиная с Windows и OS/2, .EXE файлам были присвоены сигнатуры файлов, чтобы указать целевую платформу исполнения. Для PE файлов, эта сигнатура расположена непосредственно перед структурой заголовка PE файла. В Windows и OS/2 сигнатура была первым первым словом заголовка файла. Для PE файлов Windows NT использует для сигнатуры DWORD

Макрос, представленный выше, возвращает смещение, по которому расположена сигнатура, независимо от типа исполнимого файла. В зависимости от того, PE файл это или нет, заголовок файла расположен либо за сигнатурой типа DWORD, либо начиная с сигнатуры типа WORD. Чтобы разобраться во всех тонкостях, я написал функцию ImageFileType ( см. ниже ), возвращающую тип файла:

PEFILE.C

DWORD  WINAPI ImageFileType (
    LPVOID    lpFile)
{
    /* DOS вначале расположена сигнатура DOS. */
    if (*(USHORT *)lpFile == IMAGE_DOS_SIGNATURE)
        {
        /* Определим местоположение заголовка PE файла */
        if (LOWORD (*(DWORD *)NTSIGNATURE (lpFile)) ==
                                IMAGE_OS2_SIGNATURE ||
            LOWORD (*(DWORD *)NTSIGNATURE (lpFile)) ==
                             IMAGE_OS2_SIGNATURE_LE)
            return (DWORD)LOWORD(*(DWORD *)NTSIGNATURE (lpFile));

        else if (*(DWORD *)NTSIGNATURE (lpFile) ==
                            IMAGE_NT_SIGNATURE)
            return IMAGE_NT_SIGNATURE;

        else
            return IMAGE_DOS_SIGNATURE;
        }

    else
        /* неизвестный тип файла */
        return 0;
}

Приведенный выше код показывает, как быстро становится полезным макрос NTSIGNATURE. Он облегчает сравнение различных типов файлов, и возвращает соответствующий тип для данного файла. В файле заголовков WINNT.H определены четыре различных типа файлов:

WINNT.H

#define IMAGE_DOS_SIGNATURE             0x5A4D      // MZ
#define IMAGE_OS2_SIGNATURE             0x454E      // NE
#define IMAGE_OS2_SIGNATURE_LE          0x454C      // LE
#define IMAGE_NT_SIGNATURE              0x00004550  // PE00
 

Сначала выглядит странным, что тип файла Windows executable ( исполнимый файл Windows 3.1 ) отсутствует в этом списке. Но далее, после небольшого расследования, причина становится ясна: на самом деле нет различия между исполнимыми файлами Windows и OS/2, за исключением спецификации версии операционной системы. Обе операционные системы имеют одинаковые структуры исполнимых файлов.

Возвращаясь к переносимому формату исполнимых файлов, мы находим, что после обнаружения местоположения сигнатуры, заголовок PE файла находится через четыре байта. Следующий макрос возвращает заголовок PE файла:

PEFILE.C

#define PEFHDROFFSET(a) ((LPVOID)((BYTE *)a +  \
    ((PIMAGE_DOS_HEADER)a)->e_lfanew + SIZE_OF_NT_SIGNATURE))
 

Единственная разница между этим и предыдущим макросом заключается в добавлении константы SIZE_OF_NT_SIGNATURE. Нужно заметить, эта константа не определена в файле заголовков WINNT.H, вместо этого я определил ее в PEFILE.H как размер DWORD

Сейчас, когда мы знаем местоположение заголовка PE файла, мы можем проверить данные в заголовке простым присваиванием адреса указателю на структуру, как в следующем примере:

PIMAGE_FILE_HEADER   pfh;

 pfh = (PIMAGE_FILE_HEADER)PEFHDROFFSET (lpFile);
 

В этом примере, lpFile содержит базовый адрес исполнимого файла, отображенного в память, и в этом заключается удобство файлов, отображенных в памяти. Не нужно производить операций файлового ввода/вывода; Вы просто ссылаетесь на указатель pfh, чтобы достичь информации в файле.
Структура заголовка PE файла определена как:

typedef struct _IMAGE_FILE_HEADER {
    USHORT  Machine;
    USHORT  NumberOfSections;
    ULONG   TimeDateStamp;
    ULONG   PointerToSymbolTable;
    ULONG   NumberOfSymbols;
    USHORT  SizeOfOptionalHeader;
    USHORT  Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

#define IMAGE_SIZEOF_FILE_HEADER             20
 

Заметьте, что размер этой структуры для удобства определен в файле заголовков. Это облегчает получение размера структуры, однако мне показалось удобнее использовать оператор sizeof к самой структуре, потому что такой способ не требует от меня вспомнить имя константы IMAGE_SIZEOF_FILE_HEADER в дополнение к самому названию структуры IMAGE_FILE_HEADER. Другими словами, запоминание имен всех структур показалось мне довольно утомительным, тем более, что они нигде более не документированы, за исключеним файла заголовков WINNT.H.

Информация в заголовке PE файла является высокоуровневой, и используется системой или приложениями чтобы определить, как поступать с данным файлом. Первое поле, Machine, используется для идентификации типа машины, для которой приложение было построено, например, DEC® Alpha, MIPS R4000, Intel® x86, или другие типы процессоров. Система использует эту информацию, чтобы быстро определить, что представляет собой данный файл без углубления в дальнейшие подробности его содержимого.

Поле Characteristics содержит специфические характеристики файла, например, признак наличия отдельного файла с отладочной информацией. Возможно удалить отладочную информацию из самого PE файла и сохранить ее в отдельном отладочном файле ( .DBG ) для использования отладчиком. Затем при отладке отладчику нужно знать, содержится ли отладочная информация в отдельном файле или нет, и удалена ли она из исполнимого файла. Отладчик мог бы найти это, просмотрев содержимое исполнимого файла. Чтобы устранить необходимость отладчику просматривать содержимое всего файла, было введено это поле, содержащее признак удаления из файла отладочной информации (IMAGE_FILE_DEBUG_STRIPPED). Отладчик может быстро определить, присутствует ли в файле отладочная информация.

Файл WINNT.H определяет несколько других флагов, управляющих характеристиками PE файла ( как расмотренный выше признак наличия отладочной информации ). Я оставил в качестве упражнения читателям выяснить, какие это флаги и посмотреть, есть ли среди них интересные. Они размещены в файле заголовков WINNT.H сразу же после определения структуры IMAGE_FILE_HEADER, описанной выше.1)

Одно из других полезных полей в структуре заголовка PE файла - NumberOfSections. Оно содержит количество сегментов, или, более точно, количество заголовков сегментов и количество тел сегментов, имеющихся в исполнимом PE файле - для облегчения получения информации по ним. Каждые заголовок и тело сегмента расположены в файле последовательно, так что число сегментов необходимо, чтобы определить, где заканчиваются заголовки и тела сегментов. Следующая функция извлекает число сегментов из заголовка PE файла:

PEFILE.C

int   WINAPI NumOfSections (
    LPVOID    lpFile)
{
    /* Число сегментов из заголовка PE файла. */
    return (int)((PIMAGE_FILE_HEADER)
                  PEFHDROFFSET (lpFile))->NumberOfSections);
}
 

Как Вы можете видеть, PEFHDROFFSET и другие макросы достаточно сильно облегчают жизнь.

 

Опциональный заголовок

 

Следующие 224 байта в исполнимом файле занимает опциональный заголовок. Несмотря на то, что в его названии присутствует слово "опциональный", дальнейшее повествование убеждает, что это вовсе не опциональный компонент PE файла. Указатель на опциональный заголовок может быть получен с помощью макроса OPTHDROFFSET:

PEFILE.H

#define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a                 + \
    ((PIMAGE_DOS_HEADER)a)->e_lfanew + SIZE_OF_NT_SIGNATURE + \
    sizeof (IMAGE_FILE_HEADER)))
 

Опциональный заголовок содержит более полезную информацию об исполнимом файле, такую, как начальный размер стека, адрес точки входа программы, предпочтительный адрес загрузки, версию операционной системы, информацию о выравнивании сегментов и так далее. Структура IMAGE_OPTIONAL_HEADER, представляющая опциональный заголовок, выглядит так:

WINNT.H

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Стандартные поля.
    //
    USHORT  Magic;
    UCHAR   MajorLinkerVersion;
    UCHAR   MinorLinkerVersion;
    ULONG   SizeOfCode;
    ULONG   SizeOfInitializedData;
    ULONG   SizeOfUninitializedData;
    ULONG   AddressOfEntryPoint;
    ULONG   BaseOfCode;
    ULONG   BaseOfData;
    //
    // Дополнительные поля NT.
    //
    ULONG   ImageBase;
    ULONG   SectionAlignment;
    ULONG   FileAlignment;
    USHORT  MajorOperatingSystemVersion;
    USHORT  MinorOperatingSystemVersion;
    USHORT  MajorImageVersion;
    USHORT  MinorImageVersion;
    USHORT  MajorSubsystemVersion;
    USHORT  MinorSubsystemVersion;
    ULONG   Reserved1;
    ULONG   SizeOfImage;
    ULONG   SizeOfHeaders;
    ULONG   CheckSum;
    USHORT  Subsystem;
    USHORT  DllCharacteristics;
    ULONG   SizeOfStackReserve;
    ULONG   SizeOfStackCommit;
    ULONG   SizeOfHeapReserve;
    ULONG   SizeOfHeapCommit;
    ULONG   LoaderFlags;
    ULONG   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;

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

 

Стандартные поля

 

Для начала заметим, что структура поделена на "стандартные поля" и "дополнительные поля NT". Стандартные поля - имеющиеся и в Common Object File Format ( общий формат объектных файлов - COFF), который используется большинством исполнимых файлов UNIX. Несмотря на то, что стандартные поля имеют такие же названия, как определено в COFF, в действительности Windows NT использует некоторые из них совершенно для других целей, нежели предписано COFF, и для них лучше было бы подобрать другие имена.

 

Magic. Я не смог определить, для чего используется это поле. Для тестового приложения EXEVIEW.EXE, его значение было 0x010B или 267.
MajorLinkerVersion, MinorLinkerVersion. Содержат версию линковщика, создавшего данный файл. Предварительный Windows NT Software Development Kit (комплект разработчика - SDK), поставляемый с Windows NT build 438, включает линковщик версии 2.39 (2.27 hex).
SizeOfCode. Размер исполнимого кода.
SizeOfInitializedData. Размер инициализированных данных.
SizeOfUninitializedData. Размер неинициализированных данных.
AddressOfEntryPoint. Из всех стандартных полей, поле AddressOfEntryPoint является наиболее интересным для формата PE файлов. Это поле содержит адрес точки входа приложения, и, что, вероятно, более важно для для хакеров, местоположение конца Import Address Table (таблицы импортируемых адресов - IAT). Следующая функция показывает, как извлечь точку входа исполнимого файла Windows NT из опционального заголовка:

PEFILE.C

LPVOID  WINAPI GetModuleEntryPoint (
    LPVOID    lpFile)
{
    PIMAGE_OPTIONAL_HEADER   poh;

    poh = (PIMAGE_OPTIONAL_HEADER)OPTHDROFFSET (lpFile);

    if (poh != NULL)
        return (LPVOID)poh->AddressOfEntryPoint;
    else
        return NULL;
}
 
BaseOfCode. Относительное смещение сегмента кода (".text" сегмент) в загруженном файле.
BaseOfData. Относительное смещение сегмента неинициализированных данных (".bss" сегмент) в загруженном файле.

 

 

Дополнительные поля Windows NT

 

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

 

ImageBase. Предпочтительный адрес в адресном пространстве процесса для загрузки исполнимого файла. Линковщик, поставляемый с Microsoft Win32 SDK for Windows NT подставляет значение по умолчанию 0x00400000, но Вы можете переписать этот адрес с помощью опции линковщика -BASE:.
SectionAlignment. Сегменты загружаются в адресное пространство процесса последовательно, начиная с ImageBase. SectionAlignment предписывает минимальный размер, который сегмент может занять при загрузке - так что сегменты оказываются выровненными по границе SectionAlignment.
Выравнивание сегмента не может быть меньше размера страницы ( в настоящий момент 4096 байт на платформе x86 ), и должно быть кратно размеру страницы, как предписывает поведение менеджера виртуальной памяти Windows NT. 4096 байт являются значением по умолчанию, но может быть установлено также другое значение, используя опцию линковщика -ALIGN:
FileAlignment. Минимальная гранулярность сегментов в исполнимом файле до его загрузки. Пояснение: линковщик дополняет нулями тела сегментов, ( сырые данные сегментов ), чтобы их минимальный размер был кратен FileAlignment в файле. Линковщик версии 2.39, уже упоминавшийся ранее в этой статье, выравнивает содержимое тел сегментов по границе 0x200 байт. Это значение должно быть степенью двойки между 512 и 65,535.
MajorOperatingSystemVersion. Означает старшую часть версии операционной системы Windows NT, в настоящее время равной 1 для Windows NT версии 1.0.
MinorOperatingSystemVersion.Означает младшую часть версии операционной системы Windows NT, в настоящее время равной 0 для Windows NT версии 1.0.
MajorImageVersion. Используется для обозначения старшей части версии приложения; в Microsoft Excel версии 4.0 значение этого поля было бы 4.
MinorImageVersion. Используется для обозначения младшей части версии приложения; в Microsoft Excel версии 4.0 значение этого поля было бы 0.
MajorSubsystemVersion. Означает старшую часть версии Win32-подсистемы Windows NT, в настоящее время равной 3 для Windows NT версии 3.10.
MinorSubsystemVersion. Означает младшую часть версии Win32-подсистемы Windows NT, в настоящее время равной 10 для Windows NT версии 3.10.
Reserved1. Значение неизвестно, в настоящее время системой не используется и устанавливается линковщиком в 0.
SizeOfImage. Содержит объем адресного пространства, зарезервированного в адресном пространстве процесса для загружающегося исполнимого файла. Это число сильно зависит от SectionAlignment. Простой пример:
Представим, что система имеет фиксированный размер страницы 4096 байт. Если мы имеем исполнимый файл из 11 сегментов, каждый из которых меньше 4096 байт, выравненных по границе 65,536 байт, поле SizeOfImage должно быть установлено 11 * 65,536 = 720,896 ( 176 страниц ). Тот же самый файл, построенный с выравниванием 4096 байт, в результате будет иметь размер в памяти 11 * 4096 = 45,056 ( 11 страниц ) для поля SizeOfImage. Это простой пример, в котором каждый сегмент требует менее одной страницы памяти. В действительности линковщик определяет точное значение SizeOfImage, подсчитывая требуемое место для каждого сегмента. Сначала он определяет размер каждого сегмента, затем округляет это число, чтобы оно стало кратно размеру страницы, и наконец, он вычисляет количество страниц, чтобы их размер стал кратен SectionAlignment. Эти размеры далее суммируются по каждому сегменту.
SizeOfHeaders. Это поле содержит размер места, занимаемого всеми заголовками файла, включая заголовок MS-DOS, загловок PE файла, опциональный заголовок PE файла, и заголовки всех сегментов. Тела сегментов начинаются по смещению в файле, хранимому в этом поле.
CheckSum. Контрольная сумма используется для проверки целостности исполнимого файла во время загрузки. Это поле устанавливается и проверяется линковщиком. Алгоритм, используемый для подсчета контрольной суммы, является служебной информацией и не подлежит разглашению.
Subsystem. Это поле используется для идентификации подсистемы исполнения данного исполнимого файла. Значения всех возможных подсистем содержатся в файле заголовков WINNT.H сразу же после определения структуры IMAGE_OPTIONAL_HEADER2)
DllCharacteristics. Флаги, используемые в .DLL для указания наличия точки входа для старта/завершения процесса и его потоков ( DllMain ).
SizeOfStackReserve, SizeOfStackCommit, SizeOfHeapReserve, SizeOfHeapCommit. Эти поля контролируют объем адресного пространства, зарезервированного и выделенного для стека и кучи по умолчанию. По умолчанию, и для стека ,и для общей кучи зарезервировано 16 страниц, и выделено по одной странице. Эти значения могут устанавливаться опциями линковщика -STACKSIZE: и -HEAPSIZE: соответственно.
LoaderFlags. Указывает отладчику остановиться после загрузки, перейти к отладке после загрузки, или, значение по умолчанию, просто запустить файл.
NumberOfRvaAndSizes. Это поле содержит размер массива DataDirectory, расположенного далее. Важно отметить, что это поле содержит именно размер этого массива, а не количество элементов в нем.
DataDirectory. Каталог данных - содержит указатели на другие важные компоненты PE файла. В действительности, это есть ни что иное, как массив структур IMAGE_DATA_DIRECTORY, расположенных в конце опционального заголовка. В настоящее время формат PE файлов определяет 16 возможных типов каталогов данных, 11 из которых используются чаще всего.

 

Каталоги данных

Каталоги данных, как определено в файле заголовков WINNT.H, есть:

WINNT.H

// Каталоги данных
// Каталог экспортируемых объектов
#define IMAGE_DIRECTORY_ENTRY_EXPORT         0
// Каталог импортируемых объектов
#define IMAGE_DIRECTORY_ENTRY_IMPORT         1
// Каталог ресурсов
#define IMAGE_DIRECTORY_ENTRY_RESOURCE       2
// Каталог исключений
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION      3
// Каталог безопасности
#define IMAGE_DIRECTORY_ENTRY_SECURITY       4
// Таблица переадресации
#define IMAGE_DIRECTORY_ENTRY_BASERELOC      5
// Отладочный каталог
#define IMAGE_DIRECTORY_ENTRY_DEBUG          6
// Строки описания
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT      7
// Машинный значения (MIPS GP)
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR      8
// Каталог TLS ( Thread local storage - локальная память потоков )
#define IMAGE_DIRECTORY_ENTRY_TLS            9
// Каталог конфигурации загрузки
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10
  

Каждый каталог данных является структурой, определенной как IMAGE_DATA_DIRECTORY. Несмотря на то, что все каталоги данных одинаковы, каждый конкретный тип каталога данных уникален. Определение каждого из них описано ниже в этой статье в главе "Предопределенные сегменты".

WINNT.H

typedef struct _IMAGE_DATA_DIRECTORY {
    ULONG   VirtualAddress;
    ULONG   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
 

Каждый элемент в массиве каталогов данных содержит свой размер и относительный виртуальный адрес каталога. Чтобы найти местоположение некоторого каталога, Вы должны определить относительный адрес из массива каталогов данных в опциональном заголовке. Далее используйте виртуальный адрес для определения, в каком сегменте находится нужный каталог. После определения сегмента, содержащего нужный каталог данных, используйте заголовок этого сегмента для определения смещения в файле, указывающего местоположение нужного каталога данных.

Таким образом, для доступа к каталогам данным мы должны сначала изучить сегменты, описанные ниже. Пример нахождения местоположения каталогов данных находится непосредственно после следующей главы.

 

Сегменты PE файла

 

Спецификация PE файла состоит из заголовков, описанных ранее, и общих объектов, называемых сегментами. Сегменты содержат собственно содержимое файла, включая код, данные, ресурсы и прочую информацию о PE файле. Каждый сегмент имеет заголовок и тело сегмента ( непосредственно данные ). Заголовки сегментов описаны ниже, однако тела сегментов не имеют жестко заданной структуры. Они организованы почти всегда так, как того захотел конкретный линковщик, поскольку заголовки содержат достаточно информации для дальнейшей обработки данных.

 

Заголовки сегментов

 

Заголовки сегментов размещаются последовательно сразу же за опциональным заголовком PE файла. Каждый заголовок секции имеет длину 40 байт и расположены они без выравнивания. Заголовок сегмента определяется следующей структурой:

WINNT.H

#define IMAGE_SIZEOF_SHORT_NAME              8

typedef struct _IMAGE_SECTION_HEADER {
    UCHAR   Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            ULONG   PhysicalAddress;
            ULONG   VirtualSize;
    } Misc;
    ULONG   VirtualAddress;
    ULONG   SizeOfRawData;
    ULONG   PointerToRawData;
    ULONG   PointerToRelocations;
    ULONG   PointerToLinenumbers;
    USHORT  NumberOfRelocations;
    USHORT  NumberOfLinenumbers;
    ULONG   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
 

Как Вы можете получить информацию из заголовка некоторого сегмента ? Так как заголовки сегментов расположены последовательно без какого либо предопределенного порядка, заголовки сегментов должны идентифицироваться по имени. Следующий пример показывает, как найти заголовок сегмента PE файла по имени, переданному в качестве аргумента:

PEFILE.C

BOOL    WINAPI GetSectionHdrByName (
    LPVOID                   lpFile,
    IMAGE_SECTION_HEADER     *sh,
    char                     *szSection)
{
    PIMAGE_SECTION_HEADER    psh;
    int                      nSections = NumOfSections (lpFile);
    int                      i;


    if ((psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET (lpFile)) !=
         NULL)
        {
        /* найдем заголовок сегмента по имени */
        for (i=0; i<nSections; i++)
            {
            if (!strcmp (psh->Name, szSection))
                {
                /* скопируем данные */
                CopyMemory ((LPVOID)sh,
                            (LPVOID)psh,
                            sizeof (IMAGE_SECTION_HEADER));
                return TRUE;
                }
            else
                psh++;
            }
        }

    return FALSE;
} 

Функция просто находит первый заголовок сегмента с помощью макроса SECHDROFFSET. Затем функция организует цикл через все заголовки сегментов, сравнивая имя очередного сегмента с именем сегмента, который нужно найти. Если сегмент найден, функция копирует данные из файла, отображенного в память, в структуру, переданную как аргумент в эту функцию. После этого поля структуры IMAGE_SECTION_HEADER могут быть доступны непосредственно из этой структуры.

 

Поля заголовка сегмента

 

 

Name. Каждый заголовок сегмента имеет свое имя, поле Name, длиной до 8 символов, в котором первым символом должна быть точка "."
PhysicalAddress или VirtualSize. Второе поле - уния, в настоящий момент не используется.
VirtualAddress. Это поле содержит виртуальный адрес в адресном пространстве процесса, в который загружается сегмент. Действительный адрес создается из значения этого поля, добавленного к виртуальному адресу ImageBase в структуре опционального заголовка. Однако помните, что если файл является DLL, нет гарантии того, что DLL будет загружена по адресу, указанному в поле ImageBase. Так что после загрузки файла, действительное значение ImageBase нужно проверить программно с помощью функции GetModuleHandle.
SizeOfRawData. Это поле содержит размер тела сегмента, связанный с полем FileAlignment. Действительный размер тела сегмента будет числом, меньшим или равным ближайшему числу, кратному полю FileAlignment. После загрузки файла в адресное пространство процесса, размер тела сегмента становится меньшим или равным ближайшему числу, кратному полю SectionAlignment.
PointerToRawData. Это смещение на собственно тело сегмента в файле.
PointerToRelocations, PointerToLinenumbers, NumberOfRelocations, NumberOfLinenumbers. Ни одно из этих полей не используется.
Characteristics. Определяет характеристики сегмента. Значения этого поля могут быть найдены в файле заголовков WINNT.H.3)

 

 

Значение Описание
0x00000020 Сегмент кода
0x00000040 Сегмент инициализированных данных
0x00000080 Сегмент неинициализированных данных
0x04000000 Сегмент не может быть кэширован
0x08000000 Сегмент не может быть поделен на страницы
0x10000000 Сегмент раздеяют все процессы, загрузившие данный файл
0x20000000 Сегмент может быть исполнен
0x40000000 Сегмент может быть прочитан
0x80000000 В сегмент может быть осуществлена запись

 

 

Местоположение каталогов данных

 

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

Для облегчения этой процедуры, была написана следующая функция для определения местоположения любого каталога, определенного в файле WINNT.H:

PEFILE.C

LPVOID  WINAPI ImageDirectoryOffset (
        LPVOID    lpFile,
        DWORD     dwIMAGE_DIRECTORY)
{
    PIMAGE_OPTIONAL_HEADER   poh;
    PIMAGE_SECTION_HEADER    psh;
    int                      nSections = NumOfSections (lpFile);
    int                      i = 0;
    LPVOID                   VAImageDir;

    /* должен быть от 0 до (NumberOfRvaAndSizes-1). */
    if (dwIMAGE_DIRECTORY >= poh->NumberOfRvaAndSizes)
        return NULL;

    /* Получим смещения опционального заголовка и заголовка сегмента */
    poh = (PIMAGE_OPTIONAL_HEADER)OPTHDROFFSET (lpFile);
    psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET (lpFile);

    /* Найдем относительный виртуальный адрес каталога */
    VAImageDir = (LPVOID)poh->DataDirectory
                       [dwIMAGE_DIRECTORY].VirtualAddress;

    /* Найдем сегмент, содержащий нужный каталог */
    while (i++<nSections)
        {
        if (psh->VirtualAddress <= (DWORD)VAImageDir &&
            psh->VirtualAddress +
                 psh->SizeOfRawData > (DWORD)VAImageDir)
            break;
        psh++;
        }

    if (i > nSections)
        return NULL;

    /* Вернем смещение на каталог данных */
    return (LPVOID)(((int)lpFile +
                     (int)VAImageDir. psh->VirtualAddress) +
                    (int)psh->PointerToRawData);
}
 

Функция сначала проверяет, находится ли индекс на затребованный каталог данных в допустимом диапазоне. Затем она получает указатели на опциональный заголовок и на заголовок первого сегмента. Из опционального заголовка функция определяет виртуальный адрес каталога, и использует его для определения сегмента, в теле которого размещается каталог данных. После идентификации тела соответствующего сегмента, местоположение каталога данных находится переводом относительного виртуального адреса каталога данных в его смещение в файле

 

Предопределенные сегменты

 

Приложения под Windows NT обычно имеют 9 предопределенных сегментов, называемых .text, .bss, .rdata, .data, .rsrc, .edata, .idata, .pdata, и .debug. Некоторым приложениям не нужны все из этих сегментов, в то время как другие могут определять дополнително к этим и свои собственные сегменты для удовлетворения некоторых специфических требований. Это похоже на сегменты кода и данных в MS-DOS и Windows версии 3.1. На самом деле, способ, которым приложение определяет уникальные сегменты, используя стандартные директивы компилятора для именования сегментов данных и кода или используя опцию именования сегментов компиллятора -NT - является тем же самым, с помощью которого приложения определяли уникальные сегменты кода и данных в версии 3.1.

Ниже описываются несколько наиболее интересных сегментов, общих для типичных PE файлов Windows NT

 

Сегмент исполнимого кода, .text

 

Единственная разница между Windows версии 3.1 and Windows NT - то, что в Windows NT все сегменты кода ( как они назывались в Windows весрии 3.1 ) комбинируются в один сегмент кода, называемый ".text". Так как Windows NT использует страничную организацию виртуальной памяти, нет преимуществ в разделении кода на несколько ограниченных сегментов кода. Более того, наличие только одного сегмента кода облегчает жизнь и операционной системе, и разработчику приложений.

Сегмент .text также содержит точку входа, упоминавшуюся ранее. Также непосредственно перед точкой входа расположена таблица импортируемых адресов (IAT). ( Наличие IAT в сегменте кода имеет смысл, потому что эта таблица представляет собой лишь серию инструкций jump, адреса которых фиксированы и известны ). Когда исполнимый файл Windows NT загружается в адресное пространство процесса, IAT заполняется действительными адресами импортируемых функций. Чтобы найти IAT в загружаемом файле, загрузчик просто находит точку входа и основывается на факте, что IAT расположена непосредственно перед точкой входа. Поскольку все элементы в этой таблице одинакового размера, не составляет труда пройтись по ее элементам ,чтобы найти ее начало.

 

Сегменты данных, .bss, .rdata, .data

 

Сегмент .bss представляет неинициализированные данные приложения, включая все переменные, декларированные как статические во всех функциях или модулях.

Сегмент .rdata представляет данные только для чтения, такие, как строки, константы и информацию отладочного каталога.

Все другие переменные ( за исключением автоматических, которые создаются на стеке ) хранятся в сегменте .data. В основном, это глобальные переменные приложения и отдельных модулей.

 

Сегмент ресурсов, .rsrc

 

Сегмент .rsrc содержит информацию о ресурсах приложения. Он начинается с каталога ресурсов, как и большинство других сегментов, но этот каталог структурирован в виде дерева ресурсов. Структура IMAGE_RESOURCE_DIRECTORY, приведенная ниже, формирует корень и ветви дерева:

WINNT.H

typedef struct _IMAGE_RESOURCE_DIRECTORY {
    ULONG   Characteristics;
    ULONG   TimeDateStamp;
    USHORT  MajorVersion;
    USHORT  MinorVersion;
    USHORT  NumberOfNamedEntries;
    USHORT  NumberOfIdEntries;
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
 

Посмотрев на структуру каталога, Вы не найдете никакого указателя на следующий узел. Вместо этого, есть два поля, NumberOfNamedEntries и NumberOfIdEntries, используемые для указания количества элементов, содержащихся в каталоге. Под словом содержащихся я имел в виду, что элементы каталога расположены непосредственно после самого каталога в теле сегмента. Именованные элементы расположены первыми и упорядочены в алфавитном порядке, далее следуют элементы, идентифицируемые целым числом, упорядоченные по своему идентификатору.

Элемент каталога состоит из двух полей, как описывается нижеприведенной структурой IMAGE_RESOURCE_DIRECTORY_ENTRY:

WINNT.H

typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
    ULONG   Name;
    ULONG   OffsetToData;
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
 

Эти два поля используются для различных вещей, в зависимости от уровня дерева. Поле Name используется для идентификации типа ресурса, имени ресурса либо языка ресурса. Поле OffsetToData всегда используется для указания на потомка, либо в ветви дерева, либо в конечном узле.

Конечные узлы - это наинизшие узлы в дереве ресурсов. Они определяют размер и местоположение непосредственно данных ресурса. Каждый конечный узел представляет собой структуру IMAGE_RESOURCE_DATA_ENTRY:

WINNT.H

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
    ULONG   OffsetToData;
    ULONG   Size;
    ULONG   CodePage;
    ULONG   Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
  

Два поля OffsetToData и Size содержат местоположение и размер непосредственно данных ресурса. Так как эта информация используется по большей частью функциями после загрузки приложения, удобнее было сделать поле OffsetToData относительным виртуальным адресом. Это именно такой случай. Достаточно интересно, что все другие смещения, такие, как указатели из элемента каталога на другие каталоги, являются смещениями относительно адреса корневого элемента.

Основные типы ресурсов определены в файле заголовков WINUSER.H и приводятся ниже:

WINUSER.H

/*
 * Предопределенные типы ресурсов
 */
#define RT_CURSOR           MAKEINTRESOURCE(1)
#define RT_BITMAP           MAKEINTRESOURCE(2)
#define RT_ICON             MAKEINTRESOURCE(3)
#define RT_MENU             MAKEINTRESOURCE(4)
#define RT_DIALOG           MAKEINTRESOURCE(5)
#define RT_STRING           MAKEINTRESOURCE(6)
#define RT_FONTDIR          MAKEINTRESOURCE(7)
#define RT_FONT             MAKEINTRESOURCE(8)
#define RT_ACCELERATOR      MAKEINTRESOURCE(9)
#define RT_RCDATA           MAKEINTRESOURCE(10)
#define RT_MESSAGETABLE     MAKEINTRESOURCE(11)
 

На верхнем уровне дерева, значения MAKEINTRESOURCE, показанные выше, помещаются в поле Name соответствующего элемента, идентифицируя тип ресурса.

Каждый элемент корневого каталога указывает на потомка во втором уровне дерева. Последние также являются каталогами, содержащие собственные элементы. На этом уровне каталоги используются для идентификации имен ресурсов данного типа. Если бы Вы имели несколько меню в своем приложении, для каждого из них на втором уровне каталога было бы по одному элементу.

Как Вы, вероятно, могли заметить, ресурсы могут быть идентифицированы либо по имени, либо по целому числу. Они различаются на этом уровне иерархии с помощью поля Name в элементе каталога. Если самый значащий бит поля Name установлен, оставшиеся 31 бит используются как смещение на структуру IMAGE_RESOURCE_DIR_STRING_U:

WINNT.H

typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
    USHORT  Length;
    WCHAR   NameString[ 1 ];
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
  

Эта структура содержит 2-х байтовое поле длины имени Length, и следующее за ним имя NameString в кодировке UNICODE.

Если же самый значащий бит поля Name сброшен, оставшиеся 31 бит используются как целый идентификатор ресурса. Рисунок 2 показывает ресурс меню как именованный ресурс и строковую таблицу как ID ресурс.

Если бы у нас было два ресурса меню, один идентифицируемый по имени, а другой - по целому числу, в этом каталоге ресурсов появились бы два элемента. Именованый ресурс располагался бы первым, а за ним - ресурс, идентифицируемый целым числом. Поля каталога NumberOfNamedEntries и NumberOfIdEntries содержали бы значение 1, означающее наличие одного элемента соответствующего типа идентификации.

Ниже второго уровня дерево ресурсов более не ветвится. Первый уровень ветвей каталога представляет тип ресурса, второй уровень - идентификаторы объектов ресурсов. Третий уровень представляет собой соответствие между индивидуально идентифицированным ресурсом и его соответствующим ID языка. Для обозначения языка ресурса используется поле Name элемента каталога, причем оно идентифицирует и главный язык, и ID диалекта языка ресурса. Win32 SDK для Windows NT содержит перечень значений по умолчанию языков ресурсов. Для значения 0x0409, 0x09 означает главный язык LANG_ENGLISH, а 0x04 определен как диалект SUBLANG_ENGLISH_CAN. Весь набор идентификаторов языков определен в файле заголовков WINNT.H, составной части Win32 SDK для Windows NT.

Так как элемент идентификации языка является последним элементом в дереве, поле OffsetToData этой структуры является смещением на конечный узел - структуру IMAGE_RESOURCE_DATA_ENTRY, упоминавшуюся ранее.

Возвращаясь назад к рисунку 2, Вы можете увидеть по одному элементу данных на каждый узел в директории языков. Этот элемент содержит просто размер данных ресурса и относительный виртуальный адрес, по которому они расположены.

Одно из преимуществ в существовании столь сложной структуры в сегменте ресурсов .rsrc - это то, что Вы можете манипулировать различной информацией из сегмента без обращения непосредственно к данным ресурсов. Например, Вы можете подсчитать, сколько у Вас ресурсов каждого типа, сколько ресурсов ( и есть ли такие ) используют некоторый язык, существует ли какой-либо ресурс, и каков размер ресурсов некоторого типа. Для демонстрации использования такого рода информации, следующая функция показывает, как определить тип различных ресурсов, включенных в файл:

PEFILE.C

int     WINAPI GetListOfResourceTypes (
    LPVOID    lpFile,
    HANDLE    hHeap,
    char      **pszResTypes)
{
    PIMAGE_RESOURCE_DIRECTORY          prdRoot;
    PIMAGE_RESOURCE_DIRECTORY_ENTRY    prde;
    char                               *pMem;
    int                                nCnt, i;


    /* Получить корневой католог дерева ресурсов */
    if ((prdRoot = PIMAGE_RESOURCE_DIRECTORY)ImageDirectoryOffset
           (lpFile, IMAGE_DIRECTORY_ENTRY_RESOURCE)) == NULL)
        return 0;

    /* Выделим достаточно памяти для хранения ресурсов всех типов */
    nCnt = prdRoot->NumberOfIdEntries * (MAXRESOURCENAME + 1);
    *pszResTypes = (char *)HeapAlloc (hHeap,
                                      HEAP_ZERO_MEMORY,
                                      nCnt);
    if ((pMem = *pszResTypes) == NULL)
        return 0;

    /* Установим указатель на элементы первого типа  */
    prde = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)((DWORD)prdRoot +
               sizeof (IMAGE_RESOURCE_DIRECTORY));

    /* Цикл по всем типам ресурсов в каталоге ресурсов */
    for (i=0; i<prdRoot->NumberOfIdEntries; i++)
        {
        if (LoadString (hDll, prde->Name, pMem, MAXRESOURCENAME))
            pMem += strlen (pMem) + 1;

        prde++;
        }

    return nCnt;
}
 

Эта функция возвращает список имен типов ресурсов в строке pszResTypes. Заметьте, что в сердце этой функции вызывается функция LoadString с аргументом Name как строковый идентификатор для каждого типа ресурсов в каталоге ресурсов . Если Вы посмотрите файл PEFILE.RC, вы обнаружите, что я определил серию строк типов ресурсов, идентификаторы ID которых определены так же, как спецификаторы типов в каталоге ресурсов. Также в библиотеке PEFILE.DLL есть функция, возвращающая общее число объектов ресурсов в сегменте .rsrc. Также довольно легко написать подобную функцию или любую другую функцию, извлекающую прочую информацию из этого сегмента.

 

Сегмент экспортируемых данных, .edata

 

Сегмент .edata содержит экспортируемые данные для приложения или DLL. Когда он присутствует, этот сегмент содержит каталог для манипуляций информацией об экспортируемых данных

WINNT.H

typedef struct _IMAGE_EXPORT_DIRECTORY {
    ULONG   Characteristics;
    ULONG   TimeDateStamp;
    USHORT  MajorVersion;
    USHORT  MinorVersion;
    ULONG   Name;
    ULONG   Base;
    ULONG   NumberOfFunctions;
    ULONG   NumberOfNames;
    PULONG  *AddressOfFunctions;
    PULONG  *AddressOfNames;
    PUSHORT *AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
 

Поле Name в этом каталоге идентифицирует имя исполнимого модуля. Поля NumberOfFunctions и NumberOfNames содержат количество функций и имен функций, экспортируемых из модуля

Поле AddressOfFunctions - это смещение на список указателей экспортируемых функций. Поле AddressOfNames указывает на начало списка имен экспортируемых функций, разделенных нулями. AddressOfNameOrdinals - смещение на список 2-х байтовых целых чисел - номеров экспортируемых функций.

Эти три AddressOf... поля - относительные виртуальные адреса в адресном пространстве процесса, в которое был загружен данный файл. После загрузки модуля, относительные виртуальные адреса нужно увеличить на базовый адрес загрузки модуля для получения точных адресов в адресном пространстве загрузившего процесса. Однако, перед загрузкой файла, эти адреса могут быть определены вычитанием из соответствующего поля AddressOf... виртуального адреса заголовка сегмента (VirtualAddress) и добавлением к результату смещения на тело сегмента (PointerToRawData) - полученное значение можно использовать как смещение в файле. Следующий пример иллюстрирует эту технику:

PEFILE.C

int  WINAPI GetExportFunctionNames (
    LPVOID    lpFile,
    HANDLE    hHeap,
    char      **pszFunctions)
{
    IMAGE_SECTION_HEADER       sh;
    PIMAGE_EXPORT_DIRECTORY    ped;
    char                       *pNames, *pCnt;
    int                        i, nCnt;

    /* Получим заголовок секции и указатель на каталог данных
       для сегмента .edata */
    if ((ped = (PIMAGE_EXPORT_DIRECTORY)ImageDirectoryOffset
            (lpFile, IMAGE_DIRECTORY_ENTRY_EXPORT)) == NULL)
        return 0;
    GetSectionHdrByName (lpFile, &sh, ".edata");

    /* Определим смещение на имена экспортируемых функций */
    pNames = (char *)(*(int *)((int)ped->AddressOfNames -
                               (int)sh.VirtualAddress   +
                               (int)sh.PointerToRawData +
                               (int)lpFile)    -
                      (int)sh.VirtualAddress   +
                      (int)sh.PointerToRawData +
                      (int)lpFile);

    /* Подсчет размера памяти для размещения всех строк. */
    pCnt = pNames;
    for (i=0; i<(int)ped->NumberOfNames; i++)
        while (*pCnt++);
    nCnt = (int)(pCnt. pNames);

    /* Выделение памяти для имен функций. */
    *pszFunctions = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nCnt);

    /* Копируем все строки в выделенный буфер. */
    CopyMemory ((LPVOID)*pszFunctions, (LPVOID)pNames, nCnt);

    return nCnt;
}
 

Заметьте, что в этой функции переменной pNames присваивается сначала адрес смещения, а уже затем действительное смещение. Оба они, и адрес смещения, и собственно смещение, являются относительными виртуальными адресами и должны быть странслированы в действительный адрес, как показано в этой функции. Вы могли бы написать подобную функцию для определения целых идентификаторов или адресов функций, но зачем зря напрягаться, если я уже все сделал для Вас ? Функции GetNumberOfExportedFunctions, GetExportFunctionEntryPoints, и GetExportFunctionOrdinals уже есть в библиотеке PEFILE.DLL.

 

Сегмент импортируемых данных, .idata

 

Сегмент .idata содержит импортируемые данные, включая каталог импортируемых данных и таблицу имен импортируемых адресов. Несмотря на то, что определен каталог IMAGE_DIRECTORY_ENTRY_IMPORT, соответствующая структура каталога не включена в файл WINNT.H. Вместо нее есть несколько других структур, названных IMAGE_IMPORT_BY_NAME, IMAGE_THUNK_DATA, и IMAGE_IMPORT_DESCRIPTOR. Я лично не смог определить, как приведенные структуры связаны с сегментом .idata, так что я потратил несколько часов, разбираясь в теле сегмента .idata, и в результате определил собственную структуру, намного проще. Я назвал эту структуру IMAGE_IMPORT_MODULE_DIRECTORY.

PEFILE.H

typedef struct tagImportDirectory
    {
    DWORD    dwRVAFunctionNameList;
    DWORD    dwUseless1;
    DWORD    dwUseless2;
    DWORD    dwRVAModuleName;
    DWORD    dwRVAFunctionAddressList;
    }IMAGE_IMPORT_MODULE_DIRECTORY,
     * PIMAGE_IMPORT_MODULE_DIRECTORY;
 

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

Одно из полей структуры IMAGE_IMPORT_MODULE_DIRECTORY - dwRVAModuleName - относительный виртуальный адрес, указывающий на имя модуля. Также в этой структуре есть два бесполезных поля dwUseless, служащих для заполнения места, так что структура остается правильно выровненной внутри сегмента. Спецификация формата PE файлов упоминает иногда о флагах импорта ( метка даты/времени и страшая/младшая части версии ), но эти поля оставались незаполненными во время моих экспериментов, поэтому я считаю их бесполезными.

Основываясь на определении этой структуры, Вы можете извлечь имена модулей и всех функций в конкретном модуле, импортируемых в данный исполнимый файл. Следующая функция показывает, как извлечь имена всех импортируемых данным PE файлом модулей:

PEFILE.C

int  WINAPI GetImportModuleNames (
    LPVOID    lpFile,
    HANDLE    hHeap,
    char      **pszModules)
{
    PIMAGE_IMPORT_MODULE_DIRECTORY  pid;
    IMAGE_SECTION_HEADER            idsh;
    BYTE                            *pData;
    int                             nCnt = 0, nSize = 0, i;
    char                            *pModule[1024];
    char                            *psz;

    pid = (PIMAGE_IMPORT_MODULE_DIRECTORY)ImageDirectoryOffset
             (lpFile, IMAGE_DIRECTORY_ENTRY_IMPORT);
    pData = (BYTE *)pid;

    /* Найдем местоположение заголовка сегмента ".idata". */
    if (!GetSectionHdrByName (lpFile, &idsh, ".idata"))
        return 0;

    /* Извлечем все импортируемые модули. */
    while (pid->dwRVAModuleName)
        {
        /* Разместим буфер для абсолютных смещений строк. */
        pModule[nCnt] = (char *)(pData +
               (pid->dwRVAModuleName-idsh.VirtualAddress));
        nSize += strlen (pModule[nCnt]) + 1;

        /* Увеличим указатель на следующий элемент каталога
	   импортируемых данных. */
        pid++;
        nCnt++;
        }

    /* Скопируем все строки в один большой кусок памяти. */
    *pszModules = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nSize);
    psz = *pszModules;
    for (i=0; i<nCnt; i++)
        {
        strcpy (psz, pModule[i]);
        psz += strlen (psz) + 1;
        }

    return nCnt;
}
 

Эта функция достаточно прямолинейна. Однако, одна вещь требует пояснения - присмотритесь к циклу while. Этот цикл завершается, когда выражение pid->dwRVAModuleName становится равным 0. Это означает, что в конце списка структур IMAGE_IMPORT_MODULE_DIRECTORY находится нулевая структура, содержащая 0 как минимум в поле dwRVAModuleName. Я выяснил такое поведение во время моих экспериментов, и позже оно подтвердилось в спецификации формата PE файлов.

Первое поле структуры, dwRVAFunctionNameList - относительный виртуальный адрес списка относительных виртуальных адресов, каждый из которых указывает на имя функции в файле. Как показано в следующем дампе, имена модулей и имена всех импортируемых функций расположены в сегменте .idata:

E6A7 0000 F6A7 0000  08A8 0000 1AA8 0000  ................
28A8 0000 3CA8 0000  4CA8 0000 0000 0000  (...<...L.......
0000 4765 744F 7065  6E46 696C 654E 616D  ..GetOpenFileNam
6541 0000 636F 6D64  6C67 3332 2E64 6C6C  eA..comdlg32.dll
0000 2500 4372 6561  7465 466F 6E74 496E  ..%.CreateFontIn
6469 7265 6374 4100  4744 4933 322E 646C  directA.GDI32.dl
6C00 A000 4765 7444  6576 6963 6543 6170  l...GetDeviceCap
7300 C600 4765 7453  746F 636B 4F62 6A65  s...GetStockObje
6374 0000 D500 4765  7454 6578 744D 6574  ct....GetTextMet
7269 6373 4100 1001  5365 6C65 6374 4F62  ricsA...SelectOb
6A65 6374 0000 1601  5365 7442 6B43 6F6C  ject....SetBkCol
6F72 0000 3501 5365  7454 6578 7443 6F6C  or..5.SetTextCol
6F72 0000 4501 5465  7874 4F75 7441 0000  or..E.TextOutA..
  

Этот дамп взят из сегмента .idata файла приложения-примера EXEVIEW.EXE. Этот конкретный сегмент представляет собой начало списка импортируемых модулей и имен функций. Если Вы проверите правую часть дампа, Вы можете заметить, что строки напоминают имена знакомых функций Win32 API и названия модулей, в которых они расположены. Просматривая сверху вниз, Вы найдете функцию GetOpenFileNameA и далее имя модуля - COMDLG32.DLL, в котором находится эта функция. Сразу же после них, Вы найдете функцию CreateFontIndirectA и имя модуля GDI32.DLL, затем - функции GetDeviceCaps, GetStockObject, GetTextMetrics и так далее.

Такая структура сохраняется на протяжении всего сегмента .idata. Имя первого модуля - COMDLG32.DLL, второго - GDI32.DLL. Заметьте, что только одна функция импортируется из первого модуля, в то время, как множество функций импортируется из второго модуля. В обоих случаях имена функций и имена модулей, к которым они относятся, упорядочены так, что сначала идет имя функции, далее имя модуля, и далее оставшиеся имена функций из этого модуля ( если есть ).

Следующая функция показывает, как извлечь имена функций для указанного модуля:

PEFILE.C

int  WINAPI GetImportFunctionNamesByModule (
    LPVOID    lpFile,
    HANDLE    hHeap,
    char      *pszModule,
    char      **pszFunctions)
{
    PIMAGE_IMPORT_MODULE_DIRECTORY  pid;
    IMAGE_SECTION_HEADER     idsh;
    DWORD                    dwBase;
    int                      nCnt = 0, nSize = 0;
    DWORD                    dwFunction;
    char                     *psz;


    /* Найдем заголовок сегмента ".idata". */
    if (!GetSectionHdrByName (lpFile, &idsh, ".idata"))
        return 0;

    pid = (PIMAGE_IMPORT_MODULE_DIRECTORY)ImageDirectoryOffset
             (lpFile, IMAGE_DIRECTORY_ENTRY_IMPORT);

    dwBase = ((DWORD)pid. idsh.VirtualAddress);

    /* Найдем идентификатор модуля. */
    while (pid->dwRVAModuleName &&
           strcmp (pszModule,
                  (char *)(pid->dwRVAModuleName+dwBase)))
        pid++;

    /* Возврат, если модуль не найден. */
    if (!pid->dwRVAModuleName)
        return 0;

    /* Подсчет числа имен функций и их длин. */
    dwFunction = pid->dwRVAFunctionNameList;
    while (dwFunction                      &&
           *(DWORD *)(dwFunction + dwBase) &&
           *(char *)((*(DWORD *)(dwFunction + dwBase)) +
            dwBase+2))
        {
        nSize += strlen ((char *)((*(DWORD *)(dwFunction +
             dwBase)) + dwBase+2)) + 1;
        dwFunction += 4;
        nCnt++;
        }

    /* Выделим память для имен функций. */
    *pszFunctions = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nSize);
    psz = *pszFunctions;

    /* Скопируем имена функций в выделенную память. */
    dwFunction = pid->dwRVAFunctionNameList;
    while (dwFunction                      &&
           *(DWORD *)(dwFunction + dwBase) &&
           *((char *)((*(DWORD *)(dwFunction + dwBase)) +
            dwBase+2)))
        {
        strcpy (psz, (char *)((*(DWORD *)(dwFunction + dwBase)) +
                dwBase+2));
        psz += strlen((char *)((*(DWORD *)(dwFunction + dwBase))+
                dwBase+2)) + 1;
        dwFunction += 4;
        }

    return nCnt;
}
 

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

Последнее поле, dwRVAFunctionAddressList - относительный виртуальный адрес списока виртуальных адресов, которые будут помещены в сегмент данных при загрузке файла. Однако, до загрузки файла эти виртуальные адреса заменяются относительными виртуальными адресами, в точности соответствующими списку имен импортируемых функций. Так что перед загрузкой файла будет существовать два одинаковых списка относительных виртуальных адресов, указывающих на имена импортируемых функций.

 

Сегмент отладочной информации, .debug

 

Отладочная информация первоначально помещается в сегмент .debug. Формат PE файлов также поддерживает отдельные отладочные файлы ( обычно имеющие расширение .DBG ), содержащие централизованно всю отладочную информацию. Отладочный сегмент содержит информацию для отладки, однако каталоги отладочной информации расположены в сегменте .rdata, описанном выше. Каждый из этих каталогов ссылается на отладочную информацию в сегменте .debug. Структура IMAGE_DEBUG_DIRECTORY каталога отладочной информации определена ниже:

WINNT.H

typedef struct _IMAGE_DEBUG_DIRECTORY {
    ULONG   Characteristics;
    ULONG   TimeDateStamp;
    USHORT  MajorVersion;
    USHORT  MinorVersion;
    ULONG   Type;
    ULONG   SizeOfData;
    ULONG   AddressOfRawData;
    ULONG   PointerToRawData;
} IMAGE_DEBUG_DIRECTORY, *PIMAGE_DEBUG_DIRECTORY;
 

Сегмент разделен на отдельные части данных, представляющих различные типы отладочной информации. Для каждого из них имеется каталог отладочной информации, описанный выше. Ниже приведены различные типы отладочной информации:

WINNT.H

#define IMAGE_DEBUG_TYPE_UNKNOWN          0
#define IMAGE_DEBUG_TYPE_COFF             1
#define IMAGE_DEBUG_TYPE_CODEVIEW         2
#define IMAGE_DEBUG_TYPE_FPO              3
#define IMAGE_DEBUG_TYPE_MISC             4
  

Поле Type в каждом каталоге определяет, какой тип отладочной информации содержится в данном каталоге. Как Вы можете видеть из вышеприведенного списка, формат PE файла поддерживает множество различных типов отладочной информации, так же как и некоторые другие информационные поля. Один из типов, IMAGE_DEBUG_TYPE_MISC - уникален. Этот тип был добавлен, чтобы содержать различную информацию об исполнимом файле, которая не может быть отнесена к каким-либо более структурированным сегментам данных в PE файле. Это единственное место во всем PE файле, где обязательно должно быть имя самого файла. Если есть экспортируемые данные, то имя файла будет включено и в сегмент экспортируемых данных.

Каждый тип отладочной информации имеет собственную структуру заголовка, определяющего его данные. Каждый из них определен в файле заголовков WINNT.H. Одно из приятных обстоятельств - то, что два поля структуры IMAGE_DEBUG_DIRECTORY идентифицируют отладочную информацию. Первое из них, AddressOfRawData - относительный виртуальный адрес данных после загрузки файла. Другое, PointerToRawData - это смещение в PE файле, по которому находятся эти данные. Это значительно облегчает извлечение любой отладочной информации.

В качестве последнего примера, рассмотрим следующую функцию, извлекающую имя файла из структуры IMAGE_DEBUG_MISC:

PEFILE.C

int    WINAPI RetrieveModuleName (
    LPVOID    lpFile,
    HANDLE    hHeap,
    char      **pszModule)
{

    PIMAGE_DEBUG_DIRECTORY    pdd;
    PIMAGE_DEBUG_MISC         pdm = NULL;
    int                       nCnt;

    if (!(pdd = (PIMAGE_DEBUG_DIRECTORY)ImageDirectoryOffset
               (lpFile, IMAGE_DIRECTORY_ENTRY_DEBUG)))
        return 0;

    while (pdd->SizeOfData)
        {
        if (pdd->Type == IMAGE_DEBUG_TYPE_MISC)
            {
            pdm = (PIMAGE_DEBUG_MISC)
                ((DWORD)pdd->PointerToRawData + (DWORD)lpFile);

            nCnt = lstrlen (pdm->Data)*(pdm->Unicode?2:1);
            *pszModule = (char *)HeapAlloc (hHeap,
                                            HEAP_ZERO_MEMORY,
                                            nCnt+1;
            CopyMemory (*pszModule, pdm->Data, nCnt);

            break;
            }

        pdd ++;
        }

    if (pdm != NULL)
        return nCnt;
    else
        return 0;
}
 

Как Вы можете видеть, структура отладочного каталога позволяет относительно легко обращаться к любой отладочной информации. После получения структуры IMAGE_DEBUG_MISC, извлечение имени файла настолько же просто, как вызов функции CopyMemory.

Как упоминалось выше, отладочная информация может быть перемещена в отдельный .DBG файл. Windows NT SDK включает утилиту REBASE.EXE, служащую для этих целей. Например, следующая команда удалит отладочную информацию из исполнимого файла TEST.EXE:

rebase -b 40000 -x c:\samples\testdir test.exe

Отладочная информация будет помещена в новый файл TEST.DBG, расположенный в указанном каталоге, в данном случае в c:\samples\testdir. В начале этого файла расположена структура IMAGE_SEPARATE_DEBUG_HEADER, за ней - копии заголовков сегментов, существующих в новом исполнимом файле. За заголовками сегментов расположены данные сегмента .debug. Таким образом, за заголовками сегментов расположена серия структур IMAGE_DEBUG_DIRECTORY и соответствующие им данные. Собственно отладочная информация сохраняет ту же структуру, что и описанная выше отладочная информация обычного PE файла.

 

Краткое изложение формата PE файла

 

Формат PE файлов в Windows NT представляет совершенно новую структуру для разработчиков, знакомых со средой Windows и MS-DOS. В то же время разработчики, знакомые со средой UNIX, найдут, что формат PE похож, если не полностью соответствует, спецификации COFF

Собственно формат состоит из MZ заголовка MS-DOS, программы реального режима, сигнатуры PE файла, заголовка PE файла, опционального заголовка, заголовков всех сегментов, и, наконец, из тел сегментов.

Опциональный заголовок заканчивается массивом каталогов данных, являющихся относительными виртуальными адресами на каталоги данных, содержащихся в телах сегментов. Каждый каталог данных идентифицирует, как структурированы данные в теле конкретного сегмента

Файлы PE формата имеют одиннадцать предопределенных сегментов, являющихся общими для приложений Windows NT, но любое приложение может определять собственные сегменты для кода и данных.

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

 

Описания функций PEFILE.DLL

 

Библиотека PEFILE.DLL состоит главным образом из функций, которые либо считают смещения в PE файле, либо копируют некую информацию из файла в указанные структуры. Всем функциям первым аргументом должен передаваться указатель на начало файла - исполнимый файл должен быть отображен в память, и его базовый адрес передается в lpFile, первом аргументе каждой функции.

Имена функций сделаны значащими, и каждая функция приводится с кратким комментарием, объясняющим ее использование. Если после просмотра списка функций Вы не сможете определить, какая функция для чего используется, посмотрите на приложение-пример EXEVIEW.EXE, там вы найдете практическое применение этих функций. Нижеприведенный список функций также может быть найден в файле заголовков PEFILE.H:

PEFILE.H

/* Вычисляет смещение на MZ заголовок MS-DOS. */
BOOL WINAPI GetDosHeader (LPVOID, PIMAGE_DOS_HEADER);

/* Определяет тип .EXE файла. */
DWORD WINAPI ImageFileType (LPVOID);

/* Вычисляет смещение на заголовок PE файла. */
BOOL WINAPI GetPEFileHeader (LPVOID, PIMAGE_FILE_HEADER);

/* Вычисляет смещение на опциональный заголовок .*/
BOOL WINAPI GetPEOptionalHeader (LPVOID,
                                  PIMAGE_OPTIONAL_HEADER);

/* Возвращает адрес точки входа. */
LPVOID WINAPI GetModuleEntryPoint (LPVOID);

/* Возвращает количество сегментов в файле. */
int  WINAPI NumOfSections (LPVOID);

/* Возвращает предпочтительный базовый адрес исполнимого
   файла при его загрузке в адресное пространство процесса. */
LPVOID WINAPI GetImageBase (LPVOID);

/* Определяет местоположение в исполнимом файле указанного
   каталога данных. */
LPVOID WINAPI ImageDirectoryOffset (LPVOID, DWORD);

/* Извлекает имена всех сегментов файла. */
int WINAPI GetSectionNames (LPVOID, HANDLE, char **);

/* Копирует заголовок указанного сегмента. */
BOOL WINAPI GetSectionHdrByName (LPVOID,
                                  PIMAGE_SECTION_HEADER, char *);

/* Возвращает список имен импортируемых модулей,
   разделенных нулевым символом. */
int WINAPI GetImportModuleNames (LPVOID, HANDLE, char  **);

/* Возвращает список имен импортируемых из указанного
   модуля функций, разделенных нулевым символом. */
int WINAPI GetImportFunctionNamesByModule (LPVOID, HANDLE,
                                           char *, char  **);

/* Возвращает список имен экспортируемых функций,
   разделенных нулевым символом. */
int WINAPI GetExportFunctionNames (LPVOID, HANDLE, char **);

/* Возвращает количество экспортируемых функций. */
int WINAPI GetNumberOfExportedFunctions (LPVOID);

/* Возвращает список виртуальных адресов экспортируемых функций. */
LPVOID WINAPI GetExportFunctionEntryPoints (LPVOID);

/* Возвращает список номеров экспортируемых функций. */
LPVOID WINAPI GetExportFunctionOrdinals (LPVOID);

/* Опрелеляет общее число объектов ресурсов. */
int WINAPI GetNumberOfResources (LPVOID);

/* Возвращает список всех типов ресурсов,
   используемых в модуле. */
int WINAPI GetListOfResourceTypes (LPVOID, HANDLE, char **);

/* Определяет, удалена ли из исполнимого файла
   отладочная информация. */
BOOL WINAPI IsDebugInfoStripped (LPVOID);

/* Возвращает имя исполнимого файла. */
int WINAPI RetrieveModuleName (LPVOID, HANDLE, char **);

/* Определяет, является ли файл правильным отладочным файлом. */
BOOL WINAPI IsDebugFile (LPVOID);

/* Возвращает отладочный заголовок из
   отладочного файла. */
BOOL WINAPI GetSeparateDebugHeader(LPVOID,
                                   PIMAGE_SEPARATE_DEBUG_HEADER);
  

В дополнение к вышеприведенным функциям, все макросы, упоминавшиеся ранее, также определены в файле заголовков PEFILE.H. Вот их полный список:

/* Смещение на сигнатуру PE файла                           */
#define NTSIGNATURE(a) ((LPVOID)((BYTE *)a                +  \
                        ((PIMAGE_DOS_HEADER)a)->e_lfanew))

/* Операционные системы MS идентифицируют PE файлы по сигнатуре
   размером dword; заголовок PE файла расположен непосредственно
   после этого dword					     */
#define PEFHDROFFSET(a) ((LPVOID)((BYTE *)a               +  \
                         ((PIMAGE_DOS_HEADER)a)->e_lfanew +  \
                             SIZE_OF_NT_SIGNATURE))

/* Опциональный заголовок - сразу после заголовка PE файла   */
#define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a               +  \
                         ((PIMAGE_DOS_HEADER)a)->e_lfanew +  \
                           SIZE_OF_NT_SIGNATURE           +  \
                           sizeof (IMAGE_FILE_HEADER)))

/* Заголовки сегментов - сразу после опционального заголовка */
#define SECHDROFFSET(a) ((LPVOID)((BYTE *)a               +  \
                         ((PIMAGE_DOS_HEADER)a)->e_lfanew +  \
                           SIZE_OF_NT_SIGNATURE           +  \
                           sizeof (IMAGE_FILE_HEADER)     +  \
                           sizeof (IMAGE_OPTIONAL_HEADER)))
  

Чтобы использовать библиотеку PEFILE.DLL, просто включите файл заголовков PEFILE.H и слинкуйте Ваше приложение с этой библиотекой. Все функции являются самодостаточными, однако, некоторые основываются на информации, возвращаемой другими функциями этой библиотеки. Например, функция GetSectionNames полезна для извлечения имен всех сегментов. В то же время, чтобы быть способным извлечь заголовок некоторого сегмента ( определенного разработчиком приложения во время компилляции ), Вы должны сначала получить список имен сегментов, а затем вызвать функцию GetSectionHeaderByName с точным указанием имени сегмента.

Наслаждайтесь !

1) Как я полагаю, не у всех есть файл WINNT.H, поэтому я взял на себя смелость привести его фрагмент, описывающий все флаги поля Characteristics структуры IMAGE_FILE_HEADER, естественно, с переводом:

#define IMAGE_FILE_RELOCS_STRIPPED           0x0001
	// Информация о переадресации удалена из файла
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002
	// Файл является исполнимым  (т.е. нет неразрешенных внешних ссылок).
#define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004
	// Информация о номерах строк удалена из файла
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED       0x0008
	// Информация о локальных символах удалена из файла
#define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010
	// Agressively trim working set - ? нет мыслей
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080
	// Обратный порядок байт в машинном слове
#define IMAGE_FILE_32BIT_MACHINE             0x0100
	// Слово составляет 32 бита.
#define IMAGE_FILE_DEBUG_STRIPPED            0x0200
	// Отладочная информация удалена их файла
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400
	// Если файл расположен на переносимом носителе, скопировать
	// и запустить из файла подкачки
#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800
	// Если файл расположен на сетевом диске, скопировать
	// и запустить из файла подкачки
#define IMAGE_FILE_SYSTEM                    0x1000
	// Системный файл
#define IMAGE_FILE_DLL                       0x2000
	// Файл является DLL
#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000
	// Файл должен запускаться на UP машине.
	// Что такое "UP машина" - я не знаю
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000
	// Обратный порядок байт в машинном слове
	// (зачем в таком случае IMAGE_FILE_BYTES_REVERSED_LO ?)
Назад

2) Аналогично примечанию 1):

#define IMAGE_SUBSYSTEM_UNKNOWN              0
	// Неизвестная подсистема.
#define IMAGE_SUBSYSTEM_NATIVE               1
	// Файл, не требующий никаких подсистем.
#define IMAGE_SUBSYSTEM_WINDOWS_GUI          2
	// Файл, запускаемый в Windows GUI подсистеме.
#define IMAGE_SUBSYSTEM_WINDOWS_CUI          3
	// Файл, запускаемый в символьной (консольной) подсистеме Windows.
#define IMAGE_SUBSYSTEM_OS2_CUI              5
	// Файл, запускаемый в символьной (консольной) подсистеме OS/2.
#define IMAGE_SUBSYSTEM_POSIX_CUI            7
	// Файл, запускаемый в символьной (консольной) подсистеме Posix..
#define IMAGE_SUBSYSTEM_RESERVED8            8
	// Зарезервировано
Назад

3) В приведенной таблице перечислены отнюдь не все возможные флаги. Ниже я привел все значения, для которых не было сказано, что они "Зарезервированы", или просто закомментированы:

#define IMAGE_SCN_CNT_CODE                   0x00000020
	// Сегмент содержит код
#define IMAGE_SCN_CNT_INITIALIZED_DATA       0x00000040
	// Сегмент содержит инициализированные данные
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA     0x00000080
	// Сегмент содержит неинициализированные данные

#define IMAGE_SCN_LNK_OTHER                  0x00000100
	// Зарезервировано
#define IMAGE_SCN_LNK_INFO                   0x00000200
	// Сегмент содержит комментарии или другой тип информации
#define IMAGE_SCN_LNK_REMOVE                 0x00000800
	// Содержимое сегмент не станет отображаться в адресное пространство
	// процесса
#define IMAGE_SCN_LNK_COMDAT                 0x00001000
	// Сегмент содержит общие данные
#define IMAGE_SCN_MEM_FARDATA                0x00008000
#define IMAGE_SCN_MEM_PURGEABLE              0x00020000
#define IMAGE_SCN_MEM_16BIT                  0x00020000
#define IMAGE_SCN_MEM_LOCKED                 0x00040000
#define IMAGE_SCN_MEM_PRELOAD                0x00080000

#define IMAGE_SCN_ALIGN_1BYTES               0x00100000
#define IMAGE_SCN_ALIGN_2BYTES               0x00200000
#define IMAGE_SCN_ALIGN_4BYTES               0x00300000
#define IMAGE_SCN_ALIGN_8BYTES               0x00400000
#define IMAGE_SCN_ALIGN_16BYTES              0x00500000
	// Выравнивание по умолчанию, если не указаны другие
#define IMAGE_SCN_ALIGN_32BYTES              0x00600000
#define IMAGE_SCN_ALIGN_64BYTES              0x00700000

#define IMAGE_SCN_LNK_NRELOC_OVFL            0x01000000
	// Сегмент содержит информацию для переадресации
#define IMAGE_SCN_MEM_DISCARDABLE            0x02000000
	// Сегмент может быть выгружен
#define IMAGE_SCN_MEM_NOT_CACHED             0x04000000
	// Сегмент не кэшируется
#define IMAGE_SCN_MEM_NOT_PAGED              0x08000000
	// Сегмент не делится на страницы
#define IMAGE_SCN_MEM_SHARED                 0x10000000
	// Сегмент разделяется между всеми процессами, использующими
	// данный файл
#define IMAGE_SCN_MEM_EXECUTE                0x20000000
	// Сегмент может быть исполнен
#define IMAGE_SCN_MEM_READ                   0x40000000
	// Сегмент может быть прочитан
#define IMAGE_SCN_MEM_WRITE                  0x80000000
	// В сегмент может осуществляться запись
// Все прочие флаги зарезервированы
Назад

Дальше