Использование подпрограмм
Общая идея использования подпрограмм очевидна: если в программе требуется многократно
выполнять один и тот же фрагмент, его можно оформить в виде подпрограммы и вызвать
по мере необходимости. Если подпрограмма не требует для своего выполнения никаких
параметров и не должна возвращать в основную программу результат своей работы,
то дело ограничивается оформлением текста подпрограммы в виде процедуры, завершающейся
командой ret, и вызовом этой процедуры с помощью команды
call. Как уже отмечалось
ранее, подпрограмма может и не образовывать процедуру, а быть просто частью
основной программы. Важно только, чтобы у нее была входная метка, и чтобы она
завершалась командой ret.
В следующем примере подпрограмма delay используется для включения в основной
текст программы программных задержек фиксированной величины.
Пример 3-8. Вызов подпрограммы без параметров
code segment
assume cs:code,ds:data
delay proc ;Процедура-подпрограмма
push CX ;Сохраним СХ основной программы
mov CX,2000 ;Счетчик внешнего цикла
del1: push CX ;Сохраним его
mov CX,0 ;Счетчик внутреннего цикла
del2: loop del2 ;Внутренний цикл (64К шагов)
pop CX ;Восстановим внешний счетчик
loop del1 ;Внешний цикл (2000 шагов)
pop CX ; Восстановим СХ программы
ret ;Возврат в подпрограмму
delay endp
main proc
mov AX,data ;Настроим DS
mov DX,AX ;на сегмент данных
mov AH,09h ;Функция вывода на экран
mov DX,offset npl1 ;Адрес первой строки
mov CX,3 ;Будем выводить строки в цикле
cntrl1: int 21h ;Вызов DOS
cal1 delay ;Вызов подпрограммы задержки
add DX,msg_len ;Прибавим к смещению длину строки
loop cntrl ;Цикл вызовов DOS
mov AX,4C00h ;Завершение программы
int 21h
main endp
code ends
data segment
msg1 db "Процесс стартовал",13,10,'$'
msg_len=$-msg1
msg2 db "Процесс идет",13,10,'$'
msg3 db "Процесс завершается",13,10,'$'
data ends
stk segment stack
dw 128 dup(')
stk ends
end main
В тексте программы сначала
описана процедура-подпрограмма, затем основная программа. Как уже отмечалось,
порядок их описания роли не играет; важно только, чтобы в завершающей директиве
окончания трансляции end был указан в качестве точки входа адрес основной программы
(main в нашем примере).
Подпрограмма реализует задержку с помощью вложенных циклов с командой
loop,
использующей в качестве счетчика шагов регистр СХ. В основной программе этот
регистр используется для организации цикла вывода трех строк. Поэтому первое,
что должна сделать подпрограмма - это сохранить содержимое регистра СХ, для
чего естественно использовать стек. Перед завершающей командой ret регистр СХ
должен быть восстановлен. Фрагмент, реализующий задержку, был описан ранее,
в разделе 3.2.
Основная программа выводит на экран с помощью функции 09h три строки текста.
Для упрощения программы, а также чтобы продемонстрировать некоторые приемы программирования,
вывод строк реализован в цикле. Строки сделаны одной длины, и модификация смещения
к очередной строке выполняется прибавлением к содержимому регистра DX длины
строки. Полезно обратить внимание на организацию цикла в основной программе.
В цикл, помимо команды вызова подпрограммы задержки и предложения, модифицирующего
регистр DX, включена лишь команда int 21h. Регистр АН с номером функции заново
не настраивается. Это и не нужно, так как DOS, выполняя затребованную операцию,
первым делом сохраняет все регистры программы, а перед возвратом в программу
их восстанавливает. Поэтому, вызывая функции DOS (или
BIOS) можно не заботиться
о сохранении регистров - их содержимое система на разрушает. Надо только иметь
в виду, что многие функции DOS и BIOS после своего завершения возвращают в программу
некоторую информацию (число реально введенных символов, доступный объем памяти,
номер видеорежима и т.п.) Обычно эта информация возвращается в регистре АХ,
однако могут использоваться и другие регистры или их сочетания. Поэтому, обращаясь
в программе к системным функциям, необходимо ознакомиться с их описанием и,
в частности, посмотреть, какие регистры они могут использовать для возвращаемых
значений.
Запустив программу, можно убедиться в том, что строки текста появляются на экране
через заметные промежутки времени.
В примере 3-8 подпрограмма не требовала параметров. Чаще, однако, подпрограмма
должна принимать один или несколько параметров и возвращать результат. В этом
случае необходимо организовать взаимодействие основной программы и подпрограммы.
Никаких специальных средств языка для этого не существует; передачу параметров
в подпрограмму и из нее программист организует по своему усмотрению. Для передачи
параметров как в одну, так и в другую сторону можно использовать регистры общего
назначения, ячейки памяти или стек. Например, нетрудно преобразовать подпрограмму
delay из примера 3-8 так, чтобы ей можно было передавать величину требуемой
задержки. Пусть эта величина (в числе шагов внешнего цикла) передается в регистре
SI.
Пример 3-8а. Подпрограмма задержки с одним параметром, передаваемом в регистре SI
delay proc ;Процедура- подпрограмма
push CX ;Сохраним СХ основной программы
mov CX,SI ;Счетчик внешнего цикла
del1: push CX ;Сохраним его
mov CX,0 ;Счетчик внутреннего цикла
del2: loop del2 ;Внутренний цикл (64К шагов)
pop CX ;Восстановим внешний счетчик
loop del1 ;Внешний цикл (2000 шагов)
pop CX ;Восстановим СХ программы
ret ;Возврат в программу
Можно пойти еще дальше и составить подпрограмму таким образом, чтобы передаваемый в нее параметр характеризовал время задержки в секундах. Если не связываться с использованием системного таймера в качестве инструмента для определения интервала времени, а по-прежнему реализовывать задержку с помощью процессорного цикла, ее величина будет зависеть от скорости работы конкретного компьютера и должна быть подобрана экспериментально. Приведенный ниже вариант подпрограммы правильно работал на процессоре Pentium с тактовой частотой 200 МГц.
Пример 3-8б. Подпрограмма задержки с преобразованием параметра, передаваемого в регистре SI
delay proc ;Процедура-подпрограмма
push AX ;Сохраним все
push BX ;используемые
push CX ;в программе
push DX ;регистры
mov AX,SI ;первый сомножитель в AX
mov BX,600 ;второй экспериментально
;подобранный сомножитель
mul BX ;Произведение в DX:AX
mov CX,AX ;Нам оно нужно в CX
del1: push CX ;Сохраним его
mov CX,0 ;Счетчик внутреннего цикла
del2: loop del2 ;внутренний цикл (64К шагов)
pop CX ;Восстановим внешний счетчик
loop del1 ;Внешний цикл ( 2000 шагов)
pop DX ;Восстановим
pop CX ;все сохраненные
pop BX ; в начале подпрограммы
pop AX ;регистры
ret ;Возврат в программу
Эксперименты показали,
что для получения правильной задержки значение параметра, обозначающее число
секунд, следует умножать на 600. Поскольку при умножении в системе команд МП
86 первый сомножитель должен находиться в регистре АХ, а второй не может быть
непосредственным значением и тоже, следовательно, должен быть помещен в один
из регистров, и, к тому же, произведение занимает два регистра
DX:AX, приходится
сохранять при входе в подпрограмму не один регистр, как в предыдущем примере,
а 4. Передаваемый в SI параметр переносится в АХ, в ВХ загружается второй сомножитель,
а из полученного с помощью команды mul произведения используется младшая часть,
находящаяся в АХ. Таким образом, для данного варианта подпрограммы значение
задержки не должно превышать 109 с (109 х 600 = 65500, что почти совпадает с
максимально возможным значением 65535).
Следует обратить внимание на опасность, подстерегающую нас при выполнении операции
умножения. Пусть значение передаваемого параметра составляет всего 5. При умножении
на 600 получится число 3000, которое безусловно помещается в регистре АХ. Однако
операция умножения 16-разрядных операндов
mul BX
всегда, независимо от конкретной
величины произведения, помещает его в пару регистров
DX:AX, и, следовательно,
при небольшой величине произведения регистр DX будет обнуляться. Поэтому, хотя
мы и не используем старшую часть произведения и фактически ее может и не быть,
сохранение и последующее восстановление регистра DX является обязательным.
Передача параметров в подпрограмму через регистры общего назначения или даже
через сегментные регистры вполне возможна, однако на практике для передачи параметров
чаще всего используют стек, хотя бы потому, что регистров немного, а в стек
можно поместить любое число параметров. При этом применяется своеобразная методика
работы со стеком не с помощью команд push и pop, а с помощью команд mov с косвенной
адресацией через регистр ВР, который архитектурно предназначен именно для адресации
к стеку. Преобразуем пример 3-8а так, чтобы единственный в этом примере параметр
(условная величина задержки) передавался в подпрограмму не через регистр
SI,
а через стек. Вызов подпрограммы delay в этом случае должен выполняться следующим
образом:
push 2000 ;Проталкиваем в стек значение параметра
call delay ;Вызываем подпрограмму delay
Текст подпрограммы подвергнется значительным изменениям:
Пример 3-8в. Передача параметра через стек
delay proc ;Процедура-подпрограмма
push CX ;Сохраним СХ основной программы
push BP ;Сохраним BP
mov BP,SP ;Настроим BP на текущую вершину стека
mov CX, [BP+6] ;Скопируем из стека параметр
del1: push CX ;Сохраним его
mov CX,0 ;Счетчик внутреннего цикла
del2 loop del2 ;Внутренний цикл(64К шагов)
pop CX ;Восстановим внешний счетчик
loop del1 ;Внешний цикл
pop BP ;Восстановим BP
pop CX ;и СХ программы
ret 2 ;Возврат и снятие со стека
;ненужного уже параметра
Команда call, передавая
управление подпрограмме, сохраняет в стеке адрес возврата в основную программу.
Подпрограмма сохраняет в стеке еще два 16-разрядных регистра. В результате стек
оказывается в состоянии, изображенном на рис. 3.9.
После сохранения в стеке исходного содержимого регистра ВР (в основной программе
нашего примера этот регистр не используется, однако в общем случае это может
быть и не так), в регистр ВР копируется содержимое указателя стека, после чего
в ВР оказывается смещение вершины стека. Далее командой mov в регистр СХ заносится
содержимое ячейки стека, на 6 байтов ниже текущей вершины. В этом месте стека
как раз находится передаваемый в подпрограмму параметр, как это показано в левом
столбце рис. 3.8. Конкретную величину смещения относительно вершины стека надо
для каждой подпрограммы определять индивидуально,
Рис. 3.8. Состояние стека в подпрограмме после сохранения регистров.
исходя из того, сколько
слов сохранено ею в стеке к этому моменту. Напомним, что при использовании косвенной
адресации с регистром ВР в качестве базового, по умолчанию адресуется стек,
что в данном случае и требуется.
Параметр, полученный таким образом, используется далее в подпрограмме точно
так же, как и в примере 3-8а.
Выполнив возложенную на нее задачу, подпрограмма восстанавливает сохраненные
ранее регистры и осуществляет возврат в основную программу с помощью команды
ret, в качестве аргумента которой указывается число байтов, занимаемых в стеке
отправленными туда перед вызовом подпрограммы параметрами. В нашем случае единственный
параметр занимает 2 байт. Если здесь использовать обычную команду ret без аргумента,
то после возврата в основную программу параметр останется в стеке, и его надо
будет оттуда извлекать (между прочим, не очень понятно, куда именно, ведь все
регистры у нас могут быть заняты). Команда же с аргументом, осуществив возврат
в вызывающую программу, увеличивает содержимое указателя стека на значение ее
аргумента, тем самым осуществляя логическое снятие параметра. Физически этот
параметр, как, впрочем, и все остальные данные, помещенные в стек, остается
в стеке и будет затерт при дальнейших обращениях к стеку.
Разумеется, в стек можно было поместить не один, а сколько угодно параметров.
Тогда для их чтения надо было использовать несколько команд mov со значениями
смещения ВР+6, ВР+8, BP+0Ah и т.д.
Рассмотренная методика может быть использована и при дальних вызовах подпрограмм,
но в этом случае необходимо учитывать, что дальняя команда call сохраняет в
стеке не одно, а два слова, что повлияет на величину рассчитываемого смещения
относительно вершины стека.