Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Учебное пособие 3000239.doc
Скачиваний:
23
Добавлен:
30.04.2022
Размер:
1.12 Mб
Скачать

7.2. Подпрограммы-процедуры

Довольно часто в программах, особенно больших, приходится несколько раз решать одну и ту же подзадачу и потому приходится многократно выписывать одинаковую группу команд, решающих эту подзадачу. Чтобы избежать повторного выписывания такой группы команд, ее обычно выписывают один раз и оформляют соответствующим образом, а затем в нужных местах программы просто передают управление на эти команды, которые, проработав, возвращают управление обратно. Такая группа команд, которая решает некоторую подзадачу и которая организована таким образом, что ее можно использовать любое число раз и из любых мест, называется подпрограммой. По отношению к подпрограмме остальную часть программы принято называть основной программой.

Легко понять, что подпрограммы – это аналог процедур из языков высокого уровня: сама подпрограмма – это аналог описания процедуры, и обращение к подпрограмме – аналог оператора процедуры. Однако реализации подпрограмм в языке ассемблера – вещь более сложная, чем описание процедур в языках высокого уровня.

Дело в том, что в этих языках за нас многое делают трансляторы, а при программировании на языке ассемблера нам никто не помогает, поэтому здесь все приходится делать самим. Какие проблемы возникают при использовании подпрограмм и как они решаются, будет рассмотрено далее.

Размещение подпрограммы

Первая проблема – где размещать подпрограмму? Вообще говоря, где угодно. Но при этом надо понимать, что сама по себе подпрограмма не должна выполняться, а может выполняться лишь тогда, когда к ней обратятся. Поэтому размещать ее надо так, чтобы на нее случайно не попадало управление. Если имеется несколько подпрограмм, то их обычно размещают рядом. Обычно подпрограммы (п/п) размещают либо в конце сегмента команд за командой FINISH (см. рис. 36, а), либо в самом начале этого сегмента – перед той командой, с которой должно начинаться выполнение программы (см. рис. 36, б). В больших программах подпрограммы нередко размещают в отдельном сегменте команд (см. рис. 36, в).

C SEGMENT C SEGMENT C1 SEGMENT

B

п/п

п/п

EG: …

FINISH BEG: … C1 ENDS

п/п

… C SEGMENT

… BEG: …

C ENDS C ENDS …

END BEG END BEG C ENDS

END BEG

а) б) в)

Рис. 36. Варианты размещения подпрограммы

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

Вторая проблема – как описывать подпрограмму? В общем случае группу команд, образующих подпрограмму, можно никак не выделять в тексте программы. Однако в языке ассемблера подпрограммы принято оформлять специальным образом - в виде процедур. Это дает определенную выгоду, о которой будет сказано чуть позже. В дальнейшем мы будем использовать только такие подпрограммы, которые описаны в виде процедур.

Описание подпрограммы в виде процедуры выглядит так:

<имя процедуры> PROC <параметр>

<тело процедуры>

<имя процедуры> ENDP

Как видно, перед телом процедуры (ее командами) ставится директива PROC (procedure), а за ним – директива ENDP (end of procedure). В обеих этих директивах указывается одно и то же имя – имя, которое мы дали подпрограмме, процедуре. Следует обратить внимание, что в директиве PROC после имени не ставится двоеточие (как и в других директивах), однако это имя считается меткой, считается, что оно метит первую команду процедуры. Например, имя процедуры можно указать в команде перехода, и тогда будет осуществлен переход на первую команду процедуры.

У директивы PROC есть параметр – это либо NEAR (близкий), либо FAR (дальний). Параметр может и отсутствовать, тогда считается, что он равен NEAR (в связи с этим параметр NEAR обычно не указывается). При параметре NEAR или при отсутствии параметра процедура называется «близкой», при параметре FAR – «дальней». К близкой процедуре можно обращаться только из того сегмента команд, где она описана, и нельзя обращаться из других сегментов, а к дальней процедуре можно обращаться из любых сегментов команд (в том числе и из того, где она описана). В этом и только в этом различие между близкими и дальними процедурами.

Следует отметить, что в языке ассемблера имена и метки, описанные в процедуре, не локализуются внутри нее, поэтому они должны быть уникальными, не должны совпадать с другими именами, используемыми в программе. В частности, хотя в языке ассемблера и можно описать одну процедуру внутри другой, но никакой выгоды это не дает, поэтому в языке ассемблера обычно не используется вложенность процедур.

