

              Глава 2. СТРУКТУРНОЕ ПРОГРАММИРОВАНИЕ 2: ПРОЕКТИРОВАНИЕ
                          И РЕАЛИЗАЦИЯ МОДУЛЬНЫХ ПРОГРАММ


             Принципы модульного программирования
             Реализация модульных программ на языке Ассемблер
             Типы кодирования
             Интерфейс с языками высокого уровня
             Назначение  и использование локального ЗУ в памяти
             Заключение


             В главе  1  обсуждение было сфокусировано на средствах струк-
         турного программирования и их применении в  среде макроассемблера
         MASM. В главе 2 представлены методы структурного программирования
         и их применение в среде MS-DOS и микропроцессоров 8086/8088.
             Кроме этого, представленный материал содержит еще две отдель-
         ные темы.  Эти темы связаны с проектированием модульных  программ
         на  языке  Ассемблер и реализацией этого проектирования путем ис-
         пользования  макроассемблера  MASM,  макроопределений  и   прочих
         средств,  относящихся к этим проблемам. Обе темы затрагивают осо-
         бенности написания,  наглядность (удобство чтения),  надежность и
         удобство  сопровождения прикладных программ.  Короче говоря,  эти
         методы вместе и отдельно могут быть использованы для структуриро-
         вания прикладных программ с целью повышения их качества.

                       Принципы модульного программирования

             При анализе программ,  написанных на языке Ассемблер,  обычно
         трудно  сразу  установить их структуру.  Несмотря на самые лучшие
         намерения большинства программистов,  их  программы  могут  иметь
         настолько запутанные связи и громоздкие скопления кодов, что час-
         то для их полного понимания требуется почти божественная проница-
         тельность.  Это  утверждение  ни в коем случае не рассматривается
         как неуважение к специалистам в области  программирования.  Труд-
         ности  установления  структуры являются результатом необходимости
         одновременно иметь дело с большим  количеством  деталей.  Имеются
         два направления в решении этой проблемы.  Первое направление сос-
         тоит в упрощении используемого программного кода,  замене сложной
         инструкции  последовательностью  с  более  понятными структурами.
         Технические приемы,  разработанные в главе 1,  следуя по  данному
         пути,  уменьшают количество деталей, возникающих при программиро-
         вании на языке Ассемблер.  Однако,  программисту все  еще  трудно
         справляться с большим количеством функциональных деталей.
             Второе направление состоит в применении  технических приемов,
         порожденных языками высокого уровня десятилетия тому назад.  Кон-
         цепции декомпозиции и модульного проектирования оказались  приме-
         нимыми  и  к программированию на языке Ассемблер.  Эти концепции,
         упоминаемые под общим  заголовком  "структурное  проектирование",
         позволяют  программисту  сегментировать  общую программную задачу
         таким образом,  чтобы одновременно обходиться только охватываемым
         контролем количеством деталей. Это и будет темой нашего последую-
         щего обсуждения.


                                      - 2-2 -
                               Опции проектирования

             Модульное проектирование и декомпозиция относятся  к процессу
         расчленения  больших  проблем  на более узкие,  более управляемые
         подпроблемы.  Первым шагом проектирования является решение, в ка-
         ком месте должна быть граница между этими подпроблемами.
             Для получения максимальных преимуществ от  использования  мо-
         дульного  программирования   каждая подпроблема или модуль должны
         иметь один вход и один выход.  В этом случае можно легко отслежи-
         вать  поток  управления в программе.  В любом месте модуля должна
         иметься возможнось увидеть точку входа в  модуль  и  сказать:  "Я
         знаю значения регистров X,Y и Z в этой точке, потому что они ука-
         зываются как...",  и затем проследить функционирование модуля без
         тревоги  об  искажении программы.  Один вход обеспечивает возврат
         потока управления в точку вызова при вызове модуля.  По этой при-
         чине,  модульные программы почти всегда выполняются как структуры
         CALL-RET.
             Использование нескольких  предложений  RET в модуле не должно
         нарушать правило одного входа, поскольку все инструкции RET возв-
         ращают  управление в одну и ту же точку.  Точно также,  переход к
         общему RET в конце модуля, не изменяет структуру модуля, а добав-
         ляет  лишь  коды  в модуль и увеличивает его сложность.  С другой
         стороны,  вход или выход из модуля не по этому правилу перечерки-
         вает  наибольшие  преимущества  модульного программирования:  яс-
         ность, удобство сопровождения.
             Имеется исключение из правила входа в модуль.  Это происходит
         при использовании таблицы переходов для реализации потока  управ-
         ления внутри программы. Таблица перехода используется путем "про-
         талкивания" адреса возврата в стек, вычисления индекса требуемого
         адреса  перехода в таблице и выполнения перехода в памяти. Пример
         этого приема показан в листинге  программы  драйвера  устройства,
         приведенной в главе 6.
             При практическом выполнении декомпозиции модулей можно  самим
         найти  некоторое  количество  альтернативных решений.  Прежде чем
         осуществить правильный выбор, необходимо знать альтернативы. Цель
         состоит  в  выборе таких альтернатив,  которые создадут наилучшие
         условия проектирования.

                            Функциональная декомпозиция

             При обращении к проблеме на стадии проектирования первым аль-
         тернативным выбором должна быть функциональная декомпозиция, т.е.
         разбиение проблемы на более узкие,  вполне поддающиеся управлению
         функциональные единицы, где каждая единица выполняет завершенную,
         легко идентифицируемую задачу.  Имеется множество путей определе-
         ния  содержания задачи.  Вот лишь некоторые примеры подобных еди-
         ниц, которые выполняют определенные функции: получение квадратно-
         го корня некоторого числа;  выполнение всех операций относительно
         указанного устройства таких, как операции в/в диска, операции в/в
         клавиатуры;  выполняющие  общую группу действий в указанное время
         такие, как инициализация областей данных; и единицы, которые вза-
         имодействуют последовательно или используют общие элементы данных
         такие,  как считывание данных с клавиатуры и преобразование их  в
         целые значения.
             В настоящее время в области программирования на языках  высо-
         кого уровня чаще всего принимаются такие решения,  которые предс-
         тавляют собой наилучший способ по использованию сегментации прог-

                                      - 2-3 -
         рамм. При  программировании на языке Ассемблер обычно нельзя быть
         столь последовательным. Каждый из ранее приведенных способов под-
         водит, по крайней мере, к начальной точке для разбиения проблемы.
         Часто обнаруживается,  что некоторые модули связываются с помощью
         одного набора критериев, а другие модули - с помощью другого. По-
         ка каждый модуль включает  легко  понимаемые  программные  секции
         (обычно две страницы или меньше), Вы находитесь на правильном пу-
         ти.

                  Минимизации количества передаваемых параметров

             Иногда обнаруживается, что после определения модулей програм-
         мы   создано  нечто  громоздкое и неуклюжее.  Это часто случается
         тогда,  когда модули при выполнении возложенных на них задач тре-
         буют доступа к обширному количеству данных.  Чаще всего это легко
         может произойти,  если на модуль возложено выполнение  нескольких
         опций.  Чтобы  знать  состояние программы в данное время,  модуль
         должен принимать очень много различных переменных.  Если это так,
         и  выявлено,  что модуль принимает большое количество параметров,
         необходимо ответить на следующие две группы вопросов:
             Первая: В этом модуле предпринята попытка выполнения несколь-
         ких функций? Требует ли модуль параметры, используемые в не отно-
         сящихся к данному модулю секциях?  Если ответы на эти вопросы по-
         ложительные,  то  необходимо  снова   обратиться   к   дальнейшей
         сегментации этого модуля.
             Вторая: Модуль представляет собой функциональный разрез?  Яв-
         ляются ли на самом деле вызывающий и вызываемый модули частью од-
         ной и той же функции? Если это так, то поместите их вместе в один
         модуль, даже если результирующий модуль окажется слишком большим.
         Затем попробуйте выполнить сегментацию  модуля  снова  различными
         способами.
             Сегментация модулей через функциональный разрез часто  проис-
         ходит тогда,  когда программист обнаруживает, что две программные
         секции идентичны или сильно похожи друг на друга. Программист за-
         тем  пытается создать из них один модуль.  Это не модульное прог-
         раммирование,  поскольку результирующий модуль имеет не  функцио-
         нальное соединение.
             Если в процессе проектирования будет обнаружено,  что  ничего
         сделать нельзя,  чтобы избежать использования большого числа ссы-
         лок на данные или передачи меток параметров,  вернитесь обратно в
         начало проектирования и проверьте корректность поставленной проб-
         лемы.

                    Минимизации количества необходимых вызовов

             Одним из существенных преимуществ модульного программирования
         является  то,  что  программа  основного уровня очень часто может
         быть сконструирована для чтения как последовательность вызываемых
         процедур.  Этот факт существенно повышает "понимаемость" програм-
         мы,  поскольку читатель может познакомиться с ее основным потоком
         и  функционированием  после  прочтения  только одной-двух страниц
         программного кода.  Однако   эта особенность может также иметь  и
         недостатки. Одна из многих верхних статистических оценок програм-
         мирования говорит о том, что 90% времени выполнения типовых прог-
         рамм расходуется в 10 % кода программы. При этом подразумевается,
         что если эти 10 %  содержат большое количество цепочечных вызовов
         процедур, то суммарное время, затрачиваемое на управление  выпол-

                                      - 2-4 -
         нением программы,  может стать непреодолимым препятствием на пути
         использования этого подхода.
             Прежде чем отказаться от модульности проектируемой программы,
         проверьте,  что скрывается под зависимостью программы от времени.
         Во-первых, большинство программ затрачивают большую часть времени
         выполнения на ожидание ввода информации с клавиатуры. После нажа-
         тия клавиши требуемые функции, с точки зрения выполнения длитель-
         ного процесса, обычно не расходуют время. Различие между 100 мик-
         росекунд и 100 миллисекунд для среднего пользователя неразличимо.
             Противоположным для некоторых мнением является то,  что дейс-
         твующий механизм пары CALL - RET не перекрывает потребляемое вре-
         мя. По сравнению с инструкциями перехода инструкция CALL выполня-
         ется на 30-50% дольше, а RET в среднем длиннее на 1 цикл.  Только
         когда во внимание принимаются накладные расходы передачи парамет-
         ров,  сохранения регистров и т.д., называемые служебными расхода-
         ми,   модульные  программы  начинают выглядеть медленнее по срав-
         нению с немодульными программами. В дополнение к тому, что модули
         модульных программ обычно являются более общими,  чем их неструк-
         турированные дубликаты, модули модульных программ могут использо-
         вать ссылки на память или стек с большей частотой. Дополнительное
         время,  расходуемое на вычисление действительного адреса  в  теле
         модуля,  может привести к замедлению выполнения конкретного моду-
         ля, чем узко закодированная конкретная программа.
             Преимущества служебных программ  и программ общего назначения
         заключаются в том, что модуль может быть использован виртуально в
         некотором  месте  программы.  При написании немодульной программы
         программист может потратить несколько часов, пытаясь открыть: ис-
         пользуется ли регистр,  или хуже того, верно ли то, что он должен
         использоваться. При модульном программировании программист не ин-
         тересуется тем,  какие регистры он использует в настоящий момент,
         пока вызываемый модуль копирует его параметры в стек  и сохраняет
         весь  набор  регистров на входе.  Эти особенности создают возмож-
         ность сначала использовать приемы модульного программирования для
         повышения  скорости кодирования и затем переработки программы для
         удаления "узких" мест.
             Для областей,  чувствительных к скорости работы, лучшей реко-
         мендацией является выбор основной ветви  программы.  Если  модуль
         упоминается только в чувствительной к скорости работы программной
         секции,  то он может быть включен в "ветвь"  внутри   вызывающего
         модуля.  Если другие секции используют модуль,  то они могут быть
         скопированы в вызывающий модуль в необходимое место.  В  связи  с
         тем,  что  основной вызывающий модуль станет большим,  необходимо
         вставить комментарии в его тело, помечающие включаемый модуль как
         блок его владельца.  Будущие читатели смогут затем прочитать ком-
         ментарии для определения функций модуля и пропустить его мимо для
         возобновления чтения основного кода.

                        Правила модульного программирования

             Наиболее существенные  концепции  модульного программирования
         можно изложить в виде следующих правил:
             - Разделение и подчинение.  Разделите проблему на узкие функ-
         циональные задачи, каждая из которых независима от других, исклю-
         чая необходимые для нее параметры;
             - Один вход - один выход.  Модуль должен  иметь  только  одну
         точку  входа,  в  которую передается управление при всех вызовах.
         Возвращать управление модуль должен в ту точку в программном  по-
         токе,  из  которой было передано управление (адрес возврата может

                                      - 2-5 -
         быть модифицирован,  как обсуждается в следующем разделе по пере-
         даче параметров);
             - "KISS-принцип"(Keep It Simple,Stuiped - делай  все попроще,
         дурачок). Избегайте сложности при кодировании модуля. Используйте
         сложную логику только при условии хорошего документирования, объ-
         ясняющего каждый шаг и способ его проектирования;
             - Упрятывание подробностей. Ограничивайте подробности исполь-
         зования регистров, структуры локальных данных и т.д. для внутрен-
         них модулей. Не допускается реализацию модуля перебрасывать в ос-
         тавшуюся часть программы;
             - Если модуль использует  особую  переменную,  сделайте  так,
         чтобы  переменная была документируемым параметром. Документируйте
         все действия, чтобы модуль имел глобальные данные;
             - Планируйте  обнаружение  ошибок и действия,  которые должны
         быть  предприняты при возникновении  ошибки.  Ответственность  за
         обработку  исключительных ситуаций,  как известно,  должна возла-
         гаться на конкретные модули. Обычно модули нижнего уровня переда-
         ют отчеты об ошибках в вызывающий модуль. Ответственность за при-
         нятие решений по этим ошибкам обычно  резервируется  за  модулями
         верхнего уровня.

                               Справочная литература
             То, что представлено в данном разделе,  является кратким вве-
         дением в концепции  структурного  программирования  и  модульного
         проектирования.  За неимением места,  мы не могли полностью обсу-
         дить данный предмет.  Однако,  по этой проблеме доступно огромное
         количество литературы.  Если Ваша цель состоит в том, чтобы стать
         профессионалом в области программного обеспечения,  то купите не-
         которые  из этих книг и прочитайте их.  Представленные ниже книги
         являются классическими работами по данному  предмету  и  отражают
         узкие  примеры  превосходных  работ на доступном профессиональном
         уровне:
             DeMarco, T. Structured Analysis and System Specification. New
               York: Yourdon,1978.
             Kane,G.,D.Hawkins and  L.Leventhal.68000  Assembly   Language
               Programming,Berkeley:Osborne/McGraw-Hill,1981.
             Tausworthe, R.C.   Standardized   Development   of   Computer
               Software.  Part  1.  Englewood Cliffs,  N.J.:Prentice-Hall,
               1977.
             Yourdon, E.U.,   and   L.L.Constantine.   Structured  Design.
               Englewood Cliffs, N.J.:Prentice-Hall,1977.
             Yourdon, E.U.  Techniques  of  Program  Structure and Design.
               Englewood Cliffs, N.J.:Prentice-Hall,1975.

                 Реализация модульных программ на языке Ассемблер

             До сих пор мы вели абстрактный разговор о  модулях,  передаче
         параметров  и других подобных термах.  Теперь наступило время на-
         чать рассмотрение отношения этой информации  к  конкретному  миру
         языка Ассемблер в среде MS-DOS,  макроассемблера MASM и микропро-
         цессора 8086.
             Модули в  среде MASM лучше всего поддерживаются с помощью ди-
         рективы PROC. Мы будем использовать ее все время в качестве мето-
         да определения точек входа и выхода  программы.  Теперь расширим
         ее  использование  для определения  границ  конкретных  модулей.
         Директива PROC используется MASM для определения метки в програм-

                                      - 2-6 -
         ме,  поэтому дадим этой метке либо атрибут near (близкий) или ат-
         рибут far (далекий).  Этот атрибут используется для генерации как
         правильных типов инструкции CALL, так и правильных типов инструк-
         ции RET. Подробное представление этих типов инструкций приводится
         далее  в  разделе "Типы кодирования".  Здесь нас прежде всего ин-
         тересует то, что директива PROC является удобным способом обозна-
         чения блока программы с одним уникальным входом и постоянным  вы-
         ходом, которые образуют основу модуля.

              Определение параметра, аргумента, переменной константы

             Мы "вращались"  вокруг  слов параметр,  аргумент и переменная
         подобно шарикам для настольного тенниса. Большей частью эти слова
         имели  взаимозаменяемые  значения.  Теперь  необходимо определить
         различия между ними (хотя некоторые из них несомненно будут  тре-
         бовать уточнения).  После этой главы мы будем всегда возвращаться
         к правильным способам,  однако, сейчас необходимо пояснить основ-
         ные понятия и положения.
             Словарным понятием "параметр" является "элемент характеристи-
         ки".  В  общем смысле параметр представляет собой ссылку на любую
         часть данных, используемую модулем, которая в целом не содержится
         внутри этого модуля.  Почему добавлены слова "ссылка на"? Потому,
         что параметр это не сами данные, и, даже, не адрес данных. Скорее
         всего, параметр это местодержатель (элемент характеристики). Нап-
         ример, рассмотрим уравнение Y+1. Нельзя написать модуль для оцен-
         ки этого уравнения,  потому что Y не является конкретным значени-
         ем!  Y есть параметр, который заменяется действительным значением
         во время оценки этого уравнения.  Действительное значение называ-
         ется "аргументом".
             Мы еще не определили понятие "переменная". Строго говоря, это
         нечто, размещенное в регистре или ячейке памяти и содержащее пор-
         цию данных,  которые могут в дальнейшем изменяться.  В предыдущем
         примере Y также является переменной,  поскольку он  изменяется  в
         зависимости от требуемых обстоятельств.  Поэтому аргументы автома-
         тически являются переменными (но не наоборот).
             Таким образом,  если  объект данных может изменяться,  то это
         переменная.  Если же эта переменная требуется в модуле для выпол-
         нения возложенной  на него задачи,  то она в то же время является
         параметром.  Аргумент - это действительное значение, которое при-
         нимает переменная при вызове модуля.
             Нам также необходимо рассмотреть специальный случай "констан-
         ты".  Константа - это объект данных, значение которого никогда не
         изменяется.  В языке Ассемблер константы могут появляться в  двух
         случаях.  Они  могут быть частью непосредственных данных для инс-
         трукций (например, mov al,4) или они могут быть размещены в памя-
         ти подобно другим данным.  Когда константа помещена в память, она
         отличается от переменной исключительно тем,  что "только  читает-
         ся" и никогда не записывается.
             Может ли параметр также быть константой? Если константа явля-
         ется типом памяти,  то определенно да. Однако, при попытке непос-
         редственного использования константы данных в  качестве параметра
         могут возникнуть определенные проблемы. Непосредственные данные в
         подпрограмме передавать сами себя не могут. Непосредственные дан-
         ные должны содержаться в чем-либо:  в регистре, ячейке памяти или
         в стеке.  В языках высокого уровня о  преобразовании  констант  с
         целью  их размещения заботится компилятор.  В языке Ассемблер это
         должен делать сам программист.

                                      - 2-7 -
                                Параметры и модули

             Мы определили,  что параметры представляют  собой  какие-либо
         данные,  требуемые модулем для выполнения возложенной на него за-
         дачи,  и которые размещаются вне модуля. Мы также определили, что
         параметры определяют и переменные.  Таким образом, вырисовывается
         второе большое преимущество модулей.  В связи с тем, что входными
         данными для модуля являются переменные, то они могут быть измене-
         ны для подходящего конкретного случая.  Тем самым, модулям прида-
         ется  больше общности,  позволяя им быть повторно-используемыми в
         любом месте любой программы.
             В действительности, параметры являются необязательными компо-
         нентами модульного программирования.  Можно иметь модуль, который
         не принимает внешние параметры,  а функционирует исключительно  с
         внутренними данными. Простая программа выработки звукового сигна-
         ла консоли не имеет параметров.  Более  общим  примером  является
         простая  программа для чтения чисел с клавиатуры.  Хотя программа
         чтения числа будет возвращать значение,  программе не нужен ника-
         кой аргумент, передаваемый для нее.
             Объединяя требующиеся  входные параметры и вырабатываемые вы-
         ходные значения, можно сформировать следующие четыре группы моду-
         лей:
             1. Модули, не принимающие входные параметры и не вырабатываю-
         щие выходные значения.
             2. Модули,  принимающие входные параметры и не вырабатывающие
         выходные значения.
             3. Модули,  не принимающие входные параметры и вырабатывающие
         выходные значения.
             4. Модули,  принимающие  входные  параметры и вырабатывающие
         выходные значения.
             Обычно первые две группы модулей,  не вырабатывающие выходные
         данные,  называются подпрограммами, а последние два типа, выраба-
         тывающие выходные данные, функциями. Заметим, что различие произ-
         водится в зависимости от того, требуют ли модули входных парамет-
         ров, хотя как программист вы интуитивно осознаете различие.

                             Опции передачи параметров

             Для программ, будь они подпрограммами или функциями, принима-
         ющими входные параметры,  должна быть разрешена проблема передачи
         им  параметров.  При  программировании на языке высокого уровня в
         этом отношении программист обычно не имеет выбора.  В  языке  Ас-
         семблер таких опций много.  Мы рассмотрим все опции, хотя исполь-
         зовать некоторые из них необходимо с осторожностью.

                              Передача через регистры

             Наиболее общим способом передачи данных  при программировании
         на  языке Ассемблер является способ передачи данных через регист-
         ры. Немедленная доступность и высокая скорость делают их основны-
         ми  средствами  для решения поставленной задачи.  Регистры всегда
         отделены от кода операции, независимо от используемой программной
         среды.  Почти  все вызываемые функции MS-DOS передают свои данные
         таким способом.  Короткие программы языка  Ассемблер,  являющиеся
         интерфейсными программами  с MS-DOS,  для манипулирования данными
         часто используют те  же  самые регистры, которые требуют функции,
         вызывающие эти данные MS-DOS.Это приводит к ощущению создания па-

                                      - 2-8 -
         раметра в том же регистре, что и MS-DOS.
             Одной из трудностей, возникающей при использовании этого спо-
         соба, является то, что количество регистров, имеющееся в наличии,
         ограничено.  Если имеется программа, требующая большее количество
         регистров, чем имеется в наличии, то это вызывает лишние хлопоты.
         Новые микропроцессоры имеют меньшие ограничения,  чем старые,  но
         количество регистров все равно ограничено. Кроме того, если необ-
         ходимо  переместить  часть  программы из одного типа процессора в
         другой, то ситуация, в которой два процессора могут совместно ис-
         пользовать один и тот же набор регистров,  маловероятна. Вам при-
         дется перепроектировать все интерфейсы модулей.
             Другой трудностью является то, что необходимо непрерывно сох-
         ранять используемую дорожку, на которую выводится каждый регистр.
         Эта  игра "кто первый" может наскучить даже наиболее опытному иг-
         року. Особое расстройство вызывает случай, когда принято решение,
         что регистр X освободился и, следовательно, освободилась програм-
         ма модуля.  Позднее,  когда принимается решение об  использовании
         того же самого модуля в другом месте, может оказаться, что не ос-
         вободился как раз только регистр X.  Так команда PUSH  (запомнить
         содержимое регистра в стеке) записывает в стек значение, содержа-
         щееся в регистре X,  затем выполняется вызов и команда POP  (выб-
         рать  значение  регистра из стека) выбирает из стека значение ре-
         гистра и заносит его в регистр X. Таким образом, в результате та-
         кого оборота X содержит возвращаемое значение. Видите, что теперь
         освободилось? И так случается очень часто.
             Практически, ограничением  передачи параметров через регистры
         является ограничение объема информации,  передаваемой  через  ре-
         гистры,  до 16 бит,  т.е. размера наибольшего регистра. В связи с
         тем,  что большинство переменных имеют тенденцию использоваться в
         виде  байтов  или слов,  ограниченный размер регистра не является
         серьезной проблемой. Когда передаваемые данные  превышают  размер
         регистра,  вместо  данных  вызывающая программа может передать их
         адрес в памяти. Конечно, для правильного использования данных вы-
         зывающая программа должна знать, какой был указан тип данных. При
         вызове функций MS-DOS всякий раз,  когда они требуют большого ко-
         личества  данных,  используется этот механизм передачи данных,  с
         заключающимся в указании адреса данных в памяти.

                    Передача данных через общую область памяти

             Следующим способом передачи данных является использование за-
         ранее  подготовленной области памяти.  Понятие "заранее подготов-
         ленная" используется в том смысле,  что и вызывающая и вызываемая
         программы "договорились" между собой о том, что их данные переда-
         ются в некоторую область основной памяти.  Программа А знает, что
         вывела  квитанции  об оплате за последний месяц в область памяти,
         помеченную как FOO, и программа В знает как выглядит эта информа-
         ция  в области памяти FOO.  Поэтому область памяти FOO называется
         общей областью памяти.
             Передача данных через общую область памяти имеет,  по крайней
         мере,  один существенный момент. В пределах физических возможнос-
         тей  используемого  компьютера  можно  передать  столько  данных,
         сколько необходимо.  Передача данных через общую  область  памяти
         позволяет  передавать и принимать целую гамму свободных регистров
         и допускает передачу данных любого размера от буфера в  один байт
         до нескольких килобайт.
             Кроме того, передача данных через общую область памяти делает

                                      - 2-9 -
         доступными передаваемые данные любому модулю, которому они требу-
         ются. Это большое преимущество, т.к. запрашиваемые данные переда-
         ются  от модуля верхнего уровня через большое количество внутрен-
         них модулей к модулю нижнего уровня.  Поэтому,  каждый модуль  не
         должен обрабатывать те данные, которые он не использует.
             Отрицательной стороной этого способа передачи данных является
         то,  что  зависимость от общей памяти может ограничить общность и
         повторность использования модулей.  Рассмотрим ряд модулей, пред-
         назначенных  для  чтения и записи файлов.  Если модули кодируются
         для использования общего блока памяти,  то может возникнуть проб-
         лема  одновременного  открытия  двух файлов.  Если программа была
         спроектирована для выполнения сравнения,  то она должна  скопиро-
         вать  один набор данных из буфера в область памяти для предотвра-
         щения возможной перезаписи буфера.
             Последний недостаток  способа передачи данных через общую об-
         ласть памяти вытекает из его особенности.  В связи с тем, что об-
         ласть памяти доступна любому модулю,  это  типичный  случай "игры
         по правилам". Защита данных от случайного разрушения почти невоз-
         можна.  Обычно  это  не является большим риском (поскольку ошибки
         программы общие),  но становится очень существенным фактором  при
         рассмотрении повторно используемого программирования (рассматрива-
         емого в следующем разделе "Типы кодирования").

                      Передача данных через память программы

             Передача данных  через память программы является одним из ва-
         риантов передачи данных через общую область памяти.  Первым отли-
         чием  является  то,  что  данные располагаются в памяти программы
         (программном сегменте),  а вторым - то, что местоположение данных
         определяется с помощью инструкции CALL, поскольку данные размеща-
         ются непосредственно после вызова.
             Вызываемая программа  выбирает  адрес возврата из стека,  ис-
         пользуемого в качестве указателя на  область  памяти,  складывает
         размер области памяти с адресом возврата и помещает его обратно в
         стек.  При возврате управления в вызывающую программу адрес возв-
         рата располагается сразу же после области данных.
             Это кажется  удобным до тех пор,  пока не примем во внимание,
         что микропроцессор 8086 специально спроектирован  для  разделения
         программы  и областей данных.  Передача данных через память прог-
         раммы требует,  чтобы программный сегмент и сегмент  данных  были
         установлены в одно и то же значение, поэтому адрес возврата отно-
         сится к программному сегменту.
             Самой наихудшей проблемой способа передачи данных  через  па-
         мять программы является то,  что он требует манипуляции стека,  в
         котором происходит то самое "сближение" для текущей самомодифици-
         руемой  программы.  Одно правило,  которое всегда необходимо пом-
         нить, состоит в том, чтобы никогда, никогда не модифицировать па-
         мять  программы!  Если  все  же поддаться соблазну,  то программа
         станет почти неподдающейся отладке без тщательного анализа логики
         программного обеспечения.

                              Передача данных в стек

             Передача данных в стек является способом,  используемым боль-
         шинством компиляторов языков высокого уровня для реализации вызо-
         ва  процедур.  При этом способе передачи данных перед выполнением

                                      - 2-10 -
         вызова все требуемые параметры заносятся в стек. После вызова вы-
         зывающая программа осуществляет доступ к данным без их пересылки.
         Проектировщики семейства микропроцессоров  8086  поддержали  этот
         способ  при обеспечении регистра BP (base pointer - указатель ба-
         зы). Регистр BP имеет удивительную особенность адресации его опе-
         рандов относительно сегмента стека.  Это означает,  что при уста-
         новке значения регистра BP  в  правильное  положение,  содержимое
         стека может быть адресовано путем использования индексной адреса-
         ции.
             Что же такое "правильное положение" при загрузке в BP? Это не
         сам SP (stack pointer - указатель стека),  поскольку SP указывает
         на адрес возврата в стек.  Данные обычно начинаются с ячейки SP+2
         или ячейки SP+4. Почему плюс 2, или плюс 4? Потому, что для вызо-
         ва  процедуры  near (близкий) процессор запоминает только текущее
         смещение (указатель инструкции) в стеке (2 байта), в то время как
         для  вызова процедуры far (далекий) процессор запоминает смещение
         и сегмент программы в стеке (4 байта). Вызываемая программа может
         быть  закодирована  для  начала доступа в правильном положении (в
         зависимости от типа программы) при использовании следующей  адре-
         сации:


             NEAR                          FAR
             mov  bp,sp                    mov  bp,sp
             mov  <1-й аргумент>,[bp+2]
             mov                           <1-й аргумент>,[bp+4]
             ...                             ...

             Заметим, что если необходимо  сохранить  содержимое  регистра
         BP, то обычно в этом случае вызываемая программа должна поместить
         BP в стек, изменив адрес первого параметра на [BP+4] для програм-
         мы near и на [BP+6] для программы far. Чтобы избежать это измене-
         ние адресов,  необходимо перед тем,  как  поместить  параметры  в
         стек, передать вызывающей программе ответственность за сохранение
         BP.  Однако,  из-за причин обеспечения совместимости это не реко-
         мендуется.  Вместо этого более предпочтительным способом передачи
         параметров является структура, показанная в листинге 2-1. Исполь-
         зование этой структуры,  заимствованной большинством языков высо-
         кого уровня,  поможет при разработке  мобильных,  многократно ис-
         пользуемых   программ.   Эти   программы  могут  быть  собраны  в
         "инструментальный набор", который необходимо использовать во мно-
         гих местах для облегчения программирования и повышения производи-
         тельности работы.
             При возврате вызываемой программы параметры, которые были по-
         мещены в стек,  теперь должны быть удалены.  Вызывающая программа
         может  удалить параметры либо извлечением из стека (путем исполь-
         зования инструкции POP),  либо просто добавлением хранимых  пара-
         метров  в  регистр SP,  например,  по инструкции add SP,N,  где N
         представляет собой  количество  байтов,  занимаемых  параметрами.
         Этот способ, показанный в листинге 2-1, эффективно урезает стек в
         первоначальное положение. Альтернативно ответственность за очист-
         ку  стека может быть назначена вызываемой программе путем исполь-
         зования инструкции RET N, где N опять количество байтов, занимае-
         мое параметрами.  При любом способе N равно количеству помещенных
         с помощью инструкции PUSH слов, умноженное на 2.
             Различие между  этими двумя способами состоит в том,  что при
         использовании инструкции RET N   программа  должна  вызываться  в

                                      - 2-11 -
         точности  с правильным количеством параметров.  Если имеется не N
         байтов параметров, то инструкция RET N неправильно выровняет стек
         и произойдет авария системы. Напротив, если стек очищает вызываю-
         щая программа путем использования инструкции add SP,N,  то каждый
         вызов  в  целевую программу может передавать различное количество
         параметров.

                      Листинг 2-1. Передача параметров в стек
         -----------------------------------------------------------------
                                ; Вызывающая процедура

              ...          ...
             push      <argument_N>   ; пересылка последнего аргумента
              ...          ...
             push      <argument_2>   ; пересылка   второго   аргумента
             push      <argument_1>   ; пересылка первого аргумента
             call      <myproc>       ; вызов процедуры
             add       sp,<2N>        ; очистка стека
              ...          ...
                                ; Вызываемая процедура

         <myproc> PROC NEAR           ; пример вызова процедуры near
             push      bp             ; сохранение старого BP
             mov       bp,sp          ; указатель ссылки на стек
              ...          ...
             mov       <dummy>,[bp+4] ; доступ к первому параметру
             mov       <dummy>,[bp+6] ; доступ ко второму параметру
              ...          ...
             mov       <dummy>,[bp+2+2N] ; доступ к последнему параметру
              ...          ...
             mov       sp,bp          ; восстановление SP
             pop       bp             ; удаление сохраненного BP
             ret                      ; возврат в вызывающую программу
         <myproc> ENDP
         ----------------------------------------------------------------

             До тех пор,  пока вызывающая программа обрабатывает стек пра-
         вильно,  проблем  не будет.  (Конечно,  если вызываемая программа
         сможет использовать различное количество  параметров,  выдаваемых
         от вызова к вызову).
             С целью облегчения программирования следует  заменить простой
         вызов внешними участками программы, использующими инструкции PUSH
         (записать в стек), MOV (переслать), POP (извлечь из стека) и про-
         чие. Это как раз одно из мест для вывода известных и простых мак-
         росов для выполнения этих рутинных операций. Макросы, приведенные
         в  листинге 2-2,  помогают вызывающей программе поддерживать стек
         во время передачи параметров.  Аналогично, макросы, приведенные в
         листинге 2-3, помогают вызываемой программе при доступе и возвра-
         те параметров из стека.  Все регистры, используемые в этих макро-
         сах, должны быть длиной в слово, потому что инструкции PUSH и POP
         не работают с 8-битовыми регистрами.

                                      - 2-12 -
         Листинг 2-2. Макросы @CallS и @FCallS для передачи параметров стек
         ------------------------------------------------------------------
         ;; **** Макрос @PushIm: запись в стек непосредственных данных
         ;;                                  через регистр BP
         @PushIm MACRO   arg
                 mov     cs:mem_16,&arg
                 push    cs:mem_16
                 ENDM
         ;; **** Макрос  Вызов подпрограммы: @Calls  имя,<arg1,arg2,...>
         @CallS  MACRO   имя_программы,список_аргументов
         ?count  =       0
                 IRP     argn,<&arg_list>
                 push    &&argn          ; передача параметра
         ?count  =       ?count+1
                 ENDM
                 @PushIm %?count         ; передача количества параметров
                 call    &routine_name   ; вызов программы
                 add     sp,2*(1+?count) ; очистка стека
                 ENDM
         ;; **** Макрос  Вызов функции: @FCallS  имя,<arg1,arg2,...>
         @FCallS MACRO   имя_пр-мы,список_арг-тов,возвращаемое_значение
         ?count  =       0
                 IRP     argn,<&arg_list>
                 push    &&argn          ; передача параметра
         ?count  =       ?count+1
                 ENDM
                 @PushIm %?count         ; передача количества параметров
                 call    &routine_name   ; вызов программы
                 pop &return_val         ; получение  возвращаемого знач-я
                 if ?count               ; если не нуль ...
                 add sp,2*?count         ; очистка стека
                 ENDIF    ENDM
         -----------------------------------------------------------------
                   Листинг 2-3. Макросы @Accept,@RetVal и @CRet
                 для приема в стек и возврата параметров из стека
         -----------------------------------------------------------------
         ;; **** Макрос  @RetVal: @RetVal регистр
         @retVal MACRO   возвращаемое_значение
                 mov     [bp+4],return_val ; возврат слова
                 ENDM
         ;; **** Макрос  @Accept: @Accept <reg1,reg2,...>
         @Accept MACRO   список_регистров
                 push    bp              ; сохранение указателя базы
                 mov     bp,sp           ; уст-ка BP для доступа к парам.
                 mov     &pnum,[bp+4]    ; получение количества парам-ов
         ?count  =       0
                 IRP     reg,<&reg_list>
         ?count  =       ?count+1
                 push    &&reg           ; сохр-е рег-ра для нового знач.
                 mov     &&reg,[bp+4+?count*2] ; получение параметра
                 ENDM
                 ENDM
         ;; **** Макрос  @CRet: @CRet <reg1,reg2,...>
         @CRet   MACRO   список_регистров
                 IRP     reg,<&reg_list>
                 pop     &&reg           ; восст-е сохраненного регистра
                 ENDM
                 pop     bp              ; восстановление указателя базы
                 ret                     ; возврат из программы
                 ENDM

                                      - 2-13 -
             Макрос  @PushIm  позволяет  пользователям   микропроцессоров
         8086/8088 помещать непосредственные данные в стек.  Для использо-
         вания макроса сначала необходимо определить в программном сегмен-
         те местоположение слова mem_16.  Несмотря на то, что передача не-
         посредственных  данных  в  стек  медленная  и принимаются большие
         коды, такой алгоритм работы создает большую свободу использования
         регистров.
             Символ ?count  в  макросах  @CallS и @FCallS используется для
         сообщения вызываемой программе количества  предусмотренных  пара-
         метров;  для приема количества байтов,  помещенных в стек;  и для
         использования при очистке стека после вызова.  Если  целевая  или
         вызываемая  программа уже знает сколько параметров было в нее пе-
         редано (обычно является случайным),  то эти макросы  должны  быть
         модифицированы,  чтобы  обойтись  без передачи и очистки счетчика
         параметров.  Заметим, что счетчик параметров также используется в
         качестве  поля  возврата  значения  для  вызова  функций (макросы
         @FCallS и @RetVal).
             Макрос  @RetVal предназначен  для  использования  с  макросом
         @FCallS  и  замещает счетчик параметров,  помещенный в стек с по-
         мощью макроса @FCallS,  16-битовым значением для возврата в вызы-
         вающую программу.
             Макрос @Accept  целевой  программы  работает  либо с макросом
         @CallS, либо с макросом @FCallS для передачи параметров из стека в
         регистр.  Этот макрос сохраняет регистры,  используемые в процессе
         работы.  Символ ?count используется здесь для определения смещения
         следующего параметра в стеке.  В связи с тем,  что макрос  @Accept
         работает  в  направлении вверх по стеку (увеличение смещения),  то
         этот макрос выбирает параметры из стека в порядке,  обратном тому,
         в котором они были помещены в стек! Заметим также, что оба макроса
         @Accept и @RetVal предполагают  вызов  процедуры  near  (близкий),
         поскольку они допускают только 2-байтовый адрес возврата.
             Последний целевой макрос @CRet восстанавливает регистры,  ко-
         торые были сохранены макросом @Accept.  В связи с тем,  что  инс-
         трукции  POP  должны  быть в обратном порядке по отношению к инс-
         трукциям  PUSH,  список  аргументов  для  макроса  @CRet   должен
         располагаться в порядке,  обратном тому, какой был при выполнении
         макроса @Accept.  Последним действием, предпринимаемым перед инс-
         трукцией RET, является восстановление указателя базы, сохраненно-
         го макросом @Accept.
             Приведенные макросы представлены здесь скорее в качестве при-
         меров, нежели рабочих копий и могут быть улучшены для обеспечения
         более полного использования.  Например,  параметр инструкции PUSH
         (push &&argn) для обработки непосредственных  данных  в  качестве
         параметров может быть замещен более общим макросом PushOp из гла-
         вы 1.  Одним из ограничений текущей версии является то,  что инс-
         трукция  mov [bp+4],return_value в макросе @RetVal не может возв-
         ращать переменные  памяти в стек,  потому что семейство микропро-
         цессоров 8086 не поддерживает инструкцию   пересылки   память-па-
         мять. Для распознавания пересылки память-память и генерации пере-
         дачи через  непосредственный  регистр  этот  макрос  должен  быть
         переделан.
             Кроме того,  необходимо иметь в виду,  что макросы, представ-
         ленные в листингах 2-2 и 2-3, реализуют вызывающую программу, ко-
         торая  несовместима  ни с одним известным языком высокого уровня.
         Характерно, что эти процедуры в качестве дополнительного аргумен-

                                      - 2-14 -
         та  передают  количество  аргументов  для  вызываемой процедуры и
         возвращают значения для вызывающей  процедуры  непосредственно  в
         стек.
             MASM обеспечивает для вызываемой программы  некоторые  средс-
         тва,  упрощающие  доступ  к  данным  в стеке.  Благодаря описанию
         structure (структура),  которая описывает данные в стеке и вырав-
         нивает указатель базы (BP) на начало структуры,  к данным в стеке
         можно обращаться по символическим именам. Это помогает предотвра-
         щать  фатальные ошибки кодирования,  которые являются результатом
         указания неправильного смещения. Листинг 2-4 демонстрирует дирек-
         тиву MASM STRUC в этом контексте.

               Листинг 2-4. Символический доступ к содержимому стека
                                по директиве STRUC
         -----------------------------------------------------------------

                               ; Вызывающая процедура

                  ...        ...
                 push   <argument_1> ; пересылка 1-го аргумента
                 push   <argument_2> ; пересылка 2-го аргумента
                  ...        ...
                 push   <argument_N> ; пересылка последнего аргумента
                 call   <myproc>     ; вызов процедуры
                  ...        ...

                               ; Вызываемая процедура

         StackFrame     STRUC        ; описание шаблона стека
                 dw     ?            ; сохраненный BP
                 dв     ?            ; адрес возврата (используйте "dw"
         ;                             для NEAR (близкий))
         paramN  dw     ?            ; последний параметр
                  ...       ...
         param2  dw     ?            ; 2-й параметр
         param1  dw     ?            ; 1-й параметр
         StackFrame     ENDS         ; конец описания шаблона
         ;
         base    EQU    [bp]         ; база шаблона
         ;
         <myproc> PROC  FAR          ; пример вызова far (далеко)
                 push   bp           ; сохранение старого BP
                 mov    bp,sp        ; указатель ссылки в стеке
                  ...      ...
                 mov    <dummy>,base.param1 ; доступ к 1-му параметру
                 mov    <dummy>,base.param2 ; доступ ко 2-му параметру
                  ...      ...
                 mov    <dummy>,base.paramN ; доступ к последнему пар-ру
                  ...      ...
                 mov    sp,bp        ; восстановление SP
                 pop    bp           ; сброс сохраненного BP
                 ret    (2N)         ; возврат в вызывающую программу
         <myproc> ENDP
         ----------------------------------------------------------------

             Листинги 2-1 и 2-4 различаются по трем важным аспектам.  Пер-
         вое  отличие заключается в порядке помещения параметров в стек. В

                                      - 2-15 -
         листинге 2-1 вызывающая программа размещает свои параметры в сте-
         ке в обратном порядке (от последнего к первому), в то время как в
         листинге 2-4 в прямом порядке  (от  первого  к  последнему).  Для
         структуры  StackFrame при работе с листингом 2-1 порядок парамет-
         ров должен быть изменен на противоположный  (т.е.  реверсирован).
         (Назначение  порядка  "от  первого к последнему" может привести к
         путанице в этой точке. На самом деле назначается порядок следова-
         ния параметров "слева - направо",  т.е.  как они появлялись бы на
         языке высокого уровня).
             Второе отличие  между примерами заключается в способе очистки
         переданных параметров из стека. В примере, приведенном в листинге
         2-1,  вызывающая  программа очищает параметры в стеке посредством
         инструкции add SP,<2N>.  В листинге 2-4 вызываемая программа очи-
         щает стек, используя инструкцию ret (2N).
             Последнее отличие заключается в том, что в листинге 2-1 пока-
         зана программа near (близкий),  в то время как в листинге 2-4 по-
         казана программа far (далекий).  Если структура  StackFram  будет
         использоваться с процедурой near, то необходимо заменить директи-
         ву dd директивой dw.  Это вызывает резервирование в  шаблоне  для
         адреса  возврата  вызывающей  программы только двух байтов,  в то
         время как для вызываемой процедуры far требуется четыре  байта. С
         другой  стороны,  если  структура  будет использована в программе
         прерывания, то  после  директивы dd необходимо будет добавить до-
         полнительную директиву dw для резервирования памяти  для  флажков
         процессора, помещаемых в стек при прерывании.
             Директива STRUC  не  выполняет добавление никакого кода в ко-
         нечную программу.  Эта директива только описывает  смещения,  ис-
         пользуемые с указателем базы BP,  для облегчения задачи обращения
         к параметрам.
             Стек также обеспечивает удобное место для хранения возвращае-
         мых значений, но мы отложим обсуждение этой темы до тех пор, пока
         не  обсудим различия между функциями и подпрограммами в последую-
         щих разделах этой главы.

                    Краткое изложение опций передачи параметров

             Существует три возможных способа передачи данных модулям:
             1. Передача  через  регистры - допускается несколько парамет-
         ров; это наилучший способ для простого интерфейса и для обработки
         исключительных ситуаций, а также для возврата значений.
             2. Передача через общую область памяти  -  ограничивает  гиб-
         кость и общность модулей,  но имеет преимущества обеспечения дос-
         тупности данных для всех модулей.
             3. Передача данных в стек - предпочтительный способ обработки
         данных;  превосходит в общности (многократно-используемые модули)
         и  вырабатывает  модульные программы;  необходим для интерфейса с
         большинством языков высокого уровня.
             Кроме того,  при передаче данных способом,  отличным от выше-
         приведенных, каждый модуль должен принимать в качестве параметров
         необходимые ему данные не только для себя самого, но и для других
         модулей,  которые он вызывает в процессе своей работы. Иногда это
         может  привести к большому списку параметров для модулей верхнего
         уровня.
             В действительности,  по всей вероятности, для организации пе-
         редачи данных модулю может потребоваться использование комбинации
         этих  способов  (за исключением передачи данных в память програм-
         мы).

                                      - 2-16 -

                    Передача параметров по значению или адресу

             После принятия решения о том, как передавать параметры, необ-
         ходимо ответить на вопрос:  в какой форме использовать аргументы?
         Вспомните,  что аргумент представляет собой вызываемое  значение,
         которое присваивается параметру. Это значение может быть либо не-
         посредственными данными, либо адресом данных.

                               Передача по значению

             Чаще всего передача параметров в языке  Ассемблер выполняется
         путем  передачи значения.  При этом способе передачи в вызывающую
         программу передаются действительные данные (их значения). Целевая
         программа получает число,  хранящееся в регистре, либо помещенное
         в стек.
             Хранение  данных  в общей памяти может представлять собой не-
         сколько специальных случаев. В первом случае данные передаются по
         адресу,  поскольку вызывающая и вызываемая программы осуществляют
         обращение к данным посредством значений общих адресов.  В  другом
         случае  данные  в общей области могут быть либо значениями,  либо
         адресами,  и проблема упрощается,  базируясь на решении о природе
         данных в общем блоке. Если данные являются значениями, то они пе-
         редаются по значению.  Если данные являются адресом, то они пере-
         даются по адресу.
             Если параметры, содержащие непосредственные данные, передают-
         ся в стек, то пользователи микропроцессоров 8086 и 8088 не должны
         испытывать страх перед некоторыми дополнительными усилиями,  свя-
         занными с передачей значений в стек.  Пользователи усовершенство-
         ванного микропроцессора 80x86 могут использовать  инструкцию
                     PUSH <immediate>
         (переслать в стек непосредственные данные),  однако  пользователи
         других микропроцессоров должны передавать данные в стек через не-
         посредственный регистр.  Для  этого может быть использован макрос
         @PushIm,  рассмотренный  в  главе 1, однако, для этого приложения
         его сложность не указывается.  Если используется вызывающая прог-
         рамма, приведенная в листинге 2-1, то для передачи непосредствен-
         ных данных в стек доступен регистр BP (указатель базы).  Почти во
         всех соглашениях по архитектуре микропроцессоров  8086  для  этих
         целей предназначен регистр AX. Любые непосредственные данные, ко-
         торые необходимо переслать в стек, передаются с помощью следующих
         двух строк программы:

               mov   ax,<immediate_data>
               push ax

             Способ передачи  параметров по значению унаследовал ограниче-
         ние передаваемого значения при использовании регистра  и передаче
         данных в стек до 16 бит.  На самом деле,  8-битовые данные вообще
         не могут быть помещены в стек.  Конечно же,  имеются пути  обхода
         этого ограничения, примером этого является макрос @PushOp из гла-
         вы 1. Данные, относящиеся к большим структурам, иногда могут быть
         переданы  в стек словами,  но если вызываемая программа не должна
         получать свои параметры из стека,  то передача адреса данных нам-
         ного удобней.

                                      - 2-17 -
                                Передача по адресу
             При передаче  по  адресу вызываемая программа получает только
         адрес данных.  Доступ  к данным осуществляется путем использования
         этого адреса.  Имеется несколько непосредственных преимуществ дан-
         ного способа. Первое состоит в том, что если данные не расположены
         в различных сегментах,  то все адреса могут  содержаться  в  одном
         16-битовом  значении,  являющимся соглашением по использованию ре-
         гистра или стека. Второе преимущество заключается в том, что прог-
         рамма  становится  более общей,  поскольку указание другого адреса
         создает новый набор данных. Третье преимущество состоит в том, что
         вызываемая  программа может непосредственно манипулировать данными
         для возврата значения в то же самое  место  вызывающей  программы,
         которое содержало первоначальное значение. Если данные, подлежащие
         передаче,  не размещены в памяти (т.е.  являются непосредственными
         данными),  иногда могут возникнуть проблемы.  В этих случаях (или,
         если обнаружено их простое несоответствие для передачи всех требу-
         емых  адресов  в стек) может быть использован тип смешанного пара-
         метра: блок аргументов.
             Блок аргументов или параметров является специальной формой пе-
         редачи по адресу.  В этом случае требуемые аргументы содержатся  в
         непрерывном  участке памяти.  Однако,  в отличие от передачи через
         общую область памяти,  вызывающая процедура не имеет полных сведе-
         ний  об  этом блоке.  При вызове процедуры в качестве параметра ей
         передается адрес этого блока.  Хотя может оказаться неудобным раз-
         мещать  все  требуемые аргументы в блоке,  но это дает возможность
         избежать необходимости размещения всех этих значений в стеке. Если
         блок уже существует для других целей, то передача параметров через
         блок аргументов имеет еще больший смысл.

                      Защита целостности передаваемых данных

             Имеется другой аспект опции "передача по ...", который являет-
         ся очень важным и облегчающим использование. Этот аспект относится
         к целостности данных или к их защите от непреднамеренного  измене-
         ния или порчи.  При типовом использовании данные,  передаваемые по
         значению,являются копией действительных данных. Как таковая, вызы-
         ваемая  программа  может манипулировать данными по любому пути без
         изменения данных в вызывающей программе.  С другой  стороны,  если
         вызываемая программа принимает адрес данных, по которому программа
         затем может изменять данные,  возможно изменение  функционирования
         вызывающей программы. Данные, передаваемые по значению, рассматри-
         ваются затем как защищенные,  а данные,  передаваемые  по  адресу,
         рассматриваются как подвергаемые риску.
             Удивительно, что переменные,  передаваемые в регистр,   иногда
         рассматриваются как передаваемые по адресу,  поскольку  регистры в
         аппаратных средствах являются простыми специализированными   адре-
         сами.  Это различие делается потому,  что данные в регистрах  под-
         вержены риску, если подпрограмма или функция изменят данные в  ре-
         гистре и это повлияет на основную программу.
             Относительно степени подверженности данных постороннему   воз-
         действию  отсутствуют  жесткие и прочные правила.   Такие понятия,
         как "передача по значению" и "передача по  адресу"   могут  помочь
         оценить ситуацию, но действительное решение по использованию  типа
         передачи зависит от того,  насколько ценны данные для   вызывающей
         программы (степень риска), и имела ли доступ вызываемая  программа
         к  первоначальным данным.  Это  в действительности определяет  на-
         сколько большая защита требуется для этих данных.

                                      - 2-18 -

                       Функции в сравнении с подпрограммами

             Это сравнение  часто требуется для вызываемой программы,  при
         возврате новых данных в вызывающую программу.  Как было  отмечено
         ранее,  те  программы,  которые  возвращают значения,  называются
         функциями, а программы, не возвращающие значения, - подпрограмма-
         ми.  В  языках  высокого  уровня функции ограничиваются возвратом
         только одного значения. Любая другая информация,  возвращаемая  в
         вызывающую процедуру, передается обратно путем модификации одного
         или нескольких параметров.  В языке Ассемблер применяются  другие
         ограничения. Рассмотрим эти опции.

                           Возврат значений в регистрах

             Простейшим способом возврата значения является способ возвра-
         та значения в регистре.  Как и при передаче параметров, эта опция
         может  быть ограничена количеством доступных регистров и размером
         возвращаемых данных. Положительным моментом этого способа возвра-
         та является то, что данные легко доступны и могут быть легко про-
         верены или использованы.
             Возврат значения  в регистре имеет смысл для часто вызываемых
         функций.  Это не требует специальной установки и  предварительной
         подготовки  буферов  и пр.  Большинство функций MS-DOS возвращает
         свои значения этим способом. Однако, если все функции в программе
         возвращают свои данные через регистры,  то придется столкнуться с
         задачей "большой бухгалтерии и  перемешивания".  Кроме  этого,  в
         связи с тем, что регистры являются элементами, в которых произво-
         дится большинство вычислений,  налицо жесткая конкуренция  по  их
         использованию.
             Чаще всего регистры должны использоваться для небольших, час-
         то используемых вызываемых программ,  возвращающих немного значе-
         ний и для программ,  возвращаемые значения которых должны  немед-
         ленно  подвергаться  вычислениям.  Одним из примеров этого случая
         могла бы быть функция чтения символьных значений и преобразования
         их в числовые значения.
             Большинство языков высокого уровня для  возвращаемых значений
         используют различные технические приемы. Так, для возврата байто-
         вого значения или значения,  длиной в слово,  обычно используется
         регистр AX. Если необходимо возвратить значение, длиной в двойное
         слово,  такое как указатель far (далекий),  то  младшее  значащее
         слово (или часть смещения) возвращается в регистре AX,  а старшее
         значащее слово (или часть сегмента) возвращается в регистре DX. В
         тех  случаях,  когда в вызывающую программу необходимо возвратить
         более двух слов, данные помещаются в буфер памяти, а указатель на
         этот буфер возвращается в вызывающую программу.  Способы управле-
         ния этим указателем зависят от конкретного языка.

                         Возврат значений в общей области

             Возврат значений  в общей области необходим для функций.  Для
         этого используется метод "стороннего эффекта", представляющий ра-
         зумное, простое средство для возврата большого количества данных.
         Методом стороннего эффекта он назван потому, что операция переда-
         чи  не  сразу  очевидна  из  прочтения раздела "вызов" вызывающей
         программы и происходит как случайный результат процедуры. Так как
         это не очевидно из вызова,  то для ясности в документацию необхо-

                                      - 2-19 -
         димо добавить описание того,  какие значения возвращаются и поче-
         му.
             В связи с этим,  если вместо параметра в регистре  или  стеке
         передается адрес общей области, то ожидаемые в этой отдельной об-
         ласти памяти возвращаемые значения  становятся  более  очевидными
         для читателя.  Кроме того, получается преимущество общности, т.к.
         процедура может непосредственно возвращать свои значения  в любую
         ячейку буфера.

                             Возврат значений в стеке

             Последним способом  возврата значений является помещение их в
         стек.  Эта операция требует использования регистра BP для адреса-
         ции  стека  (таким же способом,  как передача параметров в стек).
         При возврате значений, значения загружаются в стек в одну из яче-
         ек памяти выше адреса возврата. Если процедура вызывается с пара-
         метрами,  то для сохранения возвращаемого значения может быть ис-
         пользована  одна  из ячеек параметров.  Если процедура вызывается
         без параметров,  то вызывающая процедура должна поместить в  стек
         фиктивный аргумент,  чтобы отвести место для возвращаемого значе-
         ния.
             При возврате  значений  в стек вызываемая программа не должна
         очищать стек с помощью инструкции RET N.  Вместо этого вызывающая
         процедура должна использовать для очистки стека получение возвра-
         щаемых значений через простые инструкции POP (восстановление сло-
         ва из стека).
             Если возвращаемые  значения слишком большие для удобного рас-
         положения в стеке,  то вызываемая программа может возвратить ука-
         затель на ячейку памяти,  где находится действительное возвращае-
         мое значение.  В этом случае вызывающая программа должна  принять
         решение о месте области буфера.

                        Отчеты об исключительных ситуациях

             Здесь рассматривается индикация о состоянии возврата  или  об
         обнаруженных и выдаваемых ошибках.  Во многих прикладных програм-
         мах одной из требуемых опций является необходимость иметь вызыва-
         емые процедуры,  функции и подпрограммы, обеспечивающие некоторые
         типы индикации об ошибках или кодах состояния. Вероятно, читатель
         уже заметил,  что большинство функций MS-DOS вызывает возврат ко-
         дов состояния о завершении.  Часто для индикации  наличия  ошибки
         используется  бит переноса в одном или более регистрах,  обычно в
         регистре AX, содержащем подробную информацию о типе ошибки.
             Бит переноса используется для номера причины.  Его легко про-
         верить (с помощью инструкций JC - переход,  если был перенос, или
         с помощью инструкции JNC - переход, если не было переноса); легко
         установить,  дополнить или очистить (с помощью инструкций  STC  -
         установка флажка переноса, CMC - дополнение флажка переноса и CLC
         - сброс флажка переноса);  а также можно сохранить и восстановить
         (с  помощью инструкций PUSHF - запоминание флажков в стеке и POPF
         - извлечение флажков из стека).  Доступ к флажку  переноса  более
         совершенен  чем доступ к любому другому биту состояния в архитек-
         туре микропроцессоров 8086/8088.  Это сочетание обеспечивает иде-
         альный  механизм  для  индикации наличия исключительной ситуации.
         Конечно,  программист должен помнить об очистке бита переноса для
         индикации  нормального завершения в случае,  если ошибка не прои-
         зошла,  потому что бит переноса может быть уже установлен при вы-

                                      - 2-20 -
         полнении обычной операции.
             После того,  как вызывающая программа обнаружит, что возникла
         ошибка, программа должна установить природу ошибки. Иногда после-
         дующая информация не требуется. Если требуется дополнительная ин-
         формация, то для полного кода полезен выделенный регистр. Логично
         выбрать регистр AX, но в связи с тем, что от этого регистра зави-
         сит так много других операций (например,  MUL - умножение аккуму-
         лятора на операнд и DIV - деление аккумулятора  на  операнд),  он
         может  оказаться недоступным.  Какой бы регистр не выбирался,  он
         должен содержать не только код ошибки, но также и код нормального
         завершения. В случае, если первичная информация об ошибке потеря-
         на, программа может повторно проверить регистр для получения сос-
         тояния завершения. Если информация критическая, выберите значение
         для нормального завершения, которое является ненормальным резуль-
         татом.  Это означает,  нельзя использовать значение нуля для нор-
         мального завершения,  потому что другая ошибка может легко почис-
         тить  код состояния.  MS-DOS обеспечивает обслуживание отчетов об
         ошибках для  использования  с  программами,  выполняющими  другие
         программы.  Если  подпроцесс  хочет вернуть код ошибки в процесс,
         который вызывал этот подпроцесс,  он  может  поступить  так,  как
         часть  функционального  вызова процесса завершения - функция 4Сh.
         Затем порождающий процесс может получить этот код  возврата через
         функцию MS-DOS 4Dh. Затем можно получить код возврата порожденно-
         го процесса. Этот механизм используется только с программами, вы-
         полняемыми  под  управлением функции 4Bh - функции загрузки и вы-
         полнения программы.

                                 Типы кодирования

             Для большинства основных программ на любом языке программиро-
         вания  программист  редко  интересуется  подробностями выполнения
         программы процессором.  Подробности обработки в/в, управления па-
         мятью,  размещения программы в памяти при ее выполнении, как пра-
         вило,  предоставляются для управления операционной системе. Одна-
         ко,  имеется ряд моментов, когда требуется более непосредственное
         управление программной средой.  В эти моменты программисту  может
         потребоваться  знание  и готовность принять ответственное решение
         по вопросам механизма загрузки,  размещения и выполнения програм-
         мы.  Примерами, когда это требуется, могут служить: написание ав-
         тономных программ,  функционирующих без присутствия MS-DOS;  под-
         держка  оверлейных  (перекрываемых)  программ  для  использования
         больших программ в ограниченной физической  памяти;  и  написание
         драйверов прерывания или рекурсивных программ.
             Во время выполнения программы размещение ее в памяти  отража-
         ется двумя путями. Во первых, для связи счетчика программы (также
         называемого как указатель инструкции) или адреса ссылки  памяти с
         блоком  физической  памяти используются регистры сегмента.  Затем
         внутри этого блока формируется действительная  ссылка,  используя
         смещение от начала этого блока. Это смещение появляется в счетчи-
         ке программы,  в ссылках на память и внутри косвенных  ссылок  на
         память через регистры.
             Что это означает для выполнения программ с  различными типами
         кодирования?  Эти  типы ссылок и случаи,  когда они используются,
         определяют как программа загружается в память,  какие особенности
         она  может использовать и как программа может быть структурирова-
         на. Рассмотрим как создаются эти ссылки и как их использовать для
         создания более совершенных программ.

                                      - 2-21 -

                       Размещение программного кода в памяти

             Понимание альтернатив размещения программного кода  в  памяти
         требует  ясного понимания работы как инструкций управления выпол-
         нением программы (CALL - вызов процедуры, RET - возврат из проце-
         дуры  и JMP - безусловный переход), так и доступа к памяти микро-
         процессора 8086, так как это прежде всего ограничивает возможнос-
         ти программиста при размещении программы в доступном пространстве
         памяти.
             Инструкции управления  выполнением  программы  часто называют
         инструкциями передачи управления,  которые включают две  основные
         инструкции  CALL  -  вызов процедуры и JMP - безусловный переход.
         Каждый случай, когда программа начинает выполнение с нового места
         в памяти, называется "пунктом назначения". Каждая из этих функций
         имеет три опции реализации для указания пункта  назначения. Этими
         опциями являются: текущее относительное размещение, адресация от-
         носительно текущего сегмента и абсолютная адресация.

                             Относительное размещение

             Текущее относительное размещение иногда называют  относитель-
         ным  PC (program counter) счетчиком программы,  который вычисляет
         адрес пункта назначения от текущего адреса и смещение. Для форми-
         рования  адреса пункта назначения смещение добавляется к текущему
         размещению. В связи с тем, что полная операция в целом не зависит
         от абсолютного расположения программного кода в памяти, результи-
         рующий адрес имеет независимое размещение.  Если в памяти пересы-
         лается целый блок программы, то созданный скорректированный адрес
         пункта назначения указывает на новое положение  инструкции пункта
         назначения.
             Этот способ вычисления адреса передачи используется  во  всех
         инструкциях условного перехода,  во всех внутрисегментных (корот-
         ких или близких) инструкциях JMP (безусловный переход) и  во всех
         внутрисегментных  (близких)  инструкциях  CALL (вызов процедуры).
         "Непосредственная" означает, что инструкция (JMP или CALL) содер-
         жит смещение как непосредственные данные.  Напротив,  "косвенная"
         (непрямая) инструкция - это инструкция (JMP или CALL) для адреса,
         содержащегося  в  16-битовом регистре (только смещение),  или для
         адреса,  содержащегося в 16-битовой или 32-битовой ячейке  памяти
         (смещение или смещение и сегмент).  В связи с тем, что прямые пе-
         редачи управления не включают действительные адреса, то они могут
         быть  размещены  в  памяти где угодно и даже могут быть пересланы
         внутри сегмента, пока исходные инструкции (JMP и CALL) и програм-
         ма пункта назначения пересылаются совместно.

                     Адресация относительно текущего сегмента

             Адресация относительно  текущего сегмента указывает на значе-
         ние действительного смещения для загрузки в  указатель инструкции
         (как  при  косвенной инструкции CALL - вызвать процедуру) или для
         использования в качестве указателя  данных.  Ссылки,  выполняемые
         этим способом, всегда указывают на ту же самую ячейку внутри бло-
         ка памяти,  адресуемого с помощью соответствующего регистра  сег-
         мента. Как таковые, программные коды или данные не могут быть пе-
         ресланы внутри сегмента.  Однако,  такие программные  коды  могут
         быть  пересланы  в памяти,  если регистр сегмента для этого блока

                                      - 2-22 -
         также изменен. Т.к. сегменты должны быть выровнены на границу па-
         раграфа (шестнадцатиричный адрес XXXX0), то программный код может
         быть переслан только путем прибавления 16 байтов (один параграф).
             Этот тип адресации используется внутри сегментными (близкими)
         косвенными инструкциями JMP - безусловный переход и CALL  - вызов
         процедуры,  где новое значение указателя инструкции пункта назна-
         чения выбирается из регистра или  ячейки  памяти.  Эта  адресация
         также  используется  во  всех ссылках на данные независимо от ис-
         пользуемого сегмента (DS,  ES или SS). Код, использующий этот тип
         ссылок,  рассматривается  еще как переместимый,  пока обновляются
         регистры сегмента для отражения позиции программного кода.

                               Абсолютная адресация

             Абсолютная адресация выполняется в тех  случаях,  когда  явно
         указывается  адрес  физической памяти.  Для выполнения абсолютной
         адресации в семействе микропроцессоров 8086 необходимо явно  ука-
         зать  адрес сегмента и смещение.  Эта ссылка каждый раз указывает
         на одну и ту же ячейку памяти.  Абсолютная адресация в  микропро-
         цессоре 8086 используется редко. Только несколько инструкций мик-
         ропроцессора 8086 имеют способность генерировать абсолютные адре-
         са.  Этими  инструкциями  являются  : внутрисегментные  (далекие)
         инструкции JMP - безусловный переход и CALL - вызов  процедуры, а
         также инструкции LDS и LES (загрузка указателя,  используя DS или
         ES).  Инструкции JMP и CALL (непосредственные или косвенные)  об-
         новляют  не только смещение (указатель инструкции),  но и регистр
         сегмента кода (CS).  Он указывает физический адрес памяти. В свою
         очередь,  инструкции LDS и LES не только загружают смещение в 16-
         битовый регистр,  но и загружают  либо  регистр  сегмента  данных
         (DS), либо регистр дополнительного сегмента (ES). Опять это физи-
         ческий адрес памяти.
             Другим способом  создания абсолютного адреса является исполь-
         зование инструкции MOV - пересылка и POP - извлечь из  стека  для
         непосредственной загрузки константы в один из регистров сегмента.
         Заметим однако,  что значение,  пересылаемое с помощью инструкции
         POP в регистр CS, в процессорах iAPX186, iAPX188, или iAPX286 не-
         допустимо и не должно выполняться, если только по причине совмес-
         тимости.

                              Типы программного кода

             При обсуждении свойств программы рассматривался тип сложности
         ее адресации.  Если программа содержит лишь одну абсолютную ссыл-
         ку,  то такая программа называется программой, имеющей абсолютную
         адресацию, или неперемещаемая. Ее нельзя перемещать в памяти.
             Внимательные читатели  могут  подумать  о  том,  что допущена
         ошибка.  В конце концов точка входа в  программу  макроассемблера
         (MASM) указывается как far (далекий) - и все. Выполнимые програм-
         мы с расширением .EXE загружают регистры DS и  ES  по  инструкции
         MOV - переслать. Оба этих факта, как кажется, подразумевают непе-
         реместимую программу,  но операционная система  MS-DOS  выполняет
         загрузку  программы  в память по различным адресам как требуется.
         Ключом к этой дилемме является то,  что используемые значения  не
         являются константами в MS-DOS.  Макроассемблер MASM и компоновщик
         LINK обращаются с именами сегментов и именем процедуры far (дале-
         кий)   специальным   способом   обработки,   который   называется
         relocation map (схема настройки). При загрузке программы в память
         MS-DOS читает схему настройки и изменяет значения тех ссылок, ко-
         торые содержат адреса сегментов.  Для программистов  важно  заме-
         тить,  что MS-DOS не расширяет это правило для стандартных значе-
         ний данных,  и загрузка одного из регистров  сегмента  константой
         это  не  то же самое,  что использование имени сегмента или имени
         процедуры far (далекий).

                                 Переместимый код

             Макроассемблер MASM  и  компоновщик  LINK обычно вырабатывают
         перемещаемые программы.  Т.е.  при нормальном  использовании  они
         создают такие программы, которые могут быть перемещены в памяти с
         помощью MS-DOS,  и при этом правильно функционировать.  Изменится
         только содержимое регистров сегментов.  Это свойство используется
         множеством прикладных программ.  Одни программы  могут  загружать
         другие программы,  используя функцию 4Вh (полезную для оверлейных
         - перекрываемых программ). Несколько программ могут быть загруже-
         ны  в  память одновременно (полезно для мультизадачных систем или
         программ резидентной памяти, таких как, например, программы пред-
         варительной подкачки данных для печати).
             Как уже указывалось,  MS-DOS выполняет эту возможность  путем
         изменения только значений регистров и таких мест в программе, ко-
         торые ссылаются на имя сегмента или имя процедуры  far (далекий).
         Можно  также  расширить эти концепции гибкости на области данных,
         используемые программой. Обычно перемещаемые программы содержат и
         перемещаемые  области  данных.  Когда  загрузчик  MS-DOS помещает
         программу в память, он назначает значения для всех ссылок на сег-
         менты раньше ссылок на программные сегменты.  Листинг 2-5,  полу-
         ченный из программного файла .EXE стандартного  типа,  показывает
         ссылку на сегмент данных, используемую для загрузки регистра сег-
         мента данных. Листинг 2-6 показывает эквивалентную программу, по-
         лученную с помощью макроассемблера MASM.

              Листинг 2-5. Исходный код для заголовка программы .EXE
         -----------------------------------------------------------------

         data_seg SEGMENT              ; определение сегмента данных
                  ...                  ; значения и области данных

         data_seg ENDS
         code_seg SEGMENT              ;  определение кодового сегмента
                  ASSUME cs:code_seg
                  ASSUME ds:data_seg
         main     PROC   FAR           ; точка входа в программу
         start:
                  mov    ax,data_seg   ;  передача адреса сегмента данных
                  mov    ds,ax         ; ... в AX и оттуда в ...
                  mov    es,ax         ; ... регистры сегмента
                  ...     ...
         -----------------------------------------------------------------

             При стандартном использовании переменная data_seg не является
         константой.Скорее, эта переменная является перемещаемым значением
         сегмента, которое указано в листинге 2-6 макроассемблера MASM че-
         тырьмя знаками "тире" и буквой R.  При загрузке программы  MS-DOS
         вставляет в программу действительное значение для его использова-

                                      - 2-24 -
         ния в процессе выполнения программы.  Это значение является адре-
         сом ячейки памяти,  в которую была загружена переменная data_seg.
         Таким образом, с помощью MS-DOS, программный код и области данных
         могут быть перемещены в физической памяти.

                 Листинг 2-6. Листинг для заголовка программы .EXE
         -----------------------------------------------------------------

         0000                           code_seg SEGMENT
                                                 ASSUME cs:code_seg ASSUME
                                                 ds:data_seg
         0000                           main     PROC   FAR
         0000                           start:
         0000 B8 ---- R                          mov     ax,data_seg
         0003 8E D8                              mov     ds,ax
         0005 8E C0                              mov     es,ax
         -----------------------------------------------------------------

                             Отдельные области данных

             Если в программе определяется несколько сегментов данных (ис-
         пользуя,  соответственно,  директивы ASSUME), то внутренние прог-
         раммы могут иметь отдельные области данных. Но, при обычном стиле
         программирования каждый раз при вызове программы каждая программа
         ограничена доступом к одной и той же области данных. Область дан-
         ных предназначена для программы и наоборот.
             При обычном использовании назначенные области не являются по-
         мехой,  так как большинство программ выполняется последовательно,
         одна за другой. Но, что произойдет, если попытаться выполнить од-
         ну  и  ту же  процедуру более одного раза и в одно и то же время?
         Будет или нет более поздний вызов  перезаписывать  предшествующие
         вызовы данных,  из-за того,  что программа использует только одну
         область данных? Здесь можно удивиться, почему одна и та же проце-
         дура будет вызываться более одного раза одновременно?
             По крайней мере,  это возможно  в  трех  случаях.  Во-первых,
         мультизадачные  системы  могут  иметь множество выполняемых прог-
         рамм, разделяющих общие библиотеки программ, называемые библиоте-
         ками  исполнимых  модулей (потому что программный код доступен во
         время исполнения,  а не включается в программу во время компонов-
         ки).  Вместо наличия нескольких копий внутренних программ, разме-
         щенных в программном файле,  библиотеки исполнимых модулей  имеют
         только одну копию программы, размещенной в памяти (для более под-
         робного обсуждения библиотек исполнимых модулей смотри  главу 3).
         Если бы даже все они могли выполнять одну и ту же программу в од-
         но и то же время,  библиотеки исполнимых модулей должны  были  бы
         иметь отдельные области данных,  чтобы избежать неумышленное сов-
         местное использование и порчу данных.
             Второй случай, когда одна и та же процедура может быть вызва-
         на программами одновременно,  происходит  в  системах  управления
         прерываниями.  Допустим,  что  выполняется  некоторая программа и
         произошло прерывание из-за некоторого внешнего события.  Програм-
         ма, обслуживающая прерывание, начинает выполнение и ей необходимо
         вызвать программу,  которая была прервана.  Если она не имеет от-
         дельной области данных, то программа обслуживания прерывания раз-
         рушит данные, относящиеся к прерванной программе. По этой причине
         программам обслуживания прерываний необходимо иметь отдельные об-
         ласти данных.

                                      - 2-25 -

                               Рекурсивные программы

             Третий случай использования отдельных областей данных  проис-
         ходит  тогда,  когда программе необходимо вызвать саму себя.  Это
         является общим средством решения проблем и носит название "рекур-
         сия".  Хорошим примером этого механизма является функция вычисле-
         ния факториала некоторого целого числа.  В листинге  2-7  показан
         пример  решения проблемы вычисления факториала.  Алгоритм решения
         не столь элегантен,  и не содержит проверку на  переполнение  при
         умножении,  но  он выглядит удовлетворительно для значений N от 1
         до 7.

              Листинг 2-7.  Решение проблемы рекурсии для вычисления
                                    факториала
         -----------------------------------------------------------------

         factor  PROC NEAR              ; нахождение факториала числа N
                 cmp  ax,2              ; уже достигнут конец?
                 jne  subfact           ; нет, вычисление (N - 1)!
                 mov  ax,2              ; да, выполнение сначала
                 ret
         subfact:
                 push ax                ; сохранение текущего значения N
                 sub  ax,1              ; получение N - 1
                 call factor            ; запрос (N - 1)!
                 pop  bx                ; восстановление значения N
                 mul  bx                ; N x (N [min]-1)! = N!
                 ret
         factor  ENDP
         -----------------------------------------------------------------

           Повторно-входимый код - необходимое условие локальной памяти

             Во всех  вышеперечисленных  случаях  данные  программы должны
         храниться отдельно от ее программного кода так,  чтобы  несколько
         процедур, каждая с ее собственными областями данных, могли бы вы-
         полнять программный код в одно и то  же  время.  Если  встретился
         этот критерий,  то говорят, что программа повторно-входимая (пов-
         торно-используемая).  Т.е. программа может быть вызвана (введена)
         при  одном  программном  потоке,  пока все еще выполняется другой
         программный поток. Слова "программный поток" указывают на то, что
         неважно,  вызвана ли программа другой программой, или она вызвала
         сама себя (рекурсия).
             В действительности, данные, подлежащие сохранению, сохраняют-
         ся  в стеке вызывающей программы.  Это возможно только при рекур-
         сии,  поскольку программист знает,  когда управление передается в
         новую программу, и может предвидеть необходимость установки новой
         области данных.  Для многопользовательских прикладных программ  и
         прикладных  программ обработчиков-прерываний это не является дос-
         таточным и программы должны иметь  свои  данные,  защищенные  все
         время.  Управление  в такие программы может быть передано в любое
         время.  В этих случаях устанавливайте локальную  область  данных,
         когда программа вводится в первый раз. Эта локальная область дан-
         ных может быть распределена двумя различными способами:  в  стеке
         или в памяти.

                             Локальная память в стеке

             Для локальной памяти в стеке может быть  зарезервирован  блок
         стека  посредством  уменьшения указателя стека.  Любые прерывания
         или вызовы,  происходящие после этого,  могут теперь обеспечить в
         этом блоке стека сохранение любых локальных данных, относящихся к
         прерванной программе. Это самый удобный способ сохранения данных,
         но требующий, чтобы доступ ко всем локальным переменным выполнял-
         ся через регистр BP (для обсуждения этого  смотри  предшествующий
         раздел,  озаглавленный  "Передача  данных в стек").  Пример этого
         способа, снабженный примечаниями, содержится в листинге 2-8.

               Листинг 2-8. Использование стека для локальной памяти
         -----------------------------------------------------------------

                           ; Вызывающая процедура

                 ...    ...
                push   <argument_3> ; передача 3-го аргумента
                push   <argument_2> ; передача 2-го аргумента
                push   <argument_1  ; передача 1-го аргумента
                call   Example      ; вызов процедуры
                add    sp,6         ; очистка стека
                 ...    ...

                           ; Вызываемая процедура

         Stackframe    STRUC       ; определение шаблона структуры стека
         LocWord dw    ?           ; локальная переменная Word (слово)
         LocChar db    14 dup (?)  ; локальный массив символов
         LocIndx dw    ?           ; другая локальная переменная Word
         XamplBP dw    ?           ; сохраняемое значение BP
                 dw    ?           ; адрес возврата (NEAR call)
         Param1  dw    ?           ; 1-й параметр (передан последним)
         Param2  dw    ?           ; 2-й параметр
         Param3  dw    ?           ; 3-й параметр (передан первым)
         Stackframe    END         ; конец определения шаблона
         ;
         base    EQU   [bp - offset XamplBP] ; выравнивание BP с шаблоном
         ;
         Example PROC  NEAR        ; начало процедуры
                 push  bp          ; сохранение старого указателя базы
                 mov   bp,sp       ; выравнивание Stackframe со стеком
                 sub   sp,offset XamplBP ; резервирование памяти в стеке
                 push  si          ; сохранение используемых регистров
                 push  di
                 ...   ...                        ...
                 mov   si,base.Param1  ; доступ к 1-му параметру
                 mov   al,base.LocWord ; доступ к локальным переменным
                 ...   ...                        ...
                 pop   di          ; восстановление сохраненных регистров
                 pop   si          ;
                 mov   sp,bp       ; удаление локальных переменных
                 pop   bp          ; восстановление первоначального BP
                 ret               ; возврат без очистки
         Example ENDP              ; конец процедуры Example (пример)

                                      - 2-27 -

             Т.к. структура  Stackframe  определяется  в текущем сегменте,
         нет необходимости перекрывать сегмент. Если используется смещение
         из другого сегмента, как например, при попытке использования шаб-
         лона из другого сегмента данных, то в ссылке необходимо использо-
         вать  конструкцию SS:  override (заместить).  Отказ от выполнения
         этого требования приводит к выдаче сообщения об ошибке в макроас-
         семблере  MASM  "Can't  reach with segment reg" (невозможно найти
         регистр сегмента). Если когда-либо появится это сообщение, то оно
         означает, что для определения сегмента, к которому осуществляется
         доступ,  текущий сегмент не принят во внимание и нужно решать эту
         проблему.
             Если  в стеке распределяется локальная память,  то перед воз-
         вратом управления  из программы она должна быть освобождена.  Это
         можно выполнить путем добавления размера локальной памяти к стеку
         (возвращение к прежнему состоянию sub sp,offset bp или восстанов-
         ление регистра BP из сохраненного значения (mov  sp,bp)).  Память
         можно  не  освобождать с помощью инструкции RET N,  т.к.  текущая
         вершина стека не содержит адрес возврата!
             В большинстве компиляторов с языков высокого уровня  предпоч-
         тительным способом хранения локальных данных является использова-
         ние этой "временной" памяти в  стеке.  Переменные,  помещаемые  в
         этот тип памяти,  иногда упоминаются как локальные,  динамические
         или автоматические переменные.  Листинг 2-8 представляет типичную
         последовательность событий,  происходящих на входе типичных прог-
         рамм на языке высокого уровня. Процедура устанавливает новый блок
         данных  (сохранение регистра BP и установка регистра BP в текущем
         SP),  распределяет локальную память (вычитает из SP) и  сохраняет
         регистры, которые она может разрушить.
             Рис. 2-1 представляет структуру стека, как она выглядит внут-
         ри программы Example (пример), и показывает как шаблон Stackframe
         (блок данных стека) выравнивается вместе со стеком.  Заметим, что
         выравнивание   выполняется   благодаря    описанию    базы    как
         "[BP-offset XamplBP]". Т.к. XamplBP выровнен с ячейки сохраненно-
         го BP в стеке,  то выбранное описание  base.XamplBP  эквивалентно
         [BP  -  offset XamplBP + offset XamplBP],  а последнее есть то же
         самое,  что [BP + 0].  Другим важным моментом  является  то,  что
         структура шаблона стека должна начинаться с объявления таких эле-
         ментов, которые будут размещаться в нижней памяти.

                Инструкции ENTER и LEAVE для локальной памяти стека

             В более старших моделях семейства микропроцессоров 8086 фирмы
         "Интел"  обеспечены две новые инструкции для помощи при использо-
         вании локальной памяти в стеке. Все процессоры iAPX186, iAPX188 и
         iAPX286 поддерживают инструкции ENTER (вход) и LEAVE (выход). Ин-
         струкция ENTER используется для установки локальной памяти в сте-
         ке, когда программа вводится впервые, а инструкция LEAVE освобож-
         дает эту локальную память,  когда программа  осуществляет  выход.
         Дополнительные  инструкции  ENTER  и LEAVE имеют возможность под-
         держки указателей блоков,  которые используются определенным бло-
         ком структурированного языка высокого уровня,  например,  такого,
         как язык программирования Паскаль.
             В связи со сложностью этих инструкций,  здесь представлены их
         макросы,  эквивалентные листингу 2-9.  Это позволит пользователям
         микропроцессоров  8086/8088 прочувствовать преимущество этих инс-

                                      - 2-28 -
         трукций при переходе к более современному микропроцессору.  Обра-
         тите внимание на то, что макросы enter и leave отклоняются от не-
         официального стандарта отсутствием в качестве префикса  знака  @,
         потому  что они предназначены для замены инструкций ENTER и LEAVE
         при использовании микропроцессоров 8086/8088.
             При выполнении  инструкция  ENTER  осуществляет  в  стеке три
         действия.  Она всегда помещает значение регистра BP в стек.  Если
         значение level (уровень) больше или равно 1,  то инструкция копи-
         рует предыдущие значения регистра BP в стек.  Если значение local
         (локальный)  больше  или равно 1,  то инструкция открывает прост-
         ранство для локальной памяти в  стеке  путем  вычитания  значения
         local из старого BP в стеке (первая инструкция push).

          Старшие адреса .----------------------------------.-----
                         | Предыдущие структуры Stackframe  |   ^
                         |----------------------------------|   |
                         |             Param3               |   |
                  [BP+8] |----------------------------------|   |
                         |             Param2               |   |
                  [BP+6] |----------------------------------|   |
                         |             Param1               |   |
                  [BP+4] |----------------------------------|   |
                         |                                  |   |
                  [BP+2] |----------------------------------|   |
                         |             XamplBP              |Stackframe
                  [BP+0] |----------------------------------|   |
                         |             LocIndx              |   |
                  [BP-2] |----------------------------------|   |
                         |             LocChar              |   |
                  [BP-4] |----------------------------------|   |
                         |/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\|   |
                                                                |
                          /\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\    |
                  [BP-16]|----------------------------------|   |
                         |             LocWord              |   v
                  [BP-18]|----------------------------------|-----
                         |      Сохраненное значение SI     |
                         |----------------------------------|
                         |      Сохраненное значение DI     |
           Вершина стека |----------------------------------|
                         |          Доступная память        |
          Младшие адреса  ----------------------------------

               Рис.2-1. Локальная память стека и доступ к параметрам

             Инструкция LEAVE выполняет действия, обратные действиям инс-
         трукции ENTER, пока регистр BP оставлен в, или сброшен в первона-
         чальное значение, установленное инструкцией ENTER.
             Наиболее запутанной фазой этой операции является ее  связь  с
         указателем блока данных.  На рис.2-2 показано состояние (и содер-
         жимое) стека для ряда операций, которые состоят из четырех после-
         довательных инструкций ENTER.
             Каждый элемент стека на рис.2-2  отображает  в  символической
         форме 2 байта (по этой причине все значения параметров local (ло-
         кальный) для инструкции ENTER умножаются на 2 байта. Это не явля-
         ется ограничением инструкции ENTER). Стрелки на рис. 2-2 в симво-
         лической  форме  отображают  ссылку  одного  элемента  на  другой

                                      - 2-29 -
         элемент (т.е. элемент содержит адрес другого элемента).

              ENTER 4,1           ENTER 2,2           ENTER 4,3
              xxxxxxxx            xxxxxxxx            xxxxxxxx
          .-->[старое BP]<---BP#1 [старое BP]         [старое BP]
          ----[FP # 1   ]  ^      [FP # 1   ]         [FP # 1   ]
              [local    ]  |      [local    ]         [local    ]
         SP-->[local    ]  |      [local    ]         [local    ]
                           |  .-->[BP # 1   ]<---BP#2 [старое BP]
                            --|---[FP # 1   ]  ^      [FP # 1   ]
                               ---[FP # 2   ]  |      [FP # 2   ]
                            SP--->[local    ]  |      [local    ]
                                               |  .-->[BP # 2   ]<--BP#3
                                               |  |   [FP # 1   ]
                                                --|---[FP # 2   ]
                                                   ---[FP # 3   ]
                                                      [local    ]
                                                 SP-->[local    ]

                    Рис.2-2. Действие инструкции ENTER в стеке

             Первая инструкция ENTER (уровень 1) устанавливает  единствен-
         ный указатель блока данных,  указывающий на свой собственный блок
         данных, и открывает верхнее пространство в стеке для 4 байтов па-
         мяти.  Вторая инструкция ENTER (уровень 2) не только создает свой
         собственный указатель блока (FP#2), но и копирует указатель блока
         данных  из  предыдущего  блока  данных (FP#1).  Вторая инструкция
         ENTER создает только 2 байта локальной памяти. Последняя инструк-
         ция  ENTER  (уровень 3) переносит шаг 1 операции дальше,  копируя
         указатели блока предыдущих двух уровней (FP#1 и FP#2).
             Почему выполнение примера последовательно начинается с уровня
         1 инструкции ENTER,  а не с инструкции ENTER уровня 0?  Уровень 0
         инструкции  ENTER просто помещает содержимое регистра BP в стек и
         вычитает значение local (локальный) из указателя стека,  устанав-
         ливая  регистр  BP для указания на только что помещенное значение
         регистра BP. Указатели блока данных не копируются. Уровень 0 инс-
         трукции  ENTER является идеальным для создания локальной памяти в
         стеке.  При использовании инструкции ENTER  вместе  с  директивой
         STRUC,  инструкция  ENTER может почти автоматически создавать ло-
         кальную память стека, которая легко доступна.

                 Листинг 2-9. Эквиваленты макросов для инструкций
                                   ENTER и LEAVE
         -----------------------------------------------------------------
         ;; МАКРООПРЕДЕЛЕНИЯ ДЛЯ ИНСТРУКЦИЙ ENTER И LEAVE
         ;;
         ;; Описания базовой адресации для использования при доступе
         ;; к элементам в блоке стека, созданном инструкцией ENTER
         ;;
         pbase equ     [BP + 4]           ;; доступ к параметрам
         lbase equ     [BP - ??tsize]     ;; доступ к локальным данным
         fbase equ     [BP - ??fsize]     ;; доступ к указателю блока
         ;; Form  ENTER     local <immediate 16>, level <immediate 8>
         ;;
         ;; ENTER-- Создание блока стека и распределение локальной памяти
         ;; Копирование указателя блока стека из предыдущей программы в
         ;; новый блок стека для этой программы и открытие пространства

                                      - 2-30 -
         ;; в стеке для новой локальной памяти
         ;;
         enter  MACRO   local,level
                ??tsize = local + level * 2
                ??fsize = level * 2
                push    bp
                IF (level NE 0)
                  IF (level GT 1)
                    REPT level - 1
                      sub       bp,2
                      push      [bp]
                    ENDM
                  ENDIF
                    mov bp,sp
                  IF (level GT 1)
                    add bp,(level - 1) * 2
                  ENDIF
                  push bp
                ELSE
                  mov   bp,sp
                ENDIF
                sub     sp,local
                ENDM
         ;; Form LEAVE
         ;;
         ;; LEAVE-- Выполнение процедуры возврата, удаляющей блок стека
         ;; и локальной памяти, установленной по инструкции ENTER
         ;;
         leave  MACRO
                mov    sp,bp
                pop    bp
                ENDM
         -----------------------------------------------------------------

             На листинге 2-10 показан фрагмент программы создания  локаль-
         ной памяти в стеке с использованием инструкции ENTER.  Этот фраг-
         мент программы определяет,  распределяет и  использует  локальную
         память из стека.  Инструкция ENTER способствует обучению резерви-
         рования необходимого количества памяти благодаря  оператору  MASM
         SIZE.  Знак  процента  (%) требуется только с реализацией макроса
         инструкции ENTER.  При использовании  версии  машинной  программы
         (поддерживаемой  макроассемблером  MASM 2.0 и выше путем указания
         переключателя .286С) знак процента(%) должен быть опущен.

             Листинг 2-10. Создание и ссылка к локальной памяти стека
                                по инструкции ENTER
         -----------------------------------------------------------------

         ?data_1      STRUC
         my_var dw    ?
         ?data_1      ENDS
         test   PROC  NEAR
                ENTER %(size ?data_1),0  ; распределение локальной памяти
                mov   lbase.my_var,10    ; запоминание значения в л.п.
         -----------------------------------------------------------------

                                      - 2-31 -

             Символ lbase  в  листинге 2-9 определен как базовый адрес для
         доступа ко всем  локальным  переменным.  Действительной  ссылкой,
         создаваемой в инструкции MOV, является:

                          mov   [BP - ??tsize].my_var,10

             Символ ??tsize устанавливается реализацией макроса инструкции
         ENTER в количества  байтов,  добавляемых  в  стек  по  инструкции
         ENTER,  не  включая значение регистра BP.  Значение этого символа
         вычисляется как local + level * 2. При вычитании значения символа
         ??tsize  из  содержимого регистра BP,  результатом является адрес
         верхней части стека.  Таким образом,  все ссылки структуры  имеют
         положительное  смещение от символа lbase.  Даже если используется
         версия машинного кода инструкции ENTER, можно легко написать мак-
         рос,  который  вычисляет  значение символа ??tsize и создает инс-
         трукцию ENTER таким образом, что этот механизм может быть с успе-
         хом использован на процессорах 186/188/286.
             Другим символом, определенным в листинге 2-9, является символ
         pbase  - базовый адрес для доступа ко всем переменным, переданным
         в стек.  Значение символа pbase равно [BP + 4],  чтобы охватить 2
         байта, помещенные в стек как часть near (близкий) инструкции CALL
         (вызвать процедуру) и 2 байта, требующиеся для регистра BP, поме-
         щенного  в стек по инструкции ENTER.  После того,  как определена
         структура параметров стека,  символ pbase можно  использовать  по
         имени  его  поля  для символического доступа к данным,  например,
         pbase.my_param.
             Получив описание простого использования инструкции ENTER, вер-
         немся к вопросу об указателях блока данных.  Что это такое? Каждый
         указатель  блока данных указывает на начало блоков данных предыду-
         щей программы стека.  Путем загрузки регистра BP содержимым одного
         из  указателей блока данных,  размещенного в текущем блоке,  может
         быть получен доступ к  локальным  переменным  предыдущего  уровня.
         Первоначально  это было спроектировано для реализации языков прог-
         раммирования высокого уровня таких  как,  например,  Паскаль,  где
         программа  имеет  автоматический  доступ  к переменным порождающей
         программы.  Если читатель недостаточно созрел в отношении высокого
         уровня структурного программирования на языке Ассемблер, то он ве-
         роятно  пропустит  использование  возможностей   указателя   блока
         инструкции ENTER.  Во всяком случае,  если читатель решил испробо-
         вать использование инструкции ENTER с указателем блока,  то незна-
         чительные  эксперименты  дадут  ему  возможность прочувствовать ее
         функционирование.

                  Краткое изложение размещения программного кода

             Заметим, что  повторная  входимость  не  является  необходимым
         условием переместимости программ, и что переместимость не является
         необходимым условием  повторной  входимости  программ.  "Настройка
         программ"  (путем  модификации адресов при размещении программы по
         определенному  адресу)  применяется  для  возможности  перемещения
         программы  в памяти.  "Повторная входимость" применяется для прог-
         рамм,  имеющих "безопасную" локальную память.  "Рекурсивные" прог-
         раммы -  это тип повторно-входимых программ с ослабленными ограни-
         чениями,  когда программист знает в какой точке должны быть сохра-
         нены данные при подготовке следующего вызова.
             Кроме того,  при написании повторно-входимых  программ  нельзя
         забывать о том, что параметры программы также должны быть повторно
         -входимыми.  Когда новая процедура или задача получает управление,
         данные  должны  передаваться в такую область вызываемой программы,
         которая либо всегда защищена (как стек),  либо всегда  сохраняемая
         (например,  все  программы  обслуживания прерываний сохраняют свои
         регистры при вызове программы).
             Также следует помнить о том, что имеется два типа переместимо-
         го программного кода.  К первому типу относится настраиваемая сис-
         тема программ операционной системы MS-DOS, когда MS-DOS, используя
         схему настройки,  изменяет значения переменных сегмента для  того,
         чтобы настроить программу.  Ко второму типу относятся самонастраи-
         вающиеся программы,  для которых не требуется схема настройки. Са-
         монастраивающимися  программами могут быть только такие программы,
         которые  имеют только адресацию смещения в инструкциях  CALL  (вы-
         звать процедуру) и JMP (безусловный переход).

                        Интерфейс с языками высокого уровня

             Наиболее общим использованием языка Ассемблер в настоящее вре-
         мя является применение его в качестве приложения к языку  програм-
         мирования высокого уровня.  При разработке программы, как правило,
         обычно используют язык высокого уровня и лишь небольшую часть  мо-
         дулей пишут на языке Ассемблер. Язык Ассемблер используется тогда,
         когда критичны скорость работы программы или ее размер,  или когда
         язык  высокого уровня не обеспечивает доступ к полным возможностям
         или к аппаратным средствам.
             Имеется три  главных области,  относящихся к связи программ на
         языке Ассемблер  с программами на языке высокого уровня. Это - со-
         гласование имен между двумя модулями;  обработка любых специальных
         установок,  которые могут требовать язык программирования и компи-
         лятор  языка; настройка модулей языка Ассемблер для надлежащей по-
         следовательности вызова и инструмента передачи параметров, исполь-
         зуемых компилятором конкретного языка высокого уровня.
             В прошлом,  для языков высокого уровня  было  достаточно  мало
         правил  регулировки  соглашений о присвоении имен и последователь-
         ностей вызова.  Сегодня ситуация во многом изменилась, т.к. многие
         компиляторы  следуют стандартам Американского национального инсти-
         тута стандартов (ANSI).  В связи с широким использованием компиля-
         торов  языков  высокого уровня фирмы "Майкрософт" и в связи с тем,
         что они придерживаются стандартов ANSI,  оказалось возможным  выб-
         рать компиляторы  фирмы  "Майкрософт"  с  языков программирования:
         Бэйсик, Си, Фортран  и  Паскаль для иллюстрации соглашений  вызова
         подпрограмм.

                 Соглашения о связях для языка Си фирмы "Майкрософт"
             Соглашения о  связях,  проиллюстрированные  в  листинге   2-8,
         представляют типичную программу на языке программирования Си. Если
         бы программу Example перетранслировать на язык Си, то ее начальные
         предложения были бы похожи на следующие:
              void Example (Param1, Param2, Param3)
                   int Param1, Param2, Param3 ;
                   {
                   int LocIndx ;
                   char LocChar [14] ;
                   int LocWord ;
                   ...   ...

                                      - 2-33 -

             В языке  Си все подпрограммы являются также функциями;  любая
         подпрограмма может возвращать значение в вызывающую  программу. В
         связи с тем,  что приведенная функция не возвращает значение, она
         объявлена как пустая функция (void).
             Язык программирования Си обеспечивает использование автомати-
         ческих  (automatic)  переменных для запоминания локальных данных.
         Заметим, однако, что отсутствует стандарт, предписывающий порядок
         размещения в стеке локальных переменных.
             В листинге 2-8, рис. 2-1 и в приведенном выше фрагменте прог-
         раммы показано как язык Си помещает свои аргументы в порядке, об-
         ратном их объявлению.  Целью этого способа является то,  что если
         передается переменное количество параметров,  то вызываемая прог-
         рамма может всегда найти самый левый  параметр  на  фиксированной
         позиции  в  стеке.  Параметр  Param1  будет  всегда размещаться в
         [BP + 4],  независимо от того, какое количество параметров было в
         действительности передано. Программы на языке Си, допускающие ис-
         пользование этой особенности,  обычно используют самый левый  или
         первый параметр для передачи общего количества параметров,  пере-
         даваемых в вызываемую  программу.  При  этом  способе  вызываемая
         программа  может определить сколько параметров ей необходимо про-
         читать.
             Другой особенностью,  отмечаемой в языке Си, является то, что
         параметры почти всегда передаются по значению.  Если вызвать про-
         цедуру Example (пример) с переменной FOO,  то содержимое перемен-
         ной FOO будет помещено в стек.  Вызываемая программа, таким обра-
         зом,  работает с копией переданной переменной,  а не с ней самой.
         Исключение этого способа состоит в том, что массив обычно переда-
         ется  по адресу.  (В стандартном языке Си идентификатором массива
         является его адрес,  так что это кажущееся исключение в  действи-
         тельности  согласуется с синтаксисом языка Си).  Однако,  язык Си
         позволяет программисту передавать адрес  любой  переменной,  если
         потребуется.
            Компилятор языка Си фирмы "Майкрософт" поддерживает богатейшую
         среду программирования,  позволяющую опытному программисту полное
         управление памятью,  используемой модулем.  В приведенном примере
         представлена среда программирования языка Си по умолчанию, состо-
         ящая из вызова программы near (близкий) и ссылки  near на данные.
             Вопреки уже сделанным попыткам, не будем заменять версию про-
         цедуры Example (пример) программой на языке Си.  Одно из препятс-
         твий, которое необходимо преодолеть, состоит в согласовании имен,
         используемых между вызывающей программой на языке Си и вызываемой
         программой на языке Ассемблер.  Проблема заключается в  том,  что
         компилятор языка Си ставит префикс "подчеркивание" (_) перед все-
         ми именами.  Когда компилятор генерирует вызов программы  Example
         (пример),  он  реально предполагает,  что именем программы пункта
         назначения является "_Example".  Возможно,  что эта  терминология
         разработана для предотвращения коллизий между пространством имени
         компилятора и пространством имени Ассемблера. Если и вызывающая и
         вызываемая программы написаны на языке Си, то компилятор трансли-
         рует обе ссылки и поэтому нет никаких неприятностей.  Когда  одна
         из ссылок на языке Ассемблер,  то необходимо выполнить трансляцию
         самим. Эта трансляция применяется для выдачи имен переменных гло-
         бальных данных также успешно, как и программных меток.

                                      - 2-34 -
             Следует отметить два существенных момента:
             - в языке программирования Си имена ограничены 8 символами;
             - все имена в языке программирования Си чувствительны  к  ре-
         гистру.
             В языке программирования Си "Example"  и  "example"  это  два
         разных имени. Программы на языке программирования Ассемблер долж-
         ны ассемблироваться с переключателем /mx для предохранения от ис-
         пользуемого регистра любого имени.

                                                       Таблица 2-1
                 Соглашения о связях для языка Си фирмы "Майкрософт"
         _______________________________________________________________
                                   |
              Соглашение           |                 Описание
         __________________________|____________________________________
                                   |
          Ссылка на программу      | Near (близкие) или far (далекие)
          Ссылки на данные         | Near (близкие) или far (далекие)
          Стек очищается           | вызывающей программой
          Параметры передаются в   | обратном порядке
          Параметры передаются по  | значению
          Значения возвращаются в  | регистре AX или DX:AX
          Длина имени              | 8 символов
          Всем именам предшествует | символ "подчеркивание" (_)
         __________________________|____________________________________

             Последним требованием к языку  программирования  Си  является
         возможность  вызова  программ на языке Ассемблер.  Это требование
         обеспечивается тем,  что на языке Ассемблер функция  должна  быть
         объявлена  как public (общая),  а на языке Си - как extern (внеш-
         няя).  Краткие сведения о соглашениях о связях для языка програм-
         мирования Си фирмы "Майкрософт" приведены в таблице 2-1.

              Соглашения о связях для языка Паскаль фирмы "Майкрософт"

             Если листинг 2-8 приближается к синтаксису вызова  для  языка
         Си фирмы "Майкрософт", то соглашения о связях для компилятора язы-
         ка Паскаль фирмы "Майкрософт" лучше выражаются с  помощью примера,
         приведенного в листинге 2-4.  Вызов на Паскале эквивалентен вызову
         процедуры myproc (моя процедура),  закодированному следующим обра-
         зом:

               procedure MyProc (Param1, Param2, Param3 : integer) ;
               begin
                ...

             Главное отличие между языками Си и Паскаль состоит в том, что
         Паскаль  выполняет гораздо больше строгих проверок.  Эти проверки
         гарантируют:  что при вызове передается правильное  количество  и
         типы параметров;  что значения функции используются способом, со-
         ответствующим их типу и т.д.  Таким образом,  в отличие от  языка
         Си, в языке Паскаль программа должна быть объявлена либо процеду-
         рой (подпрограмма,  которая не возвращает значения), либо функци-
         ей.
             Язык Паскаль также обеспечивает  использование автоматических
         переменных для памяти локальных данных. Как и в языке Си, в языке

                                      - 2-35 -
         Паскаль нет стандартных решений о  назначении  порядка  локальных
         переменных в стеке. Также, как и в языке Си, в Паскале память для
         локальных переменных распределяется при вершине стека на  входе в
         вызываемую программу.  Если процедура MyProc использует локальные
         переменные LocIndx,  LocChar и LocWord,  то они должны будут ссы-
         латься   так,  как  показано в структуре StackFrame листинга 2-8.
         Паскаль эквивалентен программе, похожей на ниже приведенную:

               procedure MyProc (Param1, Param2, Param3 : integer) ;
               var
                   LocIndx, LocWord : integer ;
                   LocChar [1..14] : character ;
               begin
                ...

             Из листинга 2-4 можно увидеть,  что в отличие  от  языка  Си,
         язык  Паскаль  помещает  свои аргументы в порядке,  в котором они
         объявляются,  слева направо. Причина, по которой этот способ воз-
         можен,  состоит в том,  что компилятор языка Паскаль гарантирует,
         что все обращения, выдаваемые программой, обеспечивают правильное
         количество  и  типы аргументов.  Язык Паскаль просто не позволяет
         передавать переменное количество параметров,  поэтому порядок пе-
         редачи, используемый в языке Си, не требуется в языке Паскаль.
             Следствием строгой проверки вызова,  выполняемой языком  Пас-
         каль,  является то, что вызываемая программа всегда получает одно
         и то же количество аргументов,  позволяющее  вызываемой программе
         использовать  инструкцию RET N для очистки стека, нежели зависеть
         от вызывающей программы.
             Другим сходством  с  языком Си является то,  что язык Паскаль
         обычно передает свои переменные по значению,  однако, если требу-
         ется,  то переменные можно передавать по адресу, используя описа-
         ние var (переменная).

                                                         Таблица 2-2
                Соглашения о связях языка Паскаль фирмы "Майкрософт"
         ________________________________________________________________
                                       |
                  Соглашение           |           Описание
         ______________________________|_________________________________
                                       |
          Программные ссылки           | Far (далекие)
          Ссылки на данные             | Far (далекие)
          Стек очищается               | вызываемой программой ( RET N)
          Параметры передаются в       | порядке объявления
          Параметры передаются по      | значению
          Значения возвращаются в      | регистре AX или DX:AX
          Длина имени                  | 8 символов
          Все имена являются           | нечувствительными к регистру
         ______________________________|_________________________________

            В отличие от языка Си, компилятор языка Паскаль фирмы "Майкро-
         софт"  использует  модуль памяти LARGE (большой),  предполагающий
         вызовы far (далекий) и ссылки по памяти far (далекие).  Также,  в
         отличие  от Языка Си,  язык Паскаль распознает имена на любом ре-
         гистре,  несмотря на то,  что функция языка Ассемблер должна быть
         еще  объявлена как public (общая),  а ссылки языка Паскаль должны
         быть объявлены как extern (внешние).  Краткие сведения о соглаше-

                                      - 2-36 -
         ниях о связях в языке Паскаль фирмы "Майкрософт" приведены в  таб-
         лице 2-2.

           Соглашения о связях языков Фортран и Бэйсик фирмы "Майкрософт"

             Стандартные компиляторы  языков  программирования  Бэйсик   и
         Фортран  фирмы "Майкрософт" очень похожи на компилятор языка Пас-
         каль фирмы "Майкрософт". Таблица 2-3 справедливо показывает,  как
         много имеется сходства при стандартных вызовах. Однако, имеются и
         отличия.  Главным отличием от соглашений о связях  языка  Паскаль
         состоит в том,  что и Бэйсик и Фортран передают свои аргументы по
         ссылкам. Так как эти языки программирования передают адреса пере-
         менных,  любые  манипуляции  переменными,  выполняемые вызываемой
         программой, также изменяют значения переменных в вызывающей прог-
         рамме.
             В действительности, сходство между всеми четырьмя интерфейса-
         ми означает, что как легко написать подпрограмму на языке Ассемб-
         лер для Бэйсика и Фортрана,  также легко и для Паскаля или  языка
         Си.  Однако,  в  программах языков Бэйсик и Фортран для установки
         правильного интерфейса требуются большие усилия.
             Эквивалентом предложения  extern (внешний) в языке Бэйсик яв-
         ляется предложение DECLARE (объявить),  в то время  как  в  языке
         Фортран  требуется  предложение INTERFACE (интерфейс).  Каждое из
         этих предложений информирует соответствующий  компилятор  о  том,
         что  имя соответствующей программы должно находиться вне текущего
         модуля.  Для информирования компилятора о том, как сформатировать
         и  сгенерировать  правильный вызов,  могут потребоваться дополни-
         тельные параметры.  Соглашения о связях для языков Бэйсик и Форт-
         ран фирмы "Майкрософт" приведены в таблице 2-3.

                                                         Таблица 2-3
                  Соглашения о связях для языков Бэйсик и Фортран
                                 фирмы "Майкрософт"
          ___________________________________________________________
         |                      |                 |                  |
         |     Соглашение       |    Бэйсик       |   Фортран        |
         |______________________|_________________|__________________|
         |                      |                 |                  |
         |Ссылки на программы   |Far (далекий)    |Far (далекий)     |
         |Ссылки на данные      |Far (далекий)    |Far (далекий)     |
         |Стек очищается        |     вызываемой программой (RET N)  |
         |Параметры передаются в|         в порядке объявления       |
         |Параметры передаются  |по адресу Far(да-|по адресам Near   |
         |                      |лекий)           |(близкий)/Far (да-|
         |                      |                 |лекий)            |
         |Значения возвращаются |       регистре AX или DX:AX        |
         |Длина имени           |40 символов      |6 символов        |
         |Все имена представлены|на верхнем ре-   |нечувствительны   |
         |                      |гистре           |к регистру        |
         |______________________|_________________|__________________|

                         Модель сегмента фирмы "Майкрософт"

             Версия 5.0  макроассемблера  MASM фирмы "Майкрософт" дает воз-
         можность программисту быстро указывать надлежащие имена сегментов
         и  устанавливать  их для данного языка программирования с помощью
         директивы MODEL. Даже без версии 5.0 установка надлежащего шабло-

                                      - 2-37 -
         на языка Ассемблер относительно проста. Все четыре языка програм-
         мирования используют одинаковые первичные  имена  сегментов.  Имя
         кодового сегмента "_TEXT", а имя сегмента данных "_DATA". Для от-
         дельных интерфейсов могут потребоваться  дополнительные сегменты,
         и  многие  модели  компиляторов фирмы "Майкрософт" требуют, чтобы
         стек помещался бы в сегмент данных.  Однако, простой программе на
         языке Ассемблер не нужно беспокоиться об этом,  так как она может
         просто отключить функционирование стека в вызывающей  программе и
         все необходимые установки будет поддерживать главная подпрограмма
         языка высокого уровня.

                  2  0Назначение и использование локального ЗУ в памяти

             Имеется три способа распределения памяти для переменных.  Бу-
         дем рассматривать локальное ЗУ (локальную память)  в  оперативной
         памяти и локальную память в стеке. Сейчас рассмотрим локальное ЗУ
         в распределяемой памяти.  Распределяемая память должна получаться
         из неиспользуемой памяти системы (часто называемой пулом памяти).
         MS-DOS поддерживает функции,  которые могут быть использованы для
         распределения,  перераспределения и установки размера блоков сис-
         темной памяти.  После распределения памяти программист может реа-
         лизовать  свою  личную (персональную) схему управления памяти для
         управления памятью в узком участке.  Однако сейчас сконцентрируем
         свое  внимание  на  возможностях MS-DOS,  начиная с функции 48h -
         "распределить память".
             После получения блока памяти,  программа должна иметь возмож-
         ность его адресации.  Память, распределяемая MS-DOS, предоставля-
         ется  в  "кусках" по 16 байтов,  называемых "paragraph" (парагра-
         фом). MS-DOS возвращает указатель на эту память, которая содержит
         16-битовый адрес памяти блока. Сегменты адресуются как параграфы,
         при этом указатель должен быть загружен в один из регистров  сег-
         мента (но не в регистр CS!). Обычно для повторного доступа к бло-
         ку памяти используется либо сегмент данных, либо внешний сегмент.
         Если подпрограмма, которая распределяла память, не является глав-
         ной подпрограммой программы, то старое значение регистра сегмента
         должно  сохраняться и восстанавливаться перед выходом из подпрог-
         раммы.  Кроме того,  перед выходом из подпрограммы распределенная
         память  должна быть возвращена в систему.  Для возврата распреде-
         ленной памяти блока в систему используется функция 49h  MS-DOS  -
         "освободить распределенную память". В листинге 2-11 показано, как
         подпрограмма исполняемой программы .EXE будет  распределять,  ис-
         пользовать  и освобождать память,  используемую как локальную па-
         мять (локальное ЗУ).


          Листинг 2-11. Распределение локальной памяти посредством MS-DOS
         -----------------------------------------------------------------

                     common   SEGMENT ; общие данные, используемые всеми
                     com_1   dw     ?
                     com_2   db     14 DUP (?)
                     common    ENDS
                     dummy_dat  STRUC      ; описание структуры,
                     dummy_1 dw     ?      ; используемой с
                     dummy_2 db     14 DUP (?) ; распределенной памятью
                     dummy_dat ENDS
                             ASSUME ds:common ; доступ к данным COMMON

                                      - 2-38 -
                     local_example  PROC    NEAR ; процедура example
                     push    ds        ; сохранение предыдущего DS
         B8 ---- R   mov     ax,common ; COMMON настраиваемая MS-DOS
                     mov     ds,ax
                     push    es        ; сохранение предыдущего ES
                     mov     ah,048h   ; распределение памяти
                     mov     bx,1      ; запрос 1 блока (16 байт)
                     int     21h       ; вызов MS-DOS
                     jc      not_alloc ; перенос означает сбой распред-я
                     mov     es,ax     ; если распределена, то ее адрес
         ;
         ;                 три примера адресации
         ;
         A1 0000 R   mov     ax,com_1 ; надлежащий сег.-предполагается DS
         B8 0000     mov     ax,dummy_1 ; ошибочный сег.-непосредственный
         26: A1 0000 mov     ax,es:dummy_1 ; надлежащий сег.-замещаемый
                     ;
                     mov     ah,049h   ; освобождение распредел. памяти
                     int     21h       ; вызов MS-DOS
                     jnc     free_ok   ; нет переноса т.е. хорошо
                     not_alloc:
                     ; Сообщение об ошибке, если сбой, распр-я или удал-я
                     free_ok
                     pop     es        ; восстановление ES
                     pop     ds        ; восстановление DS
                     ret
                     local_example   ENDP  ; конец примера
         -----------------------------------------------------------------

             Листинг 2-11 содержит оба вызова функций MS-DOS "Распределить
         память" и "Освободить память". Вместо регистра DS для указания на
         только что распределенную память,  был использован регистр ES,  а
         регистр  DS зарезервирован для доступа к области общих переменных
         программы.  Заметим, что в отличие от примера стека, для выполне-
         ния доступа к используемой структуре,  определенной здесь, требу-
         ется оператор замещения сегмента (:). Без замещения сегмента инс-
         трукция  mov  ax,dummy_1 не выполняет генерацию ссылки на память,
         используя регистр ES,  но взамен этого генерирует загрузку смеще-
         ния (в нашем случае нуль) в регистр AX.  При добавлении замещения
         сегмента к инструкции mov ax,es:dummy_1 макроассемблер MASM гене-
         рирует  передачу  памяти  из смещения dummy_1 во внешний сегмент.
         Замещение сегмента в листинге 2-11  показано  с  байтом  префикса
         26:.
             При использовании в  программе  нескольких  сегментов  данных
         программист несет ответственность за управление используемыми об-
         ластями данных. Например, если программа X распределяет локальную
         память  и  обновляет  регистр  DS для доступа к этой области,  то
         программист должен помнить о том, что эта область данных по умол-
         чанию  принимается областью данных для всех программ,  вызываемых
         программой X.  Общая область данных,  которая была  определена  в
         программе, доступна еще путем загрузки либо регистра DS, либо ре-
         гистра ES из переменной сегмента,  как показано в  листинге  2-6.
         Программы, изменяющие содержимое своих регистров сегмента, должны
         сохранять и  восстанавливать  первоначальные  значения  регистров
         сегмента для предотвращения своих порождаемых задач от ошибок.
             Всякий раз,  когда в программе используется более одного сег-
         мента данных или внешних сегментов, программист должен быть очень

                                      - 2-39 -
         внимателен к директивам ASSUME (присвоить),  используемым в прог-
         рамме.  При ассемблировании обычных ссылок на память макроассемб-
         лер MASM сначала ищет их таблицу внешних символов для имен  пере-
         менных,   к  которым  осуществляется  доступ.  Если  MASM  найдет
         переменную в таблице символов,  он пытается создать  ссылку,  ис-
         пользуя сегмент,  в котором определена эта переменная.  Если этот
         сегмент отсутствует (т.е. отсутствует соответствующее предложение
         ASSUME), то MASM генерирует сообщение об ошибке "Can't reach with
         segment reg" (нельзя найти регистр сегмента).
             Если MASM не может найти переменную в таблице символов, то он
         предполагает,  что она находится в сегменте данных.  Если  и  это
         предположение  окажется  неудачным,  то  MASM  пытается исправить
         ошибку во время второй передачи путем присоединения  к инструкции
         префикса замещения сегмента.  При неудаче,  получение этого байта
         вызывает другое сообщение об ошибке "Phase error  between passes"
         (ошибка фазы между передачами).
             В случае неудачи или ссылки "вперед", т.е. когда имя перемен-
         ной  еще не находится в таблице символов,  программист должен ис-
         пользовать оператор замещения сегмента (:) для более чистого  оп-
         ределения   макроассемблером  MASM  используемого  сегмента.  Для
         управления доступом в программе также полезен оператор  SEG. Этот
         оператор позволяет программисту получать значение сегмента (базо-
         вый адрес сегмента) для любой  определенной  переменной.  Ссылки,
         создаваемые с помощью предложения SEG, настраиваются MS-DOS и по-
         лезны для создания настраиваемых ссылок вместо абсолютных ссылок.

                       Введение в управление памятью в MS-DOS

             Пример, приведенный в листинге  2-11,  зависит  от  имеющейся
         свободной памяти внутри системы. По умолчанию MS-DOS вырабатывает
         распределение всей памяти для себя при загрузке.  Вызов распреде-
         ления памяти будет неудачным,  т.к. процесс уже имеет всю память,
         даже если он не знает про это. Если программа желает использовать
         функцию распределения памяти,  то некоторая память, полученная во
         время загрузки,  должна быть возвращена в систему. Обычно процесс
         будет  стремиться вернуть всю память ,  которую он не занял прог-
         раммным кодом или буферами.
             Функция, обеспечиваемая  MS-DOS  для обработки возврата части
         распределенной памяти программы в систему,  является функцией 4Ah
         -  "Модификация блока распределенной памяти".  Она позволяет про-
         цессу "вырезать" память из его блока распределения памяти, приня-
         того по умолчанию.
             Заметим, что имеются способы предотвращения процесса от расп-
         ределения всей памяти при загрузке, но их рассмотрение отложим до
         главы 3,  где тема загрузки программ и программных файлов  MS-DOS
         будет обсуждаться более подробно.
             Параметры, требующиеся для функции "Модифицировать блок расп-
         ределения памяти",  это адрес сегмента блока,  который необходимо
         модифицировать, и новый размер блока. Адрес сегмента блока, кото-
         рый содержит программу (размер которой нужно модифицировать), пе-
         редается через PSP (Program Segment Prefix - сегмент программного
         префикса).  PSP является разделом памяти, которым начинается каж-
         дая программа MS-DOS. Подробно содержимое PSP описывается в главе
         3.  Сейчас же для нас важно только то, что адрес сегмента PSP яв-
         ляется адресом сегмента блока, который необходимо модифицировать,
         и нам необходим этот адрес.
             Что касается определения этого параметра, то оно различно для

                                      - 2-40 -
         файлов  типа .COM и файлов типа .EXE. Рис. 2-3 показывает располо-
         жение  памяти для файлов типа .COM и типа .EXE.  PSP является пер-
         вым элементом для каждого типа файлов.  В программе типа .COM  PSP
         содержится в первых 256 байтах программного сегмента и адрес прог-
         раммного сегмента (во всех регистрах  сегмента)  является  адресом
         сегмента PSP.
             Для файлов типа .EXE PSP располагается в его собственном  сег-
         менте. Однако, всякий раз при загрузке программы типа .EXE и полу-
         чении управления от MS-DOS,  оба регистра DS и ES  содержат  адрес
         сегмента PSP.  Таким образом,  для программы любого типа адрес PSP
         можно будет получить, по крайней мере, из регистра DS или регистра
         ES.  Кроме  того,  пользователи MS-DOS версии 3.0 (или выше) могут
         использовать программу получения адреса сегмента программного пре-
         фикса (PSP) - функцию 62h.  MS-DOS возвращает значение адреса сег-
         мента программного префикса в регистре BX.
             Т.к. функция "Модифицировать блок распределенной памяти" пред-
         полагает адрес блока в регистре ES,  то функция может быть вызвана
         непосредственно  при  выполнении запуска программы,  поскольку ре-
         гистр ES уже имеет адрес PSP.

                |\/\/\/\/\/|                         |\/\/\/\/\/|
                |  Память  |                         |  Память  |
                |файла типа|                         |файла типа|
                |   .COM   |                         |   .EXE   |
           0000 |----------|           PSP           |----------|
                |    PSP   |<----------------------->|    PSP   |
           0100 |----------|                         |----------|
                |   Коды   |                         | Сегмент A|
                |    или   |                         |----------|
                |  данные  |                         | Сегмент B|
                |----------|     Конец программы     |----------|
                |   Стек   |<----------------------->| Сегмент C|
           FFFE |----------|                         |----------|
            или | Неисполь-|                         | Неисполь-|
          более |  зуемая  |                         |  зуемая  |
         высокая|  память  |   более высокая память  |  память  |
          память ---------- <-----------------------> ----------

                  Рис.2-3. План памяти программы MS-DOS и сегмента
                           программного префикса

             После того, как найден адрес блока памяти, необходимо опреде-
         лить общее количество памяти,  необходимое для сохранения. Разли-
         чия между программами .COM и программами  .EXE  здесь  становятся
         уже  более заметными.  Для программы типа .EXE размер должен быть
         определен путем вычитания адреса начала сегмента  PSP  из  адреса
         сегмента  фиктивного сегмента,  расположенного в конце программы,
         как показано в листинге 2-12.  Почему используются адреса сегмен-
         тов?  Функция  4Ah ожидает размер в параграфах,  а адрес сегмента
         обычно является адресом параграфа.

          Листинг 2-12. Функция 4Ah - "Модифицировать блок распределенной
                  памяти" - изменение размера программы типа .EXE
         -----------------------------------------------------------------

         resize PROC NEAR
                mov     ax,es           ; получение адреса PSP

                                      - 2-41 -
                mov     bx,SEG end_addr ; получение адреса сл. сегмента
                sub     bx,ax           ; разность - размер программы
                mov     ah,04Ah         ; модификация распредел-й памяти
                int     21h             ; вызов MS-DOS
                jnc     short resize_ok ; нет переноса => хорошо
                mov     ax,04C00h       ; перенос => сбой--авар. заверш.
                int     21h
         resize_ok:
                ret
         resize ENDP
         ;
         ; Оставшаяся  часть программного кода в этой программе END_ADDR
         ; является последним  элементом  перед  предложением  END.  Это
         ; сделано для того,  чтобы предложение END_ADDR связалось бы как
         ; последний сегмент, если используется более одного исходного
         ; файла.
         ;
          end_addr  SEGMENT
          end_addr  ENDS
                  END
         -----------------------------------------------------------------

             Для программы типа .COM требуется меньше хлопот. В отличие от
         программы типа .EXE,  которая имеет определенный размер, установ-
         ленный  компоновщиком  LINK,  программы  типа .COM могут изменять
         свой размер.  Размещение стека в программе типа .COM, которое ус-
         танавливается MS-DOS,  может изменяться от конца сегмента (FFFEh)
         на 256 байтов больше  программы  (минимальный  размер,  требуемый
         MSDOS для стека).  Пользователь может выбирать первое,  принимая,
         что MS-DOS обеспечила и изменила размер стека (установленный раз-
         мер 64 кбайт (1000h) параграфов) или все,  что осталось; или вто-
         рое, пересылая стек и изменяя размер, основываясь на этом. Второй
         выбор освобождает больше памяти и,  таким образом, является более
         предпочтительным и рекомендуемым  фирмами "Майкрософт"  и  "ИБМ".
         Листинг 2-13 содержит пример программы .COM, который устанавлива-
         ет свой собственный стек и изменяет размер своего начального бло-
         ка распределения на более умеренный размер.

          Листинг 2-13. Функция 4Ah - "Модифицировать блок распределенной
                  памяти" - изменение размера программы типа .COM
         -----------------------------------------------------------------

         code_seg  SEGMENT
                   ASSUME  cs:code_seg
                   ORG     0000h
         seg_org   EQU     $
                   ORG     0100h
         main      PROC    FAR
         start:
                   mov     sp,offset stack
                   call    resize
         ;
         ; здесь может выполняться оставшаяся часть программы
         ;
         main      ENDP
         resize    PROC    NEAR
                   mov     bx,(offset last_byte - seg_org + 15) shr 4

                                      - 2-42 -
                   mov     ah,04Ah        ; модификация распр-й памяти
                   int     21h            ; вызов MS-DOS
                   jnc     short resize_ok ; нет переноса => хорошо
                   mov     ax,04C00h      ; перенос => сбой--ав.заверш.
                   int     21h
         resize_ok:
                   ret
         resize    ENDP
                   db      32 DUP ('stack   ')
         stack:
         last_byte EQU     $
         code_seg  ENDS
                   END     start
         -----------------------------------------------------------------

             Интересной частью  этой программы является способ определения
         размера результирующей программы.  Для преобразования  количества
         байтов программы в количество параграфов  по-существу посредством
         деления на 16,  используется оператор SHR  макроассемблера  MASM.
         Что не является таким очевидным так это то,  почему seg_org вычи-
         тается из смещения last_byte.  Оператор SHR не выполняется, когда
         применяется  для смещения,  и он вырабатывает сообщение об ошибке
         "Constant was expected" (ожидалась константа).  Однако,  разность
         между двумя смещениями считается постоянной, делая выражение при-
         емлемым для макроассемблера MASM.  Заметим,  что  seg_org  должна
         иметь нулевое смещение так,  чтобы размер был относительно начала
         сегмента. Если бы использовалась метка start, то были бы потеряны
         последние 100 (шестнадцатиричное значение) байт программы. (Заме-
         тим,  что last_byte:  для вычислений работает также хорошо, как и
         last_byte equ $).
             Кроме того,  для успешного  использования освобождения памяти
         прием  вычитания двух смещений (либо метки,  либо количества) для
         получения константы,  может быть успешно использован для всех ти-
         пов  операций,  где  размер требуется в выражениях,  использующих
         константы.  Рассмотрение этого применения для задач  выравнивания
         буферы данных на границу параграфа будет приведено в главе 6.

                   Распределение памяти из языков высокого уровня

             Большинство языков  высокого  уровня  обрабатывает  проблемы,
         связанные с распределением или изменением размера  блоков памяти.
         Пользователю не нужно добавлять программы изменения размера блока
         начального распределения в процессе использования функций  управ-
         ления памятью языка высокого уровня. Например, в библиотеке языка
         программирования Си функции malloc и calloc  работают  независимо
         от начального распределения памяти.




                Защита данных и управление областью действия данных

             Технические приемы, используемые при повторно  входимом коди-
         ровании, подвели нас к другому аспекту модульного  программирова-
         ния:  защите данных в программе от случайного  изменения.  Разру-
         шение  ценных  данных  наиболее  часто  происходит  тогда, когда
         одна часть программы ошибочно изменяет данные, относящиеся к дру-

                                      - 2-43 -
         гой части программы. Путем следования некоторым основным правилам
         возможность такого события может быть значительно уменьшена.  Са-
         мое главное правило состоит в том,  что модульность данных  прог-
         раммы также хороша как и модульность программного кода, управляю-
         щая рядом данных,  к которым подпрограмма может иметь доступ. Это
         понятие очень часто называется "областью действия данных".  Обра-
         тимся к тому,  что мы уже изучили, и посмотрим как это можно при-
         менить для наших новых проблем.

                 Локальная память в сравнении с глобальной памятью

             Память человека в любое конкретное  время  может  иметь  дело
         только с ограниченным количеством понятий.  Для программистов это
         подразумевает то,  что с ростом количества манипулируемых и запо-
         минаемых элементов растет и количество сделанных ошибок.  При ис-
         пользовании локальной памяти для подпрограмм программист уменьша-
         ет  количество  элементов данных,  которое должно быть запомнено.
         Чем иметь дело с областями данных,  содержащими сотни переменных,
         программист может теперь иметь дело с областями данных,  содержа-
         щими небольшое количество данных.  Может  существовать  небольшое
         количество "узких" областей данных,  каждая из которых может быть
         проверена программой,  использующей ее, потому что каждая область
         безопасна только тогда,  когда есть уверенность, что другие прог-
         раммы не связаны с ней.  Любой из представленных способов для пов-
         торно-входимых программ служит для распределения временной локаль-
         ной памяти данных.
             Глобальные области данных, также известные как общие области,
         могут быть разбиты на модули. В этом случае вместо одной монолит-
         ной  области данных создается некоторое количество узких областей
         данных. После этого подпрограммы будут иметь дело только с такими
         участками глобальных данных, которые требуются для обработки. Это
         накладывает на часть программистов требование  быть внимательными
         и осторожными с директивами ASSUME в содержимом регистров сегмен-
         та,  но такая явная обработка общих данных к тому же делается чи-
         ще,  чем  доступ и затем изменение критических данных.  Например,
         общая область данных, содержащая строки текста и символьные конс-
         танты,  не нуждается в части подпрограмм вычисления чисел,  также
         как таблицы значений синуса и косинуса не нужны подпрограмме вво-
         да информации с терминала.
             В стек должно передаваться столько параметров,  сколько необ-
         ходимо  для  уменьшения количества внутренних обращений к данным.
         Всякий раз, когда несколько программ должны иметь доступ к облас-
         тям данных с целью передачи параметров, вероятность ошибки увели-
         чивается.
             Общие данные обычно должны быть определены с помощью директи-
         вы DEFINE DATA (определить данные) так,  чтобы содержимое области
         оставалось  без изменения и не было субъектом случайного удаления
         при ошибочном освобождении программой с помощью функции  "Освобо-
         дить распределенную память".

                          Использование регистров сегмента

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


                        Управление размером доступных данных

             Программист в дальнейшем может управлять размером окна данных
         путем установки bounds-checking (проверка границ)  в  массиве,  к
         которому осуществляется доступ.  Одна из наиболее типичных ошибок
         в данных происходит тогда, когда доступ к массиву выполняется че-
         рез его границы.  Все, что происходит на границе массива, теряет-
         ся. Проверка границы массива может быть выполнена с помощью прос-
         того  макроса,  как показано в листинге 2-14.  Для программистов,
         работающих с процессором 80x86, для выполнения этой проверки пре-
         дусмотрена инструкция BOUND (граница).  Для обеспечения совмести-
         мости с инструкцией BOUND был написан макрос bound,  показанный в
         листинге 2-14.

                    Листинг 2-14. Макрос проверки границ массива
         -----------------------------------------------------------------

         ; Сравнение границ массива, содержащихся в общем регистре REG,
         ; с двумя последовательными значениями, размещенными в памяти
         ; по адресу MEM32. Это есть сравнение целых со знаком.
         bound   MACRO   reg,mem32
                 LOCAL   out_bound,in_bound
                 pushf                          ; сохранение флажков
                 cmp     reg,word ptr mem32     ; проверка нижнего предела
                 jl      out_bound              ; превышение индекса
                 cmp     reg,word ptr mem32 + 2 ; проверка верхн. предела
                 jle     in_bound               ; индекс хороший
         out_bound:
                 popf                           ; очистка стека
                 INT     5                      ; принимаемое действие
         in_bound:
                 popf                           ; восстановление флажков
                 ENDM
         -----------------------------------------------------------------

             Макрос bound сравнивает содержимое общего регистра,  содержа-
         щего индекс массива,  с двумя последовательными ячейками  памяти.
         Первая  ячейка  памяти  содержит нижний предел индекса,  а вторая
         ячейка - верхний предел индекса.  Инструкция BOUND выполняет пре-
         рывание 5-го типа (int5), если проверяемый индекс выходит за пре-
         делы границы.  Пользователи этой версии макроса могут модифициро-
         вать макрос bound для выполнения любых своих действий.

                             Защита целостности данных

             Другой областью, восприимчивой к разрушению, является стек. В
         связи  с  тем,  что стек смешивает коды и данные,  ошибка в стеке
         непременно явится результатом ошибки в программе при попытке про-
         цессора использовать данные как ссылку на инструкцию.
             Два наиболее  общих случая разрушения стека вызывают проблемы
         ошибочного   выравнивания.  Первый  случай - из-за несоответствия
         операций PUSH и POP,  а второй - благодаря попытке извлечь данные

                                      - 2-45 -
         с помощью инструкции POP, которые были помещены по другую сторону
         инструкций CALL или RET. Эти проблемы можно избежать только путем
         удаления особого внимания "спариванию" инструкций PUSH и POP, ис-
         пользуемых в программе,  и обеспечения гарантии в том,  что такие
         пары не выполняют ссылки за границы подпрограммы.
             В случае передачи параметров возникает вопрос, а как подпрог-
         рамма выполняет очистку стека?  Обычно, правило для такого случая
         состоит  в том,  что программа,  передавшая в стек данные по инс-
         трукции PUSH,  получает данные из стека по инструкции  POP.  Если
         следовать  этому  правилу,  то путем чтения листинга одной,  а не
         двух подпрограмм программист может проверить,  что стек выровнен.
         Однако,  жесткое следование этому правилу предотвращает использо-
         вание для очистки стека инструкций микропроцессора  8086  RET  N.
         Если  интерфейс между двумя программами полностью отлажен и наде-
         жен, то приемлемый риск состоит в использовании инструкции RET N.
             Всякий раз, когда программа должна быть закодирована для при-
         ема переменного количества параметров,  не  следует  использовать
         инструкцию  RET N.  Имеются различные пути ограничения глобальной
         возможности очистки стека только  для  установленного  количества
         переменных,  но все,  вызываемые ими трюковые манипуляции со сте-
         ком,  являются затруднительными для понимания и более трудными  в
         отладке. Если подпрограмма должна принимать переменное количество
         параметров,  то очищать эти параметры из стека должна  вызывающая
         подпрограмма.  Кроме  того,  вызывающая  подпрограмма должна ясно
         указывать вызываемой программе количество  передаваемых  для  нее
         параметров.
             Все операции, выполняемые над стеком, за исключением операций
         PUSH  и POP,  должны происходить под флагом указателя стека и для
         доступа к стеку использовать регистр BP.  Это значит,  что указа-
         тель стека должен быть установлен в значение ниже манипулируемого
         элемента.  Если манипулируемые данные  остаются  неизменными,  то
         возникает  прерывание.  По этой же причине нельзя непосредственно
         манипулировать указателем стека до тех пор,  пока  не  произойдет
         переключение  стека,  или открытие памяти стека.  Если произойдет
         прерывание в то время,  когда указатель стека не указывает истин-
         ную  вершину  стека,  то данные в стеке могут быть потеряны.  Все
         сказанное выше не является предостережением для умелого использо-
         вания манипуляции стеком.

                                     Заключение

             В зтой главе было рассмотрено множество тем: от теоретической
         природы  структурного  программирования  до  подробного изложения
         макроассемблера MASM, MS-DOS и функционирования семейства микроп-
         роцессоров 8086. Была предпринята попытка получить некоторые аль-
         тернативные подходы в структурном программировании для нужд поль-
         зователя  ПЭВМ.  Хотя маловероятно,  что все или даже большинство
         этих технических приемов появится в Ваших небольших программах на
         языке  Ассемблер,  мы  будем благодарны за то,  что многие из них
         найдут применение в Ваших обширных проектах.  И,  если вспомнится
         только один момент,  то все равно: сначала все обдумайте, а коди-
         руйте только после обдумывания.
             Большинство более практических указаний о MASM и MS-DOS вновь
         "всплывут на поверхность" при обсуждении других вопросов в после-
         дующих  главах данного руководства.  Попытайтесь вывести примеры,
         приведенные в этой главе,  и Вы получите удовольствие от  их  ис-
         пользования.  Из приведенного выше  Вам понадобится многое. Боль-

                                      - 2-46 -
         шая  часть  отдельных  вводных  сведений  по  управлению  памятью
         MS-DOS,   приведенных в этой главе, послужит фундаментом для гла-
         вы 3 "Управление программами и памятью".
