ELF (Executable and Linking Format) — бинарный формат, который ядро Linux использует для исполняемых файлов, разделяемых объектов, релоцируемых объектных файлов и core-дампов. Файл начинается с фиксированного заголовка, за которым следуют две таблицы: program header table описывает сегменты для загрузчика, section header table описывает секции для линковщика и отладчика. Обе таблицы опциональны, но исполняемый файл всегда содержит хотя бы program header table.
ELF header расположен строго по смещению 0 и имеет размер 52 байта для 32-битного формата или 64 байта для 64-битного. Он описывается структурой
Первые 16 байт занимает массив
Остальные поля заголовка:
Если количество program headers превышает
Program header table — массив структур
Структура 64-битного program header:
Обратите внимание: в 64-битном варианте
Типичный текстовый сегмент имеет флаги
Поле
Section header table описывает логическую структуру файла: секции кода, данных, символов, релокаций, строк. Таблица используется линковщиком, отладчиком и утилитами вроде
Каждый элемент — структура
Сегмент может содержать одну или несколько секций. Например, сегмент
При вызове
Для динамически скомпонованных файлов
Утилита
Пример вывода
Здесь видно три загружаемых сегмента: только для чтения (заголовки и
Все структуры следуют правилу «естественного» выравнивания: 4-байтные поля выравниваются по 4-байтной границе, 8-байтные — по 8-байтной.
Эти команды безопасны и не модифицируют файл.
ELF header: точка входа в файл
ELF header расположен строго по смещению 0 и имеет размер 52 байта для 32-битного формата или 64 байта для 64-битного. Он описывается структурой
Elf32_Ehdr или Elf64_Ehdr из заголовочного файла <elf.h>.Первые 16 байт занимает массив
e_ident, который определяет базовые свойства файла:| Индекс | Макрос | Значение |
|---|---|---|
| 0–3 | EI_MAG0–EI_MAG3 | Магическое число: 0x7f 'E' 'L' 'F' |
| 4 | EI_CLASS | Разрядность: ELFCLASS32 или ELFCLASS64 |
| 5 | EI_DATA | Порядок байтов: ELFDATA2LSB (little-endian) или ELFDATA2MSB (big-endian) |
| 6 | EI_VERSION | Версия спецификации (текущая — 1) |
| 7 | EI_OSABI | Целевая ОС/ABI: ELFOSABI_LINUX, ELFOSABI_SYSV, ELFOSABI_FREEBSD и др. |
| 8 | EI_ABIVERSION | Версия ABI |
| 9–15 | EI_PAD | Зарезервировано, заполнено нулями |
Остальные поля заголовка:
e_type— тип файла:ET_EXEC(исполняемый),ET_DYN(разделяемый объект),ET_REL(релоцируемый),ET_CORE(дамп памяти).
e_machine— целевая архитектура:EM_X86_64,EM_386,EM_ARM,EM_AARCH64,EM_MIPSи т. д.
e_entry— виртуальный адрес точки входа. Если файл не имеет точки входа (например, релоцируемый объект), поле равно нулю.
e_phoff— смещение program header table в байтах от начала файла.
e_shoff— смещение section header table.
e_phentsize/e_phnum— размер одного элемента и количество элементов в program header table.
e_shentsize/e_shnum— аналогично для section header table.
e_shstrndx— индекс секции, содержащей таблицу имён секций.
Если количество program headers превышает
PN_XNUM (0xffff), поле e_phnum содержит 0xffff, а реальное число хранится в sh_info нулевого элемента section header table. Аналогичный механизм применяется для e_shnum при превышении SHN_LORESERVE (0xff00).Program header table: карта сегментов для загрузчика
Program header table — массив структур
Elf32_Phdr или Elf64_Phdr, каждая из которых описывает один сегмент. Сегмент — непрерывная область файла, которая отображается в память при загрузке. Таблица присутствует только в исполняемых файлах и разделяемых объектах.Структура 64-битного program header:
C:
typedef struct {
uint32_t p_type;
uint32_t p_flags;
Elf64_Off p_offset;
Elf64_Addr p_vaddr;
Elf64_Addr p_paddr;
uint64_t p_filesz;
uint64_t p_memsz;
uint64_t p_align;
} Elf64_Phdr;
Обратите внимание: в 64-битном варианте
p_flags стоит вторым полем, тогда как в 32-битном — седьмым. Это единственное структурное различие между Elf32_Phdr и Elf64_Phdr.Типы сегментов
p_type | Назначение |
|---|---|
PT_LOAD | Загружаемый сегмент. Байты из файла отображаются в память начиная с p_vaddr. Если p_memsz > p_filesz, разница заполняется нулями (типично для .bss). |
PT_DYNAMIC | Указывает на секцию .dynamic с информацией для динамического линковщика. |
PT_INTERP | Содержит путь к интерпретатору (обычно /lib64/ld-linux-x86-64.so.2). Встречается не более одного раза и должен предшествовать загружаемым сегментам. |
PT_NOTE | Секция заметок (например, .note.gnu.build-id). |
PT_PHDR | Описывает расположение самой program header table в памяти. |
PT_GNU_STACK | Расширение GNU: ядро Linux использует флаги этого сегмента для определения исполняемости стека. Если PF_X установлен — стек исполняемый. |
PT_NULL | Неиспользуемый элемент, игнорируется. |
Флаги сегментов
PF_R— чтение.
PF_W— запись.
PF_X— исполнение.
Типичный текстовый сегмент имеет флаги
PF_X | PF_R, сегмент данных — PF_W | PF_R.Выравнивание
Поле
p_align задаёт выравнивание сегмента в файле и в памяти. Для загружаемых сегментов должно выполняться условие: p_vaddr ≡ p_offset (mod p_align). Значения 0 и 1 означают отсутствие требований к выравниванию. В остальных случаях p_align должно быть степенью двойки.Section header table: карта секций для инструментов
Section header table описывает логическую структуру файла: секции кода, данных, символов, релокаций, строк. Таблица используется линковщиком, отладчиком и утилитами вроде
objdump и readelf, но не требуется для загрузки процесса ядром.Каждый элемент — структура
Elf32_Shdr или Elf64_Shdr. Ключевые поля:sh_name— индекс в строковой таблице имён секций (.shstrtab).
sh_type— тип секции:SHT_PROGBITS(данные программы),SHT_SYMTAB(таблица символов),SHT_STRTAB(строковая таблица),SHT_RELA(релокации с аддендом),SHT_NOBITS(секция не занимает места в файле, например.bss).
sh_flags— флаги:SHF_WRITE(записываемая),SHF_ALLOC(занимает память при выполнении),SHF_EXECINSTR(содержит исполняемый код).
sh_addr— виртуальный адрес секции в памяти (еслиSHF_ALLOCустановлен).
sh_offset— смещение в файле.
sh_size— размер в байтах.
sh_link,sh_info— семантика зависит от типа секции.
Связь секций и сегментов
Сегмент может содержать одну или несколько секций. Например, сегмент
PT_LOAD с флагами PF_X | PF_R обычно включает секции .text и .rodata. Сегмент PT_LOAD с флагами PF_W | PF_R включает .data и .bss. Секция .bss имеет тип SHT_NOBITS: она не занимает места в файле, но занимает память при загрузке.Типичные секции исполняемого файла
| Секция | Тип | Назначение |
|---|---|---|
.text | SHT_PROGBITS, SHF_ALLOC | SHF_EXECINSTR | Машинный код |
.rodata | SHT_PROGBITS, SHF_ALLOC | Константы, строковые литералы |
.data | SHT_PROGBITS, SHF_ALLOC | SHF_WRITE | Инициализированные глобальные переменные |
.bss | SHT_NOBITS, SHF_ALLOC | SHF_WRITE | Неинициализированные глобальные переменные |
.dynamic | SHT_DYNAMIC | Записи для динамического линковщика |
.symtab | SHT_SYMTAB | Таблица символов (может отсутствовать в stripped-бинарниках) |
.strtab | SHT_STRTAB | Строки имён символов |
.shstrtab | SHT_STRTAB | Строки имён секций |
.interp | SHT_PROGBITS | Путь к динамическому линковщику |
.note.gnu.build-id | SHT_NOTE | Уникальный идентификатор сборки |
.plt / .got | SHT_PROGBITS | Таблицы для ленивого связывания внешних функций |
Как ядро Linux загружает ELF-файл
При вызове
execve() ядро передаёт управление функции load_elf_binary() в fs/binfmt_elf.c. Последовательность:- Проверка магического числа. Ядро читает первые 16 байт и проверяет
\x7fELF, класс, порядок байтов иEI_OSABI.
- Чтение ELF header. Извлекаются
e_phoff,e_phnum,e_phentsize,e_entry,e_machine.
- Проверка архитектуры. Значение
e_machineсравнивается с поддерживаемыми для текущего ядра.
- Обход program header table. Ядро ищет элементы
PT_LOADи отображает каждый в память черезelf_map()(обёртка надmmap). Для каждого сегмента вычисляются адрес, размер и права доступа на основеp_flags.
- Обработка
PT_INTERP. Если сегмент присутствует, ядро загружает указанный интерпретатор (динамический линковщик) и передаёт ему управление вместо прямого перехода наe_entry.
- Обработка
PT_GNU_STACK. Флаги этого сегмента определяют, будет ли стек отображён с правом исполнения. ЕслиPF_Xотсутствует, стек становится неисполняемым (защита от shellcode на стеке).
- Настройка стека. Ядро формирует начальный стек: аргументы
argv, переменные окруженияenvp, вспомогательный векторauxv(содержитAT_PHDR,AT_ENTRY,AT_PAGESZи другие записи).
- Передача управления. Регистр инструкций устанавливается на
e_entry(или на точку входа интерпретатора, если используетсяPT_INTERP).
Роль динамического линковщика
Для динамически скомпонованных файлов
e_entry указывает на код внутри ld-linux-*.so. Интерпретатор выполняет релокации, загружает зависимые разделяемые библиотеки, перечисленные в .dynamic (записи DT_NEEDED), и только затем передаёт управление реальной точке входа программы.Практический разбор через readelf
Утилита
readelf из binutils позволяет inspect любой ELF-файл без запуска:
Bash:
## ELF header
readelf -h /usr/bin/ls
## Program headers (сегменты)
readelf -l /usr/bin/ls
## Section headers (секции)
readelf -S /usr/bin/ls
## Динамическая секция
readelf -d /usr/bin/ls
## Символы (если не stripped)
readelf -s /usr/bin/ls
Пример вывода
readelf -l для типичного 64-битного исполняемого файла:
Код:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x0001f8 0x0001f8 R 0x8
INTERP 0x000238 0x0000000000000238 0x0000000000000238 0x00001c 0x00001c R 0x1
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x003a20 0x003a20 R 0x1000
LOAD 0x004000 0x0000000000004000 0x0000000000004000 0x012345 0x012345 R E 0x1000
LOAD 0x017000 0x0000000000017000 0x0000000000017000 0x000abc 0x000abc RW 0x1000
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
Здесь видно три загружаемых сегмента: только для чтения (заголовки и
.rodata), исполняемый (.text), и читаемо-записываемый (.data). Сегмент GNU_STACK без PF_X означает неисполняемый стек.Различия между 32-битным и 64-битным ELF
| Параметр | ELF32 | ELF64 |
|---|---|---|
Размер ElfN_Ehdr | 52 байта | 64 байта |
Размер ElfN_Phdr | 32 байта | 56 байт |
Позиция p_flags в Phdr | 7-е поле | 2-е поле |
| Адреса и смещения | 32 бита | 64 бита |
| Максимальный размер файла/адресного пространства | 4 ГБ | Теоретически 16 ЭБ |
Все структуры следуют правилу «естественного» выравнивания: 4-байтные поля выравниваются по 4-байтной границе, 8-байтные — по 8-байтной.
Типичные ошибки и аномалии при анализе
- Отсутствие section header table. Некоторые бинарники (особенно packed или специально подготовленные) не содержат секционной таблицы. Ядро при загрузке её не использует, поэтому файл остаётся исполняемым.
readelf -Sпокажет пустой вывод.
- Несколько
PT_LOADс пересекающимися адресами. Формально допустимо, но может указывать на packer или ручную модификацию.
PT_GNU_STACKсPF_X. Означает исполняемый стек. В современных дистрибутивах это редкость и обычно результат явного указания-z execstackпри компиляции.
e_entryравен нулю. Допустимо для релоцируемых объектов (ET_REL), но дляET_EXECилиET_DYNэто аномалия.
- Несоответствие
e_machineи архитектуры ядра. Ядро вернётENOEXECпри попытке запуска.
Проверка формата без запуска
Bash:
## Быстрая идентификация через file(1)
file /usr/bin/ls
## Вывод: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),
## dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, ...
## Проверка магического числа вручную
xxd -l 16 /usr/bin/ls
## 00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000
## Проверка прав сегментов на исполняемость стека
readelf -l /usr/bin/ls | grep GNU_STACK
Эти команды безопасны и не модифицируют файл.
