Отметьте корректные ассемблерные инструкции синтаксис intel

Free Pascal в своих блоках asm поддерживает синтаксис Intel для семейства процессоров Intel Ix86.Синтаксис Intel в вашем блоке asm преобразуется компилятором в синтаксис AT&T, после чего ассемблерный блок вставляется в компилируемый исходный код. Поддерживаемые конструкции ассемблера являются поднабором обычного синтаксиса ассемблера. Из этого следует, что специфические конструкции не поддерживаются в Free Pascal, но существуют в Turbo Pascal:

Квалификатор TBYTE не поддерживается.

Идентификатор & не поддерживается.

Оператор HIGH не поддерживается.

Оператор LOW не поддерживается.

Операторы OFFSET и SEG не поддерживаются. Используйте LEA и вариации инструкции Lxx вместо него.

Выражения со строковыми константами не допускаются.

Доступ к полям записей через скобки не допускается.

Преобразование типов с обычными типами Pascal не допускается, только распознаваемые типы ассемблера допускается преобразовывать.
Например:

mov al, byte ptr MyWord — допускается,
mov al, byte(MyWord) — допускается,
mov al, shortint(MyWord) –- НЕ допускается.

Преобразование типов с константами не допускается.
Например

const s= 10; const t = 32767;

в Turbo Pascal:

mov al, byte(s) –- бесполезное преобразование типов.
mov al, byte(t) –- ошибка синтаксиса!

Анализатор в обоих случаях выдаст ошибку.

Ссылки на выражения, содержащие только константы, не допускаются (во всех случаях они не работают в защищённом режиме, например, под LINUX i386 ).

Примеры:

mov al,byte ptr [‘c’] — НЕ допускается.
mov al,byte ptr [100h] — НЕ допускается.

(Это связано с ограничениями GNU Assembler ).

Скобки внутри квадратных скобок не допускаются.

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

mov al,[ds:bx] — НЕ допускается

используйте вместо этого:

mov al,ds:[bx]

Допустимые способы индексации:

Sreg:[REG+REG*SCALING+/-disp]

SReg:[REG+/-disp]– Sreg:[REG]

Sreg:[REG+REG+/-disp]

SReg:[REG+REG*SCALING]

Где Sreg является не обязательным и определяет сегмент.
Примечание:

1.В отличие от Turbo Pascal порядок следования имеет значение.

2.Значение Scaling должно быть числом, а не идентификатором или символом.

Примеры:

const myscale = 1;

mov al,byte ptr [esi+ebx*myscale] — НЕ допускается.

используется:

mov al, byte ptr [esi+ebx*1]

Допустимы синтаксис идентификатора переменной выглядит следующим образом (ID = идентификатор переменной или типизированной константы):

1. ID

2. [ID]

3. [ID+expr]

4. ID[expr]

Допустимые поля записей следующие:

1. ID.subfield.subfield …

2. [ref].ID.subfield.subfield …

3. [ref].typename.subfield …

Локальные метки. В отличие от Turbo Pascal, локальные метки должны содержать, по крайней мере, один символ после указателя локальной метки.
Например:

@: — НЕ допускается

используйте вместо этого:

@1: — допускается

В отличие от Turbo Pascal, локальные ссылки не могут использоваться как ссылки, только как перемещения.
Например:

lds si,@mylabel — НЕ допускается

В отличие от Turbo Pascal, сегменты SEGCS, SEGDS, SEGES и SEGSS в настоящее время не поддерживаются. (Их планируется добавить в будущем).

В отличие от Turbo Pascal, где спецификаторы размера памяти могут быть практически везде, встроенный ассемблер Free Pascal Intel требует использовать спецификаторы размера памяти внутри квадратных скобок.
Пример:

mov al,[byte ptr myvar] — НЕ допускается.

Используйте:

mov al,byte ptr [myvar] — допускается.

Регистры базы и индекса должны быть 32-разрядными (ограничение GNU Assembler ).

XLAT является эквивалентным XLATB.

Поддерживаются только опкоды Single и Double FPU.

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

Встроенный ассемблер Intel поддерживает следующие макросы:

@Result — представляет результат работы функции и возвращает значение.

Self — представляет указатель на метод объекта в методах.

Подготовка к работе

