АРИФМЕТИЧЕСКИЕ
КОМАНДЫ
Микропроцессор может
выполнять целочисленные операции и операции с плавающей точкой. Для этого в его
архитектуре есть два отдельных блока:
•устройство для
выполнения целочисленных операций;
•устройство с
плавающей точкой.
Каждое из этих
устройств имеет свою систему команд. В принципе, целочисленное устройство может
взять на себя многие функции устройства с плавающей точкой, но это потребует больших
вычислительных затрат.
!!! Для большинства
задач, использующих язык ассемблера, достаточно целочисленной арифметики.
Сложение
беззнаковых чисел
Микропроцессор
выполняет сложение операндов по правилам сложения двоичных чисел.
• add операнд1,операнд2
— команда сложения с принципом действия:
операнд1 = операнд1 +
операнд2
·
xadd назначение,источник — обмен местами
и сложение.
Команда позволяет
выполнить последовательно два действия:
•обменять
значения назначение и источник;
•поместить
на место операнда назначение сумму:
назначение = назначение + источник.
!!!
В операции сложения должны участвовать операнды одного формата (b-b, w-w)
!!!
Возможны сочетания
регистр - регистр
память - регистр
регистр - память
регистр - непосредственный
операнд
неп. Операнд - регистр
!!!
Операция память - память выполняется через промежуточный регистр
Пример1 Вычисление суммы двух чисел
. . .
slag1 dw 250
slag2 dw 125
rez dw ?
. . .
mov ax,slag1
add ax,slag2
mov rez,ax
. . .
• inc операнд —
операция инкремента, то есть увеличения значения операнда на
1;
операнд
= операнд +1
!!! Операнд м.б.
регистром или адресом памяти и !!! не м.б. непоср.операндом
Пример2. Увеличение числа на 1
Mov ax,slag1
inc ax
mov
rez,ax
Особые ситуации,
которые могут возникать при сложении:
1) значение
результата превышает размерности поля
операнда
для фиксирования ситуации выхода за разрядную
сетку результата в микропроцессоре предусмотрено специальное средство: флаг
переноса CF
(curry
flag). Он располагается в
бите 0 регистра флагов eflags/flags. Именно установкой этого флага фиксируется
факт переноса единицы из старшего разряда операнда.
(CF=1)
Например, при сложении операндов размером в байт
результат не должен превышать число 255. Если это происходит, то результат
оказывается неверным.
Например, выполним
сложение: 254 + 5 = 259 в двоичном виде. 11111110 + 0000101 = 1 00000011.
Результат вышел за пределы восьми бит и правильное его значение укладывается в
9 бит, а в 8-битовом поле операнда осталось значение 3, что, конечно, неверно.
Т.е. если при сложении
двух 8-битовых чисел результат занимает 9 битов, то значение старшего 9 бита
запоминается во флажке CF.
!!!
Программист должен предусматривать возможность такого исхода операции сложения
Необходимо включать участки кода после операции сложения, в которых
анализируется флаг cf.
Анализ этого флага
можно провести различными способами.
Например, использовать
команду условного перехода
jc <метка> ; Переход
на метку если cf = 1
jnc
<метка> ; Переход на метку если
cf = 0
• adc
операнд_1,операнд_2 — команда сложения с учетом флага переноса cf.
операнд_1 = операнд_1 + операнд_2 + значение_cf
это команда сложения,
учитывающая перенос единицы из старшего разряда.
Пример 3. Вычисление
суммы чисел
masm
model small
stack 256
.data
a db 254
.code
main:
mov
ax,@data
mov ds,ax
...
xor ax,ax
add al,17
add
al,a ; результат сложения
выходит за границы операнда.
jnc m1 ; проверяет
состояние флага cf - если нет переноса, то перейти на m1
adc ah,0 ;в ax сумма с учетом
переноса в старший разряд
m1: ...
exit:
mov ax,4c00h
;стандартный выход
int 21h
end
main
При сложении
чисел со знаком может произойти особая ситуация
2) результат выходит
из диапазона допустимых значений
Переполнение
регистрируется с помощью флага переполнения of.
Дополнительно к флагу
of при переносе из старшего разряда устанавливается в 1 и флаг переноса cf.
Проанализировать флаг
of можно командами условного перехода
jo \ jno .
учет особых ситуаций
должен производиться самим программистом. !!!
Команды вычитания
К командам вычитания
относятся следующие:
•sub
операнд_1,операнд_2 — команда вычитания; ее принцип действия:
операнд_1 = операнд_1 –
операнд_2
•sbb
операнд_1,операнд_2 — команда вычитания с учетом заема (флага cf ):
(subtract with borrow -вычитание с
заемом)
операнд_1 = операнд_1 – операнд_2 – значение_cf
флаг cf выполняет роль
индикатора заема 1 из старшего разряда при вычитании чисел.
!!!
Таким образом, после команды вычитания чисел без знака нужно анализировать
состояние флага cf. Если он установлен в 1, то это говорит о том, что произошел
заем из старшего разряда и результат получился в дополнительном коде.
•dec операнд —
операция декремента, то есть уменьшения значения операнда на 1;
Пример 4.
Проверка при вычитании чисел без знака
masm
model small
stack 256
.data
.code ;сегмент кода
main: ;точка входа в программу
...
xor ax,ax
mov al,5
sub al,10 ; вычитание - результат получается в
дополнительном коде (отрицательный)
jnc m1 ;нет переноса?
neg al ;в al модуль результата
m1: ...
exit:
mov ax,4c00h ;стандартный выход
int 21h
end main ;конец программы
Для того чтобы
преобразовать результат к нормальному виду (получить его модуль), применяется
команда neg, с помощью которой получается дополнение операнда. В нашем случае
мы получили дополнение дополнения или модуль отрицательного результата. А тот
факт, что это на самом деле число отрицательное, отражен в состоянии флага cf.
Дальше все зависит от алгоритма обработки.
neg операнд —
отрицание с дополнением до двух.
Команда выполняет
инвертирование значения операнд.
операнд = 0 – операнд, то есть вычитает операнд из нуля.
Команду neg операнд
можно применять:
•для смены знака;
•для выполнения
вычитания из константы.
Дело в том, что
команды sub и sbb не позволяют вычесть что-либо из константы, так как константа
не может служить операндом-приемником в этих операциях. Поэтому данную операцию
можно выполнить с помощью двух команд:
neg ax ;смена знака (ax)
...
add ax,340 ;фактически вычитание:
(ax)=340-(ax)
Советы
1. Сложение
и вычитание знаковых и беззнаковых чисел проводятся по одним и тем же
алгоритмам. ПК не знает какие числа (знаковые или беззнаковые) он складывает и
вычитает, поэтому фиксирует в флагах CF OF особенности операций. Какие числа
обрабатываются знает программист. Если предполагается, что работа идет с
беззнаковыми числами, необходимо производить анализ флага CF, а OF не
надо. Если предполагается, что работа идет со знаковыми числами, необходимо
производить анализ флага ОF,
а СF
не надо.
2. Команды
INC DEC
занимают только один байт и работают быстрее, чем команды ADD SUB,
занимающие три байта.
3. Команды
ADD SUB устанавливают флажок
переноса, а INC DEC нет
4. Кроме
флагов cf и of в регистре eflags есть еще несколько флагов, которые можно
использовать с двоичными арифметическими командами:
•zf
— флаг нуля, который устанавливается в 1, если результат операции равен 0, и в
1, если результат не равен 0;
•sf
— флаг знака, значение которого после арифметических операций (и не только)
совпадает со значением старшего бита результата, то есть с битом 7, 15 или 31.
Таким образом, этот флаг можно использовать для операций над числами со знаком.
Что делать, если
размеры операндов, участвующих в арифметических операциях, разные?
Например, предположим,
что в операции сложения один операнд является словом, а другой занимает двойное
слово. Выше сказано, что в операции сложения должны участвовать операнды одного
формата. Если числа без знака, то выход найти просто. В этом случае можно на
базе исходного операнда сформировать новый (формата двойного слова), старшие
разряды которого просто заполнить нулями. Сложнее ситуация для чисел со знаком:
как динамически, в ходе выполнения программы, учесть знак операнда? Для решения
подобных проблем в системе команд микропроцессора есть так называемые команды
преобразования типа. Эти команды расширяют байты в слова, слова — в двойные
слова и двойные слова — в учетверенные слова (64-разрядные значения). Команды
преобразования типа особенно полезны при преобразовании целых со знаком, так
как они автоматически заполняют старшие биты вновь формируемого операнда
значениями знакового бита старого объекта. Эта операция приводит к целым
значениям того же знака и той же величины, что и исходная, но уже в более
длинном формате. Подобное преобразование называется операцией распространения
знака.
Существуют два вида
команд преобразования типа:
1. Команды без операндов — эти команды работают
с фиксированными регистрами:
· cbw
(Convert Byte to Word) — команда преобразования байта (в регистре al) в слово
(в регистре ax) путем распространения значения старшего бита al на все биты
регистра ah;
· cwd
(Convert Word to Double) — команда преобразования слова (в регистре ax) в
двойное слово (в регистрах dx:ax) путем распространения значения старшего бита
ax на все биты регистра dx;
· cwde
(Convert Word to Double) — команда преобразования слова (в регистре ax) в
двойное слово (в регистре eax) путем распространения значения старшего бита ax
на все биты старшей половины регистра eax;
· cdq
(Convert Double Word to Quarter Word) — команда преобразования двойного слова
(в регистре eax) в учетверенное слово (в регистрах edx:eax) путем
распространения значения старшего бита eax на все биты регистра edx.
2. Команды movsx и
movzx, относящиеся к командам обработки строк. Эти команды обладают полезным
свойством в контексте нашей проблемы:
•movsx
операнд_1,операнд_2 — переслать с распространением знака. Расширяет 8 или
16-разрядное значение операнд_2, которое может быть регистром или операндом в
памяти, до 16 или 32-разрядного значения в одном из регистров, используя
значение знакового бита для заполнения старших позиций операнд_1. Данную
команду удобно использовать для подготовки операндов со знаками к выполнению
арифметических действий;
•movzx
операнд_1,операнд_2 — переслать с расширением нулем. Расширяет 8 или
16-разрядное значение операнд_2 до 16 или 32-разрядного с очисткой
(заполнением) нулями старших позиций операнд_2. Данную команду удобно
использовать для подготовки операндов без знака к выполнению арифметических
действий.
Пример
mov dh,-3 ;
(dh) = FD
mov
al,dh ; (al) = FD
cbw ; (ax) = FF FD
mov
dx,ax ; (dx) = FF FD = -3
Пример mov ax,
10004 (ax)=2714
cwd (dx) 0000
(ax) 2714
Умножение
и деление
Умножение
чисел без знака
Для умножения чисел
без знака предназначена команда
mul сомножитель_1
Как видите, в команде
указан всего лишь один операнд-сомножитель. Второй операнд — сомножитель_2
задан неявно. Его местоположение фиксировано и зависит от размера сомножителей.
Так как в общем случае результат умножения больше, чем любой из его сомножителей,
то его размер и местоположение должны быть тоже определены однозначно. Варианты
размеров сомножителей и размещения второго операнда и результата приведены в
табл. 2.
Таблица 2.
Расположение операндов и результата при умножении
сомножитель_1 сомножитель_2 Результат
Байт al 16 бит в ax: al —
младшая часть результата;
ah — старшая часть результата
Слово ax 32 бит в паре dx:ax: ax — младшая часть
результата;
dx — старшая часть результата
Двойное слово eax
64 бит в паре
edx:eax: eax — младшая часть результата;
edx — старшая часть результата
Из таблицы видно, что
произведение состоит из двух частей и в зависимости от размера операндов
размещается в двух местах — на месте сомножитель_2 (младшая часть) и в
дополнительном регистре ah, dx, edx (старшая часть). Как же динамически (то
есть во время выполнения программы) узнать, что результат достаточно мал и
уместился в одном регистре или что он превысил размерность регистра и старшая
часть оказалась в другом регистре? Для этого привлекаются уже известные нам по
предыдущему обсуждению флаги переноса cf и переполнения of:
•если старшая часть
результата нулевая, то после операции произведения флаги
cf = 0 и of = 0;
•если же эти флаги
ненулевые, то это означает, что результат вышел за пределы младшей части
произведения и состоит из двух частей, что и нужно учитывать при дальнейшей работе.
Рассмотрим следующий
пример программы.
Пример 5. Умножение
masm
model small
stack 256
.data ;сегмент данных
rez label word
rez_l db 45
rez_h db 0
.code ;сегмент кода
main: ;точка входа в программу
...
xor ax,ax
mov al,25
mul rez_l
jnc m1 ;если переполнение, то на м1
mov rez_h,ah ;старшую часть результата в rez_h
m1: mov rez_l,al
exit:
mov ax,4c00h ;стандартный выход
int 21h
end
main ;конец
программы
В этой программе в
строке 14 производится умножение значения в rez_l на число в регистре al.
Согласно информации в табл. 2, результат умножения будет располагаться в
регистре al (младшая часть) и регистре ah (старшая часть). Для выяснения
размера результата в строке 15 командой условного перехода jnc анализируется
состояние флага cf и если оно не равно 1, то результат остался в рамках
регистра al. Если же cf = 1, то выполняется команда в строке 16, которая
формирует в поле rez_h старшее слово результата. Команда в строке 18 формирует
младшую часть результата. Теперь обратите внимание на сегмент данных, а именно,
на строку 6. В этой строке содержится директива label. Мы еще не раз будем
сталкиваться с этой директивой. В данном случае она назначает еще одно
символическое имя rez адресу, на который уже указывает другой идентификатор
rez_l. Отличие заключается в типах этих идентификаторов — имя rez имеет тип слова,
который ему назначается директивой label (имя типа указано в качестве операнда
label). Введя эту директиву в программе, мы подготовились к тому, что,
возможно, результат операции умножения будет занимать слово в памяти. Обратите
внимание, что мы не нарушили принципа: младший байт по младшему адресу. Далее,
используя имя rez, можно обращаться к значению в этой области как к
слову.
Умножение чисел со
знаком
Для умножения чисел со
знаком предназначена команда
imul
операнд_1[,операнд_2,операнд_3]
Эта команда
выполняется так же, как и команда mul. Отличительной особенностью команды imul
является только формирование знака.
Если результат мал и умещается в одном регистре (то есть если cf = of =
0), то содержимое другого регистра (старшей части) является расширением знака —
все его биты равны старшему биту (знаковому разряду) младшей части результата.
В противном
случае (если cf = of = 1) знаком результата является знаковый бит старшей части
результата, а знаковый бит младшей части является значащим битом двоичного кода
результата.
Если вы
посмотрите описание команды imul, то увидите, что она допускает более широкие
возможности по заданию местоположения операндов. Это сделано для удобства
использования.
Деление чисел без
знака
Для деления чисел без
знака предназначена команда
div делитель
Делитель может
находиться в памяти или в регистре и иметь размер 8, 16 или 32 бит.
Местонахождение делимого фиксировано и так же, как в команде умножения, зависит
от размера операндов. Результатом команды деления являются значения частного и
остатка.
Варианты
местоположения и размеров операндов операции деления показаны в табл. 3.
Таблица 3.
Расположение операндов и результата при делении
Делимое Делитель Частное Остаток
16 бит Байт - регистр Байт
в регистре ax или ячейка памяти в регистре al Байт в регистре ah
32 бит 16 бит
dx — старшая
часть регистр Слово 16 бит Слово 16 бит
ax — младшая
часть или ячейка памяти регистре ax в регистре dx
64 бит Двойное слово Двойное
слово Двойное слово
edx — старшая часть 32
бит 32 бит
32 бит
eax — младшая часть регистр
или в регистре eax в регистре
edx
ячейка
памяти
После выполнения
команды деления содержимое флагов неопределенно, но возможно возникновение
прерывания с номером 0, называемого “деление на ноль”. Этот вид прерывания
относится к так называемым исключениям. Эта разновидность прерываний возникает
внутри микропроцессора из-за некоторых аномалий во время вычислительного
процесса. Прерывание 0, “деление на ноль”, при выполнении команды div может
возникнуть по одной из следующих причин:
•делитель равен нулю;
•частное не входит в
отведенную под него разрядную сетку, что может случиться в следующих случаях:
•при делении делимого
величиной в слово на делитель величиной в байт, причем значение делимого в
более чем 256 раз больше значения делителя;
•при делении делимого
величиной в двойное слово на делитель величиной в слово, причем значение
делимого в более чем 65 536 раз больше значения делителя;
•при делении делимого
величиной в учетверенное слово на делитель величиной в двойное слово, причем
значение делимого в более чем 4 294 967 296 раз больше значения делителя.
Пример. Выполним
деление значения в области del на значение в области delt.
Пример 6. Деление
чисел
<1> ;prg_8.6.asm
<2> masm
<3> model small
<4> stack 256
<5> .data
<6> del_b label
byte
<7> deldw 29876
<8> delt db 45
<9> .code ;сегмент кода
<10> main: ;точка входа в программу
<11> ...
<12> xor ax,ax
<13>
;последующие две команды можно заменить одной
mov ax,del
<14> mov
ah,del_b ;старший байт
делимого в ah
<15> mov
al,del_b+1 ;младший байт
делимого в al
<16> div
delt ;в al —
частное, в ah — остаток
<17> ...
<18> end main ;конец программы
Деление чисел со
знаком
Для деления чисел со
знаком предназначена команда
idiv делитель
Для этой команды
справедливы все рассмотренные положения, касающиеся команд и чисел со знаком.
Отметим лишь особенности возникновения исключения 0, “деление на ноль”, в
случае чисел со знаком. Оно возникает при выполнении команды idiv по одной из
следующих причин:
•делитель равен нулю;
•частное не входит в
отведенную для него разрядную сетку.
Последнее в свою
очередь может произойти:
•при делении делимого
величиной в слово со знаком на делитель величиной в байт со знаком, причем
значение делимого в более чем 128 раз больше значения делителя (таким образом,
частное не должно находиться вне диапазона от –128 до +127);
•при делении делимого
величиной в двойное слово со знаком на делитель величиной в слово со знаком,
причем значение делимого в более чем 32 768 раз больше значения делителя (таким
образом, частное не должно находиться вне диапазона от –32 768 до +32 768);
•при делении делимого
величиной в учетверенное слово со знаком на делитель величиной в двойное слово
со знаком, причем значение делимого в более чем 2 147 483 648 раз больше
значения делителя (таким образом, частное не должно находиться вне диапазона от
–2 147 483 648 до +2 147 483 647).
К примеру, вычислим
значение y = (a + b)/c, где a, b, c — байтовые знаковые переменные
Пример 7. Вычисление
простого выражения
<1> ;prg_8_9.asm
<2> masm
<3> model small
<4> stack 256
<5> .data
<6> a db ?
<7> b db ?
<8> c db ?
<9> y dw 0
<10> .code
<11> main: ;точка входа в
программу
<12> ...
<13> xor ax,ax
<14> mov al,a
<15> cbw
<16> movsx bx,b
<17> add ax,bx
<18> idiv c ;в al — частное, в ah
— остаток
<19> exit:
<20> mov
ax,4c00h ;стандартный выход
<21> int 21h
<22> end
main ;конец программы
В этой программе
делимое для команды idiv (строка 17) готовится заранее. Так как делитель имеет
размер байта, то делимое должно быть словом. С учетом этого сложение
осуществляется параллельно с преобразованием размера результата в слово (строки
13–16). Для примера расширение операндов со знаком производится двумя разными
командами — cbw и movsx.