double arrow

Прерывания INT

Циклы

Один из типов программных конструкций, который может быть построен с помощью условных переходов - это цикл. Цикл это ничего более, чем блок кода, который заканчивается условным переходом так, что этот код может выполняться постоянно, до тех пор, пока не выполнится условие окончания.

Циклы используются для обработки массивов, проверки статуса портов ввода-вывода, пока не будет достигнуто определенное состояние, очистки блоков памяти, чтения строк с клавиатуры, вывода строк на экран и т.п. Циклы - это основа для обработки чего-либо, что требует повторяющихся действий. Поэтому они часто используются, и Ассемблер предоставляет несколько специальных инструкций для циклов: LOOP, LOOPE, LOOPNE и JCXZ. В начале посмотрим LOOP. Предположим, что Вы хотите распечатать 17 символов строки TestString. Вы можете сделать это

...

.DATA

TestString DB 'this is a test...'

...

.CODE

...

MOV CX,17

MOV bx,OFFSET TestString

PrINTStringLOOP:

MOV dl,[bx] ;взять следующий символ

INC bx ;указать следующий символ

MOV AH,2 ;# функции DOS для вывода

INT 21h ;вызов DOS для печати символа

DEC CX ;уменьшить счетчик

JNZ PrINTStringLOOP ;делать следующий символ

...

Однако есть лучший способ. Мы отмечали, как Вы помните, что CX особенно полезен в циклах. Здесь

LOOP PrINTStringLOOP

делает то же, что и

DEC CX

JNZ PrINTStringLOOP

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

Если необходим цикл с более сложным условием окончания, чем простое уменьшение счетчика то можно воспользоваться LOOPE и LOOPNE.

LOOPE делает то же самое, что и LOOP за исключением того, что LOOPE будет заканчивать цикл либо, если счетчик в CX достиг 0, либо если флаг нуля установлен в 1. (Вспомним, что флаг нуля устанавливается в 1, если последний арифметический результат был 0, или если 2 операнда последнего сравнения были неравны.) Подобно, LOOPNE будет заканчивать цикл, если счетчик в CX достиг 0, либо если флаг нуля установлен в 0.

Допустим, Вы хотите в цикле читать символы с клавиатуры до тех пор, пока не будет нажат ВВОД или пока не будет считано 128 символов. Следующий пример использует для этого LOOPNE:

...

.DATA

KeyBuffer DB 128 DUP (?)

...

.CODE

...

MOV CX,128

MOV bx,OFFSET KeyBuffer

KeyLOOP:

MOV AH,1 ; функции DOS для ввода

INT 21h ;вызов DOS для чтения символа

MOV [bx],AL ;сохранить символ

INC bx ;указать на следующий символ

CMP AL,0dh ;нажат ВВОД?

LOOPne KeyLOOP ;если нет, взять следующий символ,

;если не считано 128 символов

...

LOOPE так же известна как LOOPZ, и LOOPNE так же известна, как LOOPNZ (аналогично JE/JZ).

С циклами связана еще одна инструкция - JCXZ. JCXZ осуществляет переход, только если CX=0; это полезный способ, проверять CX в начала цикла. Например, следующий код в котором BX указывает на блок байт, устанавливаемых в 0, а CX содержит длину блока, использует JCXZ для пропуска цикла, если CX - 0:

...

jCXz SkipLOOP ;если CX=0, ничего не делать

CLearLOOP:

MOV BYTE PTR [SI],0 ;установить следующий байт в 0

INC SI ;указать на следующий байт

LOOP CLearLOOP ;обнулить следующий символ, если он есть

SkipLOOP:

...

Почему нужно пропустить цикл, если CX - 0? Если Вы выполните LOOP с CX=0, то CX уменьшится до 0FFFFh и инструкция LOOP выполнит переход на метку назначения. Тогда цикл выполнится более 65,535 раз. Когда Вы устанавливали CX в 0, Вы подразумевали, что нет байтов для обнуления, а получилось 65,536 байт. JCXZ позволяет Вам проверить этот случай быстро и эффективно.

Несколько интересных примечаний к инструкциям цикла. Во-первых, запомните, что инструкция цикла как и условный переход, может выполнять переход на метку только в диапазоне 128 байт, до или после инструкции цикла. Циклы, больше 128 байт, требуют использования техники "Условный переход перед безусловным переходом", описанный в разделе "Условные переходы". Во-вторых, важно представлять, что ни одна инструкция цикла не действует на флаги. Это означает, что LOOP LOOPTop не точно такая же, как

