Подпрограммы

Реализация подпрограмм в ассемблере задача более сложная, чем их описание на языках высокого уровня. Во-первых, в ассемблере не существует формального деления подпрограмм на процедуры и функции, и то и другое называется процедурами. Содержательно, конечно, их можно разделить на функции и «чистые процедуры» в зависимости от того, вырабатывают они результат или нет. Во-вторых, для применения подпрограмм необходимо решить несколько вопросов.

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

Способы оформления подпрограмм. В языке ассемблера подпрограммы принято оформлять следующим образом:

 

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

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

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

 

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

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

Сообщить адрес возврата подпрограмме можно по-разному:

- Передать адрес через регистр. Основная программа сохраняет в некотором регистре адрес возврата, а процедура извлекает его оттуда и выполняет по нему переход.

- Передать адрес через стек. До вызова процедуры основная программа сохраняет адрес возврата в стеке, а процедура извлекает его и использует для перехода. Для микропроцессора 8086 и выше принято передавать адрес возврата через стек.

Команды САLL и RET позволяют упростить реализацию переходов между основной программой и процедурами. Адрес команды, следующей за инструкцией CALL, сохраняется в стеке. После этого происходит переход на первую команду вызываемой процедуры, адрес которой задан операндом инструкции CALL (табл. 71). Флаги команда не изменяет.

 

 

Табл. 71. Команда CALL.

Код Инструкция Описание
9A cd CALL ptr16:16 Дальний переход к подпрограмме.
E8 cw CALL rel16 Ближний переход к подпрограмме.
FF /2 CALL r/m16 Ближний косвенный переход к подпрограмме.
FF /3 CALL m16:16 Дальний косвенный переход к подпрограмме.

 

 

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

Команда RETвыполняет действия, обратные команде CALL, т.е. обеспечивает возврат из процедуры (табл. 72). Флаги команда не изменяет. Адрес возврата должен храниться в вершине стека. Для ближнего возврата он занимает два байта, для дальнего – четыре байта. Процессор извлекает адрес возврата и заносит его в регистры CS (при дальнем возврате) и IP, при этом содержимое указателя стека SP увеличивается на 2 или 4.

 

 

Табл. 72. Команда RET.

Код Инструкция Описание
C3 RET Ближний возврат из процедуры.
CB RET Дальний возврат из процедуры.
C2 iw RET imm16 Ближний возврат из процедуры с очисткой стека.
CA iw RET imm16 Дальний возврат из процедуры с очисткой стека.

 

 

Команды CALL и RET действуют согласованно: при ближнем вызове процедуры возврат из нее должен быть ближним, а при дальнем вызове возврат должен быть дальним. Мнемоники обеих команд одинаковы. Типы переходов между основной программой и процедурой определяет ассемблер.

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

Если процедура окажется дальней, то будет зафиксирована ошибка. Чтобы ее избежать, при обращении к дальней процедуре, которая определена после вызова команды CALL, с помощью оператора PTR необходимо явно указать тип процедуры: CALL FAR PTR P.

Способы передачи параметров. При вызове подпрограммы основная программа записывает фактические параметры (их значения или адреса) в стек, а процедура затем их оттуда извлекает. Пусть процедура Р имеет k параметров: P(a1,a2,...,ak). Будем считать, что параметры записываются в стек слева направо. Пусть каждый параметр имеет размер слова. Тогда команды основной программы, реализующие обращение к процедуре, будут следующими:

 

; обращение Р(a1,а2,...,ak)

PUSH a1

PUSH a2

...

PUSH ak

CALL P

(AB)...

 

На рис. 23 показано состояние стека после выполнения команд, т.е. на входе в процедуру.

 

 

 
 

 

 


Рис. 23. Содержимое стека при вызове подпрограммы.

 

 

