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

1117

.pdf
Скачиваний:
2
Добавлен:
07.01.2021
Размер:
880.85 Кб
Скачать

main:

;точка входа в программу

. . .

 

xor ax,ax

 

mov al,25

 

mul rez_l

 

jnc m1

;если переполнение, то на m1

mov rez_h,ah

;старшую часть результата в rez_h

m1:

 

mov rez_l,al

 

exit:

 

mov ax,4c00h

;стандартный выход

int 21h

 

end main

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

Здесь производится умножение значения в rez_l на число в регистре al. Согласно табл. 4.4, результат умножения будет располагаться в регистре al (младшая часть) и регистре ah (старшая часть). Для выяснения размера результата командой условного перехода jnc анализируется состояние флага cf, и если оно не равно 1, то результат остался в рамках регистра al. Если же cf = 1, то выполняется команда, которая формирует в поле rez_h старшее слово результата. Команда mov rez_l,al формирует младшую часть результата. Теперь обратите внимание на строку с директивой label в сегменте данных. Мы еще не раз будем сталкиваться с этой директивой. В данном случае она назначает еще одно символическое имя rez адресу, на который уже указывает другой идентификатор rez_l. Отличие заключается в типах этих идентификаторов — имя rez имеет тип слова, который ему назначается директивой label (имя типа указано в качестве операнда label). Введя эту директиву в программу, мы подготовились к тому, что, возможно, результат операции умножения будет занимать слово в памяти. Обратите внимание, что мы не нарушили принципа: младший байт по младшему адресу. Далее, используя имя rez, можно обращаться к значению в этой области как к слову.

Примечание. Для умножения сомножителей со знаком используется инструкция imul (см. уч. пос., ч.4).

4.65. Деление целых чисел без знака

Процессор 8086 может выполнять отдельные типы операций деления:

16-битового значения на 8-битовое (знаковое и беззнаковое);

32-битового операнда на 16-битовый операнд (знаковое и беззнаковое).

71

Здесь рассматриваются только варианты беззнакового деления (инструкция div). Для деления со знаком используется инструкция idiv (см.

уч. пос., ч.4).

В общем виде команда деления чисел без знака может быть представлена следующим образом:

div делитель

Делитель может находиться в памяти или в регистре и иметь размер 8 или 16 битов (для МП 8086) или размер 8, 16 или 32 бита (для МП 80386). Местонахождение делимого фиксировано и так же, как в команде умножения, зависит от размера операнда. Результатом команды деления являются значения частного и остатка.

Варианты местоположения и размеров операндов операции деления и результатов деления (МП 8086) показаны в табл. 4.5.

 

 

 

Таблица 4.5

Делимое

Делитель

Частное

Остаток

Слово (16 битов)

Байт

Байт

Байт

в регистре ax

в регистре или ячейке

в регистре al

в регистре ah

 

памяти

 

 

Двойное слово (32 бита)

Слово (16 битов)

Слово (16 битов)

Слово (16 битов)

dx — старшая часть

в регистре или ячейке

в регистре ax

в регистре dx

ax — младшая часть

памяти

 

 

После выполнения команды деления возможно возникновение прерывания с номером 0, называемого “деление на ноль”. Этот вид прерывания относится к так называемым исключениям. Данная разновидность прерываний возникает внутри микропроцессора из-за некоторых аномалий во время вычислительного процесса. Прерывание 0, т.е. “деление на ноль”, при выполнении команды div может возникнуть по одной из следующих причин:

1)делитель равен нулю;

2)частное не входит в отведенную под него разрядную сетку, что может случиться в следующих случаях:

- при делении слова на байт, если значение делимого более чем в 256 раз больше значения делителя;

- при делении двойного слова на слово, если значение делимого более чем в 65 536 раз больше значения делителя;

- при делении учетверенного слова на двойное слово, если значение делимого более чем в 4 294 967 296 раз больше значения делителя.

72

4.66. Беззнаковое деление 16-битового значения на 8-битовое

При беззнаковом делении 16-битового значения на 8-битовое значение делимое должно быть записано в регистре ax. 8-битовый делитель может храниться в любом 8-битовом общем регистре или переменной в памяти соответствующего размера. Инструкция div всегда записывает 8-битовое частное в регистр al, а 8-битовый остаток – в ah. Например:

