Перейдем к рассмотрению способов передачи параметров. Начнем с регистрового, применительно к Watcom-C.
Регистровый способ передачи параметров.
Для передачи параметров используются регистры ах, dx, bx, сх — в таком порядке. Число и назначение регистров определяется размерностью параметра.
— Параметр типа int, short и near ptr передается в одном регистре.
— Параметр типа char передается не в байтовом регистре, а в двухбайтовом, с предварительным преобразованием в int (способ преобразования определяется атрибутом signed/undigned).
— Параметры типа long, far ptr передаются так dx:ax или сх:bх.
— Если транслятору не указано использовать инструкции сопроцессора, параметры типа float/double передаются в регистрах (соответственно, в паре dx:ax или сх:bх — для float, или в четверке abcd — для double).
Параметры, оставшиеся после распределения регистров, будут передаваться с помощью стека.
Проиллюстрируем на примере функции fill, рассмотренной ранее.
Листинг. Передача параметров подпрограмме fill.
/* main6.c */
int fill( char far *, char);
char * s = «I got a lot to say! I can’t remember now!»;
main() {
int x = fill (s, ‘ ! ‘) ; }
Первому параметру типа far ptr назначаются регистры dx:ax. Остаются для передачи параметров регистры bх, сх. Второму параметру (типа char) назначается первый из свободных регистров — bх. Результат функции (типа int) возвращается в ах.
mov ax, bx ; (6)
pop ex, di, es ; (7)
ret
Регистры, которые подверглись изменениям, сохраняются, а затем восстанавливаются (7) перед выходом. Только bх использовался для передачи параметра, и потому не нуждается в сохранении.
Программист передает параметры через стек в следующих случаях:
— возник дефицит незанятых регистров;
— параметр задан структурой, размер которой превышает 8 байт;
— параметр представляет собой действительное значение, при этом транслятору задано генерирование инструкций i80x87.
Примечание:
При организации интерфейса с ассемблером обязательно задавать прототип функции. Иначе транслятору придется угадывать тип параметров — с характерным в таких ситуациях пессимизмом: если фактический параметр — типа char или int, то для формального параметра принимается наиболее «емкий» из int-типов — long int. Аналогично, near ptr на всякий случай «растягивают» до far ptr, a float преобразуют в double. В результате, выбранная транслятором схема распределения регистров для передачи параметров отличается от рассчитанной по общим правилам.
Как передать параметры посредством стека.
Этот способ передачи используется всеми трансляторами с языка Си и подходит для передачи множества параметров, но работает медленней и производит более громоздкий код.С-трансляторами. По сравнению с ранее рассмотренным регистровым способом, он порождает больше кода и работает медленней. Доступ к параметрам из подпрограммы ожидаемо усложняется и, кроме того, зависит от дальности вызова подпрограммы.
Передача при ближних вызовах.
Вернемся к функции even. Обращение к even транслируется Microsoft/Borland-C вот таким образом:
push 10 ; (1)
call even ; (2)
add sp, 2 ; (3)
mov _ch, al
Значение параметра типа char записывается в стек как слово. После обращения к even команда освобождает место в стеке, занятое параметрами. Под значением параметра в стеке записан адрес возврата <ret_addr>, в результате выполнения инструкции call. Вызов ближний, поэтому <ret_addr> — слово.
Параметр находится по адресу ss:sp+2, но адресовать его через sp нельзя — косвенная адресация через sp не поддерживается. Для доступа к произвольной записи стека специально предназначен регистр bр. При косвенной адресации с участием bр сегментная составляющая адреса берется из ss — как раз то, что требуется. Раз мы используем bр, его следует сохранить — сразу при входе в подпрограмму. Затем значение bр устанавливается равным текущего значению sp; значение bр фиксировано на время выполнения подпрограммы. На выходе из подпрограммы исходное значение bр восстанавливается из стека, а сам стек теперь выглядит так же, как при входе в подпрограмму.
Кадр стека при дальних вызовах.
В случае дальнего вызова размер ret_addr возрастет, что приведет к сползанию параметров относительно базы стека. Смещение первого параметра при far-вызове, после фиксации bр, становится равным шести.
За счет смещения вершины стека резервируются 4 байта вниз от базы стека. Эти данные адресуются отрицательными смещениями, начиная с [bр-2].
Примечание:
Машинная инструкция enter содержит два параметра — объем локальных данных и лексический уровень подпрограммы соответственно. Последнее значение в контексте языка Си всегда ноль, поскольку вложенные функции в Си не поддерживаются.
На выходе, перед выполнением pop bр необходимо, чтобы sp указывал на базу стека. Для этого достаточно выполнить присвоение.
Листинг.
subr8_1.08 (+ main4_f.c)
even:
enter 0
mov al, [bp+4]
test al
jpe >11
xor al, bit 7
11: leave
ret
Использование локальных данных проиллюстрируем примером, показанным в листинге 9. В нем приведена функция trunc для преобразования действительного значения типа double в целое число типа int, с отбрасыванием дробной части.
Листинг. Создание локальных данных и доступ к ним.
/* main9.c */
int trunc( double);
void main() {
int x = trunc (10.9 * 16.1 — 7.7); }
Параметр — действительное число типа double (8-байтное). Временные данные (слово) предназначены для чтения-записи регистра управления i80x87. Новое значение для установки требуемого режима округления, записывается в регистр управления. После инструкции округления frndint восстанавливается исходное значение, которое было сохранено в ах.
Примечание:
В этом примере острая необходимость в локальных данных отсутствует — можно было воспользоваться записью, занятой параметром. Достаточно перенести команду (*) в начало, и — после загрузки параметра в сопроцессор — значение двойного слова по адресу [bр+4] больше не нужно, место свободно. До сих пор в примерах был только один параметр. Но если программист передаст через стек нескольких параметров.
Порядок передачи параметров.
Если параметров несколько, они записываются в стек задом наперед — от конца списка параметров к началу. Заметим, что в случае передачи параметров ассемблером через регистры, порядок их следования, как обычно, прямой. В качестве примера рассмотрим вызов функции dif, которая возвращает результат вычитания параметров типа int.
/* main10.c */
int dif (int, int);
void main() { int five = dif( 12, 5);
}
Поскольку стек растет вниз, параметры расположены по возрастанию адреса — <prm1> примыкает<ret_addr>, <prm2> находится над <prm1>.
Примечание:
В трансляторах с языка Pascal (и его производных) параметры записываются в прямом порядке. Имеется возможность такой же порядок установить и для С-функций — для этого при объявлении прототипа функции следует задать атрибут pascal.
Подпрограмма dif, предназначенная для вычисления разности <prm1>-<prm2>, уже приводилась выше.
Соглашения об удалении параметров.
В трансляторах с языка С принято, что после завершения функции место, занятое параметрами, должна освобождать вызвавшая программа. Так же, как и обратный порядок записи параметров, это решение вызвано наличием функции с непостоянным числом параметров.
В языках семейства Pascal число параметров для процедуры неизменно. Поэтому там применяется альтернативный вариант, и более экономный — параметры естественным образом удаляет сама подпрограмма, перед завершением. В процессоре i80x86 для реализации этого варианта предусмотрена команда ret/retf <n>, где <n> — число, прибавляемое к sp после выполнения возврата.
В языке Си такой способ удаления параметров применяется только к тем функциям, которые объявлены с атрибутом pascal.
/* main11.c */
int pascal dif (int, int);
main() { int seven = dif( 12, 5); }
Подпрограмма dif после изменения значений регистров выглядит теперь следующим образом (напомним, что в результате применения атрибута pascal аналогично изменился порядок передачи параметров):
; subr11.08
dif : enter 0
mov ax, [bp+6] ; !
sub ax, [bp+4] ; !
leave
ret 4 ; ret
; add sp, 4