Как создать elf файл в linux

Обновлено: 08.07.2024

Просто любопытно. Это, очевидно, не очень хорошее решение для реального программирования, но я хочу сделать исполняемый файл в Bless (шестнадцатеричный редактор).

Моя архитектура - x86. Какую очень простую программу я могу сделать? Привет, мир? Бесконечный цикл? Подобно этому вопросу, но в Linux.

ОТВЕТЫ

Ответ 1

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

Эквивалент /bin/true (45 байт):

Ваш классический "Hello World!" (160 байт):

Не забудьте сделать их исполняемыми.

Ответ 2

Декомпилировать мир приветствия NASM и понять каждый байт в нем

Стандарты

ELF задается LSB:

LSB в основном ссылается на другие стандарты с незначительными расширениями, в частности:

generic (оба по SCO):

Удобное резюме можно найти по адресу:

Его структура может быть рассмотрена с помощью удобных для пользователя способов, таких как readelf и objdump .

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

Позвольте сломать минимальный исполняемый пример Linux x86-64:

Скомпилировано с помощью

  • NASM 2.10.09
  • Binutils версия 2.24 (содержит ld )
  • Ubuntu 14.04

Мы не используем программу на C, так как это осложнит анализ, который будет уровнем 2: -)

шестнадцатеричных представлений бинарных

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

Файл ELF содержит следующие части:

Заголовок ELF. Указывает на позицию таблицы заголовка раздела и таблицы заголовков программ.

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

N разделов с N <= e_shnum (необязательно в исполняемом файле)

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

N сегментов, с N <= e_phnum (необязательно в исполняемом файле)

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

Заголовок 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.

1 0: e_type = 01 00 = 1 (big endian) = ET_REl : перемещаемый формат

В исполняемом файле 02 00 для ET_EXEC .

1 2: e_machine = 3e 00 = 62 = EM_X86_64 : архитектура AMD64

1 4: e_version = 01 00 00 00 : должно быть 1

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

В исполняемом файле это b0 00 40 00 00 00 00 00 . TODO: что еще мы можем установить? Ядро, кажется, помещает IP непосредственно в это значение, оно не является жестко запрограммированным.

2 0: e_phoff = 8x 00 : смещение таблицы заголовка программы, 0, если нет.

40 00 00 00 в исполняемом файле, то есть он начинается сразу после заголовка ELF.

2 8: e_shoff = 40 7x 00 = 0x40 : смещение файла таблицы заголовка раздела, 0, если нет.

3 0: e_flags = 00 00 00 00 TODO. Специально для Arch.

3 4: e_ehsize = 40 00 : размер этого заголовка эльфа. Почему это поле? Как это может измениться?

3 6: e_phentsize = 00 00 : размер каждого заголовка программы, 0, если нет.

38 00 в исполняемом файле: длина файла составляет 56 байтов

3 8: e_phnum = 00 00 : количество записей заголовка программы, 0, если нет.

02 00 в исполняемом файле: есть 2 записи.

3 A: e_shentsize и e_shnum = 40 00 07 00 : размер заголовка раздела и количество записей

3 E: e_shstrndx ( Section Header STRing iNDeX ) = 03 00 : индекс раздела .shstrtab .

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

Массив структур Elf64_Shdr .

Каждая запись содержит метаданные о данном разделе.

e_shoff заголовка ELF дает здесь начальную позицию, 0x40.

e_shentsize и e_shnum из заголовка ELF говорят, что у нас есть 7 записей, каждый длиной 0x40 .

Таким образом, таблица берет байты от 0x40 до 0x40 + 7 + 0x40 - 1 = 0x1FF.

readelf -S hello_world.o :

struct , представленный каждой записью:

Разделы

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

Содержится в байтах от 0x40 до 0x7F.

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

В разделе Figure 4-7: Special Section Indexes есть другие магические разделы.

В индексе 0, SHT_NULL является обязательным. Существуют ли для этого другие виды использования: Какая польза от раздела SHT_NULL в ELF??

.data раздел

.data - это раздел 1:

80 0: sh_name = 01 00 00 00 : индекс 1 в таблице строк .shstrtab

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

В этих разделах хранятся инициализированные данные, которые вносят вклад в образ памяти программы.

80 4: sh_type = 01 00 00 00 : SHT_PROGBITS : содержимое раздела не задано ELF, только тем, как программа интерпретирует его. Нормально, так как a .data .

90 0: sh_addr = 8x 00 : в каком виртуальном адресе раздел будет помещен во время выполнения, 0 если не помещен

90 8: sh_offset = 00 02 00 00 00 00 00 00 = 0x200 : количество байт от начала программы до первого байта в этом разделе

a0 0: sh_size = 0d 00 00 00 00 00 00 00

Если взять 0xD байт, начиная с sh_offset 200, мы видим:

AHA! Итак, наша строка "Hello world!" находится в разделе данных, как мы сказали, это на NASM.

Как только мы закончим hd , мы рассмотрим это как:

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

b0 0: sh_addralign = 04 = TODO: зачем это выравнивание необходимо? Это только для sh_addr , а также для символов внутри sh_addr ?

