Линкфилд
Здесь может быть ваша реклама
|

 Глава 3. Команды и алгоритмы

Глава 3. Команды и алгоритмы

Организация приложений MS-DOS

Как уже отмечалось выше, язык ассемблера является отражением архитектуры процессора, и изучение языка в сущности означает изучение системы команд и способов адресации, реализуемых процессором. Одна ко любой язык программирования полезен лишь постольку, поскольку на нем можно написать какие-то работоспособные программы. В то же время трудно представить себе реальную программу, которая выполняет чисто логические или вычислительные действия, ничего не вводя и не выводя и не взаимодействуя с другими программами. Однако такие вопросы, как организация выполнимой программы, ее запуск, взаимодействие с разнообразными аппаратными и программными объектами вычислительной системы (клавиатурой, дисками, таймером, памятью, системными драй верами и проч.) и, наконец, завершение являются прерогативой операционной системы. Поэтому в программах на языке ассемблера всегда широко используются системные средства, например, для вывода па экран или ввода с клавиатуры, чтения или записи файлов, управления памятью и проч. Более того, сама внутренняя организация программы, ее структура и, в определенной степени, алгоритмы поведения в сильной степени определяются правилами организации вычислительного процесса, заложенными в DOS. Изучение языка ассемблера в отрыве от конкретной операционной системы вырождается в схоластическое занятие, результатом которого будет знание формальных правил написания программных предложений без возможности применить эти правила для создания работоспособных программ.
В то же время возможности даже такой относительно простой операционной системы, как MS-DOS, весьма велики и многообразны, и их изучение составляет самостоятельный раздел программирования. В настоящей книге средства DOS рассматриваются лишь в том минимальном объеме, который необходим для создания простых, но работоспособных программ на языке ассемблера, а также для демонстрации основных алгоритмов и приемов программирования.
Желающие получить более глубокое представление о возможностях MS-DOS и использовании функций DOS в прикладном программировании, могут обратиться к книге: К.Г.Финогенов "Самоучитель по системным функциям MS-DOS", M., Радио и связь, Энтроп, 1995.
К числу важнейших вопросов, требующих хотя бы минимального рассмотрения, следует отнести требования, предъявляемые MS-DOS к структуре прикладных программ, а также к особенностям их взаимодействия с самой DOS и с другими программами.
Программы, предназначенные для выполнения под управлением MS-DOS, можно классифицировать по разным признакам. По внутренней организации все программы принадлежат к одному из двух типов, которым соответствуют расширения имен программных файлов .ЕХЕ и .СОМ. По взаимодействию с самой DOS программы подразделяются на транзитные и резидентные. Наконец, следует выделить важнейший класс программ, служащих для обработки аппаратных или программных прерываний, и называемых обычно обработчиками прерываний. Мы не касаемся здесь таких специфических программ, как устанавливаемые драйверы устройств, командные процессоры (к их числу принадлежит COMMAND.COM) или оболочки DOS (например, широко распространенная программа Norton Commander), которые можно выделить в самостоятельные классы.
Первый пример законченной программы, рассмотренный нами в гл. 2, относился к наиболее распространенному типу .ЕХЕ-приложений. Для такой программы характерно наличие отдельных сегментов команд, данных и стека; для адресации к полям каждого сегмента используется свой сегментный регистр. Удобство .ЕХЕ-программы заключается в том, что ее можно почти неограниченно расширять за счет увеличения числа сегментов. В случае большого объема вычислений в программу можно включить несколько сегментов команд, обеспечив, разумеется, переходы из сегмента в сегмент с помощью команд дальних переходов или дальних вызовов подпрограмм. Если же программа должна обрабатывать большие объемы данных, в ней можно предусмотреть несколько сегментов данных. Каждый сегмент не может иметь размер более 64 Кбайт, однако в сумме их объем ограничивается только наличной оперативной памятью. Правда, в реальном режиме затруднительно обратиться к памяти за пределами 1 Мбайт адресного пространства, так что максимальный размер программы, если не предусматривать в ней какие-то специальные средства поочередной загрузки сегментов, ограничен величиной 550 ... 600 Кбайт. Наличие в МП 86 лишь двух сегментных регистров данных (DS и ES) несколько усложняет алгоритмы обработки больших объемов данных, так как приходится постоянно переключать эти регистры с одного сегмента на другой. Однако реально в современных процессорах имеются не два, а четыре сегментных регистра данных (DS, ES, FS и GS), которые вполне можно использовать в приложениях DOS, упростив тем самым процедуры обращения к данным и ускорив выполнение программ. Позже все эти возможности будут рассмотрены более подробно.
Во многих случаях объем программы оказывается невелик - меньше, а часто и много меньше, чем 64 Кбайт. Такую программу нет никакой необходимости составлять из нескольких сегментов: и команды, и данные, и стек можно разместить в единственном сегменте, настроив на его начало все 4 сегментных регистра. Для односегментных программ в MS-DOS существует специальный формат и специальные правила их составления. Программные файлы с программами, составленными по этим правилам, имеют расширение .СОМ. В формате .СОМ обычно пишутся резидентные программы и драйверы, хотя любую прикладную программу небольшого объема можно оформить в виде .СОМ-приложения. Если посмотреть список системных программ, входящих в DOS, и реализующих, в частности, внешние команды DOS, то можно заметить, что приблизительно треть этих программ написана в формате .COM (COMMAND.COM, FORMAT.COM, SYS.COM и др.), а две трети - в формате .EXE (FC.EXE: PRINT.EXE, XCOPY.EXE и т.д.). Ниже мы рассмотрим правила составления и особенности исполнения как .ЕХЕ-, так и .СОМ-программ.
Другой критерий классификации программ определяет способ взаимодействия прикладной программы с другими программами и самой DOS. По этому критерию программы делятся на два вида: транзитные и резидентные.
Ход выполнения транзитной программы (а к транзитным относится подавляющее большинство приложений DOS) выглядит следующим образом. Пользователь запускает программу, вводя с клавиатуры ее имя, завершаемое нажатием клавиши Enter. Соответствующие программы-компоненты DOS отыскивают на диске файл с указанным именем, загружают его в память и передают управление на входную точку этой программы. Далее программа выполняется, фактически монополизируя ресурсы компьютера. Пока она не завершилась, пользователь не имеет доступа к DOS и, соответственно, лишен возможности запустить другую программу или выполнить какую-либо команду DOS. Ввод с клавиатуры возможен только в ответ на запрос текущей программы, если в ней предусмотрено обращение к клавиатуре за получением каких-либо данных.
Совсем по-другому функционирует резидентная программа. Пользователь запускает ее точно так же, как и транзитную, вводя с клавиатуры ее имя. Программы DOS загружают программный файл в память и передают управление на точку входа. Однако дальше вычислительный процесс развивается поиному. Программа выполняет только свой начальный, инициализирующий фрагмент, после чего вызывает специальную функцию DOS (с номером 31h). Эта функция завершает программу и возвращает управление в DOS, но не освобождает память от завершившейся программы, а оставляет эту программу в памяти, делая ее резидентной. Программа остается в памяти и, можно сказать, ничего не делает. Поскольку управление передано DOS, пользователь может вводить с клавиатуры любые команды и, в частности, запускать другие транзитные (или резидентные) программы. Когда будет активизирована находящаяся в памяти резидентная программа? Как правило, резидентные программы включают в себя обработчики аппаратных или программных прерываний. Если, например, в резидентной программе имеется обработчик прерываний от системного таймера, который, как известно, выдает сигналы прерываний приблизительно 18 раз в секунду, то каждое такое прерывание будет предавать управление резидентной программе, которая может, например, периодически выводить на экран текущее время или какую-то иную информацию. Работа резидентной программы будет протекать независимо от других программ и параллельно с ними. Другим классическим примером резидентной программы является русификатор клавиатуры, который получает управление при нажатии любой клавиши, независимо от того, какая программа сейчас выполняется. Задача русификатора - определить по имеющемуся в нем флагу, на каком языке работает пользователь, и в необходимых случаях сформировать соответствующий нажатой клавише код ASCII русской буквы.
Следует заметить, что необходимость в резидентных программах возникла лишь потому, что MS-DOS является существенно однозадачной системой. В многозадачной операционной системе Windows понятие резидентной программы в принципе отсутствует.
Разумеется, своими особенностями составления и функционирования обладают и обработчики прерываний - чрезвычайно важный класс программ, обслуживающих многочисленные внешние устройства компьютера - клавиатуру, мышь, магнитные диски и проч., а также нестандартную аппаратуру, если компьютер используется для управления научной установкой или технологическим процессом.
Рассмотрим основные правила составления и функционирования перечисленных типов программ, чтобы в дальнейшем можно было использовать их в примерах, иллюстрирующих те или иные средства языка ассемблера.

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

