Чем ELF отличается от PE: ключевые различия и когда какой формат использовать

Формат исполняемого файла определяет, как операционная система загружает программу в память, где находятся код, данные, ресурсы и информация для связывания. Наиболее распространённые форматы — ELF (Executable and Linkable Format) в Unix‑подобных системах и PE (Portable Executable) в Windows. Понимание их различий помогает выбрать правильный инструментарий, избежать проблем при кросс‑компиляции и правильно отлаживать бинарники.

Общее назначение и область применения

ELF разработан как универсальный формат для объектных файлов, исполняемых программ и разделяемых библиотек в системах, соответствующих стандарту System V ABI. Он используется в Linux, *BSD, Solaris, Android (в виде модифицированного ELF) и многих встроенных платформах.

PE возник в среде OS/2 и стал стандартным форматом для Windows‑приложений, драйверов, DLL и исполняемых файлов. Он также используется в UEFI и некоторых реализациях BIOS.

Таким образом, выбор формата напрямую связан с целевой операционной системой: если вы собираете программу для Linux — ELF, для Windows — PE.

Структура заголовка

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

  • ELF: первые четыре байта — 0x7F, ‘E’, ‘L’, ‘F’ (магическое число). После них следуют поля, указывающие класс (32‑ или 64‑бит), порядок байтов, версия, тип файла (исполняемый, объектный, разделяемая библиотека), архитектура Machine, адрес входа в программу и смещения к таблицам программ и секций.
  • PE: начинается с заголовка DOS‑совместимого MZ (0x4D 0x5A). После него находится stub, а затем сигнатура PE\0\0 (0x50 0x45 0x00 0x00). После сигнатуры идут файловый заголовок (COFF) и необязательный заголовок, содержащий информацию о секциях, адресе входа, размере изображения, импорте/экспорте, базе релокации и других атрибутах, специфичных для Windows.

Важно, что оба заголовка фиксированного размера и содержат смещения к другим структурам файла (таблицам секций, символов, релокации). Это позволяет загрузчику быстро находить нужные данные без полного сканирования файла.

Секции и их назначение

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

Типичные секции в ELF:

  • .text — машинный код, обычно только для чтения и исполнения.
  • .rodata — только для чтения данные (строки, константы).
  • .data — инициализированные изменяемые данные.
  • .bss — неинициализированные данные, занимают место в памяти, но не в файле (отмечаются как тип NOBITS).
  • .symtab и .strtab — таблица символов и строки их имён.
  • .rel* и .rela* — таблицы релокации (сложные и с добавлением addend).
  • .debug_* — отладочная информация (DWARF).

Типичные секции в PE:

  • .text — код, аналогичен ELF .text.
  • .rdata — только для чтение данные (строки, константы, импорт/экспорт таблицы).
  • .data — инициализированные данные.
  • .bss — обычно не выделяется как отдельная секция; неинициализированные данные помещаются в конец .data и отмечаются флагом IMAGE_SCN_CNT_INITIALIZED_DATA с нулевым размером в файле.
  • .rsrc — ресурсы (иконки, меню, диалоги, строки).
  • .reloc — информация о базовой релокации, необходимая если образ загружается по адресу, отличном от предпочтительного.
  • .edata — таблица экспорта (для DLL).
  • .idata — таблица импорта (ссылки на функции из других DLL).

Основное отличие: в PE чётко разделены секции для импорта/экспорта и ресурсов, тогда как в ELF эти данные могут находиться в обычных секциях (.dynsym, .dynstr) или в отдельных заметках (.note*). Также PE использует концепцию «image base» — предпочтительный адрес загрузки, который влияет на расчёт виртуальных адресов секций.

Таблицы символов и механизмы релокации

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

В ELF:

  • Символы хранятся в секциях .symtab (для объектных файлов) и .dynsym (для динамических символов). Каждая запись содержит имя (смещение в строковой таблице), значение, размер, информацию о привязке (local/global/weak) и типе (object, func, section и т.д.).
  • Релокация представлена типами R_* (например, R_X86_64_64, R_X86_64_PC32) и хранится в секциях .rel* (без addend) или .rela* (с addend). Это позволяет связывальщику подставлять правильные адреса при загрузке или при динамическом связывании.