b0 8: sh_entsize = 00 = раздел не содержит таблицы. Если!= 0, это означает, что раздел содержит таблицу записей фиксированного размера. В этом файле мы видим из вывода readelf , что это имеет место для разделов .symtab и .rela.text .

.text раздел

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

.text является исполняемым, но не доступен для записи: если мы попытаемся написать ему Linux segfaults. Посмотрим, действительно ли у нас есть код:

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

Это выглядит как правильный код: a write , за которым следует exit .

Самая интересная часть - это строка a , которая делает:

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

Эта модификация возможна из-за данных раздела .rela.text .

SHT_STRTAB

Разделы с sh_type == SHT_STRTAB называются строковыми таблицами.

Они содержат нулевой разделительный массив строк.

Такие разделы используются другими разделами, когда имена строк должны использоваться. В разделе "Использование" говорится:

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

Так, например, мы могли бы иметь строковую таблицу, содержащую: TODO: нужно ли начинать с \0 ?

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

Известные строковые таблицы:

.shstrtab

Тип раздела: sh_type == SHT_STRTAB .

Общее имя: строка заголовка заголовка раздела.

Имя раздела .shstrtab зарезервировано. В стандарте говорится:

В этом разделе содержатся имена разделов.

В этом разделе указывается поле e_shstrnd самого заголовка ELF.

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

В этом разделе не указано SHF_ALLOC , поэтому оно не будет отображаться в исполняемой программе.

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

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

.symtab

Тип раздела: sh_type == SHT_SYMTAB .

Общее имя: таблица символов.

Сначала отметим, что:

В разделе SHT_SYMTAB эти числа означают, что:

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

Хороший инструмент высокого уровня для разборки этого раздела:

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

Записи имеют тип:

Как и в таблице разделов, первая запись волшебна и задана фиксированными бессмысленными значениями.

Запись 1 имеет ELF64_R_TYPE == STT_FILE . ELF64_R_TYPE продолжается внутри st_info .

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

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

10 12: st_info = 04

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

Биты 4-7 = ELF64_ST_BIND = Binding = 0 = STB_LOCAL . Требуемое значение для STT_FILE .

10 13: st_shndx = Таблица символов Таблица заголовков Индекс = f1ff = SHN_ABS . Требуется для STT_FILE .

20 0: st_value = 8x 00 : требуется для значения для STT_FILE

20 8: st_size = 8x 00 : нет выделенного размера

Теперь из readelf мы быстро интерпретируем остальные.

Есть два таких элемента, один указывает на .data , а другой на .text (индексы раздела 1 и 2 ).

TODO, какова их цель?

Затем введите наиболее важные символы:

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

_start отмечен видимостью GLOBAL , так как мы написали:

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

hello_world_len указывает на специальный st_shndx == SHN_ABS == 0xF1FF .

0xF1FF выбирается так, чтобы не противоречить другим разделам.

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

Это означает, что перемещение не повлияет на это значение: оно является константой.

Это небольшая оптимизация, которую делает наш ассемблер для нас и имеет поддержку ELF.

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

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

По умолчанию NASM размещает .symtab в исполняемом файле.

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

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

.strtab

Удерживает строки для таблицы символов.

В этом разделе sh_type == SHT_STRTAB .

Указывается на sh_link == 5 раздела .symtab .

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

.rela.text

Тип раздела: sh_type == SHT_RELA .

Общее имя: раздел перемещения.

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

В основном, он преобразует текст объекта, содержащий адрес заполнителя 0x0:

к фактическому исполняемому коду, содержащему окончательный 0x6000d8:

Указывалось sh_info = 6 раздела .symtab .

readelf -r hello_world.o дает:

Раздел не существует в исполняемом файле.

370 0: r_offset = 0xC: адрес в адрес .text , адрес которого будет изменен

370 8: r_info = 0x200000001. Содержит 2 поля:

  • ELF64_R_TYPE = 0x1: значение зависит от точной архитектуры.
  • ELF64_R_SYM = 0x2: индекс раздела, на который указывает адрес, поэтому .data , который находится в индексе 2.

AMD64 ABI говорит, что тип 1 называется R_X86_64_64 и что он представляет операцию S + A где:

  • S : значение символа в объектном файле, здесь 0 , потому что мы указываем на 00 00 00 00 00 00 00 00 из movabs $0x0,%rsi
  • a : добавление, присутствующее в поле r_added

Этот адрес добавляется в раздел, в котором работает перемещение.

Эта операция перемещения действует на 8 байтов.

380 0: r_addend = 0

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

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

Отображается только в исполняемом файле.

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

Исполняемый файл создается объектным файлом компоновщиком. Основные задания, которые выполняет компоновщик:

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

В Binutils это сводится к анализу компоновщика script и работе с множеством значений по умолчанию.

Вы можете получить компоновщик script, используемый с ld --verbose , и установить пользовательский с ld -T .

выполнять перемещение по текстовым разделам. Это зависит от того, как несколько разделов помещаются в память.

readelf -l hello_world.out дает:

В заголовке ELF e_phoff , e_phnum и e_phentsize сказали нам, что есть 2 заголовка программы, которые начинаются с 0x40 и длиной 0x38 байтов каждый, поэтому они:

Это, казалось бы, простой вопрос, но есть еще несколько вопросов для изучения в Linux. Я нашел статью по этому вопросу, но она может быть старше, и она все еще тестируется на машинах x86.

начать

Вопрос прост: как Linux выполняет мою функцию main ()?
В этом документе ниже я буду использовать простую программу на c, чтобы проиллюстрировать, как она работает. Файл этой программы называется «simple.c».

Compile

Создать исполняемый файл просто.

Что в исполняемом файле?

Чтобы увидеть, что находится в исполняемом файле, мы используем инструмент "objdump"

Вывод дает некоторую ключевую информацию. Во-первых, формат этого файла - «ELF64». Вторым является начальный адрес выполнения программы "0x080482d0".

Что такое ELF?

ELF является аббревиатурой от исполняемого и связующего формата. Это один из нескольких форматов исполняемых файлов для систем UNIX. Для нашей дискуссии интересной особенностью ELF является формат заголовка. Каждый исполняемый файл ELF имеет заголовок ELF, например:

В приведенной выше структуре поле «e_entry» является начальным адресом исполняемого файла.

Что хранится по адресу "0x080482d0"? Это начальный адрес выполнения программы?

Для этой проблемы давайте разберем «просто». Для разборки исполняемых файлов доступно несколько инструментов. Я использовал objdump здесь:

Вывод немного длинный, и я не буду анализировать весь вывод objdump. Мы намерены посмотреть, что хранится по адресу 0x080482d0. Вот вывод:

Похоже, что процедура запуска называется "_start". Он очищает регистр, помещает некоторые данные в стек и вызывает функцию.

Три вопроса

Теперь, может быть, вы уже думали об этом.Рамка стекаУ нас есть несколько вопросов.

  • Что это за шестнадцатеричные числа?
  • Что хранится по адресу 80482bc, и какая функция вызывается _start?
  • Кажется, что эти инструкции по сборке не инициализировали регистры с некоторыми значащими значениями. Так кто же будет инициализировать эти регистры?

Давайте ответим на это один за другим.

Q1> О шестнадцатеричных числах

Если вы внимательно посмотрите на результаты разборки, полученные с помощью objdump, вы можете легко ответить на этот вопрос.
Вот ответ на этот вопрос:

0x80483d0: Это адрес функции main ().

0x8048274: адрес функции _init ().

0x8048420: адрес функции _finit ().

_init и _finit являются функциями инициализации / финализации, предоставляемыми GCC.

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

Q2> Что хранится по адресу 80482bc?

Давайте снова посмотрим на адрес 80482bc в выводе разборки.
Если вы видите это, код сборки выглядит следующим образом:

* 0x8049548 здесь указатель операции. Он переходит к значению адреса, хранящемуся по адресу 0x8049548.

Подробнее об ELF и динамических ссылках

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

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

Эта концепция примерно описана следующим образом:

  1. Мы не знаем фактический адрес динамического символа во время ссылки. Только во время выполнения мы можем знать этот фактический адрес.
  2. Поэтому для динамических символов мы резервируем единицу хранения для ее фактического адреса. Загрузчик заполняет область памяти фактическим адресом динамического символа во время выполнения.
  3. Наше приложение знает место хранения динамических символов косвенно, используя операцию указателя. В нашем примере по адресу 80482bc есть простая инструкция перехода. Перемещенный модуль сохраняется по адресу 0x8049548 загрузчиком во время выполнения.

Мы можем увидеть все записи динамической ссылки, используя команду objdump:

Адрес 0x8049548 называется «JUMP SLOT», что очень уместно. Согласно этой таблице, мы на самом деле хотим вызвать __libc_start_main.

Что такое __libc_start_main?

Мы играли в эстафету, и теперь мяч был передан libc. __libc_start_main - это функция в libc.so.6. Если вы ищете исходный код __libc_start_main в glibc, его прототип может выглядеть так:

Все, что нужно сделать инструкции по сборке, - это создать стек параметров и затем вызвать __libc_start_main. Эта функция должна создать / инициализировать некоторые структуры данных / среды и затем вызвать нашу функцию main (). Давайте посмотрим на кадр стека для этого прототипа функции,

В соответствии с этим стековым фреймом мы знаем, что регистры esi, ecx, edx, esp и eax должны быть заполнены соответствующими значениями перед выполнением функции __libc_start_main (). Ясно, что эти регистры не заполнены инструкциями по сборке при запуске, которые мы показали ранее. Итак, кто заполнил эти регистры? Осталось только одно место - ядро. Вернемся теперь к третьему вопросу.

Q3> Что делает ядро?

