Как устроены файлы ELF в Linux: структура, загрузка и разбор изнутри

Когда в Linux запускается обычная программа, пользователь обычно видит только команду вроде ./app. Но внутри происходит гораздо больше: ядро открывает файл, проверяет его формат, загружает нужные части в память, подключает библиотеки и передаёт управление точке входа. Формат, который описывает такую программу, называется ELF (Executable and Linkable Format).

Понимание того, как устроены файлы ELF в Linux, полезно не только разработчикам системного ПО. Это помогает разбираться с ошибками запуска, анализировать чужие бинарники, понимать работу линковщика, отладчика, загрузчика библиотек и инструментов вроде readelf, objdump и nm.

На практике чаще всего проблема выглядит так: программа не запускается, библиотека «не находится», адреса в дампе памяти кажутся непонятными или нужно понять, что находится внутри готового бинарника. Во всех этих случаях знание структуры ELF экономит время.

Что такое ELF и зачем Linux использует этот формат

ELF — это стандартный формат хранения исполняемых файлов, объектных файлов и динамических библиотек в Linux и других Unix-подобных системах.

Один и тот же формат используется для разных задач:

  • исполняемые файлы — программы, которые можно запустить напрямую;
  • объектные файлы — промежуточные результаты компиляции с расширением .o;
  • динамические библиотеки — файлы .so, которые подключаются во время запуска программы.

Например, после компиляции:

  1. исходный код превращается компилятором в объектный ELF-файл;
  2. линковщик объединяет объектные файлы и библиотеки;
  3. получается готовый исполняемый ELF-файл;
  4. ядро Linux загружает его в память.

Главная идея ELF проста: файл содержит не только машинный код, но и описание того, как этот код использовать.

Как выглядит ELF-файл внутри

У ELF есть несколько основных уровней описания. Новички часто путают секции и сегменты, хотя они решают разные задачи.

Часть ELF Для чего нужна Кто использует Пример содержимого
ELF header Главное описание файла Ядро, инструменты анализа Тип файла, архитектура, точка входа
Program header table Описание частей для загрузки в память Загрузчик Linux Код, данные, динамический загрузчик
Section header table Описание логических частей файла Компилятор, линковщик, отладчики .text, .data, .symtab
Секции Организация содержимого Линковщик Код, строки, таблицы символов
Сегменты Описание загрузки в память Ядро Linux Области с правами чтения, записи, исполнения

Если объяснить проще: секции нужны тем, кто собирает программу, а сегменты — тем, кто её запускает.

ELF header: первая информация о файле

Самое начало ELF-файла занимает заголовок. Его задача — рассказать, что это за файл и как его читать.

В заголовке находятся такие данные:

  • магическое число ELF, по которому система понимает формат файла;
  • тип файла;
  • архитектура процессора;
  • адрес точки входа;
  • местоположение таблиц заголовков;
  • размеры структур внутри файла.

Проверить эту информацию можно командой:

readelf -h ./program

Например, среди вывода можно увидеть:

Type: EXEC
Machine: Advanced Micro Devices X86-64
Entry point address: 0x401040

Адрес Entry point — это место, куда управление попадёт после загрузки программы. Это не обязательно функция main(). Обычно между ними есть код запуска, который подготавливает окружение и только потом вызывает main.

Секции ELF: что находится внутри программы

Секции — это логическое разделение содержимого файла. Они особенно важны во время компиляции и линковки.

Самые часто встречающиеся секции:

  • .text — машинный код программы;
  • .data — изменяемые глобальные переменные с начальными значениями;
  • .rodata — данные только для чтения, например строковые константы;
  • .bss — глобальные переменные без явной инициализации;
  • .symtab — таблица символов;
  • .strtab — строки, связанные с именами символов;
  • .debug — отладочная информация, если она была сохранена.

Например, если в программе есть:

const char *message = "hello";
int counter = 10;

то строка, скорее всего, попадёт в .rodata, а переменная counter — в .data.

Посмотреть список секций можно так:

readelf -S ./program

Сегменты ELF: как программа попадает в память

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

Сегмент описывает, какую область файла нужно загрузить в память и с какими правами.

Например:

  • код программы обычно загружается с правами чтения и выполнения;
  • данные получают права чтения и записи;
  • служебная информация может вообще не попадать в память.

