В настоящем приложении описана техника составления процедур при программировании на Макроассемблере. Особое внимание уделено рассмотрению условий, при которых такие процедуры могут использоваться в программах, составленных на языках высокого уровня Си, Паскаль, Фортран и др. Именно ради этого данное приложение включено в текст книги.
При входе в ближнюю (near) подпрограмму и при возврате из нее текущее содержимое сегментного регистра cs не изменяется. Это означает, что вызов ближней подпрограммы возможен только из того сегмента, в котором она описана. Все подпрограммы, приведенные в примерах основной части книги и ее приложений, являются ближними, поэтому они могут располагаться только в разделе кодов задачи.
От простой группы команд ближняя подпрограмма отличается только тем, что первая команда обязательно имеет метку (имя), а последней выполняемой командой является ret или retn (это два разных имени одной инструкции). Она (ret) просто выталкивает содержимое верхушки стека в счетчик команд (регистр IP) и увеличивает адрес указателя стека на 2, т. е. равноценна команде pop ip. В результате происходит возврат на вызывающий модуль.
Выражение "последняя выполняемая команда" надо понимать буквально, ret завершает не текст подпрограммы, а ее выполнение. В простых случаях она может завершать текст подпрограммы, но если последняя имеет разветвления, то каждая ветвь может заканчиваться командой ret.
Обращение к подпрограмме (ее вызов) выполняет специальная команда сан, содержащая адрес точки входа. Для его указания можно использовать все стандартные способы адресации, например, имя точки входа, явное задание адреса, выбор адреса из регистра и т. д. Команда call помещает в стек адрес возврата и выполняет безусловный переход на указанную точку входа. Адресом возврата является текущее содержимое счетчика команд (IP). После выборки кода инструкции и операндов IP всегда содержит адрес начала следующей команды. Таким образом, специальная команда call нужна для того, чтобы сформировать в стеке адрес возврата для команды ret, завершающей выполнение подпрограммы.
Подпрограмма может работать, и обычно работает, со стеком. Причем к моменту выполнения команды ret в верхушке стека должен находиться адрес возврата. Сказанное не означает, что его нельзя изменять. Это делается в особых случаях, когда по каким-то причинам надо вернуться не на вызывающий модуль, а в любое другое место задачи.
Дальние подпрограммы
Дальняя (far) подпрограмма отличается от ближней тем, что она расположена не в том сегменте, в котором находится вызывающий модуль. Поэтому при обращении к удаленной подпрограмме изменяется содержимое не только счетчика команд (IP), но и кодового сегментного регистра (cs).
Мнемоническим именем команды вызова в любом случае является call, но ему могут соответствовать разные коды операций (машинных инструкций). Обнаружив в тексте команду call, Макроассемблер анализирует описание указанного в ней имени, и в зависимости от его типа (far или near) выбирает нужный код операции вызова подпрограммы. В частности, если имя соответствует удаленной процедуре, то будет выбран код операции, при выполнении которого в стек записывается сначала содержимое сегментного регистра cs, а затем счетчика команд IP. Таким образом, при входе в дальнюю подпрограмму в верхушке стека находится исходное значение IP, а перед ним — значение cs.
Последней выполняемой командой дальней подпрограммы является retf, она выталкивает из верхушки стека не одно, а два слова. Первое слово выталкивается в счетчик команд IP, а второе — в сегментный регистр cs. В результате в регистрах cs:ip оказывается полный адрес точки возврата.
Сегмент, содержащий дальнюю подпрограмму, может входить в тело задачи или располагаться во внешнем модуле. Во втором случае подпрограмма оказывается доступной не для одной, а для разных задач.
Описание подпрограмм
Для оформления подпрограмм предназначены две директивы PROC и ENDP. Первая объявляет начало блока подпрограммы, а вторая — его конец. Перед обеими директивами указывается одно и то же имя, которое является именем точки входа в подпрограмму.
Упрощенная форма директивы PROC имеет следующий вид:
имя_подпрограммы PROC far ИЛИ near
Обратите внимание на отсутствие символа "двоеточие" после имени подпрограммы. Слова far или near задают тип процедуры, т. е. характеризуют ее удаленность от точки вызова.
Явное описание процедуры с помощью указанных директив
упрощает работу программиста. При обработке директивы PROC Макроассемблер
помещает в свои рабочие таблицы имя и тип подпрограммы. Теперь, обнаружив
в вызывающем модуле команду call, он по имени процедуры сам определит
соответствующий ей код операции.
Кроме того, при компиляции блока подпрограммы, обнаружив в тексте команду
ret, Макроассемблер выберет ее код (retn или retf) для корректного возврата
в вызывающий модуль.
Упрощенная форма директивы PROC применима при работе с любой версией Макроассемблера, начиная с 5.1. В последующих версиях MASM появилась расширенная форма директивы PROC (см. раздел В.5).
Дополнительные точки входа
В зависимости от конкретного назначения подпрограмма может иметь не одну, а несколько точек входа. Для описания дополнительных точек входа в процедуры применяется специальная директива:
name LABEL far ИЛИ near
Здесь name соответствует имени точки входа, a far или near указывает ее удаленность от точки вызова. Данная директива просто описывает удаленную метку, независимо от ее конкретного назначения. Если она является точкой входа в подпрограмму, то для вызова используется команда call name. А если это продолжение программы, расположенное в друге сегменте, то переход на него выполняет команда jmp name. Пример описания подпрограмм. Для работы с окнами видеопамяти в основной части книги неоднократно использовались процедуры и Prevwin, их исходный текст приведен в примере 2.8. Покажем (см. пример В.1), что изменится в этом тексте, если процедуры явно описать как удаленные.
Пример В.1. Три подпрограммы для работы с видеоокнами
NxtWin PROC far описание процедуры NxtWin
push ax сохраняем содержимое ах
mov ax, GrUnit читаем единицу приращения окна
add Cur_win, ax увеличиваем номер окна
pop ax восстанавливаем содержимое ах
SetWin LABEL far точка входа в процедуру SetWin
@@: PushReg <ax,bx,dx> сохранение содержимого регистров
хог bx, bx признак установки окна
mov dx, Cur_win номер устанавливаемого окна
call [VMC] обращение к подпрограмме BIOS
PopReg <dx,bx,ax> восстановление содержимого регистров
ret возврат в вызывающий модуль
PrevWin LABEL far точка входа в процедуру PrevWin
push ax сохранение содержимого ах
mov ax, GrUnit читаем единицу приращения окна
sub Cur_win, ax уменьшаем номер окна
pop ax восстанавливаем содержимое ах
jmp SHORT @B переход на установку окна
NxtWin ENDP конец процедуры NxtWin
По сравнению с исходным текстом в примере В.1 добавились директивы, описывающие блок процедуры Nxtwin, и две дополнительные точки входа Setwin и Prevwin. Кроме того, введена локальная метка @@, переход на нее выполняет команда jmp SHORT @в. В оригинале ей соответствовала команда jmp SHORT Setwin. В данном случае метка Setwin описана как удаленная, короткий переход на нее не возможен, поэтому введена локальная метка.
Замечание
Подпрограммы примера В.1 еще нельзя использовать для работы. Предварительно
их надо оформить в виде программного модуля и объявить общедоступными.
Как это делается, описано в следующем разделе.
Внешние и внутренние переменные. Все переменные, описанные в тексте конкретной программы (далее — в модуле), являются внутренними или локальными. Для того чтобы некоторые из них или все стали общедоступными, их имена надо перечислить в списке следующей директивы:
PUBLIC name[[, name]]
Такая форма записи означает, что в директиве может быть указано столько имен, сколько поместится в строке, и что они отделяются друг от друга запятыми. После последнего символа запятая недопустима, для удобства чтения после запятых лучше делать пробел. Директиву можно повторять столько раз, сколько требуется для перечисления всех имен. В теле программы она обычно располагается перед описанием первого сегмента.
При обработке директивы Макроассемблер определяет типы имен по их описаниям в тексте программы и помешает список имен с указанием типов в объектный модуль. Эти данные нужны компоновщику (iink.exe), они имеют специальное назначение и не влияют на размер будущей задачи. Если вы посмотрите листинг файла, который может формировать Макроассемблер, то увидите, что таким именам присвоена характеристика Global.
Для того чтобы имена, объявленные общедоступными или глобальными, можно было использовать в другом модуле, их надо описать в нем как внешние с помощью следующей директивы:
EXTERN name:type[[, name:type]]
В программном модуле данная директива обязательно располагается перед описанием первого сегмента. Если список внешних имен большой, то директива повторяется нужное число раз. Тип зависит от назначения имени. Переменные могут иметь типы byte, word, dword и т. д. Метки и имена подпрограмм имеют тип far.
Макроассемблер заносит перечисленные имена и их типы в таблицы, которые он создает в процессе компиляции, и при каждом обнаружении любого из имен проверяет соответствие применения имени его типу. Как обычно, если использование имени в тексте модуля не соответствует его типу, выводится аварийное сообщение.
Правильность указания самих имен Макроассемблер проверить
не может.
Внешние имена и их типы нужны компоновщику (iink.exe), поэтому Макроассемблер
помещает их в объектный модуль. Это специальная информация, она не влияет
на размер строящейся задачи.
Таким образом, общедоступные имена должны быть описаны в двух модулях — источнике и приемнике. В источнике они перечисляются в директиве PUBLIC, а в приемнике — в директиве EXTERN. С их описаниями работает компоновщик. Если в одном из модулей встречается внешнее имя, то он ищет его в списках глобальных имен других модулей. При отсутствии соответствующего описания будет выдано сообщение об ошибке.
На первой стадии подготовки исходного модуля производится составление, набор текста и отладка подпрограмм, поэтому модуль удобнее включить в текст основной программы в виде отдельного сегмента, а не хранить в отдельном файле. В примере В.2 показан вариант оформления дополнительного сегмента в основной программе.
Пример В.2. Сегмент с описанием подпрограмм NxtWin, SetWin, PrevWin
subr SEGMENT word public 'subr1 ; начало сегмента
ASSUME cs:subr, ds:@data ; установка соответствия ; .386 ; тип микропроцессора
; Далее располагается текст примера В.1, содержащий
; описание подпрограмм NxtWin, SetWin и PrevWin subr ENDS ; конец сегмента
Первая директива примера В.2 открывает описание сегмента. В данном случае его параметры можно было не указывать, они приведены просто для иллюстрации. Параметр word обозначает, что сегмент располагается в памяти, начиная с четного адреса. Параметр public является признаком общедоступного сегмента. Заключенное в кавычки название сегмента передается компоновщику и становится общедоступным.
Замечание
Обратите внимание на то, что между параметрами директивы SEGMENT отсутствуют
запятые!
Директива ASSUME нужна для того, чтобы Макроассемблер мог определить, что будет находиться в сегментных регистрах cs и ds при выполнении подпрограмм. Без этого невозможна компиляция команд. С регистром cs всегда связывается имя сегмента, в котором расположена директива ASSUME. С регистром ds связывается имя сегмента данных, который описан вне данного модуля. Имя, с которым ассоциируется ds, зависит от способа описания сегмента данных в основном тексте программы. В примере В.2 предполагается, что сегмент данных был описан с помощью специальной директивы .data (см. раздел в приложении Б).
Если вы забудете указать директиву ASSUME, то при компиляции Макроассемблер может выдавать аварийные сообщения, смысл которых заключается в том, что не определен один из сегментных регистров. Чтобы лучше понять назначение директивы, уберите ее из текста и посмотрите, что из этого получится. В частности, результат зависит от версии MASM.
В примере В.2 строка, содержащая третью директиву, начинается с символа "точка с запятой". Подпрограммы Nxtwin, setwin и Prevwin составлены с использованием набора команд микропроцессора Intel 8086, поэтому в данном случае директива .386 не нужна. Однако большинство описанных в книге примеров рассчитано на возможности микропроцессора Intel 80386, и для их компиляции данная директива необходима.
Разрядность сегмента
При обработке директив описания сегментов Макроассемблер проверяет установленный тип микропроцессора и выбирает соответствующий режим выполнения команд, расположенных в сегменте (реальный или защищенный). По умолчанию установлен 16-разрядный (реальный) режим выполнения команд и набор инструкций для микропроцессора Intel 8086.
Если директива .386 предшествует описанию сегмента, то он будет объявлен как 32-разрядный, расположенные в нем команды будут рассчитаны на работу с 32-разрядными адресами. В реальном режиме результаты выполнения таких команд непредсказуемы.
Поэтому при создании программ или подпрограмм, предназначенных для выполнения в реальном режиме работы микропроцессора, директиву .386 надо располагать после описания сегмента. В таком случае она оказывает влияние только на набор инструкций микропроцессора.
Начиная с версии 6.0, Макроассемблер поддерживает директивы .486 и .586, разрешающие использование новых инструкций микропроцессоров Intel 486 и Pentium. Кроме того, появилась возможность выбора разрядности сегментов по умолчанию или ее явного описания с помощью ключевых слов USE16 И USE32.
В конце файла, формируемого Макроассемблером, приводится описание всех сегментов программного модуля. При компиляции обязательно укажите имя файла и затем проверьте, соответствуют ли указанная в нем разрядность сегментов той, которую вы предполагаете.
Расположение сегмента в тексте программы зависит от версии MASM, который вы используете. Если это MASM 6.0 и выше, то дополнительный сегмент может располагаться как перед основным текстом программы (перед сегментом кодов), так и после него. Но если вы работаете с MASM 5.1, то дополнительный сегмент может располагаться только перед сегментом кодов. В противном случае при каждом вызове подпрограмм, расположенных в дополнительном сегменте, MASM 5.1 выводит аварийное сообщение о необходимости предварительного описания подпрограммы.
Подключение исходного модуля.
Расположенные в теле задачи модули не являются общедоступными. Поэтому после отладки дополнительный сегмент с описанием подпрограмм удаляется из текста программы и помещается в отдельный файл. Имя, тип и расположение файла на жестком или гибком диске вы можете выбирать по своему усмотрению.
Для включения содержимого файла в нужном месте текста программы указывается специальная директива:
INCLUDE спецификация_файла
Спецификация должна быть настолько подробной, чтобы Макроассемблер мог найти и прочитать файл. Очень часто эта директива применяется для подключения файлов, содержащих тексты макроопределений. Обычно в установочный комплект MASM включено несколько таких файлов, они могут располагаться в специальном каталоге INCLUDE.
В данном случае нас интересуют файлы, содержащие исходные тексты общедоступных подпрограмм. На состав и назначение подпрограмм не налагается никаких специальных ограничений, должны лишь соблюдаться общие правила оформления программных сегментов, а именно.
каждый сегмент имеет уникальное имя; размер кода после компиляции не превышает 65 536 байтов; в тексте отсутствуют ошибки, т. е. он должен быть предварительно отлажен.Таким образом, содержимое включаемого файла становится частью текста программы и компилируется Макроассемблером. Если в задании на компиляцию указан файл листинга, то, просмотрев его, вы увидите полный результат компиляции, в том числе и включенного файла.
Пример объектного модуля
Для получения объектного модуля надо внести изменения в подключаемый модуль и откомпилировать его отдельно от основной задачи.
Изменения заключаются в том, что в начале текста модуля добавлены две директивы:
EXTERN — для описания используемых внешних имен; PUBLIC — для объявления подпрограмм модуля общедоступными.Кроме этого, нужен признак конца текста модуля, которым является директива END. В примере В.З показано, что изменится в модуле примера В.2.
Пример В.З. Исходный текст для получения объектного модуля
; Сюда надо вставить макроопределения из примера 2.12
subr SEGMENT word public 'subr' ; начало сегмента
EXTERN GrUnit:word, Cur win:word, VMC:dword
PUBLIC NxtWin, SetWin, PrevWin
ASSUME cs:subr ; установка соответствия
; .386 ; тип микропроцессора
; Далее располагается текст примера В.1, содержащий
; описание подпрограмм NxtWin, SetWin и PrevWin
subr ENDS ; конец сегмента
END ; конец текста модуля
Текст примера В.З начинается с комментария, напоминающего о том, что перед описанием сегмента надо расположить макроопределения PushReg и PopReg. Они используются в подпрограмме SetWin для сохранения в стеке и последующего восстановления содержимого регистров ах, bx и dx (см. пример В.1). Можно отказаться от их включения, заменив первый макровызов тремя командами push, а второй тремя командами pop. Раньше мы об этом не говорили, поскольку модуль предназначался для совместной компиляции с текстом программы, в котором описаны указанные макроопределения.
В директиве EXTERN перечислены имена переменных GrUnit, cur_win и VMC, которые описаны в сегменте данных основной программы. Назначение и способ определения значений этих переменных подробно обсуждались в главе 2 , а их описание показано в примере 2.11 той же главы.
Следующая директива PUBLIC объявляет имена подпрограмм Nxtwin, setwin и PrevWin общедоступными.
Обратите внимание на то, что в директиве ASSUME описан только кодовый сегмент, а если вы работаете с MASM 6.0 или более поздней версией, то эту директиву можно вообще исключить.
Далее в модуле должно располагаться описание подпрограмм, текст которого приведен в примере В.1, а после него директива END без указания метки, поскольку модуль не является выполняемой задачей.
Текст примера В.З не может быть использован для включения в основную программу с помощью директивы INCLUDE. Он компилируется отдельно. При этом Макроассемблер формирует объектный модуль, который понадобится компоновщику при построении задачи.
Для компоновки нужен еще один объектный модуль. Он получается при компиляции основного текста задачи. Если в основном тексте описаны подпрограммы для работы с окнами видеопамяти, то их оттуда надо удалить. Кроме того, в основной текст надо включить следующие две директивы:
PUBLIC GrUnit, Cur_win, VMC
EXTERN NxtWin:far, SetWin:far, PrevWin:far
Первая из них объявляет переменные GrUnit, cur_win и VMC общедоступными, а вторая описывает имена и типы внешних подпрограмм. После включения указанных директив основной текст задачи компилируется для получения объектного модуля.
Построение задачиУсловимся считать, что файл, содержащий объектный
модуль основного текста будущей задачи, имеет имя bmpsuper.obj, а файл,
содержащий объектные модули подпрограмм, имя bmpsub.obj. Для их объединения
в одну задачу выполняется следующая команда:
link bmpsuper bmpsub или link bmpsuper+bmpsub
В данном случае предполагается, что файлы bmpsuper и bmpsub имеют тип obj и расположены в том же каталоге, в котором находится задача iink.exe (компоновщик). Если это не так, то указывается спецификация, позволяющая найти файлы в других каталогах.
Важно
Имена объединяемых файлов может разделять либо пробел, либо знак "плюс".
Если между именами поставить запятую, то компоновщик будет обрабатывать
два файла независимо друг от друга, т. е. он попытается построить две
разные задачи. Разумеется, это приведет к ошибке, поскольку в каждом из
файлов будут обнаружены неопределенные внешние имена.
Первым в списке должен располагаться файл, содержащий основной текст задачи, из которого вызываются подпрограммы, описанные в последующих файлах. Если имя строящейся задачи явно не указано, то ей будет присвоено имя первого файла и тип ехе. В нашем случае имя задачи bmpsuper.exe. Если компоновщик не обнаружил ошибок, то задачу можно выполнять.
Заключение.
При программировании на ассемблере можно использовать подпрограммы, хранящиеся
либо в исходных модулях, подключаемых во время компиляции основного текста,
либо в виде объектных модулей, объединяемых с главным модулем при построении
задачи. Чему отдать предпочтение, решать вам.
Однако если основной модуль создается на одном из алгоритмических языков
(Фортран, Паскаль, Си и пр.), то вспомогательные подпрограммы, составленные
на ассемблере, должны оформляться в виде объектных модулей.
И последний совет. Постепенно у вас накопится достаточно много объектных
модулей. Для упрощения собственного труда их лучше объединить в одну или
несколько библиотек. В комплект поставки Макроассемблера обязательно входит
библиотекарь, хранящийся в файле нь.ехе. Он выполняет много полезных функций,
связанных с созданием, просмотром, пополнением и изменением библиотек
объектных модулей. Библиотекарь поддерживает активный диалог с оператором,
поэтому научиться работать с ним несложно.
Характерной особенностью подпрограмм является то, что используемые при вычислениях величины передаются им в виде входных параметров. В свою очередь, подпрограммы могут возвращать результаты вычислений в виде выходных параметров. Способ доступа к параметрам зависит от того, где они расположены.
Наиболее простой формой является передача параметров в регистрах общего назначения. Она используется процедурами BIOS и DOS. В этом случае доступ к параметрам осуществляется наиболее просто и быстро, но существует ряд ограничений, сводящих на нет это преимущество. Здесь для нас существенно одно из таких ограничений, а именно то, что компиляторы с алгоритмических языков обычно не используют регистры для передачи параметров. В некоторых из них, например в Си++, предусмотрена возможность размещения параметров в регистрах, но это уже относится к категории трюков или уловок, но не к стандартам программирования.
Если создаваемая вами подпрограмма предназначена для использования в программах, составленных на одном из алгоритмических языков, то она должна быть рассчитана на расположение параметров в стеке, поскольку в большинстве случаев компиляторы размещают их именно в нем. К описанию особенностей компиляторов мы вернемся после обсуждения техники передачи параметров при программировании на ассемблере.
Общие сведенияСтеком называется любой произвольно выбранный блок оперативной памяти, работа с которым производится по принципу "последнее записанное — первое считанное" (LIFO - last in first out). Иначе говоря, выражение "стек" характеризует не тип памяти, а способ работы с ней.
В процессе выполнения задачи в регистре ss хранится код сегмента оперативной памяти, в котором расположена область стека, а текущий адрес (смещение) верхушки стека в этом сегменте хранится в регистре SP, который называется указателем стека. Разрядность регистров зависит от режима работы микропроцессора: в реальном режиме они содержат по 16 разрядов, а в защищенном режиме по 32 разряда.
Стек нарастает в сторону уменьшения адресов, поэтому
при входе в задачу регистр зр содержит наибольший доступный адрес в области
стека. При записи данных в стек адрес, хранящийся в зр, уменьшается, а
при их выборке из стека — увеличивается.
Для записи и чтения данных в режиме LIFO предназначены команды PUSH и
POP, которые неоднократно использовались в примерах. Команда push предварительно
уменьшает содержимое регистра sp, а затем записывает операнд в вычисленный
адрес. Команда pop, наоборот, сначала считывает операнд, а затем увеличивает
содержимое зр. В обоих случаях адрес, хранящийся в sp, изменяется на размер
операнда, который может составлять 2 или 4 байта. Один байт записать в
стек нельзя, команда push преобразует его в слово.
Кроме команд push и pop в режиме LIFO со стеком работают
команды вызова обычных (сан) и прерывающих (int) подпрограмм и команды
возврата из
НИХ (ret И iret).
Для непосредственного доступа к области стека выделен специальный регистр ВР. Он может использоваться в обычных командах (пересылки, сложения и пр.) в тех случаях, когда один их операндов расположен в области стека. Если операнд находится в регистре bp, то при его обработке микропроцессор, по умолчанию, выбирает в качестве сегментного регистра ss, a не DS, как обычно. В случае необходимости можно явно указать любой другой сегментный регистр.
Новое макроопределение
Перед обращением к подпрограмме в стек записываются ее параметры. Запись в стек обычно выполняет команда push. Для сокращения текста программы и придания ему большей наглядности можно использовать специальный макровызов. Текст соответствующего макроопределения приведен в примере В.4.
Пример В.4. Макроопределение для вызова подпрограмм
@Invoke macro name, par ; заголовок макроопределения
irp r, <par> ; начало оператора повторения
push r ; заготовка повторяемой команды
endm ; конец оператора повторения
call name ; заготовка команды вызова подпрограммы
endm ; конец текста макроопределения
В приведенных ранее примерах неоднократно использовался макровызов FushReg, текст его макроопределения приведен в примере 2.12. В данном случае к этому тексту добавилась только одна строка, содержащая заготовку команды сан. Поэтому в результате макроподстановки в текст программы сначала будет включена группа команд push, а затем команда call.
Если макроопределение примера В.4 включено в текст основной задачи, то для его использования в нужном месте указывается следующий макровызов:
@Invoke имя_процедуры <список_параметров>
Имя процедуры может быть как внешним, так и внутренним. Список параметров обязательно заключается в угловые скобки, а параметры отделяются друг от друга запятыми, после которых допустим пробел. Форма записи параметров стандартная для команды push. Пример использования макровызова приведен в конце данного раздела.
Доступ процедур к параметрамПри входе в процедуру в верхушке стека расположен адрес возврата, а перед ним параметры. Для работы с параметрами входящие в тело процедуры команды должны иметь прямой доступ к области стека. Как было сказано выше, для этой цели удобно использовать регистр вр, но при входе в подпрограмму его содержимое не определено. Поэтому в начале подпрограммы надо сохранить исходное содержимое bр и записать в него опорный (базовый) адрес, которым является текущее значение указателя стека.
охранение содержимого bр нужно для того, чтобы выполнение данной подпрограммы не влияло на выполнение вызывающего модуля. При выходе из подпрограммы перед выполнением команды ret сохраненное значение надо вытолкнуть в bр.
Таким образом, большинство подпрограмм, ориентированных на работу со стеком, начинается с двух следующих команд:
push bp ; сохранение исходного содержимого bpmov
bp, sp ; запись в bpдреса верхушки стека
Давайте уточним, что находится в стеке после выполнения этих команд. Для определенности будем считать, что вызвана внешняя подпрограмма, имеющая два параметра, каждый из которых занимает одно слово. Микропроцессор работает в реальном режиме, поэтому адрес возврата занимает два слова. В таком случае стек содержит величины, приведенные в табл. В.1.
Таблица В.1. Вариант размещения данных в стеке
Смещение |
Что находится
в слове |
bp+ 0 |
Исходное содержимое
регистра bp |
bp+ 2 |
Младшая часть
адреса возврата (IP) |
bp+ 4 |
Старшая часть
адреса возврата (cs) |
bp+ 6 |
Второй параметр
подпрограммы |
bp+ 8 |
Первый параметр
подпрограммы |
В соответствии с табл. В.1, при сделанных выше допущениях, полный адрес первого параметра равен ss: [bp+8], а второго — ss: [bp+б]. Как уже говорилось, сегментный регистр ss в записи операндов не указывается, поскольку в данном случае он используется по умолчанию. Например, произведение параметров можно вычислить с помощью двух команд:
mov ах, [bp+б] ; ах = значение первого параметра
mul [bp+81 ; dx:ax = ах * значение второго параметра
В общем случае адрес параметра, т. е. значение, прибавляемое к ьр, зависит от размеров предшествующих величин и его собственного размера. Поэтому при разработке подпрограммы надо предварительно определить, что конкретно будет находиться в стеке при ее вызове.
Внешняя процедура cnvindec. Рассмотрим простую внешнюю процедуру, которая преобразует последовательность десятичных цифр, представленных в коде ASCII, в код десятичного числа. Цифры в коде ASCII получаются, например, при вводе чисел с клавиатуры. Для того чтобы результат ввода можно было использовать при вычислениях, последовательность цифр надо преобразовать в шестнадцатеричный код числа.
Алгоритм формирования десятичного числа следующий. Обозначим формируемое число как result и предположим, что в исходном состоянии result = о. В таком случае на шаге номер I значение result умножается на 10 и к произведению прибавляется код очередной цифры:
result = result * 10 + digit [I]
Перед прибавлением кода очередной цифры его надо преобразовать в двоичный код. В формате ASCII коды цифр изменяются от 30h до зэь, поэтому для преобразования из кода цифры вычитается код нуля (зсш). Кроме того, надо проверить, действительно ли очередной символ строки является цифрой, и если это не так, то процесс формирования числа прекращается.
Завершенный текст подпрограммы приведен в примере В.5. Перед ее вызовом в стеке указывается полный адрес преобразуемой строки (сегмент и смещение).
Сформированное число помещается в стек на место адреса строки. Кроме того, при возврате из подпрограммы в регистре ai находится код символа, при обнаружении которого было прекращено формирование результата. Им может быть любой символ, кроме цифры. Вариант обращения к подпрограмме описан ниже.
Пример В.5. Исходный текст процедуры cnvindec
PUBLIC cnvindec объявляем процедуру общедоступной
subr SEGMENT word public 'subr'; начало сегмента subr
ASSUME cs:subr cs ассоциируется с subr
.386 задаем тип микропроцессора
dten dd 10 константа для умножения на 10
cnvindec PROC far начало блока процедуры
push bp сохранение содержимого bp
mov bp, sp bp = sp базовый адрес в стеке
push edx сохраняем содержимое edx
push fs сохраняем содержимое fs
push si сохраняем содержимое si Ifs si,
[bp+6] fs:si = адрес начала строки текста mov dword ptr [bp+6], 0; result
= 0 очистка результата
cnvloop: xor eax, eax очистка еах
lods byte ptr fs:[s ]; al = очередной символ строки
cmp al, '0' код символа меньше кода цифры 0 ?
jb endcnv ; -> да, конец формирования числа
cmp al, '9' К°Д символа больше кода цифры 9 ?
ja endcnv -> да, конец формирования числа
sub al, 30h вычитаем код цифры О
xchg eax, [bp+6j переставляем еах и result
mul cs:dten edx:eax = result * 10
add [bp+6], eax result = result + eax
jmp short cnvloop -> на начало цикла преобразования
endcnv: pop si восстанавливаем содержимое si
pop fs восстанавливаем содержимое fs
pop edx восстанавливаем содержимое edx
pop bp восстанавливаем содержимое bp
ret возврат из подпрограммы
cnvindec ENDP конец блока процедуры
subr ENDS конец сегмента subr
END конец текста модуля
Подпрограмма примера В.5 оформлена в виде готового для компиляции модуля. Способ оформления такого модуля описан в предыдущем разделе и показан в примере В.З. Поэтому мы начнем с основного текста.
В сегменте subr перед текстом процедуры описано двойное слово dten и ему присвоено значение 10. Эта переменная используется в процедуре при умножении, она нужна потому, что операндом команды mul не может быть константа 10.
Процедура преобразования имеет имя cnvindec. Ее текст начинается с подготовки регистра bpсохранения в стеке используемых регистров. После этого в регистры fs:si загружается адрес преобразуемой строки.
Важно
Перед вызовом процедуры в стек сначала записывается сегмент, а затем смещение
строки. Только при выполнении этого условия команда Ifs поместит в регистр
fs код сегмента, а в регистр si — смещение.
После загрузки адреса строки в регистры параметры не нужны и отведенное
для них место используется для размещения формируемого числа. Предварительно
команда mov dword ptr [bp+6],
о очищает два слова стека с адресами [bp+6]
И [bp+8].
Цикл формирования числа начинается с команды, имеющей метку
cnvloop, и заканчивается командой jmp short cnvloop. Код формируемого
числа может содержать до 32-х разрядов, поэтому вычислительные операции
выполняются с операндами, имеющими размер двойного слова.
Цикл начинается с очистки регистра еах и записи в его младший байт
(al) кода очередного символа. Затем проверяется, чему соответствует этот
код. Если он соответствует цифре, то из содержимого
al вычитается код цифры 0, производится перестановка содержимого
еах и result и выполняется умножение result
* 10. В связи с тем, что константа dten
расположена в кодовом сегменте, в команде mui перед ней явно указано имя
регистра cs. Младшая часть результата умножения (содержимое еах) прибавляется
к result и происходит возврат на начало цикла
формирования числа.
Если очередной символ не является цифрой, то выполнение цикла прекращается
и происходит переход на метку endcnv. Начиная
с этой метки расположены команды, восстанавливающие содержимое сохраненных
в стеке регистров, и команда retf, завершающая выполнение подпрограммы.
Замечание
Значение формируемого подпрограммой примера В. 5 числа может изменяться
в пределах от 0 до 4294967295 (232 — 1). Контроль переполнения
результата отсутствует, это сделано для упрощения текста подпрограммы.
При необходимости вы можете ввести такой контроль или увеличить диапазон
допустимых значений числа. Напомним, что при умножении на 10 старшая часть
произведения находится в регистре edx, но подпрограмма не работает с этим
регистром.
Использование процедуры cnvindec. Для использования в задачах модуль примера В. 5 компилируется, и полученный объeктный модуль объединяется с объектным модулем основной задачи. Как это делается, описано в данном приложении в Работа процедур со стеком В данном разделе описаны правила, которых следует придерживаться при составлении внешних подпрограмм, ориентированных на работу со стеком.
Распределение пространства стека. Общий случай распределения пространства стека при выполнении процедуры показан в табл. В.2.
Таблица В.2. Распределение пространства стека в порядке увеличения адресов
Общедоступная область стека |
Промежуточные переменные подпрограммы |
Исходное содержимое регистра bр или ebp |
Адрес возврата из подпрограммы |
Параметры подпрограммы |
Недоступная часть стека |
Общедоступная область расположена в начале стекового сегмента, ее минимальный адрес (смещение) равен нулю, а максимальный хранится в указателе стека (в регистре sp). Обычно она используется для хранения содержимого регистров и передачи параметров вызываемым подпрограммам.
Место для промежуточных переменных резервирует подпрограмма, если в этом есть необходимость. Она же сохраняет в стеке исходное содержимое регистра bpли ebp при работе в 32-разрядном режиме. Во время выполнения подпрограммы адрес, в котором сохранено исходное значение регистра bp(или ebp), используется в качестве базы для доступа команд к параметрам или промежуточным переменным.
Адрес возврата и параметры размещает в стеке основная задача, вызывающая данную подпрограмму. При входе в подпрограмму указатель стека содержит адрес первого свободного слова, в которое обычно помещается исходное значение регистра bpли ebp.
Недоступная часть стека названа так не потому, что она физически недоступна, а потому, что подпрограмма не должна изменять хранящиеся там данные. Если перед размещением параметров стек был полностью очищен, то недоступной области просто не существует.
Промежуточные переменныеB зависимости от конкретных особенностей подпрограммы при ее выполнении может возникнуть необходимость в использовании переменных для хранения промежуточных результатов вычислений в оперативной памяти. Назовем такие переменные "промежуточными".
До сих пор, в приводимых примерах переменные располагались в разделе данных задачи. Только в примере В.5 переменная dten хранится в сегменте кодов. Как правило, у внешних подпрограмм нет собственного сегмента данных, исключения возможны, но они встречаются редко. Размещать же промежуточные результаты в сегменте данных основной задачи не целесообразно, поскольку они используются только во время выполнения подпрограммы и не нужны в других случаях.
Пространство для размещения промежуточных переменных лучше всего выделять в стеке при входе в подпрограмму и освобождать его перед выходом из нее. Для резервирования требуемого пространства после сохранения содержимого регистра bpнадо просто уменьшить текущее значение указателя стека на суммарный размер промежуточных переменных, выраженный в байтах. В примере В.6 показано, как это обычно делается.
Пример В.6. Варианты оформления начала подпрограммы
; Вариант 1 — использование
трех команд
push bp ; сохранение содержимого bp
mov bp, sp ; запись в bpдреса верхушки стека
sub sp, N ; резервирование N байтов в стеке
; Вариант 2 — специальная команда enter
enter N, 0 ; заменяет три команды варианта 1
В первом варианте примера В.6 показано, как резервируется пространство размером N байтов с помощью обычных команд. Начиная с модели Intel 80286, у микропроцессоров появилась специальная команда enter. Она сохраняет в стеке содержимое регистра bp, копирует в bpдрес верхушки стека и уменьшает на N содержимое sp, т. е. по результату эквивалентна трем командам варианта 1. При использовании во внешних процедурах второй параметр команды enter равен нулю.
Важно
Ни при каких обстоятельствах значение указателя стека не может быть нечетным
числом. Поэтому количество байтов, отводимых для размещения промежуточных
переменных, обязательно должно быть четным. Однако это не означает, что
в пространстве стека нельзя размещать байты и работать с ними.
После выполнения любого из вариантов примера В.6 регистр bpспользуется для прямого доступа к параметрам и промежуточным переменным. Параметры расположены выше, а промежуточные переменные — ниже находящейся в bpточки отсчета. Обозначим смещение параметра или переменной как хх. В таком случае, при обращении к параметрам содержимое в bpувеличивается на величину смещения ([bp+хх]), а при обращении к промежуточным переменным оно уменьшается на величину смещения ([bp-хх]).
Прежде чем использовать переменные в командах, надо вычислить смещение каждой из них относительно регистра bp. Оно зависит от размеров предыдущих и данной переменной и не может быть равно нулю. Например, первая по порядку переменная может быть байтом, словом или двойным словом, ее адрес будет соответственно равен [bp-i], [bp-2] или [bp-4].
Имена параметров и переменных
Запись адреса в явном виде вполне корректна, но не наглядна.
Для того чтобы текст подпрограммы был более понятен, при визуальном анализе
лучше использовать имена.
Особенность данного случая в том, что переменные и параметры распределяются
не статическим, а динамическим способом. Это не позволяет использовать
для их описания обычные директивы db, dw и пр. Вместо этого используется
директива EQU (эквивалентно). Перед ней указывается имя параметра или
переменной, а после нее описание адреса и размера.
Предположим, что внешней подпрограмме передаются два параметра, каждый из которых имеет размер слова. Один их них задает ширину строки на экране в точках, а второй — количество строк на экране. В главе 2 для обозначения этих величин были введены имена Horsize и versize. Для присвоения этих имен параметрам в текст модуля подпрограммы надо включить две следующие директивы:
Horsize EQU word ptr [bp + 6] ; количество точек в строке
Versize EQU
word ptr [bp + 8] ; количество строк на экране
В этом примере предполагается, что перед вызовом внешней процедуры в стек сначада было записано значение параметра versize, а затем Horsize, например, так:
@Invoke имя_подпрограммы <Versize, Horsize>
Только в таком случае описание параметров соответствует их реальному расположению в стеке.
Обычно директивы EQU размещаются в начале тела модуля, вне сегмента (или вне сегментов). Имена, присвоенные в подпрограмме, являются локальными, поэтому вполне допустимо их совпадение с именами, описанными в основной программе или в других подпрограммах.
При указанном описании параметров для вычисления количества точек на экране в тексте подпрограммы выполняются следующие две команды:
mov ах, Horsize ; ах = количество точек в строке mui Versize
; dx:ax = Horsize * Versize
Описание промежуточных переменных отличается от описания параметров только указанием отрицательного смещения относительно bp, пример:
Address EQO word ptr [bp — 2] ; описание переменной Address
Следует отметить, что последовательность директив EQU является своеобразным описанием формальных параметров. Если такие директивы включены в текст модуля, то по его распечатке можно определить тип и последовательность указания параметров при вызове процедуры.
Контроль пространства стека
Контроль состояния стека нужен в тех случаях, когда задача использует вложенные процедуры и уровень вложенности достаточно велик. Вложенными называются процедуры последовательно вызывающие друг друга. При этом пространство стека, доступное каждой последующей процедуре, сокращается. При неудачном стечении обстоятельств оно может быть просто исчерпано. Особенно активно используют стек рекурсивные процедуры, способные многократно вызывать самих себя, правда, в графических задачах они обычно не используются.
Если проверка свободного пространства стека предусмотрена, то она выполняется в начале процедуры. При корректной работе со стеком размер свободного пространства в байтах равен значению указателя стека (содержимому регистра sp). Для выполнения контроля надо сравнить текущее содержимое sp с той величиной, которая нужна для выполнения процедуры. Если содержимое sp больше требуемого значения, то процедура может быть выполнена, в противном случае обычно выводится аварийное сообщение и выполнение задачи прекращается.
Процедура может использовать стек для размещения промежуточных переменных, хранения содержимого регистров и для вызова вложенных процедур. Определить требуемый размер пространства в стеке можно только на основании анализа исходного текста конкретной процедуры. Это должен делать ее разработчик.
Следует отметить, что проверка доступного пространства в стеке не является обязательной. Ее можно использовать на стадии отладки задачи, а затем исключить из подпрограмм. Напоминаем, что размер стекового сегмента указывается при его описании в тексте основной программы, поэтому всегда можно выбрать оптимальное значение.
Очистка стека является обязательным действием, выполняемым перед возвратом из процедуры. На стадии отладки задачи многие ошибки могут быть связаны с некорректными действиями при очистке стека.
В общем случае подпрограмма использует три разные области стека (см. табл. В.2) и их очистка производится разными способами.
Прежде всего, очищается общедоступная часть стека. Для этого количество использованных в подпрограмме команд push и pop должно совпадать.
Следующим шагом является освобождение пространства, выделенного для хранения промежуточных переменных, разумеется, если оно выделялось. В примере В.6 показаны два варианта резервирования пространства в стеке. В зависимости от используемого варианта выбирается способ освобождения стека.
Если применялся первый вариант примера В.6, то возможны два способа освобождения зарезервированного пространства. Первый способ заключается в увеличении содержимого регистра зр на величину м, соответствующую размеру выделенного пространства в байтах (add sp, N). Второй способ состоит в том, что в регистр sp копируется содержимое bp, при условии, что оно не изменялось в процессе выполнения подпрограммы (mov sp, bp).
Если для резервирования пространства в стеке использовался
второй вариант примера В.6, т. е. команда enter, то для его освобождения
применяется специальная команда leave, не имеющая параметров. При ее выполнении
содержимое регистра bР копируется в sp, поэтому оно не должно изменяться
при выполнении подпрограммы.
После выполнения описанных действий в верхушке стека должно находиться
исходное содержимое регистра bp. Оно выталкивается командой pop bp, после
которой можно выполнить ret для завершения подпрограммы.
Таким образом, если процедура ориентирована на передачу параметров в стеке, то ее выполнение завершают следующие две команды:
pop bp ; восстановление содержимого bpret ; возврат из подпрограммы
Удаление параметров. При возврате таким способом в основную задачу или в вызывавшую процедуру в стеке остаются параметры, которые надо удалить. Это можно сделать либо при выполнении команды ret, либо в вызывающем модуле сразу после возврата из подпрограммы.
Если в качестве параметра команды ret указать число к, то после выборки адреса возврата оно будет прибавлено к указателю стека. Такая модификация команды ret введена специально для удаления параметров при возврате из подпрограммы. Число к задает размер освобождаемой области стека в байтах, оно всегда четное. Например, если при вызове процедуры в стек было записано м параметров, каждый из которых имел размер слова, то к = 2м.
Для удаления параметров в вызывавшей процедуре сразу после возврата из подпрограммы выполняется команда add sp, к, где к задает размер освобождаемой области стека в байтах, о чем мы только что говорили.
Какой из двух способов лучше? Однозначного ответа на этот вопрос не существует, на практике применяются оба варианта. Удаление параметров при выполнении команды ret проще, но оно не допустимо, если в стеке находятся выходные параметры подпрограммы, предназначенные для вызывающего модуля. Во избежание подобных случаев при вызове подпрограммы в стеке можно указывать адреса выходных параметров, а в подпрограмме записывать результаты вычислений не в стек, а по указанным адресам. По окончании выполнения подпрограммы адреса параметров не нужны, и их можно удалять командой ret.
Таким образом, вы можете выбрать любой из двух вариантов удаления параметров, но желательно остановиться на одном варианте и использовать его во всех случаях.
Учитывая практическую важность сказанного в состав Макроассемблера, начиная с MASM 6.0, включены специальные средства для описания подпрограмм и оформления их вызова. В данном разделе приведен краткий обзор этих средств и пример их использования. При этом автор стремился выделить наиболее важные вопросы, ответ на которые не всегда очевиден.
В полный комплект поставки MASM 6.0 и последующих версий входит подробный HELP, содержащий описание директив, операторов и прочих атрибутов языка. Работая с Макроассемблером, вы всегда можете получить дополнительные сведения по интересующему вас вопросу.
Различие алгоритмических языковАлгоритмические языки созданы и создаются для решения различных классов или категорий задач, поэтому они принципиально отличаются друг от друга. Здесь нас интересуют только те различия, которые относятся к действиям, связанным с вызовом процедур. Основная часть вызова процедуры в большинстве алгоритмических языков имеет следующий вид:
имя_процедуры (список_параметров)
Однако соответствующая ей последовательность команд зависит от конкретных особенностей компилятора. Прежде всего, в именах процедуры и параметров могут различаться или не различаться заглавные и строчные буквы. Далее, параметры могут записываться в стек в порядке их перечисления в списке или в обратном порядке. Количество параметров в списке может быть фиксированным или переменным. Наконец, параметры могут удаляться из стека при возврате из процедуры или в вызывающем модуле.
В табл. В.З перечислены особенности компиляторов, свойства которых может учитывать Макроассемблер при компиляции программных модулей.
|
С |
Sy |
St |
В |
F |
Р |
Различаются
строчные и заглавные |
Да |
Да |
Да |
Нет |
Нет |
Нет |
Строчные буквы
в именах преобразуются |
Нет |
Нет |
Нет |
Да |
Да |
Да |
Параметры записываются
в стек в порядке их перечисления в списке |
Нет |
Нет |
Нет |
Да |
Да |
Да |
Параметры записываются
в стек в обратном |
Да |
Да |
Да |
Нет |
Нет |
Нет |
Параметры удаляются
в процедуре при выполнении команды ret |
Нет |
Нет |
* |
Да |
Да |
Да |
Параметры удаляются
в вызывающем модуле (команда add sp, N) |
Да |
Да |
* |
Нет |
Нет |
Нет |
Допустим список
параметров переменной длины (тип varagr) |
Да |
Да |
Да |
Нет |
Нет |
Нет |
В табл. В.З использованы следующие сокращения имен языков: С — Си, Sy — Syscall, St — Stdcall, В — Basic (Бейсик), F — Fortran (Фортран), Р — Pascal (Паскаль). Пояснять смысл имен Си, Бейсик, Фортран и Паскаль нет необходимости. Слова Syscall и Stdcall не соответствуют ни одному из конкретных языков, их точное назначение в HELP не описано, об этом можно только догадываться. Звездочки в столбце St обозначают, что место удаления параметров зависит от формы задания их списка. При обычной форме вызова их удаляет процедура, а если количество параметров переменное, то они удаляются в вызывающем модуле.
Для того чтобы Макроассемблер учитывал при компиляции программного модуля перечисленные в табл. В.З особенности, у директив MODEL, PROC и PROTO (см. ниже) появился новый спецификатор Langtype. Его допустимыми значениями являются имена С, Syscall, Stdcall, Basic, Fortran и Pascal. Имя должно быть указано явно, умолчаний не существует.
Замечание
При описании названий языков в тексте книги мы также указываем русскую
транскрипцию их названий, но в записи параметров на языке Макроассемблера
используются только латинские имена.
Полное описание процедуры
По-прежнему допустима сокращенная форма описания процедуры, при которой указывается только ее тип far или near Однако для того чтобы Макроассемблер взял на себя оформление текста процедуры, необходимо ее полное описание, которое имеет следующий вид:
метка PROC тип [спецификаторы] [, список_параметров]
Тип процедуры желательно указывать всегда, допустимые типы far, farie, far32, near, nearlG И near32. Имена far И near (без Цифр) используются в тех случаях, когда тип процедуры соответствует типу сегмента, в котором она расположена. Тип сегмента, в свою очередь, зависит от модели памяти и директив .386, .486, .586.
Спецификаторы указывают тип языка (описан выше), доступность для других модулей (private, Public, Export) и аргументы пролога и эпилога. По умолчанию выбирается тип языка, описанный в директиве MODEL, общедоступная процедура public и стандартная форма пролога и эпилога, которая описана ниже.
Важно
Разделителями между именами типа и спецификаторов могут быть только пробелы.
Первая запятая является признаком конца спецификаторов и начала списка
параметров. При несоблюдении этого правила Макроассемблер выдает различные
сообщения об ошибках и прерывает компиляцию модуля.
Список параметров по своему назначению аналогичен последовательности директив EQU, описанных в предыдущем разделе и позволяет использовать в основном тексте процедуры имена вместо ссылок на регистр bр. При его обработке Макроассемблер вычисляет смещение каждого параметра относительно регистра bp и подставляет нужные величины в команды подпрограммы. Если вы посмотрите листинг модуля, то увидите значения смещений для всех перечисленных в списке имен.
Параметры должны иметь уникальные имена, отличающиеся от других имен, описанных в данном модуле. После каждого имени указывается символ "двоеточие" и размер параметра (word, dword и т. п.). Если это не сделано, то по умолчанию размер параметра зависит от разрядности сегмента, в котором расположена процедура, — word для 16-разрядных и dword для 32-разрядных сегментов.
Таким образом, в полном описании процедуры появились два наиболее важных элемента — тип языка и список фактических параметров. Они содержат исчерпывающую информацию о способе компиляции процедуры.
Пролог и эпилог
В процессе компиляции подпрограммы Макроассемблер включает в объектный модуль две группы команд, одна из которых называется прологом (prologue), а другая эпилогом (epilogue). Пролог вставляется перед основным текстом подпрограммы, а эпилог перед командой ret. Вставляемые команды вам уже известны.
Стандартный вариант пролога содержит следующие две команды:
push bp ; сохранение исходного содержимого bx
mov bp, sp ; bx = адрес верхушки стека
Соответственно эпилогом являются команды
pop bp ; восстановление содержимого регистра bx
ret N ; изменение команды ret зависит от langtype
Если значением Langtype является Basic, Fortran или Pascal, то при формировании эпилога к команде ret будет добавлен суммарный размер параметров в байтах для очистки стека при возврате из подпрограммы. Если значением Langtype является С, то команда ret не изменяется, а в текст вызывающего модуля вставляется команда add sp, N.
Обычно команды пролога и эпилога отсутствуют в листинге. Для того чтобы они в нем оказались в начале текста модуля, укажите директиву .Listaii (точка перед именем обязательна).
Пролог и эпилог можно как исключить, так и расширить. Для исключения пролога в текст модуля перед описанием сегмента включается директива
OPTION PROLOGUE: NONE
Аналогичная директива существует и для эпилога, но ее использовать не обязательно. Макроассемблер вставляет эпилог только перед командой ret, поэтому для исключения эпилога достаточно использовать имена retn, retf или ret N, в зависимости от обстоятельств. Обратите внимание, наличие или отсутствие эпилога не влияет на включение команды add sp, N, поскольку она расположена в другом программном модуле.
В пролог можно добавить сохранение в стеке содержимого используемых регистров, а в эпилог — их восстановление. Для этого в описание процедуры включается спецификатор USES, а после него перечисляются используемые в подпрограмме регистры.
Замечание
Разделителем между именами регистров является пробел, указание запятых
не допустимо.
Если процедура содержит свой сегмент данных, то в пролог можно включить команды переопределения содержимого регистра ds, при этом в эпилог добавляется команда, восстанавливающая из стека исходное значение регистра ds. Для выполнения этих действий в описание процедуры включается спецификатор <ioadds> (угловые скобки обязательны).
Таким образом, пролог и эпилог позволяют не записывать в основном тексте вспомогательные действия, выполняемые при входе в процедуру и выходе из нее. Целесообразность включения команд пролога и эпилога решается в каждом конкретном случае с учетом назначения процедуры.
Оформление процедуры cnvindec. В комплект поставки Макроассемблера входят исходные тексты программных модулей, ил тюстрирующие различные случаи применения директивы PROC. Тем не менее автор счел целесообразным показать, что изменится в тексте процедуры cnvindec (см. пример В.5), если в ее описание включить полную форму директивы PROC. Измененный текст приведен в примере В.7.
Пример В.7. Измененный текст процедуры cnvindec
.LI STALL разрешаем печатать все
subr SEGMENT word public ' su эг ' ; начало сегмента s,ubr
.386 задаем тип микропроцессора
dten dd 10 константа для умножения на 10
cnvindec PROC FAR PASCAL USES e Зх fs si, address :dword
Ifs si, address fs:si = адрес начала строки текста
mov address, 0 result = 0 очистка результата
cnvloop: xor eax, eax очистка еах
lods byte ptr fs : [si] al = очередной символ строки
cmp al, '0' код символа меньше кода цифры 0 ?
jb endcnv -> да, конец формирования числа
cmp al, '9' код символа больше кода цифры 9 ?
ja endcnv -> да, конец формирования числа
sub al, 30h вычитаем код цифры 0
xchg eax, address переставляем еах и result
mul cs :dten edx: eax = result * 10
add address, eax result = result + eax
jmp short cnvloop -> на начало цикла преобразования
endcnv : pop si восстанавливаем содержимое si
pop fs восстанавливаем содержимое fs
pop edx восстанавливаем содержимое edx
pop bp восстанавливаем содержимое bp
retf возврат из подпрограммы
cnvindec ENDP конец блока процедуры
subr ENDS конец сегмента subr
END конец текста модуля
В тексте примера В.7 отсутствуют директивы PUBLIC и ASSUME. Первая из них не нужна потому, что при полном описании процедура, по умолчанию, является общедоступной. Директива ASSUME необходима только при использовании компилятора MASM 5.1, а данный пример предназначен для компиляции на более поздних версиях, которые не требуют указания этой директивы. Зато в тексте модуля появилась новая директива .Listaii, она нужна для того, чтобы Макроассемблер включил в листинг команды пролога.
В основном тексте процедуры отсутствуют ссылки на регистр bp, вместо них используется имя параметра address. Если вы посмотрите листинг, то увидите, что ему соответствует тип Dword и значение [bp+6].
В пролог, кроме двух стандартных команд, включены команды, выполняющие сохранение в стеке регистров edx, fs и si. Эпилог в данном случае исключен, поскольку использована команда retf, а не ret. Поэтому команды, восстанавливающие содержимое регистров si, fs, edx и bp, включены в текст процедуры, первая из них имеет метку endcnv.
Замечание
Процедура примера В. 7 предназначена для вызова из программных модулей,
составленных на языке ассемблера. Это объясняется тем, что сформированное
число возвращается в стеке, именно по этой причине в тексте процедуры
использована команда retf, исключающая вставку эпилога. Если вызывающий
модуль составлен на алгоритмическом языке, то взять результат из стека,
без специальных ухищрений, невозможно. Проще внести изменения в основной
текст процедуры, позволяющие вызывать ее из модуля, составленного на любом
языке.
Первый вариант таких изменений заключается в том, что перед выходом из процедуры сформированное число помещается в регистр еах. Для этого пять последних команд текста процедуры заменяются следующими:
endcnv: raov еах, address ; копируем результат в еах
ret ; стандартная форма команды возврата
В данном случае выполнение процедуры завершает команда ret, поэтому Макроассемблер вставит перед ней эпилог, а к команде ret добавит операнд, равный 4, для выталкивания параметров из стека.
Обоснованием этого варианта является то, что в алгоритмических языках функция возвращает результат в регистре еах (или ах). Например, в модуле, составленном на Фортране, возможна такая форма вызова данной функции:
argument = cnvindec (string)
Описанный вариант применим, если результатом является только одно число. В общем случае у процедуры появляются дополнительные параметры, содержащие адреса для записи результатов вычислений. В конце раздела В. 4 было сказано, что если выходные параметры заданы в виде адресов, то при возврате из процедуры допустимо их удаление из стека.
При описании примера В.5 говорилось, что в регистре al передается код символа, являющегося ограничителем числа. Если он используется в вызывающем модуле, то к процедуре примера В.7 надо добавить еще один параметр, содержащий адрес для записи кода символа.
Таким образом, при составлении основного текста процедуры важно учитывать, что именно передается в качестве параметра — значение переменной или ее адрес. От этого будут зависеть действия, выполняемые как в самой процедуре, так и в вызывающем модуле.
Директива вызова процедуры
Для вызова процедур, ориентированных на передачу параметров в стеке, предназначена специальная директива:
Invoke имя_процедуры [, список_параметров]
Список, если он указан, содержит фактические параметры, имена которых должны быть явно описаны в вызывающем модуле или объявлены в директиве EXTERN. Параметры отделяются от имени процедуры и друг от друга запятыми. В качестве параметров могут использоваться регистры, пары регистров, имена и адреса переменных. При этом в любом случае размеры фактических параметров должны соответствовать размеру формальных параметров, перечисленных в описании процедуры.
Если в списке указано имя переменной, то процедуре передается
ее значение. Если указано имя регистра, то передается его содержимое.
Для передачи содержимого пары регистров между их именами дважды указывается
символ "двоеточие". Например, если формальный параметр описан
как двойное слово и предназначен для передачи адреса, то в качестве фактического
параметра можно указать пару регистров DS: -.si.
Для передачи адреса переменной перед ее именем указывается ключ ADDR.
Если формальный параметр описан как двойное слово, то процедуре передается
сегмент, в котором описана переменная и ее смещение (адрес) относительно
начала сегмента. Если формальный параметр описан как слово, то процедуре
передается только сегмент, в котором расположена переменная, в некоторых
случаях это может пригодиться на практике.
При передаче адреса вместо имени переменной может быть указано выражение в форме, позволяющей Макроассемблеру однозначно определить адрес (смещение) переменной в сегменте, например, ADDR iinbuf+2.
При обработке директивы invoke Макроассемблер вставляет
в объектный модуль группу команд, выполняющих размещение параметров в
стеке, а после них команду вызова процедуры. Обычно результат подстановки
в листинге отсутствует. Чтобы увидеть его в начале текста модуля, надо
поместить директиву .Listaii (точка обязательна).
Последовательность записи параметров в стек зависит от имени, указанного
в качестве Langtype в описании процедуры. Если указаны имена Basic, Fotran
или Pascal, то параметры записываются в стек в порядке их перечисления
в списке директивы invoke. Если же указаны имена С, Syscall или Stdcall,
то параметры записываются в стек в обратном порядке (см. табл. В.З). Это
обеспечивает соответствие способов записи и использования передаваемых
в стеке параметров.
Вызов cnvindec
В качестве примера покажем, как может выглядеть вызов процедуры, описанной в примере В.7. Для этого в вызывающем модуле указывается следующая директива:
Invoke cnvindec, ADDR linbuf
Предполагается, что имя linbuf соответствует буферу, содержащему строку цифр в коде ASCII (см. раздел В.З). Если linbuf расположена в сегменте данных, то директива преобразуется в следующие команды:
push ds ; запись содержимого ds
push offset linbuf ; запись адреса linbuf
call cnvindec ; вызов подпрограммы cnvindec
Процедура содержит один параметр, поэтому последовательность команд, записывающих величины в стек, не зависит от языка.
Замечание
Описание процедуры обязательно должно предшествовать ее вызову директивой
Invoke. Поэтому если вызывающая часть и процедура находятся в одном программном
модуле, то текст процедуры должен быть расположен перед текстом вызывающей
части. Это требование остается в силе независимо от того, в одном или
в разных сегментах программного модуля описаны процедура и вызывающая
часть.
Прототип процедуры
Если процедура и вызывающий модуль готовятся независимо друг от друга и объединяются только при компоновке задачи, то выполнить указанное выше условие невозможно. В таком случае в текст вызывающего модуля включается прототип процедуры. Для его описания существует специальная директива, имеющая следующий формат:
имя_процедуры PROTO тип [langtype] [, список_параметров ]
Для составления прототипа нужен либо исходный текст процедуры, либо исчерпывающее описание способа вызова соответствующего ей объектного или библиотечного модуля, либо образец прототипа, взятый из другого программного модуля, вызывающего данную процедуру.
Рассмотрим конкретный случай. Предположим, что процедура,
описанная в примере В. 7, подготовлена в виде объектного модуля. Для создания
ее прототипа надо взять из примера В. 7 строку, содержащую директиву PROC,
заменить слово PROC на PROTO и исключить описание сохраняемых в стеке
регистров (USES edx fs si).
В результате получится следующий прототип:
cnvindec PROTO FAR PASCAL, addressrdword
Из этого примера видно, что для получения прототипа из описания директивы PROC исключаются только те спецификаторы, которые нужны при компиляции процедуры и не используются при компиляции вызывающего модуля.
При компиляции вызывающего модуля Макроассемблеру недоступно описание процедуры, поэтому имя процедуры, ее тип и значение Langtype он "принимает на веру", но имена параметров проверяет обязательно.
В списке PROTO указываются имена формальных параметров. Они не должны совпадать с именами переменных, описанных в вызывающем модуле. В противном случае Макроассемблер выдаст сообщение о повторном определении имени и прервет процесс компиляции. При успешной компиляции имена и размеры параметров включаются в объектный модуль и используются компоновщиком при сборке задачи. На стадии сборки известны описание процедуры и способ ее вызова и компоновщик проверяет их соответствие друг другу.
Заключение.
Практическая ценность директив PROC, PROTO и INVOKE состоит в том, что
при их использовании Макроассемблер самостоятельно учитывает особенности
компиляторов при формировании параметров в стеке и оформлении пролога
и эпилога процедуры. Это позволяет разрабатывать процедуры, не зависящие
от языка программирования, на котором составлен вызывающий модуль. Кроме
того, директива PROTO позволяет использовать в вызывающем модуле, составленном
на языке ассемблера, модули из библиотек, входящих в состав компиляторов
Си, Фортрана, Паскаля и Бейсика. Однако для этого вам должно быть доступно
описание этих библиотек.
В тексте данного раздела мы не упоминали об одной важной детали. Для успешной
разработки общедоступных процедур недостаточно знать только язык ассемблера.
Вы должны иметь представление не только о языке программирования, на котором
будет составлен вызывающий модуль, но и о возможностях конкретного компилятора
с этого языка. Все существующие компиляторы поддерживают расширенные версии
языков программирования, Макроассемблер не является исключением. А вот
в чем заключается это расширение, зависит от конкретной реализации компилятора.
И последнее. Навыки программирования приобретаются не при чтении книг,
а в процессе практической деятельности. Успехов вам на этом поприще, уважаемый
Читатель и Программист!