Структура исполняемого файла 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 байт)
Адрес | Тип | Имя | Описание | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
00h | char[8] | SectName | Имя секции, если имя Практически любая программа под Windows работает с такими её DLL-ками: kernel32.dll, user32.dll, gdi32.dll и т.д.. Поэтому, EXE-шник должен уметь импортировать функции данных библиотек, то есть работать с каталогом импорта ImportDir. Каталог импорта сразу же начинается с таблицы импорта ImportDirTable, которая описывает остальную информацию об импорте. Такая таблица состоит из элементов ImportDirTableItem, указывающих, как минимум, на каждую импортируемую библиотеку. Последний элемент, указывающий на конец таблицы, заполнен нулями.
Элемент таблицы каталога импортируемых объектов ImportDirTableItem (размер 14h байт)
Параметр LibName указывает на имя библиотеки, которое должно заканчиваться нулём. FuncNameList указывает на список адресов (0-ой адрес — конец списка), по которым находится сначала Hint — (укороченный идентификатор точки входа), а затем имя функции, заканчивающееся нулём. Параметр FuncAddrList указывает на точно такой же список адресов, находящийся (по моим наблюдениям) перед ImportDirTable. Формат EXE-файла здесь описан не полностью. Остальное будет описано позже. Однако, этого уже достаточно для создания компилятора. Для глубокого изучения EXE-шника, написана специальная программа «EXE-исследователь». Последнюю версию данной программы можно скачать на страничке Download Национальная библиотека им. Н. Э. Баумана
|
Расширение файла | .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, существуют только два типа поправок:
Есть также другие поправки, определенные в 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. Помимо стандартных секций РЕ-файла, определены три других специальных значения.
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 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, и для них лучше было бы подобрать другие имена.
Дополнительные поля Windows NT
Дополнительные поля, добавленные в формате PE файлов Windows NT, предоставляют поддержку загрузчику для специфичного поведения процессов Windows NT. Ниже следует краткое описание этих полей:
Выравнивание сегмента не может быть меньше размера страницы ( в настоящий момент 4096 байт на платформе x86 ), и должно быть кратно размеру страницы, как предписывает поведение менеджера виртуальной памяти Windows NT. 4096 байт являются значением по умолчанию, но может быть установлено также другое значение, используя опцию линковщика -ALIGN:
Представим, что система имеет фиксированный размер страницы 4096 байт. Если мы имеем исполнимый файл из 11 сегментов, каждый из которых меньше 4096 байт, выравненных по границе 65,536 байт, поле SizeOfImage должно быть установлено 11 * 65,536 = 720,896 ( 176 страниц ). Тот же самый файл, построенный с выравниванием 4096 байт, в результате будет иметь размер в памяти 11 * 4096 = 45,056 ( 11 страниц ) для поля SizeOfImage. Это простой пример, в котором каждый сегмент требует менее одной страницы памяти. В действительности линковщик определяет точное значение SizeOfImage, подсчитывая требуемое место для каждого сегмента. Сначала он определяет размер каждого сегмента, затем округляет это число, чтобы оно стало кратно размеру страницы, и наконец, он вычисляет количество страниц, чтобы их размер стал кратен SectionAlignment. Эти размеры далее суммируются по каждому сегменту.
Каталоги данных
Каталоги данных, как определено в файле заголовков WINNT.H, есть:
Каждый каталог данных является структурой, определенной как IMAGE_DATA_DIRECTORY. Несмотря на то, что все каталоги данных одинаковы, каждый конкретный тип каталога данных уникален. Определение каждого из них описано ниже в этой статье в главе «Предопределенные сегменты».
Каждый элемент в массиве каталогов данных содержит свой размер и относительный виртуальный адрес каталога. Чтобы найти местоположение некоторого каталога, Вы должны определить относительный адрес из массива каталогов данных в опциональном заголовке. Далее используйте виртуальный адрес для определения, в каком сегменте находится нужный каталог. После определения сегмента, содержащего нужный каталог данных, используйте заголовок этого сегмента для определения смещения в файле, указывающего местоположение нужного каталога данных.
Таким образом, для доступа к каталогам данным мы должны сначала изучить сегменты, описанные ниже. Пример нахождения местоположения каталогов данных находится непосредственно после следующей главы.
Сегменты PE файла
Спецификация PE файла состоит из заголовков, описанных ранее, и общих объектов, называемых сегментами. Сегменты содержат собственно содержимое файла, включая код, данные, ресурсы и прочую информацию о PE файле. Каждый сегмент имеет заголовок и тело сегмента ( непосредственно данные ). Заголовки сегментов описаны ниже, однако тела сегментов не имеют жестко заданной структуры. Они организованы почти всегда так, как того захотел конкретный линковщик, поскольку заголовки содержат достаточно информации для дальнейшей обработки данных.
Заголовки сегментов
Заголовки сегментов размещаются последовательно сразу же за опциональным заголовком PE файла. Каждый заголовок секции имеет длину 40 байт и расположены они без выравнивания. Заголовок сегмента определяется следующей структурой:
Как Вы можете получить информацию из заголовка некоторого сегмента ? Так как заголовки сегментов расположены последовательно без какого либо предопределенного порядка, заголовки сегментов должны идентифицироваться по имени. Следующий пример показывает, как найти заголовок сегмента PE файла по имени, переданному в качестве аргумента:
Функция просто находит первый заголовок сегмента с помощью макроса SECHDROFFSET. Затем функция организует цикл через все заголовки сегментов, сравнивая имя очередного сегмента с именем сегмента, который нужно найти. Если сегмент найден, функция копирует данные из файла, отображенного в память, в структуру, переданную как аргумент в эту функцию. После этого поля структуры IMAGE_SECTION_HEADER могут быть доступны непосредственно из этой структуры.
Поля заголовка сегмента
Местоположение каталогов данных
Каталоги данных расположены в телах соответствующих сегментов. Обычно каталог данных является первой структурой в теле сегмента, однако это не является обязательным. По этой причине, Вы должны использовать информацию как из заголовка сегмента, так и из опционального заголовка для определения местоположения определенного каталога данных.
Для облегчения этой процедуры, была написана следующая функция для определения местоположения любого каталога, определенного в файле 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_DIRECTORY — dwRVAModuleName — относительный виртуальный адрес, указывающий на имя модуля. Также в этой структуре есть два бесполезных поля 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 . Приведите фрагмент программы, читающей из РЕ файла список импортируемых программой функций ОС.
Добавил: | DMT | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Дата создания: | 30 декабря 2007, 18:56 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Дата обновления: | 30 декабря 2007, 18:56 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Просмотров: | 10463 последний сегодня, 21:49 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Комментариев: | 2 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Вопрос 4. Формат исполняемого РЕ файла ОС Windows . Приведите фрагмент программы, читающей из РЕ файла список импортируемых программой функций ОС. Комментарии для «Вопрос 4. Формат исполняемого РЕ файла ОС Windows . Приведите фрагмент программы, читающей из РЕ файла список импортируемых программой функций ОС. «
|