Проверить сегменты можно командой:

readelf -l ./program

Типичная картина выглядит примерно так:

  • LOAD — область, которую нужно загрузить в память;
  • DYNAMIC — информация для динамического линковщика;
  • INTERP — путь к загрузчику динамических библиотек;
  • GNU_STACK — параметры стека.

Как Linux запускает ELF-файл шаг за шагом

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

  1. Shell передаёт имя файла ядру.

    Команда запуска вызывает системный вызов execve().

  2. Ядро проверяет ELF-заголовок.

    Оно убеждается, что файл подходит для текущей архитектуры.

  3. Создаётся адресное пространство процесса.

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

  4. Запускается динамический загрузчик.

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

  5. Управление передаётся точке входа.

    После выполнения подготовительного кода начинается выполнение самой программы.

Статическая и динамическая линковка ELF

Один из практических вопросов при работе с ELF — как программа получает зависимости.

Вариант Как работает Плюсы Минусы
Статическая линковка Библиотеки включаются внутрь файла Меньше внешних зависимостей Файл больше, обновлять сложнее
Динамическая линковка Библиотеки подключаются при запуске Меньший размер, проще обновление библиотек Нужно наличие совместимых библиотек

Проверить зависимости программы можно командой:

ldd ./program

Если программа неожиданно перестала запускаться после обновления системы, первым делом стоит проверить именно этот список.

Какие ошибки чаще всего делают при работе с ELF

Путают секции и сегменты

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

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

Ищут main() как точку входа

В ELF нет требования, что выполнение начинается с main. Реальная точка входа обычно находится раньше и принадлежит стартовому коду.

Смотрят только на размер файла

Размер ELF не показывает напрямую, сколько памяти займёт программа. Некоторые области могут быть виртуальными или отображаться специальным образом.

Проверяют библиотеку не тем инструментом

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

Как анализировать ELF на практике

Не нужно сразу читать весь стандарт ELF. Для большинства задач достаточно нескольких команд.

Задача Команда Что получите
Узнать тип файла file program Архитектура и формат
Посмотреть заголовок readelf -h program Основные параметры ELF
Посмотреть секции readelf -S program Структура содержимого
Посмотреть сегменты readelf -l program Что попадёт в память
Проверить библиотеки ldd program Зависимости .so
Посмотреть символы nm program Имена функций и объектов

Что выбрать в зависимости от задачи

  • Нужно понять, почему программа не запускается.

    Начните с file, затем проверьте зависимости через ldd. Частая причина — несовместимая архитектура или отсутствующая библиотека.

  • Нужно разобраться, что внутри бинарника.

    Используйте readelf -S и objdump. Они покажут код, секции и служебную информацию.

  • Нужно изучить работу загрузчика.

    Смотрите таблицу сегментов через readelf -l и обращайте внимание на записи LOAD и DYNAMIC.

  • Нужно отлаживать программу.

    Не удаляйте отладочные секции и собирайте бинарник с символами отладки.

Практические рекомендации при работе с ELF

Если приходится регулярно анализировать Linux-бинарники, полезно придерживаться нескольких правил:

  • Всегда начинайте с простых инструментов: file, readelf, ldd.
  • Не меняйте бинарник вручную без понимания структуры ELF.
  • При проблемах с запуском сначала проверяйте архитектуру и зависимости.
  • Для изучения программы используйте копию файла, а не оригинальный бинарник.
  • Помните, что отсутствие некоторых секций не всегда означает повреждение файла.

Главное, что стоит запомнить про ELF

ELF — это не просто «формат исполняемого файла». Это описание того, как программа устроена, какие данные в ней находятся и как операционная система должна её загрузить.

Для практической работы достаточно держать в голове три уровня:

  • ELF header отвечает за общую информацию о файле;
  • секции помогают организовать содержимое во время сборки;
  • сегменты определяют, что именно попадёт в память при запуске.

Если нужно быстро разобраться с неизвестным бинарником, не пытайтесь сразу изучать весь стандарт ELF. Откройте файл через file, посмотрите заголовки через readelf, проверьте зависимости через ldd — в большинстве реальных задач этого достаточно, чтобы понять, что происходит.

PEFile.ru