
Процедуры
До сих пор мы рассматривали примеры программ, предназначенные для однократного выполнения. Но, приступив к программированию достаточно серьезной задачи, вы наверняка столкнетесь с тем, что у вас появятся повторяющиеся фрагменты кода. Одни из них могут состоять всего из нескольких команд, другие занимать и достаточно много места в исходном коде. В последнем случае эти фрагменты существенно затруднят чтение текста программы, снизят ее наглядность, усложнят отладку и послужат неисчерпаемым источником ошибок. В языке ассемблера есть несколько средств, решающих проблему дублирования фрагментов программного кода. К ним относятся:
-процедуры;
-макроподстановки (макроассемблер);
-генерация и обработка программных прерываний.
В данной главе рассматриваются только основные понятия, относящиеся к вызову процедур. Ввиду важности этого вопроса мы продолжим его изучение в главе 15 в контексте темы модульного программирования на ассемблере. Актуальная для программирования под Windows проблема разработки библиотек DLL на ассемблере описана в [8]. Макроассемблеру посвящена глава 14.
Процедура, или подпрограмма, — это основная функциональная единица декомпозиции (разделения на части) некоторой задачи. Процедура представляет собой группу команд для решения конкретной подзадачи и обладает средствами получения управления из точки вызова задачи более высокого уровня и возврата управления в эту точку. В простейшем случае программа может состоять из одной процедуры. Другими словами, процедуру можно определить как правильным образом оформленную совокупность команд, которая, будучи однократно описана, при необходимости может быть вызвана в любом месте программы.
Для описания последовательности команд в виде процедуры в языке ассемблера используются две директивы: PROC и ENDP.
Среди большого количества операндов директивы PROC следует особо выделить [расстояние]. Этот атрибут может принимать значения NEAR или FAR и характеризует возможность обращения к процедуре из другого сегмента кода. По умолчанию атрибут [расстояние] принимает значение NEAR.
Процедура может размещаться в любом месте программы, но так, чтобы на нее случайным образом не попало управление. Если процедуру просто вставить в общий поток команд, то процессор воспримет команды процедуры как часть этого потока и, соответственно, начнет выполнять эти команды. Учитывая это обстоятельство, есть следующие варианты размещения процедуры в программе: в начале программы (до первой исполняемой команды); в конце программы (после команды, возвращающей управление операционной системе);промежуточный вариант — внутри другой процедуры или основной программы (в этом случае необходимо предусмотреть обход процедуры с помощью команды безусловного перехода J М Р);в другом модуле (библиотеке DLL).
Размещение процедуры в начале сегмента кода предполагает, что последовательность команд, ограниченная парой директив PROC и ENDP, будет размещена до метки, обозначающей первую команду, с которой начинается выполнение программы. Эта метка должна быть указана как параметр директивы END, обозначающей конец программы:
model small
.stack 100h
.data
.code
my_proc procnear
…
ret
my_proc endp
start:
…
end start
Объявление имени процедуры в программе равнозначно объявлению метки, поэтому директиву PROC в частном случае можно рассматривать как завуалированную форму определения программной метки. Поэтому сама исполняемая программа также может быть оформлена в виде процедуры, что довольно часто и делается с целью пометить первую команду программы, с которой должно начаться выполнение. При этом не забывайте, что имя этой процедуры нужно обязательно указывать в заключительной директиве END. Такой синтаксис мы уже неоднократно использовали в своих программах. Так, последний рассмотренный фрагмент эквивалентен следующему:
model small
.stack 100h
.data
.code
my_proc procnear
…
ret
my_proc endp
start proc
…
start endp
end start
В этом фрагменте после загрузки программы в память управление будет передано первой команде процедуры с именем start.
Размещение процедуры в конце программы предполагает, что последовательность команд, ограниченная директивами PROC и ENDP, находится следом за командой, возвращающей управление операционной системе:
model small
.stack 100h
.data
.code
start:
..
mov ax,4c00h
int 21h ;возврат управления операционной системе
my_proc procnear
…
ret
my_proc endp
end start
Промежуточный вариант расположения тела процедуры предполагает ее размещение внутри другой процедуры или основной программы. В этом случае необходимо предусмотреть обход тела процедуры, ограниченного директивами PROC и ENDP, с помощью команды безусловного перехода JМР:
model small
.stack 100h
.data
.code
start:
…
jmp ml
my_proc procnear
…
ret
my_proc endp
ml:
…
mov ax,4c00h
int 21h ;возврат управления операционной системе
end start
Последний вариант расположения описаний процедур — в отдельном сегменте кода — предполагает, что часто используемые процедуры выносятся в отдельный файл, который должен быть оформлен как обычный исходный файл и подвергнут трансляции для получения объектного кода. Впоследствии этот объектный файл с помощью утилиты tlink можно объединить с файлом, в котором данные процедуры используются. С утилитой tlink мы познакомились в главе 6. Этот способ предполагает наличие в исходном тексте программы еще некоторых элементов, связанных с особенностями реализации концепции модульного программирования в языке ассемблера. Поэтому в полном объеме этот способ будет рассмотрен в главе 15.
Как обратиться к процедуре? Так как имя процедуры обладает теми же атрибутами, что и обычная метка в команде перехода, то обратиться к процедуре, в принципе, можно с помощью любой команды перехода. Но есть одно важное свойство, которое можно использовать благодаря специальному механизму вызова процедур. Суть состоит в возможности сохранения информации о контексте программы в точке вызова процедуры. Под контекстом понимается информация о состоянии программы в точке вызова процедуры. В системе команд процессора есть две команды для работы с контекстом — CALL и RET.
Команда CALL осуществляет вызов процедуры (подпрограммы). Синтаксис команды:
call [модификатор] имя_процедуры
Подобно команде JMP команда CALL передает управление по адресу с символическим именем имя_процедуры, но при этом в стеке сохраняется адрес возврата (то есть адрес команды, следующей после команды CALL).
Команда RET считывает адрес возврата из стека и загружает его в регистры CS и EIP/IP, тем самым возвращая управление на команду, следующую в программе за командой CALL Синтаксис команды:
ret [число]
Необязательный параметр [число] обозначает количество элементов, удаляемых из стека при возврате из процедуры. Размер элемента определяется хорошо знакомыми нам параметрами директивы SEGMENT — use!6 и use32 (или соответствующим параметром упрощенных директив сегментации). Если указанпараметр usel6, то [число] — это значение в байтах; если use32 — в словах.
Для команды CALL, как и для JMP, актуальна проблема организации ближних и дальних переходов. Это видно из формата команды, где присутствует параметр [модификатор]. Как и в случае команды JMP, вызов процедуры командой CALL может быть внутрисегментным и межсегментным.
При внутрисегментном вызове процедура находится в текущем сегменте кода
(имеет тип near), и в качестве адреса возврата команда CALL сохраняет только
содержимое регистра IP/EIP, что вполне достаточно.
При межсегментном вызове процедура находится в другом сегменте кода (имеет тип far), и для осуществления возврата команда CALL должна запомнить содержимое обоих регистров (CS и IP/EIP), при этом в стеке сначала запоминается содержимое регистра CS, затем — регистра IP/EIP.
Важно отметить, что одна и та же процедура не может быть одновременно процедурой ближнего и дальнего типов. Таким образом, если процедура используется в текущем сегменте кода, но может вызываться и из другого сегмента программы, то она должна быть объявлена процедурой типа far. Подобно команде JMP, существуют четыре разновидности команды CALL. Какая именно команда будет сформирована, зависит от значения модификатора в команде вызова процедуры CALL и атрибута дальности в описании процедуры. Если процедура описана в начале сегмента данных с указанием дальности в ее заголовке, то при ее вызове параметр [модификатор] можно не указывать: транслятор сам разберется, какую команду CALL ему нужно сформировать. Если же процедура описана после ее вызова, например, в конце текущего сегмента или в другом сегменте, то при ее вызове нужно указать ассемблеру тип вызова, чтобы он мог за один проход правильно сформировать команду CALL. Значения модификатора такие же, как и у команды ЗМР, за исключением значения SHORT PTR.
С директивой PROC используются еще несколько директив: ARG, RETURNS, LOCAL, USES. Их назначение — помочь программисту выполнить некоторые рутинные действия при вызове и возврате из процедуры (заодно и повысив надежность кода). Директивы ARG и RETURNS назначают входным и выходным параметрам процедуры, передаваемым через стек, символические имена. Директива USES в качестве параметров содержит имена используемых в процедуре регистров. При обработке этой директивы ассемблер формирует входной и выходной коды процедуры (из команд PUSH и POP), обеспечивающие сохранение и восстановление регистров. Директива LOCAL предназначена для выделения кадра стека для локальных переменных, что позволяет экономить память, занимаемую программой в целом. Подробно эти директивы обсуждаются в главе 15.
Необходимо заметить, что в данном разделе приведена информация о порядке описания процедур, принятом в TASM. Описание и использование процедур в MASM имеет особенности, о которых можно узнать из материала главы 15.
Последний и, наверное, самый важный вопрос, возникающий при работе с процедурами, как правильно передать параметры процедуре и вернуть результат? Этот вопрос тесно связан с концепцией модульного программирования и подробно будет рассматриваться в главе 15. С примерами использования процедур вы можете познакомиться в листингах подпрограмм, предназначенных для вычисления четырех основных арифметических действий с двоичными и десятичными (BCD) числами и находящихся среди прилагаемых к книге файлов в каталоге главы 8. Кроме того, вопросы организации рекурсивных и вложенных процедур рассмотрены в [8].