Выполнение подпрограмм


Основные моменты выполнения подпрограммы иллюстрируются на Рис. 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

.

.

.

с помощью дальнего.

Загрузка...