Формат ELF изнутри: заголовки, сегменты, секции и загрузка исполняемого файла Linux

ELF (Executable and Linking Format) — бинарный формат, который ядро Linux использует для исполняемых файлов, разделяемых объектов, релоцируемых объектных файлов и core-дампов. Файл начинается с фиксированного заголовка, за которым следуют две таблицы: program header table описывает сегменты для загрузчика, section header table описывает секции для линковщика и отладчика. Обе таблицы опциональны, но исполняемый файл всегда содержит хотя бы program header table.

ELF header: точка входа в файл​


ELF header расположен строго по смещению 0 и имеет размер 52 байта для 32-битного формата или 64 байта для 64-битного. Он описывается структурой Elf32_Ehdr или Elf64_Ehdr из заголовочного файла <elf.h>.

Первые 16 байт занимает массив e_ident, который определяет базовые свойства файла:

ИндексМакросЗначение
0–3EI_MAG0EI_MAG3Магическое число: 0x7f 'E' 'L' 'F'
4EI_CLASSРазрядность: ELFCLASS32 или ELFCLASS64
5EI_DATAПорядок байтов: ELFDATA2LSB (little-endian) или ELFDATA2MSB (big-endian)
6EI_VERSIONВерсия спецификации (текущая — 1)
7EI_OSABIЦелевая ОС/ABI: ELFOSABI_LINUX, ELFOSABI_SYSV, ELFOSABI_FREEBSD и др.
8EI_ABIVERSIONВерсия ABI
9–15EI_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: она не занимает места в файле, но занимает память при загрузке.

Типичные секции исполняемого файла​


СекцияТипНазначение
.textSHT_PROGBITS, SHF_ALLOC | SHF_EXECINSTRМашинный код
.rodataSHT_PROGBITS, SHF_ALLOCКонстанты, строковые литералы
.dataSHT_PROGBITS, SHF_ALLOC | SHF_WRITEИнициализированные глобальные переменные
.bssSHT_NOBITS, SHF_ALLOC | SHF_WRITEНеинициализированные глобальные переменные
.dynamicSHT_DYNAMICЗаписи для динамического линковщика
.symtabSHT_SYMTABТаблица символов (может отсутствовать в stripped-бинарниках)
.strtabSHT_STRTABСтроки имён символов
.shstrtabSHT_STRTABСтроки имён секций
.interpSHT_PROGBITSПуть к динамическому линковщику
.note.gnu.build-idSHT_NOTEУникальный идентификатор сборки
.plt / .gotSHT_PROGBITSТаблицы для ленивого связывания внешних функций

Как ядро Linux загружает ELF-файл​


При вызове execve() ядро передаёт управление функции load_elf_binary() в fs/binfmt_elf.c. Последовательность:

  1. Проверка магического числа. Ядро читает первые 16 байт и проверяет \x7fELF, класс, порядок байтов и EI_OSABI.
  2. Чтение ELF header. Извлекаются e_phoff, e_phnum, e_phentsize, e_entry, e_machine.
  3. Проверка архитектуры. Значение e_machine сравнивается с поддерживаемыми для текущего ядра.
  4. Обход program header table. Ядро ищет элементы PT_LOAD и отображает каждый в память через elf_map() (обёртка над mmap). Для каждого сегмента вычисляются адрес, размер и права доступа на основе p_flags.
  5. Обработка PT_INTERP. Если сегмент присутствует, ядро загружает указанный интерпретатор (динамический линковщик) и передаёт ему управление вместо прямого перехода на e_entry.
  6. Обработка PT_GNU_STACK. Флаги этого сегмента определяют, будет ли стек отображён с правом исполнения. Если PF_X отсутствует, стек становится неисполняемым (защита от shellcode на стеке).
  7. Настройка стека. Ядро формирует начальный стек: аргументы argv, переменные окружения envp, вспомогательный вектор auxv (содержит AT_PHDR, AT_ENTRY, AT_PAGESZ и другие записи).
  8. Передача управления. Регистр инструкций устанавливается на 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​


ПараметрELF32ELF64
Размер ElfN_Ehdr52 байта64 байта
Размер ElfN_Phdr32 байта56 байт
Позиция p_flags в Phdr7-е поле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