Вызов процедур и возврат из них

Следующая проблема - как осуществляются вызовы процедур и возвраты из них? При программировании на языке высокого уровня для запуска процедуры в работу достаточно лишь выписать ее имя и ее фактические параметры. При этом нас не волнует, как процедура начинает работать, как она возвращает управление в основную программу: за все это отвечает транслятор. Но если мы программируем на языке ассемблера, тогда все переходы между основной программой и процедурой приходится реализовывать нам самим. Рассмотрим, как это делается.

Здесь две проблемы: как из основной программы заставить работать процедуру и как вернуться из процедуры в основную программу. Первая проблема решается просто: достаточно выполнить команду перехода на первую команду процедуры, т. е. указать в команде перехода имя процедуры. Сложнее со второй проблемой. Дело в том, что обращаться к процедуре можно из разных мест основной программы, а потому и возвращаться из процедуры надо в разные места. Сама процедура, конечно, не знает, куда ей надо вернуть управление, зато это знает основная программа. Поэтому при обращении к процедуре основная программа обязана сообщить ей так называемый адрес возврата – адрес той команды основной программы, на которую процедура обязана сделать переход по окончании своей работы. Обычно это адрес команды, следующей за командой обращения к процедуре. Именно этот адрес основная программа и сообщает процедуре, именно по нему процедура и выполняет возврат в основную программу. Поскольку при разных обращениях к процедуре ей указывают разные адреса возврата, то она и возвращает управление в разные места основной программы.

Как сообщать адрес возврата? Это можно сделать по-разному. Во-первых, его можно передать через регистр: основная программа записывает в некоторый регистр адрес возврата, а процедура извлекает его оттуда и делает по нему переход. Во-вторых, это можно сделать через стек: прежде чем обратиться к процедуре, основная программа записывает адрес возврата в стек, а процедура затем считывает его отсюда и использует для перехода. В ПК принято передавать адрес возврата через стек, поэтому в дальнейшем мы будем рассматривать только этот способ передачи адреса возврата.

Передачу адреса возврата через стек и возврат по этому адресу можно реализовать с помощью тех команд, которые мы уже знаем. Однако в реальных программах процедуры используются очень часто, поэтому в систему команд ПК включены специальные команды, которые упрощают реализацию переходов меж­ду основной программой и процедурами. Это команды CALL и RET. Основные варианты этих команд следующие:

Вызов процедуры (переход с возвратом):

CALL <имя процедуры>

Возврат из процедуры (return): RET

Команда CALL записывает адрес следующей за ней команды в стек и затем осуществляет переход на первую команду указанной процедуры. Команда RET считывает из вершины стека адрес и выполняет переход по нему.

Рассмотрим следующий пример. Пусть мы отлаживаем свою программу и для этого вставляем в разные ее места отладочную печать одних и тех же переменных X и Y. Такая печать осуществляется несколькими командами, они многократно повторяются, поэтому имеет смысл описать эту печать как процедуру и указывать в основной программе лишь короткие обращения к ней:

;основная программа ;процедура

... PR PROC

CALL PR OUTINT X

(a) … OUTINT Y, 8

... NEWLINE

CALL PR RET

(b) ... PR ENDP

Когда выполняется первая команда CALL, то она записывает в стек адрес следующей за ней команды (адрес а) и передает управление на начало процедуры PR – на команду OUTINT X. Начинает работать процедура: она печатает X и Y и перево­дит строку. После этого команда RET извлекает из стека находящийся там адрес а и делает переход по нему. Тем самым возобновляется работа основной программы – с команды, следующей за первой командой CALL. Повторное обращение к процедуре происходит аналогично, но по второй команде CALL в стек уже будет записан другой адрес – адрес b, поэтому процедура на этот раз вернет управление в другое место основной программы – на команду, следующую за второй командой CALL.

Теперь следует кое-что уточнить относительно команд CALL и RET.

Напомним, что в ПК адрес команды, которая должна быть выполнена следующей, задается регистрами CS и IP. Если описание процедуры размещено в том же сегменте команд, где происходит обращение к ней, тогда переход на нее и возврат из нее должны быть близкими, т. е. должен меняться только указатель команд IP и не должен меняться сегментный регистр CS, т. к. мы все время остаемся в одном и том же сегменте команд. Но если процедура расположена в другом сегменте команд, тогда переход на нее и возврат из нее должны быть дальними, т. е. должны меняться оба этих регистра.

