Перейти к содержанию

Формат ELF

ELF (Executable and Linkable Format) — стандартный бинарный формат в Linux. Один и тот же формат используется для объектных файлов, исполняемых файлов, разделяемых библиотек и core-дампов.

Типы ELF-файлов

Тип ELF-файла записан в заголовке (e_type). Основные типы:

Тип Название Когда встречается
ET_REL Relocatable объектный файл (.o)
ET_EXEC Executable классический исполняемый файл без PIE
ET_DYN Shared object разделяемая библиотека (.so) и PIE-бинарь
ET_CORE Core file дамп памяти упавшего процесса

Примеры получения каждого типа:

# ET_REL
g++ -c main.cpp -o main.o && readelf -h main.o | grep Type

# ET_EXEC (статический, нет PIE)
g++ -static -no-pie main.cpp -o main_static && readelf -h main_static | grep Type

# ET_DYN (библиотека)
g++ -shared -fPIC lib.cpp -o libfoo.so && readelf -h libfoo.so | grep Type

# ET_DYN (PIE — современный бинарь по умолчанию)
g++ main.cpp -o main && readelf -h main | grep Type

Структура ELF-файла

ELF-файл состоит из трёх логических частей:

  1. ELF-заголовок (Elf64_Ehdr) — тип файла, архитектура, точка входа, расположение таблицы секций и таблицы сегментов.
  2. Секции (sections) — логические блоки, используемые линковщиком (описываются таблицей SHT).
  3. Сегменты (program headers) — описывают, как файл отображается в память при загрузке (используются ядром и загрузчиком).

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

ELF-файл на диске
┌──────────────────────────────────────────────────────┐
│              ELF header (Elf64_Ehdr)                 │
│  e_type, e_machine, e_entry (точка входа _start)     │
│  e_phoff ──▶ program headers    e_shoff ──▶ таблица  │
│              (для ldso)                    секций    │
│                                            (для ld)  │
├──────────────────────────────────────────────────────┤
│         Program Header Table (PHT)                   │
│  описывает сегменты: PT_LOAD, PT_DYNAMIC, PT_INTERP  │
├──────────────────────────────────────────────────────┤
│  .interp   │ путь к ld-linux-x86-64.so.2             │
├────────────┼─────────────────────────────────────────┤
│  .text     │ машинный код                            │
├────────────┼─────────────────────────────────────────┤
│  .rodata   │ строки, константы                       │
├────────────┼─────────────────────────────────────────┤
│  .data     │ инициализированные глобальные переменные│
├────────────┼─────────────────────────────────────────┤
│  .bss      │ (только размер; нулей в файле нет)      │
├────────────┼─────────────────────────────────────────┤
│  .symtab   │ полная таблица символов (для ld, gdb)   │
│  .strtab   │ строки имён символов                    │
├────────────┼─────────────────────────────────────────┤
│  .dynsym   │ таблица динамических символов (для ldso)│
│  .dynstr   │ строки динамических имён                │
│  .dynamic  │ DT_NEEDED, DT_RPATH, адреса таблиц      │
│  .plt/.got │ инфраструктура ленивого связывания      │
│  .rela.*   │ таблицы релокаций                       │
├────────────┼─────────────────────────────────────────┤
│  .shstrtab │ имена секций                            │
├──────────────────────────────────────────────────────┤
│         Section Header Table (SHT)                   │
│  описывает каждую секцию: имя, тип, смещение, размер │
└──────────────────────────────────────────────────────┘

File view vs runtime view