Для доступа к элементам стека необходимо использовать регистр BP. Для этого в стеке следует сохранить содержимое BP, передать в него адрес вершины стека (содержимое регистра SP), а затем использовать выражение вида [BP+i] для доступа к параметрам процедуры. Таким образом, действия при входе в процедуру будут следующими:

 

; действия при входе в процедуру

P PROC ; объявление процедуры P

PUSH ВР ; сохранить ВР

MOV BP, SP ; BP - на вершину стека

... ; команды процедуры

 

После сохранения в стеке регистра ВР для доступа к параметрам процедуры следует использовать выражения: [BP+4] – для доступа к последнему параметру, [ВР+6] – для доступа к предпоследнему параметру и т.д. Например, запись последнего параметра в регистр АХ (AX:=ak) будет выполнен командой MOV АХ, [ВР+4] (рис. 24).

 

 
 

 


Рис. 24. Содержимое стека при выходных действиях подпрограммы.

После извлечения из стека параметров должны следовать команды самой процедуры. После их завершения процедура обязана восстановить состояние стека, например, командой MOV SP, BP. В результате к концу работы процедуры в вершине стека будет находиться прежнее значение регистра ВР. После восстановления ВР, в вершине стека будет находиться адрес возврата.

Очистку стека от параметров может выполнить основная программа. Для этого в ней после команды CALL P необходимо выполнить команду ADD SP,2*k. Однако такой способ не является оптимальным, т.к. обращений к процедуре, как правило, много и вызывать команду ADD придется многократно. Лучше возложить обязанность по очистке стека на саму процедуру.

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

Операнд команды RET указывает, на сколько байт требуется очистить стек. Например, для очистки стека от k параметров, каждый из которых имеет размер слова, операнд должен быть равен 2*k. Адрес возврата в операнде не учитывается, т.к. команда RET считывает его до очистки стека. Команда RET эквивалентна RET 0, т.е. выполняет возврат без очистки стека. Таким образом, при выходе из процедуры действия должны быть следующими:

 

; действия при выходе из процедуры

POР ВР ; восстановить прежнее значение BP

RET 2*k ; очистка стека от k параметров-слов и возврат

ENDP

 

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

Хранение локальных данных процедур. Для хранения локальных данных процедур (временных переменных, необходимых только на время выполнения процедуры) можно использовать регистры. Однако если процедура требует много локальных переменных, регистров может не хватить. Поэтому лучше резервировать место в стеке. При входе в процедуру в вершине стека выделяется требуемое число байтов для локальных данных, а перед выходом они освобождаются. В результате место в памяти выделяется только на время выполнения процедуры.

Для выделения памяти необходимо запомнить в стеке текущее значение регистра ВР и затем установить его на вершину стека. Затем необходимо уменьшить значение указателя стека SP на число выделяемых байтов. Например, если процедуре Р требуется 3 байта (предположим, 2 байта под локальную переменную А и 1 байт под локальную переменную В), тогда указанные действия реализуются следующими командами:

 

P PROC

PUSH ВР ; сохранение в стеке содержимого BP

MOV BP, SP ; BP - на вершину стека

SUB SP, 3 ; резервирование места в стеке

... ; команды процедуры

 

На рис. 25 показано состояние стека после выполнения команд. Доступ к локальным данным процедуры выполняется с помощью выражений вида [BP-k]. Например, [BP-2] есть адрес локальной переменной A, а [BP-3] – адрес локальной переменной В.

При завершении работы процедуры следует выполнить обратные действия:

 

MOV SР, ВР ; освобождение локальных данных

POP ВР ; восстановление прежнего значения ВР

RET ; возврат из процедуры

Р ENDS ; конец процедуры P

 

 


Рис. 25. Содержимое стека при хранении локальных данных.

Контрольные вопросы

1. Опишите структуру подпрограммы на языке ассемблера.

2. Перечислите способы размещения подпрограмм.

3. Объясните механизм вызова подпрограмм.

4. Как реализуются способы передачи параметров подпрограмм.