Графические устройства

         

Классификация подпрограмм

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

Ближние подпрограммы

При входе в ближнюю (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. С их описаниями работает компоновщик. Если в одном из модулей встречается внешнее имя, то он ищет его в списках глобальных имен других модулей. При отсутствии соответствующего описания будет выдано сообщение об ошибке.


Содержание раздела