В PE:

  • Таблица символов реализована через структуры COFF: IMAGE_SYMBOL и IMAGE_AUX_SYMBOL. Для экспорта/импорта используются отдельные структуры (IMAGE_EXPORT_DIRECTORY, IMAGE_IMPORT_DESCRIPTOR).
  • Релокация в PE — это базовая релокация, хранящаяся в секции .reloc. Каждая запись указывает смещение внутри изображения, которое нужно скорректировать при загрузке по адресу, отличающемуся от ImageBase. Для более тонкой релокации (например, относительные адреса) используются фикс‑up записи в импортных таблицах.

Практический вывод: в ELF гибче представлены типы релокации, что упрощает поддержку разных архитектур и моделей связывания (статическое, динамическое, Position‑Independent Code). PE ориентирован на модель с фиксированным базовым адресом и явной базовой релокацией, что упрощает загрузчик Windows, но делает добавление новых типов релокации более сложным.

Как выбрать формат для проекта

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

  1. Кросс‑компиляция: если вы собираете одну и ту же исходную кодовую базу для Linux и Windows, вам потребуется инструментарий, способный генерировать оба формата (например, GCC с целевыми triple x86_64-linux-gnu и x86_64-w64-mingw32). Понимание различий помогает настроить флаги (-fPIC, -shared) и правильно указать библиотеки.
  2. Отладка и анализ бинарников: инструменты вроде readelf, objdump работают с ELF, а dumpbin и peview — с PE. Знание структуры ускоряет поиск секций с кодом, импорта или ресурсов.
  3. Создание собственного загрузчика или упаковщика: при написании загрузчика для ОС или упаковщика (например, UPX) необходимо точно знать, где находятся заголовки, таблицы импорта и секции с кодом, чтобы не повредить выполнимый образ.
  4. Работа с плагинами и модулями: некоторые системы поддерживают загрузку модулей в формате ELF (например, ядро Linux) или PE (например, плагины для Windows‑приложений). Понимание формата позволяет реализовать безопасную загрузку и проверку подписи.

Типичные ошибки и как их избежать

При работе с этими форматами часто встречаются следующие недочёты:

  • Неправильные флаги секций: выставление флага исполнения на секцию с данными может привести к ошибке защиты памяти при выполнении. Следует проверять, что секции .text имеют флаг SHF_EXECINSTR (ELF) или IMAGE_SCN_MEM_EXECUTE (PE), а данные — только чтение/запись.
  • Отсутствие таблицы импорта в PE DLL: если при сборке DLL забыть указать экспортируемые функции, получатели не смогут связаться с библиотекой. Проверяйте наличие секции .edata и корректность AddressOfFunctions.
  • Неучёт базовой релокации при загрузке по другому адресу: в Windows, если образ загружается не по ImageBase, загрузчик применяет базовую релокацию. Если секция .reloc повреждена или отсутствует, приложение может падать с ошибкой статуса STATUS_INVALID_IMAGE_FORMAT. При создании собственного загрузчика убедитесь, что вы обрабатываете эту секцию.
  • Смешивание 32‑ и 64‑битных полей: при ручном правке бинарника (например, через hex‑редактор) легко перепутать размеры полей (например, e_entry в ELF64 — 8 байт, в ELF32 — 4 байта). Всегда сверяйтесь с заголовком класса (EI_CLASS).
  • Игнорирование выравнивания: оба формата требуют определённого выравнивания секций (например, 0x1000 для виртуального адреса в PE, часто 0x20 или 0x40 для смещения в файле). Нарушение выравнивания приводит к ошибкам загрузчика.

Практический следующий шаг

Если вам нужно просто определить, какой формат имеет данный файл, выполните одну из следующих проверок:

  1. Откройте файл в шестнадцатеричном редакторе и посмотрите первые байты:
    • ELF: 7F 45 4C 46
    • PE: 4D 5A (MZ) followed later by 50 45 00 00
    • Используйте системные утилиты:
      • file <имя> в Linux покажет «ELF …» или «PE32…»/«PE32+…»;
      • В Windows команда dumpbin /headers <имя> выведет заголовок PE, либо сообщит, что файл не является допустимым исполняемым образом.
      • Для более глубокого анализа воспользуйтесь специализированными инструментами:
        • ELF: readelf -h <файл> (заголовок), readelf -S <файл> (секции).
        • PE: objdump -x <файл> (при наличии GNU binutils с поддержкой PE) или peview <файл> GUI‑утилита.

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

        Понимание различий между ELF и PE не только помогает избежать типичных проблем при сборке и отладке, но и даёт представление о том, как операционные системы организуют выполнимый код в памяти. Это знание полезно как при разработке кроссплатформенных приложений, так и при анализе вредоносного ПО или создании собственных систем загрузки.

        PEFile.ru