Когда мы выполняем программу, вводя имя в оболочку, вот что происходит с Linux:

  1. Оболочка вызывает системный вызов ядра «execve» с параметрами argc / argv.
  2. Дескриптор системного вызова ядра начинает обработку этого системного вызова. В коде ядра этот дескриптор - «sys_execve». На компьютерах с архитектурой x86 приложения пользовательского режима передают все необходимые параметры ядру через следующие регистры.
  • ebx: строка имени исполнителя
  • ecx: указатель массива argv
  • edx: указатель на массив переменных среды
  1. Общий дескриптор системного вызова ядра execve, то есть do_execve, вызывается. Он создает структуру данных, копирует все данные пользовательского пространства в пространство ядра и, наконец, вызывает search_binary_handler (). Linux может поддерживать несколько форматов исполняемых файлов, таких как a.out и ELF. Для этой функции существует структура данных "struct linux_binfmt". Для каждого загрузчика двоичного формата в этой структуре данных будет указатель на функцию. search_binary_handler () найдет подходящий дескриптор и вызовет его. В нашем случае подходящим дескриптором является load_elf_binary (). Разъяснение каждой детали функции - очень утомительная работа. Так что я не буду делать это здесь. Если вам интересно, почитайте соответствующие книги. Далее идет конец функции.Вначале создается структура данных ядра для файловых операций для чтения в образе ELF. Затем он создает другую структуру данных ядра, которая содержит: размер кода, начало сегмента данных, начало сегмента стека и т. Д. Затем назначьте этому процессу страницу пользовательского режима, скопируйте переменные argv и окружения в выделенный адрес страницы. Наконец, указатели argc и argv и указатели массива переменных среды помещаются в стек пользовательского режима через create_elf_tables (), а start_thread () используется для запуска процесса.

При выполнении инструкции по сборке _start кадр стека будет выглядеть следующим образом.

Инструкции по сборке получают всю информацию из стека следующими способами:

Теперь все готово к работе.

А как насчет других регистров?

Для esp это используется как основание стека приложения. После получения всей необходимой информации подпрограмма _start просто настраивает указатель стека (esp), убирая 4 младших бита регистра esp. Это имеет смысл. Для нашей основной программы это нижняя часть стека. Для edx он используется rtld_fini, который является деструктором приложения. Ядро использует следующее определение макроса, чтобы установить его в 0:

0 означает, что мы не будем использовать эту функцию на x86 Linux.

О инструкции по сборке

Откуда эти ассемблерные коды? Это часть кодов GCC. Объектные файлы для этих кодов обычно находятся в / usr / lib / gcc-lib / i386-redhat-linux / XXX и / usr / lib, где XXX - номер версии gcc. Имена файлов: crtbegin.o, crtend.o и gcrt1.o.

резюме

Давайте подведем итоги всего процесса.

  1. GCC компилирует вашу программу с помощью crtbegin.o / crtend.o / gcrt1.o. Другие библиотеки по умолчанию динамически связаны по умолчанию. Начальный адрес исполняемой программы установлен на _start.
  2. Ядро загружает исполняемый файл и создает раздел тела, раздел данных, раздел bss и раздел стека, в частности, ядро ​​распределяет страницы для параметров и переменных среды и помещает всю необходимую информацию в стек.
  3. Поток управления переходит на _start. _start получает всю информацию из стека, установленного ядром, устанавливает стек параметров для __libc_start_main и вызывает __libc_start_main.
  4. __libc_start_main инициализирует некоторые необходимые вещи, особенно потоковую среду библиотеки C (например, malloc), и вызывает нашу основную функцию.
  5. Наш main будет называться main (argv, argv). На самом деле, интересным моментом здесь является сигнатура основной функции. __libc_start_main считает подпись main главной (int, char, char ) Если вам интересно, попробуйте следующую процедуру.

вывод

В Linux наша функция C main () выполняется в сотрудничестве GCC, libc и двоичного загрузчика Linux.

просто любопытно. Это, очевидно, не очень хорошее решение для фактического программирования, но, скажем, я хотел сделать исполняемый файл в Bless (hex editor).

моя архитектура x86. Какую очень простую программу я могу сделать? Привет миру? Бесконечная петля? Похожие на этой вопрос, но в Linux.

Как упоминалось в моем комментарии, вы по существу будете писать свой собственный ELF-заголовок для исполняемого файла, устраняя ненужные разделы. Есть еще несколько необходимых разделов. Документация по адресу Muppetlabs-TinyPrograms делает справедливую работу, объясняя этот процесс. Для удовольствия, вот несколько примеров:

эквивалент /bin / true (45 байт):

классический 'Привет, Мир!'(160 байт):

Не забудьте сделать их исполняемыми.

Декомпилируйте NASM hello world и поймите каждый байт в нем

стандарты

ELF указывается LSB:

LSB в основном ссылается на другие стандарты с незначительными расширениями, в частности:

generic (оба по SCO):

удобное резюме можно найти по адресу:

его структура может быть рассмотрена читаемым человеком способом с помощью утилит, таких как readelf и objdump .

создать пример

давайте сломаем минимальный запускаемый Linux x86-64 пример:

  • NASM 2.10.09
  • Binutils версии 2.24 (содержит ld )
  • Ubuntu 14.04

мы не используем программу на C, так как это усложнит анализ, это будет Уровень 2 :-)