.586                         ; Размещение трансляции всех

                               ; команд (386-486-Pentium)

code segment usee16 ; Начало сегмента команд

                                ; 16-разрядное приложение

assume CS:code, DS: data

 main proc                 ; Начало главной процедуры

mov  AX,  data           ; Инициализация

mov DS, AX               ;сегментного регистра DS

...                             ;Текст главной процедуры

mov  AX,4C00h           ;Вызов функции DOS

int 2 In                      ; Завершение программы

main endp                  ; Конец главной процедуры

code ends                  ; Конец сегмента команды

data segments use16  ; Начало сегмента данных

...                               ; Определения данных

data ends                    ; Конец сегмента данных

stk segment stack         ; Начало сегмента данных

  db  256 dup(0)            ; Стек

stk ends                       ; Конец сегмента стека

end main                      ; Конец программы и точка входа

Программа начинается с директивы ассемблера .586, разрешающей использовать в тексте программы весь набор команд процессора Pentium (кроме привилегированных). Если программа будет использовать только базовый набор команд МП 86, указание этой директивы не обязательно.
С другой стороны, ее указание не обязывает нас обязательно использовать команды Pentium. Если в программе предполагается использовать лишь дополнительные команды процессоров 486 или 386, то вместо .586 можно написать .486 или .386.
Указание любого номера 32-разрядного процессора приведет к тому, что по умолчанию программа будет транслироваться, как 32-разрядное приложение, в то время как нам нужно создать обычное 16-разрядное приложение. Для того, чтобы все адреса в программе рассматривались, как 16-битовые, необходимо придать сегментам команд и данных описатели use16. Для сегмента стека этот описатель не нужен, так как в стеке нет поименованных ячеек.
Программа состоит из трех сегментов - команд, данных и стека. Имена сегментов выбраны произвольно. Собственно программа обычно состоит из процедур. Деление на процедуры не обязательно, но повышает ее наглядность и облегчает передачу управления на подпрограммы. В рассматриваемом примере сегмент команд содержит единственную процедуру main, открываемую оператором ргос (от procedure, процедура) и закрываемую оператором endp (end procedure, конец процедуры). Перед обоими операторами указывается имя процедуры, которое в дальнейшем может использоваться в качестве относительного адреса процедуры (в сущности, относительного адреса первого выполнимого предложения этой процедуры). У нас это имя выступает в качестве параметра завершающей программу директивы end. Имена процедур, так же, как и имена сегментов, выбираются произвольно.
Если программа имеет сегмент данных с какими-либо данными, то для того, чтобы к этим данным можно было обратиться, необходимо занести сегментный адрес сегмента данных в один из сегментных регистров. Обычно в качестве такого регистра выбирают DS. Таким образом, предложения, с которых начался текст главной процедуры