Эти команды безопасны и не модифицируют файл.

Источники​


 

Инструменты для анализа ELF​


Классический набор: binutils​


Поставляется почти везде (apt install binutils / dnf install binutils):

  • readelf — самый полный дампер структур: заголовки, program/section headers, символы, релокации, .dynamic. Не требует библиотек BFD, работает даже с битыми/нестандартными файлами. Флаги: -h, -l, -S, -s, -d, -r, -n (notes), -A (атрибуты архитектуры).
  • objdump — дизассемблирование (-d, -D для всех секций), вывод всех заголовков (-x), содержимое секций (-s). Для расширенного набора архитектур нужна сборка с multiarch: objdump -d --start-address=0x... --stop-address=0x... file.
  • nm — список символов: nm -C (demangle C++), nm -D (динамические), nm -u (undefined).
  • strings — извлечение строк: strings -a -t x -n 6 binary (все секции, hex-оффсеты, минимальная длина).
  • size, addr2line, c++filt, ar, strip — вспомогательные.
  • ldd — список зависимых библиотек. Внимание: фактически исполняет загрузчик на файле — для недоверенных бинарников безопаснее readelf -d | grep NEEDED или objdump -p | grep NEEDED.

file и статическая идентификация​


Bash:
file -k binary          # краткое описание, не останавливается на первом совпадении
file --mime-type binary

[HEADING=1]быстрая проверка магии без file(1)[/HEADING]
xxd -l 20 binary

Динамический анализ и отладка​


  • gdb + плагин pwndbg или GEF — лучшие инструменты для интерактивного анализа: отображение заголовков ELF, heap, паттерны поиска, эмуляция. gdb ./binary, info files, maintenance info sections.
  • ltrace — трассировка вызовов библиотечных функций.
  • strace — системные вызовы: strace -f -e trace=file ./binary покажет, какие файлы открываются.
  • LD_DEBUG — трассировка самого динамического линковщика без внешних инструментов:

    Bash:
    LD_DEBUG=libs ./binary    # какие библиотеки и по каким путям ищутся
    LD_DEBUG=symbols ./binary # разрешение символов
    LD_DEBUG=bindings ./binary
  • perf, valgrind — профилирование и проверка памяти (полезно при реверсе поведения).

Дизассемблеры и декомпиляторы​


  • Ghidra (NSA, open source) — полный декомпилятор, поддержка десятков архитектур, headless-режим для автоматизации: analyzeHeadless /tmp/proj proj -import binary.
  • IDA Free — классика, free-версия для некоммерческого использования.
  • Binary Ninja — современный UI, хороший API на Python.
  • Radare2 /rizin — консольный фреймворк:

    Bash:
    r2 -A ./binary
    [HEADING=1]внутри:[/HEADING]
    [HEADING=1]aaa     — автоанализ[/HEADING]
    [HEADING=1]afl     — список функций[/HEADING]
    [HEADING=1]pdf @ main — дизасм функции[/HEADING]
    [HEADING=1]iz      — строки в data-секциях[/HEADING]
    [HEADING=1]ii      — импорты[/HEADING]
  • Cutter — GUI поверх rizin.
  • angr — символьное исполнение и анализ на Python, полезно для CTF.

Программный парсинг ELF​


Когда нужен доступ к структурам из кода:

  • pyelftools (Python) — чтение и разбор ELF:

    Python:
    from elftools.elf.elffile import ELFFile
    
    with open('binary', 'rb') as f:
        elf = ELFFile(f)
        print(elf.header['e_entry'], elf.header['e_machine'])
        for seg in elf.iter_segments():
            print(seg['p_type'], hex(seg['p_vaddr']), seg['p_memsz'])
  • LIEF (Python/C++/Rust) — не только чтение, но и модификация ELF: добавление секций, правка заголовков, парсинг .dynamic. Удобно для экспериментов с форматом в лаборатории.
  • libelf (C) — низкоуровневая библиотека, лежит в основе многих утилит.

