1117
.pdfmain: |
;точка входа в программу |
. . . |
|
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