Секции — взгляд линковщика. Сегменты — взгляд загрузчика. Загрузчик не знает про .text или .rodata по отдельности: он видит PT_LOAD-сегменты и отображает их в память через mmap. Несколько секций с одинаковыми правами объединяются в один сегмент, чтобы сократить число страниц.

 Файл на диске                     Виртуальная память процесса
 (file view: секции)               (runtime view: сегменты)

 ┌──────────────┐                  ┌─────────────────────────────┐
 │  ELF header  │                  │                             │
 ├──────────────┤  PT_LOAD #1      │  .text + .rodata            │
 │    .text     │ ────────────────▶│  r-x  (read + execute)      │
 ├──────────────┤  (один mmap)     │                             │
 │   .rodata    │                  ├─────────────────────────────┤
 ├──────────────┤                  │                             │
 │    .data     │  PT_LOAD #2      │  .data + .bss               │
 ├──────────────┤ ────────────────▶│  rw-  (read + write)        │
 │    .bss      │  (нули для .bss  │  .bss занимает память, но   │
 ├──────────────┤   MAP_ANONYMOUS) │  не место в файле           │
 │   .dynamic   │ ─ ─ ─ ─ ─ ─ ─ ─ ▶│  .dynamic (внутри PT_LOAD#2)│
 ├──────────────┤   PT_DYNAMIC     │  ldso читает по указателю   │
 │  .dynsym     │   (только        │  из PT_DYNAMIC; отдельного  │
 │  .plt / .got │    указатель,    │  mmap для .dynamic нет      │
 │    ...       │    не mmap)      └─────────────────────────────┘
 └──────────────┘

Основные секции

Секция Содержимое
.text Машинный код программы
.data Инициализированные глобальные и статические переменные
.rodata Константные данные: строковые литералы, константы
.bss Неинициализированные глобальные/статические переменные (нет данных в файле, только размер)
.symtab Полная таблица символов (для линковщика и отладчика)
.dynsym Таблица динамических символов (для динамического линковщика)
.strtab Строки с именами символов из .symtab
.shstrtab Строки с именами секций
.rel.* / .rela.* Таблицы релокаций
.interp Путь к динамическому загрузчику (например, /lib64/ld-linux-x86-64.so.2)
.dynamic Массив структур Elf64_Dyn с параметрами динамической линковки
.plt Таблица переходов (Procedure Linkage Table)
.got / .got.plt Таблица глобальных смещений (Global Offset Table)

.interp — нуль-терминированная строка с путём к динамическому линковщику. Ядро читает её при запуске ELF-файла и передаёт управление указанному загрузчику.

.dynamic — массив структур Elf64_Dyn, где каждая запись задаёт один параметр: список нужных библиотек ( DT_NEEDED), rpath (DT_RUNPATH/DT_RPATH), адреса таблиц релокаций и строк.

.plt и .got — инфраструктура для ленивого (lazy) связывания: при первом вызове функции из .so динамический линковщик находит её адрес и записывает в .got.plt; последующие вызовы идут напрямую.

Секции — это file view: они существуют в ELF-файле для линковщика и могут быть stripped. Сегменты (program headers) — это runtime view: загрузчик смотрит именно на них (PT_LOAD, PT_DYNAMIC, PT_INTERP) и по ним отображает участки файла в память.

Утилиты для работы с ELF

readelf

Работает напрямую с ELF-структурами:

readelf -h a.out   # заголовок файла
readelf -S a.out   # таблица секций
readelf -l a.out   # таблица сегментов (program headers)
readelf -s a.out   # таблица символов
readelf -d a.out   # секция .dynamic
readelf -r a.out   # таблицы релокаций

objdump

objdump -d a.out           # дизассемблирование
objdump -d -M intel a.out  # Intel-синтаксис
objdump -x a.out           # заголовок + секции + символы
objdump -t a.out           # таблица символов
objdump -C -t a.out        # с демангированием C++-имён

nm

nm a.out          # все символы
nm -D a.out       # только динамические символы
nm -C a.out       # с демангированием

file

file a.out
# a.out: ELF 64-bit LSB pie executable, x86-64, ...

objcopy

objcopy копирует ELF-файлы, позволяя выбирать/удалять секции и менять формат.

Получить «голый» бинарный дамп (например, для прошивки микроконтроллера):

objcopy -O binary a.out a.out.bin

Извлечь только секцию .text:

objcopy --only-section=.text -O binary a.out text.bin

Удалить отладочную информацию:

objcopy --strip-debug a.out a.out.stripped

PLT и GOT: механизм ленивого связывания

При динамической линковке адрес функции из .so неизвестен до момента загрузки. Для эффективного разрешения адресов используется пара структур:

  • GOT (Global Offset Table) — массив указателей. Динамический линковщик заполняет его адресами функций и переменных из .so.
  • PLT (Procedure Linkage Table) — набор маленьких trampoline-функций. При первом вызове функции PLT-запись вызывает линковщик, который находит адрес и записывает его в GOT. При последующих вызовах PLT-запись прыгает напрямую через GOT.

Это называется ленивым связыванием (lazy binding): адреса разрешаются не при загрузке, а при первом фактическом вызове. Ленивое связывание можно отключить переменной окружения LD_BIND_NOW=1 или флагом -Wl,-z,now — тогда все адреса разрешаются при старте программы (медленнее запуск, но нет отложенных ошибок при отсутствующих символах).

Связанные темы

Источники

  • man readelf — утилита readelf
  • man objdump — утилита objdump
  • man nm — утилита nm
  • man objcopy — утилита objcopy
  • man 5 elf — описание структур ELF-формата
  • System V ABI: ELF Specification