Как устроен исполняемый файл Windows: формат PE простыми словами

Любая программа в Windows — от калькулятора до игры — это исполняемый файл, чаще всего с расширением .exe или .dll. Внутри он устроен не как «свалка машинного кода», а по строгому стандарту, который называется PE-формат (Portable Executable). Понимание этого формата полезно любому, кто занимается разработкой, анализом подозрительных файлов или просто хочет понять, почему программа запускается или падает. В этой статье разберём устройство PE-файла по частям, без излишней теории, но так, чтобы картина сложилась целиком.

Главный принцип такой: PE-файл состоит из заголовков, которые описывают программу для операционной системы, и секций — блоков данных разного назначения. Когда вы запускаете файл, загрузчик Windows читает заголовки, выделяет память, копирует туда нужные части файла и передаёт управление коду. Ошибка на любом из этих шагов означает, что программа не запустится.

Что такое PE-формат и зачем он нужен

Процессор умеет выполнять только машинные команды, но сам по себе набор команд — это ещё не программа. Операционной системе нужно знать: где начинается код, какие библиотеки требуются, сколько памяти выделить, куда поместить данные. Для этого нужен договорённый формат контейнера. В Windows таким контейнером исторически стал PE, пришедший на смену более старому формату NE (New Executable) времён 16-разрядных систем.

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

PE-формат охватывает не только .exe. Под него подпадают:

  • .exe — обычные приложения;
  • .dll — библиотеки с функциями для других программ;
  • .sys — драйверы устройств;
  • .ocx, .cpl и другие компоненты — элементы управления и панели конфигурации.

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

Общая структура файла: два этажа

Условно PE-файл можно представить как двухэтажное здание. Первый этаж — служебные описания, второй — содержимое.

  1. Заголовки. Блоки метаданных в начале файла: сигнатура, описание архитектуры, точка входа, таблица секций, информация об импорте и экспорте.
  2. Секции. Основные блоки данных: собственно код, глобальные переменные, ресурсы (иконки, строки интерфейса), таблица перемещений и прочее.

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

DOS-заголовок: наследие 1980-х

Самое начало любого PE-файла занимает DOS-заголовок — рудимент совместимости с MS-DOS. Его главное поле сегодня одно: смещение до настоящей PE-сигнатуры. Там же лежит маленькая DOS-программа-заглушка. Если попытаться запустить современный exe в DOS, она выведет что-то вроде «This program cannot be run in DOS mode». Практической пользы для современных программ в ней нет, но поле сохраняется ради обратной совместимости и строгого порядка байтов.

PE-сигнатура и COFF-заголовок

По смещению из DOS-заголовка находится четырёхбайтовая сигнатура PE\0\0 — маркер того, что перед нами действительно Portable Executable. Сразу за ней идёт COFF-заголовок (Common Object File Format), который сообщает базовые сведения:

  • машину (архитектуру) — например, x86, x64 или ARM64; файл, собранный под одну архитектуру, на другой напрямую не запустится;
  • количество секций — чтобы загрузчик знал, сколько записей читать дальше;
  • временную метку компиляции — момент сборки файла;
  • характеристики — флаги вроде «исполняемый образ», «DLL», «32-битный».

Опциональный заголовок: самый информативный блок

Название «опциональный» обманчиво: для исполняемого файла этот заголовок обязателен. Именно здесь живёт то, что определяет запуск:

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

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

Секции: из чего состоит содержимое

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

Секция Что содержит Зачем нужна
.text Машинный код программы Исполняемые инструкции; обычно только для чтения и выполнения
.data Инициализированные данные Глобальные переменные с начальными значениями
.rdata Константы и таблицы Строковые литералы, таблица импорта, отладочные данные
.bss / .sbss Неинициализированные данные Переменные без начального значения; в файле места почти не занимают
.rsrc Ресурсы Иконки, курсоры, диалоги, версии, локализованные строки
.reloc Таблица перемещений Позволяет загрузить образ по другому адресу, если основной занят
.edata / .idata Экспорт и импорт Списки предоставляемых и запрашиваемых функций

У каждой секции в таблице секций указаны размер, смещение в файле, адрес в памяти и права доступа (чтение, запись, исполнение). Эти права важны для безопасности: например, секция кода не должна быть доступна на запись, а секция данных — на исполнение. Современные процессоры и система поддерживают это аппаратно через механизм DEP (Data Execution Prevention): попытка выполнить код из области данных приводит к аварийному завершению программы.

Импорт: откуда программа берёт чужие функции

Почти ни одна программа в Windows не пишет всё сама. Вывод окна, работа с файлами, сеть, рисование — всё это функции системных библиотек, прежде всего kernel32.dll, user32.dll, gdi32.dll и других. Механизм, связывающий программу с этими библиотеками, называется импортом.

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

  1. Читает список DLL из таблицы импорта.
  2. Находит каждую библиотеку (в папке приложения, системных каталогах, по путям поиска) и загружает её, если она ещё не в памяти.
  3. Находит в каждой библиотеке адреса запрошенных функций.
  4. Записывает эти адреса в специальные ячейки памяти программы — так называемую таблицу адресов импорта (IAT).