mov AX,data ;Инициализация


mov DS,АХ ;сегментного регистра DS

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


Точно также обязательными являются и завершающие предложения

mov AX,4C00h ;Вызов функции DOS


int 21h ;завершения программы

в которых вызывается функция DOS с номером 4Ch. Эта функция, как уже отмечалось, завершает программу, освобождает занимаемую ею память и передает управление командному процессору COMMAND.COM. Еще два замечания следует сделать относительно процедуры трансляции и компоновки программы. Если сегмент данных расположить после сегмента команд, как это сделано в нашем примере, то у транслятора возникнут сложности при обработке встречающихся в программных предложениях имен полей данных, так как эти имена еще неизвестны транслятору. Для того, чтобы такие, как говорят, "ссылки вперед" могли правильно обрабатываться, следует в команде вызова транслятора TASM заказать два прохода. Это делается указанием ключа /m2.
С другой неприятностью мы столкнемся, если попытаемся включить в программу операции с 32-разрядными операндами (даже и с командами МП 86). Компоновщик TASM по умолчанию запрещает такого рода операции. Чтобы преодолеть этот запрет, следует в команде вызова компоновщика указать ключ /3.
Таким образом, приведенный в гл. 1 командный файл должен выглядеть (для подготовки программы P.ASM) следующим образом:

tasm /z /zi /n /m2 p,p,p


tlink /x /v /3 p,p

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

Пример 3-1. Получение текущего диска

; Опишем сегмент команд

assume CS:code,DS:data

code segment

main proc

move AX, data   ;Настроим DS

mov DS,AX    ; на сегмент данных

mov AH,19h    ; Функция DOS получения

int 21h             ; текущего диска

add disk,AL   ; Преобразуем номер в код

                        ; ASCII

mov AH,09h  ; Функция DOS вывода на экран

mov DX,offset msg  ; Адрес строки

int 21h                       ; Вызов DOS

mov  AH,01h            ; Функция DOS ввода символа

int 2 In                       ; Вызов DOS

mov AX,4C00h        ; Функция DOS завершения

int 21h                       ; программы

code ends

;Опишем сегмент данных

data segment use16

msg db "Текущий диск"   ; Выводимый на экран текст

disk db " A:",13,10,"$"     ; Продолжение текста

data ends

; Опишем сегмент стека

stk segment stack

 db 256 dup(U)         ; Стек

stk ends

end main

