- FasmWorld - https://fasmworld.ru -

Процедуры с переменным количеством параметров

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

Вообще, на мой взгляд, процедуры с переменным количеством параметров являются не самым удачным и потенциально небезопасным приёмом программирования. Даже в C/C++ лучше их избегать. В большинстве случаев можно обойтись процедурой с фиксированным количеством параметров. Например, передавать процедуре 2 параметра: адрес массива переменной длины и количество элементов в этом массиве.

Для создания процедуры с переменным количеством параметров необходимо, чтобы стек очищался вызывающим кодом. Поэтому процедура должна заканчиваться командой RET [1] без операндов. Параметры должны помещаться в стек в обратном порядке. Кстати, именно такие соглашения вызова используются компиляторами языка C.

Процедура должна на этапе выполнения определять, сколько параметров ей было передано. Это можно реализовать несколькими способами:

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

Третий способ является опасным! Если строка не соответствует количеству параметров или, например, её вводит пользователь, то процедура может прочитать из стека больше, чем надо, или даже что-то записать. Подобные свойства программ широко используются хакерами 🙂

Способ 1

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

;Процедура вычисления среднего арифметического.
;Первый параметр - количество чисел, остальные параметры - числа.
;Возвращает значение в регистре AX
average:
    push bp             ;Сохранение BP
    mov bp,sp           ;BP=SP
    push cx             ;Сохранение других используемых регистров
    push dx
    push si

    mov cx,[bp+4]       ;CX=первый параметр
    mov si,6            ;SI=6, (BP+SI)=адрес второго параметра
    xor ax,ax           ;AX=0, в AX будет вычисляться сумма чисел
    jcxz av_ret         ;Если CX=0, то выход из процедуры
av_lp:
    add ax,[bp+si]      ;Прибавление к AX очередного параметра из стека
    add si,2            ;SI=SI+2, (BP+SI)=адрес следующего параметра
    loop av_lp          ;Команда цикла
    cwd                 ;DX:AX=AX
    idiv word[bp+4]     ;Деление суммы на первый параметр

av_ret:
    pop si              ;Восстановление регистров
    pop dx
    pop cx
    pop bp
    ret                 ;Возврат из процедуры

Параметры в стеке адресуются с помощью пары регистров: BP и SI. Перед началом цикла стоит команда JCXZ [2]. Если количество параметров равно 0, то цикл выполняться не будет и процедура вернёт 0. Примеры вызова процедуры:

    push 2              ;Второе число
    push 10             ;Первое число
    push 2              ;Количество чисел = 2
    call average        ;Вызов процедуры average
    add sp,6            ;Восстановление стека

    push -5             ;Третье число
    push 99             ;Второе число
    push 0              ;Первое число
    push 3              ;Количество чисел = 3
    call average        ;Вызов процедуры average
    add sp,8            ;Восстановление стека

Способ 2

Этот способ хорошо использовать, когда есть такие значения, которые параметр принимать не может. Например, адрес строки не может быть равен 0, так как для COM-программы по этому адресу размещаются служебные структуры DOS. Впрочем, почти во всех операционных системах ноль обозначает недействительный адрес. Следующая процедура выводит несколько строк на консоль. Последний параметр всегда должен быть нулевым.

;Процедура вывода нескольких строк на консоль.
;Параметры - адреса строк. Последний параметр должен быть равен 0
print_strs:
    push bp             ;Сохранение BP
    mov bp,sp           ;BP=SP
    push ax             ;Сохранение других используемых регистров
    push dx
    push si

    mov si,4            ;SI=4, (BP+SI)=адрес первого параметра
    mov ah,9            ;Функция DOS 09h - вывод строки
pss_lp:
    mov dx,[bp+si]      ;DX=очередной параметр
    test dx,dx          ;Проверка DX
    jz pss_ret          ;Если DX=0, то выход из процедуры
    int 21h             ;Обращение к функции DOS
    add si,2            ;SI=SI+2, (BP+SI)=адрес следующего параметра
    jmp pss_lp          ;Переход к началу цикла

pss_ret:
    pop si              ;Восстановление регистров
    pop dx
    pop ax
    pop bp
    ret                 ;Возврат из процедуры

Примеры вызова процедуры:

    push 0              ;Последний параметр = 0
    push str_3          ;Строка 3
    push str_2          ;Строка 2
    push str_1          ;Строка 1
    call print_strs     ;Вызов процедуры print_strs

    push 0              ;Последний параметр = 0
    push s_pak          ;Строка 'Press any key...'
    call print_strs     ;Вызов процедуры print_strs
    add sp,12           ;Восстановление стека
    ...
;---------------------------------------------------------------------
;Данные
str_1 db 'Hello $'
str_2 db 'asmworld.ru!$'
str_3 db 13,10,'$'
s_pak db 'Press any key...$'

Обратите внимание, что стек восстанавливается один раз для двух вызовов процедуры. Параметры от первого вызова остаются в стеке и никак не мешают вызвать функцию второй раз. Правда при этом расходуется больше памяти в стеке 🙂

Результат выглядит следующим образом:

Полный исходный код примера: vararg.asm [3]