Hexdumps

глобальный файл структура

файл ELF содержит следующие части:

заголовок ELF. Указывает на положение таблицы заголовка раздела и таблицы заголовка программы.

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

N секций, с N <= e_shnum (необязательный на исполняемый файл)

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

N сегментов, с N <= e_phnum (необязательно для исполняемого файла)

порядок этих частей составляет не исправлено: единственная исправленная вещь-заголовок ELF, который должен быть первым в файле: Generic docs say:

эльф заголовок

самый простой способ наблюдать за заголовком:

байт в объектном файле:

0 0: EI_MAG = 7f 45 4c 46 = 0x7f 'E', 'L', 'F' : эльф магия номер

0 4: EI_CLASS = 02 = ELFCLASS64 : 64 бит elf

0 5: EI_DATA = 01 = ELFDATA2LSB : big endian data

0 6: EI_VERSION = 01 : версия формата

0 7: EI_OSABI (только в обновлении 2003) = 00 = ELFOSABI_NONE : никаких расширений.

0 8: EI_PAD = 8x 00 : зарезервировано байт. Должно быть установлено в 0.

1 0: e_type = 01 00 = 1 (обратный порядок байтов) = ET_REl : формат перемещаемых

на исполняемом файле это 02 00 на ET_EXEC .

1 2: e_machine = 3e 00 = 62 = EM_X86_64 : архитектура AMD64

1 4: e_version = 01 00 00 00 : должно быть 1

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

на исполняемом файле это b0 00 40 00 00 00 00 00 . TODO: на что еще мы можем это установить? Ядро, похоже, помещает IP непосредственно в это значение, оно не жестко закодировано.

2 0: e_phoff = 8x 00 : смещение таблицы заголовка программы, 0 если нет.

40 00 00 00 на исполняемом файле, т. е. он запускается сразу после заголовка ELF.

2 8: e_shoff = 40 7x 00 = 0x40 : смещение файла таблицы заголовка раздела, 0 если нет.

3 0: e_flags = 00 00 00 00 TODO. Arch specific.

3 4: e_ehsize = 40 00 : размер этого заголовка elf. TODO почему это поле? Как она может меняться?

3 6: e_phentsize = 00 00 : размер каждого заголовка программы, 0 если нет.

38 00 on исполняемый файл: это 56 байт

3 8: e_phnum = 00 00 : количество записей заголовка программы, 0 если нет.

02 00 на исполняемом файле: есть 2 записи.

3 A: e_shentsize и e_shnum = 40 00 07 00 : размер заголовка раздела и количество записей

3 E: e_shstrndx ( Section Header STRing iNDeX ) = 03 00 : индекс .

заголовок раздела таблица

массив Elf64_Shdr структуры.

каждая запись содержит метаданные о данном разделе.

e_shoff заголовка ELF дает начальную позицию, 0x40 здесь.

e_shentsize и e_shnum из заголовка ELF говорят, что у нас есть 7 записей, каждая 0x40 байт.

таким образом, таблица принимает байты от 0x40 до 0x40 + 7 + 0x40 - 1 = 0x1FF.

readelf -S hello_world.o :

struct в лице каждой записи:

разделы

90 0: sh_addr = 8x 00 : в каком виртуальном адресе будет размещен раздел во время выполнения, 0 если не помещено

90 8: sh_offset = 00 02 00 00 00 00 00 00 = 0x200 : число байт от начала программы до первого байта в этом разделе

a0 0: sh_size = 0d 00 00 00 00 00 00 00

если мы возьмем 0xD байта, начиная с sh_offset 200, видим:

АХА! Так наши "Hello world!" строка находится в разделе данных, как мы сказали, чтобы быть на NASM.

как только мы закончим hd , мы будет выглядеть так:

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

b0 0: sh_addralign = 04 = TODO: почему это выравнивание необходимо? Это только для sh_addr , или также для символов внутри sh_addr ?

b0 8: sh_entsize = 00 = раздел не содержать таблицу. Если != 0, это означает, что раздел содержит таблицу записей фиксированного размера. В этом файле мы видим из readelf выведите, что это относится к .symtab и .rela.text разделы.

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

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

.text является исполняемым, но не записываемым: если мы попытаемся написать на него Linux с падениями. Давайте посмотрим, действительно ли у нас есть какой-то код:

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

это выглядит как правильный код: write затем exit .

самая интересная часть-line a что делает:

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

это изменение возможно из-за данных .

sht_strtab и атрибут

разделы sh_type == SHT_STRTAB называют строка таблицы.

они содержат null разделенный массив веревка.

такие разделы используются другими разделами при использовании имен строк. В разделе using говорится:

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

например, у нас может быть таблица строк, содержащая: TODO: она должна начинаться с ?

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

примечательные разделы таблицы строк:

.shstrtab

тип раздела: sh_type == SHT_STRTAB .

общее имя: таблица строк заголовка раздела.

название раздела .shstrtab зарезервирован. Стандарт гласит:

этот раздел содержит имена разделов.

на этот раздел указывает e_shstrnd поле самого заголовка ELF.

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

