[Назад] [Далее] | |
Процедуры могут получать или не получать параметры из вызывающей процедуры и могут возвращать или не возвращать результаты (процедуры, которые что-либо возвращают, называются функциями в языке Pascal, но ассемблер не делает каких-либо различий между ними).
Параметры можно передавать с помощью одного из шести механизмов:
Параметры можно передавать в одном из пяти мест:
Так что всего в ассемблере возможно 30 различных способов передачи параметров для процедур. Рассмотрим их по порядку.
Процедуре передается собственно значение параметра. При этом фактически значение параметра копируется, и процедура использует его копию, так что модификация исходного параметра оказывается невозможной. Этот механизм применяется для передачи небольших параметров, таких как байты или слова.
Например, если параметры передаются в регистрах:
mov ax,word ptr value ; сделать копию значения call procedure ; вызвать процедуру
Процедуре передается не значение переменной, а ее адрес, по которому процедура должна сама прочитать значение параметра. Этот механизм удобен для передачи больших массивов данных и для тех случаев, когда процедура должна модифицировать параметры, хотя он и медленнее из-за того, что процедура будет выполнять дополнительные действия для получения значений параметров.
mov ax,offset value call procedure
Этот механизм объединяет передачу по значению и по ссылке. Процедуре передают адрес переменной, а процедура делает локальную копию параметра, затем работает с ней, а в конце записывает локальную копию обратно по переданному адресу. Этот метод эффективнее обычной передачи параметров по ссылке в тех случаях, когда процедура должна обращаться к параметру достаточно большое число раз, например, если используется передача параметров в глобальной переменной:
mov global_variable,offset value call procedure [...] procedure proc near mov dx,global_variable mov ax,word ptr [dx] (команды, работающие с АХ в цикле десятки тысяч раз) mov word ptr [dx],ax procedure endp
Этот механизм отличается от предыдущего только тем, что при вызове процедуры предыдущее значение параметра никак не определяется, а переданный адрес используется только для записи в него результата.
Это механизм, который используют макроопределения, директива EQU, а также, например, препроцессор С при обработке команды #define. При реализации этого механизма в компилирующем языке программирования (к которому относится и ассемблер) приходится заменять передачу параметра по имени другими механизмами при помощи, в частности, макроопределений.
Если определено макроопределение
pass_by_name macro parameter1 mov ax,parameter1 endm
то теперь в программе можно передавать параметр так:
pass_by_name value call procedure
Примерно так же поступают языки программирования высокого уровня, поддерживающие этот механизм: процедура получает адрес специальной функции-заглушки, которая вычисляет адрес передаваемого по имени параметра.
Как и в предыдущем случае, здесь процедура получает адрес функции, вычисляющей значение параметра. Такой механизм удобен, если вычисление значения параметра требует много ресурсов или времени, например, если функция должна выбрать один из нескольких ходов при игре в шахматы, вычисление каждого параметра может занимать несколько минут. При передаче параметров отложенным вычислением функция получает адрес заглушки, которая при первом обращении к ней вычисляет значение параметра и сохраняет его во внутренней локальной переменной, а при дальнейших вызовах возвращает ранее вычисленное значение. Если процедуре вообще не потребуются значения части параметров (например, если первый же ход приводит к мату), то использование отложенных вычислений способствует значительному выигрышу. Этот механизм чаще всего применяется в системах искусственного интеллекта и операционных системах.
Рассказав об основных механизмах того, как передавать параметры процедуре, рассмотрим применяемые в ассемблере варианты, где их передавать.
Если процедура получает небольшое число параметров, идеальным местом для их передачи оказываются регистры. Примерами использования этого метода могут служить практически все вызовы прерываний DOS и BIOS. Языки высокого уровня обычно используют регистр АХ (ЕАХ) для того, чтобы возвращать результат работы функции.
Когда не хватает регистров, один из способов обойти это ограничение — записать параметр в переменную, к которой затем обращаться из процедуры. Этот метод считается неэффективным, и его использование может привести к тому, что рекурсия и повторная входимость станут невозможными.
Параметры помещаются в стек сразу перед вызовом процедуры. Именно этот метод используют языки высокого уровня, такие как С и Pascal. Для чтения параметров из стека обычно используют не команду POP, а регистр ВР, в который помещают адрес вершины стека после входа в процедуру:
push parameter1 ; поместить параметр в стек push parameter2 call procedure add sp,4 ; освободить стек от параметров [...] procedure proc near push bp mov bp,sp (команды, которые могут использовать стек) mov ax,[bp+4] ; считать параметр 2. ; Его адрес в сегменте стека ВР + 4, потому что при выполнении ; команды CALL в стек поместили адрес возврата - 2 байта для процедуры ; типа NEAR (или 4 - для FAR), а потом еще и ВР - 2 байта mov bx,[bp+6] ; считать параметр 1 (остальные команды) рор bp ret procedure endp
Параметры в стеке, адрес возврата и старое значение ВР вместе называются активационной записью функции.
Для удобства ссылок на параметры, переданные в стеке, внутри функции иногда используют директивы EQU, чтобы не писать каждый раз точное смещение параметра от начала активационной записи (то есть от ВР), например так:
push X push Y push Z call xyzzy [...] xyzzy proc near xyzzy_z equ [bp+8] xyzzy_y equ [bp+6] xyzzy_x equ [bp+4] push bp mov bp,sp (команды, которые могут использовать стек) mov ax,xyzzy_x ;считать параметр X (остальные команды) pop bp ret 6 xyzzy endp
При внимательном анализе этого метода передачи параметров возникает сразу два вопроса: кто должен удалять параметры из стека, процедура или вызывающая ее программа, и в каком порядке помещать параметры в стек. В обоих случаях оказывается, что оба варианта имеют свои «за» и «против», так, например, если стек освобождает процедура (командой RET число_байтов), то код программы получается меньшим, а если за освобождение стека от параметров отвечает вызывающая функция, как в нашем примере, то становится возможным вызвать несколько функций с одними и теми же параметрами просто последовательными командами CALL. Первый способ, более строгий, используется при реализации процедур в языке Pascal, а второй, дающий больше возможностей для оптимизации, — в языке С. Разумеется, если передача параметров через стек применяется и для возврата результатов работы процедуры, из стека не надо удалять все параметры, но популярные языки высокого уровня не пользуются этим методом. Кроме того, в языке С параметры помещают в стек в обратном порядке (справа налево), так что становятся возможными функции с изменяемым числом параметров (как, например, printf — первый параметр, считываемый из [ВР+4], определяет число остальных параметров). Но подробнее о тонкостях передачи параметров в стеке рассказано далее, а здесь приведен обзор методов.
В этом необычном методе передаваемые процедуре данные размещаются прямо в коде программы, сразу после команды CALL (как реализована процедура print в одной из стандартных библиотек процедур для ассемблера UCRLIB):
call print db "This ASCIZ-line will be printed",0 (следующая команда)
Чтобы прочитать параметр, процедура должна использовать его адрес, который автоматически передается в стеке как адрес возврата из процедуры. Разумеется, функция должна будет изменить адрес возврата на первый байт после конца переданных параметров перед выполнением команды RET. Например, процедуру print можно реализовать следующим образом:
print proc near push bp mov bp,sp push ax push si mov si,[bp+2] ; прочитать адрес ; возврата/начала данных cld ; установить флаг направления ; для команды lodsb print_readchar: lodsb ; прочитать байт из строки, test al,al ; если это 0 (конец строки), jz print_done ; вывод строки закончен int 29h ; вывести символ в AL на экран jmp short print_readchar print_done: mov [bp+2],si ; поместить новый адрес возврата в стек pop si pop ax pop bp ret print endp
Передача параметров в потоке кода, так же как и передача параметров в стеке в обратном порядке (справа налево), позволяет передавать различное число параметров, но этот метод — единственный, позволяющий передать по значению параметр различной длины, что и продемонстрировал этот пример. Доступ к параметрам, переданным в потоке кода, несколько медленнее, чем к параметрам, переданным в регистрах, глобальных переменных или стеке, и примерно совпадает со следующим методом.
Блок параметров — это участок памяти, содержащий параметры, так же как и в предыдущем примере, но располагающийся обычно в сегменте данных. Процедура получает адрес начала этого блока при помощи любого метода передачи параметров (в регистре, в переменной, в стеке, в коде или даже в другом блоке параметров). В качестве примеров использования этого метода можно назвать многие функции DOS и BIOS, например поиск файла, использующий блок параметров DTA, или загрузка (и исполнение) программы, использующая блок параметров ЕРВ.