Арифметические инструкции ассемблер

ассемблер

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

Предыдущие статьи

Арифметические инструкции ассемблер

Большинство тех инструкций, о которых ты узнаешь из этой статьи, принимают на входе два аргумента. Каждый отдельно взятый аргумент — это либо регистр, либо память, либо константа. Почти все их сочетания допустимы: регистр-регистр, регистр-память, память-регистр, регистр-константа, память-константа. Единственное недопустимое сочетание — память-память.

Регистры могут быть 16- или 8-битными. 16-битные регистры: AX, BX, CX, DX, SI, DI, BP и SP. 8-битные регистры: AH, AL, BH, BL, CH, CL, DH и DL.

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

У всех программ, которые мы с тобой сейчас напишем, есть повторяющееся действие: вывести результат на экран. Чтобы не писать код для этого действия каждый раз заново, давай напишем для него подпрограмму.

Что такое подпрограмма? Это кусок кода, который выполняет какую-то небольшую задачу. К подпрограмме обычно обращаются через инструкцию call. Все подпрограммы заканчиваются инструкцией 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 (Carry Flag; флаг переноса).

Аналогичным образом работает инструкция shr, но только она сдвигает биты регистра вправо, а не влево.

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

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

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

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

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

У shl (логический сдвиг влево) есть синоним — sal. Две эти инструкции полностью идентичны — вплоть до того, что генерируются в один и тот же машинный код.

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

В 8088 доступны три логические инструкции: and, or и xor.
Три логические инструкции ассемблер
Инструкция and эквивалентна оператору & в Си и JavaScript; or — оператору |, а xor — оператору ^.

Еще есть инструкция not, которая принимает только один параметр. Она инвертирует все биты указанного регистра. ( not al эквивалентна оператору ~ в Си и JavaScript).

Три логические инструкции ассемблер

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

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

Инструкции инкремента и декремента

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

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

В итоге программа выводит на экран вот такую строку: 012345678987654321.

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

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

Написание простой игры на ассемблере «Угадай число»

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

Как она будет работать? После запуска игры компьютер загадывает число, выводит на экран знак вопроса и ждет ответа игрока. Если число, введенное игроком, отличается от того, которое загадал компьютер, игра снова выводит знак вопроса. Когда игрок наконец угадывает число, программа печатает его на экране и добавляет смайлик (знак двоеточия и закрывающую скобку). Вот ассемблерный код, который воплощает описанную задумку.
Написание простой игры на ассемблере
Как компьютер загадывает число? Он считывает из порта 0x40 псевдослучайное число. Этот порт подключен к микросхеме таймера. Таймер без остановки отсчитывает такты процессора. Когда ты считываешь значение с его порта, то каждый раз получаешь псевдослучайное число в диапазоне от 0x00 до 0xFF. Вот и весь секрет.

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

Давай перепишем нашу игру, воспользовавшись этой особенностью NASM.

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

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

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

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

Инструкция Оператор
xor ^
or |
and &
not ~
neg
shl <<
shr >> (для беззнаковых целых)
sar >> (для целых со знаком)

Итого

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

Понравилась статья? Поделиться с друзьями:
Добавить комментарий