Структура исполняемого файла Windows


Оглавление (нажмите, чтобы открыть):

Алгоритм загрузки PE файлов в Windows

Привет всем, кто забрел на эту заметку. Мной будет затронута не самая любимая тема: системное программирование. Дело в том, что волей судьбы я с ним столкнулся, в университете, если быть точнее. Пришлось знатно покопаться в самых низких слоях программирования, алгоритм разгрузки pe файлов оттуда. Надеюсь, что кому-нибудь пригодится мой опыт в этом деле, что вряд ли. Но тем не менее, я им поделюсь.

Если вам не интересен краткий экскурс в теорию работы pe загрузчика, то добро пожаловать сразу к реализации загрузчика PE файлов на C. Конечно же на Си, никаких скриптов, абстракций и прочего высокоуровнего кайфа, только хардкор!

Что такое PE файл

PE( Portable Executable ) это основной формат исполняемых файлов для операционных систем семейства Windows NT. Используется для представления исполняемых файлов (.exe), динамических библиотек (.dll) и драйверов (.sys). Является расширением формата исполняемых файлов для DOS, то есть PE файл является корректной программой для системы DOS, но это нам не интересно. Мы будем запускать PE файлы под Windows собственноручно написанным загрузчиком.

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

Алгоритм загрузки PE файла в Windows

Очень краткое описание алгоритма, достаточное для понимания реализации. Итак, в работе загрузчика можно выделить два основных этапа.

1) физическое копирование секций файла в память;
2) логическая обработка скопированного образа программы.

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

Физическое копирований секций файла в память

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

Действия первого этапа:
1) считать в память DOS-заголовок и определить смещение до NT-заголовка;
2) считать NT-заголовок;
3) определить из NT-заголовка начальный адрес загрузки (ImageBase) и размер образа исполняемого файла(SizeOfImage). ImageBase это адрес, по которому PE файл хочет загрузиться, но на деле редко ему это удается.
4) попытаться выделить память по указанному адресу(ImageBase) указанного размера (VirtualAlloc). Если по этому адресу выделить память не удается, проверить наличие таблицы релоков(таблицы перемещений). Если релоки присутствуют, выделить память по новому адресу с с учетом смещения. Память выделяется с правами для чтения и записи без учета прав доступа, указанных в таблице секций;
5) проверить корректность заголовков и таблицы секций;
6) скопировать данные секций из файла в память;

Логическая обработка скопированного образа программы

Все скопировано в память, пора пробежаться по образу и выполнить необходимые манипуляции вплоть до вызова точки входа.

Действия второго этапа:
1) настроить таблицу релоков, если был изменен адрес загрузки(подробнее в реализации);
2) обработать таблицу импорта, если она присутствует. Для этого необходимо загрузить все библиотеки, которые еще не были загружены. Они грузятся рекурсивно с первого шага первого этапа, следовательно во время их загрузки могут быть загружены другие библиотеки. Каждая библиотека должна быть загружена только один раз, для этого следует вести список загруженных библиотек;
3) в таблице экспорта библиотеки необходимо найти все импортируемые из нее функции. Должны быть найдены все импортируемые функции всех библиотек, иначе процесс загрузки прерывается с ошибкой. При загрузке библиотеки должна вызываться ее точка входа (AddressOfEntryPoint), которая является адресом функции: BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved); Эта функция вызывается при загрузке и перед выгрузкой библиотеки. Во втором параметре передается константа, указывающая причину вызова. В случае библиотеки это DLL_PROCESS_ATTACH . Если она вернула FALSE , значит библиотека загрузилась с ошибкой и следует прерывать процесс загрузки;
4) выставить права доступа в соответствии с указанными в таблице секций(VirtualProtect);
5) передать управление точке входа.

На этом шаги наконец закончились. Если все прошло успешно, наша программа запустится. Естественно система будет ругаться, мы вторгаемся во владения ядра и берем на себя его функции.

Заключение

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

Структура исполняемых файлов в ОС

Обфускаторы

Отладчики

Отладчик или дебаггер (англ. debugger) является модулем среды разработки или отдельным приложением, предназначенным для поиска ошибок в программе. Отладчик позволяет выполнять пошаговую трассировку, отслеживать, устанавливать или изменять значения переменных в процессе выполнения программы, устанавливать и удалять контрольные точки или условия остановки и т. д.

Обфускация (от лат. obfuscare — затенять, затемнять; и англ. obfuscate — делать неочевидным, запутанным, сбивать с толку) или запутывание кода — приведение исходного текста или исполняемого кода программы к виду, сохраняющему ее функциональность, но затрудняющему анализ, понимание алгоритмов работы и модификацию при декомпиляции.

«Запутывание» кода может осуществляться на уровне алгоритма, исходного текста и/или ассемблерного текста. Для создания запутанного ассемблерного текста могут использоваться специализированные компиляторы, использующие неочевидные или недокументированные возможности среды исполнения программы. Существуют также специальные программы, производящие обфускацию, называемые обфускаторами (англ. Obfuscator).

Исполнимый (исполняемый) модуль, исполнимый файл (англ. executable file) — файл, содержащий программу в виде, в котором она может быть (после загрузки в память и настройки по месту) исполнена компьютером.

Чаще всего он содержит двоичное представление машинных инструкций для определённого процессора (по этой причине на программистском сленге в отношении него используют слово бинарник — кальку с английского binary), но может содержать и инструкции на интерпретируемом языке программирования, для исполнения которых требуется интерпретатор. В отношении последних часто используется термин «скрипт».

Исполнением бинарных файлов занимаются аппаратно- и программно-реализованные машины. К первым относятся процессоры — например, семейств x86 или SPARC. Ко вторым — виртуальные машины, например, виртуальная машина Java или .NET Framework. Формат бинарного файла определяется архитектурой исполняющей его машины. Известны машины, реализованные как аппаратно, так и программно, например, процессоры семейства x86 и виртуальная машина VMware.

Статус исполнимости файла чаще всего определяется принятыми соглашениями. Так, в одних операционных системах исполнимые файлы распознаются благодаря соглашению об именовании файлов (например, путём указания в имени расширения файла — .exe или .bin), тогда как в других исполнимые файлы обладают специфичными метаданными (например, битом разрешения execute в UNIX-подобных операционных системах).

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

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

Форматы исполняемых файлов
Windows, DOS и OS/2 .COM • .EXE (MZ / NE / LE / LX / PE)
Unix a.out • COFF • ECOFF • ELF • Mach-O • SOM • XCOFF
Прочие Intel HEX • PEF • SREC

Дата добавления: 2014-10-15 ; Просмотров: 554 ; Нарушение авторских прав? ;

Нам важно ваше мнение! Был ли полезен опубликованный материал? Да | Нет

PE-формат. Часть 1 — Базовая информация

Данная статья является первым маленьким шажком на пути к написанию собственного несложного упаковщика. Знаний предстоит получить весьма большое количество, так что садитесь поудобнее, запасайтесь попкорном и готовьтесь к чтению. В статье большое количество отсылок к MSDN, поэтому не ленитесь открывать и изучать те структуры, на которые я ссылаюсь. Если будете следовать моим простым советам, обучение пойдет гораздо проще. Еще я бы рекомендовал скачать какой-нибудь PE-редактор, например, CFF Explorer и скормить ему реальный PE-файл, чтобы можно было вживую пробежаться по тем структурам, которые я здесь описываю.

PE-формат (Portable Executable) — это формат всех 32- и 64-разрядных исполняемых файлов в ОС Windows. Такой формат имеют файлы exe, dll, ocx, sys и т.д. (Разумеется, exe под DOS не в счет). В этой статье я расскажу самую базовую информацию об устройстве этого формата и его структурах. Практически самое полное и доступно изложенное описание можно найти в статье Криса Касперски, и она является обязательной к прочтению, если вы действительно решили во всем этом разобраться. Помните — никто не говорил, что будет легко, но у вас появился отличный шанс показать, что вы настоящие мужики с железными волосатыми яйцами, разобравшись во всем этом.

Каждый исполняемый файл формата PE состоит из множества взаимосвязанных структур, содержащих информацию о самом файле, об импортируемых и экспортируемых им функциях, о перемещаемых элементах, ресурсах, Thread Local Storage и многое другое. Начнем по порядку. На рисунке ниже приведена обобщенная структура файла PE-формата:

Каждый PE-файл состоит из вышеперечисленных элементов, они являются обязательными. С самого верху находится MS-DOS-заголовок — наследие еще с тех самых времен, когда происходил переход с DOS на Windows, поддерживающий новый PE-формат. Наверняка вы замечали, что все исполняемые файлы начинаются с букв «MZ» — это сигнатура как раз характерна для структуры, названной IMAGE_DOS_HEADER и располагающейся в самом начале PE-файла. Поля этой структуры по большей части нам неинтересны, так как необходимы для запуска из-под DOS. Следует обратить внимание на следующие поля: e_magic — собственно, это поле размером два байта содержит сигнатуру ‘MZ’ (сокращение от имени Марк Збиновски, который являлся ведущим разработчиком MS-DOS и архитектором формата PE); e_lfanew — указатель на начало PE-заголовка (см. рисунок выше). Это поле должно указывать на первый байт PE-заголовка (IMAGE_NT_HEADERS), т.е. на сигнатуру «PE\0\0», причем значение этого поля должно быть выровнено по границе двойного слова. Крис Касперски в своей статье упоминает еще поле e_cparhdr, но, по всей видимости, оно по-прежнему никем не проверяется.

Весь смысл DOS-заголовка в том, чтобы передать управление на идущую далее DOS-заглушку, если вдруг кто-то запустить виндовый бинарник под досом. Обычно эта заглушка (по сути — обычная DOS-программа) выдает текст «This program cannot be run in DOS mode.», но ничто не мешает запихать туда и досовую версию программы 🙂

Далее идет уже упомянутая сигнатура PE-файла (4 байта: ‘P’, ‘E’, 0, 0), после которой начинается структура IMAGE_FILE_HEADER. Эта структура подробно описана в MSDN или в статье Криса, тем не менее, я заострю внимание на некоторых ее полях:
Machine — архитектура, на которой может запускаться файл;
NumberOfSections — количество секций в PE-файле. Допустимое значение — от 1 до 0х60. Секция — это некая область памяти, обладающая определенными характеристиками и выделяемая системой при загрузке исполняемого файла;
SizeOfOptionalHeader — размер идущего за этой структурой опционального заголовка в байтах;
Characteristics — поле флагов характеристик PE-файла. Тут содержится информация о том, имеет ли файл экспортируемые функции, перемещаемые элементы, отладочную информацию и т.д.
Остальные поля при загрузке ни на что не влияют.

Далее идет опциональный заголовок PE-файла. На самом деле, никакой он не опциональный, без него файл загружен не будет, хотя размер этого заголовка может и варьироваться. И вновь я приведу описание самых важных полей:
Magic — для 32-разрядных PE-файлов это поле должно содержать значение 0х10B, а для 64-разрядных — 0х20B. Дальше я расскажу, в чем отличие 32- и 64-разрядных версий.
AddressOfEntryPoint — адрес точки входа относительно базового адреса загрузки файла (ImageBase). О способах адресации, используемых в PE-файлах, я расскажу дальше.
ImageBase — базовый адрес загрузки PE-файла. В памяти по этому адресу после загрузки будет располагаться вышеописанная структура IMAGE_DOS_HEADER. Если у файла имеется таблица перемещаемых элементов (о ней тоже далее), то этот адрес может варьироваться, а ImageBase будет содержать лишь рекомендуемый адрес загрузки.
FileAlignment и SectionAlignment — файловое и виртуальное выравнивание секций. В обязательном порядке должны быть выполнены следующие условия:
1. SectionAlignment >= 0х1000;
2. FileAlignment >= 0х200;
3. SectionAlignment >= FileAlignment.
В Windows NT возможно создание невыровненных файлов, но в этом случае физические и виртуальные адреса каждой секции должны совпадать, и SectionAlignment должно быть равно FileAlignment.

SizeOfImage — это поле содержит размер в байтах загруженного образа PE-файла, который должен быть равен виртуальному адресу последней секции плюс ее виртуальный выровненный размер.
SizeOfHeaders — размер всех заголовков. Это поле говорит загрузчику, сколько байт считать от начала файла, чтобы получить всю необходимую информацию для загрузки файла. Значение поля не должно превышать относительного виртуального адреса первой секции.
CheckSum — контрольная сумма файла, которая проверяется загрузчиком только для самых важных системных файлов.
Subsystem — подсистема файла. Самые распространенные — IMAGE_SUBSYSTEM_WINDOWS_GUI (GUI-интерфейс Windows) и IMAGE_SUBSYSTEM_WINDOWS_CUI (консольный интерфейс). За остальными — в статью Криса или MSDN.
SizeOfStackReserve и SizeOfStackCommit, SizeOfHeapReserve и SizeOfHeapCommit — размер соответственно стека и кучи, которые должны быть зарезервированы и выделены для PE-файла. 0 — значение по умолчанию. Если SizeOfStackCommit > SizeOfStackReserve или SizeOfHeapCommit > SizeOfHeapReserve, то файл загружен не будет.
NumberOfRvaAndSizes — количество элементов в таблице DATA_DIRECTORY, расположенной в самом конце опционального заголовка. Может варьироваться от 0 до 16, но все линковщики ставят значение 16, даже если не используют все элементы таблицы. Это связано с ошибками в системном загрузчике (как я понял, только в Win7 загрузчик наконец-то не содержит этих ошибок).
Остальные поля снова никому не сдались, в том числе и системному загрузчику.

Перед тем, как рассматривать таблицу DATA_DIRECTORY, я расскажу о способах адресации, которые наиболее часто используются в PE-файлах.
1. Виртуальная адресация, VA. Такие адреса отсчитываются от начала адресного пространства (т.е. от 0) и являются абсолютными.
2. Относительная виртуальная адресация (RVA). Эти адреса отсчитываются от базового адреса загрузки образа исполняемого файла, т.е. от того адреса, по которому был загружен исполняемый файл.
3. Сырые адреса, т.е. адреса непосредственно от начала файла формата PE на диске, а не в памяти.
Некоторые структуры используют и другие типы адресации.
RVA и VA легко преобразуются друг в друга: VA = RVA + базовый адрес загрузки.
Как я уже говорил выше, базовый адрес загрузки содержится в поле ImageBase опционального заголовка PE, но может варьироваться, если файл имеет таблицу перемещаемых элементов (relocations).

Теперь перейдем к разбору DATA_DIRECTORY. Каждый элемент в этой таблице (IMAGE_DATA_DIRECTORY), располагающейся в конце опционального заголовка, имеет собственное назначение. Лучше всего про это прочесть в MSDN или в статье Криса.
Каждый элемент таблицы представляет из себя структуру, содержащую два поля — виртуальный адрес тех данных, на которые указывает данный элемент, и их размер. Например, первый элемент таблицы (IMAGE_DIRECTORY_ENTRY_IMPORT) указывает на таблицу импортируемых функций из различных модулей (как правило, DLL-файлов).
Некоторые из подобных таблиц я разберу в следующих статьях, но, если не терпится узнать побольше прямо сейчас, читайте опять-таки статью Касперски, хотя и там не все эти таблицы описаны.

За DATA_DIRECTORY (т.е. после конца опционального заголовка) начинается таблица секций. Между концом опционального заголовка и таблицей секций могут присутствовать неиспользуемые байты. Существует макрос, позволяющий найти начало таблицы секций, называется он IMAGE_FIRST_SECTION и определен в WinNT.h. Каждая секция описывается структурой IMAGE_SECTION_HEADER, и идут эти структуры друг за другом. Их количество содержится в поле NumberOfSection файлового заголовка.
Как обычно, опишу только реально используемые загрузчиком поля этой структуры.
Name — имя секции. Предоставляется только для удобства и может содержать что угодно. Единственное, что нужно знать — имя секции, содержащей ресурсы файла, должно всегда быть равно «.rsrc», иначе Проводник Windows не сможет отобразить информацию о версии файла и его иконку. Разумеется, это косяк разработчиков Windows.
VirtualAddress — VA секции в памяти, должен быть выровнен на величину Section Alignment.
PointerToRawData — указатель на данные в файле, которые будут использоваться для инициализации памяти секции, должен быть выровнен на величину File Alignment.
VirtualSize и SizeOfRawData — виртуальный и физический размер секции, соответственно. Значение VirtualSize может быть невыровненным, но при загрузке образа всегда автоматически выравнивается по границе SectionAlignment. Значение SizeOfRawData обязано быть выровненным на границу FileAlignment, иначе файл загружен не будет. Впрочем, SizeOfRawData для последней секции может быть и невыровненным. Если SizeOfRawData Автор dx Опубликовано Июль 24, 2011 Август 4, 2011 Рубрики Windows, Для новичков Метки dll, exe, pe, Portable Executable, задротство, формат исполняемых файлов

PE-формат. Часть 1 — Базовая информация: 17 комментариев

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

Есть экзешник, к которому по не вполне ясным причинам компилятор приделал таблицу экспорта. Посмотрел в OllyDbg, вроде он из себя ничего не грузит. Пробовал удалить секцию с помощью утилиты strip, но тогда экзешник перестает загружаться, дескать «неверный формат». В чем может быть проблема? Может, IMAGE_FILE_HEADER.Characteristics нужно подправить? Или еще какие-то тонкие моменты могут быть?

Из IMAGE_FILE_HEADER.Characteristics надо убрать флаг IMAGE_FILE_DLL, скорее всего он там есть.
Убить экспорт несложно, достаточно занулить VirtualAddress и Size в нулевой IMAGE_DATA_DIRECTORY (удобно сделать с помощью того же CFF Explorer’а), а потом убрать флаг IMAGE_FILE_DLL.

Еще небольшой вопрос. А размеры полей в PE-заголовке для 64-х битных программ отличаются? Или там просто флаг, дескать прога 64-х битная. а остальное — как обычно? Насколько я понимаю, там нет смысла удваивать размеры полей.

Я же в статье написал, в чем отличие. В PE64 все поля, которые содержат VA, имеют размер QWORD, т.е. 64 бита.

И действительно. Моя ошибка. За инфу спасибо.

Спасибо автору статьи. Давно хотел всё это вспомнить, повторить, а тут всё как на ладони))))

>Между концом опционального заголовка и таблицей секций могут присутствовать неиспользуемые байты.
У других авторов написано, что нет, она сразу следует за заголовком. На практике я не встречал, чтобы там был пробел.

Размер опционального заголовка, идущего следом за IMAGE_FILE_HEADER’ом. Должен указывать на первый байт Section Table (т.е. e_lfanew + 18h + SizeOfOptionalHeader = &Section Table), где 18h — sizeof(IMAGE_FILE_HEADER). Если это не так, файл не загружается. И хотя некоторые загрузчики вычисляют указатель на Section Table отталкиваясь от NumberOfRvaAndSizes, закладываться на это не стоит, т.к. системные загрузчики этого мнения не разделяют.

Вот цитата из статьи Криса. По всей видимости, ничто мешает SizeOfOptionalHeader сделать больше, чем реальный размер OptionalHeader. Например, можно урезать NumberOfRvaAndSizes, не изменяя при этом SizeOfOptionalHeader, и получить некоторое количество неиспользуемых байтов. Сам не пробовал так делать (или пробовал, но уже не помню об этом), но не думаю, что это сделает бинарник неработоспособным.

Вот, сделал бинарник с урезанным NumberOfRvaAndSizes (разумеется, он запускается и работает). В нем, если в hex-редакторе глянуть, между опциональными заголовками и таблицей секций написано, что Вася Пупкин передает привет Москве 🙂

Да, вынужден согласиться. Оф-стандарт подтверждает Криса:
This table immediately follows the optional header, if any. This positioning is required because the file header does not contain a direct pointer to the section table. Instead, the location of the section table is determined by calculating the location of the first byte after the headers. Make sure to use the size of the optional header as specified in the file header.

Кстати, одно замечание про NumberOfRvaAndSizes.
>Может варьироваться от 0 до 16, но все линковщики ставят значение 16, даже если не используют все элементы таблицы. Это связано с ошибками в системном загрузчике (как я понял, только в Win7 загрузчик наконец-то не содержит этих ошибок).
Я не думаю, что дело в загрузчике (хотя может и в нем тоже). В стандарте ничего не сказано про это, но, скажем, в winnt.h прописано жестко: #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 и далее в описании структуры: IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];. То есть, как бэ негласное соглашение. Ну или другими словами, правая рука не знала, что делает левая.

В WinXP’овом загрузчике проблем с отсутствием неких неиспользуемых DataDirectories нет в целом, мой бинарник, который я ради примера сделал, запускается и на ней. Но там есть проблема, например, если будет отсутствовать столько DataDirectory’ий, что Debug Directory тоже не будет (не помню уже, до какого числа надо урезать NumberOfRvaAndSizes, чтобы такое получилось). В таком случае, даже если Debug-директория была пустой (заполненной нулями), в XP файл не загрузится. В Win Nt (и, кажется, при каких-то условиях и в XP) файл не загрузится, если у него нет таблицы импорта. Были и еще какие-то ошибки, не помню уже. В Vista какие-то из них поправили, в семерке вроде как совсем всё хорошо.

С NumberOfRvaAndSizes ошибки есть у многих. В NeoHexEditor, как я вижу, совсем всё плохо 🙂
В Cff Explorer’е при NumberOfRvaAndSizes

На мой взгляд самая адекватная статья в Руненте про формат PE. Мыщъх хоть и пишет много, пытается дать как можно больше инфы, но мягкоговоря описывает всё не совсем внятно. На статью наткнулся случайно, остановился почитать — не зря потратил время, сам хоть и кодер, но время потратил не зря. DX, ты молодец 😉

Статья хорошая, дебильную рожу в начале статьи можно убрать — все портит.

«Практически самое полное и доступно изложенное описание можно найти в статье Криса Касперски». — а название статьи не подскажете?

Видимо речь о: «Путь воина – внедрение в pe/coff-файлы»

Большое спасибо) Я ее нашел, но есть и еще одна (кому интересно из тех, кто будет читать комменты: «Техника внедрения и удаления кода из PE-файлов»). Вот и гадал, какая из двух подразумевается. Не считая его книги «Компьютерные вирусы изнутри и снаружи».

Дебильную рожу в начале лучше оставить т.к. она «отфильтровывает» совсем дибилов которые напрягаются от ее вида. Ради Вашего же блага, ептель.

p.s. Статейка зачет, в принципе как и всегда. Зашел перечитать — подзамылилось немного.

Добавить комментарий Отменить ответ

Поиск

Предупреждение

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

Структура исполняемого файла Windows

Исследуем формат EXE-файла

Целью работы компилятора является получение EXE-файла. Поэтому, исследуем его структуру.

EXE-файлы появились ещё в DOS и потом они с небольшими изменениями перекочевали в Windows. Формат EXE-файла под Windows называется PE-файлом. Он организован в виде линейного потока данных.

Формат PE-файла

Заголовок MS-DOS
Программа-заглушка
Заголовок PE
Доп. заголовок PE
Массив DataDir
Заголовки сегментов
Тела сегментов
Остальные области данных

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

Залоговок MS-DOS (размер 40H байт)

Адрес Тип Имя Описание
00h word Magic Магическая сигнатура DOS-файла — два символа «MZ», явно от MZ -club 🙂
02h word LastByteCount Количество байт на последней странице файла
04h word PageCount Количество страниц в файле
06h word RelocCount Количество релокейшенов
08h word HeaderSize Размер заголовка в параграфах
0Ah word MinAlloc Мин. выделение памяти в параграфах
0Ch word MaxAlloc Макс. выделение памяти в параграфах
0Eh word InitSS Начальное (относительное) значение регистра SS
10h word InitSP Начальное значение регистра SP
12h word CheckSum Контрольная сумма
14h word InitIP Начальное значение регистра IP
16h word InitCS Начальное (относительное) значение регистра CS
18h word RelocAddr Адрес на релокейшены и программу-заглушку
1Ah word OverlayCount Количество оверлеев
1Ch word Res1[4] Зарезервировано
24h word OEMIdentifier Для OEMInfo
26h word OEMInfo Информация о программе
28h word Res1[10] Зарезервировано
3Ch dword PEHeaderAddr Адрес в файле заголовка PE

Для Windows-программы заголовок MS-DOS не содержит релокейшины (пока даже и не знаю что это такое), то есть Relocations = 0, поэтому RelocAddr указывает сразу на программу-заглушку. Но нам важен заголовок PE, его адрес находиться в PEHeaderAddr.

Залоговок PE (размер 18H байт)

Адрес Тип Имя Описание
00h dword Magic Магическая сигнатура PE-файла 4550H или «PE», 0H, 0H
04h word CPUType Тип процессора
06h word SectionCount Количество сегментов
08h dword DateTime Дата/время создания/модификации линкером
0Сh dword SymbolTableAddr Адрес местонахождения таблицы символов
10h dword SymbolTableSize Размер таблицы символов
14h word OptionalHeaderSize Размер доп. заголовка PE
16h word Flags Предназначение программы

Сразу за основным заголовком идёт дополнительный заголовок PE.

Доп. залоговок PE (размер 18H — 77H байт)

Адрес Тип Имя Описание
18h word Magic Всегда 10Bh
1Ah byte MajorLinkVer Версия линкера, создавшего данный файл
1Bh byte MinorLinkVer
1Ch dword CodeSize Размер исполнительного кода
20h dword InitDataSize Размер инициализированных данных
24h dword UnInitDataSize Размер неинициализированных данных
28h dword EntryPointAddr Адрес, относительно ImageBase, по которому передаётся управление при запуске программы или адрес инициализации/завершения библиотеки
2Ch dword CodeBase Относительное смещение сегмента кода
30 dword DataBase Относительное смещение сегмента неинициализированных данных
34h dword ImageBase Предподчтительный адрес для загрузки исполнимого файла (по умолчанию 400000H)
38h dword SectionAlign Выравнивание программных секций (по умолчанию 1000H)
3Ch dword FileAlign Минимальная гранулярность сегментов, то есть размер сегментов должен быть кратен FileAlign, должен быть равен значению степени 2 между 200H и 10000H (по умолчанию 200H)
40h word MajorOSVer Старший номер версии OS, необходимый для запуска программы
42h word MinorOSVer Младший номер версии OS
44h word MajorImageVer Пользовательский старший номер версии, задается пользователем при линковке программы и им же и используется
46h word MinorImageVer Пользовательский младший номер версии, задается пользователем при линковке программы и им же и используется
48h word MajorSubSysVer Старший номер версии Win32
4Ah word MinorSubSysVer Младший номер версии Win32
4Ch dword Res1
50h dword ImageSize Виртуальный размер в байтах всего загружаемого образа, вместе с заголовками, кратен ObjectAlign
54h dword HeaderSize Общий размер всех заголовков: MS-DOS, PE, доп PE и всех сегментов
58h dword CheckSum Контрольная сумма (не используется и равна 0)
5Ch word SubSystem Подсистема, необходимая для запуска данного файла //(0 — неизвестная подсистема, 1 — не требует подсистему, 2 — Windows GUI, 3 — Windows консоль. )
5Eh word DllFlags Специальные флаги при загрузке, начиная с NT 3.5 не используются
60h dword StackReserveSize Память, требуемая для стека приложения, память резервируется, но выделяется только StackCommitSize байтов, следующая страница является охранной. Когда приложение достигает этой страницы, то страница становится доступной, а следующая страница — охранной, и так до достижения нижней границы, после чего Windows убивает программу с сообщением о конце стека
64h dword StackCommitSize Объем памяти, отводимый в стеке немедленно после загрузки
68h dword HeapReserveSize Максимальный возможный размер локального хипа
6Ch dword HeapComitSize Отводимый при загрузке хип
70h dword LoaderFlags Данный параметр устарел
74h dword DataDirSize Указывает размер массива DataDir, расположенный ниже (по умолчанию 10h)

Далее идёт массив DataDir, 8-байтные элементы которого состоят из двух 4-х байтных: адрес и размер.

Массив DataDir (размер 78H — F8H байт)

Адрес Тип Имя Описание
78h qword ExportDir Каталог экспортируемых объектов
80h qword ImportDir Каталог импортируемых объектов
88h qword ResourceDir Каталог ресурсов
90h qword ExceptionDir Каталог исключений
98h qword SecurityDir Каталог безопастности
A0h qword BaseRelocDir Каталог переадресаций
A8h qword DebugDir Отладочный каталог
B0h qword CopyrightDir Каталог описаний
B8h qword CpuSpecDir Каталог значений, специфичных для процессора
C0h qword TLSDir Каталог TLS (Thread local storage — локальная память потоков)
C8h qword ConfigDir Каталог конфигураций загрузки
D0h qword ResDir11
D8h qword ResDir12
E0h qword ResDir13
E8h qword ResDir14
F0h qword ResDir15

Элемент массива DataDir (размер 8 байт)

Адрес Тип Имя Описание
00h dword Addr Адрес каталога
04h dword Size Размер каталога

Далее идёт подряд несколько сегментов, количество указано в SectionCount.

Заголовок сегмента (размер 2Ch байт)

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

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

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

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

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

Приложения под 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, приведенная ниже, формирует корень и ветви дерева:

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

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

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

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

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

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

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

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

Как Вы, вероятно, могли заметить, ресурсы могут быть идентифицированы либо по имени, либо по целому числу. Они различаются на этом уровне иерархии с помощью поля Name в элементе каталога. Если самый значащий бит поля Name установлен, оставшиеся 31 бит используются как смещение на структуру IMAGE_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 — это то, что Вы можете манипулировать различной информацией из сегмента без обращения непосредственно к данным ресурсов. Например, Вы можете подсчитать, сколько у Вас ресурсов каждого типа, сколько ресурсов ( и есть ли такие ) используют некоторый язык, существует ли какой-либо ресурс, и каков размер ресурсов некоторого типа. Для демонстрации использования такого рода информации, следующая функция показывает, как определить тип различных ресурсов, включенных в файл:

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

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

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

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

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

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