Рассмотрим текст приведенного примера. После настройки сегментного регистра DS на сегмент данных, вызывается функция DOS с номером 19h, которая позволяет получить код текущего диска. У этой функции нет никаких параметров, а результат своей работы она возвращает в регистре AL в виде условного кода. 0 обозначает диск А:, 1 диск В:, 2 диск С: и т.д. Если, например, пользователь работает на диске F, то функция 19h вернет в AL код 5.
Для преобразования кода диска в его буквенное обозначение, мы воспользовались широко распространенным приемом. В полях данных определена символьная строка, которая будет выводиться на экран. Для удобства работы она разделена на две части, каждая из которых имеет свое имя. Началу строки присвоено имя msg, а той ее части, которая начинается с обозначения диска А:, имя disk (разумеется, имена выбраны произвольно). Если посмотреть на таблицу кодов ASCII, то можно заметить, что код каждой следующей буквы алфавита на 1больше предыдущей. Таким образом, если к коду латинской буквы A (41h) прибавить 1, получится код буквы В, а если прибавить, например, 5, получится код буквы F. Именно эта операция и выполняется в предложении

add disk,AL ;Преобразуем номер в код ASCII

где к байту с адресом disk прибавляется код, возвращенный функцией DOS.
Выполнив модификацию строки, мы выводим ее на экран уже знакомой нам функцией DOS 09h. Она выводит все символы строки, пока не встретится с символом $, которым наша строка и завершается. Перед знаком S в строке имеются два числа: 13 и 10. При выводе текстовой строки на экран любой функцией DOS код 13 трактуется DOS, как команда вернуть курсор в начато строки ("возврат каретки"), а код 10 - как команда на перевод строки. Два эти кода переводят курсор в начало следующей строки экрана. В данном случае, когда на экран ничего больше не выводится, можно было обойтись и без этих кодов, которые включены лишь в познавательных целях.
Между прочим, правильная работа программы основана на том предположении (безусловно правильном), что ассемблер расположит наши данные в памяти в точности в том же порядке, как они описаны в программе. Именно это обстоятельство и позволяет дробить единую строку на части, не опасаясь, что в память они попадут в разные места, что привело бы, разумеется, к непредсказуемому результату. После вывода на экран сообщения о текущем диске в программе вызывается функция DOS с номером 01h. Эта функция вводит с клавиатуры один символ. Если символов нет (мы после запуска программы не нажимали на клавиши), функция 01h ждет нажатия фактически любой клавиши (более точно - любой алфавитно-цифровой или функциональной клавиши). Такой весьма распространенный прием позволяет остановить выполнение программы до нажатия клавиши, что дает возможность программисту посмотреть, что вывела программа на экран, и оценить правильность ее работы.
Наконец, последнее действие носит, как уже отмечалось, сакраментальный характер. Вызовом функции DOS 4Ch программа завершается с передачей управления DOS.
Взглянем еще раз на текст программы 3-1. Если не считать первых предложений инициализации регистра DS, то в программе имеется лишь одна строка, носящая, можно сказать, вычислительный характер - это прибавление полученного кода диска к содержимому байта памяти. Все остальные строки служат для вызова тех или иных функций DOS - получения информации о текущем диске, вывода строки на экран, остановки программы и, наконец, ее завершения. Это подтверждает высказанное выше утверждение о важности изучения системных средств и широком использовании их в программах на языке ассемблера. Разумеется, в программе могут быть и сколь угодно сложные и протяженные участки обработки данных и других вычислений, но такие операции, как ввод с клавиатуры, вывод на экран, работа с файлами, получение, как в нашем примере, системной информации и многое другое выполняется исключительно с помощью вызова тех или иных функций DOS (или BIOS). Программу на языке ассемблера просто невозможно написать без использования системных средств.


Структура и образ памяти программы .СОМ
Как уже отмечалось, программа типа .СОМ отличается от программы типа .ЕХЕ тем, что содержит лишь один сегмент, включающий все компоненты программы: PSP, программный код (т.е. оттранслированные в машинные коды программные строки), данные и стек. Структура типичной программы типа .СОМ на языке ассемблера выглядит следующим образом:

code segment:

assume CS:text,DS:text

org 100h ;Местодля PSP

main proc

...             ; Текст программы

main endp

...           ; Определения данных

code ends

end main

