Существуют несколько способов передачи параметров в процедуру.
1. Параметры можно передавать через регистры.
Если процедура получает небольшое число параметров, идеальным местом для их передачи оказываются регистры. Существуют соглашения о вызовах, предполагающие передачу параметров через регистры ECX и EDX. Этот метод самый быстрый, но он удобен только для процедур с небольшим количеством параметров.
2. Параметры можно передавать в глобальных переменных.
Параметры процедуры можно записать в глобальные переменные, к которым затем будет обращаться процедура. Однако этот метод является неэффективным, и его использование может привести к тому, что рекурсия и повторная входимость 3 станут невозможными.
3. Параметры можно передавать в блоке параметров.
Блок параметров – это участок памяти, содержащий параметры и располагающийся обычно в сегменте данных. Процедура получает адрес начала этого блока при помощи любого метода передачи параметров (в регистре, в переменной, в стеке, в коде или даже в другом блоке параметров).
|
|
4. Параметры можно передавать через стек.
Передача параметров через стек – наиболее распространённых способ. Именно его используют языки высокого уровня, такие как С++ и Паскаль. Параметры помещаются в стек непосредственно перед вызовом процедуры.
При внимательном анализе этого метода передачи параметров возникает сразу два вопроса: кто должен удалять параметры из стека, процедура или вызывающая её программа, и в каком порядке помещать параметры в стек. В обоих случаях оказывается, что оба варианта имеют свои «за» и «против». Если стек освобождает процедура, то код программы получается меньшим, а если за освобождение стека от параметров отвечает вызывающая программа, то становится возможным вызвать несколько функций с одними и теми же параметрами просто последовательными командами CALL. Первый способ, более строгий, используется при реализации процедур в языке Паскаль, а второй, дающий больше возможностей для оптимизации, – в языке С++.
Основное соглашение о вызовах языка Паскаль предполагает, что параметры кладутся в стек в прямом порядке. Соглашения о вызовах языка С++, в том числе одно из основных соглашений о вызовах ОС Windows stdcall, предполагают, что параметры помещаются в стек в обратном порядке. Это делает возможной реализацию функций с переменным числом параметров (как, например, printf). При этом первый параметр определяет число остальных параметров.
push <параметр n >
...
push <параметр1>
call Procedure
В приведённом выше участке кода в стек кладутся несколько параметров и затем вызывается процедура. Следует помнить, что команда CALL также кладёт в стек адрес возврата. Таким образом, перед выполнением первой команды процедуры стек будет выглядеть следующим образом.
|
|
Адрес возврата оказывается в стеке поверх параметров. Однако поскольку в рамках своего участка стека процедура может обращаться без ограничений к любой ячейки памяти, нет необходимости перекладывать куда-то адрес возврата, а потом возвращать его обратно в стек. Для обращения к первому параметру используют адрес [ESP + 4] (прибавляем 4, т.к. на архитектуре Win32 адрес имеет размер 32 бита), для обращения ко второму параметру – адрес [ESP + 8] и т.д.
После завершения работы процедуры необходимо освободить стек. Если используется соглашение о вызовах stdcall (или любое другое, предполагающее, что стек освобождается процедурой), то в команде RET следует указать суммарный размер в байтах всех параметров процедуры. Тогда команда RET после извлечения адреса возврата прибавит к регистру ESP указанное значение, освободив таким образом стек. Если же используется соглашение о вызовах cdecl (или любое другое, предполагающее, что стек освобождается вызывающей программой), то после команды CALL следует поместить команду, которая прибавит к регистру ESP нужное значение.
; Передача параметров и возврат из процедуры с использованием соглашения о вызовах stdcall
.686
.model flat, stdcall
option casemap: none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.data
x dd 0
y dd 4
.code
program:
push y; Кладём в стек два параметра размером по 4 байта
push x
call Procedure
push 0
call ExitProcess
Procedure proc
ret 8; В команде возврата указываем, что надо освободить 8 байт стека
Procedure endp
end program
; Передача параметров и возврат из процедуры с использованием соглашения о вызовах cdecl
.686
.model flat, c
option casemap: none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.data
x dd 0
y dd 4
.code
program:
push y; Кладём в стек два параметра размером по 4 байта
push x
call Procedure
add esp, 8; Освобождаем 8 байт стека
push 0
call ExitProcess
Procedure proc
ret; Используем команду возврата без параметров
Procedure endp
end program
5. Параметры можно передавать в потоке кода.
В этом необычном методе передаваемые процедуре данные размещаются прямо в коде программы, сразу после команды CALL. Чтобы прочитать параметр, процедура должна использовать его адрес, который автоматически передаётся в стеке как адрес возврата из процедуры. Разумеется, процедура должна будет изменить адрес возврата на первый байт после конца переданных параметров перед выполнением команды RET.
.686
.model flat, stdcall
option casemap: none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.code
program:
call Procedure; Команда CALL кладёт в стек адрес следующей команды
db 'string',0; В нашем случае – адрес начала строки
push 0
call ExitProcess
Procedure proc
pop esi; Извлекаем из стека адрес начала строки
xor eax, eax; Обнуляем EAX, в нём будет храниться количество символов
L1: mov bl, [esi]; Заносим в регистр BL байт, хранящийся по адресу ESI
inc esi; Увеличиваем значение в регистре ESI на 1
inc eax; Увеличиваем значение в регистре EAX на 1
cmp bl, 0; Сравниваем прочитанный символ с нулём
jne L1; Если не 0, переходим к началу цикла
push esi; Кладём в стек адрес байта, следующего сразу за строкой
ret; Возврат из процедуры
Procedure endp
end program