Заметьте, что в этой функции переменной 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.

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Вопрос 4. Формат исполняемого РЕ файла ОС Windows . Приведите фрагмент программы, читающей из РЕ файла список импортируемых программой функций ОС.

Адрес Тип Имя Описание
00h char[8] SectName Имя секции, если имя Практически любая программа под Windows работает с такими её DLL-ками: kernel32.dll, user32.dll, gdi32.dll и т.д.. Поэтому, EXE-шник должен уметь импортировать функции данных библиотек, то есть работать с каталогом импорта ImportDir. Каталог импорта сразу же начинается с таблицы импорта ImportDirTable, которая описывает остальную информацию об импорте. Такая таблица состоит из элементов ImportDirTableItem, указывающих, как минимум, на каждую импортируемую библиотеку. Последний элемент, указывающий на конец таблицы, заполнен нулями.

Элемент таблицы каталога импортируемых объектов ImportDirTableItem (размер 14h байт)

Адрес Тип Имя Описание
00h dword FuncNameList Список имён импортируемых функций
04h dword Res1
08h dword Res2
0Ch dword LibName Имя библиотеки
10h dword FuncAddrList Список адресов импортируемых функций

Параметр LibName указывает на имя библиотеки, которое должно заканчиваться нулём. FuncNameList указывает на список адресов (0-ой адрес — конец списка), по которым находится сначала Hint — (укороченный идентификатор точки входа), а затем имя функции, заканчивающееся нулём. Параметр FuncAddrList указывает на точно такой же список адресов, находящийся (по моим наблюдениям) перед ImportDirTable.

Формат EXE-файла здесь описан не полностью. Остальное будет описано позже. Однако, этого уже достаточно для создания компилятора.

Для глубокого изучения EXE-шника, написана специальная программа «EXE-исследователь». Последнюю версию данной программы можно скачать на страничке Download

Национальная библиотека им. Н. Э. Баумана
Bauman National Library

Персональные инструменты

PE (Portable Executable)

Portable Executable

Расширение файла .exe, .dll, .ocx, .sys, .scr, .drv, .cpl или .efi
Интернет-тип носителя application/vnd.microsoft.portable-executable[1] и application/efi[2
Тип формата двоичный, исполняемый, объектный, динамическая библиотека


Portable Executable — (PE, [по́тэбл экзэкью́тэбл] — переносимый исполняемый) — формат исполняемых файлов, объектного кода и динамических библиотек, используемый в 32- и 64-битных версиях операционной системы Microsoft Windows. На данный момент существует два формата PE-файлов: PE32 и PE32+. PE32 формат для x86 систем, а PE32+ для x64. Описанные структуры можно наблюдать в заголовочном файле WINNT.h , который поставляется вместе SDK.

Содержание

Исполнительные файлы

DOS header (IMAGE_DOS_HEADER) и DOS stub

DOS заголовок. Это самая первая структура в файле и она имеет размер 64 байта. В этой структуре наиболее важные поля это e_magic и e_lfnew.

e_magic — собственно, это поле размером два байта содержит сигнатуру ‘MZ’ (сокращение от имени Марк Збиновски, который являлся ведущим разработчиком MS-DOS и архитектором формата PE);

e_lfanew — указатель на начало PE-заголовка (см. рисунок выше). Это поле должно указывать на первый байт PE-заголовка (IMAGE_NT_HEADERS), т.е. на сигнатуру «PE\0\0», причем значение этого поля должно быть выровнено по границе двойного слова.

После 64 первых байт файла стартует DOS-stub или DOS-заглушка. Эта область в памяти которая в большинстве своём забита нулями. Заглушка лежит после dos-header(а) и перед PE заголовком. Служит она только для обратной совместимости. В неё может быть записана мини версия DOS программы ограниченную в 192 байта (256 — конец заглушки, 64 — размер dos заголовка).

PE-Header (IMAGE_NT_HEADER)

PE-Header, иными славами PE-заголовок. Расположение — 0x18 байт.

File header (IMAGE_FILE_HEADER) содержит в себе набор полей:

  • Machine — WORD архитектура, на которой может запускаться файл;
  • NumberOfSections — DWORD количество секций в PE-файле. Допустимое значение — от 1 до 0х60. Секция — это некая область памяти, обладающая определенными характеристиками и выделяемая системой при загрузке исполняемого файла;
  • SizeOfOptionalHeader — WORD размер идущего за этой структурой опционального заголовка в байтах;
  • Characteristics — WORD поле флагов характеристик PE-файла. Тут содержится информация о том, имеет ли файл экспортируемые функции, перемещаемые элементы, отладочную информацию и т.д.
  • PointerToSymbolTable: DWORD — смещение (RAW) до таблицы символов, а SizeOfOptionalHeader — это размер данной таблицы. Данная таблица призвана служить для хранения отладочной информации, но отряд не заметил потери бойца с самого начала службы. Чаще всего это поле зачищается нулями.
  • TimeDateStamp: WORD — число хранящее дату и время создания файла.

Optional header (IMAGE_OPTIONAL_HEADER)

PE-заголовок. Этот заголовок является обязательным и имеет 2 формата PE32 и PE32+ (IMAGE_OPTIONAL_HEADER32 и IMAGE_OPTIONAL_HEADER64 соответственно). Формат хранится в поле Magic: WORD. Заголовок содержит необходимую информацию для загрузки файла.

  • AddressOfEntryPoint: DWORD — RVA адрес точки входа. Может указывать в любую точку адресного пространства. Для .exe файлов точка входа соответствует адресу, с которого программа начинает выполняться и не может равняться нулю!
  • BaseOfCode: DWORD — RVA начала кода программы (секции кода).
  • BaseOfData: DWORD — RVA начала кода программы (секции данных).
  • ImageBase: DWORD — предпочтительный базовый адрес загрузки программы. Должен быть кратен 64кб. В большистве случаев равен 0x00400000.
  • SectionAligment: DWORD — размер выравнивания (байты) секции при выгрузке в виртуальную память.
  • FileAligment: DWORD — размер выравнивания (байты) секции внутри файла.
  • SizeOfImage: DWORD — размер файла (в байтах) в памяти, включая все заголовки. Должен быть кратен SectionAligment.
  • SizeOfHeaders: DWORD — размер всех заголовков (DOS, DOS stub, PE, Section) выравненный на FileAligment.
  • NumberOfRvaAndSizes: DWORD — количество каталогов в таблице директорий (ниже сама таблица). На данный момент это поле всегда равно символической константе IMAGE_NUMBEROF_DIRECTORY_ENTRIES, которая равна 16-ти.
  • DataDirectory[NumberOfRvaAndSizes]: IMAGE_DATA_DIRECTORY — каталог данных. Проще говоря это массив (размером 16), каждый элемент которого содержит структуру из 2-ух DWORD-ых значений.

Каждый элемент таблицы представляет из себя структуру, содержащую два поля — виртуальный адрес тех данных, на которые указывает данный элемент, и их размер. Например, первый элемент таблицы (IMAGE_DIRECTORY_ENTRY_IMPORT) указывает на таблицу импортируемых функций из различных модулей (как правило, DLL-файлов). [Источник 1]

Section headers (IMAGE_SECTION_HEADER)

Сразу за массивом DataDirectory друг за другом идут секции. Таблица секций представляет из себя суверенное государство, которое делится на NumberOfSections городов. Каждый город имеет своё ремесло, свои права, а также размер в 0x28 байт. Количество секций указано в поле NumberOfSections, что хранится в File-header-е. Существует макрос, позволяющий найти начало таблицы секций, называется он IMAGE_FIRST_SECTION и определен в WinNT.h. Каждая секция описывается структурой IMAGE_SECTION_HEADER, и идут эти структуры друг за другом. Их количество содержится в поле NumberOfSection файлового заголовка.

  • Name: BYTE[IMAGE_SIZEOF_SHORT_NAME] — название секции. На данный момент имеет длину в 8 символов.
  • VirtualSize: DWORD — размер секции в виртуальной памяти.
  • SizeOfRawData: DWORD — размер секции в файле.
  • VirtualAddress: DWORD — RVA адрес секции.
  • SizeOfRawData: DWORD — размер секции в файле. Должен быть кратен FileAligment.
  • PointerToRawData: DWORD — RAW смещение до начала секции. Также должен быть кратен FileAligment…
  • Characteristics: DWORD — атрибуты доступа к секции и правила для её загрузки в вирт. память.

Export table

В самом первом элементе массива DataDirectory хранится RVA на таблицу экспорта, которая представлена структурой IMAGE_EXPORT_DIRECTORY. Эта таблица свойственна файлам динамических библиотек (.dll). Основной задачей таблицы является связь экспортируемых функций с их RVA. Эта структура содержит три указателя на три разные таблицы. Это таблица имён (функций) (AddressOfNames), ординалов(AddressOfNamesOrdinals), адресов(AddressOfFunctions). В поле Name хранится RVA имени динамической библиотеки. Ординал — это как посредник, между таблицей имён и таблицей адресов, и представляет из себя массив индексов (размер индекса равен 2 байта). [Источник 2]

Import table

  • Стандартный импорт — в DataDirectory под индексом IMAGE_DIRECTORY_ENTRY_IMPORT хранится таблица импорта. Она представляет собой массив из элементов типа IMAGE_IMPORT_DESCRIPTOR. Таблица импорта хранит (массивом) имена функций/ординалов и в какое место загрузчик должен записать эффективный адрес этой функций.
  • Bound import — при данной схеме работы в поля TimeDateStamp и ForwardChain заносится -1 и информация о связывании хранится в ячейке DataDirectory с индексом IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT. Алгоритм работы заключается в следующем — в виртуальную память приложения выгружается необходимая библиотека и все необходимые адреса «биндятся» ещё на этапе компиляции.
  • Delay import — при данном методе подразумевается что .dll файл прикреплён к исполняемому, но в память выгружается не сразу , а только при первом обращении приложения к символу (так называют выгружаемые элементы из динамических библиотек). За отложенным импортом загрузчик обращается к DataDirectory[IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT]

Метаданные, PE-формат, «.NET»

Платформа .NET корпорации Microsoft расширила формат PE с помощью функций, которые поддерживают общеязыковую среду исполнения (Common Language Runtime — CLR). Среди дополнений — заголовок CLR и секция данных CLR. После загрузки двоичного файла загрузчик ОС приводит к выполнению CLR через ссылку в таблице импорта PE/COFF. Затем CLR загружает заголовок CLR и секции данных.

Секция данных CLR содержит два важных сегмента: сегмент метаданных и сегмент кода промежуточного языка (IL):

  • Метаданные содержат информацию, относящуюся к сборке, включая манифест сборки. Манифест подробно описывает сборку, включая уникальный идентификатор (с помощью хеша, номера версии, и т. д.), данные об экспортируемых компонентах, расширенную информацию о типе (поддерживаемую общей системой типов (Common Type System — CTS)), внешние ссылки, и список файлов в сборке. Среда CLR широко использует метаданные.
  • Код промежуточного языка (Intermediate Language — IL) — абстрактный, независимый от языка код, который удовлетворяет требованиям общего промежуточного языка (Common Intermediate Language — CIL) .NET CLR . Термин «промежуточный» относится к природе кода IL, обладающего межъязыковой и кроссплатформенной совместимостью. Этот промежуточный язык, подобный байт-коду Java, позволяет платформам и языкам поддерживать общую среду .NET CLR. IL поддерживает объектно-ориентированное программирование (полиморфизм, наследование, абстрактные типы, и т. д.), исключения, события, и различные структуры данных.

PE32 х86 и РЕ32+ х64

PE формат — это формат исполняемых файлов всех 32- и 64- разрядных Windows систем. На данный момент существует два формата PE-файлов: PE32 и PE32+. PE32 формат для x86 систем, а PE32+ для x64. Описанные структуры можно наблюдать в заголовочном файле WINNT.h, который поставляется вместе SDK.

Во всех 32-разрядных ветках ОС Windows объектные .OBJ , библиотечные .LIB и исполняемые .EXE и .DLL файлы хранятся в едином формате COFF (Common Object File Format), который используется некоторыми системами семейства Unix и ОС VMS.

Формат PE (Portable Executable) является специализацией COFF для хранения исполняемых модулей. Он был стандартизован Tool Interface Standard Committee (Microsoft, Intel, IBM, Borland, Watcom и др.) в 1993 г., а затем понемногу обновлялся (последнее обновление было проведено в феврале 1999 г., но оно не учитывает поддержки метаданных для .NET, добавленной в 2000 г.). Название Portable Executable связано с тем, что данный формат не зависит от архитектуры процессора, для которого построен исполняемый файл. На сегодняшний день существует два формата PE-файлов: PE32 и PE32+. Оба они ограничивают адресное пространство программы размеров в 4 Гб (0xFFFFFFFF), но PE32 использует 32-битовые адреса (архитектура Win32), а PE32+ – 64-битовые адреса (архитектура Win64). Большинство описанных ниже структур и констант содержатся в стандартном заголовочном файле Windows WINNT.H .

Использование в других операционных системах

Формат PE также используется ReactOS, поскольку ReactOS предназначена для того, чтобы быть двоично совместимой с Windows на уровне кода. Кроме того, он исторически использовался многими другими операционными системами, включая SkyOS и BeOS R3. Однако и SkyOS, и BeOS в конечном счёте перешли на формат ELF.

Поскольку платформа разработки Mono намеревается быть двоично совместимой с Microsoft .NET, она использует тот же формат PE, что и в реализации Microsoft.

На платформе x86 в Unix-подобных операционных системах некоторые двоичные файлы Windows (в формате PE) могут быть выполнены с помощью Wine. HX DOS Extender также использует формат PE для собственных 32-разрядных двоичных файлов DOS, кроме того, может в некоторой степени выполнить существующие двоичные файлы Windows в DOS, действуя, таким образом, как Wine для DOS.

Mac OS X 10.5 имеет возможность загружать и интерпретировать PE-файлы, однако они не являются двоично совместимыми с Windows. [Источник 3]

Форматы РЕ и COFF объектных файлов

Автор: Мэтт Питрек
Источник: Секреты системного программирования в Windows 95. Глава 8.

Опубликовано: 05.04.2001
Исправлено: 15.04.2009
Версия текста: 1.1

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

В этой главе мы совершим экскурс в переносимый исполняемый (РЕ — Portable Executable) формат файла, который фирма Microsoft разработала для использования во всех ее операционных системах Win32 (Windows NT, Windows 95, Win32s).

Возможно, читатель удивится тому, что я рассказываю о РЕ-формате в этой книге, тогда как несколько описаний этого формата можно найти на CD-ROM Microsoft Developer Network. Главная причина, по которой я решил описать РЕ-формат, — это тот факт, что внутри самой Windows 95 используются те же ключевые структуры данных, что и в файлах РЕ-формата. Так, например, Windows 95 отображает заголовок РЕ-файла в память и использует его для представления загружаемого модуля. Для того чтобы понять, как работает ядро Windows 95, необходимо разобраться с РЕ-форматом. Гарантирую, что это достаточно просто.

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

РЕ-формат играет и будет играть в обозримом будущем ключевую роль во всех операционных системах Microsoft, включая Cairo. Даже если вы программируете в Windows 3.1, используя Visual C++, вам все равно придется пользоваться РЕ-файлами (32-разрядные расширенные компоненты DOS в Visual C++ используют этот формат). А если вы собираетесь заняться любой работой, связанной с системным программированием низкого уровня в Windows 95, практические знания по РЕ-файлам просто необходимы.

При обсуждении РЕ-формата я не собираюсь тщательно перебирать груды шестнадцатеричных кодов или без конца объяснять назначения отдельных битов. Вместо этого я хочу изложить концепции, заложенные в РЕ-формат, и связать их с понятиями, которые ежедневно встречаются при программировании в Win32. Например, концепция локальных переменных для цепочек (а-ля __declspec (thread)) выводила меня из терпения до тех пор, пока я не увидел, с какой простотой и изяществом она реализована в исполняемом файле. Учитывая, что многие программисты имеют опыт работы в Win 16, я покажу, как связаны конструкции в РЕ-формате с их эквивалентами в 16-разрядных файловых форматах.

Вместе с новыми форматами исполняемых файлов Microsoft также ввела новые форматы объектных модулей и библиотек, создаваемые ее собственными компиляторами и ассемблерами, (Новый файловый формат LIB, по существу, представляет собой просто связку объектных файлов, упорядоченных с помощью индекса; когда в дальнейшем я ссылаюсь в этой книге на объектные файлы, я буду иметь в виду как COFF-объектные, так и LIB-файлы.) Эти новые объектные и LIB-файловые форматы имеют немало общих концепций с форматом РЕ. До настоящего времени не существовало общедоступного источника информации об объектных и LIB-файлах фирмы Microsoft, и даже к моменту написания этой книги информация очень скудна. Поэтому стоит осветить также форматы объектных файлов и LIB-файлов.

Общеизвестно, что Windows NT (первая из операционных систем Win32) унаследовала многое от VAX VMS и UNIX. Многие ведущие разработчики NT перед своим приходом в Microsoft программировали и работали именно над этими системами. Вполне естественно, что, когда им пришлось создавать NT, чтобы сохранить свое время и силы, они использовали ранее написанные и опробованные средства. Исполняемый формат и формат объектного модуля, который эти средства создавали и с которым они работали, называется COFF (Common Object File Format — стандартный формат объектного файла).

Относительно устаревшую (по компьютерным меркам) сущность COFF можно усмотреть в том, что некоторые поля в файлах имеют восьмеричный формат. COFF-формат был сам по себе неплохой отправной точкой, но нуждался в расширении, чтобы удовлетворить потребностям новых операционных систем, таких как Windows NT или Windows 95. Результатом такого усовершенствования явился РЕ-формат (не забывайте: РЕ означает Portable Executable — переносимый исполняемый). Этот формат называется переносимым, так как все реализации Windows NT в различных системах (Intel 386, MIPS, Alpha, Power PC и т.д.) используют один и тот же исполняемый формат. Конечно, имеются различия, например, связанные с двоичной кодировкой команд процессора. Нельзя запустить на Intel исполняемый РЕ-файл, откомпилированный в MIPS. Тем не менее существенно, что нет нужды полностью переписывать загрузчик операционной системы и программные средства для каждого нового процессора.

Microsoft стремилась усовершенствовать Windows NT, и это хорошо иллюстрируется тем, что Microsoft отказалась от своих существующих 32-разрядных средств и файловых форматов. Драйверы виртуальных устройств, написанные для Windows З.х, использовали другой 32-разрядный формат файла (LE-формат) задолго до появления NT на свет. Следуя принципу «Если не поломано, не надо и чинить», заложенному в Windows, Windows 95 использует как РЕ-, так и LE-формат. Это позволило Microsoft широко использовать существующие программы под Windows 3-х.

Вполне естественно ожидать совершенно другого исполняемого формата для совершенно новой операционной системы (какой является Windows NT), но другой вопрос— форматы объектных модулей (.OBJ и LIB). До появления 32-разрядной версии 1.0 Visual C++ все компиляторы Microsoft пользовались спецификацией Intel OMF (Object Module Format — формат объектного модуля). Компиляторы Microsoft для реализации Win32 создают объектные файлы в формате COFF. Некоторые конкуренты Microsoft, например Borland, отказались от формата COFF объектных файлов и продолжали придерживаться формата OMF Intel. В результате компании, производящие объектные и LIB-файлы, рассчитанные на использование с несколькими компиляторами, будут вынуждены возвратиться к системе поставок различных версий их продуктов для различных компиляторов (если они не сделали этого до сих пор).

Те пользователи, которые любят усматривать во всех действиях Microsoft скрытность, могут увидеть в смене объектных форматов стремление Microsoft воспрепятствовать своим конкурентам. Чтобы гарантировать «совместимость» с Microsoft вплоть до уровня объектных файлов, другие фирмы будут вынуждены конвертировать все свои 32-разрядные средства в форматы COFF OBJ и LIB. Подводя итог, можно сказать, что объектные и LIB-файловые форматы являются еще одним примером отказа Microsoft от существующих стандартов при выборе приоритетов развития этой фирмы.

Вместе с некоторыми определениями структур для объектных файлов формат COFF РЕ-формат задокументирован (в самом размытом смысле этого слова) в файле заголовка WINNT.H. (Я буду пользоваться именами полей из WINNT.H позже в этой главе.) Примерно посредине WINNT.H находится секция, озаглавленная «Image Format». Эта секция начинается с небольших фрагментов из старых добрых заголовков форматов DOS MZ и NE перед переходом к новой информации, связанной с РЕ. WINNT.H дает определения структур исходных данных, используемых РЕ-файлами, однако содержит всего лишь прозрачный намек на полезные комментарии, объясняющие назначение структур и флагов. Автор заголовочного файла для РЕ-формата (некий Michael J. O’Leary) определенно питает склонность к длинным, описательным именам, а также к глубоко вложенным структурам и макросам. Программируя с использованием WINNT.H, нередко можно встретить, например, такое выражение:

Вы, вероятно, захотите не просто прочесть о том, из чего состоят РЕ-файлы, но и самим просмотреть несколько РЕ-файлов, чтобы на практике увидеть, как реализуются представленные здесь концепции. Если вы используете средства Microsoft для Win32, то программу DUMPBIN из Visual C++, а также Win32 SDK, можно применить для того, чтобы «препарировать» файлы РЕ и COFF OBJ/LIB и представить их в удобоваримом виде. DUMPBIN даже имеет замечательный параметр для дизассемблирования программных секций изучаемого файла. Интересно отметить, что фирма Microsoft, которая запрещает пользователям дизассемблировать свои программы, в данном случае предоставила средство для простого дизассемблирования программ и библиотек динамической компоновки (DLL). Если бы возможность дизассемблировать ЕХЕ и объектные файлы не была бы полезна, зачем понадобилось бы Microsoft снабжать этим DUMPBIN? В этом случае можно сказать: «Делай, как мы говорим, а не так, как мы делаем».

Пользователи Borland могут использовать TDUMP, чтобы просматривать РЕ-файлы, однако TDUMP не воспринимает объектные файлы формата COFF. Здесь нет ничего страшного, так как компилятор Borland вообще не создает объектные файлы формата COFF. Бросая свой вызов, я тоже написал программу просмотра РЕ- и COFF-OBJ/LIB-файлов (PEDUMP), которая, на мой взгляд, осуществляет более наглядный вывод, чем DUMPBIN. Несмотря на то, что моя программа не может дизассемблировать, во всем остальном она полностью функционально эквивалентна DUMPBIN и, кроме того, содержит некоторые новые черты, делающие ее достойной внимания. Исходный текст программы PEDUMP находится на специальной дискете — вот почему я не привожу ее здесь полностью. Вместо этого я предоставляю образец вывода PEDUMP, чтобы проиллюстрировать концепции по мере их изложения.

Программа PEDUMP

Программа PEDUMP является служебной, запускаемой из командной строки и предназначена для вывода РЕ-файлов и файлов формата COFF OBJ/LIB. Она использует возможности терминала Win32, чтобы обойтись без кропотливой работы с интерфейсом пользователя. PEDUMP имеет следующий синтаксис: PEDUMP [ключи] имя файла . Имеющиеся ключи можно увидеть, запустив PEDUMP без аргументов. PEDUMP использует следующие ключи:

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

PEDUMP осуществляет вывод в стандартный выходной файл (например, на экран), так что этот вывод можно переадресовать в какой-либо файл с помощью знака «>» (больше), введенного в командной строке.

Исходные файлы для PEDUMP включены вместе с ней. PEDUMP была создана с помощью компилятора Microsoft Visual C++ 2.0, хотя я также компилировал эту программу по мере ее усовершенствования, пользуясь Borland C++ 4.x.

Основные сведения о форматах Win32 и РЕ

Перед тем как начать обсуждение формата РЕ-файла, я хотел бы рассмотреть несколько новых основных идей, позволивших создать такой формат. В процессе этого обсуждения я буду использовать понятие модуль, подразумевая под ним текст программ, данные и ресурсы исполняемого файла или DLL, которые были загружены в память. Помимо текста программы и данных, которые использует непосредственно программа, модуль также включает вспомогательные данные, используемые Windows для того, чтобы определить, где расположены в памяти текст программы и данные. В Win 16 вспомогательные структуры данных находятся в базе данных модуля (сегмент, на который ссылается HMODULE). В Win32 эта информация содержится в заголовке РЕ-файла (структура IMAGE_NT_HEADERS), о котором вскоре я подробно расскажу.

Самое важное из того, что следует знать о РЕ-файлах, это то, что исполняемый файл на диске и модуль, получаемый после загрузки, очень похожи. Причиной этого является то, что загрузчик Windows должен создать из дискового файла исполняемый процесс без больших усилий. Точнее говоря, загрузчик попросту использует отображенные в память файлы Win32, чтобы загрузить соответствующие части РЕ-файла в адресное пространство программы. Здесь уместна аналогия со строительством сборных домиков. У вас есть относительно немного элементов, расставляя их по своим местам и скрепляя стандартными соединениями, вы достаточно быстро собираете целый дом, — вся работа состоит из простого защелкивания таких стандартных соединений. И такой же простой задачей, как подключение электричества и водопровода к сборному домику, является соединение РЕ-файла с внешним миром (т.е. подключение к нему DLL и т.д.).

Так же просто загружается и DLL. После того как ЕХЕ или .DLL модуль загружены, Windows обращается с ними так же, как и с другими отображенными в память файлами. Совершенно иная ситуация в 16-разрядной Windows. 16-разрядный NE-загрузчик файла считывает файл в память порциями и создает отдельные структуры данных для представления модуля в памяти. Когда необходимо загрузить сегмент программы или данных, загрузчик должен выделить новый сегмент из общей кучи, обнаружить, где хранятся исходные данные в исполняемом файле, отыскать это место, считать исходные данные и применить любой подходящий крепеж. Кроме того, каждый 16-разрядный модуль обязан запоминать все используемые в данный момент селекторы, независимо от того, выгружен ли сегмент.

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

Другим важным понятием, о котором необходимо знать, является RVA (Relative Virtual Address — относительный виртуальный адрес). Многие поля в РЕ-файлах задаются именно с помощью их RVA. RVA — это просто смещение данного элемента по отношению к адресу, с которого начинается отображение файла в памяти. Пусть, к примеру, загрузчик Windows отобразил РЕ-файл в память, начиная с адреса 0х400000 в виртуальном адресном пространстве. Если некая таблица в отображении начинается с адреса 0х401464, то RVA данной таблицы 0х1464:

Чтобы перевести RVA в указатель памяти, просто прибавьте RVA к базовому адресу, начиная с которого был загружен модуль. Термин базовый адрес представляет еще одно важное понятие, о котором следует помнить. Базовый адрес — это адрес, с которого начинается отображенный в память ЕХЕ-файл или DLL. Для удобства Windows NT и Windows 95 используют базовый адрес модуля в качестве дескриптора образца модуля (HINSTANCE — instance handle). To, что в Win32 базовый адрес называется HINSTANCE, может вызвать недоразумения, так как термин дескриптор образца происходит из 16-разрядной Windows. Каждая копия приложения в Win16 получает свой собственный сегмент данных (и связанный с ним глобальный дескриптор), который отличает эту копию приложения от других; отсюда и название дескриптор образца.

В Win32 нет нужды различать отдельные копии приложений, так как у них нет общего адресного пространства. Однако термин HINSTANCE сохранен, чтобы отразить преемственность Win32 по отношению к Win16. В случае Win32 существенно то, что можно вызвать GetModuleHandle() для любой DLL, которая используется в процессе, и получить указатель, который можно использовать для доступа к компонентам модуля. Под компонентами модуля я подразумеваю его импортируемые и экспортируемые функции, его перемещения, его программные секции и секции данных и т.п.

Еще одним понятием, с которым следует ознакомиться для того, чтобы исследовать РЕ- и COFF OBJ-файлы, является секция. Секция в файлах РЕ или COFF OBJ примерно эквивалентна сегменту или ресурсам в 16-разрядном NE-файле. Секции содержат либо код программ, либо данные. Некоторые секции содержат код и данные, непосредственно объявляемые и используемые программами, тогда как другие секции данных создаются компоновщиками и библиотекарями специально для пользователя и содержат информацию, необходимую для работы операционной системы. В некоторых описаниях формата РЕ фирмы Microsoft секции также называются объектами. Однако этот последний термин имеет так много (возможно, противоречащих друг другу) значений, что я решил придерживаться термина секции для обозначения им областей программного кода и данных. Секции рассмотрены более подробно в этой главе, в разделе «Часто встречающиеся секции»; пока что читателю достаточно просто знать, что такое секция.

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

Рис. 8.1. Общий формат РЕ-файла

Заголовок РЕ-файла

Первым пунктом, на котором мы остановимся в нашем путешествии по РЕ-формату, будет заголовок РЕ-файла. Как и всякий другой исполняемый формат файла Microsoft, РЕ-файл имеет набор полей, расположенных в легко доступном (по крайней мере, легко находимом) месте файла; эти поля определяют, как будет выглядеть остальная часть файла. Заголовок РЕ-файла содержит такую важную информацию, как расположение и размер областей кода программ и данных, указание на то, с какой операционной системой предполагается использовать данный файл, а также начальный размер стека.

Как и в других исполняемых форматах от Microsoft, заголовок не находится в самом начале файла. Вместо этого несколько сотен первых байтов типичного РЕ-файла заняты под заглушку DOS. Эта заглушка представляет собой минимальную DOS-программу, которая выводит что-либо вроде: «Эта программа не может быть запущена под DOS». Все это предусматривает случай, когда пользователь запускает программу Win32 в среде, которая не поддерживает Win32, получая при этом приведенное выше сообщение об ошибке (звучит разочаровывающе, конечно). После того как загрузчик Win32 отобразил в память РЕ-файл, первый байт отображения файла соответствует первому байту заглушки DOS. И это не так уж плохо. С каждой запускаемой Win32 программой пользователь получает дополнительную DOS-программу, загруженную просто так! (В Win 16 заглушка DOS не загружается в память.)

Как и в других исполняемых форматах Microsoft, настоящий заголовок можно обнаружить, найдя его стартовое смещение, которое хранится в заголовке DOS. Файл WINNT.H содержит определение структуры для заголовка заглушки DOS, что делает очень простым нахождение начала заголовка РЕ-файла. Поле e_lfanew собственно и содержит относительное смещение (или, если хотите, RVA) настоящего заголовка РЕ-файла. Чтобы установить указатель в памяти на заголовок РЕ-файла, достаточно просто сложить значение в поле с базовым адресом отображения:

Настоящее веселье начинается, когда указатель установлен на основной заголовок РЕ-файла. Основной заголовок РЕ-файла представляет структуру типа IMAGE_NT_HEADERS, определенную в файле WINNT.H. Структура IMAGE_NT_HEADERS в памяти — это то, что Windows 95 использует в качестве своей базы данных модуля в памяти. Каждый загруженный ЕХЕ-файл или DLL представлены в Windows 95 структурой IMAGE_NT_HEADERS. Эта структура состоит из двойного слова и двух подструктур, как показано ниже:

Поле Signature (сигнатура — подпись), представленное как ASCII код, — это РЕ\0\0 (два нулевых байта после РЕ). Если поле e_lfanew в заголовке DOS указало вместо обозначения РЕ обозначение NE в этом месте, значит, вы работаете с файлом Win 16 NE. Аналогично, если указано обозначение LE в поле Signature, то это файл VxD (VirtualDeviceDriver— драйвер виртуального устройства). Обозначение LX указывает на файл старой соперницы Windows 95 — OS/2.

За двойным словом — сигнатурой РЕ, в заголовке РЕ-файла следует структура типа IMAGE_FILE_HEADER. Поля этой структуры содержат только самую общую информацию о файле. Структура не изменилась по сравнению с исходными COFF-реализациями. Эта структура является частью заголовка РЕ-файла, кроме того, появляется в самом начале объектных COFF-файлов, создаваемых компиляторами Microsoft Win32. Далее приводятся поля IMAGE_FILE_HEADER.

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

WORD NumberOfSections

Количество секций в ЕХЕ- или OBJ-файле.

DWORD TimeDateStamp

Время, когда файл был создан компоновщиком (или компилятором, если это OBJ-файл). В этом поле указано количество секунд, истекших с 16:00 31 декабря 1969 года.

DWORD PointerToSymbolTable

Файловое смещение COFF-таблицы символов. Это поле используется только в OBJ- и РЕ-файлах с информацией COFF-отладчика. РЕ-файлы поддерживают разнообразные отладочные форматы, так что отладчики должны ссылаться ко входу IMAGE_DIRECTORY_ENTRY_DEBUG в каталоге данных (будет определен позднее).

DWORD NumberOfSymbols

Количество символов в COFF-таблице символов. См. предыдущее поле.

WORD SheOfOptionalHeader



Размер необязательного заголовка, который может следовать за этой структурой. В исполняемых файлах — это размер структуры IMAGE_OPTIONAL_HEADER, которая следует за этой структурой. В объектных файлах, по утверждению Microsoft, это поле всегда содержит нуль. Однако при просмотре библиотеки вывода KERNEL32.LIB можно обнаружить объектный файл с ненулевым значением в этом поле, так что относитесь к высказыванию Microsoft с некоторым скептицизмом.

WORD Characteristics

Флаги, содержащие информацию о файле. Здесь описываются некоторые важные поля (другие поля определены в WINNT.H).

Третьим компонентом заголовка РЕ-файла является структура типа IMAGE_OPTIONAL_HEADER. Для РЕ-файлов эта часть является обязательной. Формат COFF разрешает индивидуальные реализации для определения структуры дополнительной информации, помимо стандартного IMAGE_FILE_HEADER. В полях в IMAGE_OPTIONAL_HEADER разработчики РЕ-формата поместили то, что они посчитали важным дополнением к общей информации в IMAGE_FILE_HEADER.

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

WORD Magic

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

BYTE MajorLinker Version; BYTE MinorLinker Version

Версия компоновщика, который создал данный файл. Числа должны быть представлены в десятичном виде, а не в шестнадцатеричном. Типичная версия компоновщика 2.23.

DWORD SizeOfCode

Суммарный размер программных секций, округленный к верхней границе. Обычно большинство файлов имеют только одну программную секцию, так что это поле обычно соответствует размеру секции .text.

DWORD sizeOfInitializedData

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

DWORD SizeOfUninltializedData

Размер секций, под которые загрузчик выделяет место в виртуальном адресном пространстве, но которые не занимают никакого места в дисковом файле. В начале работы программы эти секции не обязаны иметь каких-либо определенных значений — отсюда название неинициализированные данные (Uninitialized Data) . Неинициализированные данные обычно находятся в секции под названием .bss.

DWORD AddressOfEntryPoint

Адрес, с которого отображение начинает выполнение. Это RVA, который можно найти в секции .text. Это поле применимо как для ЕХЕ-файла, так и для DLL.

DWORD BaseOfCode

RVA, с которого начинаются программные секции файла. Программные секции кода обычно идут в памяти перед секциями данных и после заголовка РЕ-файла. Этот RVA обычно равен 0х1000 для ЕХЕ-файлов, созданных компоновщиками Microsoft. Для TLINK32 (Borland) значение этого поля равно 0х10000, так как по умолчанию этот компоновщик выравнивает объекты на границу в 64 Кбайт в отличие от 4 Кбайт в случае компоновщика Microsoft.

DWORD BaseOfData

RVA, с которого начинаются секции данных файла. Секции данных обычно идут последними в памяти, после заголовка РЕ-файла и программных секций.

DWORD ImageBase

Когда компоновщик создает исполняемый файл, он предполагает, что файл будет отображен в определенное место в памяти. Вот именно этот адрес и хранится в этом поле. Знание адреса загрузки позволяет компоновщику провести оптимизацию. Если загрузчик действительно отобразил файл в память по этому адресу, то программа перед запуском не нуждается ни в какой настройке. Я расскажу об этом немного подробнее при обсуждении перемещений относительно базы. В исполняемых файлах NT 3.1 адрес отображения по умолчанию равен 0х10000. В случае DLL этот адрес по умолчанию равен 0х400000. В Windows 95 адрес 0х10000 нельзя использовать для загрузки 32-разрядных файлов ЕХЕ, так как он лежит в пределах линейной области адресного пространства, общего для всех процессов. Поэтому для Windows NT 3.5 Microsoft изменила для исполняемых файлов Win32 базовый адрес по умолчанию, сделав его равным 0х400000. Более старые программы, которые были скомпонованы в предположении, что базовый адрес равен 0х10000, загружаются Windows 95 дольше, потому что загрузчик должен применить базовые поправки. Я опишу базовые поправки немного позже.

DWORD SectionAlignment

После отображения в память каждая секция будет обязательно начинаться с виртуального адреса, кратного данной величине. С учетом подкачки страниц минимальная величина этого поля 0х1000 используется компоновщиком Microsoft по умолчанию. TLINK в Borland C++ использует по умолчанию 0х10000 (64 Кбайт).

DWORD FileAlignment

В случае РЕ-файла исходные данные, которые входят в состав каждой секции, будут обязательно начинаться с адреса, кратного данной величине. Значение, устанавливаемое по умолчанию, равно 0х200 байт и, вероятно, выбрано так для того, чтобы начало секции всегда совпадало с началом дискового сектора (0х200 байт — это как раз размер дискового сектора). Это поле эквивалентно размеру выравнивания сегмента/ресурса в NE-файлах. В отличие от NE-файлов, РЕ-файлы не состоят из сотен секций, так что память, теряемая при выравнивании секций файла, обычно очень незначительна.

WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion

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

WORD MajorImageVersion; WORD MinorImageVersion

Определяемое пользователем поле. Это поле позволяет иметь различные версии ЕХЕ-файлов и DLL. Эти поля устанавливаются с помощью ключа компоновщика /VERSION, например:

WORD MajorSubsystem Version; WORD MinorSubsystem Version

Это поле содержит самую старую версию подсистемы, позволяющую запускать данный исполняемый файл. Типичное значение в этом поле 4.0 (обозначает Windows 4.0, что равносильно Windows 95).

DWORD Reserved

Это поле, по-видимому, всегда равно нулю.

DWORD SizeOfImage

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

DWORD SizeOfHeaders

Размер заголовка РЕ-файла и таблицы секции (объекта). Исходные данные для секций начинаются сразу после всех составляющих частей заголовка.

DWORD Checksum

Предположительно отвечает контрольной сумме контроля циклическим избыточным кодом (CRC-контроль) для данного файла. Как и для других исполняемых форматов Microsoft, это поле обычно игнорируется и устанавливается в нуль. Однако для всех DLL драйверов, DLL, загруженных во время загрузки ОС, и серверных DLL эта контрольная, сумма должна иметь правильное значение. Алгоритм для контрольной суммы можно найти в IMAGEHLP.DLL. Исходники IMAGEHLP.DLL поставляются в WIN32 SDK.

WORD Subsystem

Тип подсистемы, которую данный исполняемый файл использует для своего пользовательского интерфейса. WINNT.H определяет следующие значения:

WORD DllCharacteristics (обозначен как вышедший из употребления в NT 3.5)

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

DWORD SizeOfStackReserve

Объем виртуальной памяти, резервируемой под начальный стек цепочки. Однако не вся эта память выделяется (см. следующее поле). По умолчанию это поле устанавливается в 0х100000 (1 Мбайт). Если пользователь указывает 0 в качестве размера стека в CreateThread(), получившаяся цепочка будет иметь стек того же размера.

DWORD SizeQfStackCommit

Количество памяти, изначально выделяемой под исходный стек цепочки. Это поле по умолчанию равно 0х1000 байт (1 страница) для компоновщиков Microsoft, тогда как TLINK32 делает его равным 0х2000 (2 страницы).

DWORD SizeOfHeapReserve

Объем виртуальной памяти, резервируемой под изначальную кучу программы. Этот дескриптор кучи можно получить, вызвав GetProcessHeap(). Однако не вся эта память выделяется (см. следующее поле).

DWORD SizeOfHeapCommit

Объем виртуальной памяти, изначально выделяемой под кучу процесса. По умолчанию компоновщик делает это поле равным 0х1000 байт.

DWORD LoaderFlags (обозначен как вышедший из употребления в NT 3.5)

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

DWORD NumberOfRvaAndShes

Количество входов в массиве DataDirectory (см. описание следующего поля). Современные программные средства всегда делают это значение равным 16.

IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]

