Основные моменты выполнения подпрограммы иллюстрируются на Рис. 5.12. В вызывающей подпрограмму программе выполняется инст-рукция CALL, которая заносит адрес следующей инструкции в стек и загружает в регистр IP адрес соответствующей подпрограммы, осуще-ствляя таким образом переход на подпрограмму. После этого под-программа выполняется, как любой другой код. В подпрограммах мо-гут (часто это так и бывает) содержаться инструкции вызовов дру-гих подпрограмм. Фактически, должным образом построенные подпрог-раммы могут даже вызывать сами себя (это называется рекурсией).
. . . .
. . . .
. . . .
|————-| |————|
1000 | mov al,1 | (в IP загружается —>| shl al,1 | 1110
|————-| 1110 и 1007 зано- | |————|
1002 | mov bl,3 | сится в стек) | | add al,bl | 1112
|————-| | |————|
1004 | call DoCalc |——————— | and al,7 | 1114
|————-| |————|
1007 | mov ah,2 |<——————- | add al,’0′ | 1116
|————-| Значение вершины | |————|
1009 | int 21h | стека 1007 извле- —| ret | 1118
|————-| кается и заносится в |————|
. . IP . .
. . . .
. . . .
Рис. 5.12 Выполнение подпрограммы.
Когда подпрограмма заканчивает работу, она вызывает инстру-кцию RET, которая извлекает из стека адрес, занесенный туда со-ответствующей инструкцией CALL, и заносит его в IP. Это приводит к тому, что вызывающая программа возобновит выполнение с инструк-ции, следующей за инструкции CALL.
Например, следующая программы выводит на экран три строки:
Привет
Пример строки
Еще одна строка
Для вывода строк вызывается подпрограмма PrintString:
DOSSEG
.MODEL SMALL
.STACK 200h
.DATA
Message1 DB ‘Привет’,0dh,0ah,0
Message2 DB ‘Пример строки’,0dh,0ah,0
Message3 DB ‘Еще одна строка’,0dh,0ah,0
.CODE
ProgramStart PROC NEAR
mov ax,@Data
mov ds,ax
mov bx,OFFSET Message1
call PrintString ; вывести строку «Привет»
mov bx,OFFSET Message2
call PrintString ; вывести строку “Пример строки”
mov bx,OFFSET Message3
call PrintString ; вывести строку “Еще одна
; строка»
mov ax,4ch ; функция DOS завершения
; программы
int 21h ; завершить программу
ProgramStart ENDP
;
; Подпрограмма вывода на экран строки, завершающейся
; нулевым символом
;
; Входные данные:
; DS:BX — указатель на выводимую строку.
;
; Нарушаемые регистры: AX, BX
;
PrintString PROC NEAR PrintStringLoop:
mov al[bx] ; получить следующий символ
; строки
and dl,dl ; значение символа равно 0?
jz EndPrintString ; если это так, то вывод
; строки завершен
inc bx ; ссылка на следующий
; символ
mov ah,2 ; функция DOS вывода символа
int 21h ; вызвать DOS для вывода
; символа
jmp PrintStringLoop ; вывести следующий символ,
; если он имеется EndPrintString:
ret ; возврат в вызывающую
; программу
PrintString ENDP
END ProgramStart
Здесь стоит отметить два момента. Во-первых, подпрограмма PrintString не настроена жестко на печать определенной строки. Она может печатать любую строку, на которую с помощью BX укажет вызывающая программа. Во-вторых, для выделения подпрограммы ис-пользованы две новых директивы — PROC и ENDP.
Директива PROC используется для того, чтобы отметить начало процедуры. Метка, указанная в директиве PROC (в данном случае — PrintString), представляет собой имя процедуры, как если бы ис-пользовалось:
PrintString LABEL PROC
Однако, директива PROC делает большее. Она определяет, какую инструкцию RET (возврат управления) — ближнюю или дальнюю — сле-дует использовать в данной процедуре.
Давайте рассмотрим последний оператор несколько подробнее. Вспомним, что когда выполняется переход на метку ближнего типа (NEAR), в IP загружается новое значение, а при переходе на даль-нюю метку (FAR) новые значения загружаются и в регистр IP, и в CS. Если инструкция CALL ссылается на дальнюю метку, загружаются и CS, и IP (как и при переходе).
Вот почему при дальнем вызове в стек заносятся и регистр CS, и IP. Иначе откуда инструкция RET получит достаточную информацию для возврата в вызывающую программу? Ведь если дальний вызов заг-рузит CS и IP, а занесет в стек только IP, то при возврате можно будет только загрузить IP из вершины стека. Тогда в результате выполнения инструкции RET пара регистров CS:IP содержала бы зна-чение CS вызываемой программы, а IP — вызывающей, что очевидно не имеет смысла.
Что же произойдет, когда в стек будут занесены оба регистра — CS и IP? Как Турбо Ассемблер узнает о типе возврата, генерируе-мом в соответствующей подпрограмме? Один из путей состоит в явном задании типа каждой инструкции возврата — RETN (возврат ближнего типа) или RETF (возврат дальнего типа), однако лучший способ зак-лючается в использовании директив PROC и ENDP.
Директива ENDP используется для того, чтобы пометить конец подпрограммы, начатой с помощью директивы PROC. Директива ENDP отмечает конец подпрограммы, которая начинается с директивы PROC с той же меткой. Например, директивы:
.
.
.
TestSub PROC NEAR
.
.
.
TestSub ENDP
.
.
.
отмечают начало и конец подпрограммы TestSub.
Директивы ENDP и PROC не генерируют выполняемого кода, ведь это директивы, а не инструкции. Все их действие заключаются в управлении типом инструкции RET данной подпрограммы.
Если операндом директивы PROC является NEAR (ближний), то все инструкции RET между директивой PROC и соответствующей ди-рективой ENDP ассемблируются, как возвраты управления ближнего типа. Если же, с другой стороны, операндом директивы PROC являет-ся FAR (дальний), то все инструкции RET в данной процедуре ассем-блируются, как возвраты управления дальнего типа.
Поэтому, чтобы, например, изменить тип всех инструкций RET в TestSub, измените директиву PROC следующим образом:
TestSub PROC FAR
В общем случае лучше там, где это возможно, использовать подпрограммы ближнего типа, так как дальние вызовы занимают боль-ше памяти и выполняются медленнее, а возвраты дальнего типа также выполняются медленнее, чем ближнего. Однако подпрограммы дальнего типа становятся необходимыми, когда объем кода вашей программы превышает 64К.
Если вы используете упрощенные директивы определения сегмен-тов, то лучше использовать директиву PROC без операндов, напри-мер:
TestSub PROC
Когда Турбо Ассемблер встречает такую директиву, он автома-тически настраивает ее тип в соответствии с выбранной с помощью директивы .MODEL моделью памяти (по умолчанию это малая модель памяти). Программы со сверхмалой, малой и компактной моделями па-мяти могут иметь вызовы ближнего типа, а вызовы в программах со средней, большой и сверхбольшой моделью памяти имеют дальний тип.
Например, в программе:
.
.
.
.MODEL SMALL ; малая модель памяти
.
.
.
TestSub PROC
.
.
.
подпрограмма TestSub вызывается с помощью ближнего вызова, а в программе:
.
.
.
.MODEL LARGE ; большая модель памяти
.
.
.
TestSub PROC
.
.
.
с помощью дальнего.
