Иллюстрированный самоучитель по Assembler

Вызовы подпрограмм


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

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

Подпрограмма может быть оформлена в виде процедуры, и тогда имя этой процедуры будет служить точкой входа в подпрограмму:

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 в данном примере) зависит от способа адресации. Косвенный дальний вызов, как и косвенный ближний, позволяет использовать различные способы адресации.


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