Любая программа в Windows — от калькулятора до игры — это исполняемый файл, чаще всего с расширением .exe или .dll. Внутри он устроен не как «свалка машинного кода», а по строгому стандарту, который называется PE-формат (Portable Executable). Понимание этого формата полезно любому, кто занимается разработкой, анализом подозрительных файлов или просто хочет понять, почему программа запускается или падает. В этой статье разберём устройство PE-файла по частям, без излишней теории, но так, чтобы картина сложилась целиком.
Главный принцип такой: PE-файл состоит из заголовков, которые описывают программу для операционной системы, и секций — блоков данных разного назначения. Когда вы запускаете файл, загрузчик Windows читает заголовки, выделяет память, копирует туда нужные части файла и передаёт управление коду. Ошибка на любом из этих шагов означает, что программа не запустится.
- Что такое PE-формат и зачем он нужен
- Общая структура файла: два этажа
- DOS-заголовок: наследие 1980-х
- PE-сигнатура и COFF-заголовок
- Опциональный заголовок: самый информативный блок
- Секции: из чего состоит содержимое
- Импорт: откуда программа берёт чужие функции
- Экспорт: что библиотека предлагает другим
- Ресурсы: всё, что видит пользователь
- Адреса и перемещения: почему файл нельзя просто «скопировать в память»
- Путь файла от двойного щелчка до работающей программы
- Где это знание применяется на практике
- Типичные заблуждения
- Что делать дальше, если тема вам пригодилась
Что такое PE-формат и зачем он нужен
Процессор умеет выполнять только машинные команды, но сам по себе набор команд — это ещё не программа. Операционной системе нужно знать: где начинается код, какие библиотеки требуются, сколько памяти выделить, куда поместить данные. Для этого нужен договорённый формат контейнера. В Windows таким контейнером исторически стал PE, пришедший на смену более старому формату NE (New Executable) времён 16-разрядных систем.
Название «переносимый» подчёркивает ключевую идею: один и тот же скомпилированный файл может работать на разных машинах с одинаковым процессором и версией системы, потому что все зависимости и настройки описаны внутри самого файла, а не заданы жёстко при сборке под конкретный компьютер.
PE-формат охватывает не только .exe. Под него подпадают:
- .exe — обычные приложения;
- .dll — библиотеки с функциями для других программ;
- .sys — драйверы устройств;
- .ocx, .cpl и другие компоненты — элементы управления и панели конфигурации.
Все они имеют одинаковую базовую структуру, различаясь назначением и некоторыми полями заголовков.
Общая структура файла: два этажа
Условно PE-файл можно представить как двухэтажное здание. Первый этаж — служебные описания, второй — содержимое.
- Заголовки. Блоки метаданных в начале файла: сигнатура, описание архитектуры, точка входа, таблица секций, информация об импорте и экспорте.
- Секции. Основные блоки данных: собственно код, глобальные переменные, ресурсы (иконки, строки интерфейса), таблица перемещений и прочее.
Заголовки всегда идут первыми, потому что загрузчик читает их до того, как решит, что делать с остальным содержимым. Секции следуют за ними, обычно выровненные по границам, кратным определённому размеру, — это ускоряет отображение файла в память.
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 и конкретные функции из них — либо по именам, либо по порядковым номерам. При запуске загрузчик делает следующее:
- Читает список DLL из таблицы импорта.
- Находит каждую библиотеку (в папке приложения, системных каталогах, по путям поиска) и загружает её, если она ещё не в памяти.
- Находит в каждой библиотеке адреса запрошенных функций.
- Записывает эти адреса в специальные ячейки памяти программы — так называемую таблицу адресов импорта (IAT).
После этого вызов функции из DLL превращается в обычный косвенный переход по адресу из IAT. Если хотя бы одной библиотеки или функции не окажется, запуск прервётся с ошибкой вида «не найдена точка входа» или «отсутствует DLL» — это одна из самых частых причин, почему перенесённая на другой компьютер программа не стартует.
Экспорт: что библиотека предлагает другим
У DLL есть обратная сторона — экспорт: список функций, которые она предоставляет. Он оформлен таблицей экспорта с именами (или номерами) функций и их адресами внутри библиотеки. Именно по этой таблице загрузчик разрешает импорт зависимых программ. У обычного exe экспорта чаще всего нет: ему никто извне функции не вызывает.
Посмотреть импорт и экспорт можно без программирования — утилитами вроде Dependency Walker, встроенного дампера из набора разработчика или сторонних просмотрщиков PE. Это первый шаг при диагностике проблем с запуском и при первичном анализе неизвестного файла.
Ресурсы: всё, что видит пользователь
Секция ресурсов хранит некодовые данные: иконку файла в проводнике, версию и название продукта, тексты интерфейса, диалоговые окна, изображения, звуки. Ресурсы организованы деревом: тип ресурса → идентификатор → язык. Благодаря этому одна программа может содержать несколько языковых версий интерфейса, а система выбирает подходящую по региональным настройкам.
Практическое следствие: иконку, строку версии или картинку во многих случаях можно посмотреть или даже заменить специальными редакторами ресурсов, не трогая код. Этим пользуются и легитимно (локализация), и злоумышленники (маскировка вредоносной программы под известную), поэтому внешнее сходство иконки само по себе ничего не гарантирует.
Адреса и перемещения: почему файл нельзя просто «скопировать в память»
Код в программе содержит абсолютные адреса переменных и функций. Но заранее неизвестно, по какому адресу образ попадёт в память: желаемый базовый адрес может быть занят другой библиотекой. Решают это две вещи.
Во-первых, ASLR (Address Space Layout Randomization) — если образ собран с поддержкой рандомизации, система специально загружает его каждый раз по новому адресу, усложняя эксплуатацию некоторых классов уязвимостей. Во-вторых, таблица перемещений (.reloc) — список мест в коде и данных, где записаны абсолютные адреса. Загрузчик вычисляет разницу между фактическим и предпочтительным адресом и поправляет каждое такое место. Этот процесс называется ребазированием.
Если у образа нет таблицы перемещений, он обязан загрузиться строго по своему базовому адресу; когда тот занят, запуск завершится неудачей.
Путь файла от двойного щелчка до работающей программы
Теперь всю картину можно собрать в последовательность:
- Проводник передаёт путь к файлу системе, та создаёт процесс.
- Загрузчик проверяет DOS-заголовок и PE-сигнатуру, убеждается, что формат корректен и архитектура подходит.
- Читаются опциональный заголовок и таблица секций: система узнаёт размер образа, точку входа, требования.
- Выделяется виртуальная память, секции отображаются в неё с нужными правами доступа.
- Разрешается импорт: загружаются зависимые DLL, заполняется IAT.
- При необходимости выполняется ребазирование по таблице .reloc.
- Выполняется инициализация среды (в упрощённом виде — подготовка стандартной библиотеки языка, конструкторы глобальных объектов).
- Управление передаётся на точку входа, и программа начинает работать.
Отсюда понятны типичные ошибки запуска: повреждённая сигнатура или заголовки («не является приложением Win32»), неподходящая архитектура, отсутствующая DLL из импорта, конфликт базовых адресов без reloc, нарушение прав доступа при выполнении.
Где это знание применяется на практике
- Диагностика запуска. Сообщение об отсутствии DLL или точки входа сразу наводит на таблицу импорта: сравните, какие библиотеки требует файл и какие доступны на машине.
- Первичная оценка неизвестного файла. Аномалии вроде исполняемой секции с правами на запись, странного набора импортируемых функций (например, работа с процессами и сетью у «просмотрщика картинок») или отсутствия подписи — поводы отнестись настороженно.
- Разработка. Понимание секций объясняет, почему нельзя писать в константы, зачем нужны выравнивание и выгрузка библиотек, как работают плагины через экспортируемые функции DLL.
- Оптимизация размера. Знание о том, что .bss не занимает места в файле, а ресурсы можно сжимать, помогает понимать, из чего складывается размер программы.
Типичные заблуждения
«Расширение определяет тип файла». На самом деле система ориентируется на содержимое: переименованный в .txt исполняемый файл останется PE-образом, а повреждённый .exe система откажется запускать независимо от имени.
«В exe лежит только код». Код — лишь одна секция. Рядом живут данные, ресурсы, таблицы импорта и метаданные, причём служебной информации в небольших программах может быть сопоставимо с полезной.
«Одинаковая иконка и версия = та же программа». Всё это — редактируемые ресурсы. Достоверность файла подтверждают цифровая подпись и её проверка, а не внешний вид.
«Если файл запустился — он безопасен». Запуск означает лишь корректность формата. Что именно делает код, формат сам по себе не показывает; для оценки поведения нужны статический анализ и песочница.
Что делать дальше, если тема вам пригодилась
Самый быстрый способ закрепить понимание — открыть любой небольшой exe или dll просмотрщиком PE-структур и найти своими глазами: DOS-заглушку, сигнатуру, таблицу секций, список импортируемых DLL. Затем попробуйте предсказать поведение: какие системные возможности использует программа, судя по импорту? Такой разбор одного файла даёт больше, чем чтение нескольких статей, и создаёт основу, если вы захотите углубиться в анализ вредоносного ПО, реверс-инжиниринг или низкоуровневую разработку под Windows.
Материал носит информационный характер. Анализ неизвестных исполняемых файлов выполняйте только в изолированной среде (виртуальной машине) и без подключения к рабочей сети; решения о безопасности конкретных файлов принимайте с участием профильного специалиста.
