Randy Kath
Резюме
Операционная система 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. Этот пример был
создан для двух целей: Итак, начнем без дальнейших промедлений
Структура PE файла
Формат PE-файлов организован в виде линейного потока данных. Он начинается с заголовка MS-DOS, программы реального режима, и сигнатуры PE файла. Далее следуют заголовок PE-файла и опциональный заголовок. После них идут заголовки всех сегментов, за которыми следуют тела этих сегментов. И ближе к концу файла расположены различные области данных, включая информацию о переадресации, таблицу символов, информацию о номерах строк и данные в таблице строк. Все их легче представить графически, как показано на рисунке 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 файла, операционная
система могла бы просто сбойнуть при
попытке запуска такого файла, и
предложить что-нибудь абсолютно
бесполезное, наподобие: Заголовок 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.
Программа обычно печатает строку
текста, например: Когда линкуется приложение для 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,
чтобы достичь информации в файле. 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, и для них лучше было бы подобрать другие имена.
Дополнительные поля Windows NT
Дополнительные поля, добавленные в формате PE файлов Windows NT, предоставляют поддержку загрузчику для специфичного поведения процессов Windows NT. Ниже следует краткое описание этих полей:
Каталоги данныхКаталоги данных, как определено в файле заголовков 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 могут быть доступны непосредственно из этой структуры.
Поля заголовка сегмента
Местоположение каталогов данных
Каталоги данных расположены в телах соответствующих сегментов. Обычно каталог данных является первой структурой в теле сегмента, однако это не является обязательным. По этой причине, Вы должны использовать информацию как из заголовка сегмента, так и из опционального заголовка для определения местоположения определенного каталога данных. Для облегчения этой процедуры, была написана следующая функция для определения местоположения любого каталога, определенного в файле 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 ?)Назад #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 // В сегмент может осуществляться запись // Все прочие флаги зарезервированыНазад |