Вызовы подпрограмм
Практически в любой программе, независимо
от ее содержания, встречаются участки,
которые требуется выполнять (возможно, с
небольшими изменениями) несколько раз по
ходу программы. Такие повторяющиеся
участки целесообразно выделить из общей
программы, оформить в виде подпрограмм и
обращаться к ним каждый раз, когда в
основной программе возникает
необходимость их выполнения.
Подпрограмма, в зависимости от выполняемых
ею функций, может требовать передачи из
вызывающей программы определенных данных (называемых
аргументами, или параметрами), возвращать в
вызывающую программу результаты
вычислений или обходиться и без того, и без
другого.
Подпрограмма может быть оформлена в виде
процедуры, и тогда имя этой процедуры будет
служить точкой входа в подпрограмму:
drawline proc ;Подпрограмма-процедура
. . . ;Тело подпрограммы
ret ;Команда возврата в вызывающую программу
drawline endp
С таким же успехом можно обойтись без процедуры, просто пометив первую строку программы некоторой меткой:
drawline: ;Подпрограмма, начинающаяся с метки
. . . ;Тело подпрограммы
ret ;Команда возврата в вызывающую программу
. . . ;Продолжение основной программы или
;другие подпрограммы
В любом случае вызов подпрограммы
осуществляется командой call. Подпрограмма
должна завершаться командой ret, служащей
для возврата управления в ту точку, откуда
подпрограмма была вызвана.
Вопросы использования подпрограмм,
передачи в них параметров и возвращения
результата будут рассмотрены в следующей
главе. Здесь мы остановимся только на таких
принципиальных архитектурных вопросах, как
механизм выполнения и возможности команд
call и ret. При этом надо иметь в виду, что
синтаксические особенности и
закономерности использования команд call и jmp
во многом совпадают, и значительная часть
пояснений к командам перехода справедлива
и для команд вызова.
Команда вызова подпрограммы call может
использоваться в 4 разновидностях. Вызов
может быть:
прямым ближним (в пределах текущего
сегмента команд);
прямым дальним (в другой сегмент команд);
косвенным ближним (в пределах текущего
сегмента команд через ячейку с адресом
перехода);
косвенным дальним (в другой сегмент команд
через ячейку с адресом
перехода).
Рассмотрим последовательно перечисленные
варианты.
Прямой ближний вызов. Как и в случае прямого
ближнего перехода, в команде прямого вызова
в явной форме указывается адрес (смещение)
точки входа в подпрограмму; в качестве
этого адреса можно использовать как имя
процедуры, так и имя метки, характеризующей
точку входа в подпрограмму. В код команды,
кроме кода операции E8h, входит смещение к
вызываемой подпрограмме. В приведенном
ниже примере подпрограмма оформлена в виде
процедуры.
code segment
main proc ;Основная программа
…
call sub ;Код Е8 dddd
…
main endp
sub proc near ;Подпрограмма
…
ret ;Код СЗ
sub endp
code ends
Процедура-программа находится в том же сегменте команд, что и вызывающая программа. В коде команды dddd обозначает смещение в сегменте команд к точке входа в подпрограмму. При выполнении команды call процессор помещает адрес возврата (содержимое регистра IP) в стек выполняемой программы (рис. 2.16), после чего к текущему содержимому IP прибавляет dddd. В результате в IP оказывается адрес подпрограммы. Команда ret, которой заканчивается подпрограмма, выполняет обратную процедуру - извлекает из стека адрес возврата и заносит его в IP.
Рис. 2.16. Участие стека в механизме вызова ближней подпрограммы.
Участие стека в механизме вызова
подпрограммы и возврата из нее является
решающим. Поскольку в стеке хранится адрес
возврата, подпрограмма, сама используя стек,
например, для хранения промежуточных
результатов, обязана к моменту выполнения
команды ret вернуть стек в исходное
состояние. Команда ret, естественно, никак не
анализирует состояние или содержимое стека.
Она просто снимает со стека верхнее слово,
считая его адресом возврата, и загружает
это слово в указатель команд IP. Если к
моменту выполнения команды ret указатель
стека окажется смещенным в ту или иную
сторону, команда ret по-прежнему будет
рассматривать верхнее слово стека, как
адрес возврата, и передаст по нему
управление, что неминуемо приведет к краху
системы.
Прямой дальний вызов. Этот вызов позволяет
обратиться к подпрограмме из другого
сегмента. В код команды, кроме кода операции
9Ah, входит полный адрес (сегмент плюс
смещение) вызываемой подпрограммы. Обычно в
исходном тексте программы с помощью
описателя far ptr указывается, что вызов
является дальним, хотя, если транслятор
настроен на трансляцию в два прохода, этот
описатель не обязателен. Структура
программного комплекса, содержащая дальний
вызов подпрограммы, может выглядеть
следующим образом:
codel segment
assume CS:codel
main proc ;Основная программа
call far ptr subr ; Код 9А dddd ssss
…
main endp
codel ends
code2 segment
assume CS:code2
subr proc far ;Объявляем подпрограмму дальней
…
ret ;Код СВ - дальний возврат
subr endp
code2 ends
Процедура-подпрограмма находится в другом сегменте команд той же программы. В коде команды dddd обозначает относительный адрес точки входа в подпрограмму в ее сегменте команд, a ssss - се сегментный адрес. При выполнении команды call процессор помещает в стек сначала сегментный адрес вызывающей программы, а затем относительный адрес возврата (рис. 2.17). Далее в сегментный регистр CS заносится 5555 (у нас это значение code2), а в IP - dddd (у нас это значение subr). Поскольку процедура-подпрограмма атрибутом far объявлена дальней, команда ret имеет код, отличный от кода аналогичной команды ближней процедуры и выполняется по-другому: из стека извлекаются два верхних слова и переносятся в IP и CS, чем и осуществляется возврат в вызывающую программу, находящуюся в другом сегменте команд. В языке ассемблера существует и явное мнемоническое обозначение команды дальнего возврата - retf.
Рис. 2.17. Участие стека в механизме вызова дальней подпрограммы.
Косвенный ближний вызов. Адрес подпрограммы содержится либо в ячейке памяти, либо в регистре. Это позволяет, как и в случае косвенного ближнего перехода, модифицировать адрес вызова, а также осуществлять вызов не с помощью метки, а по известному абсолютному адресу. Структура программы с косвенным вызовом подпрограммы может выглядеть следующим образом:
code segment
main proc ;Основная программа
…
call DS:subadr ;Код FF 16 dddd
main endp
subr proc near ;Подпрограмма
…
ret ;Код СЗ
subr endp
code ends
data segment
…
subadr dw subr ;Яейка с адресом подпрограммы
data ends
Процедура-программа с атрибутом near
находится в том же сегменте, что и
вызывающая программа, а ее относительный
адрес в ячейке subadr в сегменте данных. В коде
команды dddd обозначает относительный адрес
слова subadr в сегменте данных. Второй байт
кода команды (16h в данном примере) зависит от
способа адресации. Косвенный вызов
позволяет использовать разнообразные
способы адресации подпрограммы:
call BX ; В ВХ адрес подпрограммы
call[BX] ; В ВХ адрес ячейки с адресом подпрограммы
call[BX][SI] ;В ВХ адрес таблицы адресов подпрограмм,;в SI индекс в этой таблице.
tbl[SI] ;tbl - адрес таблицы адресов подпрограмм,
;в SI индекс в этой таблице
Косвенный дальний вызов. Отличается от косвенного ближнего вызова лишь тем, что подпрограмма находится в другом сегменте, а в ячейке памяти содержится полный адрес подпрограммы, включающий сегмент и смещение.
codel segment
main proc ;Основная программа
call dword ptr subadr ;Код FF IE dddd
…
main endp
codel ends
code2 segment
subr proc far ;Подпрограмма
…
ret ;Код СВ
subr endp
code2 ends
data segment
…
subadr dd subr ;Двухсловная ячейка с
;адресом подпрограммы
data ends
Процедура-подпрограмма с атрибутом far находится в другом сегменте команд той же программы, а ее полный двухсловный адрес - в ячейке subadr в сегменте данных. Второй байт кода команды (IE в данном примере) зависит от способа адресации. Косвенный дальний вызов, как и косвенный ближний, позволяет использовать различные способы адресации.