в этом разделе нет SHF_ALLOC отмечен, поэтому он не будет отображаться в исполняющей программе.

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

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

.symtab

тип раздела : sh_type == SHT_SYMTAB .

общее имя: таблица символов.

Сначала отметим, что:

на SHT_SYMTAB разделы, эти цифры означают, что:

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

хороший инструмент высокого уровня для разборки этого раздела:

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

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

запись 1 имеет ELF64_R_TYPE == STT_FILE . ELF64_R_TYPE продолжение внутри st_info .

10 8: st_name = 01000000 = символ 1 в .strtab , который до делает hello_world.asm

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

10 12: st_info = 04

биты 0-3 = ELF64_R_TYPE = Type = 4 = STT_FILE : основная цель этой записи-использовать st_name to Укажите имя файла, который сгенерировал этот объектный файл.

биты 4-7 = ELF64_ST_BIND = обязательного = 0 = STB_LOCAL . Требуемое значение для STT_FILE .

10 13: st_shndx = индекс заголовка раздела таблицы символов = f1ff = SHN_ABS . Требуется для STT_FILE .

20 0: st_value = 8x 00 : требуется для значения для STT_FILE

20 8: st_size = 8x 00 : не выделены размере

теперь из readelf , мы быстро интерпретируем остальные.

есть две такие записи, одна из которых указывает на .data , а другой .text (индексы разделе 1 и 2 ).

TODO какова их цель?

затем идут самые важные символы:

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

_start обозначен GLOBAL видимость, так как мы написали:

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

hello_world_len указывает на специальные st_shndx == SHN_ABS == 0xF1FF .

0xF1FF выбирается так, чтобы не конфликтовать с другими разделами.

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

это означает, что перемещение не повлияет на это значение: это константа.

это небольшая оптимизация, которую наш ассемблер делает для нас и которая имеет поддержку ELF.

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

Раздел sht_symtab на исполняемый

по умолчанию NASM помещает .symtab на исполняемом файле, а также.

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

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

.strtab

содержит строки для символа таблица.

в этом разделе sh_type == SHT_STRTAB .

он указал sh_link == 5 на .

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

.Рела.текст

тип раздела: sh_type == SHT_RELA .

общее имя: раздел переезд.

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

в основном, он переводит текст объекта, содержащий адрес заполнителя 0x0:

к фактическому исполняемому коду, содержащему окончательный 0x6000d8:

на него указал sh_info = 6 of the .

readelf -r hello_world.o выдает:

раздел не существует в исполняемом файле.

на struct является:

370 0: r_offset = 0xC: адрес в .text чей адрес это перемещение изменит

370 8: r_info = 0x200000001. Содержит 2 поля:

  • ELF64_R_TYPE = 0x1: значение зависит от точной архитектуры.
  • ELF64_R_SYM = 0x2: индекс раздела, на который указывает адрес, так что .data который находится в индексе 2.

amd64 ABI говорит, что тип 1 называется R_X86_64_64 и что он представляет собой операцию S + A где:

  • S : значение символа в объектном файле, здесь 0 потому что мы указываем на 00 00 00 00 00 00 00 00 of movabs x0,%rsi
  • A : добавление, присутствует в поле r_added

этот адрес добавляется в раздел, на котором выполняется перемещение.

эта операция перемещения действует в общей сложности 8 байт.

380 0: r_addend = 0

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

таблица заголовка программы

отображается только в исполняемом файле.

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

исполняемый файл создается компоновщиком из объектных файлов. Основные задания, которые выполняет компоновщик:

определите, какие разделы объектных файлов будут входить в какие сегменты выполнимый.

в Binutils это сводится к разбору скрипта компоновщика и работе с кучей значений по умолчанию.

вы можете получить сценарий компоновщика, используемый с ld --verbose , и установите пользовательский с ld -T .

сделать перемещение по тексту разделов. Это зависит от того, как несколько разделов помещаются в память.

readelf -l hello_world.out выдает:

на заголовке ELF, e_phoff , e_phnum и e_phentsize сказал нам, что есть 2 заголовка программы, которые начинаются с 0x40 и 0x38 байт длиной каждый, поэтому они:

Линковка это процесс компоновки различных кусков кода и данных вместе, в результате чего получается один исполняемый файл. Линковка может быть выполнена во время компиляции, во время загрузки (загрузчиком) и также во время исполнения (исполняемой программой). Раньше (конец 40-х) линковка выполнялась вручную, сейчас мы имеем программы линковщики (linkers), которые дают возможность динамической линковки разделяемых библиотек (shared libraries).

3 Основы

Пусть у нас есть два файла с кодом a.c и b.c. Чтобы скомпилировать эти два файла при помощи GCC, мы вызываем следующий код

Это вызывает следующую последовательность:

Запустить препроцессор на файле a.c и сохранить результат в промежуточный файл a.i

Запустить компилятор на a.i и сгенерировать ассемблерный код в a.s

Запустить ассемблер на a.s и сгенерировать объектный файл a.o

Работа линковщика состоит в том, чтобы получить на вход сгенерированные объектные файлы a.o и b.o и сгенерировать из них финальный исполняемый файл a.out