Программа содержит единственный сегмент code. В операторе ASSUME указано, что сегментные регистры CS и DS будут указывать на этот единственный сегмент. Оператор ORG 100h резервирует 256 байт для PSP. Заполнять PSP будет по-прежнему система, но место под него в начале сегмента должен отвести программист. В программе нет необходимости инициализировать сегментный регистр DS, поскольку его, как и остальные сегментные регистры, инициализирует система. Данные можно разместить после программной процедуры (как это показано в приведенном примере), или внутри нес, или даже перед ней. Следует только иметь в виду, что при загрузке программы типа .СОМ регистр IP всегда инициализируется числом 100h, поэтому сразу вслед за оператором ORG 100h должна стоять первая выполнимая команда программы. Если данные желательно расположить в начале программы, перед ними следует поместить оператор перехода на фактическую точку входа, например jmp entry.
Образ памяти программы типа .СОМ показан на рис. 3.1. После загрузки программы все сегментные регистры указывают на начато единственного сегмента, т.е. фактически на начато PSP. Указатель стека автоматически инициализируется числом FFFEh. Таким образом, независимо от фактического размера программы, ей выделяется 64 Кбайт адресного пространства, всю нижнюю часть которого занимает стек. Поскольку верхняя граница стека не определена и зависит от интенсивности и способа использования стека программой, следует опасаться затирания стеком нижней части программы. Впрочем, такая опасность существует и в программах типа .ЕХЕ, так как в реальном режиме нет никаких механизмов защиты, и при сохранении в стеке большего объема данных, чем может так поместиться, данные начнут затирать поля того сегмента, который расположен за стеком (если таковой сегмент существует).

Иллюстрированный самоучитель по Assembler, программы, электронные книги, раскрутка, оптимизация

Рис. 3.1. Образ памяти программы .СОМ

Программы типа .СОМ отличаются от .ЕХЕ-программ не только отсутствием сегментов данных и стека. В гл. 2 было показано, что при выравнивании сегментов на байт, что делается с помощью описателя byte

data segment byte

системные программы располагают сегменты загружаемой программы с некоторым перекрытием, что позволяет избежать пустых промежутков между сегментами в памяти, возникающих из-за того, что размеры сегментов могут быть не кратны величине параграфа - 16 байт. Такое расположение сегментов требует изменения значений ссылок на адреса ячеек памяти. В состав программного файла с расширением .ЕХЕ входит таблица с перечнем байтов программы, содержимое которых может подвергнуться изменению в процессе загрузки программы в память. Поэтому, кстати, размер файла с расширением .ЕХЕ может превышать истинный размер программы в памяти.
Программа типа .СОМ состоит из единственного сегмента, и проблема настройки ссылок не возникает. Файл с расширением .СОМ почти в точности отражает содержимое памяти после загрузки программы. Отличие заключается только в том, что в программном файле отсутствует префикс программы PSP, который система вставляет в программу в процессе ее загрузки. Таким образом, файл с расширением .СОМ обычно оказывается на 256 байт короче своего образа в памяти.
Если оттранслировать и скомпоновать программу, написанную в формате .СОМ, обычным образом, образуется программный файл с расширением .ЕХЕ. Этот файл можно запустить на выполнение, однако работать он будет неверно. Дело в том, что система, загружая файл типа .ЕХЕ в память, пристраивает перед загруженной программой префикс и настраивает на него регистры DS и ES. В результате значение DS окажется на 10h меньше, чем сегментный адрес сегмента с командами и данными, что приведет к неправильной адресации при обращении к полям данных. Программу, написанную в формате .СОМ, можно запускать только в виде файла с расширением .СОМ, для которого в DOS предусмотрен CBI алгоритм загрузки и запуска. Для того, чтобы компоновщик создал файл с расширением .СОМ, в строке запуска компоновщика необходимо предусмотреть ключ /t (при использовании компоновщика TLINK.EXE):

tlink /x /v /3 /t p,p

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

Пример 3-2. Простая .COM- программа

assume CS:code,DS:code

code segment

org 256      ; Местопод PSP

main proc

mov AH, 09h    ; Функция вывода на экран

mov DX,offset msg

int 21h

mov AX,4C00h     ; Функция завершения

int 21h                   ; программы

main endp

msg db 16,16,16 ' Программа типа .COM'17,17,17,'$'

code ends

end main

В начале программы отведено 256 байт под PSP; в программе отсутствует инициализация регистра DS; поле данных размещено в программном сегменте непосредственно после последней команды. Для разнообразия в строку, выводимую на экран, включены коды 16 и 17, которые отображаются на экране в виде залитых треугольников (рис. 3.2). Как видно из этого рисунка, программа имела имя Р. СОМ и запускалась из каталога F:CURRENT.
Рассмотрим важный в принципиальном плане вопрос о месте размещения данных в .СОМ-программе. В нашем примере данные описаны в конце программного сегмента вслед за процедурой main, которая, как и в предыдущих примерах, введена скорее для порядка, чем по необходимости.

Иллюстрированный самоучитель по Assembler, программы, электронные книги, раскрутка, оптимизация

Рис. 3.2. Вывод программы 3.2.

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

