Как запустить файл elf linux

Обновлено: 21.11.2024

Объектные файлы существуют для ускорения компиляции: с помощью make нам нужно только перекомпилировать измененные исходные файлы на основе меток времени.

1.4. Реализации

Нормальные компиляторы должны использовать отдельную автономную библиотеку для выполнения грязной работы. Например, Binutils использует BFD (в дереве и канонический источник).

Ядра не могут связываться с библиотекой или использовать библиотеку C stlib, поэтому они, скорее всего, реализуют ее сами.

2. Минимальный файл ELF

Нетривиально определить, какой файл ELF наименьшего размера или файл меньшего размера, который будет делать что-то тривиальное в Linux.

В этом примере мы рассмотрим более разумный пример hello world, который лучше описывает случаи из реальной жизни.

3. Создать пример

  • NASM 2.10.09
  • Binutils версии 2.24 (содержит ld )
  • Убунту 14.04

4. Объект hd

5. Исполняемый hd

6. Глобальная файловая структура

  • Заголовок ELF. Указывает на положение таблицы заголовков разделов и таблицы заголовков программы.
  • Таблица заголовков разделов (необязательно для исполняемого файла). Каждый из них имеет заголовки разделов e_shnum, каждый из которых указывает на положение раздела.
  • N разделов, где N (необязательно для исполняемого файла)
  • Таблица заголовков программы (только для исполняемого файла). Каждый из них имеет заголовки программы e_phnum, каждый из которых указывает на положение сегмента.
  • N сегментов с N (только для исполняемого файла)

Порядок этих частей не фиксирован: единственная фиксированная вещь — это заголовок ELF, который должен быть первым в файле. В общих документах говорится:

Хотя на рисунке таблица заголовков программы показана сразу после заголовка ELF, а таблица заголовков разделов следует за разделами, реальные файлы могут отличаться. Более того, разделы и сегменты не имеют определенного порядка. Только заголовок ELF имеет фиксированную позицию в файле.

 Рисунок 1 . Диаграмма исполняемого и связываемого формата ELF от Ange Albertini Source.

7. Раздел против сегмента

  • необработанные данные для загрузки в память, например. .data , .text и т. д.
  • или метаданные о других разделах, которые будут использоваться компоновщиком, но исчезают во время выполнения, например. .symtab , .srttab , .rela.text

Содержит информацию о том, как ОС должна загружать каждый сегмент в память, в частности, о местоположении и разрешениях.

8. Заголовок ELF

  • 0 0: EI_MAG = 7f 45 4c 46 = 0x7f 'E', 'L', 'F': магическое число ELF
  • 0 4: EI_CLASS = 02 = ELFCLASS64: 64-битный эльф
  • 0 5: EI_DATA = 01 = ELFDATA2LSB: данные с прямым порядком байтов
  • 0 6: EI_VERSION = 01 : версия формата
  • 0 7: EI_OSABI (только в обновлении 2003) = 00 = ELFOSABI_NONE : без расширений.
  • 0 8: EI_PAD = 8x 00: зарезервированные байты. Должно быть установлено значение 0.

ET_DYN сообщает ядру Linux, что код не зависит от позиции и может загружаться в произвольное место памяти с помощью ASLR.

1 8: e_entry = 8x 00 : точка входа адреса выполнения или 0, если это неприменимо, как для объектного файла, поскольку точки входа нет.

9. Таблица заголовков разделов

10. Разделы

10.1. Раздел индекса 0

Если количество разделов больше или равно SHN_LORESERVE (0xff00), e_shnum имеет значение SHN_UNDEF (0), а фактическое количество записей таблицы заголовков разделов содержится в поле sh_size заголовка раздела по адресу index 0 (иначе элемент sh_size начальной записи содержит 0).

10.1.1. SHT_NULL

10.2. раздел .data

Здесь 1 означает, что имя этого раздела начинается с первого символа этого раздела и заканчивается первым символом NUL, образуя строку .data .

Если секция появится в образе памяти процесса, этот член дает адрес, по которому должен находиться первый байт секции. В противном случае элемент содержит 0.

Также обратите внимание, что это был плохой выбор раздела: хороший компилятор C поместил бы строку вместо .rodata, потому что он доступен только для чтения и позволяет проводить дальнейшую оптимизацию ОС.

10.3. .текстовый раздел

Теперь, когда мы сделали один раздел вручную, давайте закончим и воспользуемся readelf -S для других разделов:

Если мы выполним grep b8 01 00 00 на hd , мы увидим, что это происходит только по адресу 00000210 , о чем говорится в разделе. И размер 27, что тоже подходит. Значит, мы должны говорить о правильном разделе.

для передачи адреса строки системному вызову. В настоящее время 0x0 является просто заполнителем. После связывания он будет изменен и будет содержать:

10.4. SHT_STRTAB

  • какую таблицу строк они используют
  • какой индекс в целевой таблице строк, где начинается строка

И если другой раздел хочет использовать строку d e f , он должен указывать на индекс 5 этого раздела (буква d ).

10.5. .shstrtab

На строковые индексы этого раздела указывает поле sh_name заголовков разделов, обозначающих строки.

Если мы посмотрим на названия других разделов, то увидим, что все они содержат числа, например раздел .text имеет номер 7 .

Тогда каждая строка заканчивается, когда найден первый символ NUL, например. символ 12 - это \0 сразу после .text\0 .

10.6. .симтаб

  • строки, которые задают имена символов, находятся в разделе 5, .strtab
  • данные о перемещении находятся в разделе 6, .rela.text

Однако это представление высокого уровня, в котором пропущены некоторые типы символов и в котором используются типы символов . Более подробную разборку можно получить с помощью:

10.6.1. STT_FILE

10 8: st_name = 01000000 = символ 1 в .strtab , что до следующего \0 делает hello_world.asm

Эта часть информационного файла может использоваться компоновщиком для принятия решения о том, какие разделы сегмента следует использовать: например. в скрипте компоновщика ld пишем:

Биты 0–3 = ELF64_R_TYPE = Type = 4 = STT_FILE: основная цель этой записи — использовать st_name для указания имени файла, который сгенерировал этот объектный файл.

10.6.2. STT_SECTION

10.6.3. STT_NOTYPE

Строка hello_world находится в разделе .data (индекс 1). Его значение равно 0: оно указывает на первый байт этого раздела.

в NASM. Это необходимо, поскольку он должен рассматриваться как точка входа. В отличие от C, метки NASM по умолчанию являются локальными.

10.6.3.1. SHN_ABS

st_value == 0xD == 13 значение, которое мы сохранили в сборке: длина строки Hello World! .

Если бы мы где-нибудь использовали адрес hello_world_len, ассемблер не смог бы пометить его как SHN_ABS , и позже компоновщику пришлось бы дополнительно работать над его перемещением.

10.6.4. SHT_SYMTAB в исполняемом файле

Это используется только для отладки. Без символов мы совершенно слепы и должны все перепроектировать.

Вы можете удалить его с помощью objcopy , и исполняемый файл все равно будет работать. Такие исполняемые файлы называются "зачищенными исполняемыми файлами".

10.7. .strtab

10.8. .рела.текст

.rela.text содержит данные о перемещении, в которых говорится, как следует изменить адрес при компоновке финального исполняемого файла. Это указывает на байты текстовой области, которые должны быть изменены, когда происходит связывание, чтобы указать на правильные ячейки памяти.

    370 0: r_offset = 0xC: адрес в .text, адрес которого изменит это перемещение
  • ELF64_R_TYPE = 0x1: значение зависит от конкретной архитектуры.
  • ELF64_R_SYM = 0x2: индекс раздела, на который указывает адрес, поэтому .data находится под индексом 2.
  • S : значение символа в объектном файле, здесь 0, потому что мы указываем на 00 00 00 00 00 00 00 00 movabs $0x0,%rsi
  • A : добавление, присутствующее в поле r_added

Итак, в нашем примере мы заключаем, что новый адрес будет: S + A = .data + 0 , и, следовательно, первым в разделе данных.

10.8.1. .отн.текст

Помимо sh_type == SHT_RELA также существует SHT_REL , у которого будет имя раздела .text.rel (отсутствует в этом объектном файле).

Стандарт ELF говорит, что во многих случаях можно использовать и то, и другое, и это просто вопрос удобства.

10.9. Разделы с динамическими ссылками

В этой программе не было определенных разделов, связанных с динамической компоновкой, потому что мы минимально связывали ее с помощью ld .

10.9.1. PT_INTERP

10.9.2. Динамический раздел

10.9.2.1. DT_FLAGS_1
10.9.2.1.1. DF_1_PIE

11. Таблица заголовков программы

Этот элемент задает смещение от начала файла, в котором находится первый байт сегмента.

60 0: p_filesz = d7 00 00 00 00 00 00 00 : размер, который сегмент занимает в памяти. Если меньше, чем p_memsz , ОС заполняет его нулями, чтобы соответствовать при загрузке программы. Так реализованы данные BSS для экономии места в исполняемых файлах. i368 ABI говорит о PT_LOAD:

Байты из файла сопоставляются с началом сегмента памяти. Если размер памяти сегмента (p_memsz) больше, чем размер файла (p_filesz), определяются «дополнительные» байты, содержащие значение 0 и следующие за инициализированной областью сегмента. Размер файла не может превышать объем памяти.

    60 8: p_memsz = d7 00 00 00 00 00 00 00 : размер, который сегмент занимает в памяти

70 0: p_align = 00 00 20 00 00 00 00 00 : 0 или 1 означает, что выравнивание не требуется. TODO зачем это нужно? Почему бы просто не использовать p_addr напрямую и сделать это правильно? Документы также говорят:

Второй сегмент ( .data ) аналогичен.TODO: зачем использовать смещение 0x0000d8 и адрес 0x00000000006000d8? Почему бы просто не использовать 0 и 0x00000000006000d8?

Программирование начинается с умной идеи, написания исходного кода на выбранном вами языке программирования, например C, и сохранения исходного кода в файле. С помощью адекватного компилятора, например GCC, ваш исходный код сначала транслируется в объектный код. В конце концов компоновщик переводит объектный код в двоичный файл, который связывает объектный код с библиотеками, на которые ссылаются. Этот файл содержит отдельные инструкции в виде машинного кода, которые понимает ЦП и выполняет, как только запускается скомпилированная программа.

Упомянутый выше двоичный файл имеет определенную структуру, и один из наиболее распространенных файлов называется ELF, что означает Executable and Linkable Format. Он широко используется для исполняемых файлов, перемещаемых объектных файлов, общих библиотек и дампов ядра.