Специализированные проверки​


  • checksec (из pwntools или одноимённый скрипт) — состояние защит бинарника:

    Bash:
    checksec --file=./binary
    [HEADING=1]или через pwntools:[/HEADING]
    python3 -c "from pwn import ELF; ELF('./binary').checksec()"

    Показывает: NX, PIE, RELRO, canary, RPATH/RUNPATH, FORTIFY.
  • elfedit — правка полей ELF header (EI_OSABI и т.п.).
  • patchelf — изменение интерпретатора, RPATH, SONAME (например, для изоляции зависимостей в лаборатории): patchelf --set-interpreter /path/ld.so --set-rpath /path/libs ./binary.
  • YARA — сигнатурный поиск по паттернам в бинарниках.
  • Capstone — движок дизассемблирования как библиотека (Python-биндинги), если нужно точечно разбирать байты.

Минимальный набор для быстрого осмотра незнакомого ELF​


Bash:
file binary
readelf -h binary          # тип, архитектура, точка входа
readelf -l binary          # сегменты, NX/PIE
readelf -d binary          # зависимости, RPATH (вместо ldd)
checksec --file=binary     # защиты
strings -a -n 8 binary | head -100
nm -D binary               # импорты/экспорты

Все команды статические и не исполняют анализируемый файл. Для недоверенных образцов — запускать только в изолированной VM/sandbox (qemu-user, отдельный контейнер с --network none, или full VM со снапшотом).

Для глубокой задачи обычно связка: readelf/checksec → Ghidra или IDA для декомпиляции → gdb+pwndbg для динамической проверки гипотез.
 
Вот компактная реализация парсера ELF на C, которая читает заголовок, program headers и section headers. Код использует стандартные структуры из <elf.h> и работает с 32- и 64-битными файлами (little-endian, как обычно на x86/ARM). Для простоты файл отображается через mmap, что удобно для произвольного доступа.

C:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <elf.h>

/* Вывод полей ELF header */
static void dump_ehdr(const Elf64_Ehdr *eh) {
    printf("ELF header:\n");
    printf("  Class:      %s\n", eh->e_ident[EI_CLASS] == ELFCLASS64 ? "ELF64" : "ELF32");
    printf("  Data:       %s\n", eh->e_ident[EI_DATA] == ELFDATA2LSB ? "little-endian" : "big-endian");
    printf("  Version:    %u\n", eh->e_ident[EI_VERSION]);
    printf("  OS/ABI:     %u\n", eh->e_ident[EI_OSABI]);
    printf("  Type:       %u\n", eh->e_type);
    printf("  Machine:    %u\n", eh->e_machine);
    printf("  Entry:      0x%lx\n", (unsigned long)eh->e_entry);
    printf("  Phoff:      0x%lx\n", (unsigned long)eh->e_phoff);
    printf("  Shoff:      0x%lx\n", (unsigned long)eh->e_shoff);
    printf("  Phnum:      %u\n", eh->e_phnum);
    printf("  Shnum:      %u\n", eh->e_shnum);
    printf("  Shstrndx:   %u\n", eh->e_shstrndx);
}

/* Вывод program headers */
static void dump_phdrs(const Elf64_Ehdr *eh, const uint8_t *base) {
    if (eh->e_phoff == 0 || eh->e_phnum == 0) {
        printf("No program headers.\n");
        return;
    }
    printf("\nProgram headers:\n");
    printf("%-8s %-10s %-10s %-10s %-10s %-10s %-3s %s\n",
           "Type", "Offset", "VirtAddr", "PhysAddr", "FileSiz", "MemSiz", "Flg", "Align");
    for (int i = 0; i < eh->e_phnum; i++) {
        const Elf64_Phdr *ph = (const Elf64_Phdr *)(base + eh->e_phoff + i * eh->e_phentsize);
        printf("%-8u 0x%-8lx 0x%-8lx 0x%-8lx 0x%-8lx 0x%-8lx %-3x 0x%-2lx\n",
               ph->p_type,
               (unsigned long)ph->p_offset,
               (unsigned long)ph->p_vaddr,
               (unsigned long)ph->p_paddr,
               (unsigned long)ph->p_filesz,
               (unsigned long)ph->p_memsz,
               ph->p_flags,
               (unsigned long)ph->p_align);
    }
}