Данная статья или раздел ещё не завершены
Кто-то посчитал, что статья или раздел ниже не содержит какой-то важной информации или имеет проблемы с вёрсткой/текстом. Указана причина: дать ссылки на еще подобные источники в открытом доступе{{#ifeq: {{{1}}} | nopoint | | . }}

{{#if: Доработка | }}

Итак, первая программа у нас будет выводить строку «Hello world!» в текстовый терминал. Сделано это будет на языках программирования C и ассемблер (далее ASM).

Работать будем в консоли. Про использование комманд cd, cp, mv, mkdir, pwd, ls, echo, cat и прочие базовые вещи — Что такое командная строка и как в ней работать.

Создайте себе директорию (каталог), и туда складывайте свои первые программы, можете именовать их определенным образом. Термин «папка» из мира Windows в рамках этой книги (или набора статей) использоваться не будет. Не разводите в директориях бардак, каждую свою программу держите в отдельной директории. Про системы управления версиями (VCS) тут рассказываться не будет, потому что про это уже достаточно много чего написано. Например, про Git есть книга.

Будем считать, что у вас будут такие директории:

~/learn/asm/
~/learn/c/

Первая наша программа будет написана на языке С и будет выводить текст Hello, world!.

Будем работать в директории

~/learn/c/01_hello/puts_hello/

У нас будет несколько версий puts-хелловорлдов на Си, с ними мы будем делать всякие интересные вещи. Первая версия будет иметь номер 0.1.

В итоге должно получиться такое дерево каталогов:

~
└── learn
    ├── asm
    └── c
        └── 01_hello
            └── puts_hello
                └── 0.1

Создавать все эти директории я советую через консоль, а не через графические файловые менеджеры, типа Nautilus, Dolphin и прочее.
Делать это будем в домашней директории ~
Вот примерно так:

mkdir -p ~/learn/{asm,c}
cd $_
mkdir -p 01_hello/puts_hello/0.1
cd $_

Совет

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

Приступаем к делу

Для начала, рассмотрим простейшую программу на Си и то, в какой код на ассемблере она переводится (компилируется) компилятором

Создадим файл с таким содержанием:
<syntaxhighlight lang=»c»>

  1. include <stdio.h>

int main (void)
{

 puts("Hello, World!");
 return 0;

}
</syntaxhighlight>

(все вставленные в книгу исходные коды, вставки с консольными командами, примеры работы с отладчиком, дизассемблером и прочее распространяются свободно и без каких-либо ограничений, явных и подразумеваемых, если не сказано обратное. Например, в случае, если будем рассматривать исходный код и/или дизассемблировать какую-нибудь лицензированную под GPL программу)

Для редактирования текста программ можете использовать консольные редакторы, например nano, mcedit, можете взять графические, например gedit, kate и тому подобные. Есть еще всякие сложные штуки, например emacs и vim, но на их освоение уходит достаточно много времени.

В этой книге я не буду описывать работу ни с одним из редакторов, и рассказывать о том, что надо делать все через консоль, IDE не нужны и тому подобное. Делайте как хотите. Чего уж точно не стоит делать, так это использовать офисные пакеты — они создают не обычный текстовый файл (plain text), а файл в особом формате, и компилятор на него грязно выругается. Очень желательно, чтобы в редакторе, которым вы будете пользоваться, была подсветка синтаксиса и автоматическая расстановка отступов при наборе кода.

Для начала советую начать с mcedit, т. к. там не надо думать, как из него выйти (как было когда-то с vim. Сейчас же он на Ctrl-C выдает меганужную подсказку Type :quit<Enter> to exit Vim).

Так или иначе, у вас должен получиться файл ~/learn/c/01_hello/puts_hello/0.1/hello.c в котором есть вышеприведенный текст программы, или код. Теперь его нужно преобразовать в исполняемый процессором машинный код. Для этого нужна программа — компилятор.

Чтобы откомпилировать (собрать) наш пример, можно использовать компиляторы GCC и Clang. Мы будем использовать их оба, когда будем рассматривать код на ассемблере. Но сейчас мы просто компилируем программу и запускаем ее, так что тут это совершенно неважно. Предположим что мы выбрали GCC

gcc hello.c -O2 -o hello
./hello
Hello, World!

hello.c это имя файла с кодом. Опция -O2 задает уровень оптимизации. После опции -o следует имя исполняемого файла. Естественно, опций у gcc значительно больше. Прочитать о них можно, набрав man gcc. Про процесс компиляции, ассемблирования и линковки будет сказано несколько позже.

Отлично, наша программа заработала. Что тут собственно происходит? Разберем пример по строчкам.

puts("Hello, World!")

Это основная часть программы, которая и выводит строку «Hello, World!» в консоль. Что такое puts()? Это имя вызываемой функции, которая отвечает за вывод текста. Чтобы вывести сообщение на экран, или в файл, или отправить информацию по сети, программа должна попросить об этом операционную систему (ОС, в нашем случае это ядро linux), потому что именно ОС связывает пользовательские программы (приложения) и «железо» компьютера. Для того, чтобы приложения могли обращаться к ядру, существуют системные вызовы, или сисколлы (англ. syscall). Очевидно, что если мы хотим в нашей программе вывести текст на экран, нам нужно сделать соответствующий системный вызов. Выходит, puts() — это сисколл?

Нет, мы пошли здесь немного другим путем, более простым для первого примера программы. puts() — это функция из стандартной библиотеки языка C. Если сисколл — это обращение к ядру, то вызов функции — это просто обращение к другой (пока скрытой) части программы. Функция — это часть программы (подпрограмма), которая выполняет определенную задачу. В нашем случае функция puts() делает внутри себя системный вызов (сисколл), в результате которого и выводится строка на экран.

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

Функция puts() находится в файле libc.so, который обычно располагается по адресу
/usr/lib/libc.so

При запуске нашей программы помимо исполняемого файла hello в память загружается и файл libc.so. Таким образом, мы можем вызывать оттуда функции стандартной библиотеки C для самых необходимых операций. Такие файлы, с расширением .so, называются динамические библиотеки. Они содержат такой же исполняемый код, как и наша программа hello, но их нельзя запустить на выполнение напрямую, они должны быть добавлены в память какой-либо самостоятельной программы.

Разберем первую строку:

#include <stdio.h>

#include — это не функция, не сисколл, не команда, которая переводится в машинный код. Это директива препроцессора, то есть команда для препроцессора(препроцессирование выполняется ДО непосредственного синтаксического разбора исходного текста компилятором и самой компиляции). Такие директивы начинаются с символа «#». Перед компиляцией(синтаксическим разбором исходника), компилятор обычно вызывает препроцессор cpp который делает некие преобразования над текстом программы. См. https://gcc.gnu.org/onlinedocs/gcc/Preprocessor-Options.html.

#include дает препроцессору указание — взять файл, имя которого написано дальше (stdio.h) и вставить его содержимое вместо самой директивы #include. То есть к тексту нашей короткой программы мы добавили текст из stdio.h. Что же такого нам понадобилось в этом файле? Там содержится описание функций ввода-вывода из стандартной библиотеки C, в том числе и функции puts().

Более подробный разбор препроцессора Си вы сможете прочесть в статье Сишный препроцессор (todo)

Идем дальше:

int main (void)

Вся наша программа — это тоже функция, которая называется main(). void в скобочках означает, что функция не имеет параметров. Функция puts(), как вы заметили, имеет один параметр. int — тип возвращаемого значения. Каждая функция после выполнения может возвращать значение, или результат. Функция main() должна, как правило, возвращать 0 если программа выполнилась без ошибок. Если произошла ошибка — то код ошибки. int означает целочисленный тип, то есть функция main возвращает целое число. Узнать, что вернула программа можно, выполнив команду

echo $?

Сразу после выполнения программы.

Фигурные скобки обозначают начало и конец функции, все что внутри — это ее содержимое.

Наконец,

return 0;

команда выхода из функции, возвращаем 0.

Если заменить Hello, World! на нечто другое, выводиться будет другой текст. Интересен другой момент. А именно, что если сделать вот так puts("Hello, World!"+1)? Попробуйте. Пусть это будет в новой директории

user@localhost:~/learn/c/01_hello/puts_hello/0.1$ cd ..
user@localhost:~/learn/c/01_hello/puts_hello$ cp -r 0.1 0.2
user@localhost:~/learn/c/01_hello/puts_hello$ cd 0.2/
user@localhost:~/learn/c/01_hello/puts_hello/0.2$

Вот таким вот образом мы создали новую версию puts()-хелловорлда. Осталось только подредактировать, т. е. вместо puts("Hello, World!") сделать puts("Hello, World!"+1) и произвести компиляцию. В результате мы обнаружим, что вывелась лишь часть нашей строки Hello, World!, пропустив ровно одну букву — ello, World!. Почему? Что вообще значит «прибавить 1 к строке»? Все дело в том, что в функцию puts() передается адрес, в котором расположена строка. А строка это лишь последовательность байтов, ASCII-символов. И когда мы делаем «+1» то мы увеличиваем этот самый адрес на единицу, и таким образом получается, что строка выводится не вся. Сама строка при этом не обрезается т.е. она по-прежнему будет содержать первую букву 'H'. Но как, спросите вы, функция puts() узнает, где наша строка заканчивается? Начинается-то она с адреса, который в нее передается, это понятно. Так вот, строка у нас нуль-терминированная, и конец строки обозначается нулевым байтом. puts() выведет всю строку вплоть до этого самого нулевого байта. Это не очень удачное решение т. к. требуется время чтобы узнать длину строки, прежде чем ее выводить. Если надо «сшить» две строки (т. е. объединить в одну), то требуется время на то, чтобы узнать длину первой и длину второй строки. Проведем еще один эксперимент, показывающий, что строка через puts() выводится именно до нуля. Создайте версию 0.3 нашего хелловорда, и пусть там будет вот такой вот код:
<syntaxhighlight lang=»c»>

  1. include <stdio.h>

int main (void)
{

 const char *str = "Hello\0 World!";
 puts(str);   // 5 букв
 puts(str+1); // 4 буквы
 puts(str+5); // пустота
 puts(str+6); // остальной кусок
 puts(str+7); // остальной кусок без первого байта (пробела)
 return 0;

}
</syntaxhighlight>
// обозначает начало однострочного комментария, и весь текст, идущий после него, игнорируется. Но он прекращает свое действие на следующей строке.

Тут вводится новая для нас сущность — переменная. Но в данном случае она константа. Пока что достаточно будет сказать, что эта самая переменная содержит (хранит в себе) адрес нашей строки(указатель на строку), и когда мы вызываем puts(str) мы фактически передаем этот адрес в нашу функцию. Делая так puts(str+1) мы не увеличиваем значение самой переменной str на 1, мы просто передаем в функцию puts() адрес, который на единицу больше того адреса, который у нас заключен в str.

На выходе должно получиться вот это:

Hello
ello

 World!
World!

Тут отлично можно видеть, что строка обрывается в том месте, где у нас вставлен \0. \0 это не значит вывести на печать косую черту и цифру 0. Это значит что в соответствующей шестой позиции у нас в строке будет находиться нулевой байт. Си строка и так уже содержит в себе нулевой байт в конце, который помечает ее конец. Если же вставить в строку свой собственный нулевой байт, и вызвать puts(), передав в качестве значения адрес начала строки, то строка будет выведена как раз до этого нулевого байта. Подробнее об этом написано в Escape-последовательности_в_C.

Вот как это примерно выглядит в случае puts(str);

   +      +      +      +      +
+------+------+------+------+------+------+------+------+------+------+------+------+------+
|      |      |      |      |      |      |      |      |      |      |      |      |      |
| 'H'  | 'e'  | 'l'  | 'l'  | 'o'  | '\0' | ' '  | 'w'  | 'o'  | 'r'  | 'l'  | 'd'  | '\0' |
|      |      |      |      |      |      |      |      |      |      |      |      |      |
+------+------+------+------+------+------+------+------+------+------+------+------+------+
   ^
   |
  str

Сверху через «+» помечены те байты, которые будут выведены в консоль. Кроме того, puts() еще автоматически вставляет перевод строки в конце (код перевода строки обозначается через '\n'). Это поведение задокументировано, см. man 3 puts. Все эти буквы и переводы строк представляют из себя восьмибитные байты. Позже мы увидим, как сами буквы можно задавать через их коды в 16-ричной системе счисления в эскейп-последовательностях или же просто как массив байтов.

Рассмотрим случай puts(str+1);

          +      +      +      +
+------+------+------+------+------+------+------+------+------+------+------+------+------+
|      |      |      |      |      |      |      |      |      |      |      |      |      |
| 'H'  | 'e'  | 'l'  | 'l'  | 'o'  | '\0' | ' '  | 'w'  | 'o'  | 'r'  | 'l'  | 'd'  | '\0' |
|      |      |      |      |      |      |      |      |      |      |      |      |      |
+------+------+------+------+------+------+------+------+------+------+------+------+------+
          ^
          |
        str+1

Тут вроде все ясно, передав значение на 1 большее чем str, мы выведем строку без первого байта

Рассмотрим случай puts(str+5);

+------+------+------+------+------+------+------+------+------+------+------+------+------+
|      |      |      |      |      |      |      |      |      |      |      |      |      |
| 'H'  | 'e'  | 'l'  | 'l'  | 'o'  | '\0' | ' '  | 'w'  | 'o'  | 'r'  | 'l'  | 'd'  | '\0' |
|      |      |      |      |      |      |      |      |      |      |      |      |      |
+------+------+------+------+------+------+------+------+------+------+------+------+------+
                                      ^
                                      |
                                    str+5

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

Рассмотрим случай puts(str+6);

                                             +      +      +      +      +      +
+------+------+------+------+------+------+------+------+------+------+------+------+------+
|      |      |      |      |      |      |      |      |      |      |      |      |      |
| 'H'  | 'e'  | 'l'  | 'l'  | 'o'  | '\0' | ' '  | 'w'  | 'o'  | 'r'  | 'l'  | 'd'  | '\0' |
|      |      |      |      |      |      |      |      |      |      |      |      |      |
+------+------+------+------+------+------+------+------+------+------+------+------+------+
                                             ^
                                             |
                                           str+6

Тут мы уже перешли через нулевой байт, и таким образом выводим остаток фразы. Понятно, что если сделать puts(str+7); то будет выведено то же самое, только без пробела в начале.

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

Ассемблер

Учимся читать ассемблер (синтаксис AT&T и Intel). Регистры, память, стек. Виртуальное адресное пространство процесса. Сегменты процесса

Перейдем к директории ~/learn/c/01_hello/puts_hello/0.1 и там попробуем получить ассемблерный листинг нашего самого первого хелловорда, используя компиляторы clang и gcc.
Компилировать свою программу мы будем особым образом. Примерно вот так:

gcc-5 -fno-unwind-tables -fno-asynchronous-unwind-tables -march=x86-64 -mtune=generic -O2 -S hello.c -o hello_gcc.s
clang-3.8 -fno-unwind-tables -fno-asynchronous-unwind-tables -march=x86-64 -mtune=generic -O2 -S hello.c -o hello_clang.s

Примечание: тут будут рассматриватся последние доступные на момент написания этого текста пакеты для убунт из https://launchpad.net/~ubuntu-toolchain-r/+archive/ubuntu/test и http://llvm.org/apt/.

А именно: gcc-5 (Ubuntu 5.3.0-3ubuntu1~14.04) 5.3.0 20151204 и clang version 3.8.0-svn262614-1~exp1 (branches/release_38). Со временем будут появляться более новые версии компиляторов, и возможно это все надо будет переделать.

В компиляторах других верcий, будь то идущих в стандартной поставке вместе с дистрибутивом, из сторонних репозиториев, или собранных через ./configure && make && sudo make install, сгенерированный ассемблерный код может отличаться. Хотя на таких простых примерах едва ли могут быть какие-нибудь значимые отличия. Но на более сложных примерах они будут почти наверняка.

Пока что мы не будем подробно разбирать директивы ассемблера, а просто посмотрим на разные версии хелловордов, чем они отличаются в ассемблерном представлении.

Посмотрим получившийся код хелловорда версии 0.1 для GCC и clang. Код прокомментирован

gcc:

        .file   "hello.c"
        .section        .rodata.str1.1,"aMS",@progbits,1
.LC0:                           # метка
        .string "Hello, World!" # сама строка
        .section        .text.unlikely,"ax",@progbits
.LCOLDB1:
        .section        .text.startup,"ax",@progbits
.LHOTB1:
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
        subq    $8, %rsp        # Если мы хотим вызывать функцию, вершина стека должна быть кратна 16 байт (требования 86_64 ABI). Регистр rsp как раз за это отвечает
        movl    $.LC0, %edi     # Помещаем адрес строки (адрес метки .LC0) в регистр edi
        call    puts            # Вызов функции puts которая принимает в edi указатель на строку "Hello, World!"
        xorl    %eax, %eax      # Обнуление регистра eax через xor ( eax = eax xor eax ) (то же самое, что и eax = 0) — значение, возвращаемое функцией main
        addq    $8, %rsp        # Сдвигаем стек на 8 байт назад (стек растет вверх, так что для этого надо к регистру прибавить 8)
        ret                     # Возврат (выход) из функции main
        .size   main, .-main
        .section        .text.unlikely
.LCOLDE1:
        .section        .text.startup
.LHOTE1:
        .ident  "GCC: (Ubuntu 5.3.0-3ubuntu1~14.04) 5.3.0 20151204"
        .section        .note.GNU-stack,"",@progbits

clang:

        .text
        .file   "hello.c"
        .globl  main
        .align  16, 0x90
        .type   main,@function
main:                                   # @main
# BB#0:
        pushq   %rax           # Если мы хотим вызывать функцию, вершина стека должна быть кратна 16 байт (требования 86_64 ABI). Инструкция pushq тут «заталкивает» 64-битный регистр в стек, уменьшая при этом значение %rsp регистра(указатель на вершину стека) на 8 
        movl    $.L.str, %edi  # Помещаем адрес строки (адрес метки .L.str) в регистр edi
        callq   puts           # Вызов функции puts которая принимает в edi указатель на строку «Hello, World!»
        xorl    %eax, %eax     # Обнуление регистра eax через xor ( eax = eax xor eax ) (то же самое, что и eax = 0) — значение, возвращаемое функцией main
        popq    %rcx           # Сдвигаем стек на 8 байт назад через popq интструкцию
        retq                   # Возврат (выход) из функции main
.Lfunc_end0:
        .size   main, .Lfunc_end0-main

        .type   .L.str,@object          # @.str
        .section        .rodata.str1.1,"aMS",@progbits,1
.L.str:                         # метка
        .asciz  "Hello, World!" # сама строка
        .size   .L.str, 14


        .ident  "clang version 3.8.0-svn262614-1~exp1 (branches/release_38)"
        .section        ".note.GNU-stack","",@progbits

Требования к выравниванию стека при вызове других функций (подпрограмм) описаны в документации http://www.x86-64.org/documentation_folder/abi.pdf.

В частности, про выравнивание регистра rsp — указателя на вершину стека сказано в 3.2.2 The Stack Frame страница 16.

> The end of the input argument area shall be aligned on a 16 (32, if __m256 is passed on stack) byte boundary. In other words, the value (%rsp + 8) is always a multiple of 16 (32) when control is transferred to the function entry point.

Тут основной момент заключается вот в этих двух строчках

        movl    $.LC0, %edi     # Помещаем адрес строки (адрес метки .LC0) в регистр edi
        callq   puts           # Вызов функции puts которая принимает в edi указатель на строку «Hello, World!»

Компиляторы clang и gcc по-разному обозвали метки, посредством которых мы ссылаемся на адрес строк, но в целом они сделали то же самое. Регистры это особые ячейки памяти с быстрым доступом. Инструкция movl $.LC0, %edi присваивает регистру edi адрес, соответствующий метке .LC0. Эта метка, как можно видеть, указывает в начало нашей строки "Hello, World!". Более подробно это (регистры и метки) будет разобрано в части, посвященной ассемблеру

Рассмотрим теперь второй пример («»Hello, World!»+1»). Я не буду приводить полный ассемблерный выхлоп, а покажу лишь отличия между полученным ассемблерным выводом

user@localhost:~/learn/c/01_hello/puts_hello/0.2$ diff ../0.1/hello_gcc.s hello_gcc.s 
14c14
< 	movl	$.LC0, %edi
---
> 	movl	$.LC0+1, %edi
user@localhost:~/learn/c/01_hello/puts_hello/0.2$ diff ../0.1/hello_clang.s hello_clang.s 
9c9
< 	movl	$.L.str, %edi
---
> 	movl	$.L.str+1, %edi

Отличие есть лишь в этой одной единственной строчке. А сама строка «Hello, World!» присутствует в полном объеме, т. е. компилятор не отбросил первую букву, а просто передал в функцию адрес этой строки, увеличенный на 1. И от этого первая буква не была выведена.

Отметим, что наприме в данном случае
movl $.LC0+1, %edi
Получившийся исполняемый код не будет выполнять сложение адреса строки с единицей. Сама операция .LC0+1, %edi будет произведена на этапе ассемблирования; готовая к «использованию» программа не будет на этапе исполнения прибавлять единицу к адресу метки, смещение относительно метки .LC0 на 1 будет высчитано на этапе перевода кода на ассемблере (с расширением .s) в исполняемый файл.

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

gcc:

        .file   "hello.c"
        .section        .rodata
.LC0:
        .string "Hello"
        .string " World!"
        .section        .text.unlikely,"ax",@progbits
.LCOLDB1:
        .section        .text.startup,"ax",@progbits
.LHOTB1:
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
        subq    $8, %rsp
        movl    $.LC0, %edi    # - edi = str
        call    puts           # - puts(str);
        movl    $.LC0+1, %edi  # - edi = str + 1
        call    puts           # - puts(str+1);
        movl    $.LC0+5, %edi  # - edi = str + 5
        call    puts           # - puts(str+5);
        movl    $.LC0+6, %edi  # - edi = str + 6
        call    puts           # - puts(str+6);
        movl    $.LC0+7, %edi  # - edi = str + 7
        call    puts           # - puts(str+7);
        xorl    %eax, %eax     # - eax = eax xor eax (то же самое, что и eax = 0)
        addq    $8, %rsp
        ret
        .size   main, .-main
        .section        .text.unlikely
.LCOLDE1:
        .section        .text.startup
.LHOTE1:
        .ident  "GCC: (Ubuntu 5.3.0-3ubuntu1~14.04) 5.3.0 20151204"
        .section        .note.GNU-stack,"",@progbits

clang:

        .text
        .file   "hello.c"
        .globl  main
        .align  16, 0x90
        .type   main,@function
main:                                   # @main
# BB#0:
        pushq   %rax
        movl    $.L.str, %edi   # - edi = str
        callq   puts            # - puts(str);
        movl    $.L.str+1, %edi # - edi = str+1
        callq   puts            # - puts(str+1);
        movl    $10, %edi       # - edi = '\n'    !! тут компилятор clang решил сделать небольшую оптимизацию
        callq   putchar         # - putchar('\n') !! вызвать putchar чтобы вставить перевод строки, вместо puts  
        movl    $.L.str+6, %edi # - edi = str+6
        callq   puts            # - puts(str+6);
        movl    $.L.str+7, %edi # - edi = str+7
        callq   puts            # - puts(str+7);
        xorl    %eax, %eax      # - eax = eax xor eax (то же самое, что и eax = 0)
        popq    %rcx
        retq
.Lfunc_end0:
        .size   main, .Lfunc_end0-main

        .type   .L.str,@object          # @.str
        .section        .rodata,"a",@progbits
.L.str:
        .asciz  "Hello\000 World!"
        .size   .L.str, 14


        .ident  "clang version 3.8.0-svn262614-1~exp1 (branches/release_38)"
        .section        ".note.GNU-stack","",@progbits

Компилятор gcc для записи нашей строки "Hello\0 World!" задействовал две директивы .string.

        .string "Hello"
        .string " World!"

В то время как clang ограричился одной директивой .asciiz.

        .asciz  "Hello\000 World!"

Эти директивы не являются инструкциями процессора (такими как movq, subq, callq и прочее). Это лишь указания транслятору ассемблера вставить в объектный файл определенные символы и/или последовательности байт. https://sourceware.org/binutils/docs/as/Pseudo-Ops.html — тут описаны соответствующие директивы.

https://sourceware.org/binutils/docs/as/String.html — про директиву .string.
https://sourceware.org/binutils/docs/as/Asciz.html — про директиву .asciz.

Объектный файл (с расширение .o) это файл с промежуточным представлением отдельного модуля программы, полученный в результате обработки исходного кода соотоветствующей программой. Объектный файл содержит в себе особым образом подготовленный код (часто называемый двоичным или бинарным), который может быть объединён с другими объектными файлами при помощи редактора связей (компоновщика, линкера) для получения готового исполнимого модуля, либо библиотеки. В качестве линкера мы будем использовать ld из комплекта binutils. Компилятор GCC сам не способен произвести перевод кода в объектный и исполняемый файл, он вызывает соответствующие утилиты as и ld (и возможно некоторые другие) из набора GNU Binutils https://www.gnu.org/software/binutils/.

Чтобы получить объектный файл из кода на ассемблере, выполним команду as (пока будем работать с самой первой версией хелловорда):

as hello_clang.s -o hello_clang.o
as hello_gcc.s -o hello_gcc.o

Получившиеся объектные файлы формата ELF (англ. Executable and Linkable Format — формат исполнимых и компонуемых файлов) — в данном случае не исполняемый, а компонуемый файл (linkable, линковка, компоновка — без разницы). Исполняемый файл hello получившийся при «полной» компиляции, тоже является ELF-ом. Утилита file умеет «понимать» множество разных форматов. Посмотрим на то, что она скажет на наши объектные и исполняемые файлы.

$ file hello_clang.o 
hello_clang.o: ELF 64-bit LSB  relocatable, x86-64, version 1 (SYSV), not stripped
$ file hello_gcc.o 
hello_gcc.o: ELF 64-bit LSB  relocatable, x86-64, version 1 (SYSV), not stripped
$ file hello
hello: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=13996484d52cb2088e59ae5bea10867dcaf67479, not stripped

Обьектные файлы у нас relocatable т. е. перемешаемые. Это значит, что там содержатся особые секции .rel.text и .rel.data, содержащие списки адресов, которые должны быть модифицированы, когда компоновщик объединит объектные файлы. Объектные файлы содержат в себе символы. Получить информацию о символах можно с помощью комманды nm. Посмотрим на символы из нашего объектного файла:

$ nm -f sysv hello_gcc.o 


Symbols from hello_gcc.o:

Name                  Value           Class        Type         Size             Line  Section

main                |0000000000000000|   T  |              FUNC|0000000000000015|     |.text.startup
puts                |                |   U  |            NOTYPE|                |     |*UND*

эту же информацию можно получить утилитой readelf:

$ readelf -Ws hello_gcc.o

Symbol table '.symtab' contains 12 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    9 
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
    10: 0000000000000000    21 FUNC    GLOBAL DEFAULT    6 main
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

Нас тут пока интересуют main и puts. Мы объявили функцию main и поэтому символ функции main упомянут в таблице .symtab. И Type указан как FUNC. А функция puts у нас объявлена не была, но у нас был объявлен прототип этой функции. Прототип функции был в sdtlib.h, чтобы в этом убедиться, сделаем так:

$ echo "#include <stdio.h>" | cpp | grep puts
extern int fputs (const char *__restrict __s, FILE *__restrict __stream);
extern int puts (const char *__s);

Т. е. в файл через директиву include на этапе препроцессирования было включено определение функции puts extern int puts (const char *__s); тут используется спецификатор extern, означающий что данный символ является внешним(слово extern так и переводится — «внешний»), самого «тела» функции не предоставлено. Спецификатором extern мы говорим компилятору что соответствующая функция будет найдена во время компоновки(линковки), само «тело» функции будет подключено из объектных файлов, разделяемых библиотек или статических библиотек(расширение .a). В коде на ассемблере, полученном от компилятора, никакой функции puts нет. В то же время, для функции main у присутствует само «тело» функции, т. е. что именно эта функция делает, если ее вызвать. Функция main на самом деле не является самой первым, которую программа будет выполнять; самым первым будет выполняться особый startup-код, который подключается при «обычной» комплияции, когда сам компилятор в неявном виде вызывает ld и состыковывает наш код с определенными объектными файлами и статическими библиотеками, в которых и присутствует этот startup-код. Настоящей точкой входа (адреса, с которого программа начинает выполнятся) будет функция _start (на ассемблере мы будем сразу писать с этой точки входа _start, без всяких main). Реализация функции puts, которую использует наша программа, содержится во внешних файлах, которые «подключаются» к нашей программе в процессе линковки. Притом, исполняемый код соответствующей функции puts может как.wut? Упоминание символа puts есть лишь при непосредственном вызове функции: call puts.

Функция puts содержится в

 readelf -s /lib/x86_64-linux-gnu/libc.so.6 | less
 libc.a

Динамическая библиотека (в данном случае имеет расширение .so) — файл, содержащий машинный код. Загружается в память процесса загрузчиком программ операционной системы либо при создании процесса, либо по запросу уже работающего процесса, то есть динамически. В получившемся elf файле нашего хеловорда(при обычной динамической линковке, которая происходит по умолчанию) содержится отсылка к соответствующей разделяемой библитеке, которая требуется для запуска. Есть особый исполняемый файл ld-linux.so.1 или ld-linux.so.2(в зависимости от версии glibc), который отображает эти динамические библиотеки в адресное пространство процесса при запуске исполняемого файла, и таким образом процесс может использовать соответствующие функции и данные, которые в этой динамической библиотеке объявлены. Кроме того, есть возможность статической сборки исполняемого файла, и тогда это все не требуется — всё необходимое будет включено непосредственно в сам исполняемый файл, и не будет необходимости что-либо подгружать. Статические библиотеки — файлы с расширением .a, представляют из себя архивы, в которые включены объектные .so файлы. Хотя туда можно включать любые файлы, но обычно этот формат архивов применяется для статических библотек, см. https://en.wikipedia.org/wiki/Ar_(Unix).

Более подробный разбор, что такое статические и динамические библиотеки, разделяемые объекты (shared objects) и объектные файлы будет позже, пока что нам хватит и такого понимания. За дополнительной информацией можно обратиться к […].

Данная статья или раздел ещё не завершены
Кто-то посчитал, что статья или раздел ниже не содержит какой-то важной информации или имеет проблемы с вёрсткой/текстом. Указана причина: todo overload — «Дать ссылки на внешние источники или еще лучше написать статью об этом отдельно. Хотя сейчас на данной стадии это слишком рано разбирать»&«ldd, objdump /lib/x86_64-linux-gnu/libc.so.6, _IO_puts …»{{#ifeq: {{{1}}} | nopoint | | . }}

{{#if: Доработка | }}

Знакомство с ассемблером и отладчиком

Преступим к знакомству с ассемблером. Первые наши программы на ассемблере не будут вообще выводить никакой текст, а будут просто изменять значения регистров. Создадим директорию ~/learn/asm/01_test/1/ и разместим там файл test1.s следующего содержания

	.section	.text,"ax",@progbits
	.p2align 4,,15
	.globl	_start
	.type	_start, @function
_start:
	movq	$1, %rax
	movq	$2, %rbx
	movq	$3, %rcx
	movq	$4, %rdx
	movq	$5, %rsi
	movq	$6, %rdi
	movq	$7, %rbp
	movq	$8, %rsp
	movq	$9, %r8
	movq	$10, %r9
	movq	$11, %r10
	movq	$12, %r11
	movq	$13, %r12
	movq	$14, %r13
	movq	$15, %r14
	movq	$16, %r15
	ud2
	.size	_start, .-_start

Тут мы заполняем наши регистры общего назначения значениями от 1 до 16. Регистр %rsp имеет особое назначение, и с ним так лучше не делать, но поскольку со стеком мы тут не работаем, это делать можно. Регистр %rbp также имеет отношение к работе со стеком. Регистры %rsi и %rdi тоже имеют особое назначение (инструкции movsb и подобные ей).
ud2 — это такая официально определенная (задокументированная) инструкция, вызывающая ошибку выполнения при попытке ее выполнить. Попытка ее выполнения приведет к ошибке, будет выдано сообщение Illegal instruction (core dumped) и образуется файл с дампом памяти процессора (коредампом). В нем содержится информация о состоянии процесса в момент его аварийного завершения. Cм. https://ru.wikipedia.org/wiki/Дамп_памяти.

Просто убедимся, что программа аварийно завершится и он получится при запуске

user@localhost:~/learn/asm/01_test/1$ as test1.s -o test1.o
user@localhost:~/learn/asm/01_test/1$ ld test1.o -o test1
user@localhost:~/learn/asm/01_test/1$ ./test1 
Illegal instruction (core dumped)
user@localhost:~/learn/asm/01_test/1$ ls
core  test1  test1.o  test1.s

если вы не получили файла core после запуска ./test1, вам вероятно надо сделать ulimit -c unlimited и повторить запуск. Это отключит лимит на размеры коредампа. Именно эта инструкция ud2 вызывает соответствую ошибку и выпадение в кору. Пока что мы не будем ничего делать с этим коредампом. Для корректного завершения программы, надо вызвать соответствующий системный вызов, до этого мы доберемся несколько позже. Посмотрим на точку входа нашего исполняемого файла.

user@localhost::~/learn/asm/01_test/1$ readelf -h test1
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400080
  Start of program headers:          64 (bytes into file)
  Start of section headers:          280 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         1
  Size of section headers:           64 (bytes)
  Number of section headers:         5
  Section header string table index: 2

Точка входа у нас по адресу в 0x400080

А сейчас воспользуемся отладчиком, идущим с radare2 (желательно взять версию по-новее. http://rada.re/r/down.html для установки). Еще установите плагин keystone, r2pm -i keystone, он нам очень пригодится. Советую также обратиться к https://www.gitbook.com/book/radare/radare2book/details. В данный момент нас интересует вот эта часть https://radare.gitbooks.io/radare2book/content/introduction/basic_debugger_session.html.

user@localhost:~/learn/asm/01_test/1$ r2 -d ./test1
Process with PID 8609 started...
attach 8609 8609
bin.baddr 0x00400000
Assuming filepath ./test1
Warning: Cannot initialize dynamic strings
asm.bits 64
 -- THE ONLY WINNING MOVE IS NOT TO PLAY.
[0x00400080]> pd 17
            ;-- entry0:
            ;-- section..text:
            ;-- _start:
            ;-- rip:
            0x00400080      48c7c0010000.  mov rax, 1                  ; [1] va=0x00400080 pa=0x00000080 sz=114 vsz=114 rwx=--r-x .text
            0x00400087      48c7c3020000.  mov rbx, 2
            0x0040008e      48c7c1030000.  mov rcx, 3
            0x00400095      48c7c2040000.  mov rdx, 4
            0x0040009c      48c7c6050000.  mov rsi, 5
            0x004000a3      48c7c7060000.  mov rdi, 6
            0x004000aa      48c7c5070000.  mov rbp, 7
            0x004000b1      48c7c4080000.  mov rsp, 8
            0x004000b8      49c7c0090000.  mov r8, 9
            0x004000bf      49c7c10a0000.  mov r9, 0xa
            0x004000c6      49c7c20b0000.  mov r10, 0xb                ; 11
            0x004000cd      49c7c30c0000.  mov r11, 0xc                ; 12
            0x004000d4      49c7c40d0000.  mov r12, 0xd                ; 13
            0x004000db      49c7c50e0000.  mov r13, 0xe                ; 14
            0x004000e2      49c7c60f0000.  mov r14, 0xf                ; 15
            0x004000e9      49c7c7100000.  mov r15, 0x10               ; 16
            0x004000f0      0f0b           ud2

pd 17 дизассемблирует нам 17 инструкций. Можно посмотреть встроенную справку по pd если ввести pd?.

radare2 по-умолчанию работает с intel-синтаксисом, а не AT&T на котором написан наш код. В интел синтаксисе отличается порядок операндов (источник — назначение), например mov rax, 1 в интел синтаксисе эквивалентен movq $1, %rax. Есть и другие отличия. Мы пока переключим отображение на AT&T синтаксис т. к. код у нас написан в AT&T, к интел мы вернемся потом. Для начала убедимся, что у нас тут действительно синтаксис intel

[0x00400080]> e asm.syntax
intel

И изменим его

[0x00400080]> e asm.syntax=att
[0x00400080]> e asm.syntax
att

После чего мы получим дизасм в at&t синтаксисе

[0x00400080]> pd 17
            ;-- entry0:
            ;-- section..text:
            ;-- _start:
            ;-- rip:
            0x00400080      48c7c0010000.  movq $1, %rax               ; [1] va=0x00400080 pa=0x00000080 sz=114 vsz=114 rwx=--r-x .text
            0x00400087      48c7c3020000.  movq $2, %rbx
            0x0040008e      48c7c1030000.  movq $3, %rcx
            0x00400095      48c7c2040000.  movq $4, %rdx
            0x0040009c      48c7c6050000.  movq $5, %rsi
            0x004000a3      48c7c7060000.  movq $6, %rdi
            0x004000aa      48c7c5070000.  movq $7, %rbp
            0x004000b1      48c7c4080000.  movq $8, %rsp
            0x004000b8      49c7c0090000.  movq $9, %r8
            0x004000bf      49c7c10a0000.  movq $0xa, %r9
            0x004000c6      49c7c20b0000.  movq $0xb, %r10             ; 11
            0x004000cd      49c7c30c0000.  movq $0xc, %r11             ; 12
            0x004000d4      49c7c40d0000.  movq $0xd, %r12             ; 13
            0x004000db      49c7c50e0000.  movq $0xe, %r13             ; 14
            0x004000e2      49c7c60f0000.  movq $0xf, %r14             ; 15
            0x004000e9      49c7c7100000.  movq $0x10, %r15            ; 16
            0x004000f0      0f0b           ud2

И мы получили это в том же синтаксисе, что и в исходном коде. С синтаксисом intel мы тоже вне всякого сомнения познакомимся, но не сейчас. Кроме того, тут еще есть проблема, что hex длинных опкодов выводится не полностью. Например тут

            0x00400080      48c7c0010000.  movq $1, %rax
                                        ^

Мы видим точку в конце, это означает что оно обрезано. Чтобы это исправить, можно изменить значение asm.nbytes. Посмотреть значение asm.nbytes и изменить его можно вот так

[0x00400080]> e asm.nbytes
6
[0x00400080]> e asm.nbytes=15
            ;-- entry0:
            ;-- section..text:
            ;-- _start:
            ;-- rip:
            0x00400080      48c7c001000000                                                 movq $1, %rax ; [1] va=0x00400080 pa=0x00000080 sz=114 vsz=114 rwx=--r-x .text
            0x00400087      48c7c302000000                                                 movq $2, %rbx
            0x0040008e      48c7c103000000                                                 movq $3, %rcx
            0x00400095      48c7c204000000                                                 movq $4, %rdx
            0x0040009c      48c7c605000000                                                 movq $5, %rsi

Теперь шестнадцатиричное представление инструкции выводится без сокращения.

Можно в rc файле ~/.radare2rc прописать e asm.syntax=att и e asm.nbytes=30, таким образом у нас эти переменные будет выставлены в нужные значения сразу же при запуске. Чтобы просмотреть список всех этих выставляемых переменных, можно ввести e. В radare2 так же работает автодополнение по нажатию Tab, можно прочитать встроенную справку, введя к примеру pd? (т. е. добавив вопросительный знак в конце).

Взглянем на начальное состояние регистров:

[0x00400080]> dr=
orax 0x0000003b           rax 0x00000000           rbx 0x00000000
 rcx 0x00000000           rdx 0x00000000            r8 0x00000000
  r9 0x00000000           r10 0x00000000           r11 0x00000000
 r12 0x00000000           r13 0x00000000           r14 0x00000000
 r15 0x00000000           rsi 0x00000000           rdi 0x00000000
 rsp 0x7fffae9975e0       rbp 0x00000000           rip 0x00400080
 rflags I           

Это далеко не все регистры, кроме того, регистр orax это не регистр вовсе. rsp у нас не нулевой, он указывает на стек, с ним мы поработаем несколько позже. rip это instruction pointer т. е. указатель на инструкции, он содержит в себе адрес. В данном случае, ни одной инструкции еще не было выполнено, и rip совпадает с Entry point address который нам показал readelf — 0x400080. Кроме того, обратим внимание на ;-- rip:

            ;-- rip:
            0x00400080      48c7c0010000.  movq $1, %rax               ; [1] va=0x00400080 pa=0x00000080 sz=114 vsz=114 rwx=--r-x .text
            0x00400087      48c7c3020000.  movq $2, %rbx

Тут мы видим что этот самый регистр rip указывает на конкретно вот эту инструкцию movq $1, %rax. Слева напротив самой мнемоники мы видим 48c7c0010000 — это то, как эта самая инструкция закодирована, т.е. ее представление в 16-ричной системе счисления. Выполним одну инструкцию, после чего дизассемблируем опять и посмотрим на состояние регистров

[0x00400080]> ds 1
[0x00400080]> pd 5
            ;-- entry0:
            ;-- section..text:
            ;-- _start:
            0x00400080      48c7c001000000  movq $1, %rax               ; [1] va=0x00400080 pa=0x00000080 sz=114 vsz=114 rwx=--r-x .text
            ;-- rip:
            0x00400087      48c7c302000000  movq $2, %rbx
            0x0040008e      48c7c103000000  movq $3, %rcx
            0x00400095      48c7c204000000  movq $4, %rdx
            0x0040009c      48c7c605000000  movq $5, %rsi
[0x00400080]> dr=
orax 0xffffffffffffffff   rax 0x00000001           rbx 0x00000000
 rcx 0x00000000           rdx 0x00000000            r8 0x00000000
  r9 0x00000000           r10 0x00000000           r11 0x00000000
 r12 0x00000000           r13 0x00000000           r14 0x00000000
 r15 0x00000000           rsi 0x00000000           rdi 0x00000000
 rsp 0x7fffe24a9f60       rbp 0x00000000           rip 0x00400087
 rflags 1I         

Видно, что ;-- rip: у нас сместился на одну строчку ниже, и значение rip изменилось, оно стало равным 0x00400087, т. е. теперь оно указывает на вторую инструкцию. Первая инструкция movq $1, %rax была успешно выполнена, и мы можем видеть что в регистре rax у нас хранится значение 0x00000001. Можно не делать все пошагово, а просто дать нашей программе успешно наткнуться на инструкцию ud2 и таким образом упасть. Тогда мы посмотрим состояние регистров и убедимся что в них записаны соответствующие значения

[0x00400080]> dc
attach 9766 1
[+] signal 4 aka SIGILL received 0
[0x004000f0]> 

Как мы можем видеть, наш процесс получил сигнал SIGILL. В POSIX-системах, SIGILL — сигнал, посылаемый процессу при попытке выполнить неправильно сформированную, несуществующую или привилегированную инструкцию. Инструкция ud2 является некорректной, притом ее некорректность закреплена в документации интел и амд. Обычно после SIGILL следует завершение с дампом памяти(в чем мы ранее убедились), но сейчас мы работаем в отладчике. Мы можем посмотреть состояние регистров

[0x004000f0]> dr=
orax 0xffffffffffffffff   rax 0x00000001           rbx 0x00000002
 rcx 0x00000003           rdx 0x00000004            r8 0x00000009
  r9 0x0000000a           r10 0x0000000b           r11 0x0000000c
 r12 0x0000000d           r13 0x0000000e           r14 0x0000000f
 r15 0x00000010           rsi 0x00000005           rdi 0x00000006
 rsp 0x00000008           rbp 0x00000007           rip 0x004000f0
 rflags 1IV        

И действительно, в регистры были записаны соответствующие значения. Что можно сделать еще? Давайте перезапустим наш процесс и попробуем поработать в visual mode. Посмотрев встроенную помощь d? можно найти нужную для этого команду

| do                      Open process (reload, alias for 'oo')

попробуем:

[0x004000f0]> do
Wait event received by different pid 9792
Process with PID 9821 started...
File dbg://./test1 reopened in read-write mode
attach 9821 9821
Assuming filepath ./test1
Warning: Cannot initialize dynamic strings
[0x00400080]> V

[0x00400080 608 ./test1]> x @ entry0                                 
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x00400080  48c7 c001 0000 0048 c7c3 0200 0000 48c7  H......H......H.
0x00400090  c103 0000 0048 c7c2 0400 0000 48c7 c605  .....H......H...
0x004000a0  0000 0048 c7c7 0600 0000 48c7 c507 0000  ...H......H.....
0x004000b0  0048 c7c4 0800 0000 49c7 c009 0000 0049  .H......I......I
0x004000c0  c7c1 0a00 0000 49c7 c20b 0000 0049 c7c3  ......I......I..
0x004000d0  0c00 0000 49c7 c40d 0000 0049 c7c5 0e00  ....I......I....
0x004000e0  0000 49c7 c60f 0000 0049 c7c7 1000 0000  ..I......I......
0x004000f0  0f0b 002e 7379 6d74 6162 002e 7374 7274  ....symtab..strt
0x00400100  6162 002e 7368 7374 7274 6162 002e 7465  ab..shstrtab..te
0x00400110  7874 0000 0000 0000 0000 0000 0000 0000  xt..............

Это немного не то, что нам надо. Переключим режим отображения, нажав на кнопку p некоторое количество раз, пока мы не увидем перед собой такой картины:

[0x00400080 170 ./test1]> ?0;f tmp;s.. @ entry0
- offset -       0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x7ffebe940b40  0100 0000 0000 0000 0e27 94be fe7f 0000  .........'......
0x7ffebe940b50  0000 0000 0000 0000 1627 94be fe7f 0000  .........'......
0x7ffebe940b60  5027 94be fe7f 0000 5b27 94be fe7f 0000  P'......['......
0x7ffebe940b70  8027 94be fe7f 0000 9227 94be fe7f 0000  .'.......'......
orax 0x0000003b           rax 0x00000000           rbx 0x00000000
 rcx 0x00000000           rdx 0x00000000            r8 0x00000000
  r9 0x00000000           r10 0x00000000           r11 0x00000000
 r12 0x00000000           r13 0x00000000           r14 0x00000000
 r15 0x00000000           rsi 0x00000000           rdi 0x00000000
 rsp 0x7ffebe940b40       rbp 0x00000000           rip 0x00400080
 rflags I
            ;-- entry0:
            ;-- section..text:
            ;-- _start:
            ;-- rip:
            0x00400080      48c7c001000000  movq $1, %rax               ; [1] va=0x00400080 pa=0x00000080 sz=114 vsz=114 rwx=--r-x .text
            0x00400087      48c7c302000000  movq $2, %rbx
            0x0040008e      48c7c103000000  movq $3, %rcx
            0x00400095      48c7c204000000  movq $4, %rdx
            0x0040009c      48c7c605000000  movq $5, %rsi
            0x004000a3      48c7c706000000  movq $6, %rdi
            0x004000aa      48c7c507000000  movq $7, %rbp
            0x004000b1      48c7c408000000  movq $8, %rsp
            0x004000b8      49c7c009000000  movq $9, %r8
            0x004000bf      49c7c10a000000  movq $0xa, %r9
            0x004000c6      49c7c20b000000  movq $0xb, %r10             ; 11
            0x004000cd      49c7c30c000000  movq $0xc, %r11             ; 12
            0x004000d4      49c7c40d000000  movq $0xd, %r12             ; 13
            0x004000db      49c7c50e000000  movq $0xe, %r13             ; 14
            0x004000e2      49c7c60f000000  movq $0xf, %r14             ; 15
            0x004000e9      49c7c710000000  movq $0x10, %r15            ; 16
            0x004000f0      0f0b            ud2

Вот в таком представлении мы и будем работать. Теперь, используя кнопку F7 или s мы можем пошагово выполнять инструкции, сразу же видя изменения в соответствующих регистрах процессора. При этом регистры, изменяющие свои значения, будут подсвечиваться. И метка ;-- rip: (как и значение самого регистра rip) будет смещаться вниз на каждом шаге, пока не дойдет до этой некорректной ud2 инструкции. Можно еще обратить внимание, что когда будет выполнена инструкция mov rsp, 8 вот эта часть

- offset -       0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x7ffea3cb7960  0100 0000 0000 0000 0f87 cba3 fe7f 0000  ................
0x7ffea3cb7970  0000 0000 0000 0000 1787 cba3 fe7f 0000  ................
0x7ffea3cb7980  5187 cba3 fe7f 0000 5c87 cba3 fe7f 0000  Q.......\.......
0x7ffea3cb7990  8187 cba3 fe7f 0000 9387 cba3 fe7f 0000  ................

Будет показывать что-то совсем другое, а именно

- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x00000008  ffff ffff ffff ffff ffff ffff ffff ffff  ................
0x00000018  ffff ffff ffff ffff ffff ffff ffff ffff  ................
0x00000028  ffff ffff ffff ffff ffff ffff ffff ffff  ................
0x00000038  ffff ffff ffff ffff ffff ffff ffff ffff  ................

И адрес начинаться будет с 0x00000008. Тут в этой области нам показывают содержимое стека, и когда мы меняем регистр, хранящий адрес стека (присваиваем ему число 8), мы попадаем на адреса, на которые не отображена никакая физическая память. Процесс работает не с реальными (физическими) адресами, а с виртуальными (это обеспечивается через MMU) и в это виртуальное адресное пространство процесса по тем адресам ничего нет. Можно кстати посмотреть process maps, набрав dm. Выйти из visual mode можно комбинацией Ctrl+D или q

[0x0040006b]> dm
sys   4K 0x0000000000400000 * 0x0000000000401000 s -r-x /home/user/learn/asm/01_test/1/test1 /home/user/learn/asm/01_test/1/test1
sys 132K 0x00007ffd22881000 - 0x00007ffd228a2000 s -rwx [stack] [stack]
sys   8K 0x00007ffd22917000 - 0x00007ffd22919000 s -r-x [vdso] [vdso]
sys   4K 0xffffffffff600000 - 0xffffffffff601000 s -r-x [vsyscall] [vsyscall]

Тут видно, что на адреса от 0x0000000000400000 * 0x0000000000401000 отображено содержимое секции text из нашего elf файле, занимает оно 0x1000 байт (16-ричная система счисления).
readelf показал нам ранее точку входа:

  Entry point address:               0x400080

которая не совпадает с адресом 0x400000

Давайте же посмотрим на наш elf файл в 16-ричной системе счисления

user@localhost:~/learn/asm/01_test/1$ hexdump -C test1
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  80 00 40 00 00 00 00 00  |..>.......@.....|
00000020  40 00 00 00 00 00 00 00  18 01 00 00 00 00 00 00  |@...............|
00000030  00 00 00 00 40 00 38 00  01 00 40 00 05 00 02 00  |....@.8...@.....|
00000040  01 00 00 00 05 00 00 00  00 00 00 00 00 00 00 00  |................|
00000050  00 00 40 00 00 00 00 00  00 00 40 00 00 00 00 00  |..@.......@.....|
00000060  f2 00 00 00 00 00 00 00  f2 00 00 00 00 00 00 00  |................|
00000070  00 00 20 00 00 00 00 00  00 00 00 00 00 00 00 00  |.. .............|
00000080  48 c7 c0 01 00 00 00 48  c7 c3 02 00 00 00 48 c7  |H......H......H.|
00000090  c1 03 00 00 00 48 c7 c2  04 00 00 00 48 c7 c6 05  |.....H......H...|
000000a0  00 00 00 48 c7 c7 06 00  00 00 48 c7 c5 07 00 00  |...H......H.....|
000000b0  00 48 c7 c4 08 00 00 00  49 c7 c0 09 00 00 00 49  |.H......I......I|
000000c0  c7 c1 0a 00 00 00 49 c7  c2 0b 00 00 00 49 c7 c3  |......I......I..|
000000d0  0c 00 00 00 49 c7 c4 0d  00 00 00 49 c7 c5 0e 00  |....I......I....|
000000e0  00 00 49 c7 c6 0f 00 00  00 49 c7 c7 10 00 00 00  |..I......I......|
000000f0  0f 0b 00 2e 73 79 6d 74  61 62 00 2e 73 74 72 74  |....symtab..strt|
00000100  61 62 00 2e 73 68 73 74  72 74 61 62 00 2e 74 65  |ab..shstrtab..te|
00000110  78 74 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |xt..............|
00000120  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000150  00 00 00 00 00 00 00 00  1b 00 00 00 01 00 00 00  |................|
00000160  06 00 00 00 00 00 00 00  80 00 40 00 00 00 00 00  |..........@.....|
00000170  80 00 00 00 00 00 00 00  72 00 00 00 00 00 00 00  |........r.......|
00000180  00 00 00 00 00 00 00 00  10 00 00 00 00 00 00 00  |................|
00000190  00 00 00 00 00 00 00 00  11 00 00 00 03 00 00 00  |................|
000001a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000001b0  f2 00 00 00 00 00 00 00  21 00 00 00 00 00 00 00  |........!.......|
000001c0  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|
000001d0  00 00 00 00 00 00 00 00  01 00 00 00 02 00 00 00  |................|
000001e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000001f0  58 02 00 00 00 00 00 00  90 00 00 00 00 00 00 00  |X...............|
00000200  04 00 00 00 02 00 00 00  08 00 00 00 00 00 00 00  |................|
00000210  18 00 00 00 00 00 00 00  09 00 00 00 03 00 00 00  |................|
00000220  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000230  e8 02 00 00 00 00 00 00  20 00 00 00 00 00 00 00  |........ .......|
00000240  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|
00000250  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000270  00 00 00 00 03 00 01 00  80 00 40 00 00 00 00 00  |..........@.....|
00000280  00 00 00 00 00 00 00 00  01 00 00 00 12 00 01 00  |................|
00000290  80 00 40 00 00 00 00 00  72 00 00 00 00 00 00 00  |..@.....r.......|
000002a0  08 00 00 00 10 00 01 00  f2 00 60 00 00 00 00 00  |..........`.....|
000002b0  00 00 00 00 00 00 00 00  14 00 00 00 10 00 01 00  |................|
000002c0  f2 00 60 00 00 00 00 00  00 00 00 00 00 00 00 00  |..`.............|
000002d0  1b 00 00 00 10 00 01 00  f8 00 60 00 00 00 00 00  |..........`.....|
000002e0  00 00 00 00 00 00 00 00  00 5f 73 74 61 72 74 00  |........._start.|
000002f0  5f 5f 62 73 73 5f 73 74  61 72 74 00 5f 65 64 61  |__bss_start._eda|
00000300  74 61 00 5f 65 6e 64 00                           |ta._end.|
00000308

В самом начале у нас есть ELF заголовок, последовательность байт 7f 45 4c 46, дальше всякая служебная информация, например где точка входа, на какие адреса что отображать. Это мы возможно разберем потом.

Сейчас же, обратим внимание на вот эту часть нашего вывода, которая начинается с 00000080:

00000080  48 c7 c0 01 00 00 00 48  c7 c3 02 00 00 00 48 c7  |H......H......H.|

И идущее далее. Если мы посмотрим на дизасм, который нам выдавал radare2

                            { вот это вот} 
            0x00400080      48c7c001000000  movq $1, %rax               ; [1] va=0x00400080 pa=0x00000080 sz=114 vsz=114 rwx=--r-x .text
            0x00400087      48c7c302000000  movq $2, %rbx

То можно увидеть полное соответствие, инструкция mov rax, 1 кодируется последовательностью байтов 48 c7 c0 01 00 00, эту последовательность мы видем и в дизасме radare2, и в выводе hexdump. Можно это показать

          {    mov $1, %rax  } {    mov $2, %rbx   } { ...
00000080  48 c7 c0 01 00 00 00 48  c7 c3 02 00 00 00 48 c7  |H......H......H.|

Только в radare2 мы видем все по смещению 00400080 а в самом файле по смещению 00000080. Это все можно легко проверить. Используя rasm2 мы можем ассемблировать и дизассемблировать. rasm2 -L покажет список доступных движков для ассемблирования и дизассемблирования. Можно легко отфильтровать те, которые нас на данный момент интересуют: rasm2 -L | grep x86.

Мы будем пользоваться x86.as для ассемблирования и x86 для дизассемблирования. Для примера, попробуем дизассемблировать 48 c7 c0 01 00 00 00:

$ rasm2 -a x86 -s att -b 64 -d '48 c7 c0 01 00 00 00'
movq $1, %rax
$ rasm2 -a x86 -s att -b 64 -d '48c7c001000000'
movq $1, %rax

Как мы видим, пробелы в строке полностью игнорируются. А ассемблировать можно так:

$ rasm2 -a x86.as -s att -b 64 'mov $1, %rax'
48c7c001000000

Для интел синтаксиса порядок операторов другой и нет «%» и «$» перед регистром и числовым значением соответствено:

$ rasm2 -a x86.as -s intel -b 64 'mov rax, 1'
48c7c001000000

Забегая вперед, скажу, что для более сложных инструкций отличия будут более значительны, например инструкция lea:

$ rasm2 -a x86.as -s intel -b 64 'lea rax, [rax+rbx*2+4]'
488d445804
$ rasm2 -a x86.as -s att -b 64 'leaq 4(%rax, %rbx, 2), %rax'
488d445804

Эта инструкция выполняет действие rax = rax + rbx*2 + 4 и тут intel синтаксис оказывается значительно более понятным.

Каждая «цифра» в 16-ричной системе счисления кодирует по 4 бита. f например кодирует 1111 в двоичной системе, а 7 кодирует 0111. Пара «цифр» в 16-ричной системе счисления образует 8-битный байт (бывают и байты другой битности, но мы их не будем рассматривать в рамках этой книги). Если мы вышеобозначенный f заменим на 7 то последний байт будет 0111 1111 вместо 1111 1111.

$ rasm2 -a x86.udis -s att -b 64 -d '48 c7 c0 ff ff ff 7f'
mov $0x7fffffff, %rax
$ rasm2 -a x86 -s att -b 64 -d '48 c7 c0 ff ff ff 7f'
movq $0x7fffffff, %rax

И если заменить 7f на 8f (отчего последний байт станет 0111 1111), мы на выходе получим совсем другой результат:

$ rasm2 -a x86 -s att -b 64 -d '48 c7 c0 ff ff ff 8f'
movq $-0x70000001, %rax
$ rasm2 -a x86.udis -s att -b 64 -d '48 c7 c0 ff ff ff 8f'
mov $0xffffffff8fffffff, %rax

Очевидно, что такой вот инструкцией movq мы не можем вписать в 64-битный регистр произвольное число от ffffffffffffffff до 0000000000000000 — количество байт, выделенное в опкоде под запись непосредственно того числа, которое мы хотим поместить в регистр, слишком мало. Тут происходит так называемое знаковое расширение, о чем более подробно будет сказано позже.

Рассмотрим кодирование инструкции более подробно: 48 c7 c_ ?? ?? ?? ??, где вопросительные знаки это то число, которое мы собственно и записываем в регистр. При этом используется порядок байт little endian (см. https://en.wikipedia.org/wiki/Endianness) т. е. чтоб записать единицу, необходимо сделать 48 c7 c_ 01 00 00 00. И последний бит, как уже было ранее сказано, выставляет в ffff... или в 0000... старшую часть 64-битного регистра (знаковое расширение), т. е. это означает, что 48 c7 c_ 01 00 00 00 запишет в регистр 0x1, а например 48 c7 c_ 00 01 00 00 запишет уже 0x100. А если 00 00 00 80 то записано будет число ffffffff80000000. Символом подчеркивания _ тут 48 c7 c_ ?? ?? ?? ?? обозначена та часть, меняя которую мы можем изменить то, в какой конкретно регистр будет записано наше число. Например, 48c7c001000000, 48c7c101000000, 48c7c201000000 будут записывать единицу в регистр %rax, %rcx, %rdx и %rbx соответственно. Так можно продолжать вплоть до 48c7c701000000, дальше уже будет некорректная инструкция. Это кстати можно видеть в дизасме нашей тестовой программы:

                                 V
            0x00400080      48c7c001000000                                                 movq $1, %rax ; [1] va=0x00400080 pa=0x00000080 sz=114 vsz=114 rwx=--r-x .text
            0x00400087      48c7c302000000                                                 movq $2, %rbx
            0x0040008e      48c7c103000000                                                 movq $3, %rcx
            0x00400095      48c7c204000000                                                 movq $4, %rdx
                                 ^

Вот эта самая частьc0 c3 c1 c2, отвечающая за то, в какой регистр что записать. Для регистров %r8⁠—⁠15 применяется, как нетрудно заметить, немного другие инструкции

                                 V
            0x004000b8      49c7c009000000                                                 movq $9, %r8
            0x004000bf      49c7c10a000000                                                 movq $0xa, %r9
            0x004000c6      49c7c20b000000                                                 movq $0xb, %r10
            0x004000cd      49c7c30c000000                                                 movq $0xc, %r11
            0x004000d4      49c7c40d000000                                                 movq $0xd, %r12
            0x004000db      49c7c50e000000                                                 movq $0xe, %r13
            0x004000e2      49c7c60f000000                                                 movq $0xf, %r14
            0x004000e9      49c7c710000000                                                 movq $0x10, %r15
                                 ^

49c7 вместо 48c7, а часть с c0⁠—c7 отвечает за то, в какой из этих %r? регистров записать число. Но подробно вникать в то, как какая инструкция каким образом кодируется, не нужно практически никогда. Это может быть нужно в случае, если вы пишете свой ассемблер, дизассемблер, делаете JIT компилятор (примечание: можно будет потом написать в книге о том, как это можно сделать) который непосредственно в процессе выполнения программы генерирует машинный код и помещает его в исполняемый регион памяти, пишете компилятор, который сразу же генерирует на выходе двоичный код (а не код на ассемблере, как это делает clang или gcc) или если необходимо сильно запутать код всяким там самомодифицирующимся кодом и прыжками в середину инструкций, усложнив тем самым его анализ. Кстати регистров нам доступно значительно больше, и даже эти регистры, которые мы сейчас рассматриваем, они «составлены» из отдельных кусочков-подрегистров.

Данная статья или раздел ещё не завершены
Кто-то посчитал, что статья или раздел ниже не содержит какой-то важной информации или имеет проблемы с вёрсткой/текстом. Указана причина: «рассмотреть множественность интерпретации (дизассемблирования) определенной последовательности байт»{{#ifeq: {{{1}}} | nopoint | | . }}

{{#if: Доработка | }}

48 b8 01 48 31 c0 48 8d 04 18
movabsq $0x18048d48c0314801, %rax
movl $0xc0314801, %eax leaq (%rax, %rbx), %rax
xorq %rax, %rax
xorl %eax, %eax
addl %ecx, 0x31(%rax) rorb $4, -0x73(%rax)
leal (%rax, %rbx), %eax
addb $0x18, %al

Если мы запустим нашу программу в отладчике и получим содержимое памяти от 0x00400000 до 0x00400308, то мы фактически получим весь наш ELF файл, который уместился в один сегмент. Конечно это не всегда так, но у нас очень простая программа, и тут это сработает.

[0x00400080]>  px 0x308 @ 0x400000
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x00400000  7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
0x00400010  0200 3e00 0100 0000 8000 4000 0000 0000  ..>.......@.....
0x00400020  4000 0000 0000 0000 1801 0000 0000 0000  @...............
0x00400030  0000 0000 4000 3800 0100 4000 0500 0200  ....@.8...@.....
0x00400040  0100 0000 0500 0000 0000 0000 0000 0000  ................
...

Мы можем даже сохранить (дампнуть) это в бирнарном виде, сделать это можно следующим образом

[0x00400080]> y 0x308 @ 0x400000
[0x00400080]> yp > outfile.bin

И если сопоставить получившийся outfile.bin с нашим test1 то отличий не будут (только в самый конец файла будет добавлен 0x0a байт — перенос строки (newline))
Можно еще сделать это вот так:

[0x00400080]> wt outfile.bin 0x308 @ 0x400000
dumped 0x308 bytes
Dumped 776 bytes from 0x00400000 into outfile.bin

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

{{#if: Доработка | }}

Регистры

Регистры общего назначения (A, B, C and D)

64 56 48 40 32 24 16 8
R?X
E?X
?X
?H ?L

64-bit mode-only регистры общего назначения (R8, R9, R10, R11, R12, R13, R14, R15)

64 56 48 40 32 24 16 8
?
?D
?W
?B

Сегментные регистры (C, D, S, E, F и G)

16 8
?S

Регистры-указатели (S и B)

64 56 48 40 32 24 16 8
R?P
E?P
?P
?PL

Примечание: ?PL регистры доступны только в 64-bit mode.

Индексные регистры (S и D)

64 56 48 40 32 24 16 8
R?I
E?I
?I
?IL

Примечание: ?IL регистры доступны только в 64-bit mode.

Instruction Pointer Register (I)

64 56 48 40 32 24 16 8
RIP
EIP
IP

В 32-битном режиме работы, 64-битные регистры недоступны, но есть некоторые инструкции, которые могут работать с двумя 32-битными регистрами, рассматривая их как один 64-битный, например инструкция беззнакового умножения http://x86.renejeschke.de/html/file_module_x86_id_210.html. В 64-битном режиме тоже есть инструкции, которые возвращают результат сразу в двух 64-битных регистрах (например инструкция умножения или деления, которая сразу может вернуть и результат деления, и остаток от деления).

Чтобы лучше разобраться с этими кусочками регистров, напишем несколько программ-примеров, которые что-то с этими кусочками делают. Создадим в директории ~/learn/asm/01_test/2/ файл test2.s следующего содержания

	.section	.text,"ax",@progbits
	.p2align 4,,15
	.globl	_start
	.type	_start, @function
_start:
	movabsq $0x1122334455667788, %rax


	movb $0xff, %al
	inc %al
	dec %al
	movb $0xab, %al
	inc %al
	dec %al
	
	movb $0xff, %ah
	inc %ah
	dec %ah
	movb $0xcd, %ah
	inc %ah
	dec %ah

	movw $0xffff, %ax
	inc %ax
	dec %ax
	movw $0xacbd, %ax

	movl $0xaabbccdd, %eax
	
	movl $0x7fffffff, %eax
	inc %eax
	dec %eax
	movl $0x80000000, %eax
	dec %eax
	inc %eax
	movl $0xffffffff, %eax
	inc %eax
	dec %eax
	
	movq $0xffffffffffffffff, %rax
	inc %eax
	dec %eax
	
	movq $0xffffffffffffffff, %rax
	inc %rax
	dec %rax
	
	movq $0xffffffffffffffff, %rax
	movabsq $0xffffffffffffffff, %rax
	ud2
	.size	_start, .-_start

Соберем его уже знакомым нам способом.

user@localhost:~/learn/asm/01_test/2$ as test2.s -o test2.o
user@localhost:~/learn/asm/01_test/2$ ld test2.o -o test2

Между прочим, можно создать скрипт, который будет это делать сам. Но к этой теме мы вернемся несколько позже. Сейчас же давайте запустим получившийся исполняемый файл в отладичке radare2 и перейдем в визуальный режим.

user@localhost:~/learn/asm/01_test/2$ r2 -d test2
Process with PID 960 started...
attach 960 960
bin.baddr 0x00400000
Assuming filepath ./test2
Warning: Cannot initialize dynamic strings
Warning: Too big version info field 3 (496)
asm.bits 64
 -- In Soviet Russia, radare2 has documentation.
[0x00400080]> V

Может потребоваться так же переключить отображения синтаксиса ассемблера в att: e asm.syntax=att и длину отображение инструкций e asm.nbytes=30 если это не было сделано ранее в rc файле ~/.radare2rc (о чем было ранее сказано). Потом нажимаем букву p в английской раскладке на клавиатуре, пока не перейдем к такому виду:

 [0x00400080 265 ./test2]> ?0;f tmp;s.. @ entry0
- offset -       0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x7ffdd38518f0  0100 0000 0000 0000 0727 85d3 fd7f 0000  .........'......
0x7ffdd3851900  0000 0000 0000 0000 0f27 85d3 fd7f 0000  .........'......
0x7ffdd3851910  4927 85d3 fd7f 0000 5427 85d3 fd7f 0000  I'......T'......
0x7ffdd3851920  7927 85d3 fd7f 0000 8b27 85d3 fd7f 0000  y'.......'......
orax 0x0000003b           rax 0x00000000           rbx 0x00000000
 rcx 0x00000000           rdx 0x00000000            r8 0x00000000
  r9 0x00000000           r10 0x00000000           r11 0x00000000
 r12 0x00000000           r13 0x00000000           r14 0x00000000
 r15 0x00000000           rsi 0x00000000           rdi 0x00000000
 rsp 0x7ffdd38518f0       rbp 0x00000000           rip 0x00400080
 rflags I
            ;-- entry0:
            ;-- section..text:
            ;-- _start:
            ;-- rip:
            0x00400080      48b88877665544332211                                           movabsq $0x1122334455667788, %rax ; [1] va=0x00400080 pa=0x00000080 sz=123 vsz=123 rwx=--r-x .text
            0x0040008a      b0ff                                                           movb $0xff, %al ; 255
            0x0040008c      fec0                                                           incb %al
            0x0040008e      fec8                                                           decb %al
            0x00400090      b0ab                                                           movb $0xab, %al ; 171
            0x00400092      fec0                                                           incb %al
            0x00400094      fec8                                                           decb %al
            0x00400096      b4ff                                                           movb $0xff, %ah ; 255
            0x00400098      fec4                                                           incb %ah
            0x0040009a      fecc                                                           decb %ah
            0x0040009c      b4cd                                                           movb $0xcd, %ah ; 205
            0x0040009e      fec4                                                           incb %ah
            0x004000a0      fecc                                                           decb %ah
            0x004000a2      66b8ffff                                                       movw $0xffff, %ax
            0x004000a6      66ffc0                                                         incw %ax
            0x004000a9      66ffc8                                                         decw %ax
            0x004000ac      66b8bdac                                                       movw $0xacbd, %ax
            0x004000b0      b8ddccbbaa                                                     movl $0xaabbccdd, %eax
            0x004000b5      b8ffffff7f                                                     movl $0x7fffffff, %eax
            0x004000ba      ffc0                                                           incl %eax
            0x004000bc      ffc8                                                           decl %eax
            0x004000be      b800000080                                                     movl $0x80000000, %eax
            0x004000c3      ffc8                                                           decl %eax
            0x004000c5      ffc0                                                           incl %eax
            0x004000c7      b8ffffffff                                                     movl $0xffffffff, %eax ; -1 ; -1
            0x004000cc      ffc0                                                           incl %eax
            0x004000ce      ffc8                                                           decl %eax
            0x004000d0      48c7c0ffffffff                                                 movq $-1, %rax
            0x004000d7      ffc0                                                           incl %eax
            0x004000d9      ffc8                                                           decl %eax
            0x004000db      48c7c0ffffffff                                                 movq $-1, %rax
            0x004000e2      48ffc0                                                         incq %rax
            0x004000e5      48ffc8                                                         decq %rax
            0x004000e8      48c7c0ffffffff                                                 movq $-1, %rax
            0x004000ef      48b8ffffffffffffffff                                           movabsq $0xffffffffffffffff, %rax
            0x004000f9      0f0b                                                           ud2

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

Вначале идет инструкция movabs (с суффиксом q означающим что работаем мы с 64-битным регистром).

Инструкции ассемблер GAS как правило идут с суффиксами «b», «s», «w», «l», «q» или «t» чтобы задать размер операнда.

  • b = byte (8 бит)
  • s = short (16 бит integer) или single (32-бит floating point)
  • w = word (16 бит)
  • l = long (32 бит integer или 64-бит floating point)
  • q = quad (64 бит)
  • t = 10 байт (80-бит floating point)

В 64-битном коде abs используется для кодирование mov инструкции с 64-битным смещенем или непосредственым (immediate) операндом. При этом часть abs может влиять, а может и не влиять на то, что нам сгенерирует ассемблер. Рассмотрим такой пример:

$ rasm2 -a x86.as -s att -b 64 'mov $0x1122334455667788, %rax'
48b88877665544332211
$ rasm2 -a x86.as -s att -b 64 'movq $0x1122334455667788, %rax'
48b88877665544332211
$ rasm2 -a x86.as -s att -b 64 'movabs $0x1122334455667788, %rax'
48b88877665544332211
$ rasm2 -a x86.as -s att -b 64 'movabsq $0x1122334455667788, %rax'
48b88877665544332211

Однако, если значение, которое мы хотим записать в регистр rax будет в самом начале состоять из нулей (необходимо 33 подряд идущих нулевых бита в начале), результат будет отличаться:

$ rasm2 -a x86.as -s att -b 64 'mov $0x0000000055667788, %rax'
48c7c088776655
$ rasm2 -a x86.as -s att -b 64 'movq $0x0000000055667788, %rax'
48c7c088776655
$ rasm2 -a x86.as -s att -b 64 'movabs $0x0000000055667788, %rax'
48b88877665500000000
2$ rasm2 -a x86.as -s att -b 64 'movabsq $0x0000000055667788, %rax'
48b88877665500000000

Как видно, в данном случае abs в явном виде задает, что операнд должен быть 64-битным. Если же нулевыми битами будет занято только 32 бита, ассемблер не сможет сгенерировать более короткую инструкцию. Например, возьмем такие операнды, чтобы в двоичной системе счисления тот 32 бит был нулем 0111 1111 (в 16-ричной системе счисления это будет 7f) и чтобы бит был единицей: 1000 0000 (80)

                                                VV
$ rasm2 -a x86.as -s att -b 64 'movq $0x000000007f667788, %rax'
48c7c08877667f
                                                VV
$ rasm2 -a x86.as -s att -b 64 'movq $0x0000000080667788, %rax'
48b88877668000000000

Как видим, с единицей в 32-м бите ассемблер не может сгенерировать короткую инструкцию. Это все происходит оттого, что происходит расширение соответствующего бита на старшие разряды (это т. н. знаковое расширение). Для примера movq $0x000000007f667788, %rax использовано всего 7 байт :48c7c08877667f. В куске 48c7c0 закодирована как бы сама инструкция, а вот тут 8877667f содержится само значение, только оно как бы отзеркалено, 7f байт у нас лежит в самом конце, а 88 в самом начале. Это связано с особенностями реализации процессора, его порядком байт (endian mode). Вот таким нехитрым образом нулевой байт из 7f расширяется на последущие разряды:

┏┳┳┳┳┳┳┳━┳┳┳┳┳┳┳┳━┳┳┳┳┳┳┳┳━┳┳┳┳┳┳┳┳━┓
╿╿╿╿╿╿╿╿ ╿╿╿╿╿╿╿╿ ╿╿╿╿╿╿╿╿ ╿╿╿╿╿╿╿╿ ┃
00000000 00000000 00000000 00000000 01111111 01100110 01110111 10001000
   00       00       00       00       7f       66       77       88

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

$ rasm2 -a x86.as -s att -b 64 'movq $0xffffffff7f667788, %rax'
48b88877667fffffffff
$ rasm2 -a x86.as -s att -b 64 'movq $0xffffffff80667788, %rax'
48c7c088776680

Т. е. тут мы наблюдаем обратную ситуацию. С 7f инструкция выходит длиннее, чем с 80 потому что при 80 происходит заполнение старшего куска единицами, а в данном случае именно это нам и нужно. В последнем случае происходит такое знаковое расширение бит:

┏┳┳┳┳┳┳┳━┳┳┳┳┳┳┳┳━┳┳┳┳┳┳┳┳━┳┳┳┳┳┳┳┳━┓
╿╿╿╿╿╿╿╿ ╿╿╿╿╿╿╿╿ ╿╿╿╿╿╿╿╿ ╿╿╿╿╿╿╿╿ ┃
11111111 11111111 11111111 11111111 10000000 01100110 01110111 10001000
   ff       ff       ff       ff       80       66       77       88

Сделано это было для того, чтобы такой короткой инструкцией можно было бы записать в регистр в том числе и отрицательные числа (но при этом было вполовину урезано множество положительных чисел, которые можно записать этой инструкцией). Для представления отрицательных чисел используется т. н. дополнительный код https://ru.wikipedia.org/wiki/Дополнительный_код_(представление_числа) (еще называют two’s complement).

Если мы пробуем заассемблировать инструкцию, где старшая 33-битная часть будет заполнена и не сплошными единицами и не сплошными нулями, мы всегда получаем более длинный опкод:

$ rasm2 -a x86.as -s att -b 64 'mov $0xbbaaaaaaaaaaaacc, %rax'
48b8ccaaaaaaaaaaaabb

Ассемблер старается подобрать более короткий опкод, когда возможно (если мы явно не указываем генерировать длинный опкод, как в случае с movabs). Но разные ассемблеры могут выдавать разный результирующий двоичный код. В некоторых случаях (архитектуры Intel это не касается, хотя кто знает, может быть когда-нибудь…) ассемблер может даже переставлять инструкции местами с целью лучшей оптимизации. Например, для MIPS в GAS есть директива .set noreorder которая отключает и .set reorder которая включает «перетасовку» инструкций, чтоб лучше нагружать конвейер процессора. Современные процессоры исполняют инструкции не строго последовательно, а способны обрабатывать несколько инструкций за раз (на одном ядре) и исполнять инструкции не в том порядке, в котором они записаны (внеочередное исполнение, см https://en.wikipedia.org/wiki/Out-of-order_execution) но при этом важно отметить, что процессор всегда действует таким образом, чтобы все выглядело таким образом, будто бы инструкции выполняются последовательно. Иными словами, от этого внеочередного исполнения и конвейеризации программа не станет вести себя не так, как если б процессор исполнял инстукции строго последовательно. В этой книге я не планирую описывать особенности вычислительного конвейера разных процессоров. Дополнительные ссылки https://en.wikipedia.org/wiki/Instruction_pipelining https://en.wikipedia.org/wiki/Hazard_(computer_architecture), https://en.wikipedia.org/wiki/Bubble_(computing).

Некоторые ассемблеры даже инструкцию mov $0, %rax ассемблируют таким образом, что она будет длинной. Например, сравним результат x86.as x86.ks x86.nasm (для NASM придется использовать Intel синтаксис, т. к. другой синтаксис он не поддерживает)

$ rasm2 -a x86.as -s att -b 64 'mov $0, %rax'
48c7c000000000
$ rasm2 -a x86.ks -s att -b 64 'mov $0, %rax'
48b80000000000000000
$ rasm2 -a x86.nasm -s intel -b 64 'mov rax, 0'
b800000000

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

$ rasm2 -a x86 -s att -b 64 -d '48c7c000000000'
movq $0, %rax
$ rasm2 -a x86 -s att -b 64 -d '48b80000000000000000'
movabsq $0x0, %rax
$ rasm2 -a x86 -s att -b 64 -d 'b800000000'
movl $0, %eax

Все 3 инструкции тут отличаются. В первом случае используется mov с префиксом q (movq) — означает move quad (но в самой инструкии использовано всего 4 байта под само число, записываемое в регистр. Старшая часть получается от знакового расширения нулевого бита на старшую часть). Во втором используется abs означающий запись абсолютного (64-битного) без убирания нулей в опкоде. В третьем случае у нас nasm сгенерил опкод, который записывает в регистр %eax (при этом задействуя) но инструкция, записывающая в %eax срабатывает таким образом, что старшая часть 64-битного регистра при этом обнуляется (при этом не происходит никакого знакового расширения, т. е. иснтрукцией mov $0xffffffff, %rax мы не заполняем весь 64-битный регистр целиком одними единицами, но старшая часть при этом всегда зануляется). Все 3 варианта по факту дают тот же результат, только сами опкоды отличаются в размере.

Примечание: перевести из 16-ричной системы счисления в двоичную можно с помощью rax2

$ rax2 Bx80
10000000b
$ rax2 Bx7f
1111111b

Не лишним так же будет набрать rax2 -h чтобы увидеть другие примеры использования.

Итак, нажав s мы перешагнули через эту инструкцию movabsq. Исполнение первой инструкции

            0x00400080      48b88877665544332211                                           movabsq $0x1122334455667788, %rax ; [1] va=0x00400080 pa=0x00000080 sz=123 vsz=123 rwx=--r-x .text

Приводит к записи в регистр rax значения 0x1122334455667788Мы можем видеть, что значение rax у нас изменилось:

orax 0xffffffffffffffff   rax 0x1122334455667788   rbx 0x00000000 

При этом можно заметить, что строчка ;-- rip: теперь опустилась ниже данной инструкии.

            0x00400080      48b88877665544332211                                           movabsq $0x1122334455667788, %rax ; [1] va=0x00400080 pa=0x00000080 sz=123 vsz=123 rwx=--r-x .text
            ;-- rip:
            0x0040008a      b0ff                                                           movb $0xff, %al ; 255

Регистр-указатель на текущую инструкцию %rip уменьшился на единицу, Метка ;-- rip: — теперь она ниже предыдущей инструкции movabsq. Изменившиеся регистры подсвечиваются соответствующим цветом в консоли

Далее у нас идет инструкция movb.

Инструкция movb записана с суффиксом b в конце. Это означает, что она работает с однобайтными операндами. В данном случае, она изменит значение в однобайтном регистре %al.

Если суффикс не указан, и нет операнда памяти для команды, GAS выводит размер операнда из размера целевого регистра операнда (конечный операнд). В частности, movb $0xff, %al можно поменять на mov $0xff, %al и ассемблер нам выдаст то же самое. Это можно проверить через rasm:

$ rasm2 -a x86.as -s att -b 64 'movb $0xff, %al'
b0ff
$ rasm2 -a x86.as -s att -b 64 'mov $0xff, %al'
b0ff

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

Нажмем s чтобы выполнить эту инструкцию, «перешагнуть через нее»:

           0x00400080      48b88877665544332211                                           movabsq $0x1122334455667788, %rax ; [1] va=0x00400080 pa=0x00000080 sz=123 vsz=123 rwx=--r-x .text                 
           0x0040008a      b0ff                                                           movb $0xff, %al ; 255
           ;-- rip:
           0x0040008c      fec0                                                           incb %al

Данная инструкция записала в однобайтный регистр %al значение 0xff в 16-ричной системе счисления, в десятичной системе счисления будет это будет 255 что соответствующим образом показано в «примечаниях» к инструкции. Если посмотреть на состояние регистров, то мы заметим, что регистр %rax слегка изменился. Изменилась его самая младшая половина
Было:
orax 0xffffffffffffffff rax 0x1122334455667788 rbx 0x0000000
Стало:
orax 0xffffffffffffffff rax 0x11223344556677ff rbx 0x0000000
Это все потому, что регистр %al является самым младшим кусочком регистра %rax. Таким вот образом мы можем частично перезаписать регистр %rax. Что ж, идем дальше.

Дальше у нас идет инструкция incb %al, увеличивающая регистр %al на единицу.

            ;-- rip:
            0x0040008c      fec0                                                           incb %al
            0x0040008e      fec8                                                           decb %al

Сейчас обратите внимание на значение особого регистра rflags и запомните его состояние т.к. после этой инструкции он изменится.

 rflags 1I
Выполним инструкцию.
           0x0040008c      fec0                                                           incb %al
           ;-- rip:
           0x0040008e      fec8                                                           decb %al

Как можно заметить, в регистре %rax поменялся последний байт (последние два знака):

orax 0xffffffffffffffff   rax 0x1122334455667700   rbx 0x00000000

Это все потому, что у нас произошло переполнение. Можно представить это следующим образом: если складывать в столбик два числа, 99 и 1, и при этом отбрасывать все, кроме последних двух цифр.

 99 
+ 1
___
100
 00

мы получаем ноль. Так и тут, 0xff это самое большое число, которое может содержать в себе однобайтный регистр %al, и после добавление единицы он просто обнуляется. А так как этот регистр являеся лишь кусочком 8-битного регистра %rax, мы видим «обнуление» младшей части соответсвующего регистра. При этом значение особого регистра rflags, на состояние которого я просил обратить внимание, тоже изменилось

Данная статья или раздел ещё не завершены
Кто-то посчитал, что статья или раздел ниже не содержит какой-то важной информации или имеет проблемы с вёрсткой/текстом. Указана причина: «дописать про rflags. eflags возможно стоит добавить про cmovcc инструкции. Пока не пофикшено https://github.com/radare/radare2/issues/6374 данная часть дописана не будет»{{#ifeq: {{{1}}} | nopoint | | . }}

{{#if: Доработка | }}

Арифметические операции

Некоторые системные вызовы, файловые дескрипторы, запись/чтение файлов/пайпов, стандартные потоки ввода-вывода, простейшие циклы, ветвления, рекурсия, вызов функции, адрес возврата, косвенная адресация

Получаем аргументы командной строки и переменных окружения в ассемблере

Некоторые алгоритмы и структуры данных

Пишем простейший стековый калькулятор (обратная польская нотация)

shared memory, SysV IPC, mmap, mremap, mprotect, brk/sbrk отображение файла в память процесса, mprotect, fork, clone, mutex etc…

Сокеты, tcp, udp, raw socket, select, poll, epoll

Привилегии процесса, вызовы getrlimit(2), setrlimit(2), setuid(2), seteuid(2)…

Что-нибудь про VSDO

Си

Разные типы в Си, sizeof, указатели. Вывод на печать текста через системный вызов write(), работа с файлами, функции семейства printf() scanf()

Си является языком со слабой статической типизацией, с неявным приведением типов (кастами). Есть базовые целочисленные типы, спецификаторы для них char, short, int, long, long long (при этом long, long long может идти перед int, но не char). Есть еще спецификатор, определяющее то, знаковый или беззнаковый у нас тип: signed, unsigned. Знаковость/беззнаковость char(без спецификатора знаковости) в стандарте не определена, для остальных целочисленных типов (без спецификатора знаковости) они являются знаковыми. Размеры типов определяются в байтах, но байты бывают разными. В Си байт далеко не всегда состоит из 8 бит. В заглавочном файле limits.h определен макрос CHAR_BIT, описывающий число бит в байте. Но байт не может быть менее чем 8-битным по современному стандарту, т. к. в 7 бит не хватит места отобразить требуемые по стандарту C11 SCHAR_MIN и SCHAR_MAX определенный в стандарте (см. 5.2.4.2.1 «Sizes of integer types <limits.h>»)

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf, 6.7.2 «Type specifiers <limits.h>» — заглавочный файл limits.h обычно находится в директории /usr/include/. Узнать место, где по умолчанию производится поиск и подстановка заглавочных файлов, имена которых заключены в т. н. угловые скобки, можно через echo | gcc -E -Wp,-v -

Ну что ж, давайте попробуем поработать с некоторыми типами.

<syntaxhighlight lang=»c»>

  1. include <unistd.h>

int main (void)
{

 const char nl = '\n';
 const char val_char = 0x4142434445464748ULL;
 const short val_short = 0x4142434445464748ULL;
 const long val_long = 0x4142434445464748ULL;
 const long long val_long_long = 0x4142434445464748ULL;
 const int val_int = 0x4142434445464748ULL;
 const short int val_short_int = 0x4142434445464748ULL;
 const long int val_long_int = 0x4142434445464748ULL;
 const long long int val_long_long_int = 0x4142434445464748ULL;
 const __int128 val__int128 = ((__int128)(0x4142434445464748ULL)<<64) | ((__int128)(0x494A4B4C4D4E4F50ULL));
 write(STDOUT_FILENO, "val_char:\n", sizeof("val_char:\n")-1);
 write(STDOUT_FILENO, &val_char, sizeof(val_char));
 write(STDOUT_FILENO, &nl, sizeof(nl));
 write(STDOUT_FILENO, "val_short:\n", sizeof("val_short:\n")-1);
 write(STDOUT_FILENO, &val_short, sizeof(val_short));
 write(STDOUT_FILENO, &nl, sizeof(nl));
 write(STDOUT_FILENO, "val_long:\n", sizeof("val_long:\n")-1);
 write(STDOUT_FILENO, &val_long, sizeof(val_long));
 write(STDOUT_FILENO, &nl, sizeof(nl));
 write(STDOUT_FILENO, "val_long_long:\n", sizeof("val_long_long:\n")-1);
 write(STDOUT_FILENO, &val_long_long, sizeof(val_long_long));
 write(STDOUT_FILENO, &nl, sizeof(nl));
 write(STDOUT_FILENO, "val_int:\n", sizeof("val_int:\n")-1);
 write(STDOUT_FILENO, &val_int, sizeof(val_int));
 write(STDOUT_FILENO, &nl, sizeof(nl));
 write(STDOUT_FILENO, "val_short_int:\n", sizeof("val_short_int:\n")-1);
 write(STDOUT_FILENO, &val_short_int, sizeof(val_short_int));
 write(STDOUT_FILENO, &nl, sizeof(nl));
 write(STDOUT_FILENO, "val_long_int:\n", sizeof("val_long_int:\n")-1);
 write(STDOUT_FILENO, &val_long_int, sizeof(val_long_int));
 write(STDOUT_FILENO, &nl, sizeof(nl));
 write(STDOUT_FILENO, "val_long_long_int:\n", sizeof("val_long_long_int:\n")-1);
 write(STDOUT_FILENO, &val_long_long_int, sizeof(val_long_long_int));
 write(STDOUT_FILENO, &nl, sizeof(nl));
 write(STDOUT_FILENO, "val__int128:\n", sizeof("val__int128:\n")-1);
 write(STDOUT_FILENO, &val__int128, sizeof(val__int128));
 write(STDOUT_FILENO, &nl, sizeof(nl));
 return 0;

}
</syntaxhighlight>

Функция write пишет в файловый дескриптор STDOUT_FILENO (стандартный вывод) последовательность байтов, хранимую в типе. sizeof() возвращает размер типа в байтах. Эта информация нужна функции write() чтобы было понятно, сколько именно байтов надо вывести. Символ & это взятие адреса переменной (получение указателя на нее). О работе указателей мы поговорим несколько позже. Если вы попробуете скомпилировать этот код, компилятор вам выдаст множество варнингов, связанных с тем, что мы пытаемся присвоить в тип бОльшее значение, чем он в себе может вместить. С типом __int128 вообще интересная история — такой тип не описан в стандарте Си, это — нестандартное расширение компилятора. И инициализируется о несколько странно, связано это опять таки с его нестандартностью, т. е. стандартом Си не предусмотрен тип __int128 и инициализировать его надо таким вот кривым методом — двумя кусками, один из которых сдвигаем на 64 бита влево… Впрочем, я забегаю вперед. Об операциях and, or и двоичных сдвигах будет написано позже.

Давайте пока разберемся, что же это за значение 0x4142434445464748ULL и почему оно превращается в последовательность латинских букв HGFEDCBA. Есть таблица соответствия определенных символов определенным кодам. В соответствии с этой таблицей, код 0x41 это символ A, код 0x42 это символ B, код 0x43 это символ C и так далее. Но почему мы тогда получили вместо ABCDEFG последовательность HGFEDCBA ? Ответ кроется в порядке байт (endianness), который принят в архитектуре x86 и x86-64. Более подробно этот момент освещен в соответствующей статье на вики

Взглянем на вывод нашей программы:

 val_char:
 H
 val_short:
 HG
 val_long:
 HGFEDCBA
 val_long_long:
 HGFEDCBA
 val_int:
 HGFE
 val_short_int:
 HG
 val_long_int:
 HGFEDCBA
 val_long_long_int:
 HGFEDCBA
 val__int128:
 PONMLKJIHGFEDCBA

Для val_char вывелась всего одна буква — это всё оттого, что размер у типа char на нашей платорме — 1 байт. Но вообще, размер char всегда будет 1 байт, но сам байт не всегда может состоять из 8 бит.
Из нашего присвоения
<syntaxhighlight lang=»c»>

 const char val_char = 0x4142434445464748ULL;

</syntaxhighlight>
в переменную val_char записался только самый конец — 0x48 — который соответствует символу H

Для val_short вывелось уже целых две буквы HG :
<syntaxhighlight lang=»c»>

 const short val_short = 0x4142434445464748ULL;

</syntaxhighlight>
Тут можно заменить это на
<syntaxhighlight lang=»c»>

 const short val_short = 0x4748;

</syntaxhighlight>
и поведение останется тем же. Компилятор просто срезает все старшие разряды, оставляя только этот кусочек 0x4748. И выводится у нас сначала H и потом G из-за little-endian порядка байт. Для всех прочих типов история повторяется.

Рассмотрим более подробно некоторые моменты.

Вот например строчка write(STDOUT_FILENO, "val_char:\n", sizeof("val_char:\n")-1);

и строчка write(STDOUT_FILENO, "val_short:\n", sizeof("val_short:\n")-1);

и прочие строчки, подобные ей. Что тут происходит? Функция write, как ранее уже было сказано, является сишной оберткой над линуксовым системным вызовом write. Более подробно о нем можно почитать в соотвествующей статье на википедии, можно еще набрать man 2 write или почитать в интернете: https://linux.die.net/man/2/write. Первым аргументом эта функция принимает номер файлового дескриптора, в данном случае это у нас STDOUT_FILENO который определен в заглавочном файле unistd.h, его обычно можно найти по адресу /usr/include/unistd.h, там же определен прототип функции write().
<syntaxhighlight lang=»c»>
/* Write N bytes of BUF to FD. Return the number written, or -1.

  This function is a cancellation point and therefore not marked with
  __THROW.  */

extern ssize_t write (int __fd, const void *__buf, size_t __n) __wur;
</syntaxhighlight>

И тут видно, что первый аргумент int, второй void *, третий size_t. Первый аргумент это номер файлового дескриптора. STDOUT_FILENO определен во все том же файле unistd.h
<syntaxhighlight lang=»c»>
/* Standard file descriptors. */

  1. define STDIN_FILENO 0 /* Standard input. */
  2. define STDOUT_FILENO 1 /* Standard output. */
  3. define STDERR_FILENO 2 /* Standard error output. */

</syntaxhighlight>

Т.е. если написать write(0, "val_char:\n", sizeof("val_char:\n")-1); вместо write(STDOUT_FILENO, "val_char:\n", sizeof("val_char:\n")-1); — будет то же самое. Про данный заглавочный файл еще сказано в POSIX.1-2017: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/unistd.h.html :

 The <unistd.h> header shall define the following symbolic constants for file streams:
 
 STDERR_FILENO
     File number of stderr; 2.
 STDIN_FILENO
     File number of stdin; 0.
 STDOUT_FILENO
     File number of stdout; 1. 

Параметр const void *__buf это указатель на данные, которые мы выводим. Спецификатор const означает, что данные через этот указатель не могут быть изменены, а только лишь прочитаны. Функция write() пишет из памяти в файл, этим мы говорим что она не меняет те данные, которые она пишет в файл. Более полно этот момент освещен в https://stackoverflow.com/a/34842262

Насчет вот этого __wur — эта штука определена через макрос и может быть раскрыта в специальный атрибут для компилятора GCC, который применяется только при определенном значении __USE_FORTIFY_LEVEL — https://sourceware.org/git/?p=glibc.git;a=blob;f=misc/sys/cdefs.h;h=88bc7ac94209ca7742b13c23dfe25f45aa9c5a54;hb=HEAD#l303 (в общем это пока что неважно)

Данная статья или раздел ещё не завершены
Кто-то посчитал, что статья или раздел ниже не содержит какой-то важной информации или имеет проблемы с вёрсткой/текстом. Указана причина: «размеры типов, пример кода etc»{{#ifeq: {{{1}}} | nopoint | | . }}

{{#if: Доработка | }}

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

<syntaxhighlight lang=»c»>

  1. include <stdint.h>
  2. include <inttypes.h>

int main ()
{

 …
 return 0;

}
</syntaxhighlight>

Данная статья или раздел ещё не завершены
Кто-то посчитал, что статья или раздел ниже не содержит какой-то важной информации или имеет проблемы с вёрсткой/текстом. Указана причина: безблагодатность{{#ifeq: {{{1}}} | nopoint | | . }}

{{#if: Доработка | }}

struct, union, bit field, memset, memcpy, type punning, strict aliasing rule

argv, argc, envp, getenv, getopt

Некоторые алгоритмы и структуры данных

switch, case, goto, область видимости (scope), рекурсия, приоритеты операторов в Си

Краткий обзор стандартной библиотеки Си, полезные ссылки

malloc, realloc, free, mmap, mremap

Пишем простейший стековый калькулятор (обратная польская нотация)

shared memory, SysV IPC, shared memory, mmap, отображение файла в память процесса, mremap, mprotect, fork, clone, mutex etc…

Сокеты, tcp, udp, raw socket, select, poll, epoll

Привилегии процесса, вызовы getrlimit(2), setrlimit(2), setuid(2), seteuid(2)…

Данная статья или раздел ещё не завершены
Кто-то посчитал, что статья или раздел ниже не содержит какой-то важной информации или имеет проблемы с вёрсткой/текстом. Указана причина: SUPER MEGA TODO

  • «рассказать про GNU as поподробнее. Разобрать структуру ELF файла(разделения на сегменты, адресное пространство процесса (/proc/pid/maps) ), дать описания этим .align .globl и проч. Дать описания инструкциям, которые тут встречаюься (push pop mov xor add ret call), регистров и их назначение»
  • «включить(перевести) материал из https://www3.nd.edu/~dthain/courses/cse40243/fall2015/intel-intro.html https://beginners.re/ лицензированный под CC BY-SA 4.0. Следует также рассмотреть возможность адаптации материалов из https://en.wikibooks.org/wiki/X86_Disassembly для включения их сюда»
  • «написать про процесс компиляции вообще, что компилятор создает .s файл, потом вызывается as, потом происходит линковка»
  • «добавить пояснений по поводу 0.3 хелловорда»
  • «дописать про линковку, Linker Scripts, objdump объектных файлов»
  • «jmp, control flow инструкции (je jne CMOVcc), флаги https://en.wikipedia.org/wiki/FLAGS_register , циклы, вызов си из ассемблера и ассемблера из си»
  • «разобрать системные вызовы, написать хелловорд и разобрать его работу»
  • …and moar todo!!!!111111 (пустые разделы выше){{#ifeq: {{{1}}} | nopoint | | . }}

{{#if: Доработка | }}

Архитектура Intel x86-64

Последнее обновление: 01.07.2023

Архитектура процессоров Intel x86-64 является на сегодняшний день доминирующей архитектурой для различного рода устройств — настольных компьютеров, ноутбуков, серверов.
Семейство процессоров Intel обычно классифицируется как машина с архитектурой фон Неймана — такая машина, которая содержит три основных компонента: центральный процессор (ЦП),
память и устройства ввода/вывода (I/0). Эти три компонента связаны между собой через системную шину (состоит из шины адреса, данных и управления).
Процессор взаимодействует с памятью и устройствами ввода-вывода, передавая через адресную шину числовой адрес участка памяти или порта
устройства ввода-вывода. Через шину данных процессор, память и устройства ввода-вывода обмениваются между собой данными. Через шину управления (control bus) передаются сигналы,
которые определяют направление передачи данных (в или из памяти, а также в или из устройства ввода-вывода).

Зачем изучать ассемблер в эпоху высокоуровневых языков? Ассемблер помогает лучше понять архитектуру компьютера. Знание ассемблера может помочь при
реверс-инжениринге, анализе вирусов и прочих вредоносных программ, а также при их создании и поиске уязвимостей. В конце концов понимание работы ассемблера является важным навыком в
низкоуровневом программировании, например, при написании операционных систем и драйверов.

Архитектура x86

Архитектура x86 обозначает большое семейство процессоров как с 16-битной, так и с 32-битной архитектурой набора команд. История x86 началась
с выходом процессора Intel 8086 в 1978 году. В 1979 году выходит функционально похожий на 8086 процессор Intel 8088.
Последующие поколения этой серии процессоров получили названия 80186, 80286, 80386 и 80486, что привело к возникновению термина «x86» как сокращению для семьи процессоров. В последствии
процессоры и серии процессоров Intel, которые представляли эту архитектуру, имели совершенно другие имена, например, серии Pentium, Celeron и т.д., но они принадлежали также к этой архитектуре. Кроме компании Intel
процессоры на архитектуре x86 также выпускала компания AMD, в частности, это серии процессоров Athlon, Duron и т.д.

Процессоры 8086 и 8088 были 16-битными, несмотря на 8-битную шину данных в 8088. Регистры в этих процессорах имели разрядность 16 бит, а набор инструкций работал
с 16-битными данными. 8086 и 8088 не поддерживали многие функции современных процессоров, например, виртуальную память и уровни защиты.
Эти процессоры имели 20 адресных линий, что ограничивало размер используемой память 1 мегабайтом. Но 20-битный адрес не мог поместиться в 16-битный регистр,
поэтому для работы с адресами необходимо было использовать несколько сложную систему сегментных регистров и смещений для доступа к полному адресному пространству размером 1 МБ.

В 1985 году компания Intel выпустила процессор 80386, который был важным шагом вперед в развитии архитектуры x86. Этот процессор был
32-битным. И адреса, регистры и АЛУ также имели разрядность в 32 бита, а инструкции изначально работали с операндами размером до 32 бит.
Кроме того, он использовал
защищенный режим (protected mode), в котором был реализан многоуровневый механизм привилегий из трех уровней — от 0 до 3. Уровень 0 представлял уровень с максимальными правами и предназначался
для ядра операционной системы, тогда как уровень 3 предназначался для прикладных пользовательских программ. Уровни 1 и 2 — промежуточные. Стоит отметить, что операционные системы Windows и Linux
до сих пор реализуют только 2 уровня — 0 и 3. 80386 поддерживал память размером 4 ГБ, в которой адреса были 32-битными, а манипуляции с сегментными регистрами и смещениями больше не требовались.
Кроме того, была добавлена поддержка выгружаемой виртуальной памяти.

После этого процессоры данной архитектуры стали 32-битными.

Архитектура x86 имеет прямой порядок следования байтов (little-endian) что означает, что многобайтовые значения хранятся в памяти с младшим значащим байтом по младшему адресу и старшим значащим байтом по старшему адресу.

Архитектура х64

Архитектура х64 изначально представляла расширение процессора x86 и его набора инструкций до 64 бит. Первая специафикация этой архитектуры назвалась
AMD64 и была представлена компанией AMD в 2000 году. Первый процессор AMD64, Opteron, был выпущен в 2003 году.

Компания Intel паралелльно развивала собственную 64-разрядную архитектуру, которая называлась IA-64 и которая была несовместима с х86. Результатом развития этой архитектуры стал процессор Itanium, который вышел в 2001 году. Однако затем
Intel решили пойти по пути AMD и также стали развивать 64-разрядную архитектуру как расширение для x86 и которая была бы совместима с AMD64, получившую название Intel 64.
Первым процессором Intel на 64-разрядной архитектуре — Xeon вышел в 2004 году.
В конечном счете эта архитектура стала называться x86-64, отражая эволюцию x86 до 64 бит, и, как правило, для ее названия употребляется сокращение x64.

Стоит отметить, что первая версия операционной системы Linux, которая поддерживала архитектуру x64, была выпущена в 2001 году, задолго до появления первых процессоров x64. ОС
Windows начала поддерживать архитектуру x64 в 2005 году.

Процессоры, которые реализуют архитектуры AMD64 и Intel 64, в значительной степени совместимы на уровне набора инструкций программ пользовательского режима.
Между архитектурами есть несколько различий. Как правило, компиляторы операционных систем и языков программирования управляют этими различиями, что делает их редкой проблемой для
разработчиков прикладного программного обеспечения. Разработчики же системного программного обеспечения ядра, драйверов и ассемблерного кода должны учитывать эти различия.

Основные особенности архитектуры x64:

  • x64 — это совместимое 64-битное расширение 32-битной архитектуры x86, и большинство программ, особенно прикладных приложений, написанных для 32-битной среды, должны выполняться без изменений на
    64-битном процессоре.

  • Восемь 32-битных регистров общего назначения x86 расширены до 64 бит в процессорах x64. Префикс имени регистра R указывает на 64-битные регистры. Например, в x64
    расширенный регистр x86 EAX называется RAX. Подкомпоненты регистра x86 EAX, AX, AH и AL по-прежнему доступны в x64.

  • Архитектура x64 реализует практически тот же набор инструкций, что и x86. При работе в 64-битном режиме архитектура x64 по умолчанию размер
    адреса — 64 бита, а размер операнда — 32 бита.

  • Указатель инструкций, RIP, теперь 64-битный. Регистр флагов, RFLAGS, также расширяется до 64 бит, хотя старшие 32 бита зарезервированы.
    Младшие 32 бита RFLAGS аналогичны EFLAGS в архитектуре x86.

  • Добавлено восемь 64-битных регистров общего назначения с именами от R8 до R15.

  • Добавлена встроенная поддержка для 64-битных целых чисел.

  • Процессоры x64 сохраняют возможность работы в режиме совместимости с x86. Этот режим позволяет использовать 32-разрядные операционные системы и
    позволяет любому приложению, созданному для x86, работать на процессорах x64. В 32-битном режиме совместимости 64-битные расширения недоступны.

  • Виртуальные адреса в архитектуре x64 имеют ширину 64 бита, теоретически поддерживая адресное пространство размером 16 экзабайт (EB), что эквивалентно 264 байтам.
    Однако современные процессоры AMD и Intel поддерживают только 48-битное виртуальное адресное пространство. Это ограничение снижает аппаратную сложность процессора,
    но при этом размер поддерживаемой памяти снижается до 256 терабайт виртуального адресного пространства. Процессоры текущего поколения также поддерживают максимум
    48 бит физического адресного пространства. Это теоретически позволяет процессору адресовать 256 ТБ физической оперативной памяти, но
    современные материнские платы не поддерживают такие размеры DRAM.

Типы ассемблеров

При работе следует определиться с ассемблером, который будет использоваться для сборки программ. Рассмотрим наиболее популярные ассемблеры.

Microsoft Macro Assembler (MASM)

Ассемблер Microsoft Macro Assembler или сокращенно MASM является одним из старейших развиваемых ассемблеров (первая версия вышла аж в 1981 году).
Его развивает компания Microsoft. MASM доступен в рамках такого инструмента для разработки приложений, как Visual Studio.
Преимуществом MASM является то, что MASM использует для своих инструкций синтаксис Intel. Недостатком MASM является наличие официальной поддержки только для ОС Windows.

Стоит отметить, что также существует неофициальный сайт, посвященный MASM, где можно найти дополнительную информацию по данному ассемблеру —
https://www.masm32.com/

GNU Assembler (GAS)

Ассемблер GNU Assembler или сокращенно GAS поставляется как компонент набора компиляторов GCC.
Поскольку компиляторы GCC довольно распространенны и являются кроссплатформенными, то GAC соответственно также можно использовать на разных платформах. Из недостатков
можно отметить, что GAS использует синтаксис, отличный от синтаксиса Intel (а именно синтаксис AT&T). Хотя последние версии GCC включают параметр «-masm»,
который при значении «-masm=intel» позволяет встраивать код ассемблера с использованием синтаксиса Intel. Эквивалентным параметром для GAS является «-msyntax=intel» или использование директивы «.intel_syntax».

Netwide Assembler (NASM)

Netwide Assembler или NASM развивается как opensource-проект и использует синтаксис, который похож на синтаксис Intel.
Является кросс-платформенным и работает почти на любой платформе. Официальный сайт проекта —
https://www.nasm.us/

Flat Assembler (FASM)

Flat Assembler (FASM) является кросс-платформенным и поддерживает основные ОС (Linux, Windows и MacOS). Тоже развивается как проект open source. Используемый синтаксис похож
на NASM. Примечателен тем, что написан на самом FASMe и имеет специальную небольшую IDE для написания программ. Официальный сайт проекта —
https://flatassembler.net/

YASM

YASM — это полностью переработанный ассемблер NASM под «новой» лицензией BSD. YASM позволяет использовать синтаксисы NASN и GAS.
Как и NASM, является кросс-платформенным. Официальный сайт —
https://yasm.tortall.net/

Сегодня мы поговорим о программировании на ассемблере. Вопрос «зачем кому-то в третьем тысячелетии может прийти в голову писать что-то на ассемблере» раскрыт в заметке Зачем нужно знать всякие низкоуровневые вещи, поэтому здесь мы к нему возвращаться не будем. Отмечу, что в рамках поста мы сосредоточимся на вопросе компиляции и отладки программ на ассемблере. Сам же язык ассемблера заслуживает отдельного большого поста, а то и серии постов.

Если вы знаете ассемблер, то любая программа для вас — open source.

Народная мудрость.

Введение

Существует два широко используемых ассемблерных синтаксиса — так называемые AT&T-синтаксис и Intel-синтаксис. Они не сильно друг от друга отличаются и легко переводятся один в другой. В мире Windows принято использовать синтаксис Intel. В мире *nix систем, наоборот, практически всегда используется синтаксис AT&T, а синтаксис Intel встречается крайне редко (например, он используется в утилите perf). Поскольку Windows, как известно, не существует, далее мы сосредоточимся на правильном AT&T-синтаксисе :)

Компиляторов ассемблера существует много. Мы будем использовать GNU Assembler (он же GAS, он же /usr/bin/as). Скорее всего, он уже есть вашей системе. К тому же, если вы пользуетесь GCC и собираетесь писать ассемблерные вставки в коде на C, то именно с этим ассемблером вам предстоит работать. Из достойных альтернатив GAS можно отметить NASM и FASM.

Наконец, язык ассемблера отличается в зависимости от архитектуры процессора. Пока что мы сосредоточимся на ассемблере для x86 (он же i386) и x64 (он же amd64), так как именно с этими архитектурами приходится чаще всего иметь дело. Впрочем, ARM тоже весьма распространен, главным образом на телефонах и планшетах. Еще из сравнительно популярного есть SPARC и PowerPC, но шансы столкнуться с ними весьма малы. Отмечу, что x86 и x64 можно было бы рассматривать отдельно, но эти архитектуры во многом похожи, поэтому я не вижу в этом большого смысла.

«Hello, world» на int 0x80

Рассмотрим типичный «Hello, world» для архитектуры x86 и Linux:

.data
msg:
  .ascii «Hello, world!\n»
  .set len, . msg

.text

.globl _start
_start:
  # write
  mov $4,   %eax
  mov $1,   %ebx
  mov $msg, %ecx
  mov $len, %edx
  int $0x80

  # exit
  mov $1,   %eax
  xor %ebx, %ebx
  int $0x80

Компиляция:

# Или: gcc -m32 -c hello-int80.s
as —32 hello-int80.s -o hello-int80.o
ld -melf_i386 -s hello-int80.o -o hello-int80

Коротко рассмотрим первые несколько действий, выполняемых программой: (1) программа начинает выполнение с метки _start, (2) в регистр eax кладется значение 4, (3) в регистр ebx помещается значение 1, (4) в регистр ecx кладется адрес строки, (5) в регистр edx кладется ее длина, (6) происходит прерывание 0x80. Так в мире Linux традиционно происходит выполнение системных вызовов. Конкретно int 0x80 считается устаревшим и медленным, но из соображений обратной совместимости он все еще работает. Далее мы рассмотрим и более новые механизмы.

Нетрудно догадаться, что eax — это номер системного вызова, а ebx, ecx и edx — его аргументы. Какой системный вызов имеет какой номер можно подсмотреть в файлах:

# для x86
/usr/include/x86_64-linux-gnu/asm/unistd_32.h
# для x64
/usr/include/x86_64-linux-gnu/asm/unistd_64.h

Следующая строчка из файла unistd_32.h:

… как бы намекает нам, что производится вызов write. В свою очередь, из man 2 write мы можем узнать, какие аргументы этот системный вызов принимает:

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

То есть, рассмотренный код эквивалентен:

// напомню, что stdout == 1
write(stdout, «Hello, world!\n«, 14)

Затем аналогичным образом производится вызов:

// команда `xor %ebx, %ebx` обнуляет регистр %ebx
exit(0)

Совсем не сложно!

В общем случае системный вызов через 0x80 производится по следующим правилам. Регистру eax присваивается номер системного вызова из unistd_32.h. До шести аргументов помещаются в регистры ebx, ecx, edx, esi, edi и ebp. Возвращаемое значение помещается в регистр eax. Значения остальных регистров при возвращении из системного вызова остаются прежними.

Выполнение системного вызова через sysenter

Начиная с i586 появилась инструкция sysenter, специально предназначенная (чего нельзя сказать об инструкции int) для выполнения системных вызовов.

Рассмотрим пример использования ее на Linux:

.data
msg:
  .ascii «Hello, world!\n»
  len = . msg

.text
.globl _start

_start:
  # write
  mov   $4,   %eax
  mov   $1,   %ebx
  mov   $msg, %ecx
  mov   $len, %edx
  push  $write_ret
  push  %ecx
  push  %edx
  push  %ebp
  mov   %esp, %ebp
  sysenter

write_ret:
  # exit
  mov   $1,   %eax
  xor   %ebx, %ebx
  push  $exit_ret
  push  %ecx
  push  %edx
  push  %ebp
  mov   %esp, %ebp
  sysenter

exit_ret:

Сборка осуществляется аналогично сборке предыдущего примера.

Как видите, принцип тот же, что при использовании int 0x80, только перед выполнением sysenter требуются поместить в стек адрес, по которому следует вернуть управление, а также совершить кое-какие дополнительные манипуляции с регистрами. Причины этого более подробно объясняются здесь.

Инструкция sysenter работает быстрее int 0x80 и является предпочтительным способом совершения системных вызовов на x86.

Выполнение системного вызова через syscall

До сих пор речь шла о 32-х битных программах. На x64 выполнение системных вызовов осуществляется так:

.data
msg:
  .ascii «Hello, world!\n»
  .set len, . msg

.text

.globl _start
_start:
  # write
  mov  $1,   %rax
  mov  $1,   %rdi
  mov  $msg, %rsi
  mov  $len, %rdx
  syscall

  # exit
  mov  $60, %rax
  xor  %rdi, %rdi
  syscall

Собирается программа таким образом:

as —64 hello-syscall.s -o hello-syscall.o
ld -melf_x86_64 -s hello-syscall.o -o hello-syscall

Принцип все тот же, но есть важные отличия. Номера системных вызовов нужно брать из unistd_64.h, а не из unistd_32.h. Как видите, они совершенно другие. Так как это 64-х битный код, то и регистры мы используем 64-х битные. Номер системного вызова помещается в rax. До шести аргументов передается через регистры rdi, rsi, rdx, r10, r8 и r9. Возвращаемое значение помещается в регистр rax. Значения, сохраненные в остальных регистрах, при возвращении из системного вызова остаются прежними, за исключением регистров rcx и r11.

Интересно, что в программе под x64 можно одновременно использовать системные вызовы как через syscall, так и через int 0x80.

Отладка ассемблерного кода в GDB

Статья была бы не полной, если бы мы не затронули вопрос отладки всего этого хозяйства. Так как мы все равно очень плотно сидим на GNU-стэке, в качестве отладчика воспользуемся GDB. По большому счету, отладка не сильно отличается от отладки обычного кода на C, но есть нюансы.

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

Увидим что-то вроде:

[…]
    Entry point: 0x4000b0
[…]

Далее говорим:

Какого-либо исходного кода у нас тоже нет, поэтому команда l работать не будет. Сами ассемблерные инструкции и есть исходный код! Так, например, можно посмотреть следующие 5 ассемблерных инструкций:

По понятным причинам, переход к очередной строчке кода при помощи команд n или s работать не будет. Вместо этих команд следует использовать команды перехода к следующей инструкции — ni, si, и так далее.

Смотреть и изменять значения переменных мы тоже не можем. Однако ничто не мешает смотреть и изменять значения регистров:

info registers
p/x $rcx
p $xmm1
set $r15 = 0x123

Наконец, стектрейсы нам тоже недоступны. Но ничто не мешает, например, посмотреть 8 ближайших значений на стеке:

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

gcc -S test.c -o
objdump -d ./myprog

Как альтернативный вариант, можно воспользоваться Hopper или подобным интерактивным дизассемблером.

Внезапно отладка программы, собранной без -g и/или с -O2, перестала казаться таким уж страшным делом, не так ли?

Заключение

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

Примите во внимание, что в Linux есть еще как минимум два способа сделать системный вызов — через так называемые vsyscall (считается устаревшим, но поддерживается для обратной совместимости) и VDSO (пришедший ему на замену). Эти способы основаны на отображении страницы ядра в адресное пространство процесса и призваны ускорить выполнение системных вызовов, не требующих проверки привилегий и других тяжелых действий со стороны ядра системы. В качестве примера вызова, который может быть ускорен таким образом, можно привести gettimeofday. К сожалению, рассмотрение vsyscall и VDSO выходит за рамки данного поста. Больше информации о них вы найдете по приведенным ниже ссылкам.

Ссылки по теме:

  • Хорошее объяснение vsyscall и VDSO;
  • Объяснение ELF Auxiliary Vectors;
  • Книга «Learning Linux Binary Analysis»;

Кроме того, вас могут заинтересовать статьи, посвященные ассемблеру, в замечательных блогах alexanius-blog.blogspot.ru и 0xax.blogspot.ru.

Дополнение: Шпаргалка по основным инструкциям ассемблера x86/x64

Метки: Linux, Ассемблер.

x86 assembly language is a family of low-level programming languages that are used to produce object code for the x86 class of processors. These languages provide backward compatibility with CPUs dating back to the Intel 8008 microprocessor, introduced in April 1972.[1][2] As assembly languages, they are closely tied to the architecture’s machine code instructions, allowing for precise control over hardware.

In x86 assembly languages, mnemonics are used to represent fundamental CPU instructions, making the code more human-readable compared to raw machine code.[3] Each mnemonics corresponds to a basic operation performed by the processor, such as arithmetic calculations, data movement, or control flow decisions. Assembly languages are most commonly used in applications where performance and efficiency are critical. This includes real-time embedded systems, operating-system kernels, and device drivers, all of which may require direct manipulation of hardware resources.

Additionally, compilers for high-level programming languages sometimes generate assembly code as an intermediate step during the compilation process. This allows for optimization at the assembly level before producing the final machine code that the processor executes.

Reserved keywords of x86 assembly language[4][5]

  • aaa
  • aad
  • aam
  • aas
  • adc
  • add
  • and
  • arpl
  • bound
  • bsf
  • bsr
  • bswap
  • bt
  • btc
  • btr
  • bts
  • call
  • cbtw
  • clc
  • cld
  • cli
  • cltd
  • clts
  • cmc
  • cmp
  • cmps
  • cmpxchg
  • cwtd
  • cwtl
  • daa
  • das
  • dec
  • div
  • enter
  • f2xm1
  • fabs
  • fadd
  • faddp
  • fbld
  • fbstp
  • fchs
  • fclex
  • fcom
  • fcomp
  • fcompp
  • fcos
  • fdecstp
  • fdiv
  • fdivp
  • fdivr
  • fdivrp
  • ffree
  • fiadd
  • ficom
  • ficomp
  • fidiv
  • fidivr
  • fild
  • fimul
  • fincstp
  • finit
  • fist
  • fistp
  • fisubr
  • fisubrp
  • fld
  • fld
  • fldcw
  • fldenv
  • fldl2e
  • fldl2t
  • fldlg2
  • fldln2
  • fldpi
  • fldz
  • fmul
  • fmulp
  • fnclex
  • fnint
  • fnop
  • fnsave
  • fnstenv
  • fnstew
  • fnstsw
  • fpatan
  • fprem
  • fprem
  • fptan
  • frndint
  • frstor
  • fsave
  • fscale
  • fsin
  • fsincos
  • fsqrt
  • fst
  • fstenv
  • fstew
  • fstp
  • fstsw
  • fsub
  • fsubp
  • fsubr
  • fsubrp
  • ftst
  • fucom
  • fucomp
  • fucompp
  • fwait
  • fxam
  • fxch
  • fxtract
  • fyl2x
  • fyl2xp1
  • hlt
  • idiv
  • imul
  • in
  • inc
  • ins
  • int
  • into
  • invd
  • invlpg
  • iret
  • jcxz
  • jmp
  • lahf
  • lar
  • lcall
  • ldx
  • lea
  • leave
  • les
  • lfs
  • lgdt
  • lgs
  • lidt
  • ljmp
  • lldt
  • lmsw
  • lock
  • lods
  • loop
  • loopnz
  • loopz
  • lret
  • lsl
  • lss
  • ltr
  • mov
  • movs
  • movsx
  • movw
  • movzb
  • mul
  • neg
  • nop
  • not
  • or
  • out
  • outs
  • pop
  • popa
  • popf
  • push
  • pusha
  • pushf
  • rcl
  • rcr
  • rep
  • repnz
  • repz
  • ret
  • rol
  • ror
  • sahf
  • sal
  • sar
  • sbb
  • scas
  • setcc
  • sgdt
  • shl
  • shld
  • shr
  • shrd
  • sidt
  • sldt
  • smsw
  • stc
  • std
  • sti
  • stos
  • str
  • sub
  • test
  • verr
  • verw
  • wait
  • wbinvd
  • xadd
  • xchg
  • xlat
  • xor

Mnemonics and opcodes

[edit]

Each instruction in the x86 assembly language is represented by a mnemonic which often combines with one or more operands to translate into one or more bytes known as an opcode. For example, the NOP instruction translates to the opcode 0x90, and the HLT instruction translates to 0xF4.[3] There are potential opcodes without documented mnemonics, which different processors may interpret differently. Using such opcodes can cause a program to behave inconsistently or even generate exceptions on some processors.

x86 assembly language has two primary syntax branches: Intel syntax and AT&T syntax.[6] Intel syntax is dominant in the DOS and Windows environments, while AT&T syntax is dominant in Unix-like systems, as Unix was originally developed at AT&T Bell Labs.[7] Below is a summary of the main differences between Intel syntax and AT&T syntax:

AT&T Intel
Parameter order
movl $5, %eax

Source before the destination.

mov eax, 5

Destination before source.

Parameter size
addl $0x24, %esp
movslq %ecx, %rax
paddd %xmm1, %xmm2

Mnemonics are suffixed with a letter indicating the size of the operands: q for qword (64 bits), l for long (dword, 32 bits), w for word (16 bits), and b for byte (8 bits).[6]

add esp, 24h
movsxd rax, ecx
paddd xmm2, xmm1

Derived from the name of the register that is used (e.g. rax, eax, ax, al imply q, l, w, b, respectively).

Width-based names may still appear in instructions when they define a different operation.

  • MOVSXD refers to sign extension with dword input, unlike MOVSX.
  • SIMD registers have width-named instructions that determine how to split up the register. AT&T tends to keep the names unchanged, so PADDD is not renamed to «paddl».
Sigils Immediate values prefixed with a «$», registers prefixed with a «%».[6] The assembler automatically detects the type of symbols; i.e., whether they are registers, constants or something else.
Effective addresses
movl offset(%ebx,%ecx,4), %eax

General syntax of DISP(BASE,INDEX,SCALE).

mov eax, [ebx + ecx*4 + offset]

Arithmetic expressions in square brackets; additionally, size keywords like byte, word, or dword have to be used if the size cannot be determined from the operands.[6]

Many x86 assemblers use Intel syntax, including FASM, MASM, NASM, TASM, and YASM. The GNU Assembler, which originally used AT&T syntax, has supported both syntaxes since version 2.10 via the .intel_syntax directive.[6][8][9] A quirk in the AT&T syntax for x86 is that x87 floating-point operands are reversed, an inherited bug from the original AT&T assembler.[10]

The AT&T syntax is nearly universal across other architectures (retaining the same operand order for the mov instruction); it was originally designed for PDP-11 assembly. In contrast, the Intel syntax is specific to the x86 architecture and is the one used in the x86 platform’s official documentation. The Intel 8080, which predates the x86 architecture, also uses the «destination-first» order for mov instruction.[11]

x86 processors feature a set of registers that serve as storage for binary data and addresses during program execution. These registers are categorized into general-purpose registers, segment registers, the instruction pointer, the FLAGS register, and various extension registers introduced in later processor models. Each register has specific functions in addition to their general capabilities:[3]

General-purpose registers

[edit]

  • AX (Accumulator register): Primarily used in arithmetic, logic, and data transfer operations. It is favored by instructions that perform multiplication and division, and by string load and store operations.
  • BX (Base register): Base pointer for memory access. It can hold the base address of data structures and is useful in indexed addressing modes, particularly with the MOV instruction.
  • CX (Count register): Serves as a counter in loop, string, and shift/rotate instructions. Iterative operations often use CX to determine the number of times a loop or operation should execute.
  • DX (Data register): Used in conjuction with AX for multiplication and division operations that produce results larger than 16 bits. It also holds I/O port addresses in IN and OUT instructions.
  • SP (Stack pointer): Points to the top of stack in memory. It is automatically updated during PUSH and POP operations.
  • BP (Base Pointer): Points to the top of the call stack. It is primarily used to access function parameters and local variables within the call stack.
  • SI (Source Index): Used as a pointer to the source in string and memory array operations. Instructions like MOVS (move string) use SI to read data from memory.
  • DI (Destination Index): Serves as a pointer to the destination in string and memory array operations. It works alongside SI in instructions that copy or compare data, writing results to memory.

Along with the general registers there are additionally the:

  • Instruction Pointer (IP): Holds the offset address of the next instruction to be executed within the code segment (CS). It points to the first byte of the next instruction. While the IP register cannot be accessed directly by programmers, its value changes through control flow instructions such as jumps, calls, and interrupts, which alter the flow of execution.
  • FLAGS register: Contains a set of status, control, and system flags that reflect the outcome of operations and control the processor’s operations.
  • Segment registers (CS, DS, ES, FS, GS, SS): Determine where a 64k segment starts (no FS & GS in 80286 & earlier)
  • Extra extension registers (MMX, 3DNow!, SSE, etc.) (Pentium & later only).

The IP register points to the memory offset of the next instruction in the code segment (it points to the first byte of the instruction). The IP register cannot be accessed by the programmer directly.

The x86 registers can be used by using the MOV instructions. For example, in Intel syntax:

mov ax, 1234h ; copies the value 1234hex (4660d) into register AX
mov bx, ax    ; copies the value of the AX register into the BX register

Segmented addressing

[edit]

The x86 architecture in real and virtual 8086 mode uses a process known as segmentation to address memory, not the flat memory model used in many other environments. Segmentation involves composing a memory address from two parts, a segment and an offset; the segment points to the beginning of a 64 KiB (64×210) group of addresses and the offset determines how far from this beginning address the desired address is. In segmented addressing, two registers are required for a complete memory address. One to hold the segment, the other to hold the offset. In order to translate back into a flat address, the segment value is shifted four bits left (equivalent to multiplication by 24 or 16) then added to the offset to form the full address, which allows breaking the 64k barrier through clever choice of addresses, though it makes programming considerably more complex.

In real mode/protected only, for example, if DS contains the hexadecimal number 0xDEAD and DX contains the number 0xCAFE they would together point to the memory address 0xDEAD * 0x10 + 0xCAFE == 0xEB5CE. Therefore, the CPU can address up to 1,048,576 bytes (1 MB) in real mode. By combining segment and offset values we find a 20-bit address.

The original IBM PC restricted programs to 640 KB but an expanded memory specification was used to implement a bank switching scheme that fell out of use when later operating systems, such as Windows, used the larger address ranges of newer processors and implemented their own virtual memory schemes.

Protected mode, starting with the Intel 80286, was utilized by OS/2. Several shortcomings, such as the inability to access the BIOS and the inability to switch back to real mode without resetting the processor, prevented widespread usage.[12] The 80286 was also still limited to addressing memory in 16-bit segments, meaning only 216 bytes (64 kilobytes) could be accessed at a time.
To access the extended functionality of the 80286, the operating system would set the processor into protected mode, enabling 24-bit addressing and thus 224 bytes of memory (16 megabytes).

In protected mode, the segment selector can be broken down into three parts: a 13-bit index, a Table Indicator bit that determines whether the entry is in the GDT or LDT and a 2-bit Requested Privilege Level; see x86 memory segmentation.

When referring to an address with a segment and an offset the notation of segment:offset is used, so in the above example the flat address 0xEB5CE can be written as 0xDEAD:0xCAFE or as a segment and offset register pair; DS:DX.

There are some special combinations of segment registers and general registers that point to important addresses:

  • CS:IP (CS is Code Segment, IP is Instruction Pointer) points to the address where the processor will fetch the next byte of code.
  • SS:SP (SS is Stack Segment, SP is Stack Pointer) points to the address of the top of the stack, i.e. the most recently pushed byte.
  • SS:BP (SS is Stack Segment, BP is Stack Frame Pointer) points to the address of the top of the stack frame, i.e. the base of the data area in the call stack for the currently active subprogram.
  • DS:SI (DS is Data Segment, SI is Source Index) is often used to point to string data that is about to be copied to ES:DI.
  • ES:DI (ES is Extra Segment, DI is Destination Index) is typically used to point to the destination for a string copy, as mentioned above.

The Intel 80386 featured three operating modes: real mode, protected mode and virtual mode. The protected mode which debuted in the 80286 was extended to allow the 80386 to address up to 4 GB of memory, the all new virtual 8086 mode (VM86) made it possible to run one or more real mode programs in a protected environment which largely emulated real mode, though some programs were not compatible (typically as a result of memory addressing tricks or using unspecified op-codes).

The 32-bit flat memory model of the 80386’s extended protected mode may be the most important feature change for the x86 processor family until AMD released x86-64 in 2003, as it helped drive large scale adoption of Windows 3.1 (which relied on protected mode) since Windows could now run many applications at once, including DOS applications, by using virtual memory and simple multitasking.

The x86 processors support five modes of operation for x86 code, Real Mode, Protected Mode, Long Mode, Virtual 86 Mode, and System Management Mode, in which some instructions are available and others are not. A 16-bit subset of instructions is available on the 16-bit x86 processors, which are the 8086, 8088, 80186, 80188, and 80286. These instructions are available in real mode on all x86 processors, and in 16-bit protected mode (80286 onwards), additional instructions relating to protected mode are available. On the 80386 and later, 32-bit instructions (including later extensions) are also available in all modes, including real mode; on these CPUs, V86 mode and 32-bit protected mode are added, with additional instructions provided in these modes to manage their features. SMM, with some of its own special instructions, is available on some Intel i386SL, i486 and later CPUs. Finally, in long mode (AMD Opteron onwards), 64-bit instructions, and more registers, are also available. The instruction set is similar in each mode but memory addressing and word size vary, requiring different programming strategies.

The modes in which x86 code can be executed in are:

  • Real mode (16-bit)
    • 20-bit segmented memory address space (meaning that only 1 MB of memory can be addressed— actually since 80286 a little more through HMA), direct software access to peripheral hardware, and no concept of memory protection or multitasking at the hardware level. Computers that use BIOS start up in this mode.
  • Protected mode (16-bit and 32-bit)
    • Expands addressable physical memory to 16 MB and addressable virtual memory to 1 GB. Provides privilege levels and protected memory, which prevents programs from corrupting one another. 16-bit protected mode (used during the end of the DOS era) used a complex, multi-segmented memory model. 32-bit protected mode uses a simple, flat memory model.
  • Long mode (64-bit)
    • Mostly an extension of the 32-bit (protected mode) instruction set, but unlike the 16–to–32-bit transition, many instructions were dropped in the 64-bit mode. Pioneered by AMD.
  • Virtual 8086 mode (16-bit)
    • A special hybrid operating mode that allows real mode programs and operating systems to run while under the control of a protected mode supervisor operating system
  • System Management Mode (16-bit)
    • Handles system-wide functions like power management, system hardware control, and proprietary OEM designed code. It is intended for use only by system firmware. All normal execution, including the operating system, is suspended. An alternate software system (which usually resides in the computer’s firmware, or a hardware-assisted debugger) is then executed with high privileges.

The processor runs in real mode immediately after power on, so an operating system kernel, or other program, must explicitly switch to another mode if it wishes to run in anything but real mode. Switching modes is accomplished by modifying certain bits of the processor’s control registers after some preparation, and some additional setup may be required after the switch.

With a computer running legacy BIOS, the BIOS and the boot loader run in Real mode. The 64-bit operating system kernel checks and switches the CPU into Long mode and then starts new kernel-mode threads running 64-bit code.

With a computer running UEFI, the UEFI firmware (except CSM and legacy Option ROM), the UEFI boot loader and the UEFI operating system kernel all run in Long mode.

In general, the features of the modern x86 instruction set are:

  • A compact encoding
    • Variable length and alignment independent (encoded as little endian, as is all data in the x86 architecture)
    • Mainly one-address and two-address instructions, that is to say, the first operand is also the destination.
    • Memory operands as both source and destination are supported (frequently used to read/write stack elements addressed using small immediate offsets).
    • Both general and implicit register usage; although all seven (counting ebp) general registers in 32-bit mode, and all fifteen (counting rbp) general registers in 64-bit mode, can be freely used as accumulators or for addressing, most of them are also implicitly used by certain (more or less) special instructions; affected registers must therefore be temporarily preserved (normally stacked), if active during such instruction sequences.
  • Produces conditional flags implicitly through most integer ALU instructions.
  • Supports various addressing modes including immediate, offset, and scaled index but not PC-relative, except jumps (introduced as an improvement in the x86-64 architecture).
  • Includes floating point to a stack of registers.
  • Contains special support for atomic read-modify-write instructions (xchg, cmpxchg/cmpxchg8b, xadd, and integer instructions which combine with the lock prefix)
  • SIMD instructions (instructions which perform parallel simultaneous single instructions on many operands encoded in adjacent cells of wider registers).

The x86 architecture has hardware support for an execution stack mechanism. Instructions such as push, pop, call and ret are used with the properly set up stack to pass parameters, to allocate space for local data, and to save and restore call-return points. The ret size instruction is very useful for implementing space efficient (and fast) calling conventions where the callee is responsible for reclaiming stack space occupied by parameters.

When setting up a stack frame to hold local data of a recursive procedure there are several choices; the high level enter instruction (introduced with the 80186) takes a procedure-nesting-depth argument as well as a local size argument, and may be faster than more explicit manipulation of the registers (such as push bp ; mov bp, sp ; sub sp, size). Whether it is faster or slower depends on the particular x86-processor implementation as well as the calling convention used by the compiler, programmer or particular program code; most x86 code is intended to run on x86-processors from several manufacturers and on different technological generations of processors, which implies highly varying microarchitectures and microcode solutions as well as varying gate- and transistor-level design choices.

The full range of addressing modes (including immediate and base+offset) even for instructions such as push and pop, makes direct usage of the stack for integer, floating point and address data simple, as well as keeping the ABI specifications and mechanisms relatively simple compared to some RISC architectures (require more explicit call stack details).

Integer ALU instructions

[edit]

x86 assembly has the standard mathematical operations, add, sub, neg, imul and idiv (for signed integers), with mul and div (for unsigned integers); the logical operators and, or, xor, not; bitshift arithmetic and logical, sal/sar (for signed integers), shl/shr (for unsigned integers); rotate with and without carry, rcl/rcr, rol/ror, a complement of BCD arithmetic instructions, aaa, aad, daa and others.

Floating-point instructions

[edit]

x86 assembly language includes instructions for a stack-based floating-point unit (FPU). The FPU was an optional separate coprocessor for the 8086 through the 80386, it was an on-chip option for the 80486 series, and it is a standard feature in every Intel x86 CPU since the 80486, starting with the Pentium. The FPU instructions include addition, subtraction, negation, multiplication, division, remainder, square roots, integer truncation, fraction truncation, and scale by power of two. The operations also include conversion instructions, which can load or store a value from memory in any of the following formats: binary-coded decimal, 32-bit integer, 64-bit integer, 32-bit floating-point, 64-bit floating-point or 80-bit floating-point (upon loading, the value is converted to the currently used floating-point mode). x86 also includes a number of transcendental functions, including sine, cosine, tangent, arctangent, exponentiation with the base 2 and logarithms to bases 2, 10, or e.

The stack register to stack register format of the instructions is usually fop st, st(n) or fop st(n), st, where st is equivalent to st(0), and st(n) is one of the 8 stack registers (st(0), st(1), …, st(7)). Like the integers, the first operand is both the first source operand and the destination operand. fsubr and fdivr should be singled out as first swapping the source operands before performing the subtraction or division. The addition, subtraction, multiplication, division, store and comparison instructions include instruction modes that pop the top of the stack after their operation is complete. So, for example, faddp st(1), st performs the calculation st(1) = st(1) + st(0), then removes st(0) from the top of stack, thus making what was the result in st(1) the top of the stack in st(0).

Modern x86 CPUs contain SIMD instructions, which largely perform the same operation in parallel on many values encoded in a wide SIMD register. Various instruction technologies support different operations on different register sets, but taken as complete whole (from MMX to SSE4.2) they include general computations on integer or floating-point arithmetic (addition, subtraction, multiplication, shift, minimization, maximization, comparison, division or square root). So for example, paddw mm0, mm1 performs 4 parallel 16-bit (indicated by the w) integer adds (indicated by the padd) of mm0 values to mm1 and stores the result in mm0. Streaming SIMD Extensions or SSE also includes a floating-point mode in which only the very first value of the registers is actually modified (expanded in SSE2). Some other unusual instructions have been added including a sum of absolute differences (used for motion estimation in video compression, such as is done in MPEG) and a 16-bit multiply accumulation instruction (useful for software-based alpha-blending and digital filtering). SSE (since SSE3) and 3DNow! extensions include addition and subtraction instructions for treating paired floating-point values like complex numbers.

These instruction sets also include numerous fixed sub-word instructions for shuffling, inserting and extracting the values around within the registers. In addition there are instructions for moving data between the integer registers and XMM (used in SSE)/FPU (used in MMX) registers.

Memory instructions

[edit]

The x86 processor also includes complex addressing modes for addressing memory with an immediate offset, a register, a register with an offset, a scaled register with or without an offset, and a register with an optional offset and another scaled register. So for example, one can encode mov eax, [Table + ebx + esi*4] as a single instruction which loads 32 bits of data from the address computed as (Table + ebx + esi * 4) offset from the ds selector, and stores it to the eax register. In general x86 processors can load and use memory matched to the size of any register it is operating on. (The SIMD instructions also include half-load instructions.)

Most 2-operand x86 instructions, including integer ALU instructions,
use a standard «addressing mode byte»[13]
often called the MOD-REG-R/M byte.[14][15][16]
Many 32-bit x86 instructions also have a SIB addressing mode byte that follows the MOD-REG-R/M byte.[17][18][19][20][21]

In principle, because the instruction opcode is separate from the addressing mode byte, those instructions are orthogonal because any of those opcodes can be mixed-and-matched with any addressing mode.
However, the x86 instruction set is generally considered non-orthogonal because many other opcodes have some fixed addressing mode (they have no addressing mode byte), and every register is special.[21][22]

The x86 instruction set includes string load, store, move, scan and compare instructions (lods, stos, movs, scas and cmps) which perform each operation to a specified size (b for 8-bit byte, w for 16-bit word, d for 32-bit double word) then increments/decrements (depending on DF, direction flag) the implicit address register (si for lods, di for stos and scas, and both for movs and cmps). For the load, store and scan operations, the implicit target/source/comparison register is in the al, ax or eax register (depending on size). The implicit segment registers used are ds for si and es for di. The cx or ecx register is used as a decrementing counter, and the operation stops when the counter reaches zero or (for scans and comparisons) when inequality is detected. Unfortunately, over the years the performance of some of these instructions became neglected and in certain cases it is now possible to get faster results by writing out the algorithms yourself. Intel and AMD have refreshed some of the instructions though, and a few now have very respectable performance, so it is recommended that the programmer should read recent respected benchmark articles before choosing to use a particular instruction from this group.

The stack is a region of memory and an associated ‘stack pointer’, which points to the bottom of the stack. The stack pointer is decremented when items are added (‘push’) and incremented after things are removed (‘pop’). In 16-bit mode, this implicit stack pointer is addressed as SS:[SP], in 32-bit mode it is SS:[ESP], and in 64-bit mode it is [RSP]. The stack pointer actually points to the last value that was stored, under the assumption that its size will match the operating mode of the processor (i.e., 16, 32, or 64 bits) to match the default width of the push/pop/call/ret instructions. Also included are the instructions enter and leave which reserve and remove data from the top of the stack while setting up a stack frame pointer in bp/ebp/rbp. However, direct setting, or addition and subtraction to the sp/esp/rsp register is also supported, so the enter/leave instructions are generally unnecessary.

This code is the beginning of a function typical for a high-level language when compiler optimisation is turned off for ease of debugging:

 push    rbp       ; Save the calling function’s stack frame pointer (rbp register)
 mov     rbp, rsp  ; Make a new stack frame below our caller’s stack
 sub     rsp, 32   ; Reserve 32 bytes of stack space for this function’s local variables.
                   ; Local variables will be below rbp and can be referenced relative to rbp,
                   ; again best for ease of debugging, but for best performance rbp will not
                   ; be used at all, and local variables would be referenced relative to rsp
                   ; because, apart from the code saving, rbp then is free for other uses.
                 ; However, if rbp is altered here, its value should be preserved for the caller.
 mov [rbp-8], rdx  ; Example of writing to a local variable (by its memory location) from register rdx

…is functionally equivalent to just:

Other instructions for manipulating the stack include pushfd(32-bit) / pushfq(64-bit) and popfd/popfq for storing and retrieving the EFLAGS (32-bit) / RFLAGS (64-bit) register.

Values for a SIMD load or store are assumed to be packed in adjacent positions for the SIMD register and will align them in sequential little-endian order. Some SSE load and store instructions require 16-byte alignment to function properly. The SIMD instruction sets also include «prefetch» instructions which perform the load but do not target any register, used for cache loading. The SSE instruction sets also include non-temporal store instructions which will perform stores straight to memory without performing a cache allocate if the destination is not already cached (otherwise it will behave like a regular store.)

Most generic integer and floating-point (but no SIMD) instructions can use one parameter as a complex address as the second source parameter. Integer instructions can also accept one memory parameter as a destination operand.

The x86 assembly has an unconditional jump operation, jmp, which can take an immediate address, a register or an indirect address as a parameter (note that most RISC processors only support a link register or short immediate displacement for jumping).

Also supported are several conditional jumps, including jz (jump on zero), jnz (jump on non-zero), jg (jump on greater than, signed), jl (jump on less than, signed), ja (jump on above/greater than, unsigned), jb (jump on below/less than, unsigned). These conditional operations are based on the state of specific bits in the (E)FLAGS register. Many arithmetic and logic operations set, clear or complement these flags depending on their result. The comparison cmp (compare) and test instructions set the flags as if they had performed a subtraction or a bitwise AND operation, respectively, without altering the values of the operands. There are also instructions such as clc (clear carry flag) and cmc (complement carry flag) which work on the flags directly. Floating point comparisons are performed via fcom or ficom instructions which eventually have to be converted to integer flags.

Each jump operation has three different forms, depending on the size of the operand. A short jump uses an 8-bit signed operand, which is a relative offset from the current instruction. A near jump is similar to a short jump but uses a 16-bit signed operand (in real or protected mode) or a 32-bit signed operand (in 32-bit protected mode only). A far jump is one that uses the full segment base:offset value as an absolute address. There are also indirect and indexed forms of each of these.

In addition to the simple jump operations, there are the call (call a subroutine) and ret (return from subroutine) instructions. Before transferring control to the subroutine, call pushes the segment offset address of the instruction following the call onto the stack; ret pops this value off the stack, and jumps to it, effectively returning the flow of control to that part of the program. In the case of a far call, the segment base is pushed following the offset; far ret pops the offset and then the segment base to return.

There are also two similar instructions, int (interrupt), which saves the current (E)FLAGS register value on the stack, then performs a far call, except that instead of an address, it uses an interrupt vector, an index into a table of interrupt handler addresses. Typically, the interrupt handler saves all other CPU registers it uses, unless they are used to return the result of an operation to the calling program (in software called interrupts). The matching return from interrupt instruction is iret, which restores the flags after returning. Soft Interrupts of the type described above are used by some operating systems for system calls, and can also be used in debugging hard interrupt handlers. Hard interrupts are triggered by external hardware events, and must preserve all register values as the state of the currently executing program is unknown. In Protected Mode, interrupts may be set up by the OS to trigger a task switch, which will automatically save all registers of the active task.

The following examples use the so-called Intel-syntax flavor as used by the assemblers Microsoft MASM, NASM and many others. (Note: There is also an alternative AT&T-syntax flavor where the order of source and destination operands are swapped, among many other differences.)[23]

«Hello world!» program for MS-DOS in MASM-style assembly

[edit]

Using the software interrupt 21h instruction to call the MS-DOS operating system for output to the display – other samples use libc’s C printf() routine to write to stdout. Note that the first example, is a 30-year-old example using 16-bit mode as on an Intel 8086. The second example is Intel 386 code in 32-bit mode. Modern code will be in 64-bit mode.[24]

.model small
.stack 100h

.data
msg	db	'Hello world!$'

.code
start:
    mov ax, @DATA  ; Initializes Data segment
    mov ds, ax
	mov	ah, 09h    ; Sets 8-bit register ‘ah’, the high byte of register ax, to 9, to
                   ; select a sub-function number of an MS-DOS routine called below
                   ; via the software interrupt int 21h to display a message
	lea	dx, msg    ; Takes the address of msg, stores the address in 16-bit register dx
	int	21h        ; Various MS-DOS routines are callable by the software interrupt 21h
                   ; Our required sub-function was set in register ah above

	mov	ax, 4C00h  ; Sets register ax to the sub-function number for MS-DOS’s software
                   ; interrupt int 21h for the service ‘terminate program’.
	int	21h        ; Calling this MS-DOS service never returns, as it ends the program.

end start

«Hello world!» program for Windows in MASM style assembly

[edit]

; requires /coff switch on 6.15 and earlier versions
.386
.model small,c
.stack 1000h

.data
msg     db "Hello world!",0

.code
includelib libcmt.lib
includelib libvcruntime.lib
includelib libucrt.lib
includelib legacy_stdio_definitions.lib

extrn printf:near
extrn exit:near

public main
main proc
        push    offset msg
        call    printf
        push    0
        call    exit
main endp

end

«Hello world!» program for Windows in NASM style assembly

[edit]

; Image base = 0x00400000
%define RVA(x) (x-0x00400000)
section .text
push dword hello
call dword [printf]
push byte +0
call dword [exit]
ret

section .data
hello db "Hello world!"

section .idata
dd RVA(msvcrt_LookupTable)
dd -1
dd 0
dd RVA(msvcrt_string)
dd RVA(msvcrt_imports)
times 5 dd 0 ; ends the descriptor table

msvcrt_string dd "msvcrt.dll", 0
msvcrt_LookupTable:
dd RVA(msvcrt_printf)
dd RVA(msvcrt_exit)
dd 0

msvcrt_imports:
printf dd RVA(msvcrt_printf)
exit dd RVA(msvcrt_exit)
dd 0

msvcrt_printf:
dw 1
dw "printf", 0
msvcrt_exit:
dw 2
dw "exit", 0
dd 0

«Hello world!» program for Linux in its native AT&T style assembly

[edit]

.data                         ; section for initialized data
str: .ascii "Hello, world!\n" ; define a string of text containing "Hello, world!" and then a new line.
str_len = . - str             ; get the length of str by subtracting its address

.text                         ; section for program functions
.globl _start                 ; export the _start function so it can be run
_start:                       ; begin the _start function
    movl $4, %eax             ; specify the instruction to 'sys_write'
    movl $1, %ebx             ; specify the output to the standard output, 'stdout'
    movl $str, %ecx           ; specify the outputted text to our defined string
    movl $str_len, %edx       ; specify the character amount to write as the length of our defined string.
    int $0x80                 ; call a system interrupt to initiate the syscall we have created.

    movl $1, %eax             ; specify the instruction to 'sys_exit'
    movl $0, %ebx             ; specify the exit code to 0, meaning success
    int $0x80                 ; call another system interrup to end the program

«Hello world!» program for Linux in NASM style assembly

[edit]

;
; This program runs in 32-bit protected mode.
;  build: nasm -f elf -F stabs name.asm
;  link:  ld -o name name.o
;
; In 64-bit long mode you can use 64-bit registers (e.g. rax instead of eax, rbx instead of ebx, etc.)
; Also change "-f elf " for "-f elf64" in build command.
;
section .data                           ; section for initialized data
str:     db 'Hello world!', 0Ah         ; message string with new-line char at the end (10 decimal)
str_len: equ $ - str                    ; calcs length of string (bytes) by subtracting the str's start address
                                          ; from ‘here, this address’ (‘$’ symbol meaning ‘here’)

section .text                           ; this is the code section (program text) in memory 
global _start                           ; _start is the entry point and needs global scope to be 'seen' by the
                                        ; linker --equivalent to main() in C/C++
_start:                                 ; definition of _start procedure begins here
	mov	eax, 4                   ; specify the sys_write function code (from OS vector table)
	mov	ebx, 1                   ; specify file descriptor stdout --in gnu/linux, everything's treated as a file,
                                 ; even hardware devices
	mov	ecx, str                 ; move start _address_ of string message to ecx register
	mov	edx, str_len             ; move length of message (in bytes)
	int	80h                      ; interrupt kernel to perform the system call we just set up -
                                 ; in gnu/linux services are requested through the kernel
	mov	eax, 1                   ; specify sys_exit function code (from OS vector table)
	mov	ebx, 0                   ; specify return code for OS (zero tells OS everything went fine)
	int	80h                      ; interrupt kernel to perform system call (to exit)

For 64-bit long mode, «lea rcx, str» would be the address of the message, note 64-bit register rcx.

«Hello world!» program for Linux in NASM style assembly using the C standard library

[edit]

;
;  This program runs in 32-bit protected mode.
;  gcc links the standard-C library by default

;  build: nasm -f elf -F stabs name.asm
;  link:  gcc -o name name.o
;
; In 64-bit long mode you can use 64-bit registers (e.g. rax instead of eax, rbx instead of ebx, etc..)
; Also change "-f elf " for "-f elf64" in build command.
;
        global  main                            ; ‘main’ must be defined, as it being compiled
                                                ; against the C Standard Library
        extern  printf                          ; declares the use of external symbol, as printf
                                                ; printf is declared in a different object-module.
                                                ; The linker resolves this symbol later.

segment .data                                   ; section for initialized data
	string db 'Hello world!', 0Ah, 0            ; message string ending with a newline char (10
                                                ; decimal) and the zero byte ‘NUL’ terminator
                                                ; ‘string’ now refers to the starting address
                                                ; at which 'Hello, World' is stored.

segment .text
main:
        push    string                          ; Push the address of ‘string’ onto the stack.
                                                ; This reduces esp by 4 bytes before storing
                                                ; the 4-byte address ‘string’ into memory at
                                                ; the new esp, the new bottom of the stack.

                                                ; This will be an argument to printf()
        call    printf                          ; calls the C printf() function.
        add     esp, 4                          ; Increases the stack-pointer by 4 to put it back
                                                ; to where it was before the ‘push’, which
                                                ; reduced it by 4 bytes.
        ret                                     ; Return to our caller.

«Hello world!» program for 64-bit mode Linux in NASM style assembly

[edit]

This example is in modern 64-bit mode.

;  build: nasm -f elf64 -F dwarf hello.asm
;  link:  ld -o hello hello.o

DEFAULT REL			    ; use RIP-relative addressing modes by default, so [foo] = [rel foo]

SECTION .rodata			; read-only data should go in the .rodata section on GNU/Linux, like .rdata on Windows
Hello:		db "Hello world!", 10   ; Ending with a byte 10 = newline (ASCII LF)
len_Hello:	equ $-Hello             ; Get NASM to calculate the length as an assembly-time constant
                                    ; the ‘$’ symbol means ‘here’. write() takes a length so that
                                    ; a zero-terminated C-style string isn't needed.
                                    ; It would be for C puts()

SECTION .text

global _start
_start:
	mov eax, 1				; __NR_write syscall number from Linux asm/unistd_64.h (x86_64)
	mov edi, 1				; int fd = STDOUT_FILENO
	lea rsi, [rel Hello]			; x86-64 uses RIP-relative LEA to put static addresses into regs
	mov rdx, len_Hello		; size_t count = len_Hello
	syscall					; write(1, Hello, len_Hello);  call into the kernel to actually do the system call
     ;; return value in RAX.  RCX and R11 are also overwritten by syscall

	mov eax, 60				; __NR_exit call number (x86_64) is stored in register eax.
	xor edi, edi		    ; This zeros edi and also rdi.
                            ; This xor-self trick is the preferred common idiom for zeroing
                            ; a register, and is always by far the fastest method.
                            ; When a 32-bit value is stored into eg edx, the high bits 63:32 are
                            ; automatically zeroed too in every case. This saves you having to set
                            ; the bits with an extra instruction, as this is a case very commonly
                            ; needed, for an entire 64-bit register to be filled with a 32-bit value.
                            ; This sets our routine’s exit status = 0 (exit normally)
	syscall					; _exit(0)

Running it under strace verifies that no extra system calls are made in the process. The printf version would make many more system calls to initialize libc and do dynamic linking. But this is a static executable because we linked using ld without -pie or any shared libraries; the only instructions that run in user-space are the ones you provide.

$ strace ./hello > /dev/null                    # without a redirect, your program's stdout is mixed with strace's logging on stderr.  Which is normally fine
execve("./hello", ["./hello"], 0x7ffc8b0b3570 /* 51 vars */) = 0
write(1, "Hello world!\n", 13)          = 13
exit(0)                                 = ?
+++ exited with 0 +++

Using the flags register

[edit]

Flags are heavily used for comparisons in the x86 architecture. When a comparison is made between two data, the CPU sets the relevant flag or flags. Following this, conditional jump instructions can be used to check the flags and branch to code that should run, e.g.:

	cmp	eax, ebx
	jne	do_something
	; ...
do_something:
	; do something here

Aside, from compare instructions, there are a great many arithmetic and other instructions that set bits in the flags register. Other examples are the instructions sub, test and add and there are many more. Common combinations such as cmp + conditional jump are internally ‘fused’ (‘macro fusion’) into one single micro-instruction (μ-op) and are fast provided the processor can guess which way the conditional jump will go, jump vs continue.

The flags register are also used in the x86 architecture to turn on and off certain features or execution modes. For example, to disable all maskable interrupts, you can use the instruction:

The flags register can also be directly accessed. The low 8 bits of the flag register can be loaded into ah using the lahf instruction. The entire flags register can also be moved on and off the stack using the instructions pushfd/pushfq, popfd/popfq, int (including into) and iret.

The x87 floating point maths subsystem also has its own independent ‘flags’-type register the fp status word. In the 1990s it was an awkward and slow procedure to access the flag bits in this register, but on modern processors there are ‘compare two floating point values’ instructions that can be used with the normal conditional jump/branch instructions directly without any intervening steps.

Using the instruction pointer register

[edit]

The instruction pointer is called ip in 16-bit mode, eip in 32-bit mode, and rip in 64-bit mode. The instruction pointer register points to the address of the next instruction that the processor will attempt to execute. It cannot be directly accessed in 16-bit or 32-bit mode, but a sequence like the following can be written to put the address of next_line into eax (32-bit code):

	call	next_line
next_line:
	pop	eax

Writing to the instruction pointer is simple — a jmp instruction stores the given target address into the instruction pointer to, so, for example, a sequence like the following will put the contents of rax into rip (64-bit code):

In 64-bit mode, instructions can reference data relative to the instruction pointer, so there is less need to copy the value of the instruction pointer to another register.

  • Assembly language
  • X86 instruction listings
  • X86 architecture
  • CPU design
  • List of assemblers
  • Self-modifying code
  • DOS
  • DOS API
  1. ^ «Intel 8008 (i8008) microprocessor family». www.cpu-world.com. Retrieved 2021-03-25.
  2. ^ «Intel 8008». CPU MUSEUM — MUSEUM OF MICROPROCESSORS & DIE PHOTOGRAPHY. Retrieved 2021-03-25.
  3. ^ a b c «Intel 8008 OPCODES». www.pastraiser.com. Retrieved 2021-03-25.
  4. ^ «Assembler language reference». www.ibm.com. Retrieved 2022-11-28.
  5. ^ «x86 Assembly Language Reference Manual» (PDF).
  6. ^ a b c d e Narayam, Ram (2007-10-17). «Linux assemblers: A comparison of GAS and NASM». IBM. Archived from the original on October 3, 2013. Retrieved 2008-07-02.
  7. ^ «The Creation of Unix». Archived from the original on April 2, 2014.
  8. ^ Hyde, Randall. «Which Assembler is the Best?». Retrieved 2008-05-18.
  9. ^ «GNU Assembler News, v2.1 supports Intel syntax». 2008-04-04. Retrieved 2008-07-02.
  10. ^ «i386-Bugs (Using as)». Binutils documentation. Retrieved 15 January 2020.
  11. ^ «Intel 8080 Assembly Language Programming Manual» (PDF). Retrieved 12 May 2023.
  12. ^ Mueller, Scott (March 24, 2006). «P2 (286) Second-Generation Processors». Upgrading and Repairing PCs, 17th Edition (Book) (17 ed.). Que. ISBN 0-7897-3404-4. Retrieved 2017-12-06.
  13. ^
    Curtis Meadow.
    «Encoding of 8086 Instructions».
  14. ^
    Igor Kholodov.
    «6. Encoding x86 Instruction Operands, MOD-REG-R/M Byte».
  15. ^

    «Encoding x86 Instructions».

  16. ^
    Michael Abrash.
    «Zen of Assembly Language: Volume I, Knowledge».
    «Chapter 7: Memory Addressing».
    Section «mod-reg-rm Addressing».
  17. ^
    Intel 80386 Reference Programmer’s Manual.
    «17.2.1 ModR/M and SIB Bytes»
  18. ^
    «X86-64 Instruction Encoding: ModR/M and SIB bytes»
  19. ^
    «Figure 2-1. Intel 64 and IA-32 Architectures Instruction Format».
  20. ^
    «x86 Addressing Under the Hood».
  21. ^ a b
    Stephen McCamant.
    «Manual and Automated Binary Reverse Engineering».
  22. ^
    «X86 Instruction Wishlist».
  23. ^ Peter Cordes (18 December 2011). «NASM (Intel) versus AT&T Syntax: what are the advantages?». Stack Overflow.
  24. ^ «I just started Assembly». daniweb.com. 2008.
  • Intel 64 and IA-32 Software Developer Manuals
  • AMD64 Architecture Programmer’s Manual (Volume 1-5)
  • Ed, Jorgensen (May 2018). x86-64 Assembly Language Programming with Ubuntu (PDF) (1.0.97 ed.). p. 367.

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Стоп стресс для собак инструкция таблетки отзывы
  • Как заменить замок на пластиковой двери входной своими руками пошаговая инструкция
  • Приказ распоряжение инструкция это примеры вербальных коммуникаций
  • Инструкция к часам тиссот 1853 с хронографом
  • Натрия сульфат 10 водный инструкция по применению для крс