assume CS:code,DS:code

code segment

org 256         ; Местопод PSP

main proc

jmp start      ; Первая выполнимая команда

msg db  16,16,16,'Программа типа .COM',17,17,17,'$'

start:  mov AH,09h   ; Функция вывода на экран

mov DX,offset msg

int 21h

...          ;Продолжение программы

Таким образом, данные могут быть размещены как после программы, так и среди выполнимых предложений программы. Важно только соблюсти обязательное условие: ни при каких обстоятельствах на данные не должно быть передано управление. В первом случае (пример 3-2) данные помещены за вызовом функции DOS, завершающей программу. Ясно, что после выполнения этой функции управление уже не вернется в нашу программу, а будет передано командному процессору, поэтому размещение здесь данных вполне возможно. В последнем фрагменте данные описаны, можно сказать, в середине программы. Однако перед ними стоит команда безусловного перехода jmp, которая приводит при выполнении программы к обходу данных.
А вот чего нельзя было сделать, так это разместить данные после закрытия сегмента, как это сделано в приведенном ниже (неправильном!) фрагменте:

...

main endp   ; Конец процедуры

code ends   ; Конец сегмента

msg db  16,16,16' Программа типа .COM',17,17,17,'$'

end main 


Это второе обязательное условие: из чего бы ни состояла программа, все ее компоненты должны входить в те или иные сегменты. Вне сегментов допускаются только нетранслируемые директивы ассемблера типа .586 или assume.
Наконец, третье условие, о котором уже говорилось, относится только к программам типа .COM. DOS, загрузив программу в память, инициализирует указатель команд числом 100h, т.е. адресом первой команды вслед за оператором org 100h. Поэтому главная процедура .СОМ-программы (если в ней имеется несколько процедур) обязательно должна быть первой, причем первое предложение этой процедуры должно быть выполнимой командой (например, командой jmp, как это показано выше).

Обработчики аппаратных прерываний
Обработчики прерываний являются важнейшей составной частью многих программных продуктов. Как было показано в гл. 1, прерывания разделяются на внутренние, возникающие в самом микропроцессоре в случае определенных сбоев (попытка деления на 0, несуществующая команда), внешние, приходящие из периферийного оборудования (клавиатура, мышь, диски, нестандартные устройства, подключенные к компьютеру) и программные, являющиеся реакцией процессора на команду int с тем или иным номером. В прикладных программах приходится обрабатывать, главным образом, внешние и программные прерывания. Общие принципы обслуживания тех и других прерываний одинаковы, однако условия функционирования обработчиков аппаратных прерываний имеют значительную специфику, связанную, главным образом, с тем, что прерывания от аппаратуры приходят в произвольные моменты времени и могут прервать текущую программу в любой ее точке. Обработчик прерывания должен быть написан таким образом, чтобы его выполнение ни в какой степени не отразилось на правильном функционировании текущей (прерываемой) программы.
Рассмотрим схематически структуру и функционирование программного комплекса, включающего собственный обработчик какого-либо аппаратного прерывания (рис. 3.3).

Иллюстрированный самоучитель по Assembler, программы, электронные книги, раскрутка, оптимизация

Рис. 3.3. Функционирование программного комплекса с обработчиком прерываний.

