Формат 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-файл состоит из трёх логических частей:
- ELF-заголовок (
Elf64_Ehdr) — тип файла, архитектура, точка входа, расположение таблицы секций и таблицы сегментов. - Секции (sections) — логические блоки, используемые линковщиком (описываются таблицей
SHT). - Сегменты (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¶
file¶
objcopy¶
objcopy копирует ELF-файлы, позволяя выбирать/удалять секции и менять формат.
Получить «голый» бинарный дамп (например, для прошивки микроконтроллера):
Извлечь только секцию .text:
Удалить отладочную информацию:
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 — тогда все
адреса разрешаются при старте программы (медленнее запуск, но нет отложенных ошибок при отсутствующих символах).
Связанные темы¶
- Символы и манглирование — таблица символов
.symtab/.dynsym, видимость, weak/strong - Запуск и завершение программы — как ядро и динамический линковщик используют ELF при запуске
- Стадии сборки — на каком шаге создаются объектные файлы и исполняемые ELF
Источники¶
man readelf— утилита readelfman objdump— утилита objdumpman nm— утилита nmman objcopy— утилита objcopyman 5 elf— описание структур ELF-формата- System V ABI: ELF Specification