5.6. Таблица настроек адресов

Все PE-файлы могут загружаться с любого виртуального адреса памяти, т. е. являются файлами с относительной загрузкой. Такие файлы обязаны содержать таблицу настройки адресов для каждой машинной команды в секции кода, которая содержит абсолютные адреса. Пусть, например, программа построена с базовым адресом ImageBase, равным 0x00400000, и по RVA 0x00100000 содержит команду IA-32:

00500000  A3 00800000	mov [00800000h], eax

Предположим, что фактическим адресом загрузки этой программы будет 0x01400000. Тогда адрес 0x00800000, расположенный по RVA 0x00100001, должен быть изменен на величину (0x01400000 - 0x00400000) = 0x01000000. Такое изменение абсолютных адресов и называется настройкой адресов программы.

Список всех настраиваемых RVA и способ их настройки как раз и содержится в таблице настройки, которая представляет собой набор блоков (chunks). Каждый блок содержит настройки для 4 Кб данных и начинается с заголовка IMAGE_BASE_RELOCATION, имеющей следующий вид:

Смещение (hex) Размер Тип Название Описание
00 4 DWORD VirtualAddress Начальный RVA.
04 4 DWORD SizeOfBlock Размер блока в байтах.

После этого заголовка следует массив 16-битовых описателей настройки. Количество таких описателей равно (SizeOfBlock - IMAGE_SIZEOF_BASE_RELOCATION) / 2. Таблица настройки заканчивается блоком, у которого заголовок IMAGE_BASE_RELOCATION заполнен нулями.

Каждый описатель настройки в четырех старших битах содержит тип настройки, а в 12 младших битах – смещение от начального RVA до настраиваемых данных. Обозначим через D величину настройки, т. е. разность (фактический_адрес_загрузки - ImageBase), а через A – адрес настраиваемых данных. Поддерживаются следующие типы настроек:

Значение Название Описание
0 IMAGE_REL_BASED_ABSOLUTE Нет настройки. Используется как заполнитель для выравнивания блоков на границу двойного слова. Младшие биты должны быть нулями.
1 IMAGE_REL_BASED_HIGH Добавить старшие 16 бит D к 16-битовому слову (WORD) по адресу A.
2 IMAGE_REL_BASED_LOW Добавить младшие 16 бит D к 16-битовому слову (WORD) по адресу A.
3 IMAGE_REL_BASED_HIGHLOW Добавить D к 32-битовому двойному слову (DWORD) по адресу A.
4 IMAGE_REL_BASED_HIGHADJ Этот смутно документированный описатель я понимаю так. Он занимает два 16-битовых поля. Старшие 16 бит извлекаются по адресу A, а младшие 16 бит из следующего поля описателя (как WORD). К полученному 32-битовому слову добавляется D и 0x00008000. Старшие 16 бит результата сохраняются в 16-битовом слове по адресу A. Зачем все это нужно, я не знаю.
5 IMAGE_REL_BASED_MIPS_JMPADDR Настройка инструкции перехода для MIPS ?.
6 IMAGE_REL_BASED_SECTION Зарезервировано.
7 IMAGE_REL_BASED_REL32 Зарезервировано.
9 IMAGE_REL_BASED_MIPS_JMPADDR16 Настройка инструкции перехода для MIPS16 ?.
9 IMAGE_REL_BASED_IA64_IMM64 Неизвестная мне настройка для типа IMM64 на IA-64 ?.
10 IMAGE_REL_BASED_DIR64 Добавить D к 64-битовому четверному слову (QWORD) по адресу A.
11 IMAGE_REL_BASED_HIGH3ADJ Этот описатель предположительно работает так. Он занимает три 16-битовых поля. Старшие 16 бит извлекаются по адресу A, а младшие 32 бита из двух следующих полей описателя (как DWORD). К полученному 48-битовому слову добавляется D, сдвинутое влево на 16 бит, и 0x000080000000. Старшие 16 бит результата сохраняются в 16-битовом слове по адресу A.

Очевидно, что допустимые типы настроек зависят от архитектуры процессора. На процессорах IA-32 используются только настройки IMAGE_REL_BASED_HIGHLOW (настройка 32-битового линейного адреса) и IMAGE_REL_BASED_ABSOLUTE (заглушка в конце блока). На процессорах IA-64 используются только настройки IMAGE_REL_BASED_DIR64 (настройка 64-битового линейного адреса), IMAGE_REL_BASED_IA64_IMM64 (назначение мне неизвестно) и IMAGE_REL_BASED_ABSOLUTE (заглушка в конце блока). Интересно, что размер страницы памяти на IA-64 равен 8 Кб, но блоки настройки остались размером по 4 Кб (поскольку смещение занимает 12 бит). С Windows на неинтеловских процессорах мне поэкспериментировать не довелось, поэтому остальные настройки комментировать не берусь.

