Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

epd627

.pdf
Скачиваний:
22
Добавлен:
02.05.2015
Размер:
816.97 Кб
Скачать

первая инструкция jmp представляет собой переход дальнего типа (загружаются оба регистра cs и ip), так как это переход на метку типа far, а второй переход имеет ближний тип (загружается только регистр ip), так как это переход на метку типа near. Заметим, что обе метки FarLabel и NearLabel описывают один и тот же адрес (адрес инструкции mov), но позволяют переходить на него различными способами.

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

4.56. Перемещение данных

Данные в процессоре 8086 перемещаются с помощью инструкции mov, которая записывает копию операнда-источника в операнд-приемник. Например, инструкции:

mov

ax,0

; ax := 0

mov

bx,9

; bx := 9

mov

ax,bx

; ax := bx = 9

сначала записывают в регистр ax константу 0, затем в регистр bx записывается константа 9, и, наконец, содержимое bx копируется в ax.

Значение 9 не перемещается, а копируется из bx в ax.

Винструкции mov можно использовать почти любую пару операндов, что имеет смысл, за исключением того случая, когда в качестве операнда используется сегментный регистр.

Вкачестве операнда-источника (правого операнда) инструкции mov можно использовать:

константу;

выражение, при вычислении которого получается константа;

общий регистр;

ячейку памяти, доступную с помощью одного из режимов адресации.

Вкачестве операнда-приемника (левого операнда) инструкции mov может использоваться:

общий регистр;

ячейка памяти.

Кроме того, в качестве операнда-приемника этой инструкции можно использовать сегментный регистр, но в этом случае операндом-источником может быть только общий регистр!

4.57. Арифметические операции

Процессор 8086 не может выполнять арифметические операции с плавающей точкой (действия с такими числами, как 5.2 и 1.03Е17), не

61

говоря уже о тригонометрических функциях. Эти операции может выполнять арифметический сопроцессор 8087, но если он отсутствует, процессор 8086 выполняет программно-арифметические операции (последовательности инструкций сдвигов, сложений и проверок) и выполняет существенно медленнее.

Процессор 8086 может выполнять 8- и 16-битовое сложение, вычитание, умножение и деление чисел со знаком и без знака и имеет специальные быстрые инструкции для увеличения и уменьшения операндов. В процессоре 8086 предусмотрена также поддержка операций сложения и вычитания значений, превышающих 16 битов, хотя для операций с такими значениями требуется выполнить не одну, а несколько инструкций.

4.58.Арифметические операции над целыми двоичными числами

Карифметическим операциям над целыми двоичными числами относятся:

1) сложение двоичных чисел без знака;

2) вычитание двоичных чисел без знака;

3) умножение чисел без знака;

4) деление чисел без знака;

5) вычитание и сложение операндов большой размерности;

6) сложение двоичных чисел со знаком;

7) вычитание двоичных чисел со знаком;

8) умножение чисел со знаком;

9) деление чисел со знаком.

Здесь рассматриваются первые пять операций: четыре основных арифметических действия для двоичных чисел без знака и вычитание и сложение операндов большой размерности. Остальные операции (для двоичных чисел со знаком) будут рассматриваться в уч.пос., ч.4

4.59. Сложение целых двоичных чисел без знака

Микропроцессор выполняет сложение операндов по правилам сложения двоичных чисел. Проблем не возникает до тех пор, пока значение результата не превышает размерности поля операнда. Например, при сложении операндов размером в байт результат не должен превышать число 255. Если это происходит, то результат оказывается неверным. Рассмотрим, почему так происходит.

К примеру, выполним сложение: 254 + 5 = 259 в двоичном виде: 11111110 + 0000101 = 1 00000011. Результат вышел за пределы восьми битов и правильное его значение укладывается в 9 битов, а в 8-битовом поле операнда осталось значение 00000011, что, конечно, неверно.

62

Вмикропроцессоре этот исход сложения прогнозируется и предусмотрены специальные средства для фиксирования подобных ситуаций и их обработки. Так, для фиксирования ситуации выхода за разрядную сетку результата, как в данном случае, предназначен флаг переноса cf. Он располагается в бите 0 регистра флагов flags. Именно установкой этого флага фиксируется факт переноса единицы из старшего разряда операнда.

Естественно, что программист должен предусматривать возможность такого исхода операции сложения и средства для корректировки. Это предполагает включение участков кода после операции сложения, в которых анализируется флаг cf. Анализ этого флага можно провести различными способами. Самый простой и доступный — использовать команду условного перехода jcc. Эта команда в качестве операнда имеет имя метки в текущем сегменте кода. Переход на эту метку осуществляется