mov

ax,51

; делимое

mov

dl,10

; делитель

div

dl

; после выполнения деления в al частное, в ah остаток

Заметим,

что

частное представляет собой 8-битовое значение. Это

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

mov

ax,0fffh

mov

bl,1

div

bl

генерируют прерывание по делению на 0 (как можно ожидать, прерывание по делению на 0 генерируется также, если 0 используется в качестве делителя).

Пример деления значения del (20 001) на значение delt (200):

stacksg

segment para ‘Stack’

db 256 dup (0)

stacksg

ends

datasg segment para ‘Data’

del_b

label byte

del

dw 20001

delt

db 200

datasg ends

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

main:

;точка входа в программу

. . .

xor ax,ax

;последующие две команды эквивалентны одной mov ax,del, но копируют слово побайтно

mov ah,del_b

;старший байт делимого в ah

mov al,del_b+1;младший байт делимого в al

div delt

al — частное (здесь 100), в ah — остаток (здесь 1)

. . .

 

end main

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

73

4.67. Беззнаковое деление 32-битового значения на 16-битовое

При делении 32-битового операнда на 16-битовый операнд делимое должно записываться в регистрах dx:ax. 16-битовый делитель может находиться в любом из 16-битовых регистров общего назначения или в переменной в памяти соответствующего размера. Например, в результате выполнения инструкций:

mov ax,2

mov dx,1 ; в (dx:ax) загрузить 10002h mov bx,10h

div bx

частное 1000h (результат деления 10002h на 10h) будет записано в регистре ax, а 2 (остаток от деления) – в регистре dx.

Как и при умножении, при делении имеет значение, используются операнды со знаком или без знака. Для деления беззнаковых операндов используется операция div, а для деления операндов со знаком – idiv (см.

уч. пос., ч.4).

4.68. Изменение знака

Фактически инструкция neg операнд выполняет одно действие: операнд = 0 – операнд, то есть вычитает операнд из нуля.

Команду neg операнд можно применять:

для смены знака;

для выполнения вычитания из константы.

Смена знака. Инструкция neg позволяет изменить (инвертировать) знак содержимого общего регистра или переменной в памяти. Например, после выполнения инструкций:

mov

ax,1 ; установить в регистр ax значение 1

neg

ax ; отрицание ax, он становится равным -1

mov

bx,ax ; скопировать содержимое ax в bx, т.е. bx :=ax=-1

neg

bx ; отрицание bx, он становится равным 1

в регистре ax будет содержаться значение -1, а в регистре bx – значение 1. Вычитание из константы. В связи с тем, что команды sub и sbb не позволяют вычесть что-либо из константы, так как константа не может служить операндом-приемником в этих операциях, данную операцию

выполняют с помощью двух команд:

neg ax

;смена знака содержимого регисра ax (т.е. ax:=-ax)

add ax,340

;фактически вычитание: ax := 340-ax

4.69. Преобразование размеров беззнаковых данных

Что делать, если размеры операндов, участвующих в арифметических операциях, разные? Например, предположим, что в операции сложения

74

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

Аналогично преобразование беззнакового байтового значения в слово заключается просто в обнулении старшего байта слова. Например:

mov cl,12 mov al,cl mov ah,0

преобразуют беззнаковое значение 12 в регистре cl в значение 12 размером в слово в регистре ax.

Обратные преобразования двойного слова в слово или слова в байт тоже возможны (при определенных значениях!) Рассмотрим преобразование слова в байт. Например:

mov ax,5 mov bl,al

Здесь значение 5 размером в слово в регистре ax преобразуется в байтовое значение 5 в регистре bl. Естественно, что преобразуемое значение вместится в байт. Попытка преобразовать в байт значение 100h с помощью инструкций:

mov dx,100h mov al,dl

была бы безуспешной, так как в регистр al был бы записан только младший (нулевой) байт.

Для преобразования байтового значения со знаком в регистре al в слово со знаком существует инструкция cbw. Для преобразования слова со знаком в регистре ax в двойное слово со знаком в регистрах dx: ax (старшее слово содержится в регистре dx) предусмотрена специальная инструкция cwd (см. уч. пос., ч.4). Там же – про cdq (двойное из eax – в учетверенное в edx:eax) и cwde (слово из ax в двойное слово в eax).

