В 4 Макроопределение для вызова подпрограмм
Пример В.4. Макроопределение для вызова подпрограмм
@Invoke macro name, par ; заголовок макроопределения
irp r, <par> ; начало оператора повторения
push r ; заготовка повторяемой команды
endm ; конец оператора повторения
call name ; заготовка команды вызова подпрограммы
endm ; конец текста макроопределения
В приведенных ранее примерах неоднократно использовался макровызов FushReg, текст его макроопределения приведен в примере 2.12. В данном случае к этому тексту добавилась только одна строка, содержащая заготовку команды сан. Поэтому в результате макроподстановки в текст программы сначала будет включена группа команд push, а затем команда call.
Если макроопределение примера В.4 включено в текст основной задачи, то для его использования в нужном месте указывается следующий макровызов:
@Invoke имя_процедуры <список_параметров>
Имя процедуры может быть как внешним, так и внутренним. Список параметров обязательно заключается в угловые скобки, а параметры отделяются друг от друга запятыми, после которых допустим пробел. Форма записи параметров стандартная для команды push. Пример использования макровызова приведен в конце данного раздела.
Доступ процедур к параметрам
При входе в процедуру в верхушке стека расположен адрес возврата, а перед ним параметры. Для работы с параметрами входящие в тело процедуры команды должны иметь прямой доступ к области стека. Как было сказано выше, для этой цели удобно использовать регистр вр, но при входе в подпрограмму его содержимое не определено. Поэтому в начале подпрограммы надо сохранить исходное содержимое bр и записать в него опорный (базовый) адрес, которым является текущее значение указателя стека.
охранение содержимого bр нужно для того, чтобы выполнение данной подпрограммы не влияло на выполнение вызывающего модуля. При выходе из подпрограммы перед выполнением команды ret сохраненное значение надо вытолкнуть в bр.
Таким образом, большинство подпрограмм, ориентированных на работу со стеком, начинается с двух следующих команд:
push bp ; сохранение исходного содержимого bpmov
bp, sp ; запись в bpдреса верхушки стека
Давайте уточним, что находится в стеке после выполнения этих команд. Для определенности будем считать, что вызвана внешняя подпрограмма, имеющая два параметра, каждый из которых занимает одно слово. Микропроцессор работает в реальном режиме, поэтому адрес возврата занимает два слова. В таком случае стек содержит величины, приведенные в табл. В.1.
Таблица В.1. Вариант размещения данных в стеке
Смещение | Что находится в слове |
bp+ 0 | Исходное содержимое регистра bp |
bp+ 2 | Младшая часть адреса возврата (IP) |
bp+ 4 | Старшая часть адреса возврата (cs) |
bp+ 6 | Второй параметр подпрограммы |
bp+ 8 | Первый параметр подпрограммы |
В соответствии с табл. В.1, при сделанных выше допущениях, полный адрес первого параметра равен ss: [bp+8], а второго — ss: [bp+б]. Как уже говорилось, сегментный регистр ss в записи операндов не указывается, поскольку в данном случае он используется по умолчанию. Например, произведение параметров можно вычислить с помощью двух команд:
mov ах, [bp+б] ; ах = значение первого параметра
mul [bp+81 ; dx:ax = ах * значение второго параметра
В общем случае адрес параметра, т. е. значение, прибавляемое к ьр, зависит от размеров предшествующих величин и его собственного размера. Поэтому при разработке подпрограммы надо предварительно определить, что конкретно будет находиться в стеке при ее вызове.
Внешняя процедура cnvindec. Рассмотрим простую внешнюю процедуру, которая преобразует последовательность десятичных цифр, представленных в коде ASCII, в код десятичного числа. Цифры в коде ASCII получаются, например, при вводе чисел с клавиатуры. Для того чтобы результат ввода можно было использовать при вычислениях, последовательность цифр надо преобразовать в шестнадцатеричный код числа.
Алгоритм формирования десятичного числа следующий. Обозначим формируемое число как result и предположим, что в исходном состоянии result = о. В таком случае на шаге номер I значение result умножается на 10 и к произведению прибавляется код очередной цифры:
result = result * 10 + digit [I]
Перед прибавлением кода очередной цифры его надо преобразовать в двоичный код. В формате ASCII коды цифр изменяются от 30h до зэь, поэтому для преобразования из кода цифры вычитается код нуля (зсш). Кроме того, надо проверить, действительно ли очередной символ строки является цифрой, и если это не так, то процесс формирования числа прекращается.
Завершенный текст подпрограммы приведен в примере В.5. Перед ее вызовом в стеке указывается полный адрес преобразуемой строки (сегмент и смещение).
Сформированное число помещается в стек на место адреса строки. Кроме того, при возврате из подпрограммы в регистре ai находится код символа, при обнаружении которого было прекращено формирование результата. Им может быть любой символ, кроме цифры. Вариант обращения к подпрограмме описан ниже.
Пример В.5. Исходный текст процедуры cnvindec
PUBLIC cnvindec объявляем процедуру общедоступной
subr SEGMENT word public 'subr'; начало сегмента subr
ASSUME cs:subr cs ассоциируется с subr
.386 задаем тип микропроцессора
dten dd 10 константа для умножения на 10
cnvindec PROC far начало блока процедуры
push bp сохранение содержимого bp
mov bp, sp bp = sp базовый адрес в стеке
push edx сохраняем содержимое edx
push fs сохраняем содержимое fs
push si сохраняем содержимое si Ifs si,
[bp+6] fs:si = адрес начала строки текста mov dword ptr [bp+6], 0; result = 0 очистка результата
cnvloop: xor eax, eax очистка еах
lods byte ptr fs:[s ]; al = очередной символ строки
cmp al, '0' код символа меньше кода цифры 0 ?
jb endcnv ; -> да, конец формирования числа
cmp al, '9' К°Д символа больше кода цифры 9 ?
ja endcnv -> да, конец формирования числа
sub al, 30h вычитаем код цифры О
xchg eax, [bp+6j переставляем еах и result
mul cs:dten edx:eax = result * 10
add [bp+6], eax result = result + eax
jmp short cnvloop -> на начало цикла преобразования
endcnv: pop si восстанавливаем содержимое si
pop fs восстанавливаем содержимое fs
pop edx восстанавливаем содержимое edx
pop bp восстанавливаем содержимое bp
ret возврат из подпрограммы
cnvindec ENDP конец блока процедуры
subr ENDS конец сегмента subr
END конец текста модуля
Подпрограмма примера В.5 оформлена в виде готового для компиляции модуля. Способ оформления такого модуля описан в предыдущем разделе и показан в примере В.З. Поэтому мы начнем с основного текста.
В сегменте subr перед текстом процедуры описано двойное слово dten и ему присвоено значение 10. Эта переменная используется в процедуре при умножении, она нужна потому, что операндом команды mul не может быть константа 10.
Процедура преобразования имеет имя cnvindec. Ее текст начинается с подготовки регистра bpсохранения в стеке используемых регистров. После этого в регистры fs:si загружается адрес преобразуемой строки.
Важно
Важно
Перед вызовом процедуры в стек сначала записывается сегмент, а затем смещение строки. Только при выполнении этого условия команда Ifs поместит в регистр fs код сегмента, а в регистр si — смещение.
После загрузки адреса строки в регистры параметры не нужны и отведенное для них место используется для размещения формируемого числа. Предварительно команда mov dword ptr [bp+6], о очищает два слова стека с адресами [bp+6] И [bp+8].
Цикл формирования числа начинается с команды, имеющей метку cnvloop, и заканчивается командой jmp short cnvloop. Код формируемого числа может содержать до 32-х разрядов, поэтому вычислительные операции выполняются с операндами, имеющими размер двойного слова.
Цикл начинается с очистки регистра еах и записи в его младший байт (al) кода очередного символа. Затем проверяется, чему соответствует этот код. Если он соответствует цифре, то из содержимого al вычитается код цифры 0, производится перестановка содержимого еах и result и выполняется умножение result * 10. В связи с тем, что константа dten расположена в кодовом сегменте, в команде mui перед ней явно указано имя регистра cs. Младшая часть результата умножения (содержимое еах) прибавляется к result и происходит возврат на начало цикла формирования числа.
Если очередной символ не является цифрой, то выполнение цикла прекращается и происходит переход на метку endcnv. Начиная с этой метки расположены команды, восстанавливающие содержимое сохраненных в стеке регистров, и команда retf, завершающая выполнение подпрограммы.
Замечание
Замечание
Значение формируемого подпрограммой примера В. 5 числа может изменяться в пределах от 0 до 4294967295 (232 — 1). Контроль переполнения результата отсутствует, это сделано для упрощения текста подпрограммы. При необходимости вы можете ввести такой контроль или увеличить диапазон допустимых значений числа. Напомним, что при умножении на 10 старшая часть произведения находится в регистре edx, но подпрограмма не работает с этим регистром.
Использование процедуры cnvindec. Для использования в задачах модуль примера В. 5 компилируется, и полученный объeктный модуль объединяется с объектным модулем основной задачи. Как это делается, описано в данном приложении в разделе . Здесь нас будет интересовать вызов подпрограммы задачей.
Предположим, что в разделе данных задачи зарезервирован буфер для размещения вводимой строки текста и ему присвоено имя linbuf. Для хранения сформированного числа в разделе данных надо зарезервировать двойное слово, присвоив ему подходящее имя, например, argument.
В задаче можно выбрать любой удобный для вас способ ввода текста строки с клавиатуры. Только не забывайте, что вводимые символы нужно отображать на экране монитора, а способ отображения зависит от установленного задачей видеорежима. В текстовых видеорежимах обычно применяется стандартная функция DOS, ее код ОАbp.
Подпрограммы для ввода текста в графических видеорежимах описаны в главе 5, раздел, а их использование показано в приложении А, пример А.10 .
После ввода строки, для формирования числа в основном тексте задачи выполняются следующие команды:
@Invoke cnvindec <ds, offset linbuf>; вызов подпрограммы
pop argument ; запись сформированного числа
При компиляции, обнаружив имя einvoke, Макроассемблер ищет его в своих таблицах, и если описанное в примере В.4 макроопределение включено в текст задачи, то макровызов преобразуется в следующие три команды:
push ds ; запись в стек содержимого ds
push offset linbuf ; запись в стек смещения
linbuf call cnvindec ; обращение к подпрограмме
После возврата из подпрограммы в стеке находится результат в виде двойного слова, которое задача должна вытолкнуть в специально выделенную переменную argument. Кроме того, в регистре ai находится код символа, обнаружив который подпрограмма завершила свою работу. В случае необходимости в задаче можно предусмотреть проверку кода этого символа и выполнение тех или иных действий в зависимости от результата проверки.
Таким образом, мы описали простой пример подпрограммы, параметры которой находятся в стеке, и теперь можно перейти к обсуждению общих вопросов, связанных с использованием стека в подпрограммах.