Все это учитывается, и на самом деле в ПК имеется по два варианта машинных команд CALL и RET. Если через АВ обозначить адрес возврата – адрес (смещение) команды, следующей за командой CALL, тогда действия обоих вари­антов команды CALL Р, где Р – имя процедуры, можно описать так:

близкий вызов: АВ->стек, IP:=offset P

дальний вызов: CS->стек, АВ->стек, CS:=seg P, IP:=offset P

Действия же двух вариантов команды RET таковы:

близкий возврат: стек->IР

дальний возврат: стек->IР, стек->CS

Ясно, что команды CALL и RET должны действовать согласованно: при близком вызове процедуры и возврат из нее должен быть близким, а при дальнем вызове и возврат должен быть дальним, иначе программа будет работать неправильно. В то же время в языке ассемблера оба варианта каждой из этих команд записываются одинаково. Естественно возникает вопрос: как же тогда в языке ассемблера указываются типы переходов между основной программой и процедурой? Ответ таков: явно это не указывается, эту проблему решает за нас ассемблер. Если мы описали процедуру как близкую, тогда все команды CALL, в которых указано имя этой процедуры, ассемблер будет транслировать в машинные команды близкого вызова, а все команды RET, расположенные внутри этой процедуры, ассемблер будет транслировать в машинные команды близкого возврата. Если же процедура описана как дальняя, тогда все обращения к ней будут транслироваться как дальние вызовы, а все команды RET внутри нее – как дальние возвраты. (Вне процедур RET рассматривается как близкий возврат.) Таким образом, ассемблер берет на себя обязанность следить за соответствием между командами CALL и RET, и у нас об этом не должна болеть голова. Именно в этом выгода от описания подпрограмм в виде процедур, именно из-за этого в языке ассемблера и принято описывать подпрограммы как процедуры.

Однако здесь есть одна тонкость. Ассемблер выберет правильный вариант машинной команды для CALL, только если процедура была описана раньше этой команды. Если же процедура будет описываться позже, то ассемблер, встретив команду CALL и еще не зная тип этой процедуры (близкая она или дальняя), предполагает, что она близкая (а так чаще всего и бывает), и потому всегда в подобной ситуации формирует машинную команду близкого вызова. Но если потом ассемблер обнаружит, что данная процедура описана как дальняя, то он зафиксирует ошибку. Чтобы не было такой ошибки, при обращении к дальней процедуре, которая будет описываться позже, надо в команде CALL с помощью оператора PTR явно указать, что процедура дальняя:

CALL FAR PTR P

Другие варианты команды CALL

Мы рассмотрели основной вариант команды CALL, когда в качестве ее операнда указывается имя процедуры. Но возможны и другие варианты операнда – точно такие же, как в команде безусловного перехода JMP, за исключением случая с оператором SHORT (считается, что процедуры не располагаются рядом с командами их вызова, и потому в ПК не предусмотрен короткий переход с возвратом). Примеры:

NA DW Р

FA DD Q

...

Р PROC

...

Р1:...

...

Р ENDP

Q PROC FAR

...

Q1:...

...

Q ENDP

...

CALL P1 ;близкий переход на P1 с возвратом

CALL FAR PTR Q1 ;дальний переход на Q1 с возвратом

CALL Q1 ;близкий (!) переход на Q1

;с возвратом

CALL NA ;близкий вызов процедуры Р

CALL FA ;дальний вызов процедуры Q

LEA BX, Q

CALL [BX] ;близкий (!) вызов процедуры Q

CALL DWORD PTR [BX] ; дальний вызов процедуры Q

...

При использовании этих вариантов команды CALL надо соблюдать осторожность. Во-первых, надо следить за согласованностью типа перехода с возвратом с типом возврата по команде RET, т. к. в этих случаях ассемблер уже не отвечает на такую согласованность. Во-вторых, как и в команде JMP, при использовании в команде CALL ссылок вперед или косвенных ссылок следует, если надо, уточнять тип этих ссылок (по умолчанию при ссылке вперед ассемблер транслирует близкий прямой вызов, а при косвенной ссылке – близкий косвенный вызов).