После этого вызов функции из DLL превращается в обычный косвенный переход по адресу из IAT. Если хотя бы одной библиотеки или функции не окажется, запуск прервётся с ошибкой вида «не найдена точка входа» или «отсутствует DLL» — это одна из самых частых причин, почему перенесённая на другой компьютер программа не стартует.

Экспорт: что библиотека предлагает другим

У DLL есть обратная сторона — экспорт: список функций, которые она предоставляет. Он оформлен таблицей экспорта с именами (или номерами) функций и их адресами внутри библиотеки. Именно по этой таблице загрузчик разрешает импорт зависимых программ. У обычного exe экспорта чаще всего нет: ему никто извне функции не вызывает.

Посмотреть импорт и экспорт можно без программирования — утилитами вроде Dependency Walker, встроенного дампера из набора разработчика или сторонних просмотрщиков PE. Это первый шаг при диагностике проблем с запуском и при первичном анализе неизвестного файла.

Ресурсы: всё, что видит пользователь

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

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

Адреса и перемещения: почему файл нельзя просто «скопировать в память»

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

Во-первых, ASLR (Address Space Layout Randomization) — если образ собран с поддержкой рандомизации, система специально загружает его каждый раз по новому адресу, усложняя эксплуатацию некоторых классов уязвимостей. Во-вторых, таблица перемещений (.reloc) — список мест в коде и данных, где записаны абсолютные адреса. Загрузчик вычисляет разницу между фактическим и предпочтительным адресом и поправляет каждое такое место. Этот процесс называется ребазированием.

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

Путь файла от двойного щелчка до работающей программы

Теперь всю картину можно собрать в последовательность:

  1. Проводник передаёт путь к файлу системе, та создаёт процесс.
  2. Загрузчик проверяет DOS-заголовок и PE-сигнатуру, убеждается, что формат корректен и архитектура подходит.
  3. Читаются опциональный заголовок и таблица секций: система узнаёт размер образа, точку входа, требования.
  4. Выделяется виртуальная память, секции отображаются в неё с нужными правами доступа.
  5. Разрешается импорт: загружаются зависимые DLL, заполняется IAT.
  6. При необходимости выполняется ребазирование по таблице .reloc.
  7. Выполняется инициализация среды (в упрощённом виде — подготовка стандартной библиотеки языка, конструкторы глобальных объектов).
  8. Управление передаётся на точку входа, и программа начинает работать.

Отсюда понятны типичные ошибки запуска: повреждённая сигнатура или заголовки («не является приложением Win32»), неподходящая архитектура, отсутствующая DLL из импорта, конфликт базовых адресов без reloc, нарушение прав доступа при выполнении.

Где это знание применяется на практике

  • Диагностика запуска. Сообщение об отсутствии DLL или точки входа сразу наводит на таблицу импорта: сравните, какие библиотеки требует файл и какие доступны на машине.
  • Первичная оценка неизвестного файла. Аномалии вроде исполняемой секции с правами на запись, странного набора импортируемых функций (например, работа с процессами и сетью у «просмотрщика картинок») или отсутствия подписи — поводы отнестись настороженно.
  • Разработка. Понимание секций объясняет, почему нельзя писать в константы, зачем нужны выравнивание и выгрузка библиотек, как работают плагины через экспортируемые функции DLL.
  • Оптимизация размера. Знание о том, что .bss не занимает места в файле, а ресурсы можно сжимать, помогает понимать, из чего складывается размер программы.

Типичные заблуждения

«Расширение определяет тип файла». На самом деле система ориентируется на содержимое: переименованный в .txt исполняемый файл останется PE-образом, а повреждённый .exe система откажется запускать независимо от имени.

«В exe лежит только код». Код — лишь одна секция. Рядом живут данные, ресурсы, таблицы импорта и метаданные, причём служебной информации в небольших программах может быть сопоставимо с полезной.

«Одинаковая иконка и версия = та же программа». Всё это — редактируемые ресурсы. Достоверность файла подтверждают цифровая подпись и её проверка, а не внешний вид.

«Если файл запустился — он безопасен». Запуск означает лишь корректность формата. Что именно делает код, формат сам по себе не показывает; для оценки поведения нужны статический анализ и песочница.

Что делать дальше, если тема вам пригодилась

Самый быстрый способ закрепить понимание — открыть любой небольшой exe или dll просмотрщиком PE-структур и найти своими глазами: DOS-заглушку, сигнатуру, таблицу секций, список импортируемых DLL. Затем попробуйте предсказать поведение: какие системные возможности использует программа, судя по импорту? Такой разбор одного файла даёт больше, чем чтение нескольких статей, и создаёт основу, если вы захотите углубиться в анализ вредоносного ПО, реверс-инжиниринг или низкоуровневую разработку под Windows.

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

PEFile.ru