Содержание статьи
Вспомним сигнатуру функции main в C:
int main(int argc, char** argv)
Откуда берутся число аргументов (argc) и массив указателей на их строки (argv)? Как возвращаемое значение main становится кодом возврата самой программы?
Краткий ответ: зависит от архитектуры процессора. К сожалению, доступных для начинающих материалов по самой распространенной сейчас архитектуре x86-64 не так много, и интересующиеся новички вынуждены сначала обращаться к старой литературе по 32-битным x86, которая следует другим соглашениям.
Попробуем исправить этот пробел и продемонстрировать прямое взаимодействие с машиной и ядром Linux сразу в 64-битном режиме.
Демонстрационная задача
Для демонстрации мы напишем расширенную версию hello world, которая может приветствовать любое количество объектов или людей, чьи имена передаются в аргументах команды.
$ ./hello Dennis Brian Ken
Hello Dennis!
Hello Brian!
Hello Ken!
Среда разработки
Для демонстрации мы будем использовать Linux и GNU toolchain (GCC и binutils), как самые распространенные ОС и среда разработки. Писать мы будем на языке ассемблера, потому что продемонстрировать низкоуровневое взаимодействие с ОС из языка сколько-нибудь высокого уровня невозможно.
Очень краткая справка
Чтобы упростить чтение статьи тем, кто вообще никогда не сталкивался с ассемблером x86, я использовал только самые простые инструкции и постарался аннотировать их псевдокодом везде, где возможно. Я использую синтаксис AT&T, который все инструменты GNU используют по умолчанию. Нужно помнить, что регистры пишутся с префиксом % (например, %rax), а константы — c префиксом $. Например, $255, $0xFF, $foo — значение символа foo.
Синтаксис указателей: смещение(база, индекс, множитель). Очень краткая справка:
mov <источник>, <приемник>— копирует значение из источника (регистра или адреса) в приемник;push <источник>— добавляет значение из источника на стек;pop <приемник>— удаляет значение из стека и копирует в приемник;call <указатель на функцию>— вызывает функцию по указанному адресу;ret— возврат из функции;jmp <адрес>— безусловный переход по адресу (метке);incиdec— инкремент и декремент;cmp <значение1> <значение2>— сравнение и установка флагов (например, равенство);je <метка>— переход на метку в случае, если аргументыcmpоказались равными.
Условные переходы и циклы реализуются через инструкции сравнения и условные переходы. Инструкции сравнения устанавливают определенные разряды в регистре флагов, команда условного перехода проверяет их и принимает решение, переходить или нет. Например, следующий цикл увеличивает значение регистра %rax, пока оно не станет равным 10, а затем копирует его в %rbx.
Для изучения ассемблера x86 я могу посоветовать книгу Programming From the Ground Up — к сожалению, ориентированную на 32-битную архитектуру, но очень хорошо написанную и подходящую новичкам.
mov $0, %rax # rax = 0
my_loop:
inc %rax
cmp $10, %rax
jne my_loop # Jump if Not Equal
mov %rax, %rbx # %rbx = 10
О регистрах: в x86-64 их куда больше. Кроме традиционных, добавлены регистры от %r8 до %r15, всего шестнадцать 64-битных регистров. Чтобы обратиться к нижним байтам новых регистров, нужно использовать суффиксы d, w, или b. То есть %r10d — нижние четыре байта, %r10w — нижние два байта, %r10b — нижний байт.
Что входит в соглашения ABI?
SystemV ABI, которой в большей или меньшей степени следуют почти все UNIX-подобные системы, состоит из двух частей. Первая часть, общая для всех систем, описывает формат исполняемых файлов ELF. Ее можно найти на сайте SCO.
К общей части прилагаются архитектурно зависимые дополнения. Они описывают:
- соглашение о системных вызовах;
- соглашение о вызовах функций;
- организацию памяти процессов;
- загрузку и динамическое связывание программ.
Формат ELF
Знать формат ELF в деталях, особенно его двоичную реализацию, нужно только авторам ассемблеров и компоновщиков. Эти задачи мы в статье не рассматриваем. Тем не менее пользователю следует понимать организацию формата.
Файлы ELF состоят из нескольких секций. Компиляторы принимают решение о размещении данных по секциям автоматически, но ассемблеры оставляют это на человека или компилятор. Полный список можно найти в разделе Special Sections. Вот самые распространенные:
.text— основной исполняемый код программы;.rodata— данные только для чтения (константы);.data— данные для чтения и записи (инициализированные переменные);.bss— неинициализированные переменные известного размера.
Соглашения о вызовах
Соглашение о вызовах — важная часть ABI, которая позволяет пользовательским программам взаимодействовать с ядром, а программам и библиотекам — друг с другом. В соглашении указывается, как передаются аргументы (в регистрах или на стеке), какие именно регистры используются и где хранится результат. Кроме того, оговаривается, какие регистры вызываемая функция обязуется сохранить нетронутыми (callee-saved), а какие может свободно перезаписать (caller-saved).
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»
