Чем формат ELF отличается от PE: понятное сравнение исполняемых файлов

ELF и PE — это два основных формата исполняемых файлов: ELF используется в Linux и других Unix-подобных системах, PE — в Windows. Суть различия не в «расширении файла», а в том, как устроен бинарник внутри: какие заголовки он содержит, как операционная система находит код и данные, как подключает библиотеки и с чего начинает выполнение программы. Если вы разбираетесь в разработке, реверс-инжиниринге или просто хотите понять, почему скомпилированный под Linux файл не запустится в Windows без прослойки, — ниже всё разложено по полочкам.

Главный практический ориентир такой: формат определяется целевой операционной системой и загрузчиком. Ядро Linux умеет загружать ELF, ядро Windows — PE. Формат задаёт «договорённость» между компилятором и ОС о том, где лежит точка входа, таблица секций, информация для отладчика и список нужных библиотек.

Что такое исполняемый формат и зачем он нужен

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

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

Всю эту метаинформацию описывает формат. ELF (Executable and Linkable Format) пришёл из System V и стал стандартом де-факто для Linux, BSD-систем, а также для объектных файлов и дампов ядра. PE (Portable Executable) вырос из формата COFF и используется во всех современных версиях Windows — от драйверов до приложений из Microsoft Store.

Структура файла: как устроены ELF и PE внутри

Устройство ELF

Файл ELF начинается с заголовка фиксированного размера, который содержит «магическую» последовательность байт 0x7F 45 4C 46 (в ASCII это символы DEL и «ELF»), класс формата (32 или 64 бита), порядок байтов, версию, тип файла (исполняемый, разделяемая библиотека, объектный модуль, дамп ядра), целевую архитектуру и адрес точки входа.

Дальше структура делится на две логические части, которые отвечают разным задачам:

  • Таблица программных заголовков — нужна загрузчику ядра. Она описывает сегменты: какие куски файла и куда отображать в память, с какими правами доступа. Это то, что реально попадает в адресное пространство процесса.
  • Таблица секций — нужна линковщику, отладчику и инструментам анализа. Секции (.text, .data, .bss, .rodata, .symtab, .debug_info и другие) группируют содержимое по смыслу. Для запуска программы секции не обязательны: их можно вырезать, и файл продолжит работать.

Такое разделение — одна из характерных черт ELF: «что грузить в память» и «как разбирать файл инструментами» описаны отдельно и независимо.

Устройство PE

PE-файл начинается с классической структуры MS-DOS: первые два байта — «MZ», а в начале файла лежит небольшой DOS-заголовок со старой заглушкой («This program cannot be run in DOS mode»). Это наследие совместимости: поле указывает на настоящий PE-заголовок, который начинается с сигнатуры «PE\0\0».

Дальше идут:

  • COFF-заголовок — архитектура, число секций, характеристики файла;
  • Optional Header — несмотря на название, для исполняемых файлов он обязателен: здесь точка входа, базовый адрес загрузки, выравнивание секций, версия подсистемы (консоль или GUI), размеры стека и кучи;
  • Таблица секций — .text, .data, .rdata, .rsrc, .reloc и другие;
  • Каталог данных (Data Directories) — массив указателей на важные структуры: таблицу импорта, таблицу экспорта, ресурсы, отладочную информацию, сертификат подписи, таблицу исключений.

Ключевая идея PE — каталог данных: загрузчик Windows находит всё необходимое через фиксированный массив смещений, а не через поиск именованных структур. Это делает формат жёстче, но предсказуемее для парсера.

Сравнительная таблица основных различий

Критерий ELF PE / PE32+
Основные платформы Linux, BSD, Solaris, встраиваемые Unix-системы Windows (все современные версии), UEFI
Магическая сигнатура 0x7F «ELF» в начале файла «MZ» в начале, «PE\0\0» перед основным заголовком
Описание загрузки в память Программные заголовки (сегменты) Секции + Optional Header
Метаданные для инструментов Отдельная таблица секций, может отсутствовать Единая таблица секций, всегда присутствует
Динамические библиотеки .so (shared objects) DLL
Связывание библиотек Через PLT/GOT с ленивой привязкой по умолчанию Через IAT, разрешение имён при загрузке
Ресурсы (иконки, строки интерфейса) Нет стандартного механизма, используются сторонние схемы Встроенная секция ресурсов .rsrc
Цифровая подпись Не входит в сам формат (реализуется пакетными менеджерами, detached-подписями) Встроена в формат через каталог безопасности
Допустимые роли файла Исполняемый, библиотека, объектный модуль, core dump Исполняемый, DLL, драйвер режима ядра, объектный модуль (COFF)

Динамическое связывание: самое заметное поведенческое различие

Оба формата поддерживают динамические библиотеки, но механика заметно отличается, и это влияет на производительность запуска и на приёмы анализа.

Как это работает в ELF

Вызовы внешних функций идут через две служебные структуры: GOT (Global Offset Table) и PLT (Procedure Linkage Table). По умолчанию используется ленивая привязка: при первом вызове функции управление получает динамический загрузчик, он находит адрес функции в нужной библиотеке и записывает его в GOT. Все последующие вызовы идут напрямую. Это ускоряет старт программы, потому что резолвятся только реально используемые функции.

Ленивую привязку можно отключить флагом компоновщика (например, режим BIND_NOW / full RELRO), тогда все адреса разрешаются при загрузке. Это закрывает целый класс атак, связанных с перезаписью GOT, ценой чуть более долгого старта.

Как это работает в PE

Windows использует таблицу импортов (Import Address Table). Загрузчик при запуске процесса проходит по списку импортируемых DLL и функций и заполняет IAT реальными адресами. Исторически ленивая привязка существовала через «delay-load» библиотеки, но это опциональный механизм, а не поведение по умолчанию.

