Використання підпрограм
Загальна ідея
використання підпрограм очевидна: якщо в програмі потрібно багато разів виконувати
один і той же фрагмент, його можна оформити у вигляді підпрограми і викликати в
міру необхідності. Якщо підпрограма не вимагає для свого виконання ніяких параметрів
і не повинна повертати в основну програму результат своєї роботи, то
справа обмежується оформленням тексту підпрограми у вигляді процедури, командою 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 зберігає в стеку
не одне, а два слова, що вплине на величину зсуву, що розраховується,
щодо вершини стека.
-
|