Для начала на 64-битной Ubuntu выполним
sudo apt-get install gcc-multilib
gcc prog.S -o prog
gcc -m32 prog.S simpleio_i686.S -o prog
gcc -m32 -nostdlib prog.S -o prog
— без стандартной библиотеки языка Си и startup кода
Тогда точка входа в программу называется _start
, а для завершения программы нужно использовать системный вызов exit
Название файла:
.S
если нужен препроцессор Cи
.s
если не нужен препроцессор Cи
- Комментарии как в языке Cи
- Символьные константы как в Cи
- Строки как в Cи, но без неявного
\0
- Каждая инструкция процессора на отдельной строке
- Инструкции можно помечать
LABEL:
- Метки это не переменные, а символические константы, значение которых известно при компиляции
- Директива ассемблера управляет трансляцией
- Инструкция транслируется в машинный код
Целое число (32 бит) может быть:
- Знаковым целым числом
- Беззнаковым целым числом
- Указателем любого типа
Тип никак не привязан к ячейке/регистру, в котором хранится число. Интерпретация числа зависит от выполняющейся инструкции.
У целочисленных констант поддерживаются различные основания:
- десятичное (суффикс
d
или префикс0d
) - двоичное (суффикс
b
или префикс0b
) - восьмеричное (суффикс
q
или префикс0o
) - шестнадцатеричное (суффикс
h
или префикс0h
либо0x
)
Секции — логические части программы
.text
— код программы и readonly data.data
— глобальные переменные (данные в этой секции можно модифицировать).bss
— инициализированные нулём глобальные переменные
Компоновщик объединяет кусочки секций из разных единиц трансляции в одну большую секцию.
Константы и константные строки могут размещаться в .text
или в .rodata
Глобальные переменные размещаются в .data
или в .bss
Дополнительные секции можно обозначать так:
.section .rodata, "a"
Метка — адрес, по которому размещается инструкция при выполнении программы.
Чтобы сделать метку NAME
доступной компоновщику пишем .global NAME
Точка входа в программу — метка, на которую передаётся управление в начале выполнения программы.
Точки входа:
.global _start
если без стандартной библиотеки Си
.global main
со стандратной библиотекой Си
Простейшая программа на ассемблере будет выглядеть так:
.text // секция кода программы
.global main // экспортируем точку входа
main:
call finish // вызываем подпрограмму finish // exit(0)
.align n
задаёт выравнивание данных в секции
extern
говорит компилятору, что переменная определена в другой единице трансляции
const
в Си используется как обозначение того, что память, в которой размещена переменная, является readonly
Глобальные переменные:
.byte 1, 2, 3, '\n' // размер: 8 бит
.short 10, 11 // размер: 16 бит
.int 0xff00ff00, 20 // размер: 32 бит
.quad -1 // размер: 64 бит
.float 1.5
.double 2.0
.ascii "abc"
.asciz "Hello" // строка завершается байтом \0
Резервирование памяти под массив:
.skip 4 * 1024, 0
.space 64 * 4, 0 // выделяет пространство размера 64 * 4, заполненное байтом 0
Как правило, определение данных должно быть помечено. Например:
str1:
.asciz "Hello, there\n"
Затем метка str1
может использоваться в программе:
movl $str1, %esi // положили в %esi адрес строки
31 0
+-------+---+---+
| |AH |AL | EAX // младшие 16 бит : AX
| |BH |BL | EBX // младшие 16 бит : BX
| |CH |CL | ECX // младшие 16 бит : CX
| |DH |DL | EDX // младшие 16 бит : DX
| | SI | ESI
| | DI | EDI
| | BP | EBP // регистр текущего фрейма в стеке
| | SP | ESP // указатель стека
Регистры — это ячейки памяти, находящиеся в процессоре, поэтому являются глобальными
%cl
— счётчик сдвига
eip
— адрес следeдующей инструкции
eflags
— регистр флагов процессора
Регистры %ebx
, %esi
, %edi
можем использовать для хранения промежуточных результатов.
При манипуляциях с младшим байтом (%al
, %bl
, %cl
, %dl
) остальные байты регистра не изменяются.
mov x, %edi // загружаем данные по адресу x
mov $x, %edi // кладём адрес x в регистр
(%esp)
- скобки означают обращение к памяти по адресу, который лежит в регистре
Методы адресации:
-
Регистровый — указывается имя регистра
movl %esp, %ebp
-
Непосредственный — аргумент задается в инструкции
movb $16, %cl
-
Прямой — адрес ячейки памяти задается в инструкции
movl %eax, var1
Аргумент инструкции, обращающийся к памяти, имеет следующий общий вид:
OFFSET(BASE, INDEX, SCALE)
OFFSET
— это 32-битное значение, которое можно рассматривать либо как смещение, либо как базовый адрес в памяти.
BASE
— это регистр процессора.
INDEX
— это регистр процессора.
SCALE
— это число из {1, 2, 4, 8}
.
Адрес для обращения к памяти вычисляется по формуле:
OFFSET + BASE + INDEX * SCALE
Примеры:
movl (%eax), %eax // в регистр %eax записать 4 байта, расположенные по адресу, который хранился изначально в %eax
movl %ecx, -8(%ebx) // сохранить значение из регистра %ecx по адресу %ebx - 8
movl arr(,%esi,4), %eax // в регистр %eax записать значение, расположенное по адресу arr + %esi * 4
movl -64(%ebp,%esi,4), %eax // в регистр %eax записать значение, расположенное по адресу %ebp - 64 + %esi * 4
- ZF (бит 6) — флаг нулевого результата
- SF (бит 7) — флаг отрицательного результата
- CF (бит 0) — флаг переноса из старшего бита
- OF (бит 11) — флаг переполнения
Флаг ZF устанавливается, если в результате операции был получен нуль.
Флаг SF устанавливается, если в результате операции было получено отрицательное число.
Флаг CF устанавливается, если в результате выполнения операции произошел перенос из старшего бита результата. Например, для сложения CF устанавливается если результат сложения двух беззнаковых чисел не может быть представлен 32-битным беззнаковым числом.
Флаг OF устанавливается, если в результате выполняния операции произошло переполнение знакового результата. Например, при сложении OF устанавливается, если результат сложения двух знаковых чисел не может быть представлен 32-битным знаковым числом.
Cложение addl
, и вычитание subl
устанавливают одновременно и флаг CF, и флаг OF. Сложение и вычитание знаковых и беззнаковых чисел выполняется совершенно одинаково, и поэтому используется одна инструкция и для знаковой, и для беззнаковой операции.
Инструкции ADD, SUB, CMP, INC устанавливают флаги в зависимости от результата
-
IMUL устанавливает OC в зависимости от представимости результата 32 битами, Z – неопределен, S – старший бит младших 32 битов
-
LEA, MOV – не изменяет флаги
-
AND, TEST, OR, XOR – обнуляют O, C, устанавливают S и Z в зависимости от результата
readi32 / readi64
при успешном чтении сбрасывает флаг CF, иначе устанавливает
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
js label // переход, если SF == 1
jns label // переход, если SF == 0
jg label // переход, если больше для знаковых чисел
jge label // переход, если >= для знаковых чисел
jl label // переход, если < для знаковых чисел
jle label // переход, если <= для знаковых чисел
ja label // переход, если > для беззнаковых чисел
jae label // переход, если >= (беззнаковый)
jb label // переход, если < (беззнаковый)
jbe label // переход, если <= (беззнаковый)
Стек растёт вниз по адресам
Куча растёт вверх по адресам
На стек можно сохранять только 32-битные значения
%esp
- указывает на самый младший по адресам элемент стека
push %eax
уменьшает на 4 значение %esp
, потом кладёт значение %eax
по адресу %esp
pop %eax
кладёт значение по адресу %esp
в %eax
, потом увеличивает на 4 значение %esp
ret
— возврат из подпрограммы, для этого в верхушке стека (по адресу (%esp)
) должен находиться адрес возврата
call
— кладёт в стек %eip
как адрес возврата, затем помещает в %eip
адрес вызываемой подпрограммы
Выделение памяти
sub $4, %esp // выделили 4 байта под локальную переменную
mov $42, (%esp)
Сохранение регистров
push %ebx
push %esi
/* код подпрограммы */
pop %esi
pop %ebx
ret
Организация стекового кадра
Регистр %ebp
хранит адрес стекового кадра текущей подпрограммы
(%ebp)
— адрес стекового кадра предыдущей подпрограммы, ((%ebp)
— пред-предыдущей…
Самый внешний стековый кадр хранит 0
.
// Стандартный пролог :
pushl %ebp
movl %esp, %ebp
/* код подпрограммы */
// Стандартный эпилог:
movl %ebp, %esp
popl %ebp
ret
Выравнивание
Linux x86 не требует, но рекомендует, а MacOS требует выравнивания стека по 16 байтам.
При вызове подпрограммы первый аргумент должен находиться по адресу, кратному 16.
Если выравнивание стека неизвестно:
and $-16, %esp // смещаем ESP вниз на правильную границу
При вызове подпрограммы первый аргумент должен находиться по адресу, кратному 16.
extern "C"
чтобы вызвать в C++ неманглированную ассемблерную функцию
call LABEL
— вызов подпрограммы
ret
— возврат из подпрограммы
Calling convention:
%eax
или%edx:%eax
для возврата значения из подпрограммы%eax
,%ecx
,%edx
— scratch%ebx
,%esi
,%edi
,%ebp
— callee-saved- Параметры передаются через стек
- Параметры заносятся в обратном порядке
- Стек очищается тем, кто вызвал подпрограмму (caller-cleaned)
Если подпрограмма использует регистры callee-saved, они должны быть сохранены в начале и восстановлены перед возвратом из нее.
Передача аргументов в подпрограмму
Первый параметр должен лежать в стеке по младшему адресу, следующие параметры последовательно по возрастанию адресов, поэтому параметры заносятся в стек в обратном порядке.
Если размер аргумента меньше чем 32 бита:
pushl $'\n' // всё равно выделяются 4 байта, байт \n - младший
call putchar
Если размер аргумента 64 бита:
// сохраняем в стек число 1LL как LE-значения (младший байт лежит по младшему адресу)
pushl $0
pushl $1
Доступ к аргументам
Используются положительные смещения относительно %ebp
:
movl 8(%ebp), %eax // доступ к 1-му параметру
Ниже %ebp
хранятся сохраненные регистры и область под локальные переменные
Чтение целого числа со стандартного потока ввода:
call readi32
Поcле исполнения подпрограммыв регистре %eax
находится считанное число.
Если произошла ошибка преобразования или был достигнут конец файла, флаг CF устанавливается, а при успешном чтении сбрасывается.
Вывод целого числа на стандартный поток вывода:
// тут в регистр %eax должно быть помещено выводимое число
call writei32
Вывод символа \n
:
call nl
Чтение 64-битного целого числа со стандартного потока ввода:
call readi64
Поcле исполнения подпрограммы в регистрах %eax
(младшие 32 бита) и %edx
(старшие 32 бита) находится считанное число.
Если произошла ошибка преобразования или был достигнут конец файла, флаг CF устанавливается, а при успешном чтении сбрасывается.
Вывод 64-битного целого числа на стандартный поток вывода:
// тут в регистры %eax (младшие 32 бита), %edx (старшие 32 бита) должно быть помещено выводимое число
call writei64
Суффиксы: b — 8 бит w — 16 бит l — 32 бит q — 64 бит
addl SRC, DST // DST += SRC
subl SRC, DST // DST -= SRC
incl DST // ++DST
decl DST // --DST
negl DST // DST = -DST
movl SRC, DST // DST = SRC
imull SRC // (%eax,%edx) = %eax * SRC - знаковое
mull SRC // (%eax,%edx) = %eax * SRC - беззнаковое
andl SRC, DST // DST &= SRC
orl SRC, DST // DST |= SRC
xorl SRC, DST // DST ^= SRC
notl DST // DST = ~DST
cmpl SRC, DST // DST - SRC, результат не сохраняется
testl SRC, DST // DST & SRC, результат не сохраняется
adcl SRC, DST // DST += SRC + CF
sbbl SRC, DST // DST -= SRC - C
mov откуда, куда // копирование
Типы пересылок:
- Регистр-регистр
- Регистр-память
- Память-регистр
xorl %esi, %esi // %esi = 0
Команда | Результат |
---|---|
mulb %al |
16 бит: %ax |
mulw %ax |
32 бита: младшая часть в %ax , старшая в %dx |
mull %eax |
64 бита: младшая часть в %eax , старшая в %edx |
Заносит в аргумент значение 0
или 1
в зависимости от установленных флагов.
Аргументом может быть: %al
, %bl
, %cl
, %dl
или байт в памяти.
Инструкция | Устанавливает 1, если |
---|---|
SETA | above (CF=0 and ZF=0). |
SETAE | above or equal (CF=0). |
SETB | below (CF=1). |
SETBE | below or equal (CF=1 or ZF=1). |
SETC | carry (CF=1). |
SETE | equal (ZF=1). |
SETG | greater (ZF=0 and SF=OF). |
SETGE | greater or equal (SF=OF). |
SETL | less (SF != OF). |
SETLE | less or equal (ZF=1 or SF != OF). |
SETNA | not above (CF=1 or ZF=1). |
SETNB | not below (CF=0). |
SETNC | not carry (CF=0). |
SETNE | not equal (ZF=0). |
SETNG | not greater (ZF=1 or SF != OF). |
SETNL | not less (SF=OF). |
SETNO | not overflow (OF=0). |
SETNP | not parity (PF=0). |
SETNS | not sign (SF=0). |
SETNZ | not zero (ZF=0). |
SETO | overflow (OF=1) |
SETP | parity (PF=1). |
SETPE | parity even (PF=1). |
SETPO | parity odd (PF=0). |
SETS | sign (SF=1). |
SETZ | zero (ZF=1). |
lea источник, куда
Адрес источника копируется, без обращения по этому адресу
Источник должен находиться в памяти (не может быть непосредственным значением — константой или регистром)
testb $0b00001000, %al /* установлен ли 3-й (с нуля) бит? */
je not_set
/* нужные биты установлены */
not_set:
/* биты не установлены */
testl %eax, %eax
je is_zero
/* %eax != 0 */
is_zero:
/* %eax == 0 */
Обменивает значение из регистра/памяти со значением регистра.
xchg %esi, %edi
Не принимает аргументов!
Загружает: %ah
← EFLAGS(SF:ZF:0:AF:0:PF:1:CF)
команда количество, назначение
Количество может быть задано в коде или находиться в регистре %cl
(младшие 5 бит)
Арифметические
При сдвиге вправо старший бит заполняется знаковым битом
sal %eax // %eax <<= 1
sar %eax // %eax >>= 1
sal $2, %eax // %eax <<= 2
sal %cl, %eax // %eax <<= %cl & 0x1F
sarl $4, %eax
31 0
+---------------------------------------+
до |1000 0000 0000 0000 1111 0000 0000 0000|
+---------------------------------------+
после |1111 1000 0000 0000 0000 1111 0000 0000|
+---------------------------------------+
sarl $4, %eax
31 0
+---------------------------------------+
до |0000 0000 0000 0000 1111 0000 0000 0000|
+---------------------------------------+
после |0000 0000 0000 0000 0000 1111 0000 0000|
+---------------------------------------+
Логические
При сдвиге вправо старший бит заполняется нулём
shl CNT, DST // влево
shr CNT, DST // вправо
shrl $4, %eax
31 0
+---------------------------------------+
до |1000 0000 0000 0000 1111 0000 0000 0000|
+---------------------------------------+
после |0000 1000 0000 0000 0000 1111 0000 0000|
+---------------------------------------+
shrl $4, %eax
31 0
+---------------------------------------+
до |0000 0000 0000 0000 1111 0000 0000 0000|
+---------------------------------------+
после |0000 0000 0000 0000 0000 1111 0000 0000|
+---------------------------------------+
Вращения
rol CNT, DST // влево
ror CNT, DST // вправо
roll $4, %eax
31 0
+---------------------------------------+
до |1000 0000 0000 0000 1111 0000 0000 0101|
+---------------------------------------+
после |0000 0000 0000 1111 0000 0000 0101 1000|
+---------------------------------------+
Вращения через CF
rcl CNT, DST // влево
rcr CNT, DST // вправо
rcrl %eax
CF 31 0
+---+---------------------------------------+
до | C |1001 0000 0000 0000 1111 0000 0000 0001|
+---+---------------------------------------+
после | 1 |C100 1000 0000 0000 0000 0111 1000 0000|
+---+---------------------------------------+
Расширение нулями:
movzbl %al, %esi // 8 -> 32 бита
movzwl %ax, %esi // 16 -> 32 бита
Расширение знаковым битом:
movsbl %al, %esi // 8 -> 32 бита
movswl %ax, %esi // 16 -> 32 бита
Не принимает аргументов!
Знаковое расширение %eax
→ %edx:%eax
64-битные целые требуют по несколько инструкций для обработки
64-битное значение хранится в паре регистров (напр. %eax
и %edx
)
- Логические операции — отдельно для младшей и старшей половины
- Cложение — ADD для младшей половины, ADC для старшей
- Вычитание — SUB для младшей половины, SBB для старшей
- Умножение, деление — вспомогательные функции (находятся в libgcc или аналогичной библиотеке)
Сдвиги 64-битных чисел
Пусть младшая часть числа лежит в %eax
, старшая в %edx
Можно сдвигать по одному биту и использовать CF
тогда сдвиг влево на 1:
shll %eax
rcll %edx
Или через специальные инструкции shld/shrd
для старшей части
тогда сдвиг влево на 7:
shld $7, %eax, %edx
shll $7, %eax
тогда сдвиг вправо на 7:
shrd $7, %edx, %eax
shrl $7, %edx
архитектура ЭВМ и язык ассемблера
в столбце tested_f указаны флаги процессора, которые влияют на исполнение инструкции, modif_f - это флаги, значение которых может измениться в результате выполнения инструкции, def_f - это только те флаги, значение которых может измениться специфицированным образом, undef_f - это флаги, значение которых после выполнения инструкции не определено.