Обработчик прерываний может входить в состав программы в виде процедуры, или просто являться частью программы, начинающейся с некоторой метки (входной точки обработчика) и завершающейся командой выхода из прерывания iret. Пока мы не будем рассматривать более сложный случай, когда обработчик представляет собой самостоятельную резидентную программу.
Программа, начиная свою работу, прежде всего должна выполнить инициализирующие действия по установке обработчика прерываний. В простейшем случае эти действия заключаются в занесении в соответствующий вектор полного адреса (сегмента и смещения) обработчика. Поскольку обработчик входит в состав программы, его относительный адрес известен; это имя его процедуры или метка входной точки. Что же касается сегментного адреса, то обработчик может входить в сегмент основной части программы, если она невелика по объему и занимает один сегмент, но может образовывать и самостоятельный сегмент. В любом случае в качестве сегментного адреса можно использовать имя соответствующего сегмента.
Часто инициализация обработчика, помимо установки вектора, предполагает и другие действия: сохранение исходного содержимого вектора прерывания, размаскирование соответствующего уровня прерываний в контроллере прерываний, посылка в устройство команды разрешения прерываний и проч.
Установив обработчик, программа может приступить к дальнейшей работе. В случае прихода прерывания, процессор сохраняет в стеке флаги и текущий адрес программы, извлекает из вектора адрес обработчика и передает управление на его входную точку. Все время, пока выполняется программа обработчика, основная программа, естественно, стоит. Завершающая обработчик команда irct извлекает из стека сохраненные там данные и возвращает управление в прерванную программу, которая может продолжить свою работу. Последующие прерывания обслуживаются точно так же.
Функции обработчика прерываний зависят от решаемой задачи и назначения того устройства, от которого поступают сигналы прерываний. В случае прерываний от клавиатуры задача обработчика прерываний - принять и сохранить код нажатой клавиши. Прерывания от мыши свидетельствуют о ее перемещении, что требует обновления положения курсора на экране. Если обслуживаемым устройством является физическая установка, то сигнал прерывания может говорить о том, что в установке накоплен определенный объем данных, которые надо перенести из памяти установки в память компьютера. В любом случае обработчик прерываний должен быть программой несложной, для выполнения которой не требуется много машинного времени.
Рассмотрим структуру программы с обработкой аппаратных прерываний. Наиболее удобным аппаратным прерыванием, которое можно использовать в исследовательских целях, является прерывание от системного таймера, которое генерируется 18.2 раза в секунду и служит источником сигналов для хода системных часов, отсчитывающих время, истекшее после включения машины. Замена системного обработчика на прикладной не приводит к каким-либо неприятностям, кроме, возможно, остановки на некоторое время системных часов.
Будем считать, что наш программный комплекс представляет собой программу типа .ЕХЕ и что обработчик прерываний входит в общий с основной программой программный сегмент. Для определенности будем использовать вектор 08h, хотя, разумеется, для любого другого аппаратного вектора структура программы останется той же. Поначалу приведем текст программы с некоторыми купюрами.

Пример 3-3. Обработчик прерываний от таймера

code segment

assume CS:code,DS:data

;Главная процедура

main proc

mov AX,data   ; Инициализация сегментного 

mov DS,AX     ; регистра DS

;Сохраним исходный вектор

mov AH,35h    ; Функция получения вектора

mov AL,08h    ; Номер вектора

int 21h

mov word ptr old_08,BX ; Смещение исходного обработчика

mov word ptr old_08+2,ES ; Сегмент исходного обработчика

;Установим наш обработчик

mov AH,25h    ;Функция заполнения вектора

mov AL,08h    ; Номер вектора

mov DX,offset new_08 ; Смещение нашего обработчика

push DS                    ; Сохраним DS=data

push CS                    ; Перепишем CS в DS

pop DS                      ; черезстек. DS:DX->new_08

int 21h

pop DS                         ; Восстановим DS=data

...                                   ; Продолжение основной программы

; Перед завершением программы восстановим исходный вектор

Ids DX ,old_08      ; Заполним DS:DX из old_08

mov AH,25h                     ; Функция заполнения вектора

move AL,08h           ; Номер вектора

int 21h

mov AX,4C00h         ;Функция завершения программы

int 21h

main endp

;Процедура обработчика прерываний от таймера

new_08 proc

...                ; Действия. выполняемые 18 раз в секунду

mov AL,20h     ;Разблокировка прерываний

out 20h,AL      ; в контроллере прерываний

iret                   ; Возврат в прерванную програму

new_08   endp

code ends

data segment

old_08 db 0   ; Ячейка для хранения исходного вектора

data ends

stk segment stack

db 256 dup(U)

stk ends

 end main

В приведенном примере обработчик прерываний расположен в конце программы, после главной процедуры main. Взаимное расположение процедур не имеет ни малейшего значения; с таким же успехом обработчик можно было поместить в начале программы. Не имеет также значения, выделен ли обработчик в отдельную процедуру или просто начинается с метки.
Для того, чтобы прикладной обработчик получал управление в результате прерываний, его адрес следует поместить в соответствующий вектор прерывания. При этом исходное содержимое вектора будет затерто, и если прерывания будут поступать и после завершения программы, возникнет весьма неприятная ситуация, когда управление будет передаваться по адресу, по которому в памяти может располагаться что угодно. Поэтому стандартной методикой является сохранение в памяти исходного содержимого вектора и восстановление этого содержимого перед завершением программы.
Хотя и чтение, и заполнение вектора прерываний можно выполнить с помощью простых команд mov, однако предпочтительнее использовать специально предусмотренные для этого функции DOS. Для чтения вектора используется функция с номером 35h. В регистр AL помещается номер вектора. Функция возвращает исходное содержимое вектора в парс регистров ES:BX (легко догадаться, что в ES сегментный адрес, а в ВХ смещение). Для хранения исходного содержимого вектора в сегменте данных предусмотрена двухсловная ячейка old_08. В младшем слове этой ячейки (с фактическим адресом old_08) будет хранится смещение, в старшем (с фактическим адресом old_08+2) - сегментный адрес. Для того, чтобы обратиться к словам, составляющим эту ячейку, приходится использовать описатель word ptr, который как бы заставляет транслятор на время забыть о начальном объявлении ячейки и позволяет рассматривать ее, как два отдельных слова. Если бы мы отвели для исходного вектора две 16-битовые ячейки, например