После этого мы можем запустить наш бинарный файл ./a.out. Оболочка командной строки вызовет функцию загрузчика, которая скопирует код и данные из исполняемого файла a.out в память, затем передаст управление в начало программы. Функция загрузчик называется execve, она загружает код и данные исполняемых объектных файлов в память, затем запускает их выполнение, прыгая на первую инструкцию.

4 Линковщики и Загрузчики

Линковщики (linkers) и загрузчики (loaders) выполняют концептуально разные, но в целом похожие задачи:

  • Загрузка программ. Копирование образа программы с жёсткого диска в RAM. В некоторых случаях загрузка программы (loading) также может включать выделение дисковой памяти или отображение виртульного адресного пространства на дисковое пространство.
  • Релокация (relocation). Компиляторы и ассемблеры генерируют объектный код для каждого входного модуля программы с началом адресации в нуле. Релокация — это процесс изменения адреса загрузки различных частей программы во время объединения всех секций одного типа в одну секцию. Секции кода и данных таким образом будут указывать на корректные адреса в рантайме.
  • Symbol Resolution. Программы имеют внутри себя множество подпрограмм; указание одной подпрограммы на другую подпрограмму происходит через символьные таблицы. Работа линковщика — подменять указания на символ подпрограммы на указание адреса расположения подпрограммы, изменяя объектный код.

В итоге, получается что загрузчик выполняет загрузку программ; линковщик выполняет symbol resolution; оба выполняют релокацию.

5 Объектные файлы

  • Перемещаемый объектный файл (relocatable object file) — содержит бинарный код и данные в форме, которая может быть скомпонована с другими перемещаемыми объектными файлами во время компиляции. В итоге получаем исполняемый объектный файл, скомпонованный из перемещаемых объектный файлов.
  • Исполняемый объектный файл (executable object file) — содержат бинарный код и данные в форме, которая может быть напрямую загружена в память и выполнена.
  • Разделяемый объектный файл (shared object file) — специальный тип перемещаемого объектного файла, который может быть загружен в память и слинкован динамически либо во время загрузки в память, либо во время выполнения.

Компиляторы и ассемблеры генерируют перемещаемые объектные файлы (а так же разделяемые объектные файлы). Линковщики компонуют эти объектные файлы вместе и генерируют исполняемые объектные файлы.

6 ELF

Объектные файлы разнятся в разных ОС. Первые UNIX системы использовали формат a.out. Ранние System V использовали формат COFF (common object file format). Windows NT использует разновидность формата COFF, называемую PE (portable executable); IBM использует собственный формат IBM 360. Современные UNIX системы, такие как Linux и Solaris используют формат UNIX ELF (executable and linking format).

6.1 Заголовки Elf

.text the machine code of the compiled program. .rodata read-only data, such as the format strings in printf statements. .data initialized global variables .bss uninitialized global variables. BSS (начало блока данных — block storage start), эта секция обычно пустует в объектных файлах; этакая заглушка. .symtab таблица символов, содержащая информацию о функциях и глобальных переменных, определённых и адресованных в коде программы. Эта таблица не содержит записей о локальных переменных, эта информация содержится на стеке. .rel.text список мест в секции .text, которые необходимо модифицировать, когда линковщик будет компоновать этот объект с другими объектными файлами. .rel.data информация о релокации глобальных переменных, которые объявлены, но не определены в текущем модуле программы. .debug таблица отладочных символов с записями о локальных и глобальных переменных. Эта секция будет присутствовать только если компилятору был передан флаг компиляции с таблицей отладочных символов (-g для gcc). .line отображение номеров строк в исходном C-файле и машинными кодами инструкций. Эта информация необходима для отладки программ. .strtab таблица строк для таблицы символов .symtab и секции .debug

7 Символы и адресация символов

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

  • Глобальные символы объявленые на уровне модуля — могут быть адресованы из других модулей.Все не-статические и глобальные переменные попадают в эту категорию.
  • Глобальные символы адресованные в коде, но объевленные где-то вне. Все функции и переменные с модификатором extern попадают в эту категорию.
  • Локальные символы объявленные и адресованные исключительно во входном модуле. Все статические функции и статические переменные попадают в эту категорию.

Линковщик разрещает адресацию символов путём соотношения каждой ссылки на символ только к одному определению символу из таблицы символов.

8 Линковка статических библиотек

