Исполняемый файл содержит машинный код, данные и сведения, необходимые для запуска программы. По этим сведениям операционная система формирует виртуальное адресное пространство процесса и определяет, куда передать управление. Если программа использует разделяемые библиотеки, в запуске участвует динамический загрузчик.
Формат исполняемого файла определяет, как организованы код, данные и служебные структуры. В Linux используется ELF; исторически применялся a.out. В Windows используется PE, а COM относится к старым форматам DOS. Скрипты с начальной последовательностью #! запускаются через указанный интерпретатор, а не как машинный код.
ELF-файл начинается с заголовка. Он позволяет определить формат файла, архитектуру процессора, разрядность, порядок байтов, точку входа и расположение таблиц, описывающих содержимое файла.
Основные поля заголовка Elf64_Ehdr:
| Поле | Назначение |
|---|---|
e_ident |
Идентификационные байты: сигнатура ELF и параметры представления данных |
e_type |
Тип ELF-объекта |
e_machine |
Архитектура процессора |
e_version |
Версия формата |
e_entry |
Виртуальный адрес точки входа |
e_phoff |
Смещение таблицы программных заголовков от начала файла |
e_shoff |
Смещение таблицы заголовков секций от начала файла |
e_flags |
Архитектурно-зависимые флаги |
e_ehsize |
Размер заголовка ELF |
e_phentsize, e_phnum |
Размер одной записи и число записей в таблице программных заголовков |
e_shentsize, e_shnum |
Размер одной записи и число записей в таблице заголовков секций |
e_shstrndx |
Индекс секции с таблицей строк, содержащей имена секций |
В ELF64 используются типы фиксированной ширины: Elf64_Half — 16 бит, Elf64_Word — 32 бита, Elf64_Addr, Elf64_Off и Elf64_Xword — 64 бита. Elf64_Addr обозначает адрес, а Elf64_Off — смещение внутри файла. Эти величины нельзя путать: положение данных на диске и их адрес в памяти — разные характеристики.
Посмотреть заголовок:
readelf -h ./hello
В примере из лекции команда выводит:
Class: ELF64
Data: 2's complement, little endian
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Entry point address: 0x540
Сигнатура ELF начинается с байтов 7f 45 4c 46: байт 0x7f и символы ELF. Поле Class показывает разрядность, Data — порядок байтов, Machine — целевую архитектуру. В рассматриваемом файле точка входа указана как 0x540.
Исполняемый файл описывает, какие области виртуального адресного пространства нужны процессу, где должны находиться код и данные и из каких частей файла их следует получить.
Для загрузки используются программные заголовки — записи Elf64_Phdr. Они описывают сегменты. Секции и сегменты — не одно и то же: у ELF есть отдельная таблица заголовков секций, а размещение содержимого в памяти описывается таблицей программных заголовков.
Поле Elf64_Phdr |
Назначение |
|---|---|
p_type |
Тип сегмента |
p_flags |
Права доступа: чтение, запись, выполнение |
p_offset |
Смещение содержимого сегмента в файле |
p_vaddr |
Виртуальный адрес сегмента |
p_paddr |
Поле физического адреса |
p_filesz |
Размер содержимого сегмента в файле |
p_memsz |
Размер сегмента в памяти |
p_align |
Требуемое выравнивание |
Загружаемые сегменты имеют тип PT_LOAD, числовое значение которого равно 1. Флаги определяют допустимые операции с памятью: R — чтение, W — запись, E — выполнение.
Посмотреть программные заголовки:
readelf -l ./hello
В примере присутствуют два сегмента LOAD:
| Сегмент | Смещение в файле | Виртуальный адрес | Размер в файле | Размер в памяти | Права |
|---|---|---|---|---|---|
| Первый | 0x0 |
0x0 |
0x858 |
0x858 |
R E |
| Второй | 0xdb8 |
0x200db8 |
0x258 |
0x260 |
RW |
Первый сегмент допускает чтение и выполнение, второй — чтение и запись. Их содержимое находится в разных частях файла и размещается по разным виртуальным адресам. У второго сегмента размер в памяти больше размера в файле: описание памяти процесса не обязано совпадать с размером хранящихся на диске данных.
Помимо PT_LOAD, для понимания запуска важны:
PT_INTERP — указывает интерпретатор ELF, то есть динамический загрузчик.PT_DYNAMIC — описывает область с информацией для динамического связывания.В выводе readelf эти типы отображаются как INTERP и DYNAMIC.
Точка входа — адрес, с которого начинается выполнение кода программы после подготовки её к запуску. Для ELF этот адрес записан в поле e_entry.
Точка входа не обязательно совпадает с функцией main. В примере из лекции это разные адреса:
nm ./hello | grep main
U __libc_start_main@@GLIBC_2.2.5
000000000000064a T main
nm ./hello | grep 540
0000000000000540 T _start
Заголовок содержит адрес 0x540, соответствующий _start, тогда как main находится по адресу 0x64a. Следовательно, выполнение начинается со стартового кода, а не непосредственно с main.
В общем виде подготовка программы к выполнению включает следующие действия:
У программы с разделяемыми библиотеками появляется промежуточный этап. Операционная система находит PT_LOAD и подготавливает сегменты программы, затем читает PT_INTERP и загружает указанный динамический загрузчик. Первоначально управление получает именно загрузчик, а не точка входа самой программы.
В рассматриваемом примере путь к загрузчику выглядит так:
/lib64/ld-linux-x86-64.so.2
После подготовки необходимых разделяемых библиотек управление передаётся программе. Поэтому при анализе запуска нужно различать точку входа программы, стартовый код _start и работу динамического загрузчика.
Библиотека позволяет использовать готовый код при сборке или выполнении программы. Способ подключения определяет, где этот код будет находиться после сборки.
| Характеристика | Статическая библиотека | Разделяемая библиотека |
|---|---|---|
| Обычное расширение | .a |
.so |
| Представление | Архив объектных файлов | Отдельный разделяемый объект |
| Когда используется | При создании исполняемого файла | При динамическом связывании и выполнении |
| Где находится подключённый код | В составе исполняемого файла | В отдельном файле библиотеки |
| Требуется ли отдельный файл этой библиотеки при запуске | Нет | Да |
Статическая библиотека — архив, создаваемый утилитой ar. Внутри находятся объектные файлы, обычно в формате ELF.
Создать архив из готовых объектных файлов:
ar rc libfoo_static.a libfoo_static_a.o libfoo_static_b.o
Проверить тип файла и состав архива:
file libfoo_static.a
ar -t libfoo_static.a
Ожидаемый тип файла:
libfoo_static.a: current ar archive
Список содержимого:
libfoo_static_a.o
libfoo_static_b.o
Подключить библиотеку из текущего каталога при сборке:
gcc app.o -L. -lfoo_static -o app
Параметр -L. добавляет текущий каталог в пути поиска библиотек. Параметр -lfoo_static задаёт имя библиотеки без префикса lib и расширения. Объектный файл приложения указан перед библиотекой.
Для создания разделяемой библиотеки исходные файлы сначала компилируют в объектные с параметром -fPIC, затем объединяют с помощью -shared:
gcc -Wall -fPIC -c foo_a.c
gcc -Wall -fPIC -c foo_b.c
gcc -shared -Wl,-soname,libctest.so.1 -o libctest.so.1.0 foo_a.o foo_b.o
Назначение параметров:
-Wall — включает набор предупреждений компилятора.-c — создаёт объектный файл без окончательного связывания.-fPIC — создаёт позиционно-независимый код для разделяемой библиотеки.-shared — формирует разделяемую библиотеку.-Wl,-soname,libctest.so.1 — передаёт линковщику имя библиотеки libctest.so.1.-o — задаёт имя выходного файла.Для библиотеки используются три имени:
| Имя | Роль |
|---|---|
libctest.so.1.0 |
Файл конкретной версии библиотеки |
libctest.so.1 |
Имя, указанное в SONAME |
libctest.so |
Имя для поиска библиотеки при сборке с -lctest |
В примере библиотека размещается в /opt/lib, а дополнительные имена создаются символическими ссылками:
mv libctest.so.1.0 /opt/lib/
ln -sf /opt/lib/libctest.so.1.0 /opt/lib/libctest.so.1
ln -sf /opt/lib/libctest.so.1.0 /opt/lib/libctest.so
Операции в /opt/lib требуют права записи в этот каталог.
Сборка приложения:
gcc -Wall -L/opt/lib app.o -lctest -o app
Если каталог библиотеки не входит в стандартные пути поиска при запуске, его можно указать через LD_LIBRARY_PATH:
export LD_LIBRARY_PATH=/opt/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}
./app
-L/opt/lib задаёт путь поиска во время сборки. LD_LIBRARY_PATH задаёт путь поиска во время выполнения. Успешная сборка сама по себе не означает, что динамический загрузчик найдёт библиотеку при запуске.
Зависимости от разделяемых библиотек можно посмотреть командой ldd:
ldd ./app
ldd ./libshared.so
В примерах из лекции вывод содержит libc.so.6 и динамический загрузчик /lib64/ld-linux-x86-64.so.2. Команда показывает, какие разделяемые объекты используются и по каким путям они найдены.
Внутри ELF сведения для динамического связывания доступны через программный заголовок PT_DYNAMIC. Эта область содержит массив записей Elf64_Dyn:
typedef struct {
Elf64_Sxword d_tag;
union {
Elf64_Xword d_val;
Elf64_Addr d_ptr;
} d_un;
} Elf64_Dyn;
d_tag определяет смысл записи и способ интерпретации d_un. Поле d_val хранит целое значение, а d_ptr — адрес. В ELF64 Elf64_Sxword — знаковый 64-битный тип.
Основные теги:
| Тег | Значение |
|---|---|
DT_NEEDED |
Смещение строки с именем необходимой разделяемой библиотеки |
DT_STRTAB |
Адрес таблицы строк, используемой динамическими записями |
DT_NULL |
Конец массива динамических записей |
Запись DT_NEEDED не содержит имя библиотеки непосредственно. Она содержит смещение внутри таблицы строк. Чтобы получить имя, нужно найти таблицу по DT_STRTAB, перейти на указанное смещение и прочитать байты до завершающего нуля.
Таблица строк — последовательность строк, заканчивающихся байтом \0. Ссылка на строку представляет собой её смещение от начала таблицы. Поэтому индекс здесь измеряется в байтах, а не в количестве строк.
Для примера из лекции:
\0name.\0Variable\0able\0\0xx\0
Соответствие смещений и строк:
| Смещение | Строка |
|---|---|
0 |
Отсутствующее или пустое имя, в зависимости от контекста |
1 |
name. |
7 |
Variable |
11 |
able — окончание строки Variable |
16 |
able — отдельная строка |
24 |
Пустая строка |
Смещение может указывать не только на начало отдельно записанной строки, но и на её часть: чтение всё равно продолжается до ближайшего \0.
В ELF используются таблицы строк разного назначения. Таблица, указанная через DT_STRTAB, относится к динамическим данным. Имена секций находятся в таблице, индекс которой записан в e_shstrndx; поле sh_name отдельного заголовка секции задаёт смещение имени внутри этой таблицы. Эти ссылки нужно интерпретировать в контексте той структуры ELF, которой они принадлежат.