old_08_offs dw 0 ; Для смещения

old_08_seg dw 0 ;Для сегментного адреса

то к ним можно было бы обращаться без всяких описателей.
Сохранив исходный вектор, можно установить в нем адрес нашего обработчика. Для установки вектора в DOS предусмотрена функция 25h. Она требует указания номера устанавливаемого вектора в регистре AL, a его полного адреса - в парс регистров DS:DX. Здесь нас подстерегает неприятность. Занести в регистр DX смещение нашего обработчика new_08 не составляет труда, это делается командой

mov DX,offset new_08 ;Смещение нашего обработчика

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

mov AX,seg new_08  ; Получим сегмент с процедурой new_08

mov DS,AX                 ;  Перепишем его в DS

В примере 3-3 использован другой прием - содержимое CS отправляется в стек и тут же извлекается оттуда в регистр DS:

push CS pop DS

После возврата из DOS надо не забыть восстановить исходное содержимое DS, сохраненное в стеке. Инициализация обработчика прерываний закончена. Начиная с этого момента, каждый сигнал таймера будет приводить к прерыванию продолжающейся основной программы и передаче управления на процедуру new_08.
Перед завершением программы необходимо поместить в вектор 8 адрес исходного, системного обработчика, который был сохранен в двухсловном поле old_08. Перед вызовом функции 25h установки вектора в регистры DS:DX надо занести содержимое этого двухсловного поля. Эту операцию можно выполнить одной командой Ids, если указать в качестве ее первого операнда регистр DX, а в качестве второго - адрес двухсловной ячейки, в нашем случае old_08. Именно имея в виду использование этой команды, мы и объявили поле для хранения вектора двухсловным, отчего возникли некоторые трудности при его заполнении командами mov. Если бы мы использовали второй предложенный выше вариант и отвели для хранения вектора две однословные ячейки (old_08_offs и old_08_seg), то команду Ids пришлось бы снабдить описателем изменения размера ячейки:

Ids DX,dword ptr old_08_offs

Между прочим, здесь так же разрушается содержимое DS, но поскольку сразу же вслед за функцией 25h вызывается функция 4Ch завершения программы, это не имеет значения.
Последнее, что нам осталось рассмотреть - это стандартные действия по завершению самого обработчика прерываний. Выше уже говорилось, что последней командой обработчика должна быть команда iret, возвращающая управление в прерванную программу. Однако перед ней необходимо выполнить еще одно обязательное действие - послать в контроллер
прерываний команду конца прерываний. Дело в том, что контроллер прерываний, передав в процессор сигнал прерывания INT, блокирует внутри себя линии прерываний, начиная с той, которая вызвала данное прерывание, и до последней в порядке возрастания номеров IRQ. Таким образом, прерывание, пришедшее, например, по линии IRQ 6 (гибкий диск) заблокирует дальнейшую обработку прерываний по линиям 6 и 7, а прерывание от таймера (IRQ0) блокирует вообще все прерывания (IRQ0...IRQ7, а также и IRQ8...IRQ15, поскольку все они являются разветвлением уровня IRQ2, см, гл. 1, рис. 1.11). Любой обработчик аппаратного прерывания обязан перед своим завершением снять блокировку в контроллере прерываний, иначе вся система прерываний выйдет из строя. Снятие блокировки осуществляется посылкой команды с кодом 20h в один из двух портов, закрепленных за контроллером прерываний. Для ведущего контроллера эта команда посылается в порт 20h, для ведомого - в порт A0h. Таким образом, если бы мы обрабатывали прерывания от часов реального времени (линия прерываний IRQ8, вектор 70h, ведомый контроллер), то команда конца прерывания выглядела бы так:

mov AL,20h  ;Команда конца прерывания

out A0h,AL   ; Пошлем ее в порт ведомого контроллера

Указанную последовательность команд иногда называют приказом, или командой EOI (от end of interrupt, конец прерывания).
Разобравшись в этих общих вопросах, рассмотрим пример реальной программы, включающей обработчик прерываний от таймера. Для того, чтобы приведенную выше фрагментарную программу преобразовать в действующую, надо написать содержательную часть самого обработчика, а также придумать, что будет делать основная программа после инициализации прерываний. Все это сделать очень просто.
Пусть наш обработчик в ответ на каждое прерывание от таймера выводит на экран какой-нибу

 
MKPortal©2003-2008 mkportal.it
MultiBoard ©2007-2009 RusMKPortal