Двадцать лет назад, в 1999 году, проект 86open выбрал ELF в качестве стандартного формата двоичных файлов для Unix и Unix-подобных систем на процессорах x86. К счастью, формат ELF ранее был задокументирован как в двоичном интерфейсе приложений System V, так и в стандарте интерфейса инструментов [4]. Этот факт значительно упростил соглашение о стандартизации между различными поставщиками и разработчиками операционных систем на основе Unix.

Причиной этого решения была конструкция ELF — гибкость, расширяемость и кросс-платформенная поддержка различных форматов с порядком байтов и размеров адресов. Дизайн ELF не ограничивается конкретным процессором, набором инструкций или аппаратной архитектурой. Подробное сравнение форматов исполняемых файлов см. здесь [3].

С тех пор формат ELF используется несколькими операционными системами. Среди прочего, это Linux, Solaris/Illumos, Free-, Net- и OpenBSD, QNX, BeOS/Haiku и Fuchsia OS [2]. Кроме того, вы найдете его на мобильных устройствах под управлением Android, Maemo или Meego OS/Sailfish OS, а также на игровых консолях, таких как PlayStation Portable, Dreamcast и Wii.

Спецификация не уточняет расширение имени файла для файлов ELF. Используется множество комбинаций букв, таких как .axf, .bin, .elf, .o, .prx, .puff, .ko, .so и .mod или ни одного.

Структура файла ELF

В терминале Linux команда man elf дает удобную сводку о структуре файла ELF:

Листинг 1: Справочная страница структуры ELF

ELF(5) Руководство программиста Linux ELF(5)

ИМЯ
elf - формат файлов Executable и Linking Format (ELF)

ОПИСАНИЕ
Заголовочный файл определяет формат исполняемых
бинарных файлов ELF. Среди этих файлов есть обычные исполняемые файлы, перемещаемые
объектные файлы, основные файлы и общие библиотеки.

Исполняемый файл, использующий формат файла ELF, состоит из заголовка ELF,
за которым следует таблица заголовков программы или таблица заголовков разделов, или и то, и другое.
Заголовок ELF всегда находится по нулевому смещению файла. Таблица заголовков программы
и смещение таблицы заголовков разделов в файле
определены в заголовке ELF. Две таблицы описывают остальные
особенности файла.

Как видно из приведенного выше описания, файл ELF состоит из двух разделов: заголовка ELF и данных файла. Раздел данных файла может состоять из таблицы заголовков программ, описывающей ноль или более сегментов, таблицы заголовков разделов, описывающих ноль или более сегментов, за которыми следуют данные, на которые ссылаются записи из таблицы заголовков программ, и таблицы заголовков разделов. Каждый сегмент содержит информацию, необходимую для выполнения файла во время выполнения, а разделы содержат важные данные для связывания и перемещения. На рис. 1 это схематично показано.

Заголовок ELF

Заголовок ELF имеет длину 32 байта и определяет формат файла. Он начинается с последовательности из четырех уникальных байтов: 0x7F, за которыми следуют 0x45, 0x4c и 0x46, что транслируется в три буквы E, L и F. Помимо других значений, заголовок также указывает, является ли это файлом ELF для 32 или 64-битный формат, использует прямой или прямой порядок следования байтов, показывает версию ELF, а также для какой операционной системы был скомпилирован файл, чтобы взаимодействовать с правильным двоичным интерфейсом приложения (ABI) и набором инструкций процессора.

Шестнадцатеричный дамп бинарного файла touch выглядит следующим образом:

.Листинг 2: шестнадцатеричный дамп двоичного файла