4.70. Доступ к сегментным регистрам

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

Вот, например, два способа установки регистра es в значение сегмента данных с именем

datasg segment para ‘Data’ ;сегмент данных

DataSeg dw datasg

75

datasg ends

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

. . .

 

mov

ax,datasg

; первый способ

mov

es,ax

 

. . .

 

mov

ex,[DataSeg]

; второй способ

. . .

codesg ends

Чтобы скопировать содержимое одного сегментного регистра (например, cs) в другой сегментный регистр (например, ds), придется передать значение через регистр общего назначения или стек:

mov

ax,cs

; способ 1 (передача через регистр общего назначения)

mov

ds,ax

; ds := ax = cs

или

 

 

push

cs

; способ 2 (передача через стек)

pop

ds

; ds := cs

копируют содержимое регистра в ds. Первый метод работает быстрее, но при втором методе требуется меньший объем кода.

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

4.71. Перемещение данных в стек и из стека

На вершину стека всегда указывает регистр sp. Для обращения к данным в стеке с использованием режимов адресации памяти, при которых указателем базы является регистр bp, можно использовать инструкцию mov. Например, инструкция

mov ax,[bp+4]

загружает регистр ax содержимым слова в сегменте стека со смещением bp+4.

Однако чаще к стеку обращаются с помощью инструкций push и pop. Инструкция push сохраняет операнд в вершине стека, а инструкция pop извлекает значение из вершины стека и сохраняет его в операнде. Например, инструкции:

mov ax,1 push ax pop bx

76

заносят значение (равное 1) из регистра ax в вершину стека, затем извлекают 1 из вершины стека и сохраняют ее в bx.

5.ТИПИЧНЫЕ ОШИБКИ ПРИ ВЫПОЛНЕНИИ РАБОТЫ

5.1.Перечень часто встречающихся ошибок

Вперечень таких ошибок входят:

-открытая процедура или открытый сегмент;

-программист забывает о возврате в DOS;

-программист забывает о стеке или резервирует маленький стек;

-вызывает процедуру, которая портит содержимое нужных регистров;

-неопределенные символические имена;

-повторное определение символического имени;

-неправильный порядок операндов;

-неправильное использование операндов;

-потеря содержимого регистра при умножении;

-не подготовлены регистры при делении;

-неправильное использование регистра после деления;

-потеря содержимого регистра при делении;

-неправильное использование регистров при преобразовании байта в слово и слова в двойное слово в командах cbw и cwd;

-применение команд преобразования cbw и cwd к беззнаковым данным;

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

-программист долго не использует состояние флагов;

-ошибки при использовании регистров:

а) 8- и 16-разрядный регистры для операндов одной команды; б) 8-разрядный регистр указывается для работы со стеком;

в) сегментные регистры прямо используются в арифметических вычислениях, логических операциях или для непосредственной передачи данных;

г) сегментные регистры могут использоваться либо как источники операндов, либо как приемники операндов, но никак не одновременно;

- выход из диапазона адресов.

В каждом языке имеется свое множество ошибок, которые обычно очень легко сделать, но не всегда просто обнаружить. Не является исключением и язык ассемблера. Типичные ошибки, которые допускаются при программировании на ассемблере, и рекомендации, как можно их избежать, приведены ниже. Источники данной информации: [1], [4], [5], [6].

77

5.2. Открытая процедура или открытый сегмент [5, с.120-121]

Для каждой директивы proc должна быть записана соответствующая директива endp. Если директива endp пропущена или имя процедуры в записи endp записано неверно, ассемблер генерирует предупреждающее сообщение, что процедура не закрыта надлежащим образом. Точно так же директива ends должна быть задана для каждой директивы segment. Если утверждение ends пропущено или имя сегмента в нем указано неверно, генерируется предупреждающее сообщение, что сегмент открыт. Наиболее вероятной причиной ошибки является неверная запись имени сегмента или процедуры.

sample segment

; Неверные записи

proc1proc near

 

and al,00

; а) нет соответствующей директивы endp для

ret

;

процедуры proc1

;

(имя proc1 в записи endp указано неверно)

