Основное чтение: Р. Э. Брайант, Д. Р. О'Халларон. Компьютерные системы: архитектура и программирование. Глава 3.
Мы будем использовать AT&T синтаксис для записи инструкций ассемблера x86. Для компиляции программ будем использовать gas, а точнее gcc.
gcc -m32 prog.S -o prog
Исходный файл с программой на ассемблере должен иметь суффикс .S (буква S должна быть заглавной!). Файл будет обрабатываться препроцессором, затем ассемблером.
На 64-битной Ubuntu может быть не установлена поддержка компиляции программ для 32-битной архитектуры i386. В этом случае даже простейшая программа, приведенная ниже, не скомпилируется. Чтобы установить 32-битные библиотеки выполните команду:
sudo apt-get install gcc-multilib
Если используется файл simpleio_i686.S
или simpleio_x86_64.S
, этот файл нужно просто добавить в командную строку.
Например,
gcc -m32 prog.S simpleio_i386.S -o prog
Все примеры будут рассматриваться для архитектуры x86 (i386).
x86 имеет 8 32-битных регистров общего назначения, которые называются
%eax
, %ebx
, %ecx
, %edx
, %esi
, %edi
, %ebp
, %esp
.
Регистр %esp
- указатель стека и всегда используется в этих целях.
Регистр %ebp
- указатель фрейма, то есть области данных текущей функции.
Он может использоваться и как регистр общего назначения, но мы этого делать
не будем. Таким образом для вычислений остается 6 регистров %eax
, ...,
%edi
. Из них регистр %eax
используется для возврата 32-битного
значения из функции, а пара регистров %eax
, %edx
- для возврата
64-битного значения. В любом случае вызываемая функция может испортить
регистры %eax
, %ecx
, %edx
, но состояние остальных регистров она
обязана сохранить. Итого для хранения промежуточных результатов вычислений
осталось три регистра: %ebx
, %esi
, %edi
.
В некоторых случаях необходимо использовать младшие байты 32-битных регистров,
работать с которыми можно как с 8-битовыми регистрами %al
, %bl
, %cl
,
%dl
. При манипуляциях с младшим байтом остальные байты регистра не
изменяются.
Инструкция имеет вид:
LABEL: OPCODE ARGS
Где LABEL
- необязательная метка, которую можно использовать в других местах
программы для перехода или загрузки адреса. Можно считать, что метка - это
адрес, по которому находится данная инструкция программы.
OPCODE
- мнемоника операции. Полный набор инструкций x86 очень велик,
мы будем использовать маленькое подмножество.
ARGS
- аргументы операции. У двухадресной операции операнд-приемник
результата всегда пишется вторым.
Примеры:
movl %eax, %ebx
Переслать (присвоить, скопировать) содержимое регистра %eax' в регистр
%ebx`.
incl %esi
Увеличить значение %esi
на 1.
subl $4, %edi
Вычесть число 4 из значения %edi
и сохранить результат обратно в %edi
.
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 - CF */
Операции умножения оставляют результат - 64-битное значение в паре регистров %eax (младшая часть) и %edx (старшая часть).
call label /* вызов подпрограммы */
ret /* возврат из подпрограммы */
Сначала мы будем использовать для ввода-вывода маленькую библиотеку simpleio_x86.S (или simpleio_x64.S).
Простейшая программа на ассемблере будет выглядеть так:
.text /* секция кода программы */
.global main /* экспортируем точку входа - функцию main */
main:
call finish /* вызываем подпрограмму finish: exit(0) */
Чтение целого числа со стандартного потока ввода:
/* фрагмент программы */
call readi32
По возращению в регистре %eax
находится считанное число. Если произошла
ошибка преобразования или был достигнут конец файла, флаг CF устанавливается,
а при успешном чтении сбрасывается.
Вывод целого числа на стандартный поток вывода:
call writei32
перед вызовом в регистр %eax
должно быть помещено выводимое число.
Вывод символа \n:
call nl
Чтение 64-битного числа со стандартного потока ввода:
call readi64
По возращению в регистрах %eax
(младшие 32 бита) и %edx
(старшие 32 бита) находится считанное число.
Если произошла ошибка преобразования или был достигнут конец файла,
флаг CF устанавливается,
а при успешном чтении сбрасывается.
Вывод 64-битного целого числа на стандартный поток вывода:
call writei64
перед вызовом в регистры %eax
(младшие 32 бита), %edx
(старшие 32 бита) должно быть помещено выводимое число.
Вариант библиотеки simpleio_x64.S
для x64 следует стандартному соглашению о вызовах.
Большинство арифметических инструкций в результате вычисления результата инструкции устанавливают арифметические флаги слова состояния процесса. Флаг ZF устанавливается, если в результате операции был получен нуль. Флаг SF устанавливается, если в результате операции было получено отрицательное число. Флаг CF устанавливается, если в результате выполнения операции произошел перенос из старшего бита результата. Например, для сложения CF устанавливается если результат сложения двух беззнаковых чисел не может быть представлен 32-битным беззнаковым числом. Флаг OF устанавливается, если в результате выполняния операции произошло переполнение знакового результата. Например, при сложении OF устанавливается, если результат сложения двух знаковых чисел не может быть представлен 32-битным знаковым числом.
Обратите внимание, что и сложение addl
, и вычитание subl
устанавливают
одновременно и флаг CF, и флаг OF. Сложение и вычитание знаковых и беззнаковых
чисел выполняется совершенно одинаково, и поэтому используется одна инструкция
и для знаковой, и для беззнаковой операции.
Безусловный переход выполняется с помощью инструкции 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 /* переход, если <= (беззнаковый) */
.text
Это секция кода. В ней размещается код программы и данные только на чтение.
.data
Это секция данных. Данные в этой секции можно модифицировать
.asciz "a string"
Определяет строку, завершающуюся нулем.
.space SIZE, FILL
Выделяет пространство размера SIZE, заполненное байтом FILL, например,
.space 64 * 4, 0
Выделяет место для глобального массива типа int из 64 элементов.
.int 20
Выделяет место для глобальной 32-битной переменной с начальным значеним 20.
.quad -1
Выделяет место для глобальной 64-битной переменной с начальным значением -1.
Как правило, определение данных должно быть помечено. Например,
str1: .asciz "Hello, there\n"
Затем метка str1
может использоваться в программе:
movl $str1, %esi
в этом случае адрес памяти, по которому размещается строка str1
будет загружен в регистр %esi
.
Чтобы очистить (обнулить) регистр, обычно используется инструкция xorl
.
xorl %esi, %esi // %esi = 0
Чтобы загрузить константное значение (в частности, метка - это константное значение, равное адресу размещения соответствующей инструкции в памяти) используется непосредственный аргумент, например:
movl $1, %eax // %eax = 1
movl $0xff, %ebx // %ebx = 0xff
movl $arr, %edx // в %edx поместить адрес, по которому размещается инструкция,
// помеченная addr
Непосредственный аргумент может быть первым операндом у двухадресных инструкций (addl
, etc).
-
Учебное пособие для курса ВМК "Архитектура ЭВМ и язык ассемблера": часть 1, часть 2.
-
Материалы лекций курса ВМК "Архитектура ЭВМ и язык ассемблера".