5.7. Таблица локальной памяти потоков

В этой таблице содержится описание статических переменных, относящихся к локальной памяти потоков (TLS, Thread Local Storage). TLS – это специфический для Windows способ хранения данных, при котором данные выделяются не в стеке, а являются локальными для потоков программы. В итоге каждый поток имеет собственный экземпляр данных, расположенных в TLS.

Windows API поддерживает динамическое выделение данных в TLS (см. описание функций TlsAlloc, TlsFree, TlsGetValue и TlsSetValue). В PE-файле хранится описание статически выделенных TLS-данных, которые создаются и инициализируются на этапе компиляции и сборки программы. Например, в Microsoft C++ для этого нужно написать декларацию переменной типа

__declspec(thread) int flag = 1;

Отметим, что статические TLS-данные могут использоваться только в статически загруженных PE-файлах. Если DLL загружена функцией LoadLibrary, то ее статические TLS-данные будут недоступны.

Таблица локальной памяти потоков представляет собой структуру IMAGE_TLS_DIRECTORY, имеющую разное строение в форматах PE32 и PE32+:

Смещение (hex, PE32/PE32+) Размер (PE32/PE32+) Тип (PE32/PE32+) Название Описание
00 4/8 DWORD/ULONGLONG StartAddressOfRawData Начальный адрес блока данных.
04/08 4/8 DWORD/ULONGLONG EndAddressOfRawData Конечный адрес блока данных.
08/10 4/8 DWORD/ULONGLONG AddressOfIndex Адрес индекса массива TLS.
0С/18 4/8 DWORD/ULONGLONG AddressOfCallBacks Адрес массива функций-ловушек.
10/20 4 DWORD SizeOfZeroFill Размер концевого заполнителя.
14/24 4 DWORD Characteristics Зарезервировано для атрибутов данных TLS.

Используется данная структура так. Во время создания очередного потока из кучи выделяется блок памяти для данных TLS, эта память инициализируется данными из таблицы локальной памяти потоков и указатель на нее заносится в соответствующее поле недокументированного блока описания потока (Thread Environment Block, TEB) по определенному смещению; в архитектуре IA-32 это поле доступно по адресу FS:[2Ch].

Если переменная потока частично или полностью инициализирована (как в приведенном выше примере), то поля StartAddressOfRawData и EndAddressOfRawData содержат виртуальные адреса начала и конца инициализированных данных, которые нужно поместить в переменную. Поле SizeOfZeroFill задает количество байтов, которые будут заполнены нулями после копирования инициализирующих данных.

Поле AddressOfIndex содержит виртуальный адрес индекса массива TLS (переменной __tls_index). В файле по этому адресу храниться нуль. В процессе загрузки программы загрузчик вызывает функцию TlsAlloc и заносит по этому адресу полученный индекс (DWORD). Для доступа к данным TLS компилятор генерирует такой код:

mov ecx, fs:[2Ch]      ; извлекаем указатель на массив TLS
mov eax, [__tls_index] ; извлекаем индекс массива TLS
mov eсx, [ecx+4*eax]   ; извлекаем указатель на данные TLS с соответствующим индексом
mov eax, [eсx+N]       ; извлекаем переменную по ее смещению N в данных

Поле AddressOfCallBacks указывает на массив функций-ловушек, связанных с данной переменной; последний указатель в этом списке является нулем. Ловушки вызываются операционной системой при запуске и завершении потоков; если ловушек несколько, то они вызываются в том порядке, как прописаны в массиве указателей. Каждая ловушка имеет следующий прототип PIMAGE_TLS_CALLBACK:

typedef VOID (NTAPI *PIMAGE_TLS_CALLBACK) (
  PVOID DllHandle,
  DWORD Reason,
  PVOID Reserved
);

Этот прототип и назначение параметров, что вполне логично, совпадает с прототипом функции DllMain. Насколько мне известно, на сегодня компиляторы Microsoft не поддерживают создания ловушек для данных TLS и AddressOfCallBacks всегда указывает на нулевое двойное слово.

Важно отметить, что все поля структуры IMAGE_TLS_DIRECTORY – это не RVA, а полные виртуальные адреса данных.

5.8. Таблица обработки исключений

Эта таблица присутствует во всех архитектурах, поддерживаемых Windows, кроме IA-32. Дело в том, что IA-32 использует для обработки исключений кадры стека; все остальные процессоры, включая IA-64, основаны на табличной обработке исключений.

Таблица обработки исключений представляет собой массив структур IMAGE_RUNTIME_FUNCTION_ENTRY, заканчивающийся структурой, содержащей нули. Формат этих структур зависит от процессора, я приведу описание только для IA-64.

Смещение (hex) Размер Тип Название Описание
00 4 DWORD BeginAddress Начальный адрес подпрограммы.
04 4 DWORD EndAddress Конечный адрес подпрограммы.
08 4 DWORD UnwindInfoAddress Адрес обработчика исключений.

Первые два поля таблицы содержат начальный и конечный адрес подпрограммы. При возникновении исключения системный обработчик ищет по его адресу подпрограмму, в которой произошло исключение, и извлекает третье поле, в котором содержится указатель на описатель обработки исключения. Интересующихся этим описателем отсылаю к спецификации "Intel® Itanum™ Software Conventions and Runtime Architecture Guide", п. 11.4.

5.9. Таблица сертификатов безопасности

Эта таблица не отображается в виртуальную память, поэтому поле VirtualAddress в описателе данного каталога данных содержит не RVA, а смещение от начала файла. Таблица сертификатов представляет собой массив структур WIN_CERTIFICATE, описанных в заголовочном файле WINTRUST.H. Каждая из этих структур устроена следующим образом:

Смещение (hex) Размер Тип Название Описание
00 4 DWORD dwLength Размер сигнатуры в байтах.
04 2 WORD wRevision Номер ревизии сертификата.
06 2 WORD wCertificateType Тип сертификата.
08 ? BYTE bCertificate Массив, содержащий сертификат.

Сертификаты безопасности позволяют добавлять в исполняемый файл цифровые подписи, подтверждающие аутентичность файла. Для их создания используются специальные утилиты, например SIGNTOOL.EXE, входящая в состав Platform SDK. Структура сертификатов недокументирована и зависит от используемого конкретной утилитой криптографического алгоритма. Для чтения сертификатов используются функции ImageGetCertificateHeader и ImageGetCertificateData библиотеки IMAGEHLP.DLL.

5.10. Данные, специфичные для процессора

На процессорах DEC/Compaq Alpha здесь хранится массив структур IMAGE_ARCHITECTURE_HEADER, содержащих специфическую для этих процессоров информацию. В IA-32 и IA-64 этот каталог данных используется для хранения ASCIIZ-строки, заданной в строке DESCRIPTION DEF-файла. Обычно эта строка содержит краткое описание программы и/или сведения об авторских правах.

5.11. Глобальный регистр процессора

Этот каталог данных не используется в IA-32, но используется в IA-64 и некоторых RISC-процессорах. Его поле VirtualAddress содержит RVA 64-битового значения, которое загружается в глобальный регистр (GP) процессора. Необходимость в наличии глобального регистра объясняется просто. В IA-64 все команды процессора состоят из 41 бита. Очевидно, что такая команда не может адресовать 64-разрядные адреса непосредственно, поэтому все команды адресуют данные относительно содержимого GP (программисты, работавшие на процессорах Intel x86, сразу вспомнят сегментную адресацию памяти; разница в том, то GP позволяет адресовать не 64 Кб, а 4 Мб памяти, поскольку непосредственные константы в командах IA-64 имеют длину 22 бита).

Интересно, что содержимое GP можно извлечь внутри своей программы проще. Практически каждая подпрограмма, скомпилированная для IA-64, в качестве адреса имеет указатель на структуру PLABEL_DESCRIPTOR:

struct PLABEL_DESCRIPTOR {
  ULONGLONG EntryPoint;
  ULONGLONG GlobalPointer;
};

Здесь EntryPoint – это действительный адрес входа в подпрограмму, а GlobalPointer – значение регистра GP для этой подпрограммы (такова же структура "адреса" и в некоторых RISC-процессорах, например в PowerPC). Для извлечения GP данной программы достаточно преобразовать в указатель на PLABEL_DESCRIPTOR адрес ее главной подпрограммы, например:

PLABEL_DESCRIPTOR* pLabelDesc = (PLABEL_DESCRIPTOR*)&main;
cout << hex << (ULONG_PTR)pLabelDesc->EntryPoint << endl;
cout << hex << (ULONG_PTR)pLabelDesc->GlobalPointer << endl;

5.12. Таблица конфигурации загрузки

Здесь содержится структура IMAGE_LOAD_CONFIG_DIRECTORY, специфическая для ветки Windows NT. Она позволяет изменить параметры загрузки процесса, принятые в системе по умолчанию. Для включения ее в свою программу на C++ нужно добавить в текст программы глобальную переменную

extern "C" IMAGE_LOAD_CONFIG_DIRECTORY _load_config_used = { ... };

Эта структура по-разному устроена в форматах PE32 и PE32+. Привожу ее сводное описание.

Смещение (hex, PE32/PE32+) Размер (PE32/PE32+) Тип (PE32/PE32+) Название Описание
00 4 DWORD Size Размер этой структуры в байтах.
04 4 DWORD TimeDateStamp Дата и время создания файла.
08 2 WORD MajorVersion Старшая цифра номера версии.
0A 2 WORD MinorVersion Младшая цифра номера версии
0C 4 DWORD GlobalFlagsClear Битовая маска глобальных флагов, которые нужно сбросить.
10 4 DWORD GlobalFlagsSet Битовая маска глобальных флагов, которые нужно установить.
14 4 DWORD CriticalSectionDefaultTimeout Значение таймаута по умолчанию для критических секций.
18 4/8 DWORD/ULONGLONG DeCommitFreeBlockThreshold Размер памяти, которую нужно освобождать перед ее возвратом в систему, в байтах.
1C/20 4/8 DWORD/ULONGLONG DeCommitTotalFreeThreshold Общий размер свободной памяти в байтах.
20/28 4/8 DWORD/ULONGLONG LockPrefixTable Виртуальный адрес, зарезервирован для использования ОС.
24/30 4/8 DWORD/ULONGLONG MaximumAllocationSize Максимальный размер единицы выделения памяти в байтах.
28/38 4/8 DWORD/ULONGLONG VirtualMemoryThreshold Максимальный размер виртуальной памяти в байтах.
2C/40 4/8 DWORD/ULONGLONG ProcessAffinityMask Битовая маска, показывающая на каких процессорах программа может выполняться.
30/48 4 DWORD ProcessHeapFlags Флаги кучи процесса.
34/4C 2 WORD CSDVersion Номер требуемого Service Pack.
36/4E 2 WORD Reserved1 Зарезервировано.
38/50 4/8 DWORD/ULONGLONG EditList Виртуальный адрес, зарезервирован для использования ОС.
3C/58 4/8 DWORD/ULONGLONG SecurityCookie Виртуальный адрес, зарезервирован для использования ОС.
40/60 4/8 DWORD/ULONGLONG SEHandlerTable Виртуальный адрес таблицы "безопасных обработчиков прерываний".
44/68 4/8 DWORD/ULONGLONG SEHandlerCount Количество "безопасных обработчиков прерываний".

Подробное описание использования этой структуры требует описания недокументированных особенностей загрузчика NT, поэтому я его приводить не буду. Отмечу только, что список глобальных флагов, используемых в полях GlobalFlagsClear и GlobalFlagsSet, можно найти в документации на утилиту GFLAGS.EXE, входящей в состав Resource Kit для Windows 2000 и XP.

5.13. Дескриптор .NET

Это новая структура, появившаяся в исполняемых файлах Microsoft .NET. В файлах .NET размер обычного кода и данных невелик, основное место в файле занимают метаданные и исполняемый код виртуальной машины. Входной процедурой служит небольшой фрагмент кода, который просто передает управление нужной функции в MSCOREE.DLL (CorExeMain или _CorDllMain). Именно эта функция и создает процесс .NET, извлекая из дескриптора .NET необходимую информацию.

Дескриптор .NET называется IMAGE_COR20_HEADER и имеет следующую структуру:

Смещение (hex) Размер Тип Название Описание
00 4 DWORD cb Размер этой структуру в байтах.
04 2 WORD MajorRuntimeVersion Старшая цифра номера версии исполняющей системы. Сейчас 2.
06 2 WORD MinorRuntimeVersion Младшая цифра номера версии исполняющей системы. Сейчас 0.
08 8 IMAGE_DATA_DIRECTORY MetaData RVA таблиц метаданных.
10 4 DWORD Flags Атрибуты данного файла.
14 4 DWORD EntryPointToken Токен метаданных для MethodDef входной точки .NET-процесса.
18 8 IMAGE_DATA_DIRECTORY Resources RVA и размер ресурсов .NET.
20 8 IMAGE_DATA_DIRECTORY StrongNameSignature RVA и размер хэш-ключа данной сборки.
28 8 IMAGE_DATA_DIRECTORY CodeManagerTable RVA таблицы диспетчера кода.
30 8 IMAGE_DATA_DIRECTORY VTableFixups RVA массива указателей на функции, которые требуют настройки при загрузке. ИСпользуется для поддержки недиспетчеризованных виртуальных функций C++.
38 8 IMAGE_DATA_DIRECTORY ExportAddressTableJumps RVA массива RVA, куда заносятся связки JMP для экспорта.
40 8 IMAGE_DATA_DIRECTORY ManagedNativeHeader RVA внутренней структуры .NET. Заполнена нулями.

Подробное описание виртуальной машины .NET выходит за рамки данного документа.

© 2005 Юрий Лукач