Статические библиотеки это коллекция конкатенированных объектных файлов схожего типа. Эти библиотеки хранятся на диске в архиве. Архив также содержит мета-информацию для ускорения поиска в нём. Каждый архив с ELF начинается с магической последовательности !<arch>\n. Статические библиотеки передаются на вход линковщику, который копирует только объектные модули, упоминаемые в программе. В процессе разрешения адресации символов при работе со статическими библиотеками линковщик сканирует перемещаемые объектные файлы и архивы справа-налево в порядке указания аргументов вызова. В процессе сканирования линковщик создаёт набор O-файлов (перемащаемых объектных файлов, которые будут включены в исполняемый файл); набор U-файлов (неразрешённых пока символов); набор D-файлов (символы, объявленные в предыдущих модулях). Изначально все три набора пустые.

  • На каждый следующий входной аргумент линковщик определяет передаётся ли объектный файл или архив. Если это перемещаемый объектный файл, то линковщик добавляет его в набор O, обновляет наборы U и D и переходит к следующему входному аргументу
  • Если входной аргумент архив, линковщик сканирует список членов модулей, входящих в архив, чтобы отыскать любые неразрешённые символы, находящиеся в наборе U. Если такие символы находятся, то они добавляются в список O и обновляется список U. Список D дополняется символами, найденными в архиве.
  • Когда все входные аргументы пройдены, но если набор U не пуст, то линковщик сообщает об ошибке линковки и завершает свою работу. Иначе, если набор U пуст, линковщик компонует и релоцирует объектные файлы из набора O и генерирует финальный исполняемый файл.

9 Релокация

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

  1. Релокация секций и определения символов. Линковщик объединяет все секции одного типа в новую секцию. К примеру, линковщик объединяет все секции .data всех входных перемещаемых объектов в новую секцию .data результирующего исполняемого файла. Похожий процесс происходит для секции .code. Затем линковщик указывает текущий адрес памяти для этой сгенерированной секции. Так для каждой секции и символа. После завершения этого шага каждая инструкция и глобальная переменная в прогармме будет иметь уникальный адрес в момент загрузки.
  2. Релокация адресации символов внутри секций. На этом шаге линковщик изменяет адресации на символы в коде и секциях данных так, чтобы они указывали на корректный уникальный адрес в момент загрузки.

Ассемблер при релокации создаёт секции .relo.text и .relo.data, в которых содержится информация как разрешить адресацию (адрес для обращения к символу). ELF содержит в секциях релокации следующие данные:

  • Смещение (offset). Для перемещаемых файлов значение смещения это смещение в байтах от начала секции до получившегося после релокации адреса.
  • Символ (symbol). Индекс символа в символьной таблице.
  • Тип (type). Тип релокации.

10 Динамическая линковка: разделяемые библиотеки

Статические библиотеки, описанные выше, имеют существенный недостаток. Например, возьмём стандартные функции printf и scanf. Они используются почти что в каждой программе. Пусть на системе запущено 50-100 процессов, каждый процесс содержит свою копию исполняемого кода printf и scanf — это существенный объём затраченной памяти. Разделяемые библиотеки в свою очередь направлены на исправление этого недостатка статических библиотек. Разделяемые библиотеки это объектные модули, которые могут быть загружены в память в момент исполнения программы и после слинкованы с программой. Разделяемые библиотеки (shared libraries) называют так же разделяемые объекты (shared objects). На большинстве систем UNIX они именуются с суффиксом .so; на системах HP-UX — с суфиксом .sl; на системах Microsoft они называются DLL. Чтобы собрать разделяемый объектный файл, компилятор надо вызывать со специальным флагом

Эта команда сообщает компилятору, что надо сгенерировать разделяемую библиотеку libfoo.so, собранную из объектный файлов a.o и b.o. Флаг -fPIC сообщает компилятору, что надо сгенерировать адресо-независимый код (position independent code — PIC). Теперь представим что объектный модуль bar.o зависит от a.o и b.o. В этом случае мы компилируем его так:

Эта команда создаёт исполняемый файл a.out, который будет линковаться с libfoo.so в момент загрузки. Здесь a.out не содержит в себе объектный модулей a.o и b.o, которые были бы включены в него, если бы мы использовали статическую линковку. Исполняемый файл просто содержит некоторую информацию о релокации и таблицу символов, которые позволяют адресоваться к коду и данным в libfoo.so и эта адресация будет разрешена в процессе исполнения (runtime). Таким образом, a.out это не совсем исполняемый файл, который имеет зависимость от libfoo.so. Исполняемый файл содержит секцию .interp, где содержится имя динамического линковщика (который сам является разделяемым объектом в системах Linux — ld-linux.so). Таким образом, когда исполняемый файл загружается в память, загрузчик передаёт управление динамическому линковщику. Динамический линковщик содержит некоторый код, который отображает пространство адресов динамических библиотек на пространство адресов испольняемой программы.

  1. Происходит релокация кода и данных из libfoo.so в область памяти
  2. Происходит релокация адресации в a.out на символы объявленные в libfoo.so.

В конце работы динамический линковщик передаёт контроль исполняемой программе. С этого момента местоположение разделяемого объекта зафиксировано в памяти.

11 Загрузка разделяемой библиотеки из приложения

Разделяемая библиотека может быть загружена из приложения в любой момент выполнения. Приложение может обратиться к динамическому линковщику с просьбой загрузить и прилинковать динамическую библиотеку. Linux, Solaris и другие системы поддерживают различниые функции, которые могут быть использованы для динамической загрузки разделяемых объектов. В Linux это системные вызовы dlopen, dlsym, dlclose, используемые для загрузки разделяемого объекта, поиска символа в разделяемом объекте и для закрытия разделяемого объекта.

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