Формат Portable Executable ( PE ) — это формат файла для исполняемых файлов , объектного кода , DLL и других, используемых в 32- и 64-разрядных версиях операционных систем Windows , а также в средах UEFI . [2] Формат PE — это структура данных, которая инкапсулирует информацию, необходимую загрузчику ОС Windows для управления исполняемым кодом . Сюда входят ссылки на динамические библиотеки для связывания , таблицы экспорта и импорта API , данные управления ресурсами и данные локального хранилища потоков (TLS). В операционных системах NT формат PE используется для EXE , DLL , SYS ( драйвер устройства ), MUI и других типов файлов. В спецификации Unified Extensible Firmware Interface (UEFI) указано, что PE является стандартным форматом исполняемых файлов в средах EFI. [3]
В операционных системах Windows NT PE в настоящее время поддерживает архитектуры набора инструкций (ISA) IA-32 , x86-64 (AMD64/Intel 64), IA-64 , ARM и ARM64 . До Windows 2000 Windows NT (и, следовательно, PE) поддерживала MIPS , Alpha и PowerPC ISA. Поскольку PE используется в Windows CE , она продолжает поддерживать несколько вариантов MIPS, ARM (включая Thumb ) и SuperH ISA. [4]
Аналогичными форматами PE являются ELF (используется в Linux и большинстве других версий Unix ) и Mach-O (используется в macOS и iOS ).
Microsoft перешла на формат PE с 16-битных форматов NE с появлением операционной системы Windows NT 3.1 . Все более поздние версии Windows, включая Windows 95/98/ME и дополнение Win32s к Windows 3.1x, поддерживают эту файловую структуру. Формат сохранил ограниченную поддержку устаревших версий, чтобы преодолеть разрыв между системами на основе DOS и NT. Например, заголовки PE/COFF по-прежнему включают исполняемую программу DOS , которая по умолчанию является заглушкой DOS , отображающей сообщение типа «Эта программа не может быть запущена в режиме DOS» (или подобное), хотя это может быть полноценная версия программы для DOS (более поздним известным случаем был установщик Windows 98 SE). У компоновщика Microsoft есть переключатель /STUB
для присоединения одного из них. [5] Это представляет собой форму толстого двоичного файла .
PE также продолжает обслуживать меняющуюся платформу Windows. Некоторые расширения включают формат .NET PE, версию с поддержкой 64-битного адресного пространства, называемую PE32+, и спецификацию для Windows CE.
Является ли исполняемый код 32- или 64-битным, можно узнать, проверив поле Machine в IMAGE_FILE_HEADER. [6] Являются ли адреса в исполняемом файле 32- или 64-битными, можно узнать, проверив поле Magic в IMAGE_OPTIONAL_HEADER. 10B 16 указывает на файл PE32, тогда как 20B 16 указывает на файл PE32+. [7]
Файл PE состоит из ряда заголовков и разделов, которые информируют динамический компоновщик о том, как отобразить файл в памяти. Исполняемый образ состоит из нескольких различных областей, каждая из которых требует различной защиты памяти; поэтому начало каждой области должно быть выровнено по границе страницы. [8] Например, обычно раздел .text (который содержит программный код) отображается как исполняемый/только для чтения, а раздел .data (содержащий глобальные переменные) отображается как неисполняемый/чтение-запись. Однако, чтобы избежать траты пространства, различные разделы не выравниваются по странице на диске. Часть работы динамического компоновщика заключается в том, чтобы отобразить каждую область в памяти по отдельности и назначить правильные разрешения полученным областям в соответствии с инструкциями, содержащимися в заголовках. [9]
Один из разделов, заслуживающих внимания, — это таблица адресов импорта (IAT), которая используется в качестве таблицы поиска, когда приложение вызывает функцию в другом модуле. Она может быть в форме как импорта по порядковому номеру, так и импорта по имени . Поскольку скомпилированная программа не может знать расположение памяти библиотек, от которых она зависит, требуется косвенный переход всякий раз, когда выполняется вызов API. Поскольку динамический компоновщик загружает модули и объединяет их вместе, он записывает фактические адреса в слоты IAT, так что они указывают на расположение памяти соответствующих библиотечных функций. Хотя это добавляет дополнительный переход к стоимости внутримодульного вызова, что приводит к снижению производительности, это обеспечивает ключевое преимущество: количество страниц памяти, которые необходимо копировать при записи, измененных загрузчиком, сводится к минимуму, что экономит память и время ввода-вывода на диске. Если компилятор заранее знает, что вызов будет межмодульным (через атрибут dllimport), он может создать более оптимизированный код, который просто приводит к косвенному вызову opcode . [9]
Файлы PE обычно не содержат позиционно-независимый код . Вместо этого они компилируются в предпочтительный базовый адрес , и все адреса, выдаваемые компилятором/линковщиком, фиксируются заранее. Если файл PE не может быть загружен по его предпочтительному адресу (потому что он уже занят чем-то другим), операционная система перебазирует его . Это включает в себя пересчет каждого абсолютного адреса и изменение кода для использования новых значений. Загрузчик делает это, сравнивая предпочтительный и фактический адреса загрузки и вычисляя дельта- значение. Затем оно добавляется к предпочтительному адресу, чтобы получить новый адрес ячейки памяти. Базовые перемещения сохраняются в списке и добавляются по мере необходимости в существующую ячейку памяти. Результирующий код теперь является частным для процесса и больше не может быть общим , поэтому многие преимущества экономии памяти DLL теряются в этом сценарии. Это также значительно замедляет загрузку модуля. По этой причине перебазирования следует избегать везде, где это возможно, и библиотеки DLL, поставляемые Microsoft, имеют заранее вычисленные базовые адреса, чтобы не перекрываться. В случае без перебазирования PE, таким образом, имеет преимущество в виде очень эффективного кода, но при наличии перебазирования использование памяти может быть дорогим. Это контрастирует с ELF , где полностью позиционно-независимый код обычно предпочтительнее перебазирования во время загрузки, таким образом жертвуя временем выполнения в пользу меньшего использования памяти.
В исполняемом файле .NET раздел кода PE содержит заглушку, которая вызывает запись запуска виртуальной машины CLR , _CorExeMain
или _CorDllMain
в mscoree.dll
, во многом подобно тому, как это было в исполняемых файлах Visual Basic . Затем виртуальная машина использует имеющиеся метаданные .NET, корень которых IMAGE_COR20_HEADER
(также называемый «заголовком CLR») указывается IMAGE_DIRECTORY_ENTRY_COMHEADER
(запись ранее использовалась для метаданных COM+ в приложениях COM+, отсюда и название [ необходима цитата ] ) записью в каталоге данных заголовка PE. IMAGE_COR20_HEADER
сильно напоминает необязательный заголовок PE, по сути играя свою роль для загрузчика CLR. [4]
Данные, связанные с CLR, включая саму корневую структуру, обычно содержатся в общем разделе кода, .text
. Он состоит из нескольких каталогов: метаданные, встроенные ресурсы, строгие имена и несколько для взаимодействия с собственным кодом. Каталог метаданных — это набор таблиц, в которых перечислены все отдельные сущности .NET в сборке, включая типы, методы, поля, константы, события, а также ссылки между ними и на другие сборки.
Формат PE также используется ReactOS , поскольку ReactOS предназначен для двоичной совместимости с Windows. Он также исторически использовался рядом других операционных систем, включая SkyOS и BeOS R3. Однако и SkyOS, и BeOS в конечном итоге перешли на ELF . [ необходима цитата ]
Поскольку платформа разработки Mono намерена быть двоично совместимой с Microsoft .NET Framework , она использует тот же формат PE, что и реализация Microsoft. То же самое касается и собственного кроссплатформенного .NET Core от Microsoft .
На операционных системах Unix x86 (-64) двоичные файлы Windows (в формате PE) могут быть выполнены с помощью Wine . HX DOS Extender также использует формат PE для собственных 32-битных двоичных файлов DOS, а также может, в некоторой степени, выполнять существующие двоичные файлы Windows в DOS, таким образом действуя как эквивалент Wine для DOS.
Mac OS X 10.5 имеет возможность загружать и анализировать PE-файлы, но несовместима с Windows на уровне двоичного кода. [10]
Прошивки UEFI и EFI используют переносимые исполняемые файлы, а также соглашение о вызовах Windows ABI x64 для приложений .
... Стивен Эдвардс описывает открытие, что Leopard, по-видимому, содержит недокументированный загрузчик для Portable Executables, типа файла, используемого в 32-разрядных и 64-разрядных версиях Windows. Дальнейшее исследование показало, что собственный загрузчик Leopard пытается найти файлы Windows DLL при попытке загрузить двоичный файл Windows.