Вы научитесь арифметическим операциям и создадите игру с нуля

Прочитав эту статью, вы узнаете, как использовать арифметические и логические инструкции, а также инструкции сдвига. Попутно вы научитесь создавать подпрограммы. А в конце напишете простую игрушку «Угадай число».

Написание вспомогательной программы

Все программы, которые мы сейчас будем писать, имеют повторяющееся действие: отображать результат на экране. Чтобы не писать код этого действия каждый раз заново, напишем для него подпрограмму.
Что такое подпрограмма? Это фрагмент кода, который выполняет небольшую задачу. Доступ к подпрограмме обычно осуществляется с помощью инструкции вызова. Все подпрограммы заканчиваются инструкцией ret (RETurn).
На данный момент это все, что вам нужно знать о подпрограммах. Конечно, в хорошем смысле, когда вы используете подпрограммы, вам нужно понимать еще несколько вещей, в частности, что такое указатель стека и как он изменяется при доступе к подпрограмме. А пока, в рамках текущего урока, давайте просто согласимся, что если вам нужно нарисовать букву на экране, просто вставьте эту инструкцию в свой код.

Подпрограмма call display_letter реализована ниже. Сохраните его в файл library.asm.

Важный!

В конце всех программ, которые мы пишем, вам нужно будет вставить код из library.asm. Куда это ведет? Все программы заканчиваются выходом в командную строку, и у них будут подпрограммы для вывода символа на экран (из регистра AL) и чтения символа с клавиатуры (результат помещается в регистр AL).

Учимся складывать и вычитать

Давайте использовать регистр AL и константу в качестве аргументов для инструкции сложения.

Эта программа отображает цифру 7. Потому что 4 + 3 = 7.

Давайте возьмем регистр AL и константу в качестве аргументов для инструкции вычитания.

Эта программа отображает цифру 1. Потому что 4–3 = 1.

Освоение умножения

Инструкция умножения может работать с байтами (8-битные числа) и словами (16-битные числа). Умножение - это всегда регистр AL / AX. Причем множитель может быть либо регистром (любым), либо переменной в памяти.
Просто имейте в виду, что если ваш множитель находится в AL, то множитель должен быть 8-битным, а если в AX, он должен быть 16-битным. Результат умножения оказывается либо в AX (когда мы умножаем два 8-битных числа), либо в DX: AX (когда мы умножаем два 16-битных числа).
В В приведенном ниже примере мы используем два 8-битных регистра AL (множитель) и CL (множитель). Результат записывается в 16-битный регистр AX.

Эта программа отображает число 6. Поскольку 3 × 2 = 6.
Чтобы умножить два 16-битных числа, поместите множитель в AX, а множитель в CX. Затем вместо mul cl напишите mul cx.
Обратите внимание, что оператор mul работает с целыми числами без знака. Если вам нужно умножать числа со знаком, используйте imul.

Понимание деления

Инструкция деления может работать со словами (16-битные числа) и двойными словами (32-битные числа). Дивиденд всегда равен регистру AX или DX: AX. А делителем может быть как регистр (любой), так и переменная в памяти.

Просто имейте в виду, что если ваш дивиденд находится в AX, то делитель должен быть 8-битным, а если в DX: AX он должен быть 16-битным.

Когда вы делите 16-битное число на 8-битное, результат переходит в AL, а остаток - в AH. Если вы разделите 32-битное число на 16-битное, результат перейдет в AX, остальное - в DX.

В приведенном ниже примере мы используем 16-битные и 8-битные регистры. Результат переходит в AL, остаток - в AH.

Эта программа отображает число 3. Потому что 100/33 = 3. Если вы хотите увидеть, каков остаток, добавьте эту строку сразу после той, где написана инструкция div.

Осторожно! Инструкция деления может нарушить вашу программу. Если разделить на ноль, произойдет системная ошибка, и ваша программа перейдет в командную строку.
Обратите внимание, что оператор div работает с целыми числами без знака. Если вам нужно разделить числа со знаком, используйте idiv.

Логические и арифметические сдвиги, циклический сдвиг

Инструкции по переключению (операторы ‹

Теперь я объясню, как работает инструкция по смене. Представьте, что значение регистра AL - это двоичное число. Инструкция shl просто сдвигает каждый бит двоичной единицы влево и добавляет ноль вправо. И бит, который вытесняется слева, попадает в CF (флаг переноса; флаг переноса).

Инструкция shr работает аналогичным образом, за исключением того, что она сдвигает биты регистра вправо, а не влево.
Существует также инструкция sar, которая работает почти как shr, но в отличие от shr, она не выполняет логический сдвиг, но сдвиг арифметический. Что это значит? Когда sar сдвигает бит двоичного числа вправо, он не добавляет ноль, а дублирует бит, который был до сдвига. Иногда это может быть ноль, но не всегда.

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

Вы, наверное, уже поняли, что второй аргумент для всех инструкций сдвига указывает количество позиций, на которые следует сдвинуть биты регистра. Обратите внимание: если вы укажете эту сумму в цифре, то она может быть только одна. Если вы хотите сдвинуть сразу несколько битов, используйте регистр CL.

Также есть инструкции для циклического переключения: ror, rcr, rol и rcl. В чем их особенность? Биты, которые выдвигаются с одного конца, появляются с другой стороны. Циклический сдвиг вправо выполняется инструкцией ror, а влево - инструкцией rol. rcr / rcl делают то же самое, что и ror / rol, за исключением того, что они используют один дополнительный бит CF. Добавленный бит берется из CF, а расширенный бит переходит в CF.

ИНФОРМАЦИЯ

ЗЫ (логический сдвиг влево) имеет синоним - сал. Эти две инструкции полностью идентичны - в той мере, в какой они генерируются в одном и том же машинном коде.

Три логических инструкции плюс одна бесполезная

На 8088 доступны три логические инструкции: and, or, и xor.

Оператор and эквивалентен оператору & в C и JavaScript; или для | оператор и xor для оператора ^.
Существует также инструкция not, которая принимает только один параметр. Он инвертирует все биты указанного регистра. (not al эквивалентно оператору ~ в C и JavaScript).

Кроме того, у 8088 есть инструкция neg, которая очень похожа на not, но выполняет не логическую инверсию, а арифметическую: она меняет знак заданного числа.

Также в ассемблере есть инструкция, которая абсолютно ничего не делает. Вы можете вставить его в любое место вашей программы, и это никоим образом не повлияет на поток выполнения. Конечно, за исключением того, что программа работает чуть медленнее. Это инструкция nop. Вы можете поэкспериментировать с этим. Вставьте его в любом месте после директивы org, и вы увидите, как ваша программа вырастет ровно на один байт (это размер инструкции No OPeration), но она будет работать без изменений.

Представляем инструкции увеличения и уменьшения

Инструкции увеличения и уменьшения позволяют увеличивать или уменьшать значение регистра или значение переменной в памяти на единицу. Эти инструкции работают как с байтами (8 бит), так и со словами (16 бит).

Вот и мы:
1. загружаем ASCII-код цифры ноль в AL, то есть 0x30;
2. показываем число на экране;
3. добавляем единицу в AL;
4. повторяйте шаги 2–3, пока AL не станет 0x39;
5. показать текущий символ и сделать все так же, как и раньше, но в обратном порядке;
6. вычесть единицу из AL;
7. отображаем, что произошло;
8. повторяем, пока AL не станет 0x30.

В результате программа отображает на экране следующую строку: 012345678987654321.

В этой программе есть еще одна новая инструкция для вас - cmp (CoMPartion - сравнить). Она работает так же, как инструкция вычитания, с одним существенным отличием: cmp не изменяет значение регистра. Он изменяет только биты регистра флагов.

Обычно cmp используется вместе с инструкциями условного перехода, такими как je (переход, если равно), jne (переход, если не равно) и т.п.

Что ж, теперь вы знаете достаточно инструкций по сборке, чтобы написать простую игру «Угадай число».

Как это будет работать? После запуска игры компьютер угадает число, отображает вопросительный знак на экране и ждет ответа игрока. Если введенное игроком число отличается от числа, которое угадал компьютер, игра снова отображает вопросительный знак. Когда игрок наконец угадает число, программа выводит его на экран и добавляет смайлик (двоеточие и закрывающую скобку). Вот код ассемблера, реализующий описанную идею.

Как компьютер угадывает число? Считывает псевдослучайное число из порта 0x40. Этот порт подключен к микросхеме таймера. Таймер отсчитывает циклы процессора без остановки. Когда вы читаете значение из его порта, каждый раз вы получаете псевдослучайное число в диапазоне от 0x00 до 0xFF. В этом весь секрет.
Теперь небольшой организационный момент. До сих пор, когда нам с вами требовалась буква, мы устанавливали ее с помощью шестнадцатеричного кода ASCII. Но у компилятора NASM есть приятная особенность: вы можете ввести любой символ, заключить его в апострофы, а NASM сам преобразует его в код ASCII.
Давайте перепишем нашу игру, используя эту функцию NASM.

Согласитесь, это делает исходный код более читабельным.

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

Инструкции и операторы

Если вы знаете C, Java или JavaScript, то вот небольшая таблица соответствия между инструкциями ассемблера, которые вы освоили сегодня, и операторами этих языков.