Графические устройства

Pet Block Repellent Spray отпугивающее средство для кошек


     

Параметры в стеке

Характерной особенностью подпрограмм является то, что используемые при вычислениях величины передаются им в виде входных параметров. В свою очередь, подпрограммы могут возвращать результаты вычислений в виде выходных параметров. Способ доступа к параметрам зависит от того, где они расположены.

Наиболее простой формой является передача параметров в регистрах общего назначения. Она используется процедурами BIOS и DOS. В этом случае доступ к параметрам осуществляется наиболее просто и быстро, но существует ряд ограничений, сводящих на нет это преимущество. Здесь для нас существенно одно из таких ограничений, а именно то, что компиляторы с алгоритмических языков обычно не используют регистры для передачи параметров. В некоторых из них, например в Си++, предусмотрена возможность размещения параметров в регистрах, но это уже относится к категории трюков или уловок, но не к стандартам программирования.

Если создаваемая вами подпрограмма предназначена для использования в программах, составленных на одном из алгоритмических языков, то она должна быть рассчитана на расположение параметров в стеке, поскольку в большинстве случаев компиляторы размещают их именно в нем. К описанию особенностей компиляторов мы вернемся после обсуждения техники передачи параметров при программировании на ассемблере.

Общие сведения

Стеком называется любой произвольно выбранный блок оперативной памяти, работа с которым производится по принципу "последнее записанное — первое считанное" (LIFO - last in first out). Иначе говоря, выражение "стек" характеризует не тип памяти, а способ работы с ней.

В процессе выполнения задачи в регистре ss хранится код сегмента оперативной памяти, в котором расположена область стека, а текущий адрес (смещение) верхушки стека в этом сегменте хранится в регистре SP, который называется указателем стека. Разрядность регистров зависит от режима работы микропроцессора: в реальном режиме они содержат по 16 разрядов, а в защищенном режиме по 32 разряда.

Стек нарастает в сторону уменьшения адресов, поэтому при входе в задачу регистр зр содержит наибольший доступный адрес в области стека. При записи данных в стек адрес, хранящийся в зр, уменьшается, а при их выборке из стека — увеличивается.
Для записи и чтения данных в режиме LIFO предназначены команды PUSH и POP, которые неоднократно использовались в примерах. Команда push предварительно уменьшает содержимое регистра sp, а затем записывает операнд в вычисленный адрес. Команда pop, наоборот, сначала считывает операнд, а затем увеличивает содержимое зр. В обоих случаях адрес, хранящийся в sp, изменяется на размер операнда, который может составлять 2 или 4 байта. Один байт записать в стек нельзя, команда push преобразует его в слово.

Кроме команд push и pop в режиме LIFO со стеком работают команды вызова обычных (сан) и прерывающих (int) подпрограмм и команды возврата из
НИХ (ret И iret).

Для непосредственного доступа к области стека выделен специальный регистр ВР. Он может использоваться в обычных командах (пересылки, сложения и пр.) в тех случаях, когда один их операндов расположен в области стека. Если операнд находится в регистре bp, то при его обработке микропроцессор, по умолчанию, выбирает в качестве сегментного регистра ss, a не DS, как обычно. В случае необходимости можно явно указать любой другой сегментный регистр.

Новое макроопределение

Перед обращением к подпрограмме в стек записываются ее параметры. Запись в стек обычно выполняет команда push. Для сокращения текста программы и придания ему большей наглядности можно использовать специальный макровызов. Текст соответствующего макроопределения приведен в примере В.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 находится код символа, обнаружив который подпрограмма завершила свою работу. В случае необходимости в задаче можно предусмотреть проверку кода этого символа и выполнение тех или иных действий в зависимости от результата проверки.

Таким образом, мы описали простой пример подпрограммы, параметры которой находятся в стеке, и теперь можно перейти к обсуждению общих вопросов, связанных с использованием стека в подпрограммах.

     
Назад Начало Вперед