Массив структур типа IMAGE_DATA_DIRECTORY. Начальные элементы массива содержат стартовый RVA и размеры важных частей исполняемого файла. В настоящее время некоторые элементы в конце массива не используются. Первый элемент массива — это всегда адрес и размер экспортированной таблицы функций (если она присутствует). Второй элемент массива — адрес и размер импортированной таблицы функций и т.д. Для того чтобы увидеть полный перечень определений элементов массива, см. IMAGE_DIRECTORY_ENTRY_xxx директивы #define в WINNT.H.

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

Большая часть элементов массива описывает все данные в секции. Тем не менее элемент IMAGE_DIRECTORY_ENTRY_DEBUG охватывает только небольшую часть байтов в секции .rdata. За более подробной информацией по этому вопросу обращайтесь к разделу «Секция .rdata» в этой главе.

Таблица секций

Между заголовком РЕ-файла и исходными данными для секций отображения находится таблица секций. Эта таблица содержит информацию о каждой секции отображения. Секции в отображении упорядочены по их стартовому адресу, а не в алфавитном порядке.

К настоящему моменту необходимо четко разъяснить, что же такое секция. В NE-файле программный код и данные хранятся в различных сегментах в файле. Часть заголовка NE-файла представляет собой массив структур — по одной для каждого сегмента, используемого программой. Каждая структура массива содержит информацию об одном сегменте. Хранимая информация включает тип сегмента (программа или данные), его размер и его расположение, где бы он ни находился в файле. В РЕ-файле таблица секций аналогична таблице сегментов в NE-файле.

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

Другим отличием РЕ-файлов от NE-файлов является то, как они управляют вспомогательными данными, которые используются не программой, а операционной системой. В качестве двух примеров можно привести перечень DLL, используемых исполняемыми файлами, и местонахождение таблицы привязки (fixup). В NE-файлах ресурсы не считаются сегментами. И хотя они имеют приписанные им селекторы, информация о ресурсах не хранится в таблице сегментов заголовка NE-файла. Вместо этого ресурсы сведены в отдельную таблицу в конце заголовка NE-файла. Информации об импортированных и экспортированных функциях тоже не гарантируется выделение своего собственного сегмента, она накапливается в пределах заголовка NE-файла.

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

Вскоре я начну рассматривать отдельные секции, но сначала мне нужно описать данные, с помощью которых операционная система работает с секциями. Сразу после заголовка РЕ-файла в памяти следует массив из IMAGE_SECTION_HEADER. Количество элементов этого массива задается в заголовке РЕ-файла (поле IMAGE_NT_HEADER.FileHeader.NumberOfSections). Программа PEDUMP выводит таблицу секций, все поля и атрибуты секций. Рис. 8.2 показывает вывод с помощью PEDUMP таблицы секций для типичного ЕХЕ-файла. Рис. 8.3 показывает вывод с помощью PEDUMP таблицы секций для типичного OBJ-файла.

Рис. 8.2. Типичная таблица секций ЕХЕ-файла

Рис. 8.3 Типичная таблица секций объектного файла

Каждый IMAGE_SECTION_HEADER представляет собой полную базу данных об одной секции файла ЕХЕ или OBJ и имеет следующий формат.

BYTE Name[IMAGE_SIZEOF_SHORT_NAME]

Это 8-байтовое имя в стандарте ANSI (не Unicode), которое именует секцию. Большинство имен секций начинается с точки (например, .text), но это не обязательно, вопреки тому, в чем пытаются вас уверить отдельные документы по РЕ-файлам. Пользователь может давать имена своим собственным секциям с помощью либо сегментной директивы в ассемблере, либо с помощью директив #pragma data_seg и #pragma code_seg компилятора Microsoft C/C++. (Пользователи Borland C++ должны использовать #pragma codeseg.) Необходимо отметить, что, если имя секции занимает 8 полных байтов, отсутствует завершающий байт NULL. (Программа TDUMP в Borland C++ 4.Ox упускает из виду это обстоятельство и изрыгает последующий мусор из некоторых РЕ ЕХЕ-файлов.) Если вы приверженец printf(), то можете использовать «%.8s», чтобы не копировать строку-имя в другой буфер для завершения ее нулевым байтом.

Это поле имеет различные назначения в зависимости от того, встречается ли оно в ЕХЕ- или OBJ-файле. В ЕХЕ-файле оно содержит виртуальный размер секции программного кода или данных. Это размер до округления на ближайшую верхнюю границу файла. Поле SizeOfRawData дальше в этой структуре содержит это округленное значение. Интересно, что Borland TLINK32 меняет местами значение этого поля и поля SizeOfRawData и, тем не менее, остается правильным компоновщиком. В случае OBJ-файлов это поле указывает физический адрес секции. Первая секция начинается с адреса 0. Чтобы получить физический адрес следующей секции, надо прибавить значение в SizeOfRawData к физическому адресу данной секции.

DWORD VirtualAddress

В случае ЕХЕ-файлов это поле содержит RVA, куда загрузчик должен отобразить секцию. Чтобы вычислить реальный начальный адрес данной секции в памяти, необходимо к виртуальному адресу секции, содержащемуся в этом поле, прибавить базовый адрес отображения. Средства Microsoft устанавливают по умолчанию RVA первой секции равным 0х1000. Для объектных файлов это поле не несет никакого смысла и устанавливается в 0.

DWORD SizeOfRawData

В ЕХЕ-файлах это поле содержит размер секции, выровненный на ближайшую верхнюю границу размера файла. Например, допустим, что размер выравнивания файла 0х200. Если поле VirtualSize указывает, что длина секции Ох35А байт, то в данном поле будет указано, что размер секции 0х400 байт. Для OBJ-файлов это поле содержит точный размер секции, сгенерированной компилятором или ассемблером. Другими словами, для OBJ-файлов оно эквивалентно полю VirtualSize в ЕХЕ-файлах.

DWORD PointerToRawData

Это файловое смещение участка, где находятся исходные данные для секции. Если пользователь сам отображает в память РЕ- или COFF-файл (вместо того, чтобы доверить загрузку операционной системе), это поле важнее, чем поле VirtualAddress. Причиной является то, что в этом случае получится абсолютно линейное отображение всего файла, так что данные для секций будут находиться по этому смещению, а не по RVA, указанному в поле VirtualAddress.

DWORD PointerToRelocations

В объектных файлах это файловое смещение информации о поправках для данной секции. Информация о поправках в любой секции объектного файла следует за исходными данными для этой секции. В ЕХЕ-файлах это (и следующее) поле не несет смысловой нагрузки и устанавливается в нуль. Когда компоновщик создает ЕХЕ-файл, он разрешает большинство привязок, а во время загрузки остается разрешить только базовые адресные поправки и импортированные функции. Информация о базовых поправках и импортированных функциях хранится в секциях базовых поправок и импортированных функций, так что нет необходимости в ЕХЕ-файле помещать данные поправок для каждой секции после исходных данных секции.

DWORD PointerToLinenumbers

Файловое смещение таблицы номеров строк. Таблица номеров строк ставит в соответствие номера строк исходного файла адресам, по которым можно найти код, сгенерированный для данной строки. В современных отладочных форматах, таких как формат CodeView, информация о номерах строк хранится как часть информации отладчика. В отладочном формате COFF, однако, информация о номерах строк концептуально отлична от информации о символьных именах/типах. Обычно только секции с программным кодом (например, .text или CODE) имеют номера строк. В ЕХЕ-файлах номера строк собраны в конце файла после исходных данных для секций. В объектных файлах таблица номеров строк для секции следует за исходными данными секции и таблицей перемещений для этой секции. Я рассматриваю формат таблиц номеров строк позже, в разделе «Информация COFF-отладчика».

WORD NumberOfRelocations

Количество перемещений в таблице поправок для данной секции (поле PointerToRelocations приведено выше). Это поле используется, по-видимому, только в объектных файлах.

WORD NumberOfLinenumbers

Количество номеров строк в таблице номеров строк для данной секции (поле PointerToLinenumbers приведено выше).

DWORD Characteristics

То, что большая часть программистов называет флагами (flags), формат COFF/PE называет характеристиками (characteristics). Это поле представляет собой набор флагов, которые указывают на атрибуты секции (программа/данные, предназначен для чтения, предназначен для записи и т.п.). За полным перечнем всех возможных атрибутов секции обращайтесь к IMAGE_SCN_XXX_XXX defines в файле заголовка WINNT.H. Некоторые из самых важных флагов приведены в табл. 8.1.

Флаг Использование
0х00000020 Эта секция содержит программный код. Как правило, устанавливается вместе с флагом (0х80000000)
0х00000040 Данная секция содержит инициализированные данные. Почти для всех секций, кроме исполняемых и .bss секций, этот флаг установлен
0х00000080 Данная секция содержит неинициализированные данные (например, .bss секции)
0х00000200 Данная секция содержит комментарии или какой-нибудь другой вид информации. Типичное использование такой секции — это секция .drectve, создаваемая компилятором и содержащая команды для компоновщика
0х00000800 Содержимое данной секции не должно быть помещено в конечный ЕХЕ-файл. Такая секция используется компилятором/ассемблером для передачи информации компоновщику
0х02000000 Данную секцию можно отбросить, так как она не используется программой, после того как последняя загружена. Чаще всего встречается отбрасываемая секция — это секция базовых поправок (.reloc)
0х10000000 Данная секция является совместно используемой. При использовании с DLL данные в такой секции используются совместно всеми процессами, использующими эту DLL. По умолчанию секции данных не являются совместно используемыми, т.е. каждый процесс, использующий DLL, имеет свою собственную отдельную копию такой секции данных. Говоря более техническим языком, совместно используемая секция дает указание менеджеру памяти устанавливать отображение страниц для этой секции так, что все процессы, использующие DLL, ссылаются на одну и ту же физическую страницу в памяти. Чтобы сделать секцию совместно используемой, установите атрибут SHARED во время компоновки. Например: LINK/SECTION:MYDATA,RWS. указывает компоновщику, что секция с названием MYDATA должна быть доступной для чтения, записи и совместно используемой. По умолчанию сегменты данных DLL Borland C++ имеют атрибуты совместного использования
0х20000000 Данная секция является исполняемой. Этот флаг обычно устанавливается каждый раз, когда устанавливается флаг «Программа» (Contains Code) (0х00000020)
0х40000000 Данная секция предназначена для чтения. Этот флаг почти всегда установлен для секций ЕХЕ-файлов
0х80000000 Данная секция предназначена для записи. Если этот флаг не установлен в секции ЕХЕ-файла, загрузчик должен отметить отображенные в память страницы как предназначенные только для чтения или только для исполнения. Типичные секции с этим атрибутом — это .data и .bss

Таблица 8.1. Флаги COFF-секций

Интересно отметить, чего не хватает в информации, хранящейся в каждой секции. Во-первых, следует обратить внимание на отсутствие любых атрибутов PRELOAD. Файловый формат NE позволяет пользователю определять атрибуты PRELOAD для сегментов, которые должны быть загружены сразу во время загрузки модуля. Файловый формат OS/2 2.0 LX имеет нечто похожее, что позволяет пользователю давать указание о том, что предварительно должно быть загружено до восьми страниц. РЕ-формат, напротив, не имеет ничего подобного. Исходя из этого, приходится заключить, что Microsoft абсолютно уверена в исполнимости требований загрузки страниц для своих реализаций Win32.

В РЕ-формате также отсутствует таблица поиска промежуточных страниц. Эквивалент IMAGE_SECTION_HEADER в файловом формате OS/2 LX не указывает непосредственно, где находятся в файле данные и программный код секции. Вместо этого файл формата OS/2 LX содержит таблицу поиска страниц, определяющую атрибуты и расположение в файле определенных диапазонов страниц внутри секции. РЕ-формат обходится без всего этого и гарантирует, что данные из секции будут храниться непрерывно в файле. Сравнивая два формата, можно сказать, что LX-метод более гибок, тогда как стиль РЕ намного проще в работе. Имея опыт написания программ просмотра файлов и дизассемблеров для обоих форматов, я могу лично поручиться за это!

Другим благоприятным отличием РЕ-формата от более старого NE-формата является то, что расположения элементов хранятся в виде простых смещений типа DWORD. В NE-формате расположение практически любого элемента хранилось в виде величины сектора. Чтобы посчитать действительное файловое смещение, нужно было сначала найти размер выравнивания в заголовке NE-файла и перевести его в размер сектора (обычно 16 или 512 байт). Затем нужно было умножить размер сектора на указанное смещение сектора, чтобы получить действительное файловое смещение. Если что-нибудь по случайности не хранится в виде секторного смещения в NE-файле, оно, вероятно, хранится как смещение относительно заголовка NE-файла. Ввиду того, что заголовок NE-файла не находится в начале файла, пользователю приходится привлекать в свою программу файловое смещение заголовка NE-файла. В противоположность этому РЕ-файлы определяют положение различных элементов, используя простые смещения относительно того положения, в которое файл был отображен в памяти. В общем, с РЕ значительно проще работать, чем с форматами NE, LX или LE (при условии, что можно использовать отображаемые в память файлы).

Часто встречающиеся секции

После того как читатель получил общее представление о том, что такое секция и как секции размещаются, можно перейти к более глубокому изучению секций, часто встречающихся в ЕХЕ- и объектных файлах. И хотя этот перечень ни в коем случае нельзя назвать исчерпывающим, он, тем не менее, включает секции, с которыми пользователь сталкивается ежедневно (даже если он об этом и не подозревает). Секции представлены в порядке важности и в соответствии с вероятной частотой их появления.

Секция .text

В этой секции собран весь программный код общего назначения, генерируемый компилятором или ассемблером. Поскольку РЕ-файлы работают в 32-разрядном режиме и не привязаны к 16-разрядным сегментам, нет необходимости разбивать программный код из разных исходных файлов по разным секциям. Вместо этого компоновщик объединяет все секции .text из различных объектных файлов в одну большую секцию .text в ЕХЕ-файле. Компилятор Borland C++ помещает весь программный код в сегмент с названием CODE. Таким образом, РЕ-файлы, созданные с помощью Borland C++, имеют секцию с названием CODE вместо секции .text. (См. в этой главе раздел «Секции CODE и .icode Borland» для уточнения деталей.)


Я был удивлен, обнаружив дополнительный программный код в секции .text, помимо того, который я создал компилятором или использовал из библиотек поддержки выполнения программы. В РЕ-файле, в случае вызова функции из другого модуля (например, GetMessage() из USER32.DLL), инструкция CALL, сгенерированная компилятором, не передает управление непосредственно данной функции в DLL. Вместо этого инструкция CALL передает управление команде JMP DWORD PTR[XXXXXXXX], также находящейся в секции .text. Команда JMP перескакивает к адресу, хранящемуся в двойном слове в секции .idata. Это двойное слово в секции .idata содержит настоящий адрес точки входа функции операционной системы, как показано на рис. 8.4.

После некоторых размышлений я понял, почему вызовы DLL реализованы таким образом. Стянув все вызовы данной функции DLL в одно место, загрузчик не будет «латать» каждую инструкцию, вызывающую DLL. Все, что остается загрузчику, это поместить правильный адрес целевой функции в двойное слово в секции .idata. Не нужно «латать» никаких инструкций CALL. Это представляет большое отличие от ситуации с NE-файлами, в которых каждый сегмент содержит перечень привязок, которые должны применяться к сегменту. Если какой-нибудь сегмент вызывает данную функцию из DLL 20 раз, загрузчику нужно будет 20 раз копировать адрес функции в этот сегмент. Недостатком РЕ-метода является то, что пользователь не может инициализировать переменную истинным адресом функции DLL. Например, пользователь может полагать, что нечто вроде:

поместит адрес GetMessage в переменную pfnGetMessage. В Win 16 это сработает, а в Win32 — нет. В Win32 переменная pfnGetMessage в итоге будет содержать адрес переходника JMP DWORD PTR [ХХХХХХХХ] в секции .text, о котором было упомянуто раньше. Если бы пользователь захотел сделать вызов с помощью указателя функции, то все произошло бы так, как пользователь и ожидал. Если вы захотите, однако, прочитать байты в начале GetMessage(), вам это не удастся (если вы не проделали дополнительной работы, проследовав за «указателем» .idata самостоятельно). Я вернусь к обсуждению этой темы позже в разделе «Импорт РЕ-файлов».

Рис. 8.4. Вызов импортируемых функций из РЕ-файла

После того как я написал начальный вариант этой главы, появился Visual C++ 2.0; эта версия содержала новинку в вызове импортированных функций. Если заглянуть в системные файлы заголовков из Visual C++ 2.0 (например, WINBASE.H), можно обнаружить отличие от заголовков Visual C++ 1.0. В Visual C++ 2.0 прототипы функций операционной системы в системных DLL включают __declspec(dllimport) как часть их определения. Оказалось, что __declspec(dllimport) имеет полезное свойство при вызове импортированных функций. Когда пользователь вызывает импортированную функцию, прототипированную с помощью __declspec(dllimport), компилятор не создает вызов инструкции JMP DWORD PTR[XXXXXXXX] где-нибудь еще в модуле. Вместо этого компилятор генерирует вызов функции в виде CALL DWORD PTR[XXXXXXXX]. Адрес [ХХХХХХХХ] находится в секции .idata. Это тот же самый адрес, который был бы использован, если бы использовался наш старый знакомый JMP DWORD PTR[XXXXXXXX]. Насколько я знаю, все версии Borland C++, вплоть до 4.5, не имеют этого свойства.

Секции Borland CODE и .icode

Компилятор и компоновщик Borland C++ не работают с COFF-форматом объектных файлов. Вместо этого Borland предпочел придерживаться 32-разрядной версии формата Intel OMF. Хотя Borland мог бы заставить компилятор генерировать сегменты с именем .text, эта фирма предпочла CODE в качестве имени сегмента по умолчанию. Чтобы определить имя секции в РЕ-файле, компоновщик Borland (TLINK32.EXE) извлекает имя сегмента из объектного файла и обрезает его до 8 символов (в случае необходимости). Из-за этого РЕ-файлы Borland C++ имеют секцию CODE, а не секцию .text.

Разница в именах секций, конечно, не является главным, — существует более важное отличие в том, как Borland РЕ-файлы компонуются с другими модулями. Как было сказано раньше, при обсуждении секции .text, все вызовы объектных файлов идут через переходник JMP DWORD PTR [ХХХХХХХХ]. В системе Microsoft этот переходник приходит в ЕХЕ-файл из секции .text импортируемой библиотеки. Менеджер библиотек создает импортируемую библиотеку (и переходник), когда пользователь присоединяет внешнюю DLL. В результате компоновщик не обязан «знать», как создавать эти переходники самому. Библиотека импорта — это в действительности только некоторое количество программного кода и данных для компоновки в РЕ-файл.

Система оперирования с импортированными функциями Borland иная и представляет просто обобщение действий, которые проводились для 16-разрядных NE-файлов. Библиотеки импорта, используемые компоновщиком Borland, в действительности представляют перечень имен функций и DLL, в которых они находятся. Таким образом, TLINK32 отвечает за определение того, какие привязки предназначены для внешних DLL, и за генерацию соответствующего переходника JMP DWORD PTR[XXXXXXXX]. В Borland C++ 4.0 TLINK32 хранит переходники, которые он создает, в секции с именем .icode. В Borland C++ 4.02 TLINK32 изменен, чтобы собрать все переходники JMP DWORD PTR[XXXXXXXX] в секции CODE.