DEC CX

JNZ LOOPTop

поскольку DEC изменяет флаги переполнения, знака, нуля, дополнительного переноса и четности в то время, как LOOP не изменяет флаги вообще. По той же причине инструкция DEC не точно такая же, как

SUB CX,1

JNZ LOOPTop

поскольку SUB изменяет флаг переноса, а инструкция DEC нет.

Конечно, это небольшие отличия, но они важны для понимания набора инструкций при программировании на ассемблере.

10.4 Процедуры в языке ассемблера

При составлении и вызове подпрограмм необходимо следить за тем, чтобы команды CALL и RET действовали согласовано - были одновременно близкими или дальними. В Ассемблере эта проблема снимается, если подпрограмму описать как процедуру. Процедуры имеют следующий вид:

имя_процедуры PROC [NEAR или FAR]

...

имя_процедуры ENDP

Хотя в директиве PROC после имени процедуры не ставится двоеточие, это имя относится к меткам и его можно указывать в командах перехода, в частности в команде CALL, когда надо вызвать процедуру. Это же имя должно быть повторено в директиве ENDP, заканчивающей описание процедуры. Предложения между этими двумя директивами образуют тело процедуры (подпрограмму). Имя процедуры является фактически меткой первой из команд тела, поэтому данную команду не надо специально метить.

Если в директиве PROC указан параметр NEAR или он вообще не указан, то такая процедура считается "близкой" и обращаться к ней можно только из того сегмента команд, где она описана. Дело в том, что ассемблер будет заменять все команды CALL, где указано имя данной процедуры, на машинные команды близкого перехода с возвратом, а все команды RET внутри процедуры - на близкие возвраты. Если же в директиве PROC указан параметр FAR, то это "дальняя" процедура: все обращения к ней и все команды RET внутри нее рассматриваются ассемблером как дальние переходы. Обращаться к этой процедуре можно из любых сегментов команд. Таким образом, достаточно лишь указать тип процедуры (близкая она или дальняя), всю же остальную работу возьмет на себя ассемблер: переходы на нее и возвраты из нее будут автоматически согласованы с этим типом. В этом главное (и единственное) достоинство описания подпрограмм в виде процедур. (Отметим, что метки и имена, описанные в процедуре, не локализуются в ней.)

Например, вычисление AX:=SIgn(AX) можно описать в виде процедуры следующим образом:

SIng proc far ;дальняя процедура

CMP AX,0

JE sgn1 ;AX=0 - перейти к sgn1

MOV AX,1 ;AX:=1 (флаги не изменились!)

JG sgn1 ;AX>0 - перейти к sgn1

MOV AX,-1 ;AX:=-1

sgn1: RET ;дальний возврат

SIgn ENDP

...

Возможный пример обращения к этой процедуре:

;CX:=SIgn(var)

MOV AX,var

cALl SIgn ;дальний вызов

MOV CX,AX

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

При обращении к подпрограмме в стек заносятся параметры для нее и адрес возврата, после чего делается переход на ее начало:

PUSH param1 ;запись 1-го параметра в стек

...

PUSH paramk ;запись последнего (k-го) параметра в стек

CALL SUBr ;переход с возвратом на подпрограмму

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

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

Формат команды

INT тип_прерывания

Для выхода из программы на обработку прерывания и для последующего возврата команда INT выполняет следующие действия:

- уменьшает указатель стека на 2 и заносит в вершину стека содержимое флагового регистра;

- очищает флаги TF и IF;

- уменьшает указатель стека на 2 и заносит содержимое регистра CS в стек;

- уменьшает указатель стека на 2 и заносит в стек значение командного указателя;

- вычисляет адрес вектора прерывания, умножая тип_прерывания на 4;

- загружает второе слово вектора прерываний в регистр CS;

- загружает в IP первое слово вектора прерывания;

- обеспечивает выполнение необходимых действий;

- восстанавливает из стека значение регистра и возвращает управление в прерванную программу на команду, следующую после INT.

Этот процесс выполняется полностью автоматически. Необходимо лишь определить сегмент стека достаточно большим для записи в него значений регистров.

В данной главе рассмотрим два типа прерываний: команду BIOS INT 10H и команду DOS INT 21H для вывода на экран и ввода с клавиатуры. В последующих примерах в зависимости от требований используются как INT 10H так и INT 21H.


Сейчас читают про: