Skip to content

Latest commit

 

History

History
 
 

x86_basics

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Ассемблер архитектуры x86 (32-bit, и немного про 64-bit)

Основной reference по набору команд преобразованный в HTML.

Reference по наборам команд MMX, SSE и AVX на сайте Intel.

Неплохой учебник по ассемблеру x86 на WikiBooks

32-разрядный ассемблер в 64-битных системах

Мы будем использовать 32-разрядный набор инструкций. На 64-битных архитектурах для этого используется опция компилятора gcc -m32.

Кроме того, необходимо установить стек 32-разрядных библиотек. В Ubuntu это делается всего одной командой:

sudo apt-get install gcc-multilib

Синтаксис AT&T и Intel

Исторически сложилось два синтаксиса языка ассемблера 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 содержит указатель на нижнюю границу стека, поэтому произвольным образом его использовать не рекомендуется.

Регистры x86-64

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-разрядной архитектуры

Возвращаемое значение 32-разрядного типа функции записывается в регистр eax, для возврата 64-разрядного значения используется пара eax и edx.

Вызываемая функция обязана сохранять на стеке значения регистров общего назначения ebx, ebp, esi и edi.

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

Соглашения cdecl и stdcall

Соглашения о передаче аргументов, используемые на 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.

Соглашение fastcall

Если требуется передать в функцию немного целочисленных аргументов, то можно использовать регистры, как в архитектуре ARM. Такое соглашение называется fastcall.

Соглашение fastcall используется для вызова функций ядра (системных вызовов) в UNIX-подобных системах. В частности, в Linux регистр eax используется для передачи номера системного вызова, а регистры ebx, ecx и edx - для передачи целочисленных аргументов.

Аналогичный подход используется и в архитектуре x86-64, где доступных регистров больше, чем в 32-разрядной архитектуре x86.

Соглашения о вызовах для 64-разрядной архитектуры SystemV AMD64 ABI

Целочисленные аргументы передаются последовательно в регистрах: rdi, rsi, rdx, rcx, r8, r9. Если передается более 6 аргументов, то оставшиеся - через стек.

Вещественные аргументы передаются через регистры xmm0...xmm7.

Возвращаемое значение целочисленного типа должно быть сохранено в rax, вещественного - в xmm0.

Вызываемая функция обязана сохранять на стеке значения регистров общего назначения rbx, rbp, и регистры r12...r15.

Кроме того, при вызове функции для 64-разрядной архитектуры есть дополнительное требование - перед вызовом функции стек должен быть выровнен по границе 16 байт, то есть необходимо уменьшить значение rsp таким образом, оно было кратно 16. Если кроме регистров задействуется стек для передачи параметров, то они должны быть прижаты к нижней выровненной границе стека.

Для функций гарантируется 128-байтная "красная зона" в стеке ниже регистра rsp - область, которая не будет затронута внешним событием, например, обработчиком сигнала. Таким образом, можно задействовать для адресации локальных переменных память до rsp-128.