CodeNet / Языки программирования / Ассемблер / СПРАВОЧНИК по системе программирования ТУРБО АССЕМБЛЕР 2.0
СПРАВОЧНИК по системе программирования ТУРБО АССЕМБЛЕР 2.0
Подпрограммы
-----------------------------------------------------------------
До сих пор мы рассматривали только программы, представляющие
собой одну длинную последовательность исходного кода. Каждая
программа начиналась с раздела кода, выполняла поочередно каждую
инструкцию (иногда изменяя свой маршрут при выполнении циклов или
принятии решений), а затем завершалась в конце раздела кода. Все
это хорошо для небольших программ, но в больших программах требу-
ется использования таких конструкций, которые называются подпрог-
раммами.
Возможно вы уже знакомы с подпрограммами в языках высокого
уровня. В языке Си подпрограммы называются функциями, в Паскале и
Бейсике - процедурами и функциями. Подпрограммы, процедуры и фу-
нкции представляют собой, в общем, одно и то же - отдельную часть
кода, воспринимающую определенные входные данные, выполняющую оп-
ределенные действия и, возможно, возвращающую полученное в ре-
зультате значение.
Подпрограммы позволяют вам строить программы по модульному
принципу. При этом подпрограммы позволяют "скрывать" специфичес-
кие детали (то есть убирать их на нижний уровень) и сосредоточить
внимание на общем алгоритме программы. Подпрограммы позволяют
также сделать программы намного более компактными, поскольку от-
дельную подпрограмму можно вызывать во многих местах программы и
даже выполнять различные функции, передавая ей различные значе-
ния. В больших программах (независимо от того, написаны они на
Ассемблере, Паскале или Си) подпрограммы являются существенным
средством для создания упорядоченного и легко обслуживаемого ис-
ходного кода.
Выполнение подпрограмм
-----------------------------------------------------------------
Основные моменты выполнения подпрограммы иллюстрируются на
Рис. 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
.
.
.
с помощью дальнего.