вслучае, если в результате работы предыдущей команды флаг cf установился в 1.

Всистеме команд микропроцессора имеются три команды двоичного сложения:

inc операнд ; операнд := операнд+1;

add операнд_1,операнд_2 ;операнд_1 := операнд_1 + операнд_2; adc операнд_1,операнд_2 ; сложение с учетом флага переноса cf.

;операнд_1 = операнд_1 + операнд_2 + значение_cf

Инструкция add выполняет сложение операнда-источника (правого операнда) с содержимым операнда-приемника и записывает результат в операнд-приемник.

Например, инструкции: datasg segment para ‘Data’ BaseVal dw 99

Adjustdw 10 datasg ends

codesg segment para ‘Code’

. . .

mov

dx,BaseVal

add

dx,11

sub

dx,Adjust

. . .

codesg ends

сначала загружают значение, записанное в BaseVal, в регистр dx, затем прибавляют к нему константу 11 (в результате в dx получается значение 110) и, наконец, вычитают из dx значение 10, записанное в переменной Adjust. Полученное в результате значение 100 сохраняется в регистре dx.

63

Рассмотрим пример вычисления суммы чисел stacksg segment para ‘Stack’

db 256 dup (0) stacksg ends

datasg segment para ‘Data’ a db 254

datasg ends

codesg segment para ‘Code’ ;сегмент кода main:

mov ax,datasg mov ds,ax

. . .

xor ax,ax add al,17 add al,a

jnc m1 ;если нет переноса, то перейти на m1 adc ah,0 ax сумма с учетом переноса

m1:

. . .

exit:

mov ax,4c00h ;стандартный выход int 21h

codesg ends

end main ;конец программы

В примере создана ситуация, когда результат сложения выходит за границы операнда. Эта возможность учитывается командой jnc, которая проверяет состояние флага cf (хотя можно было обойтись и без нее, всегда прибавляя значение флага, которое после операции add может быть равно как нулю, так и единице). Если он установлен в 1, то это признак того, что результат операции получился больше по размеру, чем размер операнда, и для его корректировки необходимо выполнить некоторые действия. В данном случае мы просто полагаем, что границы операнда расширяются до размера ax, для чего учитываем перенос в старший разряд командой adc.

Последняя команда (adc) — это команда сложения, учитывающая перенос единицы из старшего разряда. Механизм появления такой единицы рассматривался выше. Таким образом, команда adc является средством микропроцессора для сложения длинных двоичных чисел, размерность которых превосходит поддерживаемые микропроцессором длины полей.

Команды сложения чисел со знаком те же, что и для чисел без знака (подробнее см. уч.пос., ч.4).

64

4.60. Вычитание целых двоичных чисел без знака

Инструкция sub вычитает операнд-источник из операнда-приемника. Если уменьшаемое больше вычитаемого, то проблем нет, — разность положительна, результат верен. Если уменьшаемое меньше вычитаемого, возникает проблема: результат меньше 0, а это уже число со знаком. В этом случае результат необходимо завернуть. Что это означает? При обычном вычитании (в столбик) делают заем 1 из старшего разряда. Микропроцессор поступает аналогично, то есть занимает 1 из разряда, следующего за старшим, в разрядной сетке операнда. Поясним на примере.

05 =

00000000 00000101

--

10 = 00000000 00001010

Для того чтобы произвести вычитание, произведем воображаемый заем из старшего разряда:

1 00000000 00000101

00000000 00001010

11111111 11111011

Тем самым по сути выполняется действие

(65 536 + 5) - 10 = 65 531,

0 здесь как бы эквивалентен числу 65 536. Результат, конечно, неверен, но микропроцессор считает, что все нормально, хотя факт заема единицы он фиксирует установкой флага переноса cf. Но посмотрите еще раз внимательно на результат операции вычитания. Это же –5 в дополнительном коде! Если представить разность в виде суммы 5 + (–10).

5 =

00000000 00000101

++

(-10)=

11111111 11110110 ,

11111111 11111011

то получим тот же результат, что и в предыдущем примере.

Таким образом, после команды вычитания чисел без знака нужно анализировать состояние флага cf. Если он установлен в 1, то это говорит о том, что произошел заем из старшего разряда и результат получился в дополнительном коде.

Аналогично командам сложения группа команд вычитания состоит из минимально возможного набора. Эти команды выполняют вычитание по алгоритмам, которые мы сейчас рассматриваем, а учет особых ситуаций должен производиться самим программистом. К командам вычитания относятся следующие:

dec операнд ; операнд := операнд - 1;

sub операнд_1,операнд_2 ; операнд_1 := операнд_1 – операнд_2;

65

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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]