Основной reference по набору команд преобразованный в HTML.
Reference по наборам команд MMX, SSE и AVX на сайте Intel.
Неплохой учебник по ассемблеру x86 на WikiBooks
Мы будем использовать 32-разрядный набор инструкций. На 64-битных архитектурах
для этого используется опция компилятора gcc -m32
.
Кроме того, необходимо установить стек 32-разрядных библиотек. В Ubuntu это делается всего одной командой:
sudo apt-get install gcc-multilib
Исторически сложилось два синтаксиса языка ассемблера x86: синтаксис AT&T, используемый в UNIX-системах, и синтаксис Intel, используемый в DOS/Windows.
Различие, в первую очередь, относится к порядку аргументов команд.
Компилятор gcc по умолчанию использует синтаксис AT&T, но с указанием опции
-masm=intel
может переключаться в синтаксис Intel.
Кроме того, можно указать используемый синтаксис первой строкой в тексте самой программы:
.intel_syntax noprefix
Здесь параметр noprefix
после .intel_syntax
указывает на то, что помимо порядка аргументов, соответствующих синтаксису Intel, ещё и имена регистров не должны начинаться с символа %
, а константы - с символа $
, как это принято в синтаксисе AT&T.
Мы будем использовать именно этот синтаксис, поскольку с его использованием написано большинство доступной документации и примеров, включая документацию от производителей процессоров.
Исторически семество процессоров x86 унаследовало набор 8-битных регистров
общего назначения
семества 8080/8085, которые назывались a
, b
, c
и d
. Но поскольку
процессор 8086 стал 16-битным, то регистры стали назваться ax
, bx
, cx
и dx
.
В 32-битных процессорах они называются eax
, ebx
, ecx
и edx
, в
64-битных rax
, rbx
, rcx
и rdx
.
Кроме того, в x86 есть регистры "двойного назначения", которые можно использовать, в том числе, в качестве регистров общего назначения, если пользоваться ограниченным подмножеством команд процессора:
ebp
- верхняя граница стека;esi
- индекс элемента массива, из которого выполняется копирование;edi
- индекс элемента массива, в который выполняется копирование.
Регистр esp
содержит указатель на нижнюю границу стека, поэтому произвольным образом его использовать не рекомендуется.
64-разрядные регистры для архитектуры x86-64 именуются начиная с буквы r
. Помимо регистров rax
...rsi
, rdi
можно использовать регистры общего назначение r9
...r15
. Указатель стека хранится в rsp
, верхняя граница стекового фрейма - в rbp
.
Младшие 32-разрядные части регистров rax
...rsi
,rdi
,rsp
,rbp
можно адресовать по именам eax
...esi
,edi
,esp
,ebp
. При записи значений по 32-битным именам регистров, старшие 32 разряда обнуляются, что приемлемо для операций над 32-разрядными беззнаковыми значениями.
Для работы со знаковыми 32-разрядными значениями, например типом int
, необходимо предварительно выполнять операции знакового расширения с помощью команды movslq
Для синтаксиса Intel первым аргументов команды является тот, значение которого будет модифицировано, а вторым - которое остается неизменным.
add DST, SRC /* DST += SRC */
sub DST, SRC /* DST -= SRC */
inc DST /* ++DST */
dec DST /* --DST */
neg DST /* DST = -DST */
mov DST, SRC /* DST = SRC */
imul SRC /* (eax,edx) = eax * SRC - знаковое */
mul SRC /* (eax,edx) = eax * SRC - беззнаковое */
and DST, SRC /* DST &= SRC */
or DST, SRC /* DST |= SRC */
xor DST, SRC /* DST ^= SRC */
not DST /* DST = ~DST */
cmp DST, SRC /* DST - SRC, результат не сохраняется, */
test DST, SRC /* DST & SRC, результат не сохраняется */
adc DST, SRC /* DST += SRC + CF */
sbb DST, SRC /* DST -= SRC - CF */
Для синтаксиса AT&T порядок аргументов - противоположный, то есть команда
add %eax, %ebx
вычислит сумму %eax
и %ebx
, после чего сохранит результат
в регистр %ebx
, который указан вторым аргументом.
В отличии от процессоров ARM, где обновление регистра флагов производится
только при наличии специального флага в команде, обозначаемого суффиксом
s
, в процессорах Intel флаги обновляются всегда большинстом инструкций.
Флаг ZF
устанавливается, если в результате операции был получен нуль.
Флаг SF
устанавливается, если в результате операции было получено
отрицательное число.
Флаг CF
устанавливается, если в результате выполнения операции произошел
перенос из старшего бита результата. Например, для сложения CF
устанавливается
если результат сложения двух беззнаковых чисел не может быть представлен
32-битным беззнаковым числом.
Флаг OF
устанавливается, если в результате выполняния операции произошло
переполнение знакового результата. Например, при сложении OF
устанавливается,
если результат сложения двух знаковых чисел не может быть представлен
32-битным знаковым числом.
Обратите внимание, что и сложение add
, и вычитание sub
устанавливают
одновременно и флаг CF
, и флаг OF
. Сложение и вычитание знаковых и
беззнаковых чисел выполняется совершенно одинаково, и поэтому используется одна
инструкция и для знаковой, и для беззнаковой операции.
Инструкции test
и cmp
не сохраняют результат, а только меняют флаги.
Безусловный переход выполняется с помощью инструкции jmp
jmp label
Условные переходы проверяют комбинации арифметических флагов:
jz label /* переход, если равно (нуль), ZF == 1 */
jnz label /* переход, если не равно (не нуль), ZF == 0 */
jc label /* переход, если CF == 1 */
jnc label /* переход, если CF == 0 */
jo label /* переход, если OF == 1 */
jno label /* переход, если OF == 0 */
jg label /* переход, если больше для знаковых чисел */
jge label /* переход, если >= для знаковых чисел */
jl label /* переход, если < для знаковых чисел */
jle label /* переход, если <= для знаковых чисел */
ja label /* переход, если > для беззнаковых чисел */
jae label /* переход, если >= (беззнаковый) */
jb label /* переход, если < (беззнаковый) */
jbe label /* переход, если <= (беззнаковый) */
Вызов функции и возврат из неё осуществляются командами call
и ret
call label /* складывает в стек адрес возврата, и переход на label */
ret /* вытаскивает из стека адрес возврата и переходит к нему */
Кроме того, есть составная команда для организации циклов, которая
подразумевает, что в регистре ecx
находится счётчик цикла:
loop label /* уменьшает значение ecx на 1; если ecx==0, то
переход на следующую инструкцию, в противном случае
переход на label */
В отличии от RISC-процессоров, x86 позволяет использовать в качестве один из аргументов команды как адрес в памяти.
В синтаксисе AT&T такая адресация записывается в виде:
OFFSET(BASE, INDEX, SCALE)
, где OFFSET
- это константа, BASE
и INDEX
-
регистры, а SCALE
- одно из значений: 1
, 2
, 4
или 8
.
Адрес в памяти вычисляется как OFFSET+BASE+INDEX*SCALE
. Параметры OFFSET
,
INDEX
и SCALE
являются опциональными. При их отсутсвтвии подразумевается,
что OFFSET=0
, INDEX=0
, SCALE
равен размеру машинного слова.
В синтаксисе Intel используется более очевидная нотация:
[BASE + INDEX * SCALE + OFFSET]
.
Возвращаемое значение 32-разрядного типа функции записывается в регистр
eax
, для возврата 64-разрядного значения используется пара eax
и
edx
.
Вызываемая функция обязана сохранять на стеке значения регистров общего назначения ebx
, ebp
, esi
и edi
.
Аргументы могут передаваться в функцию различными способами, в зависимости от соглашений, принятых в ABI.
Соглашения о передаче аргументов, используемые на 32-разрядных
системах архитектуры x86. Все аргументы функций складываются справа-налево
в стек, затем вызывается функция, которая адресует аргументы через указатель
ebp
или esp
с некоторым положительным смещением.
Пример:
char * s = "Name";
int value1 = 123;
double value2 = 3.14159;
printf("Hello, %s! Val1 = %d, val2 = %g\n", s, value1, value2);
Здесь перед вызовом printf
в стек будут сложены значения, переменных,
прежде чем вызвана функция:
push value2
push value1
push s
push .FormatString
call printf
В случае использования соглашения stdcall
, вызываемая функция
обязана удалить из стека переданные её аргументы после их использования.
В случае использования соглашения cdecl
, вызывающая функция обязана
удалить из стека те переменные, которые были переданы в вызываемую функцию.
На языках Си/С++ используемые соглашения можно указывать в специцикаторах функций, например:
void __cdecl regular_function(int arg1, int arg2);
#define WINAPI __stdcall
void WINAPI winapi_function(int arg1, int arg2);
Соглашение stdcall
сейчас используется в основном в операционной системе
Windows для обращения к функциям WinAPI. Во всех остальных случаях на
32-разрядных системах используется cdecl
.
Если требуется передать в функцию немного целочисленных аргументов, то можно
использовать регистры, как в архитектуре ARM. Такое соглашение называется
fastcall
.
Соглашение fastcall
используется для вызова функций ядра (системных вызовов)
в UNIX-подобных системах. В частности, в Linux регистр eax
используется
для передачи номера системного вызова, а регистры ebx
, ecx
и edx
-
для передачи целочисленных аргументов.
Аналогичный подход используется и в архитектуре x86-64, где доступных регистров больше, чем в 32-разрядной архитектуре x86.
Целочисленные аргументы передаются последовательно в регистрах: rdi
, rsi
, rdx
, rcx
, r8
, r9
. Если передается более 6 аргументов, то оставшиеся - через стек.
Вещественные аргументы передаются через регистры xmm0
...xmm7
.
Возвращаемое значение целочисленного типа должно быть сохранено в rax
, вещественного - в xmm0
.
Вызываемая функция обязана сохранять на стеке значения регистров общего назначения rbx
, rbp
, и регистры r12
...r15
.
Кроме того, при вызове функции для 64-разрядной архитектуры есть дополнительное требование - перед вызовом функции стек должен быть выровнен по границе 16 байт, то есть необходимо уменьшить значение rsp
таким образом, оно было кратно 16. Если кроме регистров задействуется стек для передачи параметров, то они должны быть прижаты к нижней выровненной границе стека.
Для функций гарантируется 128-байтная "красная зона" в стеке ниже регистра rsp
- область, которая не будет затронута внешним событием, например, обработчиком сигнала. Таким образом, можно задействовать для адресации локальных переменных память до rsp-128
.