proc

endp

 

proc2

proc near

; б) нет соответствующей директивы ends

or

al,0ffh

 

ret

 

 

proc2 endp end

Данный вариант является правильной записью предыдущего примера: sample segment

proc1proc near and al,00 ret

proc1 endp proc2 proc near

or al,0ffh ret

proc2 endp sample ends

end

5.3.Программист забывает о возврате в DOS

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

78

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

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

DoNothing proc near nop

DoNothing endp codesg ends

end DoNothing

Выполняемый код, сгенерированный при ассемблировании и компоновке данной программы, состоит только из отдельной инструкции nop. В ассемблере директива endp (как и все другие директивы) не генерирует кода, она просто уведомляет ассемблер, что код для процедуры DoNothing закончился, директива ends просто уведомляет ассемблер об окончании сегмента codesg, а директива end DoNothing – о том, что код данного модуля закончился и программа должна начать выполнение с метки DoNothing. Нигде в выполняемом коде не содержится инструкции для передачи управления обратно в операционную систему DOS, когда программа закончится. В результате, когда программа будет запущена, после инструкции nop будут выполняться инструкции, которые случайно окажутся в памяти непосредственно за nop. В этой точке управление будет потеряно, и для возврата в операционную систему DOS может потребоваться программная или аппаратная перезагрузка. Имеется несколько вариантов возврата в DOS. Правильно завершать работу будет, например, следующая версия предыдущей программы, использующая

функцию 4Ch:

 

 

codesg segment para

‘Code’

;сегмент кода

DoNothing proc near

 

 

nop

 

 

 

mov

ah,4Ch

; функция DOS завершения процесса

int

21h

; вызвать DOS для завершения программы

DoNothing endp

 

 

codesg ends

 

 

end

DoNothing

 

 

5.4.Программист забывает о стеке или резервирует маленький стек

Вбольшинстве программ для резервирования пространства для стека должна присутствовать директива определения сегмента стека (стандартная или упрощенная) и должно быть зарезервировано достаточное пространство, чтобы его хватило для максимальных потребностей в программе (например, директивой db).

79

Ошибки, которые возникают, когда увеличивающийся стек переходит в другие части программы и портит их, обычно бывает трудно воспроизводить и отслеживать. Кроме того, многие отладчики для возврата управления из программы используют небольшое дополнительное пространство в стеке. Поэтому не следует скупиться при выделении пространства для стека. Это избавит вас от многих возможных неприятностей. Обычно выделяют стек размером 512 байтов.

Не требуется выделять стек в программах, которые предполагается преобразовать в файлы типа .com. Файлы .com выполняются со стеком, расположенным в самой вершине программного сегмента (который имеет размер 64 Кб или меньше, если доступно меньше 64 Кб), поэтому максимальный размер стека в этом случае просто равен объему памяти, оставшейся в программном сегменте. При написании программ в формате

.com следует иметь это в виду, так как при увеличении программы соответственно уменьшается стек. Нужно также учитывать, что при работе больших программ в формате .com, выполняемых на компьютерах с небольшой доступной памятью или запущенных из операционной среды DOS наряду с другими программами, могут возникнуть проблемы со стеком. Простейший способ избежать этих потенциальных проблем состоит в написании программ не в формате .com, а в формате .exe с резервированием стека большого объема.

5.5. Вызов подпрограммы, которая портит содержимое нужных регистров

При разработке программы на ассемблере нередко подразумевают, что при обращении к другим процедурам регистры остаются неизмененными. На самом деле это не так. Каждая процедура может сохранить или уничтожить содержимое любого из регистров. Рассмотрим следующий пример. Пусть требуется произвести вычисление по формуле

1000 + w1/10.

 

 

datasg segment para

‘Data’

;сегмент данных

w1 dw 200

 

 

datasg ends

 

 

codesg segment para

‘Code’

;сегмент кода

. . .

 

 

mov

bx,1000

 

; bx :=1000

mov

ax,w1

 

; ax:=w1=200

call DivideBy10

 

; разделить элемент на 10

add bx,ax

 

; вычисление суммы

;(в данном случае получится bx:=bx+ax=10+w1/10), т.е. не правильно

;правильный результат должен быть1000 + w1/10.

80

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