|
|||
СтекОкончание. Начало см. “В мир информатики” № 102–104, 106, 108 (“Информатика” № 2–4, 6, 8/2008) Пример 4. Комплексное использование стека В примере 2 было показано, как стек работает при вызове подпрограммы. В примере 3 стек дополнительно применялся для передачи данных. Становится очевидным, насколько стек универсален: он может хранить и адреса возвратов, и данные. Более того, “с машинной точки зрения” принципиальной разницы между числом или адресом, хранящимся в стеке, вообще не существует. Убедимся в этом в ходе следующего эксперимента. Пусть наша программа вызывает некоторую подпрограмму. Мы уже прекрасно понимаем, что в стеке при этом оказывается адрес памяти, куда необходимо будет вернуться после завершения подпрограммы. Покажем, что этот самый адрес можно использовать для арифметических вычислений, например, для расчета адреса данных. Обратимся к протоколу 4. Сначала вводится подпрограмма, извлекающая в BX копию адреса возврата, а затем, как обычно, по инструкции RET, передающая управление основной программе. Последняя, зная значение адреса, вычисляет местоположение находящихся внутри нее данных и извлекает их в регистр AX. Как видно из протокола 4, данные оказываются по адресу 111, что на 7 больше, чем адрес точки выхода из подпрограммы 10a. Предлагаем читателям внимательно разобрать протокол ввода и коррекции программы самостоятельно. Далее по директиве g = 100 · 10f запустим программу с адреса 100, обеспечивая ее останов в точке 10f. Убедимся, что в регистре AX действительно оказывается требуемое значение. Проделанный нами пример — не просто изящное упражнение. Перед нами один из вариантов программного кода, который способен определять свое местоположение в памяти и автоматически модифицировать адрес используемых в нем данных. Иными словами, нам удалось создать простейший вариант кода, сохраняющего свою работоспособность в любом месте памяти. Для проверки данного утверждения выполним директиву m100,113,200, которая скопирует нашу программу целиком в другую область памяти, начинающуюся с адреса 200 (проверка u200 подтвердит этот факт). Но самое интересное, что и в этом месте оперативной памяти программа по-прежнему будет правильно работать! Задание для самостоятельной работы Напишите подпрограмму, которая, получив управление, стирает инструкцию обращения к ней из основной программы, заменяя ее нулевыми кодами. Опробуйте случай, когда подпрограмма вызывается несколько раз из разных мест основной программы. При отладке будьте очень внимательны и осторожны! Пример 5. Как “устроены” В качестве последнего примера работы со стеком покажем, как реализовать подпрограмму, которая выводит некоторый текст. Иными словами, попробуем свои силы в реализации средствами отладчика Debug оператора вывода на экран (в языке Паскаль это Writeln, в языке Бейсик — PRINT). Предварительно определим коды входящих в сообщение символов. Для этого мне показалось проще воспользоваться языком Бейсик, хотя читатели, вполне возможно, предпочтут другой способ. Программа, решающая такую задачу, имеет вид: A$ = "HELLO!" В ней в теле оператора цикла выделяется каждый символ величины А$, после чего определяется его десятичный код, который затем в шестнадцатеричном представлении выводится на экран. Результат выполнения программы: 48 45 4C 4C 4F 21 Перейдем к рассмотрению протокола 5. В программе есть несколько неочевидных моментов, поэтому разберем ее подробнее. Инструкция POP BX в традиционном для наших экспериментов стиле сделает регистр BX местом хранения адреса возврата. Однако в отличие от примера 4, где этот адрес просто копировался без нарушения указателя стека, здесь использован стандартный метод извлечения из стека с потерей его содержимого. Почему? В силу специфики нашей задачи! В самом деле, когда мы вызовем нашу подпрограмму, то счетчик будет установлен на следующий за вызовом адрес. Но в нашей задаче это адрес начала текста, т.е. скорее адрес данных, чем адрес возврата. Именно поэтому мы смело “берем инициативу на себя”, но тем самым “обязуемся” осуществить возврат из подпрограммы самостоятельно. Итак, в BX — адрес выводимого текста. Теперь подготовим для цикла вывода количество символов в нем в регистре CX. Старшую часть этого регистра обнулим командой XOR CH,CH (читателям предлагается доказать, что результат логической операции “исключающее или” любой произвольной константы “с самой собой” всегда равен нулю!), а в младшую считаем первый байт, на который указывает регистр BX. Тем самым мы предполагаем, что перед текстом лежит количество байт в нем; это очень важно вспомнить при занесении кодов символов текста в память. Увеличим значение регистра-указателя BX на единицу, установив его тем самым на первую букву текста. Теперь к реализации цикла вывода на экран все подготовлено. Далее следует короткий (из пяти машинных инструкций) цикл. В регистр AL извлекается очередной символ и значение BX тут же наращивается, чтобы подготовиться к выводу следующего. В AH заносится номер функции символа на экран и следует обращение к ней по инструкции INT 10. В результате извлеченный из памяти символ оказывается на экране. Завершается цикл командой LOOP (в переводе с английского — петля), которая вычитает из CX единицу и сравнивает результат с нулем: если осталось ноль символов, то все уже напечатано и цикл завершается, иначе — повторяется начиная с команды 108. А теперь самое интересное. Регистр BX синхронно “скользил” вдоль текста, отслеживая его символы. Куда он указывает, когда весь текст закончился? Правильно, на инструкцию программы, которую необходимо выполнить следующей. Поэтому команда JMP BX и обеспечивает фактически завершение подпрограммы и возврат к нужному месту основной программы. Остальная часть протокола уже не содержит ничего, что не встречалось бы в предыдущих примерах. Поэтому автор смело оставляет его читателям для самостоятельного разбора. Не забудьте только обратить внимание на переносимость реализованного нами кода, т.е. его работоспособность при копировании в другой участок памяти. Успехов!
|