Циклы
Циклы, позволяющие выполнить некоторый участок программы многократно, в любом языке являются одной из наиболее употребительных конструкций. В системе команд МП 86 циклы реализуются, главным образом, с помощью команды loop (петля), хотя имеются и другие способы организации циклов. Во всех случаях число шагов в цикле определяется содержимым регистра СХ, поэтому максимальное число шагов составляет 64 К.
Рассмотрим простой пример организации цикла. Пусть в программе зарезервировано место для массива размером 10000 слов, и этот массив надо заполнить натуральным рядом чисел от 0 до 9999. Эти числа, заполняющие последовательные элементы массива, иногда называют числами-заполнителями. Соответствующий фрагмент программы будет выглядеть следующим образом:
;В сегменте данных
array dw 10000 dup(0)
;В программном сегменте
mov BX,offset array ; Адрес массива
mov SI,0 ;Индекс
mov AX,0 ; Начальное значение заполнителя
mov CX,10000 ; Счетчик цикла
fill: mov [BX] [SI],AX ;Заполнитель пошлем в массив
inc AX ;Инкремент заполнителя
add SI,2 ; модификация индекса
loop fill ; Команда цикла
На этапе подготовки мы заносим в регистр ВХ относительный адрес начала массива, отождествляемый с его именем array, устанавливаем начальное значение индекса элемента массива в регистре SI (с таким же успехом можно бьшо взять DI) и начальное значение числа-заполнителя. Сам цикл состоит из трех команд - единственной содержательной команды засылки числа-заполнителя в очередной элемент массива (по адресу, который вычисляется, как сумма содержимого регистров ВХ и SI), а также модификации числа-заполнителя и индекса очередного элемента массива. Завершающей командой loop управление передается на метку fill, и цикл повторяется столько раз, каково содержимое СХ, в данном случае 10000 шагов.
Следует обратить внимание на команду модификации индекса - в каждом шаге к содержимому SI добавляется 2, так как массив состоит из двухбайтовых слов. Если бы нужно было заполнить байтовый массив, то в каждом шаге содержимое регистра цикла SI следовало увеличивать на 1.
Стоит отметить некоторые детали, связанные с механизмом выполнения команды loop. При реализации этой команды процессор сначала уменьшает содержимое регистра СХ на 1, а затем сравнивает полученное число с нулем. Если СХ > 0, переход на указанную метку выполняется. Если СХ = 0, цикл разрывается и процессор переходит на команду, следующую за командой loop. Поэтому после нормального выхода из цикла содержимое СХ всегда равно 0.
Другое обстоятельство связано с кодированием команды loop. В ее коде под смещение к точке перехода отводится всего 1 байт. Поскольку смещение должно являться величиной со знаком, максимальное расстояние, на которое можно передать управление командой loop, составляет от -128 до +127 байт (хотя довольно трудно представить себе цикл, в котором переход осуществляется вперед). Другими словами, тело цикла ограничивается всего 128 байтами. Если циклически повторяемый фрагмент программы имеет большую длину, цикл придется организовать другим, более сложным способом:
;Организация длинного цикла
mov CX,10000 ;Счетчик цикла
fill: ; Метка начала цикла
... ; Тело длинного цикла
dec CX ; Декремент счетчика цикла
cmp CX,0 ; Отработано заданное число шагов?
je finish ; Да, на метку продолжения программы
jmp fill ; Нет, на начало цикла
finish: ; Продолжение программы
В этом, весьма типичном фрагменте мы "вручную" уменьшаем содержимое счетчика цикла и сравниваем полученное значение с 0. Если СХ = О, это значит, что в цикле выполнено заданное число шагов, и командой условного перехода je осуществляется переход на продолжение программы (метка finish). Если СХ еще не равно нулю, командой безусловного перехода jmp осуществляется возврат в начало цикла. Как было показано в гл. 2, команда jmp позволяет перейти в любую точку сегмента, и ограничение на размер тела цикла снимается.
При необходимости организовать вложенные циклы, для сохранения счетчика внешнего цикла на время выполнения внутреннего удобно воспользоваться стеком. В следующем фрагменте организуется временная задержка длительностью несколько секунд (конкретная величина задержки зависит от скорости работы процессора).
mov CX,2000 ;Счетчик внешнего цикла
outer: push CX ; Сохраним его в стеке
mov CX,0 ;Счетчик внутреннего цикла
inner: loop inner ; loop внутреннего цикла
pop CX ;Восстановим внешний счетчик
loop outher ; loop внешнего цикла
Программные задержки удобно использовать при отладке программ, чтобы замедлить их работу и успеть рассмотреть их частичные результаты; иногда программные задержки позволяют синхронизовать работу аппаратуры, подключенной к компьютеру, если скорость отработки аппаратурой посылаемых в нее из компьютера команд меньше скорости процессора.
В приведенном выше фрагменте внешний цикл выполняется 2000 раз; внутренний - 65536 раз. При счете числа шагов внутреннего цикла используется явление оборачивания, которое уже упоминалось ранее. Начальное значение в регистре СХ равно нулю; после выполнения тела цикла 1 раз команда loop уменьшает содержимое СХ на 1, что дает число FFFFh (которое можно рассматривать, как -1). В результате цикл повторяется еще 65535 раз, а в сумме - точно 64 К шагов.
Команда loop внутреннего цикла передает управление на саму себя, т.е. тело внутреннего цикла состоит из единственной команды loop. В этом нет ничего незаконного. Любая команда, в том числе и loop, требует какого-то времени для своего выполнения, и повторение 64 К раз команды loop дает некоторую временную задержку (на современных процессорах порядка тысячной доли секунды).
Перейдем теперь к рассмотрению команд условных переходов.
В приведенном выше фрагменте для реализации длинного цикла использовалась команда условного перехода по равенству je. В системе команд МП 86 имеется свыше трех десятков команд условных переходов, позволяющих осуществлять переходы при наличии разнообразных условий: равенства, неравенства, положительности или отрицательности результата и проч. При выполнении всех этих команд процессор анализирует содержимое регистра флагов и осуществляет (или не осуществляет) переход на указанную метку в зависимости от состояния отдельных флагов или их комбинаций. Поскольку на состояние регистра флагов влияют многие команды процессора, командами условных переходов можно пользоваться не только после команд сравнения или анализа, но и после многих других команд, если внимательно изучить влияние этих команд на флаги процессора. Приведем несколько абстрактных примеров.
cmp AX,BX ;Сравнение двух регистров
je equal ;Переход, если AX=BX
cmp SI,mem ;Сравнение регистра и ячейки памяти
jne notequ ;Переход, если SI<>mem
int 21h ;Вызов DOS
jc syserr ;Переход, если была ошибка
;и флаг CF=1
or BX,BX ;Анализ BX
jz zero ;Переход, если BX=0
inpt: in AL,DX ;Ввод данного из устройства
test AL,80h ;Анализ бита 7 в данном
je inpt ;Ввод до тех пор , пока
;бит 7=0 (ожидание установки бита 7)
test AX,7 ;Анализ битов 0,1,2 в AX
jne found ;Переход, если хотя бы 1 бит
;из них установлен
test DI,OFh ;Анализ битов 0...3 в DI
jz reset ;Переход, если все они сброшены
В гл. 2 отмечалось, что двоичные числа, записываемые в регистры процессора или ячейки памяти, можно рассматривать, либо как числа существенно положительные, т.е. числа без знака, либо как числа со знаком. Например, адреса ячеек, разумеется, не могут быть отрицательными. Поэтому число FFFFh, если по смыслу программы оно является адресом, обозначает 65535. Если, однако, то же число FFFFh получилось в арифметической операции вычитания 2 из 1, то его надо рассматривать, как - 1. Точно так же понятие знака бессмысленно по отношению к кодам символов, которые с равным успехом могут принимать любое значение из диапазона 0...255. С другой стороны, мы можем условно считать, что коды символов первой половины таблицы ASCII положительны, а коды второй половины таблицы (у них установлен старший бит) отрицательны, и использовать для обработки символов команды, чувствительные к знаку.
В составе команд условных переходов имеются две группы команд для сравнения чисел без знака (это команды ja, jae, jb, jbc, jna, jnae, jnb и jnbe) и чисел со знаком (jg, jge, jl, jle, jng, jnge, jnl и jnle). В аббревиатурах этих команд для сравнения чисел без знака используются слова above (выше) и below (ниже), а для чисел со знаком - слова greater (больше) и less (меньше).
Разница между теми и другими командами условных переходов заключается в том, что команды для чисел со знаком рассматривают понятия "больше- меньше" применительно к числовой оси -32К...0...+32К, а команды для чисел без знака - применительно к числовой оси 0...64К. Поэтому для первых команд число 7FFFh (+32767) больше числа S000h (-32768), а для вторых число 7FFFh (32767) меньше числа S000h (32768). Аналогично, команды для чисел со знаком считают, что 0 больше, чем FFFFh (-1), а команды для чисел без знака - меньше.
Рассмотрим пример использования команд условных переходов для обработки символов. Пусть мы вводим с клавиатуры некоторую строку символов (например, имя файла), и хотим, чтобы в программе эта строка была записана прописными буквами, независимо от того, какие буквы использовались при ее вводе. Между прочим, при вводе с клавиатуры команд DOS система всегда выполняет эту операцию, поэтому и команды, и ключи, и имена файлов можно вводить как прописными, так и строчными буквами - DOS во всех случаях преобразует все буквы в прописные.
code segment
assume cs:code,ds:data
main proc
mov AX,data ;Инициализация
move DS,AX ;Регистр DS
;Выведем служебное сообщение
mov AH,09h ;Функция вывода
mov DX,offset msg ;Адрес сообщения
int 21h
;Поставим запрос к DOS на ввод строки
mov AH,3Fh ;Функция ввода
mov BX,0 ;Дескриптор клавиатуры
mov CX,80 ;Ввод максимум 80 байт
mov DX, offset buf ;Адрес буфера ввода
int 21h
mov actlen,AX ;Фактически введено
;Превратим строчные русские буквы в прописные
mov CX,actlen ;Длина введенной строки
mov SI,0 ;Указатель в буфере
filter: mov AL,buf[SI] ;Возьмем символ
cmp AL,'a' ;Меньше 'a'?
jb noletter ;Да, не преобразовывать
cmp AL,'я' ;Больше 'я'?
ja noletter ;Да, не преобразовывать
cmp AL,'п' ;Больше 'п'?
ja more ; Да, на дальнейшую проверку
sub AL,20h ;'a'..'п'. Преобразуем в прописную
jmp store ;На сохранение в буфере
more: cmp AL,'p' ;Меньше 'p1' (псевдографика)?
jb noletter ;>'п',<'p'. Не изменять
sub AL,50h ;'p'...'я'. Преобразуем в прописную
store: mov buf[SI],AL ;Отправим назад в buf
noletter: inc SI ;Сместим указатель
loop filter ;Цикл по всем символам
; Выведем результат преобразования на экран для контроля
mov AX,40h ;Функция вывода
mov BX,1 ;Дескриптор экрана
mov CX,actlen ;Длина сообщения
mov DX,offset buf ;Адрес сообщения
int 21h
mov AH,01 ;Остановим программу
int 21h ;в ожидании нажатия клавиши
;Завершим программу
mov AX,4C00h
int 21h
main endp
code ends
data segment
msg db "Вводите!$"
buf db 80 dup (' ') ;Буфер ввода
actlen dw 0
data ends
stk segment stack
dw 128 dup(')
stk ends
end main
В начале программы на экран выводится служебное сообщение "Вводите!", которое служит запросом программы, адресованным пользователю. Далее с помощью функции DOS 3Fh выполняется ввод строки текста с клавиатуры. Функция 3Fh может вводить данные из разных устройств - файлов, последовательного порта, клавиатуры. Различные устройства идентифицируются их дескрипторами. При работе с файлами дескриптор каждого файла создается системой в процессе операции открытия или создания этого файла, а для стандартных устройств - клавиатуры, экрана, принтера и последовательного порта действуют дескрипторы, закрепляемые за этими устройствами при загрузке системы. Для ввода с клавиатуры используется дескриптор 0, для вывода на экран дескриптор 1.
При вызове функции 3Fh в регистр ВХ следует занести требуемый дескриптор, в регистр DX - адрес области в программе, выделенной для приема вводимых с клавиатуры символов, а в регистр СХ - максимальное число вводимых символов. Мы считаем, что пользователь не будет вводить более 80 символов. Можно ввести и меньше; в любом случае ввод строки следует завершить нажатием клавиши <Enter>. Функция 3Fh, отработав, вернет в регистре АХ реальное число введенных символов (включая коды 13 и 10, образуемые при нажатии клавиши <Enter>). В примере 3.5 число введенных символов сохраняется в ячейке actlen с целью использования далее по ходу программы.
Далее в цикле из actlen шагов выполняется анализ каждого введенного символа путем сравнения с границами диапазонов строчных русских букв. Русские строчные буквы размещаются в двух диапазонах кодов ASCII (а...п и р...с), причем для преобразования в прописные букв первого диапазона их код следует уменьшать на 20h, а для преобразования букв второго диапазона - на 50h. Поэтому анализ проводится с помощью четырех команд сравнения сmр и соответствующих команд условных переходов. Модифицированный символ записывается на то же место в буфере buf.
После завершения анализа и преобразования введенных символов, выполняется контрольный вывод содержимого buf на экран. Поскольку мы заранее не знаем, сколько символов будет введено, вывод на экран осуществляется функцией 40h, среди параметров которой указывается число выводимых символов. Так же, как и в случае функции ввода 3Fh, для функции вывода 40h в регистре ВХ необходимо указать дескриптор устройства ввода, в данном случае экрана, а в регистре DX - адрес выводимой строки.
Коды символов являются числами без знака, и использование в данном случае команд условных переходов для чисел без знака представляется логичным и даже единственно возможным. Если, однако, внимательно рассмотреть понятия больше- меньше для чисел со знаком и без знака, то легко увидеть, что пока мы сравниваем друг с другом только "положительные" или только "отрицательные" числа, команда ja эквивалентна команде jg, а команда jb эквивалентна команде jl. Однако при сравнении, например, кодов цифр с кодами русских букв, правильный результат можно получить лишь при использовании команд переходов для чисел без знака. Впрочем, всегда нагляднее и надежнее использовать те команды, которые соответствуют существу рассматриваемых данных, даже если такой же правильный результат получится и при использовании "неправильных" команд.
Более отчетливо разница между числами со знаком и без знака проявляется при использовании арифметических операций, например, операций умножения или деления. Здесь для чисел со знаком и чисел без знака предусмотрены отдельные команды:
mul - команда умножения чисел без знака;
imul - команда умножения чисел со знаком;
div - команда деления чисел без знака;
idiv - команда деления чисел со знаком.
Поясним различия этих команд на формальных примерах.
;Умножение положительных чисел со знаком
mov AL,5 ;Первый сомножитель равен 5
mov BL,7 ;Второй сомножитель равен 7
mul BL ;AX=0023h=35
mov AL,5 ;Первый сомножитель равен 5
mov BL,7 ;Второй сомножитель равен 7
imul BL ;AX=0023h=35
Обе команды, mul и imul, дают в данном случае одинаковый результат, так как положительные числа со знаком совпадают с числами без знака. Не так обстоит дело при умножении отрицательных чисел.
;Умножение отрицательных чисел со знаком
mov AL,OFCh ;Первый сомножитель=252
mov BL,4 ; Второй сомножитель =4
mul BL ;AX=03F0h =1008
mov AL,OFCh ;Первый сомножитель=-4
mov BL,4 ; Второй сомножитель =4
imul BL ;AX=FFFO=-16
Здесь действие команд mul и imul над одними и теми же операндами дает разные результаты. В первом примере число без знака FCh, которое интерпретируется, как 252, умножается на 4, давая в результате число без знака 3F0, т.е. 1008. Во втором примере то же число FCh рассматривается, как число со знаком. В этом случае оно составляет -4. Умножение на 4 дает FFF0h, т.е. -16.