Секция .data

Как по умолчанию программный код попадает в секцию .text, так и инициализированные данные попадают в секцию .data. Инициализированные данные состоят из тех глобальных и статических переменных, которые были проинициализированы во время компиляции. Они также включают строковые литералы (например, строку «Hello World» в программе C/C++). Компоновщик объединяет все секции .data из разных объектных и LIB-файлов в одну секцию .data в ЕХЕ-файле. Локальные переменные расположены в стеке цепочки и не занимают места в секциях .data и .bss.

Секция DATA

Borland C++ использует по умолчанию имя DATA для секции данных. Она эквивалентна секции .data в компиляторе Microsoft (см. предыдущий пункт «Секция .data»).

Cекция .bss

В секции .bss хранятся неинициализированные статические и глобальные переменные. Компоновщик объединяет все секции .bss из разных объектных и LIB-файлов в одну секцию .bss в ЕХЕ-файле. В таблице секций поле RawDataOffset для секции .bss устанавливается в 0, показывая, что эта секция не занимает никакого места в файле. TLINK32 не создает секцию .bss. Вместо этого он расширяет виртуальный размер секции DATA так, чтобы вместить неинициализированные данные.

Секция .CRT

Еще одна секция для инициализированных данных, используемая библиотеками поддержки выполнения программы Microsoft C/C++ (отсюда и название .CRT — C/C++ runtime libraries). Данные из этой секции используются для таких целей, как вызов конструкторов статических классов C++ перед вызовом main или WinMain.

Секция .rsrc

Секция .rsrc содержит ресурсы модуля. На ранних стадиях развития NT выходной .RES-файл 16-разрядного RC.EXE имел формат, который не воспринимался компоновщиком Microsoft. Программа CVTRES переводила эти .RES-файлы в объектные файлы COFF-формата, помещая данные ресурсов в секцию .rsrc внутри объектного файла. После этого компоновщик мог рассматривать объектный файл ресурсов как еще один объектный файл для компоновки, что позволяет компоновщику не вникать во что-либо особенное о ресурсах. Более современные компоновщики Microsoft оказались способными обрабатывать файлы .RES непосредственно. Я расскажу о формате секции ресурсов позже в этой главе, в разделе «Ресурсы РЕ-файлов».

Секция .idata

Секция .idata содержит информацию о функциях (и данных), которые модуль импортирует из других DLL. Эта секция эквивалентна справочной таблице модуля в NE-файле. Коренное отличие состоит в том, что каждая функция, импортируемая РЕ-файлом, перечислена в этой секции. Чтобы отыскать эквивалентную информацию в NE-файле, пользователю пришлось бы рыться в поправках в конце исходных данных для каждого из сегментов. Я расскажу более подробно о формате таблицы импорта позже в этой главе, в разделе «Импорт РЕ-файлов».

Секция .edata

Секция .edata представляет перечень функций и данных, которые РЕ-файл экспортирует для использования другими модулями. Ее эквивалент для NE-файла — это комбинация таблицы входа, таблицы резидентных имен и таблицы нерезидентных имен. В отличие от Win 16, здесь редко возникает необходимость экспортировать что-либо из ЕХЕ-файлов, так что обычно секцию .edata можно увидеть только в DLL. Исключением являются ЕХЕ-файлы, созданные Borland C++, которые, по-видимому, всегда экспортируют функцию (_GetExceptDLLinfo) для внутреннего использования библиотекой поддержки исполнения программы.

Формат таблицы экспорта обсуждается позже в разделе «Экспорт РЕ-фаилов» в этой главе. При использовании средств Microsoft данные из секции .edata попадают в РЕ-файл через файл .ЕХР. Другими словами, компоновщик не создает эту информацию сам. Вместо этого он полагается на менеджера библиотек (LIB32), сканирующего OBJ-файлы и создающего файл .ЕХР, который компоновщик добавляет в свой перечень модулей для компоновки. Да, именно так! Эти надоедливые файлы .ЕХР — в действительности всего лишь OBJ-файлы с другим расширением. Используя программу PEDUMP (представлена позже в этой главе) с ключом /S (показать таблицу символов), можно увидеть функции, экспортируемые через файлы .ЕХР.

Секция .reloc

Секция .reloc содержит таблицу базовых поправок (base relocation). Базовая поправка — это настройка по отношению к инструкции или значению инициализированной переменной; ЕХЕ-файлы или DLL нуждаются в такой поправке, если загрузчик не может загрузить файл по адресу, который предполагался компоновщиком. Если загрузчику удается загрузить отображение по указанному компоновщиком базовому адресу, загрузчик игнорирует поправочную информацию в этой секции.

Если вам хочется попытать счастья и вы надеетесь, что загрузчик всегда сможет загрузить отображение по указанному компоновщиком базовому адресу, используйте ключ /FIXED, чтобы компоновщик удалил эту информацию. Хотя это и сохраняет место в исполняемом файле, однако это же может сделать данный файл неработающим на других платформах Win32. Пусть, например, вы создали ЕХЕ-файл для NT и расположили его по адресу 0х10000. Если вы дали указание компоновщику удалить поправки, данный ЕХЕ-файл не будет работать в Windows 95, так как там адрес 0х10000 не доступен (наименьший адрес загрузки в Windows 95 — 0х400000, т.е. 4 Мбайт).

Необходимо отметить, что инструкции JMP и CALL, генерируемые компилятором, используют смещения относительно этих инструкций, а не действительные смещения в 32-разрядном сегменте. Если отображение необходимо загрузить по базовому адресу, отличному от указанного компоновщиком, не нужно изменять эти инструкции, поскольку они используют относительную адресацию. В результате поправок не так много, как могло бы показаться. Поправки обычно требуются только для инструкций, использующих 32-разрядные смещения для данных. Допустим, имеются следующие объявления глобальных переменных:

Если компоновщик указал в качестве базового адреса отображения 0х10000, адрес переменной i будет в итоге содержать что-нибудь наподобие 0х12004. В памяти, используемой под указатель ptr, компоновщик поместит значение 0х12004, поскольку это— адрес переменной i. Если загрузчик решит (по каким-либо причинам) загрузить файл по базовому адресу 0х70000, адресом переменной i будет в таком случае 0х72004. Однако значение переменной ptr перед инициализацией теперь будет неправильным, так как / сейчас находится на 0х60000 байт выше в памяти.

Вот здесь-то поправочная информация и вступает в игру. Секция .reloc — это перечень перемещений, т.е. мест в отображении, в которых необходимо принимать в учет различие между принятым компоновщиком адресом загрузки и реальным адресом загрузки. Я расскажу подробнее о поправках в разделе «Базовые поправки РЕ-файлов».

Секция .tls

Когда используется директива компилятора «__declspec(thread)», определяемые данные не попадают ни в секцию .data, ни в секцию .bss. Вместо этого их копия в итоге оказывается в секции .tls. Имя секции .tls происходит от thread local storage (локальная память потока). Эта секция связана с семейством функций TlsAlloc().

Локальную память потока можно представить как отдельный набор глобальных переменных для каждого потока. Это означает, что каждый поток может иметь свой набор величин статических данных, однако программный код использует эти данные, безотносительно к тому, какой поток исполняется. Рассмотрим программу, имеющую несколько потоков, которые работают над одной и той же задачей, т.е. исполняют один и тот же программный код. Если пользователь объявил переменную локального хранения потока, например:

то каждый поток будет иметь свою собственную копию переменной i.

Также возможен явный запрос на использование локальной памяти потока во время исполнения программы с помощью функций TlsAlloc, TlsSetValue и TlsGetValue. (В главе 3 подробно рассказано о функциях TlsXXX.) В большинстве случаев гораздо проще объявлять данные в программе с помощью __declspec (thread), чем распределять память для потока и запоминать указатель на нее в слоте, выделенном функцией TlsAlloc().

Необходимо упомянуть об одной отрицательной стороне секции .tls и переменных __declspec (thread). В NT и Windows 95 механизм локального хранения потока не будет работать для DLL, если эта DLL загружена динамически с помощью LoadLibrary(). Для ЕХЕ или неявно загруженной DLL все будет работать прекрасно. Если нет возможности явно скомпоновать DLL, а для потока необходима его локальная память, то придется ее распределить динамически с помощью TlsAlloc() и TlsGetValue(). Необходимо отметить, что на самом деле блоки памяти для потока не хранятся в секции .tls во время исполнения программы. Другими словами, переключая потоки, менеджер памяти не изменяет физическую страницу памяти, отображенную в секцию .tls модуля. Вместо этого секция .tls представляет просто данные, используемые для инициализации настоящих блоков данных для потока. Инициализация областей данных для потока производится совместными усилиями операционной системы и библиотек поддержки выполнения программы. Это требует дополнительных данных — каталога TLS, хранящегося в секции .rdata.

Секция .rdata

Секция .rdata имеет, как минимум, четыре предназначения. Во-первых, в ЕХЕ-файлах, созданных с помощью компоновщика Microsoft Link, секция .rdata содержит каталог отладки (в объектных файлах такого каталога нет). В ЕХЕ-файлах, созданных с помощью компоновщика TLINK32, каталог отладки находится в секции .debug. Каталог отладки представляет массив структур IMAGE_DEBUG_DIRECTORY. Эти структуры содержат информацию о типе, размере и местонахождении различных видов отладочной информации, содержащейся в файле. Есть три главных вида отладочной информации: CodeView, COFF и FPO. На рис. 8.5 показан вывод типичного каталога отладки с помощью программы PEDUMP.

Рис. 8.5. Типичный каталог отладки

Каталог отладки не обязательно должен находиться в начале секции .rdata. Для того чтобы обнаружить начало каталога отладки, следует использовать RVA, содержащийся в седьмой строке (IMAGE_DIRECTORY_ENTRY_DEBUG) каталога данных. (Каталог данных находится в конце заголовка РЕ-файла.) Чтобы определить количество входов в каталоге отладки для компоновщика Microsoft, нужно разделить размер этого каталога (находится в поле размера в указанной выше строке каталога данных) на размер структуры IMAGE_DEBUG_DIRECTORY. В случае же компоновщика TLINK32 соответствующее поле размера уже содержит количество строк каталога отладки, а не общую длину в байтах. Программа PEDUMP обрабатывает обе ситуации.

Другой важной частью секции .rdata является строка описания. Если пользователь определяет элемент DESCRIPTION в файле .DEF в своей программе, то в секции .rdata появляется строка описания. В NE-формате строка описания всегда является первой строкой нерезидентной таблицы имен. Строка описания предназначена для хранения полезного текста, описывающего файл. К сожалению, я не обнаружил простого способа найти ее. Я встречал РЕ-файлы, в которых строка описания находилась перед таблицей отладки, и файлы, в которых она была после таблицы отладки. Мне неизвестен ни один последовательный метод нахождения строки описания (даже более того — определения того, имеется ли она вообще).

Кроме того, секция .rdata используется для GUID при OLE-программировании. Библиотека импорта UUID.LIB содержит набор 16-разрядных GUID, используемых в случаях ID-интерфейсов. Эти GUID в итоге оказываются в секции .rdata ЕХЕ-файла или DLL.

Последнее применение секции .rdata, о котором мне известно,— это место для хранения каталога TLS (ThreadLocalStorage — локальная память цепочки). Каталог TLS — это специальная структура данных, используемая библиотекой поддержки выполнения программы для явного обеспечения локальной памяти цепочки для переменных, объявленных в программе. Формат каталога TLS можно найти на CD-ROM MSDN (Microsoft Developer Network) в спецификации Portable Executable and Common Object File Format. Наибольший интерес в каталоге TLS представляют указатели начала и конца копии данных, используемых для инициализации каждого блока локальной памяти цепочки. RVA каталога TLS находится в элементе IMAGE_DIRECTORY_ENTRY_TLS в каталоге данных заголовка РЕ-файла. Реальные данные, используемые для инициализации блоков TLS, можно найти в секции .tis (описана выше).

Секции .debug$S и .debug$T

Секции .debug$S и .debug$T есть только в COFF-объектных файлах; и они содержат информацию о символах CodeView и их типах. Названия этих секций произошли от названий сегментов, используемых для этой цели предыдущими компиляторами Microsoft ($$SYMBOLS и $$TYPES). Единственное назначение секции .debug$T — хранить путь к файлу .PDB, содержащему информацию CodeView о типах для всех объектных файлов проекта. Информацию CodeView для создаваемого ЕХЕ-файла компоновщик помещает в файл .PDB.

Секция .drective

Эта секция есть только в объектных файлах. Она содержит текст команд для компоновщика. Например, следующие строки появлялись в секции .drective в любом объектном файле, который я компилировал с помощью компилятора Microsoft Visual C++:

При использовании в программе __declspec(export) компилятор просто вырабатывает эквивалент командной строки в секции .drectve (например, exportMyFunction).

Секции, содержащие символ $ (для LIB и объектных файлов)

В объектных файлах секции, содержащие $ (например, .idata$2) обрабатываются компоновщиком по-особому. Компоновщик объединяет все секции, имеющие одинаковые символы в имени перед символом $. Именем получившейся секции считается то, что находится перед символом $. Таким образом, если компоновщик встречает секции с именами .idata$2 и ,idata$6, он объединяет их в одну секцию с именем .idata.

Упорядочение объединяемых секций происходит в соответствии с символами после $. Компоновщик соблюдает лексический порядок, так что секция .idata$2 будет идти перед секцией .idata$6, а секция .data$A — перед секцией .data$B.

Для чего же используется символ $? Чаще всего он используется библиотеками импорта, которые в секциях .idata$x хранят различных порции суммарной секции .idata. Это достаточно интересно. Компоновщик не должен создавать секцию .idata с нуля. Вместо этого итоговая секция .idata создается из секций объектных или LIB-файлов, которые компоновщик рассматривает как любую другую секцию, подлежащую компоновке.

Разнообразные секции

Работая с программой PEDUMP, я время от времени встречал и другие секции. Например, Windows 95 GDI32.DLL содержит секцию данных под названием _GPFIX, назначение которой предположительно связано с обработкой ошибок GP.

Отсюда можно извлечь двойной урок. Не обязательно придерживаться использования только стандартных секций, производимых компилятором или ассемблером. Если вам необходима отдельная секция, не бойтесь использовать ее. При работе с компилятором Microsoft C/C++ пользуйтесь #pragma code_seg и #pragma data_seg. Пользователи компилятора Borland могут использовать #pragma codeseg и #pragma dataseg. В ассемблере можно просто создать 32-разрядный сегмент с именем, отличным от имен стандартных секций. Компоновщик TLINK32 объединяет сегменты программного кода одного класса, так что следует либо присваивать каждому сегменту программного кода свое уникальное имя класса, либо отключить упаковку сегментов программного кода. Другой урок: необычные имена секций часто позволяют глубже взглянуть на назначение и реализацию конкретного РЕ-файла.

Импортирование в РЕ-файлах

Раньше я описывал, каким образом вызовы функций из внешних DLL не обращаются к этим DLL непосредственно. Вместо этого инструкция CALL передает управление инструкции JMP DWORD PTR[XXXXXXXX] где-то в секции .text исполняемого файла (или в секции .icode, если используется Borland C++ 4.0). Если используется _declspec(dllimport) в Visual C++, вызов функции принимает вид CALL DWORD PTR[XXXXXXXX]. В обоих случаях адрес, который ищет инструкция JMP или CALL, хранится в секции .idata. Инструкция JMP или CALL передает управление по этому адресу, являющемуся предполагаемым адресом цели. Если вы не все поняли, вернитесь к рис. 8.4.

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

Секция .idata (или таблица импорта, как мне больше нравится ее называть) начинается с массива, состоящего из IMAGE_IMPORT_DESCRIPTOR. Каждый элемент (IMAGE_IMPORT_DESCRIPTOR) соответствует одной из DLL, с которой неявно связан данный РЕ-файл. Количество элементов в массиве нигде не учитывается. Вместо этого последняя структура массива IMAGE_IMPORT_DESCRIPTOR имеет поля, содержащие NULL. Структура IMAGEJMPORT_DESCRIPTOR имеет следующий формат.

DWORD Characteristics/OriginalFirstThunk

В этом поле содержится смещение (RVA) массива двойных слов. Каждое из этих двойных слов в действительности является объединением IMAGE_THUNK_DATA. Каждое двойное слово IMAGE_THUNK_DATA соответствует одной функции, импортируемой данным ЕХЕ-файлом или DLL. Я опишу формат IMAGE_THUNK_DATA DWORD немного позже. Если используется утилита BIND, то этот массив двойных слов не изменяется, а модифицируется массив двойных слов FirstThunk (описан вкратце).

DWORD TimeDateStamp

Отметка о времени и дате, указывающая, когда был создан данный файл. Обычно это поле содержит 0. Тем не менее утилита Microsoft BIND обновляет это поле датой и временем из DLL, на которую указывает данный 1MAGE_IMPORT_DESCRIPTOR.

DWORD ForwarderChain

Это поле имеет отношение к передаче, когда одна DLL передает ссылку на какую-то свою функцию другой DLL. Например, в Windows NT KERNEL32.DLL посылает несколько своих экспортируемых функций NTDLL.DLL. Приложение может посчитать это вызовом функции в KERNEL32.DLL, но в итоге это будет вызов в NTDLL.DLL. Это поле содержит указатель в массив FirstThunk (описан вкратце). Функция, указанная этим полем, будет послана в другую DLL. К сожалению, формат посылки функции лишь вкратце описан в документации Microsoft. За дополнительной информацией о посылке обращайтесь к разделу «Передача экспорта» в этой главе.

DWORD Name

Это RVA строки символов ASCII, оканчивающейся нулем и содержащей имена импортируемых DLL (например, KERNEL32.DLL или USER32.DLL).

PIMAGE_THUNK_DATA FirstThunk;

RVA-смещение массива двойных слов IMAGE_THUNK_DATA. В большинстве случаев двойное слово рассматривается как указатель на структуру IMAGE_IMPORT_BY_NAME. Однако можно импортировать функцию также и по порядковому номеру.

Важными частями IMAGE_IMPORT_DESCRIPTOR являются имя импортируемой DLL и два массива элементов IMAGE_THUNK_DATA DWORD. Каждое двойное слово IMAGE_THUNK_DATA соответствует одной импортируемой функции. В ЕХЕ-файлах оба эти массива (на них указывают поля Characteristics и FirstThunk) идут параллельно друг другу и оканчиваются элементом-указателем NULL в конце каждого массива.

Зачем нужны два параллельных массива указателей на структуры IMAGE_THUNK_DATA? Первый массив (на него указывает поле Characteristics) оставляется неизменным. Иногда его называют таблицей имен-намеков (hint-name table). Второй массив, на который указывает поле FirstThunk в IMAGE_IMPORT_DESCRIPTOR, переписывается РЕ-загрузчиком. Загрузчик последовательно перебирает IMAGE_THUNK_DATA и находит адрес функции, на которую ссылается последний. Затем загрузчик записывает в двойное слово IMAGE_THUNK_DATA адрес импортируемой функции.

Раньше я упоминал о том, что вызовы функций DLL происходят через переходник «JMP DWORD PTR[CXXXXXXX]». [XXXXXXXX] в переходнике ссылается на один из элементов массива FirstThunk. Поскольку массив, состоящий из IMAGE_THUNK_DATA, переписывается загрузчиком и в итоге содержит адреса всех импортируемых функций, он называется «Таблица адресов импорта». Рис. 8.6 показывает оба этих массива.

Для пользователей Borland есть некоторая дополнительная тонкость в этом описании. В РЕ-файле, созданном TLINK32, отсутствует один из массивов. В таких файлах поле Characteristics в IMAGE_IMPORT_DESCRIPTOR (т.е. в массиве имен-намеков) равно нулю (очевидно, загрузчики Win32 не нуждаются в этом массиве). Таким образом, во всех РЕ-файлах вообще обязан быть только массив, на который указывает поле FirstThunk (таблица адресов импорта).

На этом можно было бы и закончить изложение, если бы при написании программы PEDUMP я не столкнулся с одной интересной проблемой. В постоянной погоне за оптимизацией Microsoft «оптимизировала» массивы IMAGE_THUNK_DATA в системных DLL под Windows NT (например, KERNEL32.DLL). После этой оптимизации IMAGE_THUNK_DATA не содержит информации, необходимой для нахождения импортируемой функции. Вместо этого двойные слова IMAGE_THUNK_DATA уже содержат адреса импортируемых функций. Другими словами, для загрузчика нет необходимости выискивать адреса функций и переписывать массив переходников с адресами импортируемых функций. Массив уже содержит адреса импортируемых функций еще до загрузки. (Утилита BIND из Win32 SDK осуществляет такую оптимизацию.) К сожалению, это вызывает трудности при работе программ просмотра, предполагающих, что массив содержит смещения RVA для элементов IMAGE_THUNK_DATA. Вы можете подумать: «А почему не использовать таблицу имен-намеков?» Это было бы идеальным решением, если бы таблица имен-намеков существовала в файлах Borland. Программа PEDUMP работает в обеих ситуациях, однако, по понятным причинам, результат получается несколько беспорядочным.

Рис. 8.6. Как РЕ-фаил импортирует функции

Поскольку таблица адресов импорта находится обычно в доступной для записи секции, то не представляет труда перехватить вызовы, сделанные ЕХЕ или DLL другой DLL. Для этого нужно просто «залатать» соответствующий элемент таблицы адресов импорта так, чтобы он указывал на желаемую функцию-перехватчик. Не нужно модифицировать код ни в вызывающей, ни в вызываемой функции. Эта возможность представляется очень полезной. В главе 10 я расскажу о программе-шпионе Win32 API, которая опирается на этот прием.

Интересно заметить, что в РЕ-файлах Microsoft таблица импорта не полностью синтезируется компоновщиком. Вместо этого все элементы, необходимые для вызова функций из других DLL, располагаются в библиотеке импорта. При компоновке DLL менеджер библиотеки (LIB.EXE) сканирует компонуемые объектные файлы и создает библиотеку импорта. Эта библиотека отличается от библиотек импорта, используемых 16-разрядными компоновщиками NE-файлов. Библиотека импорта, создаваемая 32-разрядными LIB-файлами, имеет секцию .text и несколько секций .idata$. Секция .text в библиотеке содержит переходник JMP DWORD PTR[XXXXXXXX], о котором я упоминал раньше. Имя этого переходника хранится в таблице символов объектного файла. Имя символа идентично имени экспортируемой DLL функции (например _DispatchMessage@4).

Одна из секций .idata$ в библиотеке импорта содержит двойное слово — переходник. Другая секция .idata$ резервирует место для «номера намека», за которым следует имя импортируемой функции. Этих два поля составляют структуру IMAGE_IMPORT_BY_NAME. При компоновке РЕ-файла, использующего библиотеку импорта, секции библиотеки импорта добавляются к перечню секций объектного файла, подлежащих обработке компоновщиком. Ввиду того, что переходник в библиотеке импорта имеет такое же имя, как и импортируемая функция, компоновщик воспринимает переходник как импортируемую функцию и настраивает вызовы импортируемой функции так, чтобы они указывали на переходник. Поэтому переходник в библиотеке импорта трактуется как импортируемая функция.

Помимо обеспечения порций кода для переходника импортируемой функции библиотека импорта поставляет части секции .idata (или таблицы импорта) РЕ-файла. Эти части поступают из разных секций .idata$, помещаемых библиотекарем в библиотеку импорта. Короче говоря, компоновщик не различает импортированные функции и функции из другого объектного файла. Компоновщик просто следует своим предписаниям при создании и объединении секций, и все происходит вполне естественно.

IMAGE_THUNK_DATA DWORD

Как я уже упоминал, каждое двойное слово IMAGE_THUNK_DATA соответствует импортируемой функции. Интерпретация двойного слова зависит от того, был ли файл уже загружен в память и была ли функция импортирована по имени или по номеру (импортирование по имени встречается чаще).

При импортировании функции по номеру (что бывает редко) старший бит (0х80000000) двойного слова IMAGE_THUNK_DATA данного ЕХЕ-файла устанавливается в 1. Например, рассмотрим IMAGE_THUNK_DATA со значением 0х80000112 в массиве GDI32.DLL. Этот IMAGE_THUNK_DATA импортирует 112-ю экспортируемую функцию из GDI32.DLL. Проблема при импортировании по номеру состоит в том, что фирма Microsoft не позаботилась, чтобы поддержать соответствие номеров экспорта функций Win32 API для Windows NT, Windows 95 и Win32s.

Если функция импортируется по имени, то ее двойное слово IMAGE_THUNK_DATA содержит RVA структуры IMAGE_IMPORT_BY_NAME. Это простая структура выглядит следующим образом.

WORD Hint

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

Строка ASCIIZ с именем импортируемой функции. Окончательная интерпретация двойного слова IMAGE_THUNK_DATA происходит после того, как РЕ-файл загружен загрузчиком Win32. Загрузчик Win32 использует исходную информацию из двойного слова IMAGE_THUNK_DATA для поиска адреса импортируемой функции (либо по имени, либо по номеру). Затем загрузчик записывает в двойное слово IMAGE_THUNK_DATA адрес импортируемой функции.

Сравнение IMAGE_IMPORT_DESCRIPTOR и IMAGE_THUNK_DATA

Теперь, после того как вы увидели и структуру IMAGEJMPORTJDESCRIPTOR, и структуру IMAGE_THUNK_DATA, будет просто составить отчет обо всех импортируемых функциях, которые использует ЕХЕ-файл или DLL. Для этого нужно просто перебрать последовательно все элементы массива IMAGE_IMPORT_DESCRIPTOR (каждый из которых соответствует одной импортируемой DLL). Для каждого элемента IMAGE_IMPORT_DESCRIPTOR найдите массив двойных слов IMAGE_THUNK_DATA и интерпретируйте его соответствующим образом. Рис. 8.7 показывает вывод программы PEDUMP для этой операции. (Функции без имен импортируются по номеру.)

Рис. 8.7. Типичная таблица импорта в ЕХЕ-файле (EXPLORER.EXE)

Экспорт в РЕ-файлах

Противоположностью импорту функций является их экспорт для использования ЕХЕ-файлами или другими DLL. Информация об экспортируемых функциях хранится в секции .edata РЕ-файла. Как правило, ЕХЕ-файлы, созданные Microsoft LINK, ничего не экспортируют и поэтому не имеют секции .edata. ЕХЕ-файлы, созданные с помощью TLINK32, напротив, обычно экспортируют один символ и имеют секцию .edata. Большинство DLL экспортируют функции и имеют секцию .edata. Главными компонентами секции .edata (или, другими словами, таблицы экспорта) являются таблицы имен функций, адреса точек входа и номера экспорта. В NE-файле эквивалентами таблицы экспорта являются таблица элементов, таблица резидентных имен и таблица нерезидентных имен. В NE-файлах эти таблицы хранятся как часть заголовка NE-файла, а не в сегментах или ресурсах.

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

DWORD Characteristics

Это поле, по-видимому, никогда не используется и всегда устанавливается в 0.

DWORD TimeDateStamp

Отметка о времени и дате, указывающая время создания файла.

WORD MajorVersion;WORD MinorVersion

Эти поля, по-видимому, никогда не используются и всегда устанавливаются в 0.

DWORD Name

RVA строки ASCIIZ с именем этой DLL (например, MYDLL.DLL).


DWORD Base

Начальный номер экспорта для функций, экспортируемых данным модулем. Например, если номера экспортируемых функций 10, 11 и 12, то это поле будет содержать 10.

DWORD NumberOfFunctions

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

DWORD NumberOfNames

Количество элементов в массиве AddressOfNames. Это значение соответствует количеству функций, экспортируемых по имени, которое обычно (хотя и не всегда) равно общему количеству экспортируемых функций.

PDWORD * AddressOfFunctions

Это поле является RVA и указывает на массив адресов функций. Адреса функций — это RVA точек входа для каждой экспортируемой модулем функции.

PDWORD * AddressOfNames

Это поле является RVA и указывает на массив указателей строки. Строки содержат имена функций, экспортируемых по имени из данного модуля.

PWORD * AddressOfNameOrdinals

Это поле является RVA и указывает на массив слов. По существу, в этих словах хранятся номера экспорта всех экспортируемых из данного модуля по имени функций. Однако не забудьте прибавить начальный номер экспорта, указанный в поле Base (описано выше).

Формат таблицы экспорта несколько странен. Как я упоминал раньше, обязательными при экспортировании функции являются адрес и номер экспорта. Если вы решили экспортировать функцию по имени, здесь будет имя функции. Можно было бы подумать, что разработчики РЕ-формата могли поместить все эти три компонента в одну структуру и после этого оперировать массивом таких структур. Вместо этого приходится выискивать различные части в трех различных массивах.

Наиболее важным из всех массивов, на которые указывает IMAGE_EXPORT_DIRECTORY, является массив, на который указывает поле AddressOfFunctions. Это массив двойных слов, в котором каждое двойное слово содержит RVA одной из экспортируемых функций. Номер экспорта каждой экспортируемой функции соответствует ее положению в массиве, Так, например, принимая, что номера экспорта начинаются с 1, адрес, по которому хранится адрес функции с номером экспорта, равным 1, содержится в первом элементе массива. Адрес, по которому хранится адрес функции с номером экспорта, равным 2, содержится во втором элементе массива и т.д.

Необходимо помнить о двух моментах относительно массива AddressOfFunctions. Во-первых, нельзя забывать о том, что отсчет номеров экспорта начинается с числа, содержащегося в поле Base структуры IMAGE_EXPORT_DIRECTORY. Так, если поле Base содержит 10, то первое двойное слово в массиве AddressOfFunctions соответствует номеру экспорта 10, второе — 11 и т.д. Во-вторых, следует иметь в виду, что номера экспортов могут иметь пропуски. Допустим, что явно экспортируются две функции с номерами 1 и 3. Несмотря на то, что экспортированы только две функции, массив AddressOfFunctions обязан содержать три элемента. Любые элементы массива, не отвечающие экспортируемым функциям, содержат 0.

Когда загрузчик Win32 связывает вызов функции, экспортируемой по номеру, он выполняет совсем незначительный объем работ. Он просто использует номер функции как индекс в массиве AddressOfFunctions модуля-цели. Конечно, загрузчик должен учесть, что наименьший номер экспорта может быть не равен 1, и должен поправить индексацию соответствующим образом.

Чаще всего ЕХЕ-файлы и DLL под Win32 импортируют функции по имени, а не по номеру. Здесь выходят на сцену два других массива, на которые указывает структура IMAGE_EXPORT_DIRECTORY. Массивы AddressOfNames и AddressOfNameOrdinals существуют для того, чтобы загрузчик мог быстро найти номер экспорта по заданному имени функции. Массивы AddressOfNames и AddressOfNameOrdinals содержат одинаковое количество элементов (заданное в поле NumberOfNames структуры IMAGE_EXPORT_DIRECTORY). Массив AddressOfNames — это массив указателей на имена функций, а массив AddressOfNameOrdinals — массив индексов для массива AddressOfFunctions.

Давайте посмотрим, как загрузчик Win32 обрабатывает вызов функции, импортируемой по имени. Сначала загрузчик будет искать строки, на которые указывает массив AddressOfNames. Допустим, он находит искомую строку в третьем элементе. Затем загрузчик использует найденный индекс для поиска соответствующего элемента в массиве AddressOfNameOrdinals (в данном случае это третий элемент). Последний массив — это просто набор слов, где каждое слово играет роль индекса в массиве AddressOfFunctions. Наконец последний шаг — взять значение в массиве AddressOfNameOrdinals и использовать его в качестве индекса для массива AddressOfFunctions.

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

Рис. 8.8 демонстрирует формат секции экспорта и три ее массива.

Рис. 8.8. Типичная таблица экспорта из ЕХЕ-фаила

Рис. 8.9 показывает вывод программой PEDUMP секции экспорта в KERNEL32.DLL.

Рис. 8.9. Распечатка секции экспорта для библиотеки KERNEL32.DLL с помощью программы PEDUMP

Если вы просматриваете экспорт в системных DLL (например, KERNEL32.DLL или USER32.DLL), вы можете случайно обнаружить, что часто две функции отличаются только одним символом в конце имени, например CreateWindowExA и CreateWindowExW. Вот так «явно» осуществлена поддержка уникода (Unicode). Функции, оканчивающиеся на А, являются ASCII-совместимыми (или ANSI-) функциями. Функции, оканчивающиеся на W, — это Unicode-версии этих функций. Программируя, пользователь не указывает явно, какую функцию надо вызывать. Вместо этого соответствующая функция выбирается в WINDOWS.H с помощью директивы препроцессора #ifdefs. Это иллюстрируется следующим отрывком из NT WINDOWS.H:

Передача экспорта

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

Проиллюстрируем сказанное примером. Рассмотрим следующий отрывок из вывода программой PEDUMP Windows NT3.5 KERNEL32.DLL:

Каждая функция в этом выводе передается функции в NTDLL. Таким образом, программа, вызывающая функцию HeapAlloc, в действительности вызывает функцию RtlAllocateHeap из NTDLL.DLL. Аналогично, вызов HeapFree является в действительности вызовом функции RtlHeapFree из NTDLL.

Каким образом можно узнать, что функция передается? Единственным указанием на то, что функция передается, является наличие ее адреса в таблице экспорта (секция .edata). В этом случае так называемый адрес функции в действительности является RVA строки, содержащей передаваемую DLL и имя функции. Например, в предыдущем выводе RVA для НеарАПос равно Ox43FC3. Смещение Ox43FC3 в KERNEL32.DLL попадает в секцию .edata. Это смещение имеет строка NTDLL.RtlAllocateHeap. Функция DumpExportsSection в программе PEDUMP показывает, как обнаружить передаваемые функции.

Хотя передача экспорта кажется очень приятным свойством, Microsoft не дает описания того, как использовать передачу в пользовательских DLL. До сих пор я встречал использование передачи только одной DLL (вышеупомянутая Windows NT KERNEL32.DLL). Даже несмотря на то, что мне не встречались DLL с передачами экспорта под Windows 95, загрузчик Windows 95, тем не менее, поддерживает это свойство, о чем я рассказывал в главе 3.

Ресурсы РЕ-файла

Нахождение ресурсов в РЕ-файлах сложнее по сравнению с эквивалентными NE-файлами. Формат индивидуальных ресурсов (например, меню) существенно не изменился, но в РЕ-файлах приходится рыскать по сложной иерархии, чтобы найти их.

Перемещения по иерархии каталогов ресурсов похожи на перемещения по жесткому диску. Здесь есть главный каталог (корневой), имеющий свои подкаталоги. Подкаталоги имеют свои собственные подкаталоги. В этих подкаталогах находятся файлы. Файлы аналогичны исходным данным ресурсов, содержащим такие элементы, как диалоговые шаблоны. В РЕ-файлах как корневой каталог, так и его подкаталоги являются структурами типа IMAGE_RESOURCE_DIRECTORY. Структура IMAGE_RESOURCE_DIRECTORY имеет следующий формат.

DWORD Characteristics

Теоретически это поле может содержать флаги ресурсов, но, по-видимому, оно всегда равно 0.

DWORD TimeDateStamp

Отметка о времени и дате создания ресурса.

WORD MajorVersion; WORD MinorVersion

Теоретически эти поля могли бы содержать номер версии ресурса. По-видимому, они всегда равны 0.

WORD NumberOfNamedEntries

Количество элементов массива (описан позже), использующих имена и следующих за этой структурой. За дополнительной информацией обращайтесь к описанию поля DirectoryEntries.

WORD NumberOfIdEntries

Количество элементов массива, использующих целые ID и следующих за этой структурой и всеми поименованными элементами. За дополнительной информацией обращайтесь к описанию поля DirectoryEntries.

IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[]

Это поле формально не является частью структуры IMAGE_RESOURCE_DIRECTORY. За этим полем сразу следует массив структур IMAGE_RESOURCE_DIRECTORY_ENTRY. Количество элементов в массиве равно сумме полей NumberOfNamedEntries и NumberOfIdEntries. Элементы каталога, имеющие идентификаторы-имена (а не целые ID), находятся в начале массива.

Элемент каталога может указывать либо на подкаталог (т.е, на другую IMAGE_RESOURCE_DIRECTORY), либо на IMAGE_RESOURCE_DATA_ENTRY, которая описывает, где в файле находятся исходные данные ресурсов. Как правило, необходимо пройти как минимум три уровня каталогов, перед тем как попасть на IMAGE_RESOURCE_DATA_ENTRY для данного ресурса. Каталог верхнего уровня (только один) всегда находится в начале секции ресурсов (.rsrc). Подкаталоги каталога верхнего уровня соответствуют различным типам ресурсов, находящихся в файле. Например, если РЕ-файл включает диалоги, таблицы строк и меню, этими тремя подкаталогами будут соответственно каталог диалогов, каталог таблицы строк и каталог меню. Каждый из этих «типов» подкаталогов будет в свою очередь иметь «ID»-подкаталоги. Для каждого образца заданного типа ресурса будет существовать один ID-подкаталог. Если в приведенном выше примере есть четыре диалоговых окна, каталог диалогов будет иметь четыре ID-подкаталога. Каждый ID-подкаталог будет иметь либо строковое имя (например, MyDialog), либо целый ID, используемый для идентификации ресурса в RC-файле. Рис. 8.10 наглядно иллюстрирует иерархию каталогов ресурсов.

Рис. 8.10. Иерархия ресурсов типичного РЕ-файла

Рис. 8.11 показывает вывод программой PEDUMP ресурсов файла CLOCK.EXE в Windows NT 3.5. На втором уровне отступов можно видеть пиктограммы, меню, диалоги, таблицы строк, пиктограммы групп и ресурсы версий. На третьем уровне — две пиктограммы (с ID 1 и 2), два меню (с именами CLOCK и GENERICMENU), два диалога (один с именем ABOUTBOX, а другой — с целым ID, равным 0х64) и т.д. На четвертом уровне отступов — данные для значка 1 с RVA 0х9754 длиной 0x130 байт. Аналогично данные для меню CLOCK имеют смещение Ох952С и занимают 0хЕА байт.

Рис. 8.11. Иерархия ресурсов для CLOCK.EXE

Чтобы продвинуться дальше в обсуждении форматов ресурсов, мне необходимо рассказать о формате индивидуальных типов ресурсов (диалоги, меню и т.д.). Этот рассказ занял бы целую главу. Но я решил сэкономить деревья, из которых делают бумагу. Если вам интересно, читайте файл RESFMT.TXT из Win32 SDK, в котором есть детальное описание всех типов ресурсов. Программа PEDUMP показывает иерархию ресурсов, но не затрагивает индивидуальные образцы ресурсов.

Базовые поправки РЕ-файла

Компоновщик, создавая ЕХЕ-файл, предполагает, где в памяти будет отображен файл, и затем помещает предполагаемые адреса элементов программного кода и данных в исполняемый файл. Если, однако, исполняемый файл загружается куда-нибудь в другое место в виртуальном адресном пространстве, адреса, проставленные компоновщиком, оказываются неверными. Информация, хранящаяся в секции .reloc, позволяет загрузчику РЕ-файла исправить эти адреса в загружаемом модуле. Когда загрузчику удастся загрузить файл по базовому адресу, указанному компоновщиком, информация в секции .reloc игнорируется за ненадобностью. Элементы секции .reloc называются базовыми поправками, потому что их использование зависит от значения базового адреса загружаемого отображения.

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

Вот пример того, как работают базовые поправки. Предположим, что ЕХЕ-файл скомпонован в допущении, что базовый адрес равен 0х400000. Пусть указатель, содержащий адрес какой-либо строки, имеет смещение 0х2134 в отображении. Строка начинается с физического адреса 0х404002, так что указатель содержит это значение. В момент загрузки загрузчик решает, что модуль нужно отобразить в память, начиная с физического адреса 0х600000. Разность между предполагаемым компоновщиком базовым адресом и реальным адресом загрузки называется дельта. В нашем случае дельта равна 0х200000 (0х600000-0х400000). Поскольку все отображение оказывается на 0х200000 байт выше в памяти, адрес строки теперь 0х604002. Указатель на строку теперь содержит неверное значение. Чтобы исправить его, к нему необходимо прибавить дельту (в нашем случае 0х200000).

Чтобы загрузчик Windows сделал это исправление, исполняемый файл содержит базовую поправку для того места в памяти, в котором находится указатель (его смещение в отображении равно 0х2134). Чтобы разрешить базовую поправку, загрузчик добавляет дельту к исходному значению, находящемуся по адресу, указанному в базовой настройке. В нашем случае загрузчик должен прибавить 0х200000 к исходному значению указателя (0х404002) и поместить это значение (0х604002) обратно в указатель. Раз строка действительно находится по адресу 0х604002, все снова становится правильным. Рис. 8.12 показывает весь этот процесс.

Рис. 8.12. Базовые поправки РЕ-файла

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

DWORD VirtualAddress

Это поле содержит стартовый RVA для данного куска поправок. Смещение каждой поправки, которая следует дальше, добавляется к этой величине для получения истинного RVA, к которому должна быть применена данная поправка.

DWORD SizeOfBlock

Размер данной структуры плюс все последующие поправки типа WORD. Чтобы определить количество поправок в данном блоке, нужно из значения этого поля вычесть размер IMAGE_BASE_RELOCATION (8 байт) и затем разделить на 2 (размер типа WORD). Например, если это поле содержит значение 44, то в блоке имеется 18 поправок:

WORD TypeOffset

На самом деле это не отдельное слово, а массив слов, количество элементов в котором вычисляется по формуле, приведенной в описании предыдущего двойного слова. Младших 12 разрядов каждого из этих слов представляют поправочное смещение, которое должно быть прибавлено к значению в поле VirtualAddress из заголовка данного блока поправок. Старших 4 разряда каждого слова являются типом поправки. Для РЕ-файлов, исполняемых на процессорах серии Intel, существуют только два типа поправок:

  • 0 (IMAGE_REL_BASED_ABSOLUTE). Эта поправка не имеет смысла и используется как заполнитель для выравнива­ния на границу следующего двойного слова.
  • 3 (IMAGE_REL_BASED_HIGHLOW). Поправка подразумевает прибавление как старших, так и младших 16 разрядов дельты к двойному слову, на которое указывает вычисленный RVA.

    Есть также другие поправки, определенные в WINNT.H, большая часть которых рассчитана на архитектуры процессо­ров, отличные от i386.

    Рис. 8.13 показывает некоторые базовые поправки, выведенные программой PEDUMP. Заметьте, что значения RVA, показанные на рисунке, были уже заранее определены полем VirtualAddress структуры IMAGE_BASE_RELOCATION.

    Рис. 8.13. Базовые поправки в ЕХЕ-файле

    COFF-таблица символов

    Если вас интересуют только те части РЕ-файла, которые используются операционной системой, вы можете пропус­тить этот и следующий («COFF-отладочная информация») разделы и продолжить чтение с раздела «Различия между РЕ-файлами и объектными COFF-файлами».

    В любом объектном файле в COFF-стиле, созданном компилятором Microsoft, есть таблица символов. В отличие от информации Code View, эта таблица символов не является дополнительным грузом, использующимся только при необхо­димости скомпоновать исполняемый файл с отладочной информацией. Напротив, эта таблица содержит информацию обо всех общеиспользуемых и внешних символах, на которые ссылается модуль. Информация о привязке, выдаваемая компи­лятором, относится к определенным элементам в этой таблице символов. Формат COFF-таблицы символов удивительно прост — по сравнению с очень запутанным форматом Microsoft/Intel OMF с его LNAME, PUBDEF и EXTDEF.

    Если при компиляции отладочная информация не включается, то в таблице символов объектного файла будет нахо­дится лишь небольшое количество символов. Если же включить отладочную информацию (с помощью /Zi), то компиля­тор добавит дополнительную информацию о начале, конце и длине каждой функции модуля. Если затем провести компо­новку либо с /DEBUGTYPE:COFF, либо с /DEBUGTYPE:BOTH, компоновщик поместит в получившийся ЕХЕ-файл таб­лицу символов в COFF-стиле.

    Зачем нужна COFF-информация, если есть намного более полная информация CodeView? Если используется систем­ный отладчик NT (NTSD) или отладчик NT Kernel — KD (Kernel Debugger), то в игре участвует только COFF. К тому же если ваша РЕ-программа терпит катастрофу в Windows NT, DRWTSN32 может использовать эту информацию для «разбора полетов».

    И в ЕХЕ-файлах, и в объектных файлах расположение и размер COFF-таблицы символов определены в структуре IMAGE_FILE_HEADER (см. раздел «Заголовок РЕ-файла» раньше в этой главе, чтобы освежить в памяти сведения об этой структуре). Таблица символов специально сделана простой и состоит из массива структур IMAGE_SYMBOL. Коли­чество элементов в этом массиве задается значением поля NumberOfSymbols структуры IMAGE_FILE_HEADER. Рис. 8.14 показывает пример вывода символов программой PEDUMP.

    Рис. 8.14. Типичная COFF-таблица символов

    Каждая структура IMAGE_SYMBOL имеет следующий формат:

    Давайте изучим каждое из этих полей детально.

    union (Symbol name union)

    Символьное имя можно представить двумя способами, в зависимости от его длины. Если оно не длиннее 8 знаков, то член объединения ShortName содержит символьное имя в формате ASCIIZ. Следует быть осторожным в случае, когда символьное имя содержит в точности 8 знаков; при этом строка не оканчивается нулем. Если поле Name.Short не равно нулю, следует использовать член объединения ShortName. Другой способ представления символьного имени применяет­ся, когда поле Name.Short равно 0. В этой ситуации поле Name.Long является байтовым смещением в таблице строк. Таб­лица строк — это не что иное, как массив ASCIIZ-строк, следующих одна за другой в памяти. Эта таблица начинается сразу за таблицей символов. Чтобы посчитать адрес начала таблицы строк, нужно просто умножить количество символов на размер структуры IMAGE_SYMBOL и прибавить результат к стартовому адресу таблицы символов. Длина таблицы строк в байтах находится в двойном слове, имеющем смещение 0 в таблице строк.

    DWORD Value

    Это поле содержит значение, связанное с символом. Для нормальных символов и символов данных (т.е. функции и глобальные переменные) поле Value содержит RVA элемента, на который ссылается данный символ. Это значение интер­претируется иначе для некоторых других символов. В табл. 8.2 представлен краткий перечень некоторых назначений поля Value для специальных символов.

    Таблица 8.2. Специальные символы в COFF-таблицах символов

    Имя символа Использование
    .file Индекс символьной таблицы следующего символа .file
    .data Стартовый RVA области данных. Эта область определяется исходным файлом, заданным предыдущим символом .file
    .text Стартовый RVA области программного кода. Эта область определяется исходным файлом, заданным предыдущим символом .file
    .If Количество элементов в таблице номеров строк для какой-либо функции, функция задается предыдущим символом, определяющим функцию

    SHORT SectionNumber

    Поле SectionNumber содержит номер секции, которой принадлежит символ. Например, символы для глобальных пе­ременных будут, как правило, иметь в этом поле номер секции .data. Помимо стандартных секций РЕ-файла, определены три других специальных значения.

  • 0 (IMAGE_SYM_UNDEFINED). Символ не определен. Такой номер секции используется в объектных файлах для представления символов, находящихся вне модуля, например внешних функций и внешних глобальных переменных.
  • -1 (IMAGE_SYM_ABSOLUTE). Этот символ является абсолютной величиной и не связан ни с какой конкретной сек­цией. Примерами являются локальные и регистровые переменные.
  • -2 (IMAGE_SYM_DEBUG). Данный символ используется только отладчиком и не виден из программы. Символы .file, задающие имя исходного файла, — примеры такой символьной секции.

    WORD Type

    Тип символа. Файл WINNT.H определяет достаточно широкий спектр типов символов (int, struct, enum и т.д.). (См. полный перечень в директивах #defines IMAGE_SYM_TYPE_xxx.) К сожалению, средства Microsoft, по-видимому, не ге­нерируют символов всех возможных типов. Вместо этого все глобальные переменные и функции имеют тип NULL или тип функции, возвращающей NULL.

    BYTE StorageClass

    Класс памяти символа. Как и для типов символов файл WINNT.H определяет достаточно широкий спектр классов па­мяти: automatic, static, register, label и т.д. (См. полный перечень в директивах #define IMAGE_SYM_CLASS_xxx.) Опять-таки, как и в случае типов, средства Microsoft создают только небольшое количество информации. Все глобальные переменные и функции имеют класс памяти внешний. По всей видимости, не существует способа создать символы для локальных переменных, регистровых переменных и т.д.

    BYTE NumberOfAuxSymbols

    На самом деле я немного обманул читателя. Таблица символов не является в точности массивом структур IMAGE_SYMBOL. Если символ имеет ненулевое значение в записи NumberOfAuxSymbols, то за символом следует такое же число структур IMAGE_AUX_SYMBOL. Например, за символом .file следует столько структур IMAGE_AUX_SYMBOL, сколько требуется, чтобы хранить полный путь к исходному файлу.

    К счастью, размер структуры IMAGE_AUX_SYMBOL такой же, как и у структуры IMAGE_SYMBOL, так что пользо­ватель все же может рассматривать таблицу символов как массив структур IMAGE_SYMBOL. Запомните, что индекс символа должен рассматриваться как индекс массива, даже если некоторые элементы являются вспомогательными запи­сями. Чтобы вычислить индекс следующего регулярного символа, нужно прибавить количество вспомогательных струк­тур, используемых символом. Например, пусть символ имеет индекс 1. Если он использует три вспомогательных символа, то индекс следующего регулярного символа будет равен 4.

    IMAGE_AUX SYMBOL представляет собой запутанное объединение полей. Чтобы определить, какие члены объеди­нения использовать, необходимо знать тип регулярного символа, связанного с данным вспомогательным символом. И хо­тя я так и не понял, какие поля объединения должны быть использованы в каждом случае, я уяснил себе два следующих.

    Символы, имеющие класс памяти IMAGE_SYM_CLASS_FILE, используют член объединения File в структуре IMAGE_AUX_SYMBOL.


    Символы, имеющие класс памяти IMAGE_SYM_CLASS_STATIC, используют член объединения Section в структуре IMAGE_AUX_SYMBOL.

    Все мои знания, касающиеся интерпретации вспомогательных символов, в совокупности содержатся в функции DumpAuxSymbols() в исходном файле COMMON.С в программе PEDUMP. Если вы самостоятельно обнаружите что-нибудь новенькое, смело вносите эти дополнения в эту функцию.

    Изучая информацию внутри секции символов, можно заметить, что символы расположены не хаотически. Напротив, они сгруппированы по объектным модулям (или по исходным файлам, если вам так больше нравится), из которых они появились. Первой записью в COFF-таблице символов является запись .file. Значение записи .file — это индекс в таблице символов, указывающий на следующую запись .file. Следуя по этой цепочке записей .file, можно последовательно пере­брать все объектные модули в ЕХЕ-файле. Сразу за записью .file следуют другие записи, относящиеся к данному исход­ному файлу. Например, все общедоступные символы (глобальные переменные и функции), объявленные в исходном фай­ле, идут сразу за записью .file, отвечающей данному исходному файлу. Для нормального исходного модуля «иерархия» записей выглядит следующим образом:

    COFF-отладочная информация

    Для среднего программиста термин отладочная информация включает как символьную информацию, так и информа­цию о номерах строк. В COFF-формате записи, относящиеся к символам и записи, относящиеся к номерам строк, нахо­дятся в разных областях файла. (В форматах фирмы Borland и в формате Code View для таблиц символов этих два вида информации поступают из одной и той же части файла.) Я обсудил вначале COFF-таблицу символов потому, что она име­ется как в объектных, так и в ЕХЕ-файлах. К тому же очень рано в процессе изучения РЕ-формата приходится иметь дело с полем PointerToSymbolTable в структуре IMAGE_FILE_HEADER. По этим причинам я решил рассказать о таблице сим­волов отдельно.

    Вся COFF-таблица символов ЕХЕ-файла состоит из трех частей: заголовка, информации о номерах строк и таблицы символов. Они не обязательно расположены по соседству в памяти, но компоновщик Microsoft выстраивает их таким об­разом. Полная COFF-таблица символов выглядит так:

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

    Чтобы отыскать структуру IMAGE_COFF_SYMBOLS_HEADER, нужно заглянуть в массив структур IMAGE_DEBUG_DIRECTORY в секции .rdata в файле. Структура IMAGE_DEBUG_DIRECTORY, содержащая в поле Type значение 1 (IMAGE_DEBUG_TYPE_COFF), содержит указатель на COFF-таблицу символов. Повторим еще раз вкратце: ка­талог данных (в конце заголовка РЕ-файла) содержит RVA массива структур IMAGE_DEBUG_DIRECTORY. Каждому типу отладочной информации, находящейся в файле, соответствует одна структура IMAGE_DEBUG_DIRECTORY. Если одна из этих структур IMAGE_DEBUG_DIRECTORY ссылается на отладочную информацию в COFF-стиле, она содержит RVA структуры IMAGE_COFF_SYMBOLS_HEADER. Структура IMAGE_COFF_SYMBOLS_HEADER в свою очередь содержит указатели на COFF-таблицу символов и информацию о номерах строк. Структура IMAGE_COFF_SYMBOLS_HEADER имеет следующий формат:

    Рассмотрим подробнее поля структуры IMAGE_COFF_SYMBOLS_HEADER.

    DWORD NumberOfSymbols

    Количество символов в COFF-таблице символов. Данное поле содержит такое же значение, как и поле IMAGE_FILE_HEADER.NumberOfSymbols, что обсуждалось раньше в разделе «Заголовок РЕ-файла».

    DWORD LvaToFirstSymbol

    Байтовое смещение COFF-таблицы символов по отношению к началу рассматриваемой структуры. Прибавление этой величины к RVA структуры IMAGE_COFF_SYMBOLS_HEADER даст результат, совпадающий со значением поля IMAGE_FILE_HEADER.PointerToSymbolTable.

    DWORD NumberOfLinenumbers

    Количество элементов в таблице номеров строк (рис. 8.15).

    Рис. 8.15. Типичный пример информации из таблицы номеров строк в ЕХЕ-фачле

    DWORD LvaToFirstLinenumber

    Байтовое смещение COFF-таблицы номеров строк по отношению к началу рассматриваемой структуры.

    DWORD RvaToFirstByteOfCode

    RVA первого байта исполняемого программного кода в отображении. Это поле обычно содержит значение равное RVA секции .text. Это значение также можно найти, просматривая таблицу секций исполняемого файла.

    DWORD RvaToLastByteOfCode

    RVA последнего байта исполняемого программного кода в отображении. Если есть только одна программная секция (.text), то данное поле будет равно RVA этой секции плюс размер ее исходных данных. Это значение также можно найти, просматривая таблицу секций исполняемого файла.

    DWORD RvaToFirstByteOfData

    RVA первого байта данных в отображении. Значение этого поля обычно равно RVA секции .bss.

    DWORD RvaToLastByteOfData

    RVA последнего байта доступных программе данных в отображении. Область, охватываемая полями FirstByteOfData и LastByteOfData, может перекрывать несколько секций (например, .bss, .rdata и .data).

    COFF-таблица номеров строк

    COFF-таблица номеров строк, на которую указывает структура IMAGE_COFF_SYMBOLS_HEADER, является очень простой — это просто массив структур IMAGE_LINENUMBER. Каждая структура ставит в соответствие одной строке программного кода исходного файла ее RVA в исполняемом отображении. Рис. 8.15 показывал образец таблицы номеров строк, выведенной программой PEDUMP. Структура IMAGE_LINENUMBER имеет два поля — объединение и слово.

    Если поле Linenumber (см. ниже) ненулевое, то его следует трактовать как RVA строки программного кода. Если поле Linenumber равно нулю, то данное поле содержит индекс в таблице символов. Символьная запись, на которую ссылается этот индекс, обозначает функцию. Все записи номеров строк для этой функции следуют вслед за этой специальной запи­сью. Из рассмотрения вывода программы PEDUMP видно, что таблица номеров строк состоит из записи индекса таблицы символов, за которой идут обычные записи номеров строк, а после них — другая запись индекса таблицы символов и т. д.

    WORD Linenumber

    Содержит номер строки относительно начала функции. Это поле не является номером строки в файле. Чтобы пере­вести его в удобный для использования номер строки в файле, следует найти в таблице символов номер начальной строки соответствующей функции. Соответствующая функция — это функция, имеющая 0 в данном поле в самой последней за­писи номера строки. Если что-либо осталось непонятным, смотрите вывод программы PEDUMP на рис. 8.15.

    Если необходим доступ только к номерам строк данной программной секции, то следует искать лишь соответствую­щий диапазон элементов, относящихся к номерам строк, в таблице секций. Структура IMAGE_SECTION_HEADER дан­ной секции содержит файловое смещение и счетчик номеров своих строк внутри таблицы. Объектные файлы COFF-формата тоже содержат информацию о номерах строк в формате, который был только что описан. В объектных файлах отсутствует структура IMAGE_COFF_SYMBOLS_HEADER, поэтому пользователю придется искать записи номе­ров строк с помощью структур IMAGE_SECTION_HEADER.

    Различия между РЕ-файлами и объектными COFF-файлами

    Во время всего предыдущего обсуждения я неоднократно отмечал, что многие структуры и таблицы имеют одинако­вый вид как в объектных COFF-файлах, так и в РЕ-файлах, созданных из этих объектных файлов. Как объектные COFF-файлы, так и РЕ-файлы имеют в своем начале (или около него) структуру IMAGE_FILE_HEADER. За этим заго­ловком следует таблица секций, содержащая информацию обо всех секциях файла. Оба формата также имеют одинаковые форматы таблиц номеров строк и символьных таблиц, хотя РЕ-файлы могут содержать дополнительные символьные таб­лицы не в COFF-стиле. Степень сродства двух форматов можно увидеть в исходном программном коде программы PEDUMP. Самый большой файл в этой программе — COMMON.C. Этот исходный файл содержит все подпрограммы, ко­торые могут использоваться как частями программы, осуществляющими ре-вывод, так и частями, осуществляющими вы­вод объектных файлов.

    Эта схожесть двух файловых форматов не случайна. Ее цель — максимально упростить работу компоновщика. Теоре­тически создание ЕХЕ-файла из одного объектного файла должно сводиться к вставке нескольких таблиц и изменению парочки файловых смещений в отображении. Имея это в виду можно представлять себе объектный COFF-файл как заро­дыш РЕ-файла. Отсутствуют или отличаются лишь несколько деталей, и я перечисляю их здесь.

  • Объектные COFF-файлы начинаются сразу с IMAGE_FILE_HEADER. Перед заголовком нет части кода DOS, и нет сигнатуры РЕ перед IMAGE_FILE_HEADER.
  • В объектных файлах отсутствует IMAGE_OPTIONAL_HEADER. В РЕ-файлах эта структура следует сразу за IMAGE_FILE_HEADER. Интересно отметить, что некоторые объектные файлы внутри файлов COFF LIB все-таки со­держат IMAGE_OPTIONAL_HEADER.
  • В объектных файлах нет базовых поправок. Вместо этого они имеют привязки, основанные на таблице символов. Я не за­трагиваю формата поправок COFF-файлов, так как они весьма запутаны. Если вы захотите сами покопаться в этой кон­кретной области, поля PointerToRelocations и NumberOfRelocations в строках таблицы секций указывают на поправки для каждой секции. Поправки представляют собой массив структур IMAGE_RELOCATION, определенный в файле WINNT.H. Если активизировать соответствующий ключ, программа PEDUMP может показать поправки объектного файла.
  • Информация CodeView в объектном файле хранится в двух секциях — .debug$S и .debug$T. Компоновщик, обрабаты­вая объектные файлы, не помещает эти секции в РЕ-файл. Вместо этого он собирает все эти секции и создает единую таблицу символов, которая хранится в конце файла. Формально таблица символов не является секцией (т.е. в таблице секций РЕ-файла нет элемента, соответствующего ей).

    Файлы COFF LIB

    Разобравшись в объектных COFF-файлах, нетрудно разобраться в использовании файлов COFF LIB. По существу, файлы COFF LIB — это просто набор объектных COFF-файлов плюс некоторые начальные секции, позволяющие быстро устанавливать расположение нужных объектных файлов, содержащихся внутри библиотеки. Скудная документация по формату COFF LIB называет LIB-файлы архивами. Этого названия буду придерживаться в дальнейшем и я.

    Все LIB-файлы начинаются с одной и той же восьмибайтовой сигнатуры. Эта сигнатура определена в файле WINNT.H:

    Остальная часть файла представляет ряд записей переменной длины. Каждая запись начинается структурой IMAGE_ARCHIVE_MEMBER_HEADER:

    Каждая структура IMAGE_ARCHIVE_MEMBER_HEADER отвечает либо объектному файлу внутри библиотеки, либо одной записи из небольшого набора специальных записей. Эти специальные записи находятся в начале библиотеки и су­ществуют для того, чтобы компоновщик в дальнейшем мог быстро отыскивать объектные файлы в LIB-файле. Исходные данные для члена архива следуют сразу за структурой IMAGE_ARCHIVE_MEMBER_HEADER, с которой начинается ка­ждая запись. Для большинства членов архива записей исходные данные точно такие же, как и в объектном файле. Дейст­вительно, когда программа PEDUMP проводит вывод LIB-файлов, она вызывает те же процедуры, что и при обработке объектного файла. Рис. 8.16 показывает формат LIB-файлов.

    Рассмотрим поля структуры IMAGE_ARCHIVE_MEMBER_HEADER.

    BYTE Name[16]

    Имя члена архива. Если символ «/» появляется после ASCII-строки (например, FOO.OBJ/), то строка перед символом «/» представляет имя члена. Если имя начинается с символа «/», за которым следует десятичное число (например, /104), то число является смещением имени члена архива внутри члена Longnames LIB-файла. В предыдущем примере имя члена начинается со 104-го байта от начала области Longname.

    Имеются также специальные имена для специальных членов архива:

    Рис. 8.16. СОFF-формат LIB-файлов

    Для объектных файлов внутри библиотеки импорта это поле представляет имя DLL, содержащей импортируемые функции.

    BYTE Date[12]

    Дата и время создания члена. Это число хранится в десятичном ASCII-виде.

    BYTE UserID[6]

    Десятичное ASCII-представление идентификатора пользователя. По-видимому, всегда является строкой NULL.

    BYTE GroupID[6]

    Десятичное ASCII-представление идентификатора группы. По-видимому, всегда является строкой NULL.

    BYTE Mode[8]

    Десятичное ASCII-представление файлового режима. По-видимому, всегда равно нулю.

    BYTE Size[10]

    Размер данных члена, представленный в десятичной ASCII-форме. Формат данных зависит от их типа (указан в уже описанном поле Name).

    BYTE EndHeader[2]


    Члены компоновщика

    Всякий LIB-файл имеет две секции членов компоновщика, выполняющих функцию оглавления для остальной части файла. Оба члена имеют имя «/» и различаются по порядку следования в файле. Первый член компоновщика — это пер­вый член архива с именем «/», а второй член компоновщика — это второй член архива с именем «/».

    Оба члена компоновщика — это, по существу, перечни общедоступных символов в LIB-файле вместе со смещениями внутри файла членов — объектных модулей, которые содержат общедоступные символы. Два члена компоновщика име­ют различные форматы. Зачем нужны две копии одинаковой информации? Первый член компоновщика хранит свою информацию в том порядке, в каком объектные модули идут далее в LIB-файле. Это приводит к неоптимальным поискам. Второй член компоновщика хранит свои символы в алфавитном порядке, что делает его намного более полезным для компоновщика. В соответствии с документацией Microsoft компоновщик игнорирует первый член компоновщика и всегда использует второй член.

    Первый член компоновщика имеет следующий формат.

    DWORD NumberOfSymbols

    Число общедоступных символов в данной библиотеке. Это число представлено в формате big-endian (отражает насле­дие COFF-формата для машин, отличных от машин i386). Функция ConvertBigEndian в файле LIBDUMP.C программы PEDUMP осуществляет переключение из формата big-endian в формат little-endian, используемый i386.

    DWORD Offsef[NumberOfSymbols]

    Массив файловых смещений других членов архива. Эти смещения имеют формат big-endian. Каждый из этих членов — это член типа OBJ. Каждый элемент этого массива соответствует имени символа в перечне последующих строк ASCII.

    BYTE StringTable[?]

    Это неразрывная серия строк в стиле С в памяти.

    По существу, каждый элемент массива Offset соответствует одному общедоступному символу, имя которого появляет­ся в области StringTable. Например, третий элемент массива Offsets отвечает третьей строке в области StringTable. Вывод программы PEDUMP поясняет это:

    Формат второго члена компоновщика запутаннее из-за добавления массива, необходимого для быстрого поиска сим­волов. Второй член компоновщика имеет следующий формат.

    DWORD NumberOfMembers

    Это двойное слово содержит количество членов в архиве объектных модулей, следующих дальше в файле.

    DWORD Offsets[NumberOfSymbols]

    Массив файловых смещений других членов архива. В отличие от первого члена компоновщика эти смещения заданы в естественном формате машины (т.е. в формате little-endian для i386).

    DWORD NumberOfSymbols

    Количество общедоступных символов в массиве StringTable (следовательно, и количество общедоступных символов в библиотеке). Это поле к тому же содержит количество элементов в следующем далее массиве Indices.

    WORD Indices[NumberOfSymbols]

    Данный массив содержит индексы (отсчет начинается с 1) массива Offsets (описан на два поля раньше). Этот массив идет параллельно строкам массива StringTable.

    BYTE SiringTable[NumberOfSymbols]

    Это неразрывная серия строк в стиле С в памяти.

    Для того чтобы отыскать объектный файл по его символу, используя второй член компоновщика, компоновщик сна­чала просматривает массив StringTable и вычисляет относительный индекс строки в массиве. Затем компоновщик исполь­зует этот индекс для поиска слова в массиве Indices. Наконец, компоновщик вычитает 1 из этого слова в массиве Indeces и использует результат как индекс массива Offsets. Найденное двойное слово в массиве Offsets как раз и будет смещением в объектном файле, содержащем общедоступный символ. Функция DumpSecondLinkerMember из файла LIBDUMP.C про­граммы PEDUMP показывает этот процесс в действии.

    Член Longnames

    Данные в секции архивного члена Longnames — это просто набор строк в стиле С, следующих одна за другой. Строка помещается в секцию Longnames, если она слишком велика, чтобы уложиться в 16 байт, зарезервированных для поля Name в структуре IMAGE_ARCHIVE_MEMBER_HEADER. В этом случае поле Name содержит символ «/», за которым следует десятичное ASCII-представление смещения строки в секции Longnames.

    Резюме

    Для Win32 Microsoft проделала коренные изменения в форматах объектных и исполняемых файлов. Эти изменения позволили Microsoft сэкономить время, так как использовалась работа, выполненная для других операционных систем. Главная цель такого усовершенствования файловых форматов — улучшить совместимость с различными платформами. COFF-формат объектных файлов существовал до создания Win32. РЕ-формат является расширением COFF-формата и разработан для использования на платформах Win32.

    Ценная часть как объектных, так и исполняемых файлов начинается со структуры IMAGE_FILE_HEADER. За этой структурой (и, возможно, еще одной дополнительной структурой) следует таблица секций. В таблице секций указаны ме­стонахождение и атрибуты всех секций файла. Секцией называется совокупность логически связанных программного ко­да и данных. Чтобы обеспечить быстрое нахождение информации, РЕ-файл содержит каталог данных, указывающий на важные позиции в файле (например, расположение таблицы экспорта файла). Помимо заголовка (или заголовков), таблиц секций и исходных данных секций объектные COFF- и РЕ-файлы могут также содержать информацию о именах символов и номерах строк. Эта информация хранится в конце файла после всех заголовков и данных для секций.

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

    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.

    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 файла. Структура, определяющая его содержимое, описана ниже:

    Первое поле, 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:

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

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

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

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

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

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

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

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

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

    Заметьте, что размер этой структуры для удобства определен в файле заголовков. Это облегчает получение размера структуры, однако мне показалось удобнее использовать оператор 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 файла:

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

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

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

    Опциональный заголовок содержит более полезную информацию об исполнимом файле, такую, как начальный размер стека, адрес точки входа программы, предпочтительный адрес загрузки, версию операционной системы, информацию о выравнивании сегментов и так далее. Структура IMAGE_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 из опционального заголовка:

    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_HEADER 2)

    DllCharacteristics. Флаги, используемые в .DLL для указания наличия точки входа для старта/завершения процесса и его потоков ( DllMain ).

    SizeOfStackReserve, SizeOfStackCommit, SizeOfHeapReserve, SizeOfHeapCommit. Эти поля контролируют объем адресного пространства, зарезервированного и выделенного для стека и кучи по умолчанию. По умолчанию, и для стека ,и для общей кучи зарезервировано 16 страниц, и выделено по одной странице. Эти значения могут устанавливаться опциями линковщика -STACKSIZE: и -HEAPSIZE: соответственно.

    LoaderFlags. Указывает отладчику остановиться после загрузки, перейти к отладке после загрузки, или, значение по умолчанию, просто запустить файл.

    NumberOfRvaAndSizes. Это поле содержит размер массива DataDirectory, расположенного далее. Важно отметить, что это поле содержит именно размер этого массива, а не количество элементов в нем.

    DataDirectory. Каталог данных — содержит указатели на другие важные компоненты PE файла. В действительности, это есть ни что иное, как массив структур IMAGE_DATA_DIRECTORY, расположенных в конце опционального заголовка. В настоящее время формат PE файлов определяет 16 возможных типов каталогов данных, 11 из которых используются чаще всего.

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

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

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

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

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

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

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

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

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

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

    Функция просто находит первый заголовок сегмента с помощью макроса 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 В сегмент может быть осуществлена запись
    Добавил: DMT
    Дата создания: 30 декабря 2007, 18:56
    Дата обновления: 30 декабря 2007, 18:56
    Просмотров: 10463 последний сегодня, 21:49
    Комментариев: 2

    Вопрос 4. Формат исполняемого РЕ файла ОС Windows . Приведите фрагмент программы, читающей из РЕ файла список импортируемых программой функций ОС.

    Комментарии для «Вопрос 4. Формат исполняемого РЕ файла ОС Windows . Приведите фрагмент программы, читающей из РЕ файла список импортируемых программой функций ОС. «

    Пользователь: ruslan
    Сообщений: 23
    Статус: Незримый
    Зарегистрирован:
    5 января 2008, 2:42
    Был:29 января 2008, 21:23
    Дата: 5 января 2008, 3:47 Сообщение № 1
    Формат исполняемого файла операционной системы в значительной степени отражает встроенные в операционную систему предположения и режимы поведения. Динамическая компоновка, поведение загрузчика и управление памятью – это только три примера специфических свойств операционной системы, которые можно понять по мере изучения формата исполняемых файлов.

    Исполняемый файл на диске и модуль, получаемый после загрузки, очень похожи. Загрузчик попросту использует отображенные в память файлы Win32, чтобы загрузить соответствующие части РЕ-файла в адресное пространство программы. Так же просто загружается и DLL. После того как ЕХЕ или .DLL модуль загружены, Windows обращается с ними так же, как и с другими отображенными в память файлами.

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

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

    typedef struct _IMAGE_DOS_HEADER < //DOS .EXE заголовок
    USHORT e_magic; //MZ
    USHORT e_cblp; //Байты на последней
    //странице файла
    USHORT e_cp; //Страницы в файле
    USHORT e_crlc; //Настройки
    USHORT e_cparhdr; //Размер заголовка в
    //параграфах
    USHORT e_minalloc; //Минимальная выделенная память
    USHORT e_maxalloc; //Максимальная выделенная память
    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 идентификатор (для
    //e_oeminfo)
    USHORT e_oeminfo; //OEM информация; e_oemid
    //специфический
    USHORT e_res2 [10]; //Зарезервированные слова
    LONG e_lfanew; //адрес смещения PE-заголовка
    > IMAGE_DOS_HEADER, * PIMAGE_DOS_HEADER;

    Основной заголовок РЕ-файла представляет структуру типа IMAGE_NT_НEADERS, определенную в файле WINNT.H. Структура IMAGE_NT_HEADERS в памяти – это то, что Windows использует в качестве своей базы данных модуля в памяти. Каждый загруженный ЕХЕ-файл или DLL представлены в Windows структурой IMAGE_NT_HEADERS. Эта структура состоит из двойного слова и двух подструктур, как показано ниже:

    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER OptionalHeader;

    PE File Signature

    Поле Signature (сигнатура – подпись), представленное как ASCII код, – это РЕ\0\0 (два нулевых байта после РЕ). Если поле e_lfanew в заголовке DOS указало вместо обозначения РЕ обозначение NE в этом месте, значит, вы работаете с файлом Win16 NE. Аналогично, если указано обозначение LE в поле Signature, то это файл VxD (VirtualDeviceDriver – драйвер виртуального устройства). Обозначение LX указывает на файл старой соперницы Windows 95 – OS/2.

    За двойным словом – сигнатурой РЕ, в заголовке РЕ-файла следует структура типа IMAGE_FILE_HEADER. Поля этой структуры содержат только самую общую информацию о файле.
    Далее приводятся поля IMAGE_FILE_HEADER:

    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

    Machine – это центральный процессор, для которого предназначен файл. Определены следующие идентификаторы процессоров:

    Intel I386 0xl4C
    Intel I860 0xl4D
    MIPS R3000 0х162
    MIPS R4000 0х166
    DEC Alpha AXP 0х184
    Power PC 0x1F0 (little endian)
    Motorola 68000 0х268
    PA RISC 0х290 (Precision Architecture)

    NumberOfSections – количество секций в ЕХЕ- или OBJ-файле.

    TimeDateStamp – время, когда файл был создан компоновщиком (или компилятором, если это OBJ-файл). В этом поле указано количество секунд, истекших с 16:00 31.12.1969

    PointerToSymbolTable – файловое смещение COFF-таблицы символов. Это поле используется только в OBJ- и РЕ-файлах с информацией COFF-отладчика. РЕ-файлы поддерживают разнообразные отладочные форматы, так что отладчики должны ссылаться ко входу IMAGE_DIRECTORY_ENTRY_DEBUG в каталоге данных.

    NumberOfSymbols – количество символов в COFF-таблицс символов.

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

    Characteristics – флаги, содержащие информацию о файле. Здесь описываются некоторые важные поля.
    0х0001 – файл не содержит перемещений
    0х0002 – файл представляет исполняемое отображение (т.е. это не OBJ- или LIB-файл)
    0х2000 – файл является библиотекой динамической компоновки (DLL), а не программой

    PE File Optional Header

    Третьим компонентом заголовка РЕ-файла является структура типа IMAGE_OPTIONAL_HEADER. Для РЕ-файлов эта часть является обязательной. Наиболее важными полями являются поля ImageBase и Subsystem.

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

    Subsystem – тип подсистемы, которую данный исполняемый файл использует для своего пользовательского интерфейса. WINNT.H определяет следующие значения:
    NATIVE = 1 – подсистема не требуется (например, для драйвера устройства)
    WINDOWS_GUI = 2 – запускается в подсистеме Windows GUI
    WINDOWS_GUI = 3 – запускается в подсистеме Windows character (терминальное приложение)
    OS2_GUI = 5 – запускается в подсистеме OS/2 (только приложения OS/2 IJC)
    POSIX_GUI = 7 – запускается в подсистеме Posix

    Сразу после заголовка РЕ-файла в памяти следует массив из 1MAGE_SECT10N_HEADER. Эта таблица. содержит информацию о каждой секции отображения. Количество элементов этого массива задается в заголовке РЕ-файла (поле IMAGE_NT_HEADER.FileHeader.NumberOfSections). Секции в отображении упорядочены по их стартовому адресу, а не в алфавитном порядке.
    Каждый IMAGE_SECTION_HEADER представляет собой полную базу данных об одной секции файла ЕХЕ или OBJ \ и имеет следующий формат.

    #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;

    Name[IMAGE_SIZEOE_SHORT_NAME] – 8-байтовое имя в стандарте ANSI (не Unicode), которое именует секцию.

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

    VirtualAddress – в случае ЕХЕ-файлов это поле содержит RVA, куда загрузчик должен отобразить секцию. Средства Microsoft устанавливают по умолчанию RVA первой секции равным 0х101. Для объектных файлов это поле устанавливается в 0.

    SizeOfRawData – в ЕХЕ-файлах это поле содержит размер секции, выровненный па ближайшую верхнюю границу размера файла.
    PointerToRawData – файловое смещение участка, где находятся исходные данные для секции. Если пользователь сам отображает в мять РЕ- или COFF-файл (вместо того, чтобы доверить загрузку операционной системе), это поле важнее, чем в VirtualAddress.

    PointerToRelocations – в объектных файлах это файловое смещение информации о поправках, которая следует за исходными данными для данной секции. В ЕХЕ-файлах это поле устанавливается в 0.

    PointerToLinenumhers – файловое смещение таблицы номеров строк. Таблица номеров строк ставит в соответствие номера строк исходного файла адресам, по которым можно найти код, сгенерированный для данной строки. Обычно только секции с программным кодом (например, .text или CODE) имеют номера строк. В ЕХЕ-файлах номера строк собраны в конце файла после исходных данных для секций. В объектных файлах таблица номеров строк для секции следует за исходными данными секции и таблицей перемещений для этой секции.

    NumberOfRelocations – количество перемещений в таблице поправок для данной секции (используется только в объектных файлах).
    NumberOfLinenumbers – количество номеров строк в таблице номеров строк для данной секции.
    Characteristics – набор флагов, которые указывают на атрибуты секции (программа/данные, предназначен для чтения, предназначен для записи и т.н.).

    Часто встречающиеся секции

    Секция .text (или CODE , если PE-файл создан Borland C++)

    В этой секции собран весь программный код общего назначения, генерируемый компилятором или ассемблером. Компоновщик объединяет все секции .text из различных объектных файлов в одну большую секцию .text в ЕХЕ-файле.

    Секция .data (или DATA , если PE-файл создан Borland C++)

    Инициализированные данные попадают в секцию .data. Инициализированные данные состоят из тех глобальных и статических переменных, которые были проинициализированы во время компиляции. Они также включают строковые литералы (например, строку «Hello World» в программе C/C++). Компоновщик объединяет все секции .data из разных объектных и LIB-файлов в одну секцию .data в ЕХЕ-файле. Локальные переменные расположены в стеке цепочки и не занимают места в секциях .data и .bss.

    В секции .bss хранятся неинициализированные статические и глобальные переменные. Компоновщик объединяет все сек¬ции .bss из разных объектных и LIB-файлов в одну секцию .bss в ЕХЕ-файле.

    Еще одна секция для инициализированных данных, используемая библиотеками поддержки выполнения программы Microsoft C/C++. Данные из этой секции используются для таких це¬лей, как вызов конструкторов статических классов C++ перед вызовом main или WinMain.

    Секция .rsrc содержит ресурсы модуля.

    Секция .idata (или таблица импорта) содержит информацию о функциях (и данных), которые модуль импортирует из других DLL. Таблица импорта начинается с массива, состоящего из IMAGE_IMPORT_DESCRIPTOR. Каждый элемент (IMAGE_IMPORT_DESCRIPTOR) соответствует одной из DLL, с кото¬рой неявно связан данный РЕ-файл. Количество элементов в массиве нигде не учитывается. Вместо этого последняя структу¬ра массива IMAGE_IMPORT_DESCRIPTOR имеет поля, содержащие NULL.
    Структура IMAGE_IMPORT_DESCRIPTOR имеет следующий формат

    typedef struct _IMAGE_IMPORT_DESCRIPTOR <
    union <
    DWORD Characteristics;
    DWORD OriginalFirstThunk;
    >;
    DWORD TimeDateStamp;

    DWORD ForwarderChain;
    DWORD Name;
    DWORD FirstThunk;
    > IMAGE_IMPORT_DESCRIPTOR

    Characteristics/OriginalFirstThunk – в этом поле содержится смещение (RVA) массива двойных слов. Каждое из этих двойных слов в действительности является объединением IMAGE_THUNK_DATA. Каждое двойное слово IMAGE_THUNK_DATA соответствует одной функции, импортируемой данным ЕХЕ-файлом или DLL.

    TimeDateStamp – отметка о времени и дате, указывающая, когда был создан данный файл.
    ForwarderChain – это поле имеет отношение к передаче, когда одна DLL передает ссылку на какую-то свою функцию другой DLL.
    Name – это RVA строки символов ASCII, оканчивающейся нулем и содержащей имена импортируемых DLL.

    FirstThunk – RVA-смещение массива двойных слов IMAGE_THUNK_DATA. В большинстве случаев двойное слово рассматривает¬ся как указатель на структуру IMAGE_IMPORT_BY_NAME. Это структура выглядит следующим образом:

    typedef struct _IMAGE_IMPORT_BY_NAME <
    WORD Hint;
    BYTE Name[1];
    > IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

    Hint – номер экспорта у функции импорта.
    Name – Строка ASCIIZ с именем импортируемой функции.

    Фрагмент программы читающей из РЕ файла список импортируемых программой функций ОС.

    Путь воина — внедрение в PE/COFF-файлы

    КРИС КАСПЕРСКИ

    Путь воина – внедрение в pe/coff-файлы

    …хакерство вытеснило все – голод, интерес к девушкам, друзей, учебу, родителей, смысл жизни.

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

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

    только заядлому наркоману который наконец ширнулся после двухмесячного «голода»…

    Статья подробно описывает формат PE-файлов, раскрывая особенности внутренней кухни системного загрузчика и двусмысленности фирменной спецификации, предупреждая читателя о многочисленных ловушках, подстерегающих его на пути внедрения своего кода в чужие исполняемые файлы. Здесь вы найдете большое количество исходных текстов, законченных решений и наглядных примеров, упрощающих восприятие материала. Статья ориентирована главным образом на Windows NT/9x и производные от них системы, но также затрагивает и проблему совместимости с Windows-эмуляторами, такими, например, как wine и doswin32.

    После публикации статьи, посвященной UNIX-вирусам, ко мне стали приходить письма с просьбами написать «точно такую же, но только под Windows». Действительно, внедрение постороннего кода в PE-файлы – очень перспективное и нетривиальное занятие, интересное не только вирусописателям, но и создателям навесных протекторов/упаковщиков в том числе.

    Что же до этической стороны проблемы… политика «воздержания» и удержания передовых технологий под сукном лишь увеличивает масштабы вирусных эпидемий, и когда дело доходит до схватки, никто из прикладных программистов и администраторов к ней оказывается не готов. В стане системных программистов дела обстоят еще хуже. Исходных текстов операционной системы нет, PE-формат документирован кое-как, поведение системного загрузчика вообще не подчиняется никакой логике, а допрос с пристрастием (читай – дизассемблирование) еще не гарантирует, что остальные загрузчики поведут себя точно так же.

    На сегодняшний день не существует ни одного более или менее корректного упаковщика/протектора под Windows, в полной мере поддерживающего фирменную спецификацию и учитывающего недокументированные особенности поведения системных загрузчиков в операционных системах Windows 9x/NT. Про различные эмуляторы, такие, например, как wine, doswin32, мы лучше промолчим, хотя нас так и подмывает сказать, что файлы, упакованные ASPack в среде doswin32 либо не запускаются вообще, либо работают крайне нестабильно, а все потому что упаковщик ASPack не соответствует спецификации, закладываясь на те особенности системного загрузчика, преемственности которых никто и никому не обещал. В лучшем случае авторы эмуляторов добавляют в свои продукты обходной код, предназначенный для обработки подобных вещей, в худшем же – оставляют все как есть, мотивируя это словами «повторять чужое пионерство себе дороже…»

    А восстановление пораженных объектов? Многие файлы после заражения отказывают в работе, и попытка вылечить их антивирусом лишь усугубляет ситуацию. Всякий уважающий себя профессионал должен быть готов вычистить вирус вручную, не имея под рукой ничего, кроме hex-редактора! То же самое относится и к снятию упаковщиков/дезактивации навесных протекторов. Эй! Кто там начал бурчать про злобных хакеров и неэтичность взлома? Помилуйте, что за фрейдистские ассоциации?! Ну нельзя же всю жизнь что-то ломать (надо на чем-то и сидеть!). В майском номере «Системного администратора» за 2004 год опубликована замечательная статья Андрея Бешкова, живописно обрисовывающая возню с протекторами под эмулятором wine. Как говорится, тут не до жиру – быть бы живу. Какой смысл платить за регистрацию, если воспользоваться защищенной программой все равно не удастся?!

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

    Широта охватываемых тем не позволила рассказать обо всем в одной статье, и ее пришлось разбить на две части – та, которую вы сейчас держите в руках, посвящена описанию малоизвестных особенностей PE-файлов, без знания которых свой упаковщик/протектор ни за что не написать (по крайней мере работоспособный упаковщик/протектор – точно). А конкретные механизмы внедрения чужеродного кода мы рассмотрим в следующий раз.

    Особенности структуры PE-файлов в конкретных реализациях


    Знакомство читателя с PE-форматом не входит в нашу задачу и предполагается, что некоторый опыт работы с ними у него уже имеется. Существует множество описаний PE-формата, но среди них нет ни одного по-настоящему хорошего. Официальная спецификация (Microsoft Portable Executable and Common Object File Format Specification), написанная двусмысленным библейским языком, скорее напоминает сферического коня в вакууме, чем практическое руководство. Даже среди сотрудников Microsoft нет единого мнения по поводу, как именно следует его толковать, и различные системные загрузчики ведут себя сильно неодинаково. Что же касается сторонних разработчиков, то здесь и вовсе царит полная неразбериха.

    Понимание структуры готового исполняемого файла еще не наделяет вас умением самостоятельно собирать такие файлы вручную. Операционные системы облагают нас весьма жесткими ограничениями, зачастую вообще не упомянутыми в документации и варьирующимися от одной ОС к другой. Не так-то просто создать файл, загружающийся больше чем на одной машине (которой, как правило, является машина его создателя). Один шаг в сторону – и загрузчик открывает огонь без предупреждения, выдавая малоинформативное сообщение в стиле «файл не является win32 приложением», после чего остается только гадать: что же здесь неправильно (кстати говоря, Windows 9x намного более подробно диагностирует ошибку, чем Windows NT, если, конечно, некорректный файл не вгонит ее в крутой завис, а виснет она на удивление часто – загрузчик там писали пионеры не иначе).

    Технические писатели, затрагивающие тему исполняемых файлов и совершенно не разбирающиеся в предметной области, за которую взялись, за неимением лучших идей прибегают к довольно грязному трюку и подменяют одну тему другой. Отталкиваясь от уже существующих PE-файлов, созданных линкером, они долго и занудно объясняют назначение каждого из полей, демонстративно прогуливаясь по ссылочным структурам от вершины до дна. Сложнее разобраться, почему эти структуры сконструированы именно так, а не иначе. Какой в них заложен запас прочности? Каким именно образом их интерпретирует системный загрузчик? А что на счет предельно допустимых значений? Увы, все эти вопросы остаются без ответа. Чтение статей в стиле «The Portable Executable File Format from Top to Bottom» от Randy Kath из Microsoft Developer Network Technology Group – это хороший способ запудрить себе мозги и написать мертворожденный PE-дампер, переваривающий только «честные» файлы и падающий на всех остальных (dumpbin ведь падает!). Аналогичным образом поступает и Matt Pietrek, обходящий базовые концепции PE-файла стороной и начинающий процесс описания с середины, но так и не доводящий его до логического конца.

    Иначе поступает автор статьи «Об упаковщиках в последний раз» (https://www.wasm.ru/print.php?article= packlast01 и https://www.wasm.ru/print.php?article=packers2), сосредоточивший свои усилия на исследовании системного загрузчика W2K/XP и допустивший при этом большое количество фактических ошибок, полный разбор которых потребовал бы отдельной статьи. При всей ценности этой работы она нисколько не проясняет ситуацию и только добавляет вопросов. Автор сетует на то, что работа загрузчика полностью не документирована и даже у Руссиновича обнаруживаются лишь обрывки информации. Ну была бы она документирована – что бы от этого изменилось? Какое нам дело до того, что в W2K/XP загрузка файла сводится к вызову MmCreateSection? Во-первых, в остальных системах это не так, а во-вторых, это сегодня Microsoft стремится весь ввод/вывод делать через mmap, но когда до горячих американских парней дойдет, что это тормоза, а не ускорение, политика изменится, и MmCreateSection отправятся на заслуженный отдых (в чулан ее, на полку!).

    Дизассемблировать ядро совсем небесполезно, но вот закладываться на полученные результаты, не проверив их на остальных осях, ни в коем случае нельзя! Верить в спецификации по меньшей мере наивно, ведь всякая спецификация – это только слова, а всякий программный код – лишь частный случай реализации. И то, и другое непостоянно и переменчиво. Чтение книжек (равно как и протирание штанов в учебных заведениях различной степени тяжести) еще никого не сделало программистом. Лишь «опыт, сын ошибок трудных», да общение с коллегами-системщиками, позволят избежать грубых ошибок. Как говорится, не падает только тот, кто лежит, а кто бежит – падает, наступает на грабли и попадает в логические ямы, глубокие, как колодцы из романа Мураками.

    Автор, имеющий богатый опыт в работе с PE-файлами и помнящий численные значения смещений всех структур как отче наш, в процессе работы над статьей в такие колодцы попадал неоднократно (да и сейчас там сидит). Всякое значение подобно больному зубу – если его не трогать, он не будет ныть. Отдельные пробелы, неясности и непонятности неизбежны. Когда пишешь рабочие заметки «для себя», просто махаешь рукой и говоришь: да какая разница, что этот большой красный рубильник делает? Работает ведь – и ладно… Статья – дело другое и тут хочешь не хочешь, а будь добр разложить все по полочкам! Автор выражает глубокую признательность удивительному человеку, мудрому программисту и создателю замечательного линкера ulink Юрию Харону (ftp://ftp.styx.cabel.net/pub/UniLink), терпеливо отвечавшему на мои сумбурные и нечетко сформулированные вопросы. Если бы не его консультации, эта статья ни за что бы не получилось такой, какова она есть!

    Общие концепции и требования, предъявляемые к PE-файлам

    Структурно PE-файл состоит из заголовка (header), страничного имиджа (image page) и необязательного оверлея (overlay). Представление PE-файла в памяти называется его виртуальным образом (virtual image) или просто образом, а на диске – файлом или дисковым образом. Если не оговорено обратное, то под образом всегда понимается виртуальный образ.

    Образ характеризуется двумя фундаментальными – адресом базовой загрузки (image base) и размером (image size). При наличии перемещаемой информации (relocation/fixup table) образ может быть загружен по адресу, отличному от image base и назначаемому непосредственно самой операционной системой.

    Образ делится на страницы (pages), а файл – на секторы (sectors). Виртуальный размер страниц/секторов задается явно в заголовке файла и не обязательно должен совпадать с физическим.

    Системный загрузчик требует от образа непрерывности, документация же обходит этот вопрос стороной. На всем протяжении между image base и (image base + size of image) не должно присутствовать ни одной бесхозной страницы, не принадлежащей ни заголовку, ни секциям – такой файл просто не будет загружен. (С этим не совсем согласен Юрий Харон, однако ни одного «прерывистого» файла выловить в живой природе мне не удалось, а попытка создать таковой самостоятельно всякий раз заканчивалась неизменным неуспехом). Бесхозных же секторов в любой части файла может быть сколько угодно. Каждый сектор может отображаться на любое количество страниц (по одной странице за раз), но никакая страница не может отображать на один и тот же регион памяти более одного сектора.

    Для работы с PE-файлами используются три различные схемы адресации: физические адреса (называемые также сырыми указателями или смещениями raw pointers/raw offset или просто offset), отсчитываемые от начала файла; виртуальные адреса (virtual address или сокращенное VA), отсчитываемые от начала адресного пространства процесса, и относительные виртуальные адреса (relative virtual address или сокращенно RVA), отсчитываемые от базового адреса загрузки. Все трое измеряются в байтах и хранятся в 32-битных указателях (в PE64 все указатели 64-битные, но где мы, а где PE64?). Параграфы давно вышли из моды, а жаль… Вообще-то существует и четвертый тип адресации – RRA, что расшифровывается как Raw Relative Address (сырые относительные адреса) или Relative Relative Address (относительно относительные адреса). Терминология вновь моя, ибо официального названия у такого способа адресации нет и не предвидится. Иногда его называют offset, что не совсем верно, т.к. offset бывают разные, а RRVA-адреса всегда отсчитываются от стартового адреса своей структуры (в частности, Offset ModuleName задает смещение от начала таблицы диапазонного импорта).

    Страничный имидж состоит из одной или нескольких секций. С каждой секцией связано четыре атрибута: физический адрес начала секции в файле/размер секции в файле, виртуальный адрес секции в памяти/размер секции в памяти и атрибут характеристик секции, описывающий права доступа, особенности ее обработки системным загрузчиком и т. д. Грубо говоря, секция вправе сама решать, откуда и куда ей грузиться, однако эта свобода весьма условна и на ассортимент выбираемых значений наложено множество ограничений. Начало каждой секции в памяти/диске всегда совпадает с началом виртуальных страниц/секторов соответственно. Попытка создать секцию, начинающуюся с середины, жестоко пресекается системным загрузчиком, отказывающимся обрабатывать такой файл. С концом складывается более демократичная ситуация и загрузчик не требует, чтобы виртуальный (и частично физический) размер секций был кратен размеру страницы. Вместо этого он самостоятельно выравнивает секции, забивая их хвост нулями, так что никакая страница (сектор) не может принадлежать двум и более секциям сразу. Фактически это сплошное надувательство – не выровненный (в заголовке!) размер автоматически выравнивается в страничном имидже, поэтому представленные нам полномочия на проверку оказываются сплошной фикцией.

    Все секции совершенно равноправны, и тип каждой из них тесно связан с ее атрибутами, интерпретируемыми довольно неоднозначным и противоречивым образом (см. «Таблица секций»). Реально (читай – на сегодняшний день) мы имеем два аппаратных и два программных атрибута: Accessible/Writeable и Shared/Loadable (последний – условно) соответственно. Вот отсюда и следует плясать! Все остальное – из области абстрактных концепций.

    «Секция кода», «секция данных», «секция импорта» – не более чем образные выражения, своеобразный рудимент старины, оставшийся в наследство от сегментной модели памяти, когда код, данные и стек действительно находились в различных сегментах, а не были сведены в один, как это происходит сейчас.

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

    Оверлей, в своем каноническом определении сводящийся к «хвостовой» части файла, не загружаемой в память, в PE-файлах может быть расположен в любом месте дискового образа, в том числе и посередине. Действительно, если между двумя смежными секциями расположено несколько бесхозных секторов, не приватизированных никакой секцией, такие сектора останутся без представления в памяти и имеют все основания считать себя оверлеями. Собственно говоря, оверлеями их можно называть только в переносном смысле. Спецификация на PE-файлы этого термина в упор не признает и никаких, даже самых примитивных, механизмов поддержки с оверлеями win32 API не обеспечивает (не считая, конечно, примитивного ввода/вывода).

    За сим все! Теперь, после составления контурной карты PE-файлов, можно смело приступать к ее детализации, не рискуя заблудиться в непроходимых терминологических и технических джунглях.

    Внимание! Эту статью нельзя читать как приключенческий роман или детектив. Разумеется, я приложил все усилия и как мог структурировал материал, стремясь писать максимально доходчивым языком, хотя бы и ценой второстепенных деталей. Тем не менее для оперативного переваривания информации вам придется обложиться стопками распечаток и, вооружившись hex-редактором, сопровождать чтение статьи перемещением курсора по файлу, чтобы самостоятельно «потрогать руками» все описываемые здесь структуры…

    Да осилит дорогу идущий! Когда вы доберетесь до конца, вы поймете, почему не работают некоторые файлы, упакованные ASPack/ASPrpotect, и как это исправить, не говоря уже о том, что сможете создавать абсолютно легальные файлы, которые ни один дизассемблер не дизассемблирует в принципе!

    Рубрика: Программирование / Анализ данных
    PEFile Format
    MS-DO MZ Header
    MS-DOS Real-Mode Stub Program
    PE File Signature
    PE File Header
    PE File Optional Header
    .text Section Header
    .bss Section Header
    .rdata Section Header
    . . .
    .debug Section Header
    .text Section
    .bss Section
    .rdata Section
    . . .
    .debug Section

    Рисунок 1. Схематическое изображение PE-файла

    Все PE-файлы без исключения (и системные драйверы в том числе!) начинаются с old-exe заголовка, за концом которого следует dos-заглушка (ms-dos real-mode stub program или просто stub), обычно выводящая разочаровывающее ругательство на терминал, хотя в некоторых случаях в нее инкапсулирована MS-DOS версия программы, но это уже экзотика. Мэтт Питтерек в «Секретах системного программирования под Windows 95» пишет: «после того как загрузчик win32 отобразит в память PE-файл, первый байт отображения файла соответствует первому байту заглушки DOS». Это неверно! Первый байт отображения соответствует первому байту самого файла, т.е. отображение всегда начинается с сигнатуры «MZ», в чем легко можно убедиться, загрузив файл в отладчик и просмотрев его дамп.

    PE-заголовок, в подавляющем большинстве случаев начинающийся непосредственно за концом old-exe программы, на самом деле может быть расположен в любом месте файла – хоть в середине, хоть в конце, т.к. загрузчик определяет его положение по двойному слову e_lfanew, смещенному на 3Ch байт от начала файла.

    PE-заголовок представляет собой 18h-байтовую структуру данных, описывающую фундаментальные характеристики файла и содержащую «PEx0x0»-сигнатуру, по которой файл, собственно говоря, и отождествляется.

    Непосредственно за концом PE-заголовка следует опциональный заголовок, специфицирующий структуру страничного имиджа более детально (базовый адрес загрузки, размер образа, степень выравнивания – все это и многое другое задается именно в нем). Название «опциональный» выбрано не очень удачно и слабо коррелирует с окружающей действительностью, ибо без опционального заголовка файл попросту не загрузится, так какой же он «опциональный», если обязательный? (Впрочем, когда PE-формат только создавался, все было по-другому, а сейчас мы вынуждены тащить это наследие старины за собой.) Важной структурой опционального заголовка является DATA_DIRECTORY, представляющая собой массив указателей на подчиненные структуры данных, как то: таблицы экспорта и импорта, отладочную информацию, таблицу перемещаемых элементов и т. д. Типичный размер опционального заголовка составляет E0h байт, но может варьироваться в ту или иную сторону, что определяется полнотой занятости DATA_DIRECTORY, а также количеством мусора за ее концом (если таковой вдруг там есть, хотя его настоятельно рекомендуется избегать). Может показаться забавным, но размер опционального заголовка хранится в PE-заголовке, так что эти две структуры очень тесно связаны.

    За концом опционального заголовка следует суверенная территория, оккупированная таблицей секций. Политическая принадлежность ее весьма условна. Ни к одному из заголовков она не принадлежит и, судя по всему, является самостоятельным заголовком безымянного типа (подробнее см. «SizeOfHeaders» и «Таблица секций»). Редкое внедрение в исполняемый файл обходится без правки таблицы секций, поэтому эта структура для нас ключевая.

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

    Начиная с raw offset первой секции, указанного в таблице секций, простирается страничный имидж, точнее, его упакованный дисковый образ. «Упакованный» в том смысле, что физические размеры секций (с учетом выравнивания) включают в себя лишь инициализированные данные и не содержат ничего лишнего (ну хорошо, «не должны содержать ничего лишнего…»). Виртуальный размер секций может существенно превосходить физический, что с секциями данных случается сплошь и рядом. В памяти секции всегда упорядочены, чего нельзя сказать о дисковом образе. Помимо дыр, оставшихся от выравнивания, между секциями могут располагаться оверлеи, к тому же порядок следования секций в памяти и на диске совпадает далеко не всегда…

    Одни секции имеют постоянное представительство в памяти, другие – нанимаются лишь на период загрузки, по завершении которой в любой момент могут быть безоговорочно выдворены оттуда (не сброшены в своп, а именно выдворены, то есть депортированы!). Что же до третьих – они вообще никогда не загружаются в память, ну разве что по частям. В частности, секция с отладочной информацией ведет себя именно так. Впрочем, отладочная информация не обязательно должна оформляться в виде отдельной секции, и чаще она подцепляется к файлу в виде оверлея.

    За концом последней секции обычно бывает расположено некоторое количество мусорных байт, оставляемых линкером по небрежности. Это не оверлей (к нему никогда не происходит обращений), хотя и нечто очень на него похожее. Разумеется, оверлеев может быть и несколько – системный загрузчик не налагает на это никаких ограничений, однако и не предоставляет никаких унифицированных механизмов работы с оверлеями – программа, создавшая свой оверлей, вынуждена работать с ним самостоятельно, задействовав API ввода/вывода (впрочем, «вывод» не работает в принципе, т.к. загруженный файл доступен только на чтение, запись в него наглухо заблокирована).

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

    Что можно и что нельзя делать с PE-файлом

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

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

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

    • увеличивать размер страничного имиджа, записываясь в его конец;
    • оккупировать незанятые области (например, те, что используются для выравнивания);
    • выделять память на стеке/куче, перемещая туда свое тело.

    Поскольку секции располагаются в файле по выровненным адресам, между ними практически всегда остается свободное пространство, уверенно вмещающее в себя крохотный загрузчик, подкачивающий «хвост» вируса из оверлея. Как вариант (если нет другого оверлея), можно увеличить размер последней секции и записаться в ее конец. Более радикально настроенный код может сбросить часть чужой секции в оверлей, усевшись на освободившееся место, а затем, непосредственно перед передачей управления, восстановить ее обратно. Внешний антураж выглядит просто замечательно, но задумайтесь, что произойдет, если:

    • сбрасываемый фрагмент секции будет содержать одну или несколько служебных таблиц, например таблицу импорта;
    • сбрасываемый фрагмент секции будет содержать один или несколько перемещаемых элементов.

    Таким образом, перед тем как сбрасывать что бы то ни было в оверлей, внедряемый код должен проанализировать все служебные структуры, прописанные в DATA DIRECTORY, чтобы ненароком не сбросить ничего лишнего. Затем необходимо проанализировать таблицу перемещаемых элементов (если она есть) и либо выбрать участок, свободный от перемещений, либо удалить соответствующие элементы из таблицы с тем, чтобы впоследствии обработать их самостоятельно. До ресурсов дотрагиваться ни в коем случае нельзя, иначе проводник иконки не найдет!

    Но хватит говорить о плохом. Давайте лучше о хорошем. Все секции стандартного PE-файла, за исключением секции с отладочной информацией, используют только RVA/RRA- и VA-адресацию, а это значит, что мы можем свободно перемещать секции внутри дискового образа: менять их местами, внедрять между ними оверлеи – и все это никак не скажется на работоспособности файла, поскольку страничный имидж во всех случаях будет один и тот же! Это не покажется удивительным, если вспомнить, что виртуальный и физический адреса каждой секции хранятся в различных, никак не связанных друг с другом полях, поэтому внедрение кода в середину файла еще не обозначает его внедрения в середину страничного имиджа.

    Внедряться в конец файла – слишком просто, неинтересно и небезопасно. Внедряться в начало кодовой секции со сбросом оригинального содержимого последнего в оверлей – слишком сложно. А что, если… попробовать внедриться перед началом кодовой секции, передвинув ее начало в область младших адресов? Виртуальный образ окажется при этом практически нетронутым и останется лежать по тем же самым адресам, которые занимал до вторжения, что сохранит файлу работоспособность, попутно лишая разработчика внедряемого кода контакта с перемещаемыми элементами и прочими служебными структурами данных. Все это так, за исключением одного досадного «но». Первая секция подавляющего большинства файлов уже начинается по наименьшему из всех доступных адресов, и передвигать ее просто некуда. Правда, под NT можно отключить выравнивание и делать с секциями все что угодно, но тогда файл не сможет работать под 9x (подробнее см. «FileAlignment/SectionAlignment»). То же самое относится и к уменьшению базового адреса загрузки, компенсируемого увеличением стартовых адресов всех секций, в результате чего положение страничного имиджа не изменяется, а мы выигрываем место для внедрения своего собственного кода. Увы! Служебные структуры PE-файлов активно используют RVA-адресацию, отсчитываемую от базового адреса загрузки, поэтому просто взять и передвинуть базовый адрес не получится – необходимо как минимум проанализировать таблицы экспорта/импорта, таблицу ресурсов и скорректировать все RVA-адреса, а как максимум… типичный базовый адрес загрузки для исполняемых файлов – 400000h выбран далеко не случайно. Это минимальный базовый адрес загрузки в Windows 9x, и если он будет меньше этого числа, системный загрузчик попытается переместить файл, потребовав таблицу перемещаемых элементов, а у исполняемых файлов она с некоторого времени по умолчанию отсутствует (ну разве что линкер при компоновке специально попросите). С динамическими библиотеками ситуация не так плачевна (их базовый адрес загрузки выбирается с запасом, да и таблица перемещаемых элементов, как правило, есть), однако сложность реализации внедряемого кода просто чудовищна, к тому же нестандартный адрес загрузки сразу бросается в глаза. Так что ценность этого приема очень сомнительна…

    Тем не менее раздвигать страничный имидж все-таки можно! Секция кода практически никогда не обращается к секции данных по относительным адресам, а все абсолютные адреса в обязательном порядке должны быть перечислены в таблице перемещаемых элементов (конечно, при условии, что она вообще есть). Остаются лишь RVA/VA-адреса служебных структур данных, однако их реально скорректировать и вручную. Расширение страничного имиджа с внедрением в конец кодовой секции без сброса ее в оверлей – занятие не для слабонервных, однако игра стоит свеч, поскольку такой код идеально вписывается в архитектуру существующего файла и не привлекает к себе никакого внимания. Грубо говоря, это единственный способ вторжения, который нельзя распознать визуально (подробнее см. статью «Борьба с вирусами» в октябрьском номере журнала «Системного администратора» за 2003 год).

    Описание основных полей PE-файла

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

    • регулярно штудируют фирменную спецификацию перед сном;
    • давным-давно распечатали файл WINNT.h из SDK и обклеили им стены своей хакерской берлоги на манер обоев.

    Все нижеприведенные структуры взяты именно оттуда (внимание – зачастую они именуются совсем не так, как в спецификации, что вносит в ряды разработчиков жуткую путаницу и сумятицу).

    Здесь описываются не все, а лишь самые интересные и наименее известные поля, свойства и особенности поведения PE-файлов. За остальными – обращайтесь к документации.

    Содержит сигнатуру «MZ», доставшуюся в наследство от Марка Збиновски – ведущего разработчика MS-DOS и генерального архитектора EXE-формата. Если e_magic равен «MZ», загрузчик приступает к поиску «PE»-сигнатуры, в противном случае его поведение становится неопределенным. NT и 9x поддерживают недокументированную сигнатуру «ZM», передающую управление на MS-DOS заглушку и обычно выводящую на экран «This program cannot be run in DOS mode», что в данном случае не соответствует действительности, поскольку программа запускается из Windows!

    Один из приемов заражения PE-файлов сводится к внедрению в MS-DOS заглушки, динамически восстанавливающую сигнатуру «MZ» и делающую себе exec для передачи управления программе-носителю. Для восстановления пораженных объектов просто замените «ZM» на «MZ» и при запуске файла из Windows (включая MS-DOS сессию) вирус больше никогда не получит управления.

    Возможно использовать сигнатуру «NE», передающую управление на заглушку и устанавливающую значения сегментных регистров как в com, а не exe (DS == CS). Ни HIEW, ни IDA с таким файлом работать не могут и сразу же после его загрузки вылетают в астрал.

    Размер old-exe заголовка в параграфах (1 параграф равен 200h байтам). В настоящее время никем не проверяется (ну разве что дампером каким), однако закладываться на это не стоит. Минимальный размер заголовка составляет 1 параграф, а максимальный ограничен размером самой MS-DOS заглушки, т.е. если он будет больше поля e_lfanew, файл может и не загрузиться.

    Смещение PE-заголовка в байтах от начала файла. Должно указывать на первый байт PE-сигнатуры «PEx0x0», выровненной по границе двойного слова, причем если сумма image base и e_lfanew вылетает за пределы отведенного загрузчиком адресного пространства, такой файл не грузится.

    В памяти PE-заголовок (вместе со всеми остальными заголовками) всегда располагается перед первой секцией, вплотную прижимаясь к ее передней границе («вплотную» – значит, что расстояние между виртуальным адресом первой секции и концом заголовка должно быть меньше, чем Section Alignment). На диске PE-заголовок может быть расположен в любом месте файла, например, в его середине или конце (т.е. между началом файла и первым байтом PE-заголовка могут обосноваться одна или несколько секций). Не знаю, сойдет ли какой загрузчик от этого с ума, но в Windows 9x/NT все работает. При этом SizeOf Header должно быть равно действительному размеру PE-заголовка плюс e_lfanew; SectionAlignment >= SizeOfHeaders и FirstSection.RVA >= SizeOfHeaders.

    Тип центрального процессора, под который скомпилирован файл. Если здесь будет что-то отличное от 14Ch, на I386-машинах файл просто не загрузится.

    Количество секций. Файл, не содержащий ни одной секции, завешивает Windows 9x и корректно прерывает свою загрузку под Windows NT. Максимальное количество секций определяется особенностями реализации конкретного лоадера. Так, NT переваривает «всего» 60h секций. Другие загрузчики могут иметь и более жесткие ограничения. В общем, количество секций должно быть сведено к минимуму.

    Если заявленное количество секций меньше числа записей в Section Table, то остальные секции просто не грузятся, но в целом такой файл обрабатывается вполне нормально. Настоящее веселье начинается, когда Numbers OfSection превышает количество реально существующих секций, вылетая за конец Section Table. Если здесь окажутся нули (как чаще всего и бывает), Windows 9x отреагирует вполне нормально, чего нельзя сказать о Windows NT, наотрез отказывающейся загружать такой файл. Файл с количеством секций, равным нулю, мертво завешивает Windows 9x, в то время как Windows NT обрабатывает такую ситуацию вполне нормально, выдавая неизменное «файл не является приложением win32».

    Попутно заметим, что многие упаковщики исполняемых файлов по окончании процесса распаковки искажают это поле в памяти либо увеличивая, либо уменьшая его значение, в результате чего дамперы не могут корректно сбросить такой образ на диск. В pe-tools/lord-pe используется довольно ненадежный алгоритм, сканирующий Section Table и отталкивающийся от того, что если PointerToRelocations, PointerToLinenumbers, NumberOfRelocations и NumberOf Linenumbers равны нулю, а Characteristics – нет, значит, это секция. Эту святую простоту ничего не стоит обмануть! На самом деле, проверку следует ужесточить: если очередная запись в Section Table выглядит как секция (т.е. все поля валидны) – это секция и соответственно наоборот. Под валидностью здесь понимается, что адрес начала секции выровнен в памяти и лежит непосредственно за концом предыдущей секции, а размер секции не вылетает за пределы страничного имиджа.

    Ниже приведен простой макрос, считывающий содержимое поля NumberOfSection по указателю на первый байт PE-заголовка.

    Листинг 1. Считыватель содержимого NumberOfSection

    // p – указатель на PE-заголовок

    #define xNumOfSec(p) (*((WORD*) (p+0x6)))

    Указатель на размер отладочной информации в объективных файлах. В настоящее время не используется (да и раньше он не использовался тоже). Линкеры топчут оба поля в ноль, отладчики, дизассемблеры и системный загрузчик игнорируют его. Для предотвращения сброса дампа программы на диск запишите сюда нечто отличное от нуля и подтяните (в памяти) поле NumberOfSection от реального значения до безобразия. Текущие версии pe-tools сдохнут от зависти, но если NEOx сподобится встроить нормальный валидатор, этот трюк перестанет работать.

    Размер опционального заголовка, идущего следом за IMAGE_FILE_HEADER. Должен указывать на первый байт Section Table (т.е. e_lfanew + 18h + SizeOfOptionalHeader = &Section Table), где 18h – sizeof(IMAGE_FILE_HEADER). Если это не так, файл не загружается. И хотя некоторые загрузчики вычисляют указатель на Section Table, отталкиваясь от NumberOfRvaAndSizes, закладываться на это не стоит, т.к. системные загрузчики этого мнения не разделяют.

    Листинг 2. Макросы, возвращающие размер опционального заголовка, указатель на таблицу секций, вычисленный стандартным

    и альтернативным методами. В качестве входного аргумента все трое принимают указатель на первый байт PE-заголовка

    #define xopt_sz(p) (*((WORD*)(p + 0x14 /* size of optional header */)))

    #define pSectionTable(p) ((BYTE*)(xopt_sz(p)+0x18 /* size of image heafer */+p))

    #define pSectionTable_alt(p) ((BYTE*)((*((DWORD*)(p+0x74)))*8 + 0x78 + p))

    Атрибуты файла. Если (Characteristics & IMAGE_FILE_ EXECUTABLE_IMAGE) == 0, файл не грузится, т.е. первый, считая от нуля, бит характеристик обязательно должен быть установлен. У динамических библиотек должно быть установлено как минимум два атрибута: IMAGE_ FILE_EXECUTABLE_IMAGE/0002h и IMAGE_FILE_DLL/2000h, то же самое относится и к исполняемым файлам, экспортирующим одну или более функций. Если атрибут IMAGE_FILE_DLL установлен, но экспорта нет, исполняемый файл запускаться не будет.

    Остальные атрибуты не столь фатальны и под Windows NT/9x безболезненно переносят любые значения, хотя по идее делать этого не должны. Взять хотя бы IMAGE_FILE_BYTES_REVERSED_LO и IMAGE_FILE_BYTES_ REVERSED_HI, описывающие порядок следования байт в слове. Можно глупый вопрос? Какому абстрактному состоянию процессора соответствует одновременная установка обоих атрибутов? И какие действия должен предпринять загрузчик, если установленный порядок следования байт будет отличаться от поддерживаемого процессором? Операционные системы от Microsoft, просто игнорируют эти атрибуты за ненадобностью. То же самое относится и к атрибуту IMAGE_FILE_32BIT_MACHINE/0100h, которым по умолчанию награждаются все 32-разрядные файлы (16-разрядный PE – это сильно). Впрочем, без крайней нужды лучше не экспериментировать и заполнять все поля правильно.

    Весьма интересен флаг IMAGE_FILE_DEBUG_STRIPPED/0200h, указывающий на отсутствие отладочной информации и запрещающий отладчикам работать с ней даже тогда, когда она есть. Отладочная информация привязана к абсолютным смещениям, отсчитываем от начала файла и при внедрении в файл чужеродного кода путем его расширения, отладочная информация перестает соответствовать действительности, и поведение отладчиков становится крайне неадекватным. Для решения проблемы существует три пути:

    • скорректировать отладочную информацию (но для этого нужно знать ее формат);
    • отрезать отладочную информацию от файла (но для этого ее надо найти, кроме того, за концом файла может быть расположен посторонний оверлей);
    • установить флаг IMAGE_FILE_DEBUG_STRIPPED.

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

    Иначе ведет себя флаг IMAGE_FILE_RELOCS_STRIPPED, запрещающий перемещать файл, когда релокаций нет. Когда же они есть, загрузчик может с полным основанием не обращать на него внимания. Зачем же тогда этот атрибут нужен? Ведь переместить файл без таблицы перемещаемых элементов все равно невозможно… А вот это еще как сказать! Служебные структуры PE-файла используют только относительную адресацию и потому любой PE-файл от рождения уже перемещаем. Вся загвоздка в программном коде, активно использующем абсолютную адресацию (ну так уж устроены современные компиляторы). Технически ничего не стоит создать PE-файл, не содержащий перемещаемых элементов и способный работать по любому адресу (давным-давно, когда землей владели динозавры и никаких операционных систем еще не существовало, этим мог похвастать практически каждый). Таким образом, возникает неоднозначность: то ли перемещаемых элементов нет, потому что файл полностью перемещаем и fixup ему не нужны, то ли они просто недоступны и перемещать такой файл ни в коем случае нельзя.

    По умолчанию ms link версии 6.0 и старше внедряет перемещаемые элементы только в DLL, а исполняемые файлы сходят с конвейера неперемещаемыми, однако рассчитывать на это нельзя и при внедрении собственного кода в чужеродный PE-файл необходимо удостовериться, что он не содержит перемещаемых элементов, в противном случае возникают следующие программы:

    • ваш код не может закладываться на image base и должен быть готов к загрузке по любому адресу;
    • модификация ячеек, относящихся к перемещаемым элементам, обычно заканчивается крахом программы, поскольку они автоматически «исправляются» системным загрузчиком.

    Допустим, в программе был код типа: mov eax, 0400000h (B8 00 00 40 00), поверх которого мы начертали: push ebp/mov ebp, esp (55/8B EC). Допустим также, что в силу некоторых причин базовый адрес загрузки изменился с 40.00.00h на 1.00.00.00h. Ячейка памяти, ранее хранящая непосредственный операнд инструкции mov, будет переделана в 1.00.00.00h, что превратит команду mov ebp, esp в add [eax], al со всеми вытекающими отсюда последствиями.

    Существует по меньшей мере три пути решения этой проблемы:

    • убить fixup (но тогда файл станет неперемещаемым, а ведь некоторые исполняемые файлы подспудно экспортируют одну или несколько функций и без fixup не смогут работать);
    • перезаписывать только неперемещаемые ячейки (но это приведет к размазыванию кода по всему файлу, существенно усложняя его алгоритм);
    • обрабатывать перемещаемые элементы самостоятельно, чтобы система могла перемещать файл при необходимости, но не корежила наш код, подсуньте ей пустую таблицу перемещаемых элементов (подробнее см. «Перемещаемые элементы»).

    Состояние отображаемого файла. Если здесь будет что-то отличное от 10Bh (сигнатура исполняемого отображения), файл не загрузится. PE64-файлам соответствует сигнатура 20Bh (все адреса у них 64-разрядные), а в остальном они ведут себя как и нормальные 32-разрядные PE-файлы.

    Суммарный размер секций кода, инициализированных и неинициализированных данных (т.е. секций, имеющих атрибуты IMAGE_SCN_CNT_CODE/20h, IMAGE_SCN_CNT_ INITIALIZED_DATA/40h и IMAGE_SCN_CNT_UNINITIALIZED_ DATA/80h), никем не проверяется и может принимать любые, в том числе и заведомо бессмысленные, значения.

    Всякий линкер заполняет эти поля по-своему: одни берут физический размер секций на диске, другие – виртуальный размер в памяти, выровненный по границе Section Alignment, причем алгоритм определения принадлежности секции к тому или иному типу не стандартизирован и в полку разработчиков наблюдается большой разброд и шатание. Наиболее демократичное сословие определяет «родословную» по принципу OR (т.е. секция с атрибутами 60h считается и секцией кода, и секцией данных). Иначе действует аристократическая прослойка, придерживающаяся принципа XOR и относящая к данным только секции с атрибутами 40h (80h?). Для секции кода сделано некоторое послабление (ведь всякий код на каком-то этапе обработки представляется данными) и секция с атрибутами 60h или A0h все-таки относится к коду (в противном случае образовались бы неклассифицируемые секции, размер которых не был подсчитан, а этого допускать нельзя – религия не велит).

    Как бы там ни было, системному загрузчику на это глубоко наплевать (давным-давно, когда секции кода, данных и неинициализированных данных помещались в «свои» сегменты, эти поля еще имели какой-то смысл, но сейчас это рудиментный пережиток старины).

    Относительные базовые адреса кодовой секции и секции данных. Никем не проверяется и всяким компоновщиком заполняется по-своему. Для восстановления душевного равновесия оба поля можно смело сбросить в ноль, отдавая дань древним буддийским традициям.

    Относительный адрес точки входа, отсчитываемый от начала Image Base. Может указывать в любую точку адресного пространства, в том числе и не принадлежащую страничному имиджу (например, направленную на какую-нибудь функцию внутри ядра или dll). Для передачи управления на адреса, лежащие ниже Image Base, можно использовать целочисленное переполнение. Правда, не факт, что все загрузчики поймут нас правильно (NT поймет точно, остальные не проверял), так что закладываться на это нельзя.

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

    Для exe-файлов точка входа соответствует адресу, с которого начинается выполнение и не может быть равна нулю, а для динамических библиотек – функции диспетчера, условно называемой нами DllMain, хотя на самом деле при компоновке dll с настройками по умолчанию компоновщик внедряет стартовый код, перехватывающий на себя управление и вызывающий «настоящую» DllMain по своему желанию. DllMain вызывается при следующих обстоятельствах – загрузка/выгрузка dll и создание/уничтожение потока, если точка входа в dll равна нулю, функция DllMain не вызывается.

    Обязательно учитывайте это при внедрении собственного кода в dll! Чтобы отличить dll от обычных файлов, следует проанализировать поле характеристик (см. «Characteristics»). Опираться на наличие/отсутствие таблицы экспорта ни в коем случае нельзя, поскольку экспортировать функции могут не только динамические библиотеки, но исполняемые файлы! К тому же иногда встречаются динамические библиотеки, не экспортирующие ни одной функции.

    Базовый адрес загрузки страничного имиджа, измеряемый в абсолютных адресах, отсчитываемых от начала сегмента или, в терминологии оригинальной спецификации, preferred address (предпочтительный адрес загрузки). При наличии таблицы перемещаемых элементов файл может быть загружен по адресу, отличному от указанного в заголовке. Это происходит в тех случаях, когда требуемый адрес занят системой, динамической библиотекой или загрузчику захотелось что-то подвигать.

    Если предпочтительный адрес совпадает с адресом уже загруженной системной библиотеки, поведение последней становится неадекватной. Отладчик, интегрированный в Microsoft Visual Studio, запущенный под управлением NT, проскакивает точку входа и умирает где-то в окрестностях ядра (отлаживамая программа при этом продолжает исполняться). Под Windows 98 такие файлы отлаживаются вполне нормально, но при выходе из Windows уводят ее в астрал.

    Менять чужой Image Base ни в коем случае нельзя, т.к. перемещаемым элементам в этом случае будет просто не отчего отталкиваться. И хотя системный загрузчик в большинстве случаев загрузит такой файл вполне нормально, работать он не сможет, ну во всяком случае до тех пор, пока все перемещаемые элементы не будут скорректированы надлежащим образом.

    Кратность выравнивания секций на диске и в памяти. Очень интересное поле! Официально о кратности выравнивания известно лишь то, что она представляет собой степень двойки, причем:


    • Section Alignment должно быть больше или равно 1000h байт;
    • File Alignment должно быть больше или равно 200h байт;
    • Section Alignment должно быть больше или равно File Alignment.

    Если хотя бы одно из этих условий не соблюдается, файл не будет загружен.

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

    Если Section Alignment == File Alignment, то последнее может принимать любое значение, представляющее собой степень двойки (например, 20h). Условимся называть такие файлы «невыровненными». Хотя этот термин не вполне корректен, лучшего пока не придумали.

    К невыровненным файлам предъявляется следующее, достаточно жесткое требование – виртуальные и физические адреса всех секций обязаны совпадать, т.е. страничный имидж должен полностью соответствовать своему дисковому образу. Впрочем, никакое правило не обходится без исключений, и виртуальный размер секций может быть меньше их физического размера, но не более чем Section Alignment – 1 байт (т.е. секция все равно будет выровнена в памяти). Самое интересное, что это данное правило рекурсивно, и даже среди исключений встречаются исключения – если физический размер последней секции вылетает за пределы загружаемого файла, операционная система выбрасывает голубой экран смерти и… погибает (во всяком случае, w2k sp3 ведет себя именно так, остальные не проверял). Полномочия администратора для этого не требуются и даже самая ничтожная личность может устроить грандиозный DoS.

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

    Для создания невыровненных файлов можно воспользоваться линкером от Microsoft, задав ему ключ /ALIGN:32 совместно с ключом /DRIVER. Без ключа /DRIVER ключ /ALIGN будет проигнорирован и линкер использует кратность выравнивания по умолчанию.

    Листинг 3. Макросы для выравнивания с округлением «вниз» и «вверх»

    #define Is2power(x) (!(x & (x-1)))

    #define ALIGN_DOWN(x, align) (x &

    #define ALIGN_UP(x, align) ((x & (align-1))?ALIGN_DOWN(x,align)+align:x)

    Размер страничного имиджа, выровненный на величину Section Alignment. Размер страничного имиджа всегда равен виртуальному адресу последней секции плюс ее размер (выровненный, виртуальный). Если размер страничного образа вычислен неправильно, файл не загружается.

    Листинг 4. Макрос для вычисления реального размера страничного имиджа

    #define xImageSize(p) (*(DWORD*)(pLastSection(p) + 0xC /* va */) + ALIGN_UP(*(DWORD*)(pLastSection(p) + 0x8 /* v_sz */),

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

    Обычно SizeOfHeaders устанавливается на конец Section Table, однако это не самое лучшее решение. Судите сами. Совокупный размер всех заголовков при стандартной MS-DOS заглушке составляет порядка

    300h байт или даже менее того, в то время как физический адрес первой секции – от 400h байт и выше. Отодвинуть секцию назад нельзя – выравнивание не позволяет (см. «FileAlignment/SectionAlignment»). Правда, если вынуть MS-DOS заглушку, можно ужать SizeOfHeaders до 200h байт, в аккурат перед началом первой секции, но это уже изврат. Короче говоря, если следовать рекомендациям от Microsoft,

    100h байт мы неизбежно теряем, что не есть хорошо. Вот некоторые линкеры и размещают здесь таблицу имен, содержащую перечень загружаемых DLL или что-то типа того. Поэтому, чтобы ненароком не нарваться на коварный конфликт, лучше всего подтянуть SizeOfHeaders к min(pFirstSection->RawOffset, pFirstSection->va).

    Некоторые нехорошие программы (вирусы, упаковщики, дамперы) устанавливают SizeOfHeader на raw offset первой секции, что неправильно. Между концом всех заголовков и физическим началом первой секции может быть расположено любое, кратное File Alignment, количество байт, например, 1 гигабайт, и это при том, что виртуальный адрес первой секции – 1000h. Как такое может быть? А очень просто – SizeOfHeaders [image_optional_header] CheckSum

    Контрольная сумма файла. Проверяется только NT, да и то лишь при загрузке некоторых системных библиотек и, разумеется, самого ядра. Алгоритм расчета можно найти в IMAGEHEL.DLL функция CheckSumMappedFile. По слухам, ее исходные тексты входят в SDK. У меня есть SDK, но ничего подобного я там не видел (может, плохо искал?). Впрочем, алгоритм расчета тривиален и декомпилируется на ура.

    Требуемая подсистема, которую операционная система должна предоставить файлу. Может принимать следующие значения:

    00h IMAGE_SUBSYSTEM_UNKNOWN:

    Неизвестная подсистема, файл не загружается.

    n01h IMAGE_SUBSYSTEM_NATIVE:

    Подсистема не требуется, файл исполняется в «родном» окружении ядра и скорее всего представляет собой драйвер устройства. Обычным путем не загружается, если вы пишете вирус/упаковщик/протектор, ни в коем случае не обрабатывайте таких файлов, если только точно не уверены, в том, что вы делаете. Внимание: при загрузке драйверов Windows игнорирует поле подсистемы и оно может быть любым, поэтому, если Subsystem != IMAGE_SUBSYSTEM_ NATIVE это еще не значит, что данный файл не является драйвером.

    02h IMAGE_SUBSYSTEM_WINDOWS_GUI:

    Графическая win32 подсистема. Операционная система загружает файл нормальным образом, ну а дальше пусть все, что ему нужно, добываем сам.

    03h IMAGE_SUBSYSTEM_WINDOWS_CUI:

    Терминальная (она же консольная) win32 подсистема. То же самое, что и IMAGE_SUBSYSTEM_WINDOWS_GUI, но в этом случае файлу на халяву достается автоматически создаваемая консоль с готовыми дескрипторами ввода/вывода. Вообще говоря, разница между консольными и графическими приложениями очень условна – консольные приложения могут вызывать GUI32/USER32-функции, а графические приложения – открывать одну или несколько консолей (например, в отладочных целях). Кстати говоря, с этим связана одна забавная проблема, с которой сталкиваются многие «программисты», пытающиеся подавить создание ненужного им окна (ну мало ли, может они шпиона какого пишут, а это окно его демаскирует). Предотвратить автоматическое создание окна очень просто – достаточно… не создавать его!

    05h IMAGE_SUBSYSTEM_OS2_CUI:

    Подсистема OS/2. Только для приложений OS/2 (одним из которых, кстати говоря, является всем известный HIEW) и только для Windows NT. Windows 9x не может обрабатывать такие файлы.

    07h IMAGE_SUBSYSTEM_POSIX_CUI:

    Подсистема POSIX. Только для приложений UNIX и только для Windows NT.

    09h IMAGE_SUBSYSTEM_WINDOWS_CE_GUI:

    Файл предназначен для исполнения в среде Windows CE. Ни Windows NT, ни Windows 9x не могут обрабатывать такие файлы.

    0Ah MAGE_SUBSYSTEM_EFI_APPLICATION:, 0Bh IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER:,

    0Ch IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER:

    Подсистема EFI (Extensible Firmware Initiative).

    Очень странное поле. Мэтт Питрек пишет, что оно определяет набор флагов, указывающих, при каких условиях точка входа в DLL получает управление (как то, загрузка dll в адресное пространство процесса, создание/завершение нового потока и выгрузка dll из памяти). В спецификации на PE-формат эти поля помечены как зарезервированные и Windows игнорирует их значение, поэтому у большинства файлов оно равно нулю.

    Согласно спецификации 6.0 от 1999 года (самой свежей спецификации на сегодняшний день), загрузчик должен поддерживать и другие флаги: 800h – не биндить образ, 2000h – загружать драйвер как WDM драйвер; 8000h – файл поддерживает работу под терминальным сервером. Экспериментальная проверка показала, что W2K игнорирует эти флаги.

    [image_optional_header] SizeOfStackReserve/SizeOfStackCommit, SizeOfHeapReserve/SizeOfHeapCommit

    Объем зарезервированной/выделенной памяти под стек/кучу в байтах. Если SizeOfCommit > SizeOfReverse файл не загружается. Ноль обозначает значение по умолчанию.

    Количество элементов (не байт) в DATA_DIRECTORY, следующей непосредственно за этим полем. Из-за грубых ошибок в системном загрузчике компоновщики от Borland и Microsoft всегда выставляют полный размер директории, равный 10h, даже если реально его не используют. Например, Windows 9x не проверяет, что NumberOfRva AndSizes >= RELOCATION и/или RESOURCE и если подсунуть ему запрос к одной из этих секций, а таких директорий нет – это конец. Windows NT не проверяет (при загрузке dll) «достаточности» TLS_DIRECTORY и если этот TLS-механизм активирован, а TLS-директории нет – опять кранты.

    Компоновщик Юрия Харона выгодно отличается тем, что усекает размер директории до минимума, но и кода вокруг процедуры «сокращений» там строк пятьсот, а уж сколько времени было убито в ИДЕ…

    Есть и другая проблема. По спецификации DATA_DIREC-TORY располагается в самом конце опционального заголовка и непосредственно за его концом начинается таблица секций. Таким образом, указатель на таблицу секций может быть получен либо так:

    ((BYTE*) ((*((WORD*)(p + 0x14 /* size of optional header */)))+ 0x18 /* size of image header */ + p))

    ((BYTE*) ( (*((DWORD*)(p+0x74 /* NumRVAandSize */)))*8 + 0x78 /* begin DATA_DIRECTOTY */+ p))

    Системный загрузчик использует первый способ и допускает, что между DATA_DIRECTORY и SECTION_TABLE может быть расположено некоторое количество «бесхозных» байт. Некоторые дизассемблеры и упаковщики считают иначе и ищут SECTION_TABLE непосредственно за концом DATA_DIRECTORY. Вот и давайте подсунем им подложную SECTION_TABLE! Пускай их авторы почаще заглядывают в WINNT.H, который недвусмысленно говорит, что:

    #define IMAGE_FIRST_SECTION( ntheader ) ((PIMAGE_SECTION_HEADER)((ULONG_PTR)ntheader + FIELD_OFFSET( IMAGE_NT_HEADERS, OptionalHeader ) + ((PIMAGE_NT_HEADERS)(ntheader))->FileHeader.SizeOfOptionalHeader))

    …так что лезть дизассемблером в системный загрузчик совсем необязательно!

    00h IMAGE_DIRECTORY_ENTRY_EXPORT:

    Указатель на таблицу экспортируемых функций и данных (далее по тексту просто функций). Встречается преимущественно в динамических библиотеках и драйверах, однако заниматься экспортом товаров может и рядовой исполняемый файл. Использует RVA- и VA-адресацию (подробнее см. «Экспорт»).

    01h IMAGE_DIRECTORY_ENTRY_IMPORT:

    Указатель на таблицу импортируемых функций, используемую для связи файла с внешним миром, и активируемую системным загрузчиком, когда все остальные механизмы импорта недоступны. Использует RVA- и VA-адреса (подробнее см. «Импорт»).

    02h IMAGE_DIRECTORY_ENTRY_RESOURCE:

    Указатель на таблицу ресурсов, хранящую строки, пиктограммы, курсоры, диалоги и прочие кирпичики пользовательского интерфейса (хотя какие это кирпичики? настоящие бетонные блоки!). Таблица ресурсов организована в виде трехуровневого двоичного дерева, слишком запутанного и разлапистого, чтобы его было можно привести здесь, но, к счастью, использующего только RVA-адресацию, т.е. нечувствительного к смещению «своей» секции (а это как правило секция .rsrc) внутри файла. Однако, если вы вздумаете править RVA (например, для внедрения новой секции в середину страничного имиджа или переносу image base), вам придется основательно потрудиться с этой структурой, подробное описание которой, кстати говоря, можно найти в уже упомянутой статье «The Portable Executable File Format from Top to Bottom».

    03h IMAGE_DIRECTORY_ENTRY_EXCEPTION:

    Указывает на exception directory (директорию исключений), обычно размещаемую в секции .pdata (хотя это и необязательно). Используется только на следующих архитектурах: MIPS, Alpha32/64, ARM, PowerPC, SH3, SH, WindowsCE. К микропроцессорам семейства Intel это не относится и IX386-загрузчик игнорирует это поле, поэтому оно может принимать любое значение.

    04h IMAGE_DIRECTORY_ENTRY_SECURITY:

    Указывает на Certificate Table (таблицу сертификатов), располагающуюся строго в .debug-секции и адресуемой не по RVA-адресам, а по физическим смещениям внутри файла (так происходит потому, что таблица сертификатов не грузится в память и обитает исключительно на диске). Если IMAGE_DIRECTORY_ENTRY_SECURITY != 0, ни в коем случае не пытайтесь внедрять в файл посторонний код, иначе он откажет в работе.

    05h IMAGE_DIRECTORY_ENTRY_BASERELOC:

    Он же fixup, использует RVA-адреса (см. «Перемещаемые элементы»).

    06h IMAGE_DIRECTORY_ENTRY_DEBUG:

    Отладочная информация, используемая дизассемблерами и дебеггерами. Использует RVA- и RAW OFFSET-адресацию. Системный загрузчик ее игнорирует.

    07h IMAGE_DIRECTORY_ENTRY_ARCHITECTURE:

    Он же «description». На I386-платформе, судя по всему, предназначен для хранения информации о копирайтах (на это, в частности, указывает определение IMAGE_DIRECTORY_ ENTRY_COPYRIGHT, данное в WINNT.H), за формирование которых отвечает ключ –D, переданный Багдадскому линкеру ilinlk32.exe, при этом в IMAGE_DIRECTORY_ENTRY_ ARCHITECTURE помещается RVA-указатель на строку комментариев, по умолчанию располагающуюся в секции .text. Компоновщик ms link при некоторых до конца не выясненных обстоятельствах помещает в это поле информацию об архитектуре, однако системный загрузчик ее никогда не использует.

    08h IMAGE_DIRECTORY_ENTRY_GLOBALPTR:

    Указатель на таблицу регистров глобальных указателей. Используется только на процессорах ALPHA и PowerPC. На I386-платформе это поле лишено смысла, и загрузчик его игнорирует.

    09h IMAGE_DIRECTORY_ENTRY_TLS:

    Хранилище статической локальной памяти потока (Thread Local Storage). TLS-механизм обеспечивает «прозрачную» работу с глобальными переменными в многопоточных средах без риска, что переменная в самый неподходящий момент будет модифицирована другим потоком. Сюда попадают переменные, объявленные как __declspec(thread). По причине большой причудливости и крайней тяжеловесности реализации (один шаг в сторону и операционная система стреляет без предупреждения) используется крайне редко. К тому же Windows NT и Windows 9x обрабатывают это поле сильно неодинаково. Хранилище обычно размещается в секции .tls, хотя это и необязательно. Использует RVA- и VA-адреса.

    10h IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG:

    Содержит информацию о конфигурации глобальных флагов, необходимых для нормальной работы программы, имеет смысл только в Windows NT и производных от нее системах. Это поле практически никем не используется, но если возникнет желание узнать о нем больше – см. прототип структуры IMAGE_LOAD_CONFIG_DIRECTORY32 в WINNT.h, а также ее описание в Platform SDK. За описанием самих флагов обращайтесь к утилите gflags.exe, входящей в состав Resource Kit и NTDDK. Информация о конфигурации использует VA-адресацию (точнее, пока еще не использует, но резервирует эту возможность на будущее).

    11h IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT:

    Указатель на таблицу диапазонного импорта, имеющую приоритет над IMAGE_DIRECTORY_ENTRY_IMPORT и обрабатываемую загрузчиком в первую очередь (зачастую, до IMAGE_DIRECTORY_ENTRY_IMPORT дело вообще не доходит). По устоявшейся традиции таблица диапазонного импорта размещается в PE-заголовке, хотя это и необязательно, и некоторые линкеры ведут себя иначе. Используется RVA- и RRAW OFFSET-адресация (подробнее см. «Импорт»).

    12h IMAGE_DIRECTORY_ENTRY_IAT:

    Указатель на IAT (подчиненная структура таблицы импорта). Используется загрузчиком Windows XP, остальные операционные системы это поле, по-видимому, игнорируют (подробнее см. «Импорт»).

    13h IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT:

    Указатель на таблицу отложенного импорта, использующую RVA/VA-адресацию, но фактически остающуюся не стандартизированной и отданной на откуп воле конкретных реализаторов (подробнее см. «Импорт»).

    14h IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR:

    Если не равно нулю, то файл представляет собой .NET-приложение, состоящее из байт-кода, поэтому попытка внедрения в него x86 никогда ничего хорошего не принесет.

    Четкого определения термина «секция» не существует. Упрощенно говоря, секция – это непрерывная область памяти внутри страничного имиджа со своими атрибутами, независящими от атрибутов остальных секций. Представление секции в памяти не обязательно должно совпадать с ее дисковым образом, который в принципе может вообще отсутствовать (секциям неинициализированных данных нечего делать на диске, и потому они представлены исключительно в памяти).

    Каждая секция управляется «своей» записью в одноименной структуре данных, носящей имя «таблицы секций». Таблица секций начинается сразу же за концом опционального заголовка, размер которого содержится в поле SizeOfOptionalHeader, и представляет собой массив структур IMAGE_SECTION_HEADER, количество задается полем NumberOfSection.

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

    Структура IMAGE_SECTION_HEADER состоит из следующих полей:

    Листинг 5. Прототип структуры IMAGE_SECTION_HEADER

    Структура исполняемого файла Windows

    Исследуем формат EXE-файла

    Целью работы компилятора является получение EXE-файла. Поэтому, исследуем его структуру.

    EXE-файлы появились ещё в DOS и потом они с небольшими изменениями перекочевали в Windows. Формат EXE-файла под Windows называется PE-файлом. Он организован в виде линейного потока данных.

    Формат PE-файла

    Заголовок MS-DOS
    Программа-заглушка
    Заголовок PE
    Доп. заголовок PE
    Массив DataDir
    Заголовки сегментов
    Тела сегментов
    Остальные области данных

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

    Залоговок MS-DOS (размер 40H байт)

    Адрес Тип Имя Описание
    00h word Magic Магическая сигнатура DOS-файла — два символа «MZ», явно от MZ -club 🙂
    02h word LastByteCount Количество байт на последней странице файла
    04h word PageCount Количество страниц в файле
    06h word RelocCount Количество релокейшенов
    08h word HeaderSize Размер заголовка в параграфах
    0Ah word MinAlloc Мин. выделение памяти в параграфах
    0Ch word MaxAlloc Макс. выделение памяти в параграфах
    0Eh word InitSS Начальное (относительное) значение регистра SS
    10h word InitSP Начальное значение регистра SP
    12h word CheckSum Контрольная сумма
    14h word InitIP Начальное значение регистра IP
    16h word InitCS Начальное (относительное) значение регистра CS
    18h word RelocAddr Адрес на релокейшены и программу-заглушку
    1Ah word OverlayCount Количество оверлеев
    1Ch word Res1[4] Зарезервировано
    24h word OEMIdentifier Для OEMInfo
    26h word OEMInfo Информация о программе
    28h word Res1[10] Зарезервировано
    3Ch dword PEHeaderAddr Адрес в файле заголовка PE

    Для Windows-программы заголовок MS-DOS не содержит релокейшины (пока даже и не знаю что это такое), то есть Relocations = 0, поэтому RelocAddr указывает сразу на программу-заглушку. Но нам важен заголовок PE, его адрес находиться в PEHeaderAddr.

    Залоговок PE (размер 18H байт)

    Адрес Тип Имя Описание
    00h dword Magic Магическая сигнатура PE-файла 4550H или «PE», 0H, 0H
    04h word CPUType Тип процессора
    06h word SectionCount Количество сегментов
    08h dword DateTime Дата/время создания/модификации линкером
    0Сh dword SymbolTableAddr Адрес местонахождения таблицы символов
    10h dword SymbolTableSize Размер таблицы символов
    14h word OptionalHeaderSize Размер доп. заголовка PE
    16h word Flags Предназначение программы

    Сразу за основным заголовком идёт дополнительный заголовок PE.

    Доп. залоговок PE (размер 18H — 77H байт)

    Адрес Тип Имя Описание
    18h word Magic Всегда 10Bh
    1Ah byte MajorLinkVer Версия линкера, создавшего данный файл
    1Bh byte MinorLinkVer
    1Ch dword CodeSize Размер исполнительного кода
    20h dword InitDataSize Размер инициализированных данных
    24h dword UnInitDataSize Размер неинициализированных данных
    28h dword EntryPointAddr Адрес, относительно ImageBase, по которому передаётся управление при запуске программы или адрес инициализации/завершения библиотеки
    2Ch dword CodeBase Относительное смещение сегмента кода
    30 dword DataBase Относительное смещение сегмента неинициализированных данных
    34h dword ImageBase Предподчтительный адрес для загрузки исполнимого файла (по умолчанию 400000H)
    38h dword SectionAlign Выравнивание программных секций (по умолчанию 1000H)
    3Ch dword FileAlign Минимальная гранулярность сегментов, то есть размер сегментов должен быть кратен FileAlign, должен быть равен значению степени 2 между 200H и 10000H (по умолчанию 200H)
    40h word MajorOSVer Старший номер версии OS, необходимый для запуска программы
    42h word MinorOSVer Младший номер версии OS
    44h word MajorImageVer Пользовательский старший номер версии, задается пользователем при линковке программы и им же и используется
    46h word MinorImageVer Пользовательский младший номер версии, задается пользователем при линковке программы и им же и используется
    48h word MajorSubSysVer Старший номер версии Win32
    4Ah word MinorSubSysVer Младший номер версии Win32
    4Ch dword Res1
    50h dword ImageSize Виртуальный размер в байтах всего загружаемого образа, вместе с заголовками, кратен ObjectAlign
    54h dword HeaderSize Общий размер всех заголовков: MS-DOS, PE, доп PE и всех сегментов
    58h dword CheckSum Контрольная сумма (не используется и равна 0)
    5Ch word SubSystem Подсистема, необходимая для запуска данного файла //(0 — неизвестная подсистема, 1 — не требует подсистему, 2 — Windows GUI, 3 — Windows консоль. )
    5Eh word DllFlags Специальные флаги при загрузке, начиная с NT 3.5 не используются
    60h dword StackReserveSize Память, требуемая для стека приложения, память резервируется, но выделяется только StackCommitSize байтов, следующая страница является охранной. Когда приложение достигает этой страницы, то страница становится доступной, а следующая страница — охранной, и так до достижения нижней границы, после чего Windows убивает программу с сообщением о конце стека
    64h dword StackCommitSize Объем памяти, отводимый в стеке немедленно после загрузки
    68h dword HeapReserveSize Максимальный возможный размер локального хипа
    6Ch dword HeapComitSize Отводимый при загрузке хип
    70h dword LoaderFlags Данный параметр устарел
    74h dword DataDirSize Указывает размер массива DataDir, расположенный ниже (по умолчанию 10h)

    Далее идёт массив DataDir, 8-байтные элементы которого состоят из двух 4-х байтных: адрес и размер.

    Массив DataDir (размер 78H — F8H байт)

    Адрес Тип Имя Описание
    78h qword ExportDir Каталог экспортируемых объектов
    80h qword ImportDir Каталог импортируемых объектов
    88h qword ResourceDir Каталог ресурсов
    90h qword ExceptionDir Каталог исключений
    98h qword SecurityDir Каталог безопастности
    A0h qword BaseRelocDir Каталог переадресаций
    A8h qword DebugDir Отладочный каталог
    B0h qword CopyrightDir Каталог описаний
    B8h qword CpuSpecDir Каталог значений, специфичных для процессора
    C0h qword TLSDir Каталог TLS (Thread local storage — локальная память потоков)
    C8h qword ConfigDir Каталог конфигураций загрузки
    D0h qword ResDir11
    D8h qword ResDir12
    E0h qword ResDir13
    E8h qword ResDir14
    F0h qword ResDir15

    Элемент массива DataDir (размер 8 байт)

    Адрес Тип Имя Описание
    00h dword Addr Адрес каталога
    04h dword Size Размер каталога

    Далее идёт подряд несколько сегментов, количество указано в SectionCount.

    Заголовок сегмента (размер 2Ch байт)

    Адрес Тип Имя Описание
    00h char[8] SectName Имя секции, если имя Практически любая программа под Windows работает с такими её DLL-ками: kernel32.dll, user32.dll, gdi32.dll и т.д.. Поэтому, EXE-шник должен уметь импортировать функции данных библиотек, то есть работать с каталогом импорта ImportDir. Каталог импорта сразу же начинается с таблицы импорта ImportDirTable, которая описывает остальную информацию об импорте. Такая таблица состоит из элементов ImportDirTableItem, указывающих, как минимум, на каждую импортируемую библиотеку. Последний элемент, указывающий на конец таблицы, заполнен нулями.

    Элемент таблицы каталога импортируемых объектов ImportDirTableItem (размер 14h байт)

    Адрес Тип Имя Описание
    00h dword FuncNameList Список имён импортируемых функций
    04h dword Res1
    08h dword Res2
    0Ch dword LibName Имя библиотеки
    10h dword FuncAddrList Список адресов импортируемых функций

    Параметр LibName указывает на имя библиотеки, которое должно заканчиваться нулём. FuncNameList указывает на список адресов (0-ой адрес — конец списка), по которым находится сначала Hint — (укороченный идентификатор точки входа), а затем имя функции, заканчивающееся нулём. Параметр FuncAddrList указывает на точно такой же список адресов, находящийся (по моим наблюдениям) перед ImportDirTable.

    Формат EXE-файла здесь описан не полностью. Остальное будет описано позже. Однако, этого уже достаточно для создания компилятора.

    Для глубокого изучения EXE-шника, написана специальная программа «EXE-исследователь». Последнюю версию данной программы можно скачать на страничке Download

    Мастер Йода рекомендует:  Что есть алгебраические типы
    Добавить комментарий