ПСМПС (лаб)
.pdf51
. . .
showmessage proc push AX push DX
mov AH, 09h
mov DX, offset message int 21h
pop DX pop AX ret
showmessage endp
. . .
end main
Промежуточный вариант расположения тела процедуры предполагает ее размещение внутри другой процедуры или основной программы. В этом случае необходимо предусмотреть обход тела процедуры, ограниченного директивами PROC и ENDP, с помощью команды безусловного перехода JMP.
Поскольку процедура обладает такими же атрибутами, что и обычная метка, то обратиться к процедуре можно с помощью любой команды перехода. Однако имеется существенное отличие, которое можно использовать с помощью специального механизма вызова процедур. Суть этого механизма состоит в возможности сохранения контекста программы в точке вызова процедуры, что и делает возможным возврат в эту точку после выполнения подпрограммы. Под контекстом понимается информация о состоянии программы в точке вызова процедуры. В системе команд микропроцессора имеется две команды, которые осуществляют работу с контекстом. Это инструк-
ции CALL и RET.
Инструкция CALL осуществляет вызов подпрограммы и имеет следующий формат:
call [<modifier>] <address>
Подобно инструкции JMP, данная инструкция передает управление по адресу <address>, но при этом в стеке сохраняется адрес возврата – адрес команды, следующей за командой CALL.
Инструкция RET возвращает управление вызывающей программе:
52
ret [<number>]
Команда RET считывает адрес возврата из стека и загружает его в регистры CS и IP/EIP, тем самым возвращая управление на команду, следующую в программе за инструкцией CALL. Параметр <number> не является обязательным и обозначает количество элементов, удаляемых автоматически из стека при возврате из процедуры.
Так же, как и в случае инструкции JMP, вызовы с помощью команды CALL могут быть ближними внутрисегментными и дальними межсегментными. При внутрисегментном вызове параметр <modifier> имеет значение NEAR, и в качестве адреса возврата команда CALL сохраняет только содержимое IP/EIP, что вполне достаточно для осуществления возврата. Напротив, если вызываемая процедура находится в другом сегменте (вызов с модификатором FAR), то для осуществления возврата команда CALL сохраняет в стеке регистры CS и IP/EIP. Очередность размещения их в стеке такова: сна-
чала CS , затем IP/EIP.
2.1.2. Прохождение параметров в процедуры. Локальные переменные процедуры
Процедуры могут получать или не получать параметры из вызывающей процедуры и могут возвращать или не возвращать результаты (процедуры, которые что-либо возвращают, называются функциями в языке Pascal и C, но ассемблер не делает каких-либо различий между ними).
Значения параметров могут быть переданы в подпрограмму с указанием значения или адреса ячейки, где находится значение. С этой точки зрения параметры можно передавать с помощью одного из трех механизмов:
•по значению;
•по ссылке;
•по возвращаемому значению.
С точки зрения локализации передаваемых параметров (то, где они будут находится) можно использовать следующие объекты:
•регистры общего назначения;
•глобальные переменные (общедоступные ячейки памяти модуля);
•стек программы.
В общем случае, используя различные комбинации способов передачи параметров в ассемблере, можно реализовать до 30 различных разновидностей передачи параметров.
53
Простейшим типом передачи параметра в подпрограмму является передача по значению (by value). Процедуре передается собственно значение параметра. При этом фактически значение параметра копируется, и процедура использует его копию, так что модификация исходного параметра оказывается невозможной. Этот механизм применяется для передачи небольших параметров, таких, как одиночные байты или слова. Например, если параметры передаются в регистрах:
mov ax, word ptr value |
; |
сделать |
копию значения |
call myproc |
; |
вызвать |
процедуру |
Более эффективным способом является передача параметров по ссылке (by reference), т.е. с указание адреса параметра. Процедуре при этом передается не значение переменной, а ее адрес, по которому процедура должна сама прочитать значение параметра. Этот механизм удобен для передачи больших массивов данных и в тех случаев, когда процедура должна модифицировать параметры, хотя он и медленнее из-за того, что процедура будет выполнять дополнительные действия для получения значений параметров. Например:
mov ax, offset value call myproc
Комбинированным способом передачи параметров, на основе двух предыдущих является способ передачи параметров по возвращаемому значению (by return value). Этот механизм объединяет передачу по значению и по ссылке. Процедуре передают адрес переменной, а процедура делает локальную копию параметра, затем работает с ней, а в конце записывает локальную копию обратно по переданному адресу. Этот метод эффективнее обычной передачи параметров по ссылке в тех случаях, когда процедура должна обращаться к параметру достаточно большое число раз, например, если используется передача параметров в глобальной переменной:
mov global_variable,offset value call myproc
...
myproc proc near
mov dx, global_variable mov ax, word ptr [dx]
...
54
; команды, работающие с АХ в цикле, большое число раз
...
mov word ptr [dx],ax
Если процедура получает небольшое число параметров, идеальным местом для их передачи оказываются регистры. Примерами использования этого метода могут служить практически все вызовы прерываний DOS и BIOS. Языки высокого уровня обычно используют регистр АХ (ЕАХ) , для того чтобы возвращать результат работы функции.
Когда не хватает регистров, один из способов обойти это ограничение – записать параметр в переменную, к которой затем обращаться из процедуры. Этот метод считается неэффективным, и его использование может привести к тому, что рекурсия (recursion) и повторная входимость (reentrance – реенте-
рабельность) станут невозможными.
Наиболее эффективным и универсальным способом размещения передаваемых в процедуры параметров является их передача через стек программы. Параметры помещаются в стек сразу перед вызовом процедуры. Именно этот метод используют языки высокого уровня, такие, как Си и Pascal. Для чтения параметров из стека обычно используют не команду POP, а регистр ВР (рис. 4.2.), в который помещают адрес вершины стека после входа в процедуру (необходимо учитывать, что стек растет из области старших в область младших адресов памяти):
push param1 |
; |
поместить параметр |
в стек |
push param2 |
|
|
|
call myproc1 |
|
|
|
add SP,4 |
; |
освободить стек от |
параметров |
...
myproc1 proc near push BP
mov BP,SP
; команды, которые могут использовать стек mov AX,[BP+4] ; считать параметр 2.
;Его адрес в сегменте стека ВР + 4,
;потому что при выполнении
;команды CALL в стек поместили
;адрес возврата - 2 байта для
;процедуры типа NEAR(или 4 - для FAR),
;а потом еще и ВР - 2 байта
55
mov BX,[BP+6] ; считать параметр 1 ; остальные команды
рор BP ret
myproc1 endp
|
«Дно» стека |
|
|
+6 |
param1 |
push param1 |
|
|
push param2 |
||
param2 |
|||
+4 |
|||
|
call myproc1 |
||
IP |
|||
+2 |
|||
BP |
push BP |
||
[BP] 0 |
|||
|
|
||
mov BP, SP |
|
|
Рис. 4.2. Стек программы до и после вызова процедуры myproc1
Параметры в стеке, адрес возврата и старое значение ВР вместе называются активационной записью процедуры (функции).
Для удобства ссылок на параметры, переданные в стеке, внутри функции иногда используют директивы EQU, чтобы не писать каждый раз точное смещение параметра от начала активационной записи (то есть от ВР), например так:
push X push Y push Z
call myproc2
...
myproc proc near myproc2_z equ [BP+8] myproc2_y equ [BP+6] myproc2_x equ [BP+4] push BP
mov BP,SP
;команды, которые могут использовать стек mov AX, myproc_x ;считать параметр X
;остальные команды
pop BP
56
ret 6 myproc2 endp
При анализе этого метода передачи параметров возникает сразу два вопроса: кто должен удалять параметры из стека – процедура или вызывающая ее программа, и в каком порядке помещать параметры в стек. В обоих случаях оказывается, что оба варианта имеют свои «за» и «против», так, например, если стек освобождает процедура (командой RET n), то код программы получается меньшим, а если за освобождение стека от параметров отвечает вызывающая функция, как в нашем примере, то становится возможным вызвать несколько функций с одними и теми же параметрами просто последовательными командами CALL. Первый способ, более строгий, используется при реализации процедур в языке Pascal, а второй, дающий больше возможностей для оптимизации, – в языке Си. Разумеется, если передача параметров через стек применяется и для возврата результатов работы процедуры, из стека не надо удалять все параметры, но популярные языки высокого уровня не пользуются этим методом. Кроме того, в языке Си параметры помещают в стек в обратном порядке (справа налево), так что становятся возможными функции с изменяемым числом параметров (как, например, printf – первый параметр, считываемый из [ВР+4], определяет число остальных параметров).
Часто процедурам требуются локальные переменные, которые не будут нужны после того, как процедура закончится. По аналогии с методами передачи параметров можно говорить о локальных переменных в регистрах – каждый регистр, который сохраняют при входе в процедуру и восстанавливают при выходе, фактически играет роль локальной переменной. Единственный недостаток регистров в роли локальных переменных – их слишком мало.
Наиболее распространенный способ хранения локальных переменных в процедуре – хранение в стеке. Принято располагать локальные переменные в стеке сразу после сохраненного значения регистра ВР, так что на них можно ссылаться изнутри процедуры, как на адреса [ВР-2], [ВР-4], [ВР-6] и т.д.:
myproc3 PROC NEAR
param_a equ [BP+8] ; параметры param_b equ [BP+6]
param_c equ [BP+4]
var_x equ [BP-2] ; локальные переменные var_y equ [BP-4]
|
|
57 |
|
|
push BP |
; сохранить предыдущий ВР |
|
||
mov BP,SP |
; |
установить ВР для |
этой процедуры |
|
sub SP,4 |
; |
зарезервировать 4 |
байта |
для |
;локальных переменных
;тело процедуры
mov SP,BP |
; восстановить |
SP, выбросив |
|
pop BP |
; из стека все |
локальные переменные |
|
; |
восстановить |
ВР вызвавшей процедуры |
|
ret 6 |
; |
вернуться, удалив параметры из стека |
myproc3 ENDP
Внутри процедуры myproc3 |
стек будет заполнен так, как показано на |
||
рис. 4.3. |
|
|
|
|
|
|
|
|
param_a |
|
|
|
param_b |
|
|
|
|
|
|
|
|
|
|
|
param_c |
|
|
|
|
|
|
|
IP |
|
|
|
|
|
|
|
BP |
|
[BP] |
|
|
||
|
var_x |
|
|
|
var_y |
|
[SP] |
Рис. 4.3. Стек при вызове процедуры myproc3
Последовательности команд, используемые в начале и в конце таких процедур, оказались настолько часто применяемыми, что в процессоре 80186 были введены специальные команды ENTER и LEAVE, выполняющие эти же самые действия:
myproc3 PROC NEAR |
; параметры |
||
param_a equ [bp+8] |
|||
param_b |
equ |
[bp+6] |
|
param_c |
equ |
[bp+4] |
|
var_x equ [bp-2] |
; локальные переменные |
|
58 |
var_y equ [bp-4] |
|
enter 4,0 |
; push bp |
|
; mov bp,sp |
|
; sub sp,4 |
; тело процедуры |
|
leave |
; mov sp,bp |
ret 6 |
; pop bp |
; вернуться, |
|
|
; удалив параметры |
myproc3 ENDP |
; из стека |
|
Область в стеке, отводимая для локальных переменных вместе с активационной записью, называется стековым кадром (stack frame).
2.2. Макросредства ассемблера
Одно из самых мощных языковых средств ассемблера – макроопределения. Макроопределением (или макросом) называется участок программы, которому присвоено имя и который ассемблируется всякий раз, когда ассемблер встречает это имя в тексте программы. Макрос начинается директивой MACRO и заканчивается ENDM.
<name> MACRO [<formalparam_list>] <statements>
ENDM
Для того чтобы код макроопределения был развернут в заданной точке, в программе должно быть использовано имя макроса подобно инструкции. Указание имени макроса называется макроподстановкой. Если макрос имеет список параметров, то при макроподстановке также должны быть указаны фактические параметры, в качестве которых могут выступать литеральные значения, имена регистров, символов и др.
<name> [<actualparam_list>]
Макроопределения должны располагаться до первого упоминания имени макроса, то есть до первой макроподстановки. Как правило, макроопределения указывают в начале программы до сегмента данных и кода, чтобы не ухудшать читабельность программы. Этот вариант размещения макросов следует применять в тех случаях, если определяемые макрокоманды ак-
59
туальны только в пределах одной этой программы. Если макросы имеют значение для нескольких программных проектов, то их целесообразно разместить в отдельном файле. Чтобы использовать эти макроопределения в конкретной программе, необходимо в начале исходного текста программы записать директиву INCLUDE с указанием имени файла с макросами:
include < filename>
Для макросов универсального назначения можно создать макробиблиотеку и использовать макроопределения из нее в программных проектах. Недостаток использования больших макробиблиотек состоит в том, что в исходный текст программы включаются абсолютно все макроопределения. Для устранения этого недостатка можно использовать директиву PURGE. В качестве ее операндов через запятую перечисляются имена макрокоманд, которые не должны включаться в текст программы.
Рассмотрим пример простого макроопределения:
;макрос вывода строки на экран outstring macro str
push AX mov AH, 09h
mov DX, offset str in 21h
pop AX
endm
;макрос настройка сегментного регистра на сегмент данных initdataseg macro datareg, dataseg
mov AX, nameseg mov datareg, AX
endm
;макрос завершения программы
exitprog macro mov AX, 4C00h int 21h
endm
60
Данное макроопределение может быть использовано в программе следующим образом:
text segment ‘code’
assume CS: text, DS: data initdataseg DS, data
. . .
outstring message1
. . .
outstring message2
. . .
exitprog text ends data segnent
message1 db 'Hello world$' message2 db ’Good-bye world$'
data ends
end
Как видно из примеров, по задачам, решаемым с помощью макроопределений, они подобны процедурам. Сходство их в том, что и те, и другие позволяют существенным образом сократить объем кодирования. Однажды описанная процедура или макроопределение могут быть специальным образом вызваны в любой точке кода.
Вотличие от процедуры, текст которой неизменен, макроопределение
впроцессе макрогенерации может меняться в соответствии с набором фактических параметров. При этом коррекции могут подвергаться как операнды команд, так и сами команды. Процедуры в этом отношении менее гибкие. Однако макрокоманда имеет недостаток. Он связан с тем, что при каждом вызове макрокоманды ее текст в виде макрорасширения вставляется в программу. Напротив, при вызове процедуры микропроцессор осуществляет передачу управления на начало процедуры, находящейся в некоторой области памяти в одном экземпляре. В этом случае бинарный код исполняемого модуля получается намного компактнее, хотя быстродействие программы несколько снижается за счет необходимости осуществления переходов.
Спомощью макросредств ассемблера можно не только частично изменять входящие в макроопределение строки, но и модифицировать сам набор этих строк и даже порядок их следования. Эта возможность предоставляется