В современных POSIX-системах основным форматом исполняемых файлов, объектных файлов, динамических библиотек является формат ELF. Этот формат используется и на 32-битных (Elf32), и на 64-битных (Elf64) системах и для машин с порядком байт Little-endian, и для машин с порядком байт Big-endian. Далее приведено краткое описание формата Elf32. Формат Elf64 отличается размерами полей, содержащих виртуальные адреса, размеры и смещения в файле.
В описании формата будут использоваться типы данных [u]intN_t, где u является признаком беззнаковости, а N определяет размер типа, например, uint16_t. Эти типы определены в стандартном заголовочном файле stdint.h.
#include <stdint.h>
Все типы данных и константы описаны в заголовочном файле elf.h.
В начале файла (со смещения 0 от начала) идет заголовок ELF-файла, описываемый следующей структурой:
typedef struct { unsigned char e_ident[16]; uint16_t e_type; uint16_t e_machine; uint32_t e_version; uint32_t e_entry; uint32_t e_phoff; uint32_t e_shoff; uint32_t e_flags; uint16_t e_ehsize; uint16_t e_phentsize; uint16_t e_phnum; uint16_t e_shentsize; uint16_t e_shnum; uint16_t e_shstrndx; } Elf32_Ehdr;
Структура определена таким образом, что поля структуры выровнены по естественным для данной архитектуры правилам выравнивания (то есть 16-битные поля располагаются по четным адресам, а 32-битные - по адресам кратным 4), а полный размер структуры кратен 4 байтам. 16- и 32-битные значения представлены в порядке байт, естественном для соответствующей архитектуры.
Поле e_ident содержит идентификационную информацию о файле. Поле представляет собой массив байт для того, чтобы иметь одинаковое представление на архитектурах с разным размером слова и разным порядком байт в слове. Элементы массива имеют следующее назначение:
Элемент | Значение | Описание |
---|---|---|
e_ident[0] | '\x7f' | "Магическое" значение |
e_ident[1] | 'E' | "Магическое" значение |
e_ident[2] | 'L' | "Магическое" значение |
e_ident[3] | 'F' | "Магическое" значение |
e_ident[4] | 1 | Размер слова: 0 - неизвестно, 1 - 32, 2 - 64 |
e_ident[5] | 1 | Порядок байт: 0 - неизвестно, 1 - little-endian, 2 - big-endian |
e_ident[6] | 1 | Версия формата ELF: 0 - неизвестно, 1 - текущая версия |
e_ident[7] | 0 | ОС и бинарный интерфейс, для Linux - 0 |
e_ident[8] | 0 | Версия бинарного интерфейса, для Linux - 0 |
e_ident[9] - e_ident[15] | 0 | Зарезервировано |
В дальнейшем будут приводиться значения констант для ОС Linux на архитектуре i386. За значениями констант для других операционных систем или архитектур обращайтесь к документации.
Поле e_type идентифицирует тип файла: 0 (неизвестно), 1 (объектный файл), 2 (исполняемый файл), 3 (разделяемая библиотека), 4 (core-файл).
Поле e_machine идентифицирует тип процессора: 0 (неизвестно), 3 (Intel 80386 и совместимые).
Поле e_version идентифицирует версию файла: 0 (недопустимая версия), 1 (текущая версия).
Поле e_entry определяет виртуальный адрес точки входа в программу. После загрузки программы в память управление передается на этот адрес.
Поле e_phoff задает смещение от начала файла до начала таблицы заголовков программы (program header table). Информация о таблице заголовков программы будет дана ниже.
Поле e_shoff задает смещение от начала файла до начала таблицы заголовков секций (program section table). Информация о таблице заголовков секций будет дана ниже.
Поле e_flags задает дополнительные процессорно-специфичные флаги. В настоящее время значение данного поля должно всегда быть 0.
Поле e_ehsize хранит размер заголовка ELF-файла. Его значение должно быть равно 52 (sizeof(Elf32_Ehdr)).
Поле e_phentsize хранит размер одной записи в таблице заголовков программы. Его значение должно быть 32 (sizeof(Elf32_Phdr)) или 0, если таблица заголовков программы пуста.
Поле e_phnum хранит количество записей в таблице заголовков программы.
Поле e_shentsize хранит размер одной записи в таблице заголовков секций. Его значение должно быть равно 40 (sizeof(Elf32_Shdr)) или 0, если таблица заголовков секций пуста.
Поле e_shnum хранит количество записей в таблице заголовков секций.
Поле e_shstrndx хранит индекс заголовка секции, которая хранит имена всех секций (см. ниже).
Информация, хранящаяся в ELF-файле, организована в секции. Каждая секция имеет свое уникальное имя. Некоторые секции хранят служебную информацию ELF-файла (например, таблицы строк), другие секции хранят отладочную информацию, третьи секции хранят код или данные программы.
Таблица заголовков секций представляет собой массив структур Elf32_Shdr. Количество элементов массива определяется полем e_shnum заголовка ELF-файла. Массив находится по смещению, хранящемуся в поле e_shoff. Элемент массива 0 зарезервирован и не используется для описания секций. Таким образом, описания секций находятся в элементах массива с индексами от 1 и до e_shnum - 1.
Структура Elf32_Shdr определена следующим образом:
typedef struct { uint32_t sh_name; uint32_t sh_type; uint32_t sh_flags; uint32_t sh_addr; uint32_t sh_offset; uint32_t sh_size; uint32_t sh_link; uint32_t sh_info; uint32_t sh_addralign; uint32_t sh_entsize; } Elf32_Shdr;
Поле sh_name хранит индекс имени секции. Индекс имени - это смещение в данных секции, индекс которой задается в поле e_shstrndx заголовка ELF-файла. По этому смещению размещается строка, завершающаяся нулевым байтом, являющаяся именем секции.
Таким образом, чтобы получить имя секции необходимо выполнить следующие действия:
Поле sh_type хранит тип секции. Возможные значения поля перечислены ниже.
Значение | Симв. имя | Описание |
---|---|---|
0 | SHT_NULL | Пустой заголовок секции. Значения всех прочих полей заголовка секции неопределены. |
1 | SHT_PROGBITS | Секции программы (код или данные или что-либо еще). |
2 | SHT_SYMTAB | Таблица символов (для объектных файлов или динамических библиотек). |
3 | SHT_STRTAB | Таблица строк. |
4 | SHT_RELA | Записи о перемещаемых адресах (relocations). |
5 | SHT_HASH | Хеш-таблица имен для динамического связывания. |
6 | SHT_DYNAMIC | Информация для динамического связывания. |
7 | SHT_NOTE | Произвольная дополнительная информация. |
8 | SHT_NOBITS | Секция не занимает место в файле, но занимает место в адресном пространстве процесса. |
9 | SHT_REL | Записи о перемещаемых адресах. |
Поле sh_flags хранит битовые флаги, описывающие дополнительные атрибуты.
Значение | Симв. константа | Описание |
---|---|---|
1 | SHF_WRITE | Содержимое секции должно быть доступно на запись в адресном пространстве процесса. |
2 | SHF_ALLOC | Для содержимого секции выделяется память в адресном пространстве процесса. |
4 | SHF_EXECINSTR | Секция содержит инструкции процессора. |
Флаги могут комбинироваться с помощью операции побитового или.
Поле sh_addr хранит адрес в виртуальном адресном пространстве процесса в случае, если секция загружается в виртуальное адресное пространство процесса.
Поле sh_offset хранит смещение от начала файла, по которому размещаются данные секции.
Поле sh_size хранит размер секции в байтах.
Поле sh_link хранит индекс другой секции (в некоторых специальных случаях).
Поле sh_info хранит дополнительную информацию о секции.
Поле sh_addralign хранит требование по выравниванию адреса начала секции в памяти. Значения 0 или 1 означают отсутствие требования по выравниванию. В противном случае значением поля должна быть степень 2. Например, секции, загружаемые в виртуальное адресное пространство процесса, как правило, выровнены по размеру страницы процессора (4096).
Поле sh_entsize хранит размер одной записи, если секция хранит таблицу из записей фиксированного размера.
Таблица заголовков программы содержит информацию, необходимую для загрузки программы на выполнение.
Таблица заголовков программы представляет собой массив структур Elf32_Phdr. Массив размещается по смещению от начала файла, которое хранится в поле e_phoff заголовка ELF-файла, а количество элементов массива хранится в поле e_phnum заголовка ELF-файла.
Структура Elf32_Phdr определена следующим образом.
typedef struct { uint32_t p_type; Elf32_Off p_offset; Elf32_Addr p_vaddr; Elf32_Addr p_paddr; uint32_t p_filesz; uint32_t p_memsz; uint32_t p_flags; uint32_t p_align; } Elf32_Phdr;
Поле p_type хранит тип заголовка. Некоторые возможные значения типа заголовка приведены в таблице ниже.
Значение | Симв. константа | Описание |
---|---|---|
0 | PT_NULL | Обозначает не используемую запись |
1 | PT_LOAD | Сегмент программы, загружаемый в память |
2 | PT_DYNAMIC | Информация для динамического связывания |
3 | PT_INTERP | Загрузчик программ |
4 | PT_NOTE | Дополнительная информация |
6 | PT_PHDR | Информация о самой таблице заголовков программы |
7 | PT_TLS | Thread-local storage |
Поле p_offset хранит смещение от начала файла, по которому располагается данный сегмент.
Поле p_vaddr хранит виртуальный адрес начала сегмента в памяти.
Значение поля p_paddr должно быть равно 0.
Поле p_filesz хранит размер сегмента в файле (может быть 0).
Поле p_memsz хранит размер сегмента в памяти (может быть 0).
Поле p_flags хранит флаги доступа к сегменту в памяти (могут объединяться с помощью побитового "или").
Значение | Симв. константа | Описание |
---|---|---|
1 | PT_X | Сегмент доступен на выполнение |
2 | PT_W | Сегмент доступен на запись |
4 | PT_R | Сегмент доступен на чтение |
В сегменте ELF-файла с типом PT_NOTE хранится дополнительная информация о состоянии выполнения программы. Сегмент сам содержит произвольное количество записей произвольного размера. Сегмент всегда имеет размер, кратный 4 байтам (для Elf32), и каждая запись в сегменте начинается со смещения, кратного 4 байтам. В начале каждой записи находится заголовок записи, описываемый следующей структурой:
typedef struct { Elf32_Word n_namesz; Elf32_Word n_descsz; Elf32_Word n_type; } Elf32_Nhdr;
Поле n_namesz содержит длину названия записи. Название должно быть непустой строкой, завершающейся байтом 0. Сама строка названия записи начинается сразу же после структуры Elf32_Nhdr.
Поле n_descsz содержит длину информационной части записи. Длина должна быть кратна 4 байтам. Информационная часть записи начинается сразу после названия записи с учетом выравнивания по границе 4 байт.
Поле n_type содержит тип записи. Возможные типы записи зависят от типа файла (объектный, core) и рассматриваются в соответствующих разделах.
Ниже приведен пример сегмента PT_NOTE.
См. от начала | Значение | Комментарий | |||
---|---|---|---|---|---|
0x0000 | 0x07 | 0x00 | 0x00 | 0x00 | n_namesz == 7 |
0x0004 | 0x08 | 0x00 | 0x00 | 0x00 | n_descsz == 8 |
0x0008 | 0x01 | 0x00 | 0x00 | 0x00 | n_type == 1 |
0x000c | 'G' | 'N' | 'U' | 'D' | имя записи: "GNUDBG" |
0x0010 | 'B' | 'G' | '\0' | 0x00 | |
0x0014 | 0x01 | 0x02 | 0x03 | 0x04 | информация: 0x04030201 0x08070605 |
0x0018 | 0x05 | 0x06 | 0x07 | 0x08 |
core-файл - это ELF-файл, у которого значение поля e_type заголовка равно ET_CORE (4). В core-файле таблица заголовков секций пуста, а таблица заголовков программ состоит из записей типа PT_LOAD, хранящих содержимое адресного пространства процесса на момент завершения работы процесса, и записи типа PT_NOTE, хранящей состояние процесса на момент завершения работы процесса.
Для тестового файла sample.core содержимое таблицы заголовков программ имеет следующий вид.
p_type | p_flags | p_offset | p_vaddr | p_filesz | p_memsz | p_align |
---|---|---|---|---|---|---|
PT_NOTE | --- | 0x00000234 | 0x00000000 | 1216 | 0 | 0 |
PT_LOAD | r-x | 0x00001000 | 0x08048000 | 4096 | 8192 | 4096 |
PT_LOAD | rw- | 0x00002000 | 0x0804a000 | 4096 | 4096 | 4096 |
PT_LOAD | rw- | 0x00003000 | 0x08542000 | 135168 | 135168 | 4096 |
PT_LOAD | r-x | 0x00024000 | 0x4f2d0000 | 4096 | 126976 | 4096 |
PT_LOAD | r-- | 0x00025000 | 0x4f2ef000 | 4096 | 4096 | 4096 |
PT_LOAD | rw- | 0x00026000 | 0x4f2f0000 | 4096 | 4096 | 4096 |
PT_LOAD | r-x | 0x00027000 | 0x4f2f7000 | 4096 | 1748992 | 4096 |
PT_LOAD | --- | 0x00028000 | 0x4f4a2000 | 0 | 4096 | 4096 |
PT_LOAD | r-- | 0x00028000 | 0x4f4a3000 | 8192 | 8192 | 4096 |
PT_LOAD | rw- | 0x0002a000 | 0x4f4a5000 | 4096 | 4096 | 4096 |
PT_LOAD | rw- | 0x0002b000 | 0x4f4a6000 | 12288 | 12288 | 4096 |
PT_LOAD | rw- | 0x0002e000 | 0xb778a000 | 4096 | 4096 | 4096 |
PT_LOAD | rw- | 0x0002f000 | 0xb77a1000 | 8192 | 8192 | 4096 |
PT_LOAD | r-x | 0x00031000 | 0xb77a3000 | 4096 | 4096 | 4096 |
PT_LOAD | rw- | 0x00032000 | 0xbfe6d000 | 139264 | 139264 | 4096 |
Первый сегмент (сегмент PT_NOTE) содержит информацию о состоянии процесса на момент создания core-файла.
Для core-файлов возможны следующие значения поля n_type (в таблице ниже перечислены не все возможные значения).
Значение | Симв. константа | Описание |
---|---|---|
1 | NT_PRSTATUS | Информационная часть записи имеет тип prstatus_t |
2 | NT_FPREGSET | Информационная часть записи имеет тип prfpregset_t |
3 | NT_PRPSINFO | Информационная часть записи имеет тип prpsinfo_t |
Типы структур, используемые в информационных частях записей, определены в заголовочном файле
#include <sys/procfs.h>
Структура prstatus_t определена следующим образом:
typedef struct elf_prstatus { struct elf_siginfo pr_info; // информация о сигналах short int pr_cursig; // текущий сигнал unsigned long int pr_sigpend; // множество сигналов, ожидающих доставки unsigned long int pr_sighold; // множество удерживаемых сигналов pid_t pr_pid; // pid процесса pid_t pr_ppid; // pid родителя pid_t pr_pgrp; // группа процессов pid_t pr_sid; // идентификатор сессии struct timeval pr_utime; // пользовательское время struct timeval pr_stime; // системное время struct timeval pr_cutime; // накопленное пользовательское время struct timeval pr_cstime; // накопленное системное время elf_gregset_t pr_reg; // регистры общего назначения int pr_fpvalid; // true, если использовались регистры FPU } prstatus_t;
Тип elf_gregset_t определен следующим образом:
typedef unsigned long elf_gregset_t[ELF_NGREG];
то есть представляет собой массив, в котором каждый регистр общего назначения находится по определенному индексу.
индекс | регистр |
---|---|
0 | ebx |
1 | ecx |
2 | edx |
3 | esi |
4 | edi |
5 | ebp |
6 | eax |
7 | ds |
8 | es |
9 | fs |
10 | gs |
11 | orig_eax |
12 | eip |
13 | cs |
14 | eflags |
15 | sp |
16 | ss |
При компиляции в исполняемый файл может добавляться отладочная информация, которую отладчик использует для отображения хода исполнения программы в терминах языка высокого уровня. Существует несколько форматов отладочной информации (STABS, DWARF), здесь описывается формат STABS как самый простой.
Для компиляции программы с добавлением отладочной информации в формате STABS используется опция gcc -gstabs, например
gcc -gstabs -std=gnu11 sample.c -o sample
В формате STABS отладочная информация хранится в секциях .stab и .stabstr ELF-файла.
Секция .stab содержит массив структур:
struct Stab { uint32_t n_strx; // позиция начала строки в секции .strstab uint8_t n_type; // тип отладочного символа uint8_t n_other; // прочая информация uint16_t n_desc; // описание отладочного символа uintptr_t n_value; // значение отладочного символа };
Секция .stabstr хранит символьные строки, завершающиеся байтом 0, которые используются в записях в секции .stab.
Поле n_type хранит тип записи. Возможные типы записей можно найти в stab.h, нас будут интересовать только некоторые из них.
Симв. имя | Значение | Описание |
---|---|---|
N_SO | 0x64 | Информация о единице компиляции: n_desc - язык исходного кода n_strx - индекс в секции .stabstr строки имени основного файла единицы компиляции n_value - адрес первой инструкции |
N_SOL | 0x84 | Имя файла, устанавливаемое с помощью директивы #line или имя файла, включаемого с помощью #include n_strx - индекс в секции .stabstr строки имени файла Будем предполагать, что действие имени файла, устанавливаемого в записи N_SOL, начинается со следующей записи. |
N_FUN | 0x24 | Имя функции n_value - адрес первой инструкции функции n_strx - индекс в секции .stabstr строки имени функции Имя состоит непосредственно из названия и после ‘:’ следуют типы параметров, если таковые есть |
N_SLINE | 0x44 | Номер строки исполняемого кода n_value - смещение первой инструкции строки относительно начала функции n_desc - номер строки в исходном тексте |
Каждой единице компиляции соответствуют две записи N_SO. Первая запись находится в начале описания единицы компиляции и содержит ее имя в поле n_strx и адрес первой инструкции в поле n_value. Вторая запись находится в конце описания единицы компиляции и содержит нулевое значение (пустая строка) в поле n_strx и адрес непосредственно следующий за концом кода данной единицы компиляции в поле n_value.
Записи N_SLINE упорядочены по смещениям внутри функции.
Первая запись в таблице .stab является служебной и имеет тип N_UNDF. Индекс этой записи полагается равным 0.
Формат STABS не позволяет однозначно установить адрес, на котором заканчивается тело функции. Можно использовать следующую эвристику: функция заканчивается либо с началом следующей функции, тогда адрес конца функции - это адрес начала следующей функции, либо с концом единицы компиляции, тогда адрес конца функции - это адрес, хранящийся в поле n_value записи N_SO в конце единицы компиляции.
Как обычно, предполагается, что все диапазоны адресов и значений включают в себя нижнее значение, но не включают в себя верхнее значение, то есть имеют вид [low;high).
Имя файла, в котором располагается функция, может находиться в записи N_SOL после записи N_FUN самой функции, но до первой записи N_SLINE. Следует полагать, что имя файла, в котором определена функция совпадает с именем файла, установленному до первой записи N_SLINE в данной функции.
Пример: файл file1.h:i
#include <stdio.h> void func2(int val) { printf("%d\n", val); }
Файл file1.c
#include "file1.h" int val; int func1(void) { scanf("%d", &val); return val; } int main() { func1(); func2(val); return 0; }
Записи STABS:
SO 0 2 08048430 1 file1.c FUN 0 0 08048430 3880 func2:F(0,18) ... SOL 0 0 08048430 668 file1.h SLINE 0 3 00000000 0 SLINE 0 4 00000006 0 SLINE 0 5 00000019 0 FUN 0 0 0804844b 3905 func1:F(0,1) SOL 0 0 0804844b 1 file1.c SLINE 0 4 00000000 0 SLINE 0 5 00000006 0 SLINE 0 6 0000001a 0 SLINE 0 7 0000001f 0 FUN 0 0 0804846c 3918 main:F(0,1) SLINE 0 10 00000000 0 SLINE 0 11 00000009 0 SLINE 0 12 0000000e 0 SLINE 0 13 0000001b 0 SLINE 0 14 00000020 0 SO 0 0 0804848e 0