/* Вывод section headers */
static void dump_shdrs(const Elf64_Ehdr *eh, const uint8_t *base) {
    if (eh->e_shoff == 0 || eh->e_shnum == 0) {
        printf("No section headers.\n");
        return;
    }
    /* Находим строковую таблицу имён секций */
    const Elf64_Shdr *shstr = (const Elf64_Shdr *)(base + eh->e_shoff + eh->e_shstrndx * eh->e_shentsize);
    const char *shstrtab = (const char *)(base + shstr->sh_offset);

    printf("\nSection headers:\n");
    printf("%-20s %-10s %-10s %-10s %-10s %-10s %s\n",
           "Name", "Type", "Addr", "Off", "Size", "Flags", "Link");
    for (int i = 0; i < eh->e_shnum; i++) {
        const Elf64_Shdr *sh = (const Elf64_Shdr *)(base + eh->e_shoff + i * eh->e_shentsize);
        const char *name = (sh->sh_name < shstr->sh_size) ? shstrtab + sh->sh_name : "?";
        printf("%-20s %-10u 0x%-8lx 0x%-8lx 0x%-8lx %-10lx %u\n",
               name,
               sh->sh_type,
               (unsigned long)sh->sh_addr,
               (unsigned long)sh->sh_offset,
               (unsigned long)sh->sh_size,
               (unsigned long)sh->sh_flags,
               sh->sh_link);
    }
}

int main(int argc, char **argv) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <elf-file>\n", argv[0]);
        return 1;
    }

    int fd = open(argv[1], O_RDONLY);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    struct stat st;
    if (fstat(fd, &st) != 0) {
        perror("fstat");
        close(fd);
        return 1;
    }

    uint8_t *base = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (base == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    /* Проверка магии */
    if (st.st_size < EI_NIDENT || memcmp(base, ELFMAG, SELFMAG) != 0) {
        fprintf(stderr, "Not an ELF file.\n");
        munmap(base, st.st_size);
        close(fd);
        return 1;
    }

    /* Определяем класс и используем соответствующие структуры */
    if (base[EI_CLASS] == ELFCLASS64) {
        const Elf64_Ehdr *eh = (const Elf64_Ehdr *)base;
        dump_ehdr(eh);
        dump_phdrs(eh, base);
        dump_shdrs(eh, base);
    } else if (base[EI_CLASS] == ELFCLASS32) {
        /* Для 32-битных можно аналогично использовать Elf32_* структуры */
        const Elf32_Ehdr *eh = (const Elf32_Ehdr *)base;
        printf("ELF32 header:\n");
        printf("  Entry: 0x%x\n", eh->e_entry);
        printf("  Phnum: %u, Shnum: %u\n", eh->e_phnum, eh->e_shnum);
        /* Здесь можно добавить полный вывод, но для краткости пропустим */
    } else {
        fprintf(stderr, "Unknown ELF class.\n");
    }

    munmap(base, st.st_size);
    close(fd);
    return 0;
}

Компиляция и запуск:

Bash:
gcc -o elfparse elfparse.c
./elfparse /usr/bin/ls

Ключевые моменты:

  • Используется mmap — файл отображается в память, доступ к структурам идёт через указатели.
  • Проверяется магическое число \x7fELF.
  • Для 64-битных файлов используются Elf64_* структуры, для 32-битных — Elf32_* (в примере показан только заголовок, полный разбор аналогичен).
  • e_phentsize и e_shentsize учитываются при итерации, что корректно работает даже с нестандартными размерами записей.
  • Строковая таблица имён секций находится через e_shstrndx.

Расширения:

  • Добавить поддержку big-endian (проверять EI_DATA и при необходимости менять порядок байтов).
  • Выводить имена типов сегментов/секций вместо чисел (например, PT_LOAD, SHT_PROGBITS).
  • Разбирать динамическую секцию, символы, релокации.
  • Использовать pread вместо mmap, если нужно обрабатывать файлы больше размера адресного пространства.

Этот код — учебный пример; для реального анализа лучше применять readelf, pyelftools или LIEF, но написание собственного парсера помогает глубоко понять структуру формата.
 
Назад
Верх Низ