Практическое следствие: в PE список зависимостей виден сразу и целиком, поэтому статический анализ импортов — один из первых шагов при исследовании подозрительного файла. В ELF ленивая привязка исторически давала вредоносному коду больше пространства для манёвра, хотя современные защиты (RELRO, проверка целостности) эту разницу сокращают.

Адресация и перемещения

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

  • В ELF стандартом стала позиционно-независимая сборка (PIC/PIE): код не требует правки, все ссылки идут через относительные смещения и GOT. Исполняемый файл PIE загружается по случайному адресу — это часть механизма ASLR.
  • В PE компилятор часто предполагает предпочтительный базовый адрес (по умолчанию для exe — 0x400000). Если загрузить файл по нему не удалось, загрузчик применяет перемещения (relocations) из секции .reloc, поправляя абсолютные адреса. ASLR в Windows работает поверх этого механизма.

На практике это означает, что ELF-бинарники исторически легче переносят смену базового адреса, а PE-файлы без таблицы перемещений могут не запуститься, если их предпочтительный адрес занят.

Ресурсы, подписи и метаданные уровня ОС

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

В ELF ничего подобного на уровне формата нет. Иконки и версии приложения в Linux живут в отдельных файлах (.desktop, метаданные пакета), а подпись обычно проверяется на уровне пакета или отдельного файла подписи. Исключение — некоторые платформенные решения вроде схем подписи в Android (там ELF является базовым форматом, но подпись реализована поверх).

Инструменты для работы с каждым форматом

Набор утилит отражает происхождение форматов:

  • Для ELF: readelf, objdump, nm, ldd, strace, ltrace; в составе GNU binutils и LLVM. Отладчики — gdb, lldb.
  • Для PE: dumpbin из Visual Studio, PowerShell-командлеты, сторонние анализаторы вроде PE-bear и CFF Explorer; отладчики — WinDbg, x64dbg.
  • Кроссплатформенные: radare2/Cutter, Ghidra, IDA Pro работают с обоими форматами одинаково уверенно.

Если нужно быстро осмотреть файл любого формата, самый простой первый шаг — посмотреть первые байты: 7F 45 4C 46 укажут на ELF, «MZ» — на PE.

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

Ядро операционной системы распознаёт формат при вызове exec-подобных функций. Linux умеет загружать ELF нативно; запуск PE возможен только через слой эмуляции (Wine, Proton) или виртуальную машину. Windows нативно понимает PE; подсистема WSL внутри выполняет ELF, но это уже отдельная среда с собственным загрузчиком.

Отсюда практический вывод: кроссплатформенность достигается не «конвертацией формата», а одним из путей:

  1. компиляцией исходников отдельно под каждую платформу (кросс-компиляция);
  2. использованием среды выполнения, которая абстрагирует ОС (Java, .NET, Python и подобные — они сами упакованы в ELF или PE, а ваш код остаётся переносимым);
  3. эмуляцией или контейнеризацией (Wine, WSL, виртуальные машины).

Типичные вопросы и заблуждения

Зависит ли формат от языка программирования?

Нет. C, C++, Rust, Go, Zig — любой язык, компилируемый в машинный код, даст ELF под Linux и PE под Windows. Формат выбирает компоновщик согласно целевой платформе, а не язык.

Можно ли переименовать .exe в bin и запустить в Linux?

Нет. Расширение ни на что не влияет: и ELF, и PE определяются по содержимому. Переименование не меняет внутреннюю структуру, и загрузчик откажется выполнять чужой формат.

Что такое Mach-O и при чём тут macOS?

macOS и iOS используют третий формат — Mach-O. Он решает те же задачи, но имеет собственную структуру. В этой статье он упоминается только чтобы обозначить границы: выбор «ELF против PE» актуален для мира Linux/Unix и Windows соответственно.

Одинаковы ли 32-битные и 64-битные варианты?

Логика форматов общая, но детали различаются: размер полей заголовков, выравнивание, модели памяти. В PE 64-битная версия называется PE32+, в ELF различие задаётся полем класса в заголовке. Бинарник одной разрядности не запустится на системе, которая поддерживает только другую (если нет слоя совместимости).

Что выбрать для практики: сценарии

  • Пишете серверное приложение под Linux — итог будет ELF. Обращайте внимание на зависимости от версий glibc: собранный на свежем дистрибутиве бинарник может не запуститься на старом. Статическая линковка или сборка в минимальном окружении снижают этот риск.
  • Пишете десктопное приложение под Windows — итог будет PE. Учитывайте, что подпись Authenticode и манифест (совместимость, права, DPI) задаются на этапе сборки и влияют на восприятие файла системой и антивирусами.
  • Анализируете неизвестный бинарник — начните с определения формата по первым байтам, затем смотрите таблицу импортов (PE) или динамические зависимости и секции (ELF): это быстро даёт представление о назначении файла.
  • Ищете максимум переносимости — не выбирайте формат вообще: используйте среду исполнения (VM, интерпретатор, WebAssembly), которая сама решает вопрос платформы.

Коротко о главном

ELF и PE решают одну задачу — рассказать операционной системе, как выполнить файл, — но разными средствами. ELF гибче: раздельные таблицы сегментов и секций, ленивое связывание, минимум навязанных структур. PE строже и теснее связан с Windows: единая схема секций, каталог данных, встроенные ресурсы и подпись. Выбор формата не принимают — его диктует целевая платформа. Поэтому практическая задача всегда звучит не «какой формат лучше», а «как собрать и упаковать программу так, чтобы она корректно работала там, где ей предстоит жить»: следите за зависимостями, разрядностью, настройками защиты (RELRO, ASLR) и, для Windows, за подписью и манифестом.

PEFile.ru