sbb операнд_1,операнд_2 ; вычитание с учетом заема (флаг cf ):
;операнд_1 := операнд_1 – операнд_2 – значение_cf
Таким образом, среди команд вычитания есть команда sbb, учитывающая флаг переноса cf. Эта команда подобна adc, но теперь уже флаг cf выполняет роль индикатора заема 1 из старшего разряда при вычитании чисел.
Рассмотрим пример проверки заема при вычитании чисел без знака codesg segment para ‘Code’ ;сегмент кода
main: |
;точка входа в программу |
. . . |
|
xor ax,ax |
|
mov al,5 |
|
sub al,10 |
|
jnc m1 |
;нет переноса? |
neg al |
;в al модуль результата |
m1: |
|
. . . |
|
exit: |
|
mov ax,4c00h |
;стандартный выход |
int 21h |
|
codesg ends |
|
end main |
;конец программы |
В этом примере командой sub al,10 выполняется вычитание. С исходными данными, указанными для этой команды вычитания, результат получается в дополнительном коде (отрицательный). Чтобы получить модуль отрицательного результата, применяется команда neg. Тот факт, что на самом деле число отрицательное, отражен в состоянии флага cf.
4.61. Тридцатидвухразрядные операнды
Операции add и sub работают с 8- или 16-битовыми операндами. Чтобы сложить или вычесть 32-разрядные операнды, надо разбить
операцию на ряд операций со значениями размером в слово и использовать инструкции adc и sbb.
Когда складываются два операнда, флаг переноса cf показывает, был ли выполнен перенос из приемника. Сложение двух шестнадцатеричных значений:
f f f f +0001 10000
Младшее слово результата равно нулю, перенос равен 1, поскольку результат (10000h) не вмещается в 16 битов.
66
Инструкция adc аналогична инструкции add, но в ней учитывается флаг переноса, предварительно установленный предыдущим сложением. Когда складываются два значения, превышающие по размеру слово, то младшие (менее значащие) слова нужно сложить с помощью инструкции add, а остальные слова этих значений – с помощью одной или нескольких инструкций adc. Например, следующие инструкции складывают значение в регистрах cx:bx размером в двойное слово, со значением, записанным в регистрах dx:ax:
add |
ax,bx |
; ax:= ax + bx |
adc |
dx,cx |
; dx := dx+ cx+<значение cf>, |
а в следующей группе инструкций выполняется сложение учетверенного слова в переменной DoubleLong1 с учетверенным словом в переменной
DoubleLong2:
mov |
ax, word ptr DoubleLong1 |
|
mov |
bx,word ptr DoubleLong1+2 |
|
mov |
cx,word ptr DoubleLong1+4 |
; (dx:cx:bx:ax) := учетверенное |
mov |
dx,word ptr DoubleLong1+6 |
; слово DoubleLong1 |
add |
ax,word ptr DoubleLong2 |
|
adc |
bx,word ptr DoubleLong2+2 |
|
adc |
cx,word ptr DoubleLong1+4; (dx:cx:bx:ax) := |
|
adc |
dx,word ptr DoubleLong2+6; |
:= DoubleLong1 + DoubleLong2 |
Инструкция sbb работает по тому же принципу, что и инструкция adc. Когда инструкция sbb выполняет вычитание, в ней учитывается заем, произошедший в предыдущем вычитании. Например, следующие инструкции вычитают значение, записанное в регистрах cx:bx, из значения размером в двойное слово, записанного в регистрах dx:ax:
sub |
ax,bx |
; ax:= ax - bx |
sbb |
dx,cx |
; dx := dx- cx-<значение cf> |
4.62. Технология сложения и вычитания длинных чисел
Команды сложения и вычитания работают с операндами фиксированной размерности: 8 и 16 битов (процессор 8086) или 8, 16 и 32 бита (процессор 80386). А что делать, если нужно сложить числа большей размерности, например 48 или 64 бита, используя 16-разрядные операнды?
Процесс сложения многобайтных чисел происходит так же, как и при сложении двух чисел “в столбик”, — с осуществлением, при необходимости, переноса 1 в старший разряд. Т.к. удается запрограммировать этот процесс, то значительно расширяется диапазон двоичных чисел, над которыми можно выполнять операции сложения и вычитания.
67
Принцип вычитания чисел с диапазоном представления, превышающим стандартные разрядные сетки операндов, тот же, что и при сложении, то есть используется флаг переноса cf. Нужно только представлять себе процесс вычитания в столбик и правильно комбинировать команды микропроцессора с командой sbb.
В завершение обсуждения команд сложения и вычитания отметим, что кроме флагов cf и of в регистре flags есть еще несколько флагов, которые можно использовать с двоичными арифметическими командами. Речь идет о следующих флагах:
zf — флаг нуля, который устанавливается в 1, если результат операции равен нулю, и в 0, если результат не равен нулю;
sf — флаг знака, значение которого после арифметических операций (и не только) совпадает со значением старшего бита результата, то есть с битом 7, 15 или 31. Таким образом, этот флаг можно использовать для операций над числами со знаком.
4.63. Увеличение и уменьшение на единицу
Иногда в программе на ассемблере требуется выполнить сложение, которое состоит просто в прибавлении к операнду значения 1. Например, заполним 10-байтовый массив TempArray числами 0, 1, 2, 3, 4, 5, 6, 7, 8, 9:
datasg segment para ‘Data’
TempArray db |
10 dup (?) |
|
FillCount dw ? |
|
|
datasg ends |
|
|
codesg segment para ‘Code’ |
||
. . . |
|
|
mov |
al,0 |
; первое значение, записываемое в TempArray |
mov |
bx,offset TempArray ; bx указывает на TempArray |
|
mov FillCount,10 ; число эл-тов, которыми нужно заполнить массив
FillTempArrayLoop:
mov |
[bx],al |
; установить текущий элемент TempArray |
inc |
bx |
; ссылка на следующий элемент массива TempArray |
inc |
al |
; следующее записываемое значение |
dec |
FillCount |
; уменьшить счетчик числа заполняемых эл-тов |
jnz FillTempArrayLoop ;обработать следующий эл-нт, если не все
. . .
codesg ends
Так как инструкция add занимает три байта, а инструкция inc занимает только один байт и выполняется быстрее, чем инструкция add, то предпочтительнее использовать инструкцию:
inc bx |
; bx:= bx+1, |
|
68 |
а не инструкцию: |
|
add bx,1 |
; bx:= bx+1 |
Фактически более экономно выполнить две операции inc, чем прибавить к регистру размером в слово значение 2.
Аналогично иногда из содержимого регистров и переменных в памяти нужно вычесть значение 1. Тогда применяется инструкция dec.
Инструкции inc и dec – это наиболее эффективные инструкции, с помощью которых можно увеличивать и уменьшать значения переменных в памяти и регистров. Их следует использовать там, где это возможно.
4.64. Умножение целых чисел без знака
Процессор 8086 может выполнять следующие типы операций умножения: знаковое и беззнаковое (8-разрядное и 16-разрядное).
Инструкция mul перемножает беззнаковые сомножители.
При 8-битовом (8-разрядном) умножении один из операндов должен храниться в регистре al, а другой может представлять собой любой 8- битовый общий регистр или переменную памяти соответствующего размера. Инструкция mul всегда сохраняет 16-битовое произведение в регистре ax. Например, во фрагменте программы:
mov al,25 mov dh,40 mul dh
al умножается на dh, а результат (1000) помещается в регистр ax. Другой вариант использования этой инструкции, когда один из сомножителей хранится в ячейке памяти (например, в сегменте данных переменная b1 определена как байт: b1 db 40):
mov al,25
mul b1 ; ax :=al*b1=25*40=1000
В инструкции mul требуется указывать только один операнд, другой сомножитель всегда хранится в регистре al (или в регистре ax в случае перемножения 16-битовых сомножителей).
Инструкция перемножения 16-битовых сомножителей работает аналогично. Один из сомножителей должен храниться в регистре ax, а другой может находиться в 16-разрядном общем регистре или в переменной памяти. 32-битовое произведение инструкция mul помещает в этом случае в регистры dx:ax, при этом младшие (менее значащие) 16 битов произведения записываются в регистр ax, а старшие (более значащие) 16 битов – в регистр dx. Например, инструкции
mov ax,1000
mul ax ; (dx:ax):=ax*ax
69
загружают в регистр ax значение 1000, а затем возводят его в квадрат, помещая результат (значение 1 000 000) в регистры dx:ax.
Таким образом, для умножения чисел без знака предназначена команда
mul сомножитель_1
Как видите, в команде указан всего лишь один операнд-сомножитель. Второй операнд — сомножитель_2 задан неявно. Его местоположение фиксировано и зависит от размера сомножителей. Так как в общем случае результат умножения больше, чем любой из его сомножителей, то его размер и местоположение должны быть тоже определены однозначно. Варианты размеров сомножителей и размещения второго операнда и результата умножения для микропроцессора 8086 приведены в табл. 4.4.
Таблица 4.4
Сомножитель_1 |
Сомножитель_2 |
Результат |
Байт |
al |
16 битов в ax: |
|
|
al — младшая часть результата; |
|
|
ah — старшая часть результата |
Слово |
ax |
32 бита в паре dx:ax: |
|
|
ax — младшая часть результата; |
|
|
dx — старшая часть результата |
Из таблицы видно, что произведение состоит из двух частей и в зависимости от размера операндов размещается в двух местах — на месте сомножитель_2 (младшая часть) и в дополнительном регистре ah, dx (старшая часть). Как же динамически (то есть во время выполнения программы) узнать, что результат достаточно мал и уместился в одном регистре или что он превысил размерность регистра и старшая часть оказалась в другом регистре? Для этого привлекаются уже известные нам флаги переноса cf и переполнения of:
–если старшая часть результата нулевая, то после операции произведения флаги cf = 0 и of = 0;
–если же эти флаги ненулевые, то это означает, что результат вышел за пределы младшей части произведения и состоит из двух частей, что и нужно учитывать при дальнейшей работе. Пример программы умножения:
stacksg segment para ‘Stack’ db 256 dup (0)
stacksg ends
datasg segment para ‘Data’ |
;сегмент данных |
|
rez label word |
|
|
rez_ldb 45 |
|
|
rez_h |
db 0 |
|
datasg ends |
|
|
codesg segment para ‘Code’ |
;сегмент кода |
|
|
|
70 |