$ hd /usr/bin/touch | голова -5
00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF. |
00000010 02 00 3e 00 01 00 00 00 e3 25 40 00 00 00 00 00 |..>. %@. |
00000020 40 00 00 00 00 00 00 00 28 e4 00 00 00 00 00 00 |@. (. |
00000030 00 00 00 00 40 00 38 00 09 00 40 00 1b 00 1a 00 |[email protected]@.|
00000040 06 00 00 00 05 00 00 00 40 00 00 00 00 00 00 00 |[email protected]|

Debian GNU/Linux предлагает команду readelf, входящую в состав пакета GNU ‘binutils’. В сочетании с ключом -h (сокращенная версия «–file-header») он красиво отображает заголовок ELF-файла. В листинге 3 это показано для команды touch.

.Листинг 3: Отображение заголовка файла ELF

$ readelf -h /usr/bin/touch
Заголовок ELF:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Класс: ELF64 < br />Данные: дополнение до 2, обратный порядок байтов
Версия: 1 (текущая)
ОС/ABI: UNIX - System V
Версия ABI: 0
Тип: EXEC (Исполняемый файл )
Машина: Advanced Micro Devices X86-64
Версия: 0x1
Адрес точки входа: 0x4025e3
Начало заголовков программы: 64 (байт в файле)
Начало заголовки разделов: 58408 (байт в файл)
Флаги: 0x0
Размер этого заголовка: 64 (байты)
Размер заголовков программы: 56 (байт)
Количество заголовков программы : 9
Размер заголовков разделов: 64 (байта)
Количество заголовков разделов: 27
Индекс таблицы строк заголовков разделов: 26

Заголовок программы

Заголовок программы показывает сегменты, используемые во время выполнения, и сообщает системе, как создать образ процесса. Заголовок из листинга 2 показывает, что файл ELF состоит из 9 заголовков программ размером 56 байт каждый, причем первый заголовок начинается с 64-го байта.

Опять же, команда readelf помогает извлечь информацию из файла ELF. Переключатель -l (сокращение от –program-headers или –segments) раскрывает дополнительные сведения, как показано в листинге 4.

.Листинг 4: Отображение информации о заголовках программы

$ readelf -l /usr/bin/touch

Тип файла Elf - EXEC (исполняемый файл)
Точка входа 0x4025e3
Есть 9 заголовков программы, начиная со смещения 64

Заголовки программ:
Смещение типа VirtAddr Phangaddr
Флаги Memsiz FilesAddr
PHDR 0x00000000000000004000000000000000000000040000000000000040004000000000000000400040000000000000000000001F8 Re 8
Интерп 0x0000000000400238 0x0000000000400238

0x00000000000000001C 0x00000000000000001C R 1
[Запрос интерпретатора программы: /lib64/ld-linux-x86-64.so.2]
Нагрузка 0x000000000000000000000000000000000000000000000000000040000000000000000 00000000D494 0x000000000000d494 Re 200000


0x000000000000DE10 0x000000000060de10 0x000000000060de10
0x0000000000000524 0x0000000000000748 RW 200000
ДИНАМИЧНЫЙ 0x000000000000de28 0x000000000060de28 0x000000000060de28
0x00000000000001d0 0x00000000000001d0 RW 8
Примечание 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x000000000000bc40 0x000000000040bc40 0x000000000040bc40
0x00000000000003a4 0x000000000000003a4 R 4 GNU_STACK 0x000000000000000000 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006DE10 0x000000000060De10
0x000000000060de10
0x00000000000001f0 r 1

Сопоставление раздела с сегментом:
Разделы сегмента.
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r . rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 . динамический
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got

Заголовок раздела

Третья часть структуры ELF — заголовок раздела. Он предназначен для перечисления отдельных разделов двоичного файла. Переключатель -S (сокращение от –section-headers или –sections) перечисляет различные заголовки. Что касается команды touch, то имеется 27 заголовков разделов, и в листинге 5 показаны только первые четыре плюс последний. Каждая строка содержит размер раздела, тип раздела, а также его адрес и смещение в памяти.

.Листинг 5: Детали раздела, раскрытые readelf

$ readelf -S /usr/bin/touch
Имеется 27 заголовков разделов, начиная со смещения 0xe428:

Заголовки разделов:
[Nr] Имя Тип Адрес Смещение
Размер EntSize Флаги Информация о ссылке Выравнивание
[ 0] NULL 00000000000000000 00000000
00000000000000000 00000000000000000 0 0 0
[1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[2] .note.ABI-тега Примечание 0000000000400254 00000254
0000000000000020 0000000000000000 А 0 0 4
[3 ] .note.gnu.build-i ПРИМЕЧАНИЕ 0000000000400274 00000274
.
.
[26] .shstrtab STRTAB 0000000000000000 0000e334
00000000000000ef 0000000000000000 0 0 1
Ключ к флагам:
W (запись), A (распределение), X (выполнение), M (объединение), S (строки), l (большой)
I (информация), L (порядок ссылок), G (группа), T (TLS), E (исключить), x (неизвестно)
O (требуется дополнительная обработка ОС) o (зависит от ОС), p (зависит от процессора)

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

Как вы, возможно, заметили из приведенных выше примеров, GNU/Linux дополнен рядом полезных инструментов, которые помогут вам проанализировать файл ELF. Первым кандидатом, которого мы рассмотрим, является файловая утилита.

отображает основную информацию о файлах ELF, включая архитектуру набора инструкций, для которой предназначен код в перемещаемом, исполняемом или совместно используемом объектном файле. В листинге 6 указано, что /bin/touch — это 64-битный исполняемый файл, соответствующий стандарту Linux Standard Base (LSB), динамически связанный и созданный для ядра GNU/Linux версии 2.6.32.

.Листинг 6: Основная информация с использованием файла

$ файл /bin/touch
/bin/touch: исполняемый файл ELF 64-bit LSB, x86-64, версия 1 (SYSV), динамическая компоновка, интерпретатор /lib64/l,
для GNU /Linux 2.6.32, BuildID[sha1]=ec08d609e9e8e73d4be6134541a472ad0ea34502, удалено
$

Второй кандидат — readelf. Он отображает подробную информацию о файле ELF. Список переключателей сравнительно длинный и охватывает все аспекты формата ELF. Использование ключа -n (сокращение от –notes). В листинге 7 показаны только те разделы примечаний, которые существуют в файле touch – тег версии ABI и битовая строка идентификатора сборки.

.Листинг 7: Отображение выбранных разделов файла ELF

$ readelf -n /usr/bin/touch

Отображение заметок, найденных по смещению файла 0x00000254 с длиной 0x00000020:
Владелец Размер данных Описание
GNU 0x00000010 NT_GNU_ABI_TAG (тег версии ABI)
ОС: Linux, ABI: 2.6.32

Отображение заметок, найденных по смещению файла 0x00000274 с длиной 0x00000024:
Владелец Размер данных Описание
GNU 0x00000014 NT_GNU_BUILD_ID (уникальная битовая строка идентификатора сборки)
Идентификатор сборки: ec08d609e9e8e73d4be6134541a472ad0ea34502

Обратите внимание, что в Solaris и FreeBSD утилита elfdump [7] соответствует readelf. По состоянию на 2019 г. с 2003 г. не было новых выпусков или обновлений.

Номер три — это пакет elfutils [6], доступный только для Linux. Он предоставляет инструменты, альтернативные GNU Binutils, а также позволяет проверять файлы ELF. Обратите внимание, что все имена утилит, представленных в пакете, начинаются с eu для «elf utils».

И последнее, но не менее важное: мы упомянем objdump. Этот инструмент похож на readelf, но фокусируется на объектных файлах. Он предоставляет аналогичную информацию о файлах ELF и других форматах объектов.

.Листинг 8: Информация о файле, извлеченная objdump

$ objdump -f /bin/touch

Существует также программный пакет под названием «elfkickers» [9], который содержит инструменты для чтения содержимого файла ELF, а также для управления им. К сожалению, количество релизов невелико, поэтому мы просто упомянем об этом, а не приводим дальнейших примеров.

Как разработчик, вы можете вместо этого взглянуть на pax-utils [10,11]. Этот набор утилит предоставляет ряд инструментов, помогающих проверять файлы ELF. В качестве примера, dumpelf анализирует файл ELF и возвращает заголовочный файл C, содержащий подробности — см. рис. 2.

Заключение

Благодаря сочетанию продуманного дизайна и отличной документации формат ELF работает очень хорошо и все еще используется спустя 20 лет. Утилиты, показанные выше, позволяют вам получить представление о файле ELF и понять, что делает программа. Это первые шаги для анализа программного обеспечения — удачного взлома!

Ссылки и ссылки
  • [1] Executable and Linkable Format (ELF), Wikipedia
  • [2] Фуксия ОС
  • [3] Сравнение форматов исполняемых файлов, Википедия
  • [4] Linux Foundation, Справочные спецификации
  • [5] Чиро Сантилли: Учебное пособие по ELF Hello World
  • [6] Пакет Debian elfutils
  • [7] эльфдамп
  • [8] Майкл Боэлен: 101 файл ELF в Linux: понимание и анализ
  • [9] эльфкикеры
  • [10] Защищенные/PaX-утилиты
  • [11] pax-utils, пакет Debian
Благодарности

Автор хотел бы поблагодарить Акселя Беккерта за поддержку в подготовке этой статьи.

Это задание познакомит вас с организацией файлов ELF. Вы можете выполнить это задание в любой операционной системе, поддерживающей Unix API (компьютеры Linux Openlab, ваш ноутбук с Linux или Linux VM и даже MacOS). Вам не нужно настраивать Xv6 для этого задания.

Для пользователей MacOS поддержка 32-разрядных приложений в последней версии вашей системы устарела.Поэтому, если вы уже обновили свою систему до MacOS Catalina или обновили XCode, мы рекомендуем вам выполнить домашнее задание на компьютерах Openlab.

Часть 1. Взгляните на файлы ELF

Загрузите файлы main.c и elf.c и просмотрите их. На высоком уровне в этом домашнем задании вам предлагается реализовать простой загрузчик ELF (вы расширите файл main.c) и использовать его для загрузки простого объектного файла ELF (тот, который скомпилирован из elf .с). Однако, прежде чем приступить к этой задаче, давайте познакомимся с файлами ELF.

Мы предоставляем простой Makefile, который компилирует elf.o и main как исполняемые файлы ELF. Просмотрите Makefile, а затем скомпилируйте оба файла, выполнив:

Теперь давайте взглянем на файлы ELF, которые мы только что скомпилировали. Мы будем использовать инструмент readelf:

ELF — это формат файлов, используемый для объектных файлов .o, двоичных файлов, общих библиотек и дампов ядра в Linux. На самом деле это довольно просто и хорошо продумано.

ELF имеет одинаковую компоновку для всех архитектур, однако порядок следования байтов и размер слова могут различаться; типы перемещения, типы символов и т. п. могут иметь значения, зависящие от платформы, и, конечно же, содержащийся в них код зависит от архитектуры.

Файлы ELF используются двумя инструментами: компоновщиком и загрузчиком. Компоновщик объединяет несколько файлов ELF в исполняемый файл или библиотеку, а загрузчик загружает исполняемый файл ELF в память процесса. В реальных операционных системах для загрузки может потребоваться перемещение (например, если файл динамически связан, его необходимо снова связать со всеми общими библиотеками, от которых он зависит). В этом домашнем задании мы не будем делать никакого перемещения (это слишком сложно), мы просто загрузим ELF-файл в память и запустим его.

Компоновщику и загрузчику нужны два разных представления файла ELF, т. е. доступ к нему у них разный.

С одной стороны, компоновщик должен знать, где находятся разделы DATA, TEXT, BSS и другие, чтобы объединить их с разделами из других библиотек. Если требуется перемещение, компоновщик должен знать, где находятся таблицы символов и информация о перемещении.

С другой стороны, загрузчику эти детали не нужны. Ему просто нужно знать, какие части файла ELF являются кодом (исполняемым), какие являются данными и данными только для чтения, а также где разместить BSS в памяти процесса.

Следовательно, файл ELF предоставляет два отдельных представления данных внутри файла ELF: представление компоновщика с несколькими подробностями и представление загрузчика, представление более высокого уровня с меньшим количеством деталей.

Чтобы обеспечить эти представления, каждый файл ELF содержит две таблицы (или массивы):

  • Таблица заголовков разделов Содержит указатели на разделы, используемые компоновщиком.
  • Таблица заголовков программы Содержит указатели на сегменты, используемые загрузчиком.

Обе таблицы представляют собой просто массивы записей, которые содержат информацию о каждой части файла ELF (например, где разделы/сегменты, используемые компоновщиком/загрузчиком, находятся внутри файла ELF).

Вот простой рисунок типичного файла ELF, который начинается с заголовка ELF. Заголовок содержит указатели на расположение таблиц заголовков разделов и таблиц заголовков программ в файле ELF. Затем в каждой таблице есть записи, указывающие на начальные местоположения отдельных разделов и сегментов.

Типичный файл ELF. Компоновщик использует таблицу заголовков разделов, а загрузчик использует таблицу заголовков программы.

Это немного раздражает, но части файла ELF, используемые компоновщиком, называются "Секции", а части, используемые загрузчиком, называются "сегментами" (я предполагаю, что в прошлом разные сегменты ЦП были настроены для каждая часть программы загружалась в память, отсюда и название «сегменты», например, для исполняемых частей файла ELF создавался исполняемый сегмент ЦП (т.е. один сегмент, который содержал все исполняемые секции типа .text< /tt> и .init и т. д.).

Также не запутайтесь: разделы и сегменты могут пересекаться. То есть, как правило, несколько разделов (поскольку компоновщик видит файл ELF, например .text и .init) содержатся в одном исполняемом сегменте (то, что видит загрузчик). . Посмотрите на предыдущее изображение и посмотрите, как в зависимости от перспективы одни и те же части файла ELF принадлежат одному разделу и одному сегменту. Запутанно, да? Скоро станет ясно.

Связывание представления: таблица заголовков разделов (SHT)

Таблица заголовков разделов представляет собой массив, в котором каждая запись содержит указатель на один из разделов файла ELF. Давайте посмотрим, что внутри файла ELF. Запустите эту команду и прокрутите вниз до заголовков разделов, вы увидите все «разделы» файла ELF, которые может использовать компоновщик:

Поскольку elf.c — очень простая программа, в ней есть только раздел .text (т. е. код программы), куча разделов, содержащих отладочную информацию и .symtab, который содержит импортированные и экспортированные символы.

Обратите внимание, что нет разделов .data или .bss для глобальных переменных (в elf.c нет глобальных переменных).< /p>

Вы можете поэкспериментировать, добавив неинициализированную глобальную переменную в elf.c, перекомпилировать и увидеть разницу в SHT. Затем добавьте инициализированную глобальную переменную, перекомпилируйте и снова проверьте SHT.

Убедитесь, что глобальные переменные удалены, и выполните повторную компиляцию, прежде чем продолжить задание.)

Более того, поскольку мы связали elf.c со статическим исполняемым файлом, который запускается при загрузке по адресу 0x0 (-Ttext 0 в Makefile указывает компоновщику переместить исполняемый файл во время компоновки, чтобы он работал в 0x0):

$ cat Makefile . elf: elf.old -m elf_i386 -N -e main -Ttext 0 -o elf elf.o elf.o: elf.c $(CC) -c -fno-pic -static -fno-builtin -ggdb -m32 -fno-опустить-указатель-кадра elf.c

Таблица символов содержит эти символы

$ readelf --syms elf Таблица символов '.symtab' содержит 14 записей: Число: Значение Размер Тип Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 SECTION LOCAL DEFAULT 1 2: 00000010 0 SECTION LOCAL По умолчанию 2 3: 00000000 0 Раздел Локальный по умолчанию 3 4: 00000000 0 Раздел Локальный по умолчанию 4 5: 00000000 0 Раздел Локальный по умолчанию 5 6: 00000000 0 Раздел Локальный по умолчанию 6 7: 00000000 0 Раздел Локальный по умолчанию 7 8: 00000000 0 Раздел Локальный по умолчанию 8 9: 00000000 0 Файл Локальный по умолчанию ABS ELF.C 10: 00000048 0 NOTYPE Global Default 2 __BSS_START 11: 00000000 13 Func Global Default 1 Глобал 12: 00000048 0 NOTYPE Global по умолчанию 2 _EDATA 13: 00000048 0 NOTYPE GLOBAL по умолчанию 2 _ENG

main — это наша функция (это FUNC и GLOBAL), __bss_start, _edata и _end добавляются компоновщиком для обозначения начала и конца разделов BSS, TEXT и DATA.

Если мы посмотрим на исполняемый файл main, файл ELF будет более сложным.

Он содержит все секции, которые мы упомянули в классе: .text (основной код программы), .data (секция данных для глобальных переменных), < tt>.rodata (раздел данных для глобальных переменных только для чтения), .bss (неинициализированные глобальные переменные), .init (раздел инициализации для вызова конструкторов которые запускаются перед main()), .got(таблица глобальных смещений), .plt (таблица связывания процедур для отложенного связывания импортированных функций) , и даже .interp (раздел для интерпретатора, то есть компоновщика, который связывает динамически компонуемую программу перед ее запуском, обычно это /lib/ld-linux.so.2 в системах Linux.

Представление «Выполнение»: Таблица заголовков программы (PHT)

Таблица заголовков программы содержит информацию для ядра о том, как запустить программу. Директивы LOAD определяют, какие части файла ELF отображаются в памяти программы.

Опять же, в нашем примере elf заголовок программы определяет только два сегмента. И только один из них должен быть загружен операционной системой в память для запуска.

$ readelf --program-headers elf Тип файла Elf - EXEC (Исполняемый файл) Точка входа 0x0 Имеется 2 заголовка программы, начиная со смещения 52 Заголовки программы: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000074 0x00000000 0x00000000 0x00048 0x00048 RWE 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 Отображение раздела в сегмент: Разделы сегмента. 00 .текст .eh_frame 01

Что ж, ничего удивительного: это основная функция, скомпилированная в машинный код.

Собираем все вместе

Ни SHT, ни PHT не имеют фиксированных позиций, они могут располагаться в любом месте файла ELF. Для их поиска используется ELF-заголовок, который находится в самом начале файла.

Первые байты содержат магию эльфа "\x7fELF" , за которой следует идентификатор класса (32- или 64-битный файл ELF), идентификатор формата данных (с прямым порядком байтов/с прямым порядком байтов), тип машины и т. д.

>

В конце заголовка ELF находятся указатели на SHT и PHT. В частности, таблица заголовков сегментов, используемая компоновщиком, начинается с 1120 байта в файле ELF, а таблица заголовков программ начинается с 52 байта (сразу после заголовка ELF)

$ readelf --file-header elf Заголовок ELF: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Класс: ELF32 Данные: дополнение до 2, прямой порядок байтов Версия: 1 (текущая) ОС/ ABI: UNIX - System V Версия ABI: 0 Тип: EXEC (исполняемый файл) Машина: Intel 80386 Версия: 0x1 Адрес точки входа: 0x0 Начало заголовков программы: 52 (байт в файл) Начало заголовков раздела: 1120 (байт в файл) ) Флаги: 0x0 Размер этого заголовка: 52 (байта) Размер заголовков программы: 32 (байта) Количество заголовков программы: 2 Размер заголовков раздела: 40 (байт) Количество заголовков раздела: 12 Индекс строки заголовка раздела: 11

Наконец, точка входа в этот файл находится по адресу 0x0.Это именно то, что мы сказали компоновщику — связать программу для запуска по адресу 0x00000000. И здесь функция main файла elf.c показана в objdump.

Загрузка программы в ядро

Выполнение программы начинается внутри ядра, в системном вызове exec("/bin/wc". ) указывается путь к исполняемому файлу. Ядро считывает заголовок ELF и таблицу заголовков программы (PHT), после чего выполняет множество проверок работоспособности.

Для статически связанных исполняемых файлов:

  1. Ядро считывает PHT и загружает части, указанные в директивах LOAD, в память.
  2. После загрузки частей, указанных в директивах LOAD, управление может быть передано точке входа программы, которой является main().

Для динамически подключаемых исполняемых файлов:

  1. Ядро считывает PHT и загружает в память части, указанные в директивах INTERP и LOAD. Динамически компонуемые программы всегда нуждаются в /lib/ld-linux.so в качестве интерпретатора, поскольку он включает некоторый код запуска, загружает общие библиотеки, необходимые для двоичного файла, и выполняет перемещение.
  2. После загрузки частей, указанных в директивах INTERP и LOAD, управление может быть передано интерпретатору.
  3. Динамический компоновщик (содержащийся в интерпретаторе):
    1. Просматривает раздел .dynamic, адрес которого хранится в PHT; и находит:
    2. Записи NEEDED определяют, какие библиотеки должны быть загружены перед запуском программы.
    3. Записи *REL*, содержащие адреса таблиц перемещения.
    4. Записи VER*, содержащие информацию о версиях символов.
    5. И т. д.

    Что вам нужно сделать в этом домашнем задании: загрузить файл ELF

    Хотя ELF может выглядеть немного пугающе, на практике алгоритм загрузки тривиален:

    1. Прочитайте заголовок ELF.
    2. Одно из полей заголовка ELF сообщает вам о смещении таблицы заголовков программы внутри файла.
    3. Прочитать каждую запись таблицы заголовков программы (т. е. прочитать каждый заголовок программы)
    4. Каждый заголовок программы имеет смещение и размер определенного сегмента внутри файла ELF (например, исполняемый код). Вы должны прочитать его из файла и загрузить в память.
    5. Когда закончите со всеми сегментами, перейдите к точке входа в программу. (Обратите внимание, так как в данный момент мы не контролируем размещение адресного пространства, мы загружаем секции в какое-то случайное место в памяти (место, выделенное нам функцией mmap()). Очевидно, что адрес точки входа должен быть смещением в пределах этой случайной области.Такая загрузка не будет работать для реального ELF-файла, но наш простой: он статически связан и содержит код, который может выполняться в любом месте памяти. несмотря на то, что он связан с запуском по адресу 0x0, он будет работать везде, где вы его загрузите.

    Выглядит управляемым. Сделаем пару упрощений. Сначала мы создаем очень простой файл ELF из elf.c: он содержит только одну функцию, в нем нет данных, а код может быть размещен в любом месте памяти и будет работать нормально (он просто не работает). ссылаются на любые глобальные адреса, все переменные находятся в стеке).

    Файл main.c содержит определения для структур, соответствующих заголовку ELF и записям таблицы заголовков программ. Таким образом, вы можете просто прочитать заголовок из файла ELF с помощью функций open, lseek и read, прочитать смещение заголовка программы table, получить количество записей в таблице заголовков программы из заголовка ELF и прочитать все записи одну за другой. Если запись имеет тип ELF_PROG_LOAD, вы загрузите ее в память.

    Чтобы загрузить сегмент в память, вы можете выделить исполняемую память с помощью следующей функции

    code_va = mmap(NULL, ph.memsz, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);

    где ph.memsz — размер сегмента, который мы сейчас загружаем.

    Вы должны выяснить, где находится точка входа вашей программы (она находится в одном из сегментов, а поскольку вы сопоставляете сегменты по адресам, возвращаемым функцией mmap(), вам нужно чтобы найти, где он заканчивается в вашем адресном пространстве.

    Если вы нашли точку входа, введите ее в указатель функции, соответствующий сигнатуре функции sum, и вызовите ее.

    Ваша программа должна принимать имя исполняемого файла elf в качестве единственного аргумента и возвращать результат, созданный функцией main, присутствующей в этом файле elf. В частности, если исходный код эльфа:

    Затем ваш загрузчик должен вывести следующий результат:

    Обратите внимание, что разные файлы elf (по одному) с разными определениями функции main будут переданы вашему загрузчику, чтобы проверить его правильность.

    Дополнительный балл: (бонус 10%)

    Попробуйте загрузить файл elf-data.c. Можете ли вы объяснить, почему происходит сбой?

    Устраните сбой, либо убедившись, что файл связан для работы по адресу, доступному внутри вашего процесса (несколько сложно обеспечить доступность любого конкретного адреса, но мы примем любое осмысленное решение), либо путем простого перемещения файла ELF при его загрузке.

    Отправьте свою работу

    Отправьте свое решение (zip-архив) через Gradescope в задании под названием HW3 — формат ELF. Пожалуйста, заархивируйте все ваши файлы (main.c, Makefile) и отправьте их. Если вы сделали дополнительный кредит, поместите этот main.c и Makefile в дополнительную папку с пометкой «extra». Обратите внимание, что необязательная часть должна находиться в корне zip-архива, а не внутри еще одной папки.

    Вы можете повторно отправить столько раз, сколько пожелаете. Если у вас возникнут проблемы со структурой, автогрейдер сообщит вам об этом. Структура zip-файла должна быть следующей:

    5, 0

    У меня есть несколько вопросов о файлах elf и о том, как они выполняются.

    Когда gcc компилирует файл elf, он создает исполняемый файл. Запускается ли этот исполняемый файл непосредственно аппаратным обеспечением или ядро ​​​​вмешивается, интерпретирует файл elf и размещает ассемблерный код непосредственно в памяти.

    Еще 9 обсуждений, которые могут вас заинтересовать

    1. Солярис

    Неверный класс ELF

    Ребята, кто-нибудь знает решение этой проблемы? неправильный класс ELF: ELFCLASS64 pwrcard:cms-dev:/pcard16/pwrcard/src/interfaces/interface_host>isainfo -v 64-разрядные приложения sparcv9 против 32-разрядных приложений sparc против 2 против v8plus div32 mul32. (1 ответ)

    Обсуждение начато: q8devilish

    2. Программирование оболочки и создание сценариев

    ps -elf И grep для изменений

    Надеюсь, что-то подобное уже существует. У меня есть список процессов, чьи значения "ps -elf" (поле 10) мне нужно постоянно отслеживать, и если значения поля 10 начинают значительно увеличиваться (удваиваться, утроиться), то что-то делать. Поле 10 является полем «размер памяти». (эти. (4 ответа)

    Обсуждение начато: ajp7701

    3. Программирование

    Помощь в создании файла ELF

    Привет. Как создать файл .ELF? Какой код я должен использовать, может помочь мне с простым кодом или примером? Я знаю программирование для Windows на важных языках, но это кажется более систематическим, и я действительно не знаю, как это сделать. (2 ответа)

    Обсуждение начато: linecoder

    4. BSD

    Двоичная поддержка DG/UX ELF

    У кого-нибудь есть двоичные файлы DG/UX ELF, работающие на старой системе *BSD? Я пробовал много разных типов, с скомпилированным COMPAT_SVR4 и поддержкой ibcs, все закончилось ошибками Segment Fault или Bus Error. (2 ответа)

    Обсуждение начато: dgux

    5. UNIX для продвинутых и опытных пользователей

    Динамическая загрузка и выполнение файлов ELF

    Уважаемая группа, я хочу подготовить ELF-файл, который можно будет динамически загружать по любому адресу в памяти и выполнять как новую задачу/поток/процесс. 1) для этого каковы все параметры компиляции, компоновщика при сборке файла ELF? 2) какие части файла ELF должны быть изменены при этом. (1 ответ)

    Обсуждение начато: ravinder.are

    6. Солярис

    Недействительный файл ELF

    Привет всем, у меня есть два диска, один основной, а другой зеркальный. Я проверил свой зеркальный диск, все было в порядке. После этого я загрузился со своего основного диска, сделал несколько установок, касающихся патчей ядра. Он не загружался должным образом. Он говорит ==================== Недействительный. (2 ответа)

    Обсуждение начато: jegaraman

    7. Программирование

    Таблица строк ELF

    <р>привет всем! Я хочу прочитать таблицу строк объектного файла (в формате ELF). Я получаю значение sh_name, но не могу найти способ прочитать значение в таблице строк, которое представляет этот индекс. Я программирую на C. Большое спасибо! (3 ответа)

    Обсуждение начато: nicos

    8. Linux

    редактирование файла ELF

    Здравствуйте, это не совсем относится к ядру Linux, но я все равно спрошу. Есть ли способ изменить 64-битный объектный файл ELF, чтобы он выглядел как 32-битный объектный файл ELF, и связать его (используя `ld`) с 32-битным файлом ELF? Я пробовал libelf, но безуспешно. У меня была эта красивая ссылка. (1 ответ)

    Читайте также: