5.1. Как писать ассемблерную программу

5.2. Системы счисления

5.3. Оперативная память

5.4. Стек

5.5. Сегментация памяти

5.6. Система команд

5.7. Выделение памяти под переменные и массивы

5.8. EXE- и COM-программы

5.9. Трансляция ассемблерной программы

5.10. Работа с клавиатурой и экраном

5.11. Прерывания DOS для работы с клавиатурой

5.11.1. Функция 7 прерывания int 21h

5.11.2. Функция 6 прерывания int 21h

5.11.3. Функция 0ah прерывания int 21h

5.12. Прерывания BIOS для работы с клавиатурой

5.12.1. Функция 0 прерывания int 16h

5.12.2. Функция 1 прерывания int 16h

5.13. Прерывания DOS для работы с экраном

5.14. Прерывания BIOS для работы с экраном

5.14.1. Функция 1 прерывания int 10h

5.14.2. Функция 2 прерывания int 10h

5.14.3. Функция 3 прерывания int 10h

5.14.4. Функция 6 прерывания int 10h

5.14.5. Функция 9 прерывания int 10h

5.14.6. Функция 0eh прерывания int 10h

5.15. Пример программы, работающей с клавиатурой и экраном

5.16. Задания к лабораторной работе «Клавиатура и экран»

5.17. Работа с гибкими дисками

5.18. Задания к лабораторной работе «Работа с гибкими дисками»

5.19. Перехват прерываний и создание резидентных программ

5.20. Задания к лабораторной работе «Создание резидентных программ»

5.21. Работа со звуком

5.22. Отладка программ

5.1. Как писать ассемблерную программу

Процессор всегда выполняет последовательность машинных команд (кодов). Любая машинная команда представляет собой двоичное число (например, 10001000). Очевидно, что написать в машинных кодах какую-либо программу практически невозможно хотя бы потому, что вероятность ошибки при написании программы приближается к единице, а вероятность выявления ошибок и их исправления – к нулю. В начале 50-х гг. XX века было предложено ввести для каждой машинной команды понятный человеку символический эквивалент. Так появился язык Ассемблер, т.е каждой ассемблерной команде соответствует машинная команда и наоборот.

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

Ассемблерную программу можно писать в любом редакторе. Однако, поскольку работаем «под DOS», т.е. скорее всего находимся в одной из таких сред, как DOS NAVIGATOR или FAR (или нечто подобное), то логично использовать встроенный редактор среды, в которой работаем.

Смело жмем Shift-F4 и в ответ на запрос называем наш файл, например, оригинальным названием Lab1.asm. Самое главное, не забыть поставить расширение ASM. Если назовем файл, допустим, Lab1, то при трансляции программы транслятор выдаст сообщение: Cant locate file Lab1.asm. Это означает, что транслятор не нашел файл Lab1.asm. Иначе говоря, транслятор работает только с файлами, имеющими расширение ASM.

После того как программа написана, ее надо оттранслировать и получить из нее исполняемый файл (с расширением COM или EXE). Пока не будем рассказывать, как это делается, а остановимся на том, какие программы нужны для трансляции и где их можно найти.

Нужны две программы: TASM.EXE (собственно транслятор) и TLINK.EXE (редактор связей). Зачастую TLINK требует для своей работы наличия еще нескольких (чаще всего трех) сопровождающих файлов: DPMILOAD.EXE, DPMIMEM.DLL, DPMI16BI.OVL (или нечто подобное). Помните, если программе TLINK недостает какого-либо файла, то TLINK сам при запуске об этом подробно напишет.

Кроме того, для отладки программ понадобится еще один файл: отладчик TD.EXE (Turbo Debugger). Этот отладчик позволяет выполнять программу в пошаговом режиме и одновременно следить за изменением информации в регистрах, памяти, стеке и на экране.

Все эти файлы находятся в компьютере, если на нем установлен хотя бы один язык высокого уровня (Паскаль, Си, Си++, Delphi и т.д). В любом из этих языков в поддиректории BIN найдете все вышеуказанные файлы.

Существуют и другие трансляторы, редакторы связей и отладчики, например MASM, LINK, AFD. Ими тоже можно успешно пользоваться.

5.2. Системы счисления

Любое число может быть задано в различных системах счисления. Например, число 130 в различных системах счисления выглядит так:

130 десятичная,
10000010 двоичная,
82 шестнадцатеричная,
202 восьмеричная.

Наиболее часто при написании ассемблерных программ используются числа, заданные в 10-й и 16-й системах счисления. Реже, но все-таки достаточно интенсивно, используется двоичная система счисления. Для того чтобы транслятор мог понять, о какой системе счисления идет речь при задании в программе какого-либо числа, справа от этого числа пишется соответствующая буква:

130 десятичная,
10000010b двоичная,
82h шестнадцатеричная.

Проще говоря, если буквы нет, то транслятор понимает число, как 10-е и сам переводит его в двоичный эквивалент, с которым уже и работает процессор. Если есть буква h (hexadecimal), транслятор понимает это число как 16-е и сам переводит его в двоичный эквивалент и т.д. Буква b означает двоичную систему (binary).

Транслятор считает значительно лучше, чем «человек с калькулятором», поэтому если надо в программе, допустим, занести в регистр ax значение 34*21/ 8, то пишите:

mov ax, 34*21/8,

транслятор сам все подсчитает.

Однако, если нет уверенности том, как транслятор поймет заданную конструкцию, то лучше ее не пишите. Любую конструкцию обычно можно задать разными способами. При этом лучше выбрать способ, в котором будете уверены, хотя, возможно, при этом придется написать и больше команд. Например, программист ввел в программе три переменные x, y и z:

x db 7 ;директивой db программист «попросил» ;транслятор выделить в памяти байт для ;переменной, которую программист назвал ;х, и занести в этот байт число 7;

y db 17

z db 3

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

mov al, x+2.

Если программист при этом рассчитывает, что в регистр al попадет 7+2 = 9, то он заблуждается. В al попадет 3 (адрес Х плюс 2 байта = адрес Z, по этому адресу из памяти и будет выбрана информация). Чтобы в al действительно попало 7+2 = 9, надо было писать:

mov al, x ; занести в al значение переменной х;

add al, 2 ;прибавить к содержимомуal двойку.

У новичков часто возникает вопрос: «А какую систему счисления лучше использовать в моей программе?». Отвечаем, та или иная система счисления используется в зависимости от ситуации. Например, в регистр al надо занести число 112, здесь удобно написать:

mov al, 112.

Теперь в al надо занести 12, а в регистр ah9. Можно написать:

mov al, 12 ; вalß00001100b = 0ch

mov ah, 9 ; в ah ß00001001b = 9h.

Но ведь можно написать и короче, учитывая, что ah и al составляют вместе регистр ax:

mov ax, 90ch ; в axß0000100100001100b = 090ch.

Курсивом выделена та часть числа, которая попадает в ah. Здесь удобно использовать 16-ю систему счисления. Если бы использовали 10-ю и написали бы mov ax, 912, то это было бы неправильно, поскольку транслятор, переведя 912 в двоичный код, получил бы 0000001110010000, т.е. ah попало бы 3, а в al – 144, что не соответствует заданию.

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

Отметим, что в шестнадцатеричной системе счисления в качестве цифр используются буквы: a (10), b (11) ….. f (15). Если напишем в программе число с7а3h, транслятор не поймет, что речь идет о числе, он будет считать, что это идентификатор какой-то переменной. Поэтому, если число начинается с буквы, перед этой буквой надо писать цифру 0!. В этом случае транслятор поймет, что речь идет о числе, а не о переменной. Итак, правильная запись нашего числа ‑ 0c7a3h. Начинающие программисты часто забывают об этом правиле, в результате транслятор начинает выдавать сообщения об ошибках.

5.3. Оперативная память

С точки зрения программиста оперативная память представляет собой линейный массив ячеек, размером один байт каждая. Каждой ячейке ставится в соответствие ее адрес (номер). Этот адрес принято называть абсолютным или физическим (принятое обозначение – Аф). Считается что адреса ячеек памяти, с которой работает DOS, лежат в диапазоне от 00000h до fffffh. То есть DOS работает с памятью объемом 1 Мбайт.

Чтобы прочитать байт информации из какой-то ячейки памяти или записать в эту ячейку новый байт, необходимо написать соответствующую команду и в этой команде задать необходимый адрес. Например, если в результате выполнения команды формируется физический адрес 00000h, процессор обращается к младшему байту памяти.

Информация в памяти может располагаться не только в виде отдельных байтов, но и в виде слов (2 смежных байта), двойных слов (4 байта) и т.д. При этом адресом любой структуры в памяти считается физический адрес младшего байта этой структуры. То есть, если слово занимает в памяти два байта с адресами 0002аh и 0002bh, это слово имеет физический адрес 0002ah.

5.4. Стек

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

Для того чтобы это было возможно, в состав процессора введен специальный регистр sp, называемый указателем стека. Содержимое этого регистра и адресует некоторую ячейку памяти, которую называют вершиной стека. При выполнении стековой операции (команды) процессор берет из sp адрес вершины стека и записывает в нее (или считывает из нее) информацию. При этом процессор автоматически изменяет содержимое sp, изменяя тем самым месторасположение вершины стека.

Существуют две стековые операции:

- затолкнуть информацию в стек (push);

- вытолкнуть информацию из стека (pop).

Например, команда push bx (затолкнуть в стек содержимое двухбайтового регистра bx) выполняется следующим образом. Сначала процессор вычтет из содержимого регистра sp двойку (поскольку формат bx2 байта), изменив тем самым вершину стека, а затем в эту новую вершину загрузит содержимое регистра bx. Таким образом, при заталкивании стек растет в сторону младших адресов памяти.

Команда pop bx (вытолкнуть слово из вершины стека в регистр bx) выполняется в обратном порядке. Сначала процессор по содержимому sp определит вершину стека, считает из этой вершины слово и поместит его в bx, а затем прибавит к sp двойку, изменив тем самым вершину стека.

Рассмотрим пример. Состояние стека на данный момент приведено на рис. 5.1.

 

00

 

00

 

00

 

00

 

00

 

00

sp à

43h

 

15h

Рис. 5.1

Пусть в данный момент содержимое bx = 35c7h, а содержимое ax = 2233h и выполняется последовательность команд:

push bx

push ax

pop bx.

Состояние стека и регистров после выполнения каждой команды показано соответственно на рис. 5.2‑5.4.

 

00

 

00

 

00

 

00

sp à

с7h

 

35h

 

43h

 

15h

bx = 35c7h     ax = 2233h

Рис. 5.2

 

00

 

00

sp à

33h

 

22h

 

с7h

 

35h

 

43h

 

15h

bx = 35c7h      ax = 2233h

Рис. 5.3

 

00

 

00

 

33h

 

22h

sp à

с7h

 

35h

 

43h

 

15h

bx = 2233h     ax = 2233h

Рис. 5.4

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

- call ‑ вызов подпрограммы (запоминает в стеке адрес возврата);

- ret возврат из подпрограммы (берет из стека адрес возврата);

- int n ‑ программное прерывание (запоминает в стеке адрес возврата и регистр флагов);

- iret возврат из прерывания (выталкивает из стека адрес возврата и восстанавливает флаги).

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

Распространенная ошибка при написании стековых команд заключается в использовании восьмиразрядных регистров, что не допускается системой команд. Со стеком всегда обмениваются словами, а не байтами. Например, команда push cx вполне допустима, а на команде push cl транслятор выдаст сообщение об ошибке.

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

Работая со стеком, полезно помнить два простых правила.

1. Если решили сохранить содержимое какого-то регистра в стеке (командой push), а потом восстановить его (командой pop), но в промежутке между этими операциями вы также используете стековые команды, Вы обязаны помнить: число заталкиваний в стек на этом промежутке обязательно должно быть равно числу выталкиваний из стека (количество команд push должно быть равно количеству команд pop). Если это условие не соблюдается, наш регистр будет восстановлен неправильно.

2. Если последовательно затолкнули в стек содержимое ряда регистров, то восстанавливать эти регистры из стека надо в обратном порядке. Например:

push dx

push bx

push cx

.

.

pop cx

pop bx

pop dx

5.5. Сегментация памяти

Хотя сегментация памяти уже была описана в разд. 2.5, будет полезным еще раз рассмотреть этот механизм формирования адреса памяти.

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

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

Как уже говорилось выше, DOS работает с памятью, объемом 1 Мбайт. Однако наша программа не может загрузиться в любое место этого адресного пространства. Например, пространство памяти с адресами, превышающими 640 Кбайт, отведено для системных нужд. По этим адресам располагается видеопамять, ПЗУ BIOS, стартовое ПЗУ и так далее. Младшие адреса памяти тоже заняты. Там располагаются таблица прерываний, переменные DOS и BIOS, ядро самой DOS и различные драйверы. Такое распределение памяти показано на рис. 5.5.

0

?

Занятая

область

?

640Кайт

Свободная

память

1Мбайт

Занятая

область

Рис. 5.5

Программа может загрузиться только в свободную область памяти, причем DOS загрузит ее в самое начало этой области. Но какой адрес у этого «начала свободной области»? Ответить на этот вопрос невозможно. Все зависит от числа и объема драйверов, запущенных на конкретной ПЭВМ. DOS, конечно, знает, где в данный момент начинается свободная память, но когда мы пишем программу и когда транслятор ее транслирует ни нам, не транслятору эта информация неизвестна.

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

Для того чтобы DOS могла сообщить программе, начиная с какого адреса последняя загружена в память, в состав процессора введены сегментные регистры, в которые DOS и заносит соответствующую информацию. Таких регистров четыре (в современных процессорах шесть): cs, ss, ds и es. Принято говорить, что содержимое этих регистров задает начальные адреса четырех сегментов, с которыми работает процессор. Физический адрес любой ячейки памяти формируется как сумма начального адреса сегмента и внутрисегментного смещения. Последнее часто называют эффективным адресом (Аэф).

Для адресации 1Мбайта памяти адрес должен быть 20-разрядным, а сегментные регистры 16-разрядными. Поэтому процессор, определяя по содержимому сегментного регистра начальный адрес сегмента, дописывает к этому содержимому справа 4 двоичных нуля (умножает содержимое сегментного регистра на 16). Тогда начальный адрес любого сегмента всегда кратен шестнадцати (выровнен по границе параграфа). Итак, процессор всегда формирует 20-разрядный физический адрес по формуле

Аф = (sr)*16 + Аэф.

Здесь конструкция (sr) читается как «содержимое сегментного регистра». Например, пусть в каком-то сегментном регистре записано число 2234h и Аэф = 55d0h, тогда Аф = 2233h*16+55d0h = 2233h*10h+55d0h = 22330h + +55d0h = 27900h.

Необходимо понимать, что при обращении к конкретной ячейке памяти мы не можем в какой-либо команде сразу задать ее физический адрес. Всегда задаем этот адрес как пару (начальный адрес сегмента):(внутрисегментное смещение) или сокращенно сегмент:смещение. Например, надо обратиться к ячейке с адресом 03167h. Нам придется представить адрес этой ячейки в виде пары 0316:0007h (или 0310:0067h или…). Рассмотрим назначение конкретных сегментных регистров.

Сегментный регистр CS (code segment) – задает начальный адрес сегмента, в котором располагается программа, которую в данный момент выполняет процессор. Регистр cs совместно с регистром ip (instruction pointer), в котором задается смещение в кодовом сегменте, всегда определяют адрес следующей команды программы. То есть процессор, выбирая из памяти очередную команду, всегда формирует адрес этой команды по формуле

Аф = (cs)*16+(ip).

Сегментный регистр SS (stack segment) – задает начальный адрес сегмента, в котором располагается стек. Регистр ss, совместно с регистром sp (stack pointer), задающим смещение в сегменте стека, всегда определяют физический адрес вершины стека. То есть, при выполнении стековой операции (push, pop,…) процессор всегда формирует адрес памяти по формуле

Аф = (ss)*16+(sp).

Сегментный регистр DS (data segment) – задает начальный адрес текущего сегмента данных. Смещение в этом сегменте задает эффективный адрес, который процессор формирует по информации, заданной в текущей команде (той команде, которую процессор выполняет в данный момент). Например:

mov [2], bl ;команда записывает  в память ;содержимое регистра bl. ;Адрес памяти при этом формируется по ;формуле Аф = (ds)*16+2. (Аэф = 2).
mov ax, [bx+si–7] ;команда заносит считанное из памяти ;слово в регистр ax. Адрес памяти при этом формируется по формуле ;Аф = (ds)*16+(bx)+ (si)–7. То есть Аэф ;формируется как сумма содержимого ;регистров bx и si минус 7.
mov perem, 15 ;команда записывает число15 в ;переменную, названую программистом ;perem. ;Смещение для этой переменной (Аэф) ;подсчитает транслятор. Адрес памяти ;будет считаться по формуле ;Аф = (ds)*16+Аэф.

Ни в одной из трех приведенных выше команд регистр ds явно не указан, но именно этот сегментный регистр будет браться по умолчанию. Здесь, в отличие от выборки команд и стековых операций, у нас имеется возможность сменить сегментный регистр ds, прямо указав в команде другой сегментный регистр. Например:

 mov cs:[2], bl ;команда  записывает  в память ;содержимое регистра bl. Адрес памяти ;при этом формируется по формуле ;Аф = (cs)*16+2. (Аэф = 2).

У начинающего программиста на ассемблере вряд ли возникнет потребность менять содержимое сегментных регистров cs, ss и ds, переходя тем самым к новым сегментам кода, стека и (или) данных, а менять содержимое сегментного регистра es ему возможно придется.

Сегментный регистр ES (extra segment) – «дополнительный сегмент». Он используется, если хотим обратиться к памяти, расположенной за пределами текущего сегмента данных.

Рассмотрим пример. Видеопамять для текстового режима располагается с адреса b8000h. При этом байт, расположенный по этому адресу, содержит в себе ASCII-код символа, который высвечивается в левом верхнем углу экрана, а байт по адресу b8001h – атрибуты этого символа (цвет символа и цвет фона). Допустим, хотим вывести в левом верхнем углу экрана черным цветом на белом фоне букву «Ф». Это можно сделать следующим образом:

mov ax, 0b800h

;мысленно разбили Аф = b8000h на ;пару b800: 0000h. Теперь хотим b800 ;занести в сегментный регистр es. ;Сразу это сделать невозможно (нет ;таких команд). Приходится это делать ;через какой-либо 16-разрядный ;регистр. Мы выбрали регистр ах. ;Итак, эта команда загружает в ах ;число b800h.

mov es, ax

;эта команда переписывает ;содержимое ах в es.

mov es:[0], ‘Ф’

;эта команда загружает ASCII-код ;буквы «Ф» в память по адресу b8000h. ;Аф = (es)*16+0 = b800h*10h+0 = ; = b8000h. ASCII-код подставит в ;команду транслятор вместо ;конструкции ‘Ф’.

mov es:[1], 70h

;эта команда заносит атрибуты по ;адресу b8001h. Аф = (es)*16+1 = ; = b800h*10h+1 = b8001h. ;70h – «выводить черным по белому».

5.6. Система команд

Под программной моделью понимается набор внутренних регистров и флагов процессора, которые доступны программисту. Будем использовать программную модель процессора 8086, которая является базовой для всех микропроцессоров фирмы Intel, вплоть до процессора Pentium IV. Эта модель была описана ранее в разд. 2.6

Современные процессоры фирмы Intel имеют развитую систему машинных команд. Книга с описанием всех ассемблерных команд имеет объем более 1000 листов, но начинающему программисту в его программах понадобится от силы 10 – 15 машинных команд. Ни один программист никогда не помнит все эти машинные команды наизусть. Опытный программист просто помнит, «что такая команда есть» и, когда она ему понадобится, обращается к справочнику. Остановимся только на командах, с которыми сразу столкнется в своей работе новичок.

Команда MOV-приемник, источник.

Команда передает содержимое источника в приемник. В качестве источника могут выступать регистр, ячейка памяти и непосредственный операнд (передается число, непосредственно заданное в команде). Приемником могут быть регистр или ячейка памяти. Например:

mov cx, 0b800h

;команда загружает в cх число b800h

mov al, ah

;эта команда переписывает ;содержимое аh в al.

mov perem, si

;эта команда загружает в переменную, ;которую программист назвал perem, ;содержимое регистра si.

mov bp, [bx+4]

;эта команда загружает в регистр bp ;слово из ячейки памяти с адресом ;Аф = (ds)*16+(bx)+4.

Важным является следующий момент: в качестве источника и приемника в одной команде не могут одновременно выступать две ячейки памяти!! То есть команда mov perem, [bx+4] заставит транслятор сформировать сообщение об ошибке. Правильно надо было писать, например, так:

mov ax, [bx + 4]

mov perem, ax.

Отметим также, что все, что сказано выше об источнике и приемнике, справедливо и для всех остальных команд процессора. Приведем еще один пример:

mov [si], 7      ;команда заносит в память по адресу ;Аф = (ds)*16+(si) число 7.

Синтаксически команда написана правильно, а транслятор выдает предупреждение: Argument needs type override. Дело в том, что транслятор не может по такой записи понять, что надо передавать в память ‑ байт или слово? Он может сформировать неверный код операции. О каком формате числа идет речь в такой команде, транслятору должен сообщить программист, написав:

mov byte ptr [si], 7  ;(указатель на байт) речь идет о байте.

mov word ptr [si], 7 ;речь идет о слове.

Команды INC-приемник и DEC-приемник.

Команда inc (инкремент) прибавляет единицу к содержимому приемника. Команда dec (декремент) вычитает единицу из содержимого приемника. Например:

inc cl      ;содержимое регистра cl увеличивается на ;единицу;

dec di     ;содержимое регистра di уменьшается на ;единицу;

inc word ptr [bx]

dec perem.

Команды ADD-приемник, источник и SUB-приемник, источник.

Команда add прибавляет содержимое источника к содержимому приемника, результат заносится в приемник. Команда sub вычитает содержимое источника из содержимого приемника, результат заносится в приемник. Например:

add ah, 32 ;прибавить 32 к содержимому регистра ah

sub dl, ch ;вычесть содержимое ch из содержимого dl ;(результат в dl)

sub perem, bx

add ax, [di]

add byte ptr [bx+si+2], 3.

Команда CMP-приемник, источник.

Команда cmp (сравнение) вычитает содержимое источника из содержимого приемника, но, в отличие от команды sub, результат вычитания никуда не заносится. Результатом работы команды cmp является установка соответствующих флагов в регистре флагов. Команда cmp всегда используется в паре с одной из команд «условного перехода» (je-метка – «перейти, если равно», jne-метка – «перейти, если не равно» и др.). Например:

cmp al, 0

je m1

cmp ax, bx

jne not_equal

cmp byte ptr [si – 14], 0ffh

je exit.

Команда безусловного перехода JMP-метка.

Команда осуществляет безусловный переход на указанную метку. Вместо метки транслятор при трансляции подставит необходимое смещение (число). В качестве метки можно использовать выражение, начинающееся не с цифры. Чтобы транслятор понял, что это метка, после метки ставится двоеточие (не в команде!!). Например:

 _m1:     cmp ah, 3   ; в ah тройка?

jne _m2      ;если нет, прыгаем на _m2.

jmp _m1     ;прыгаем на _m1_m2:

add bx, 32

Команды условных переходов:

je-метка        ;переход, если равно;

jz-метка        ;переход, если результат равен нулю (флаг ;zf установлен в единицу). Собственно это ;другая запись команды je

jne-метка     ;переход, если не равно (эквивалентная ;команда jnz);

ja-метка        ;переход, если больше;

jae-метка      ;переход, если больше или равно;

jb-метка        ;переход, если меньше;

jbe-метка      ;переход если меньше или равно.

Например:

sub ax, 40       ;вычитаем из ax 40

jnz m17              ;если результат не равен нулю, ; прыгаем на m17

cmp al, bh

jae povtor       ;если содержимое al больше или равно ;содержимому bh, прыгаем на povtor.

Достаточно часто приходится сталкиваться со случаем, когда на синтаксически правильной команде условного перехода транслятор выдает ошибку: Relative jump  out of range. Связано это с тем, что команда условного перехода может обеспечить прыжок только на плюс/минус 128 байт, т.е. приблизительно на 30 – 40 команд (вперед или назад по программе). А если надо прыгнуть на большее расстояние? Применяют команды безусловного перехода (jmp), обеспечивающие прыжок на плюс/минус 64 Кбайта. Например, рассмотрим фрагмент программы:

cmp ax, 0        ;в ax ноль?

je m100           ;если да, прыгаем на m100, если нет, ;идем на следующую команду

mov bx, 40

На команде je m100 транслятор выдает вышеуказанную ошибку. Перепишем этот фрагмент:

cmp ax, 0        ;в ax ноль?

jne m200         ;если нет, прыгаем на m200, если да, ;идем на следующую команду

jmp m100        ;прыгаем на m100

m200:  mov bx, 40

Логика программы не изменилась, а вот ошибки больше не будет.

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

Команда LOOP-метка.

Команда loop (цикл) вычитает единицу из содержимого регистра cx и, если в результате получился «не ноль», переходит на указанную метку. В качестве примера рассмотрим следующий фрагмент:

mov dh, 0

mov cx, 11      ;число повторений цикла

m1:

inc dh

loop m1

mov al, dh.

Данный фрагмент выполняется следующим образом: сначала в dh загружается 0. Затем в цикле к dh 11 раз прибавляется единица. В результате этого фрагмента мы будем иметь: cx = 0, dh = 11, al = 11. Конечно, тех же результатов можно было бы достичь проще:

mov cx, 0

mov dh, 11

mov al, dh,

но здесь нет цикла.

Распространенной ошибкой, приводящей к самым плачевным последствиям, является написание бесконечного цикла. Например, следующий фрагмент приведет к зависанию программы:

mov dh, 0

m1:

mov cx, 11

inc dh

loop m1.

В cx занесется 11, команда loop вычтет из cx единицу, получится «не ноль» и произойдет переход на метку m1, В cx снова занесется 11 и так до бесконечности. Метка m1 поставлена не там, где нужно (правильный вариант смотри выше).

Еще одной менее очевидной, но не менее неприятной по последствиям, ошибкой является занесение внутри цикла (по забывчивости программиста) в регистр cx новой информации, которая портит текущее значение счетчика цикла. Если же изменение cx внутри цикла нам «жизненно необходимо», то надо предварительно запомнить текущее содержимое cx (например, в стеке командой push cx), а затем восстановить это содержимое (pop cx) перед выполнением команды loop.

Команды IN al, адрес порта и OUT адрес порта, al.

Команда in передает байт из заданного в команде порта в регистр al. Команда out передает байт из регистра al в заданный в команде порт. В качестве адреса порта может выступать любое число, лежащее в диапазоне 0 – 255 (0 –ffh). Порт – это регистр, которому в системе присвоен адрес. Например, контроллер клавиатуры имеет 2 порта с адресами 20h и 21h, таймер – 4 порта с адресами 40h, 41h, 42h и 43h и.т.д. Приведем примеры команд:

in al, 60h         ;читаем скэн-код нажатой клавиши из ;порта клавиатуры

out 40h, al      ;заносим байт коэффициента пересчета ;в 0-й канал таймера.

Обратите внимание, что обмен информацией с портами ведется только через регистр al (это не совсем правильно, поскольку имеются и другие варианты команд in и out, но для начинающего программиста проще использовать только рассмотренные выше команды).

Команда AND-приемник, источник.

Команда and (логическое И) производит поразрядное логическое умножение содержимого приемника на содержимое источника. Результат заносится в приемник. Например:

Источник:

&

10011101

Приемник:

01111010

Результат:

00011000

Команда and часто используется, когда надо сбросить в ноль конкретный бит (биты) в байте или слове, не меняя значение других битов этого байта (слова). Приведем пример:

in al, 61h         ;читаем 61 порт

and  al, 11111100b       ;обнуляем два младших бита

out 61h, al       ;записываем обратно в 61 порт.

Эти три команды запрещают звучание встроенного динамика (спикера). Сначала считываем содержимое порта 61h в регистр al. Затем обнуляем два младших бита al (запрещаем звук). При этом все остальные биты оставляем в их исходном состоянии, чтобы ненароком не нарушить работу системы. После этого отправляем измененную информацию обратно в порт 61h.

Команда and также часто используется, когда надо проверить значение конкретного бита в байте или слове. Например, надо проверить, установлен ли 1-й бит регистра al  в единицу. Эту проверку можно организовать так:

and al, 00000010b

jnz m99.

Если в 1-м бите стоял 0, в результате выполнения первой команды получится ноль. Вторая команда совершает прыжок на m99, если результатом первой команды был «не ноль», то есть если 1-й бит был установлен в единицу. Недостаток такой проверки – после нее содержимое al  будет испорчено.

Команда OR-приемник, источник.

Команда or (логическое ИЛИ) производит поразрядное логическое сложение содержимого источника и содержимого приемника. Результат заносится в приемник. Например:

Источник:

V

10011101

Приемник:

01111000

Результат:

11111101

Команда or часто используется, когда надо установить в единицу конкретный бит (биты) в байте или слове, не меняя значение других битов этого байта (слова). В качестве примера приведем последовательность команд, разрешающих звучание встроенного динамика:

in al, 61h

or al, 00000011b

out 61h, al.

Команда XOR-приемник, источник.

Команда xor (исключающее ИЛИ) производит поразрядное сложение по модулю 2 содержимого приемника и содержимого источника. Результат заносится в приемник. Например:

Источник:

= 1

10011101

Приемник:

01111000

Результат:

11100101

Команда xor часто используется, если надо инвертировать значение какого-либо бита (битов) в байте или слове. Например:

xor al, 11100000b

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

xor ax, ax       ;после этого в ax будет 0.

Команда LEA-регистр, имя переменной.

Команда загружает в указанный в команде регистр эффективный адрес указанной в команде переменной (т.е. смещение этой переменной относительно начала сегмента). Например:

lea bx, perem      ;после этого в bx адрес perem.

Команда lea имеет ассемблерный эквивалент. Приведем команду, эквивалентную рассмотренной в примере:

mov bx, offset perem        ;после этого в bx адрес perem.

В дальнейшем будем использовать второй вариант записи. Он ничем не лучше первого, но более нагляден.

Команда DIV-регистр.

Команда div (деление) делит содержимое регистра ax на содержимое указанного в команде 8-разрядного регистра. Результат возвращается в al (частное) и в ah (остаток). Если в команде div  указан 16-разрядный регистр, то на его содержимое делится не ax, а регистровая пара dx:ax (старшие 2 байта в dx). Соответственно частное возвращается в ax, а остаток в dx. Например:

div cl          ;ax/cl , частное в al, остаток в ah

div bx         ;dx:ax/bx, частное в ax, остаток в dx.

Будем использовать эту команду, например, для перевода чисел из двоичной системы в десятичную.

Команда div таит в себе одну опасность. Если частное не помещается в отведенный для него регистр, происходит прерывание «Divided overflow». Например, если программа содержит такой фрагмент:

mov ax,1000

mov cl, 1

div cl,

то программа выполняться не будет, зато на экране появится надпись «Divided overflow». Связано это с тем, что при делении 1000 на 1 частное, равное 1000, не может поместиться в 8-разрядный регистр al, поскольку в al можно поместить максимум 255.

Команда INT-число.

Команда int n – программное прерывание (n – число, лежащее в диапазоне 0-255 или 0-ffh). С помощью таких команд программист вызывает сервисные подпрограммы DOS и BIOS. Эти подпрограммы принимают информацию с клавиатуры, выводят информацию на экран, работают с дисками, распределяют память и т.д. Параметры, передаваемые в подпрограмму, задаются перед вызовом int n в заранее оговоренных регистрах. Кроме того, поскольку одна и та же подпрограмма, задаваемая числом n  в команде int, часто выполняет целый набор различных сервисных функций, номер конкретной запрашиваемой функции задается перед вызовом в регистре ah. Если такая подпрограмма возвращает результаты, то они возвращаются в заранее оговоренных регистрах. Например:

mov ah, 0eh

mov al, ‘A

int 10h.

Это прерывание BIOS c номером 10h, функция 0eh. Это прерывание выводит на экран в текущую позицию курсора  символ, ASCII-код которого задан в регистре al. В нашем случае на экран выведется буква А. Никаких результатов в этом случае подпрограмма не возвращает. Второй пример:

mov ah, 7

int 21h.

Это 21-е (DOS) прерывание, функция 7. Программа в этом месте останавливается и ждет нажатия клавиши на клавиатуре. После того как клавиша нажата, ее код возвращается в регистре al.

Основные ошибки, которые допускает программист при использовании команд int:

- забыл поставить букву h в номере прерывания, в результате это оказалось совсем другое прерывание, выполняющее совсем другие функции;

- входные параметры заданы либо неправильно, либо не в тех регистрах.

Команды PUSH-регистр и POP-регистр.

Команда push заталкивает в стек содержимое регистра, а команда pop выталкивает в регистр информацию из вершины стека. Еще раз подчеркнем, что в этих командах недопустимы 8-разрядные регистры.

Команды SHR регистр, число и SHL-регистр, число.

Эти команды сдвигают содержимое указанного регистра вправо (shr) и влево (shl). Число, указанное в команд,е задает количество сдвигов (на сколько разрядов сдвигать).

5.7. Выделение памяти под переменные и массивы

Как правило, программист, пишущий на Ассемблере первую программу, стремится работать только с регистрами. Однако довольно быстро он понимает, что регистров мало и под хранение некоторых данных надо использовать память. Вот тут у него и возникает вопрос: а как задать переменную в памяти и как потом с этой переменной работать? Задать переменную очень просто:

perem db 73.

Мы задали переменную, назвав ее именем perem, попросили транслятор выделить для этой переменной байт памяти (директива db) и загрузить в этот байт число 73. Исходное значение переменной – 73, по ходу работы программы это значение может меняться. Кроме директивы db (defined byte) есть еще директивы dw (defined word) (выделить в памяти слово), dd (defined double word) (выделить двойное слово) и другие. Например:

cursor db 12

x1 dw  542h

counter dd  37*14.

Переменные заданы и работать с ними несложно:

mov ax, x1      ;записываем в ах значение переменной х1

add cursor, 2       ;прибавляем двойку к значению ;переменной cursor

mov bl, byte ptr counter+3        ;записываем в bl значение ;старшего (третьего) байта ;переменной counter.

Последняя команда требует развернутого комментария. Переменная counter описана как двойное слово (4 байта), а регистр bl имеет формат 1 байт. Для того чтобы транслятор не выдавал предупреждения о «несогласованности типов операндов», а понял, что программист знает, чего он хочет, вставлена директива byte ptr. Если в команде было написано не counter + 3, а counter, то обратились бы не к третьему, а к нулевому (младшему) байту двойного слова.

Совершенно аналогично задаются массивы:

note db 40, 35, 27, 90   ;задали массив из четырех чисел.

Обратиться, например, ко второму элементу нашего массива (число 27) можно одним из следующих способов:

mov dh, note+2

или

mov si, offset note       ;после этого в si смещение ;нулевого элемента массива

mov dh, [si+2]

или

mov si, offset note

add si, 2          ;теперь в si смещение второго элемента ;массива

mov dh, [si].

Недостаток (или достоинство?) последнего варианта в том, что мы портим si и транслятор указывает не на начало массива. Отметим также, что +2 во всех трех вариантах означает «смещение на два байта относительно начала массива» и далеко не всегда является смещением второго элемента массива. Так, например, если задан массив слов:

sl dw 707, 12, 421, 0, 1234,

то для того чтобы обратиться к его второму элементу (число 421), надо написать, допустим, так:

mov ax, sl+4        ;начало массива плюс 4 байта.

Можно задавать и символьные массивы. Например:

strochka db ‘Это строка символов’.

Транслятор сам переведет все символы, записанные между апострофами, в их ASCII-коды и получим массив из 19 байт (17 букв и 2 пробела). Иногда нужно задать большой массив, например, состоящий из 1000 элементов, причем исходное состояние элементов не важно и всем элементам можно присвоить значение 0 (или 117, или 66, …). Такая ситуация возникает, когда надо создать в памяти буфер, в который затем будет записываться информация из какого-то файла. Не писать же тысячу нулей через запятую. На этот случай имеется директива dup (повторять):

mass dw  1000 dup (117)       ;то что повторяется задается ;в скобках.

Можно создавать и более сложные конструкции:

out_string db ‘nomber of files = ’, 4 dup (?), 10,13,’$’.

Это заготовка некой строки для вывода на экран. 4 dup (?) – выделяет 4 байта, в которые программа запишет ASCII-коды цифр, из которых состоит найденное «число файлов» (вместо ? можно было написать 0, результат был бы тем же). 10 и 13 – управляющие коды, переводящие курсор в начало новой строки экрана. ‘$’ – означает конец выводимой строки. В заключение приведем пример массива, при задании которого часто встречаются ошибки:

pause db 11, 22, 33, 44, 55, 66, 77, 88

db 99, 100, 32, 0.

Если продолжаем массив на новой строчке, то надо начать эту строчку с директивы (db), а на предыдущей строчке после последнего элемента запятая не ставится!

5.8. EXE- и COM-программы

Любая программа транслируется в исполняемый файл, имеющий либо EXE, либо COM формат.

В EXE-программе создаются отдельные сегменты для кода, для данных и для стека, причем, если программа большая, то сегментов для данных и для кода может быть даже несколько. Все эти сегменты так или иначе описывает программист при написании программы. Сегмент стека в принципе можно не описывать (не создавать), в этом случае программа будет работать со стеком DOS, хотя tlink при трансляции выдаст предупреждение (Warning: no stack), которое можно проигнорировать.

В COM-программе для кода, данных и стека отводится один общий сегмент. То есть COM-формат используется при написании программ небольшого объема, полностью помещающихся в 64 Кбайта памяти. В дальнейшем, в основном, будем придерживаться именно этого формата.

Загружая в память на исполнение как COM-, так и EXE-программу, DOS, помимо самой программы, размещает в памяти еще и так называемый «префикс программного сегмента» (PSP). Занимает PSP 256 байтов памяти и в него DOS записывает различную служебную информацию. Для EXE-программ DOS сама выделяет память под PSP, т.е. программист об этом заботиться не должен. А вот для COM-программ ситуация иная, здесь программист сам должен выделить память под PSP с помощью директивы org 100h (или org 256). Ситуация в памяти после загрузки на исполнение EXE-программы показана на рис. 5.6.

ds; es à

 
 

PSP

ss à

 
 

Сегмент стека

ss:sp à

 
 

Сегмент данных

cs:ip à

 
 

Программа

(сегмент кода)

Рис. 5.6

Как видно из рис. 5.6, сегментный регистр ds сразу после загрузки указывает «не туда, куда надо» (ds указывает на первый байт PSP, а должен указывать на первый байт сегмента данных). Именно поэтому в начале EXE-программы ds нужно настроить надлежащим образом.

Стандартно EXE-программа пишется следующим образом:

stack segment  stack        ;описание сегмента стека

;Первое слово stack – название сегмента, можно было ;назвать сегмент и по-другому, например, vasya.

;Транслятор предупредит, что используем зарезервированное слово stack не по назначению. Это предупреждение ;можно проигнорировать. Второе слово stack говорит ;транслятору, а потом и DOS, что это сегмент стека.

db 100 dup (0)          ;выделяем под стек 100 байт (можно ;больше, можно меньше)

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

data segment           ;описание сегмента данных

; Здесь располагаются переменные и массивы

data ends

code segment           ;описание сегмента кода

assume cs:code, ds:data

;Это директива для транслятора. Объяснять ее назначение ;долго и сложно. Проще поверить, что она необходима

start:     ;программа начинается с метки (любой!)

mov ax, data

mov ds, ax      ;настраиваем ds на начало сегмента ;данных

;здесь пишется программа         

mov ah, 4ch

int 21h        ;стандартный выход из программы

code ends

end start         ;startметка, с которой мы начали ;программу.

Дадим необходимые пояснения. Все, что находится в строке программы после «точки с запятой», транслятор понимает как комментарий. Сегменты stack, data и code можно было назвать и другими именами, но тогда мы должны использовать в директивах ends и assume и команде mov ax, data эти другие имена. Изменять второе слово stack нельзя, тогда у нас не будет создан сегмент стека. Причины настройки ds на начало сегмента данных объяснялись в этом разделе выше. Остается выяснить, зачем нужен стандартный выход из программы.

Когда процессор выполняет программу, он «понятия не имеет», где она кончается. Он просто выбирает команды из памяти и их выполняет. Выполнив последнюю команду программы, процессор продолжит выбирать информацию из памяти и будет пытаться выполнять эту информацию в качестве команд. Поскольку после последней команды программы в памяти, скорее всего, «мусор», такая ситуация почти наверняка приведет к зависанию системы. Поэтому в конце программы организуется корректный возврат управления в ту среду (DOS NAVIGATOR или Far или ….), из которой была запущена наша программа.

Ситуация в памяти после запуска СОМ-программы представлена на рис. 5.7.

cs; es; ds; ss à

 
 

PSP

cs:ip à

 
 

Программа

ss:sp à

 

Рис. 5.7

Как видно из рис. 5.7, DOS настраивает все сегментные регистры на первый байт PSP. В регистр ip программист, выделяя директивой org 100h место под PSP, заносит 100h. После этого пара cs:ip указывает на первую команду программы. В указатель стека sp DOS загрузит число fffeh.

После этого пара ss:sp  указывает на ячейку, отстоящую почти на 64 Кайта от начала PSP, с тем, чтобы стек располагался как можно дальше от программы и данных и не мог их испортить при своем росте в сторону младших адресов.

Стандартно СОМ-программа пишется таким образом:

code segment

assume cs: code, ds: code

org 100h

start:

jmp begin        ; перепрыгиваем область данных

;здесь можно располагать переменные и массивы

begin:

; здесь располагается программа

mov ah, 4ch            

int 21h                ; стандартный выход из программы

;здесь можно располагать переменные и массивы

code ends

end start

Дадим необходимые пояснения. Здесь описывается только один сегмент. Поскольку внутри этого сегмента располагаются и данные и программа, то программист обязан не допускать ситуации, в которой процессор начал бы выполнять данные как команды. Поэтому переменные и массивы либо перепрыгивают командой jmp, либо располагают после выхода из программы.

5.9. Трансляция ассемблерной программы

Пусть написана ассемблерная программа, которую назвали, например, temp.asm. Теперь эту программу нужно оттранслировать с помощью tasm.exe и получить из нее файл temp.obj. Затем, с помощью программы tlink.exe, файл temp.obj преобразуется в исполняемый файл temp.exe (или temp.com). Для того чтобы получить файл temp.obj, надо набрать в командной строке:

tasm.exe temp. asm     (можно и проще: tasm temp)

и нажать ENTER. При этом tasm.exe и temp.asm должны быть, допустим, в той директории, где сейчас работаем (доступны компьютеру). Транслятор может и не создать файл temp.obj, если он найдет в программе ошибки.

Далее надо получить исполняемый файл. Для создания EXE-программы надо набрать в командной строке:

     tlink.exe temp.obj   (или tlink temp),

а для COM-программы:

tlink.exe temp.obj /t  (или tlink temp /t).

Очень удобно создать bat-файл, поскольку при отладке программу постоянно приходится транслировать заново. Работа tlink.exe  также не всегда завершается созданием исполняемого файла. Это бывает, если tlink.exe нашел в программе ошибки.

Приведем пример трансляции.

code segment                         ; (строка 1)

assume cs:code, ds:code       ; (строка 2)

; пропущена директива org 100h

start:                                       ; (строка 4)

jmp begin                                ; (строка 5)

mad db 100, 99, 98,               ; (строка 7)  лишняя запятая ;после 98

db  97, 96                           ; (строка 8)

nic dw 380                              ; (строка 10)

begin                                       ; (строка 12) пропущено ;двоеточие после begin

mov ah. 7                               ; (строка 14) вместо точки ;нужна запятая

mov al, nic                         ; (строка 15) не согласована ;разрядность регистра и операнда

int21h                                 ; (строка 16) пропущен пробел

mov si, offset madd          ; (строка 17) неправильно ;написано имя массива

mov [si+3], 5                  ; (строка 18) не определен формат ;записываемой информации

mov ah, 4ch

int 21h

code ends

end start

Итак, написана программа (для COM-формата) и названа temp.asm. Эта программа не имеет никакого смысла, так как не выполняет никакой полезной работы. Зато в программе намеренно сделаны ошибки, каждая из которых описана в той же строке, где она находится. Теперь мы создали файл temp.bat:

tasm temp

tlink temp /t

и запустили его. Вот что получим:

Assembling file:   temp.ASM

**Error** temp.ASM(5) Undefined symbol: BEGIN

*Warning* temp.ASM(7) Missing operand - trailing ? assumed

**Error** temp.ASM(12) Illegal instruction

**Error** temp.ASM(14) Too few operands to instruction

**Error** temp.ASM(15) Operand types do not match

**Error** temp.ASM(16) Illegal instruction

**Error** temp.ASM(17) Undefined symbol: MADD

*Warning* temp.ASM(18) Argument needs type override

Error messages:    6

Warning messages:  2

Passes:            1

Remaining memory:  381k

D:\MY_DIR\ASSEMBL>d:\my_dir\assembl\tlink temp /t

Turbo Link  Version 7.00 Copyright (c) 1987, 1994 Borland International

Fatal: Unable to open file 'temp.obj'.

Обратите внимание, что транслятор для каждой ошибки написал в скобках номер строки, в которой он эту ошибку нашел. Так что обычно ошибка легко находится программистом. ERROR – это ошибки, их надо исправлять, WARNINGпредупреждения, довольно часто (но не всегда!) их можно игнорировать. Объектный файл из-за ошибок создан не был, поэтому tlink выдал сообщения о фатальной ошибке.

Исправим в программе все ошибки, кроме одной:

code segment                    ; (строка 1)

assume cs:code, ds:code  ; (строка 2)

; пропущена директива org 100h

start:                                  ; (строка 4)

jmp begin                           ; (строка 5)

mad db 100, 99, 98            ; (строка 7)

db  97, 96                 ; (строка 8)

nic dw 380                          ; (строка 10)

begin:                                 ; (строка 12)

mov ah, 7                           ; (строка 14)

mov  al, byte ptr nic     ; (строка 15)

int 21h                                ; (строка 16)

mov si, offset mad             ; (строка 17)

mov byte ptr [si+3], 5       ; (строка 18)

mov ah, 4ch

int 21h

code ends

end start

и вновь запустим наш bat-файл. Теперь получим:

Assembling file:   temp.ASM

Error messages:    None

Warning messages:  None

Passes:            1

Remaining memory:  381k

D:\MY_DIR\ASSEMBL>d:\my_dir\assembl\tlink temp /t

Turbo Link  Version 7.00 Copyright (c) 1987, 1994 Borland International

Fatal: Cannot generate COM file : invalid initial entry point address.

Теперь tasm «доволен всем» и объектный файл создан, а вот tlink отказывается генерировать СОМ-файл. Если теперь исправим оставшуюся ошибку (вставим org 100h), то после трансляции будем иметь сообщение:

Assembling file:   temp.ASM

Error messages:    None

Warning messages:  None

Passes:            1

Remaining memory:  381k

D:\MY_DIR\ASSEMBL>d:\my_dir\assembl\tlink temp /t

Turbo Link  Version 7.00 Copyright (c) 1987, 1994 Borland International

а наш СОМ-файл будет создан.

В качестве первого примера программы на любом языке обычно используется программа, выводящая на экран надпись: HELLO, WORLD! Давайте напишем такую же программу, но несколько ее усложним.

Итак, вот что будет делать наша программа:

- очищать экран (белым цветом);

- устанавливать курсор так, чтобы надпись выводилась в центр экрана;

- выводить надпись в позицию курсора (черным цветом);

- ждать нажатия клавиши. Если нажата одна из клавиш b (blue), g (green) или r (red), цвет надписи меняется соответственно на синий, зеленый или красный. Если нажата клавиша ESC, программа завершает свою работу.

Приведем вначале текст программы:

; программа написана в СОМ-формате

code segment

assume cs:code, ds:code

org 100h

start:

jmp begin

message db 'HELLO, WORLD!'       ; надпись, которую ;мы будем выводить

attributes db 70h     ;исходные атрибуты экрана и ;надписи: экран – белый, надпись ‑ ;черная

curs_x  db 35           ; координата Х курсора (№ столбца)

curs_y  db 12           ; координата Y курсора (№ строки)

begin:             

; чистим экран

mov ch, 0        ; координата Y левого верхнего угла

;экрана (№ строки)

mov cl, 0              ; координата Х левого верхнего угла ;экрана (№ столбца)

mov dh, 24           ; координата Y правого нижнего угла ;экрана  (№ строки)  

mov dl, 79            ; координата Х правого нижнего угла ;экрана  (№ столбца)

mov al, 0              ; чистить всю заданную область (т.е., у ;нас весь экран)

mov bh, 70h    ; атрибуты (черный символ на белом фоне)

mov ah, 6        ; функция

int 10h

m4:                 

; выводим надпись

mov di, 13                          ; всего выводим 13 символов

mov si, offset message     ; в si адрес первой буквы ;надписи

m1:                               

;позиционируем курсор

mov dh, curs_y             ; в dh  строка

mov dl, curs_x              ; в dl  столбец

mov bh, 0                      ; в bh  № видеостраницы (для ;начинающих всегда 0)

mov ah, 2                      ; функция

int 10h             ; собственно вывод надписи. Курсор при

;таком выводе не сдвигается

mov ah, 9                      ; функция 

mov bl, attributes         ; атрибуты     

mov cx, 1                      ; коэффициент повторения ;(сколько раз выводить символ)  

mov al, [si]               ; в al выводимый (на экран в ;позицию курсора) символ    

int 10h

inc si                         ; в si адрес следующей буквы надписи  

inc curs_x                ; сдвигаем координату Х на ;одну ;позицию вправо

dec di                        ; уменьшаем di на единицу

jnz m1                       ; если в di «не ноль», идем на

;переустановку курсора и вывод

;следующей буквы надписи

mov curs_x, 35    ; восстанавливаем исходную координату

;курсора по Х  

mov ah, 0        ; функция

int 16h             ; здесь «висим» и ждем нажатия клавиши.

;Когда клавиша нажата, то ее скэн-код

;вернется в ah, а ASCII-код – в al.

cmp al, 1bh     ; это ESC? (1bh - ASCII-код клавиши ESC)

je exit                  ; если «да», идем на выход, «нет» -

;проверяем дальше

cmp al, 'r'                      ; это r?  

jne m2                           ; «нет» ‑ проверяем дальше

mov attributes, 74h ; «да» ‑меняем атрибуты 

jmp m4                          ; и идем на вывод надписи сначала

;Старую надпись не стираем, ;просто пишем ;поверх нее новую ;надпись с другими атрибутами

m2:

cmp al, 'g'                 ; далее аналогично

jne m3

mov attributes, 72h

jmp m4

m3:

cmp al, 'b'

jne m4                      ; это вообще «не наша» клавиша, ее

;проигнорируем  

mov attributes, 71h

jmp m4                

;выход из программы. Сюда попадем, только если ;нажмем ESC

exit:

mov ah, 4ch

int 21h

code ends

end start.

Не надо думать, что это оптимальная программа. Давайте, для примера, напишем другой вариант этой программы, теперь в EXE-формате:

stack segment stack

db 100 dup (0)

stack ends

data segment

message db 'HELLO, WORLD!$' ; надпись, ;которую

;мы будем выводить

attributes db 70h     ; исходные атрибуты экрана и

;надписи

data ends

code segment

assume cs:code, ds:data

start:

mov ax, data

mov ds, ax           ; чистим экран

m4:

mov ch, 0             ; координата Y левого верхнего угла

;экрана (№ строки)

mov cl, 0              ; координата Х левого верхнего угла

;экрана (№ столбца)

mov dh, 24           ; координата Y правого нижнего угла

;экрана  (№ строки)

mov dl, 79            ; координата Х правого нижнего угла

;экрана  (№ столбца)

mov al, 0              ; чистить всю заданную область (т.е.

;весь экран)

mov bh, attributes        ; текущие атрибуты

mov ah, 6                      ; функция

int 10h

;позиционируем курсор

mov dh, 12               ; в dh  строка

mov dl, 35                ; в dl  столбец

mov bh, 0                 ; в bh  № видеостраницы (для нас ;начинающих всегда 0)

mov ah, 2                 ; функция

int 10h                      ; выводим надпись

mov ah, 9                 ; функция

mov dx, offset message    ; в dx  адрес первой буквы

;надписи

int 21h                                ; выводим все до доллара

mov ah, 0             ; функция

int 16h                 ; здесь «висим» и ждем нажатия

;клавиши. Когда клавиша нажата, то ее ;скэн-код вернется в ah, а ASCII-код – в al.

cmp al, 1bh          ; это ESC? (1bh ‑ ASCII-код клавиши

;ESC)

je exit                  ; если «да», идем на выход. «нет» ‑

;проверяем дальше   

cmp al, 'r'            ; это r?  

jne m2                           ; «нет» ‑ проверяем дальше

mov attributes, 74h ; «да» ‑ меняем атрибуты 

jmp m4                          ; и идем на новую очистку экрана,

;установку курсора и вывод надписи

m2:

cmp al, 'g'            ; далее аналогично

jne m3

mov attributes, 72h

jmp m4

m3:

cmp al, 'b'

jne m4                  ; это вообще «не наша» клавиша, мы ее

;проигнорируем  

mov attributes, 71h

jmp m4           

; выход из программы. Сюда попадем, если нажмем ;клавишу ESC

exit:

mov ah, 4ch

int 21h

code ends

end start

Программа получилась проще. При каждом нажатии любой клавиши (кроме ESC) заново очищаем экран с текущими атрибутами, а потом выводим на него надпись, начиная с позиции курсора, который все время устанавливаем в одно и то же место. Недостаток этой программы заключается в постоянной очистке экрана. На медленных компьютерах человеку будет видно мерцание экрана.

5.10. Работа с клавиатурой и экраном

Информация, которую компьютер выводит на экран, располагается в определенной области памяти. В стандартном текстовом режиме эта область начинается с адреса b8000h. В этом режиме экранная область памяти разбита на несколько страниц. В любой момент времени на экран выводится содержимое одной из этих страниц, она называется активной. Таким образом, программист может заранее подготовить информацию в пассивной странице, а затем быстро сделать эту страницу активной. Но не будем заниматься этим, а будем всегда работать только с нулевой страницей. В рассматриваемом текстовом режиме экран имеет 25 строк (нумерация 0 – 24) и 80 столбцов (0 – 79). Таким образом, для описания текущего состояния экрана в памяти отводится 25×80 = 2000 слов. Каждое из этих слов описывает одно знакоместо. В младшем байте такого слова содержится ASCII-код выводимого символа, а в старшем байте – атрибуты, с которыми этот символ выводится на экран (цвет символа и цвет фона в знакоместе). Байт атрибутов имеет формат, представленный на рис. 5.8 (подробней см. Приложение).

D7

D6

D5

D4

D3

D2

D1

D0

Рис. 5.8

Здесь разряды D2 D0 определяют цвет символа (000 – черный, … 111 – белый). Разряд D3 задает яркость цвета символа. Разряды D6D4 задают цвет фона (000 – черный, … 111 – белый). Разряд D7 задает яркость фона или, значительно реже, мигание символа. Назначение разряда D7 (мигание/яркость) можно менять программным путем (см. разд. 5.2 в Приложении).

Все клавиши на клавиатуре делят на символьные и функциональные. Символьные клавиши (а, б, …1, 2,...) вводят символы, которые затем можно отображать на экране. То есть для символьных клавиш имеется их ASCII-код. Функциональные клавиши (F1,…DEL,…INS, …) ASCII-кода не имеют. При нажатии любой клавиши на клавиатуре происходит следующее. Контроллер клавиатуры выставляет СКЭН-код (номер) нажатой клавиши в порт с адресом 60h и через контроллер прерываний формирует на процессор запрос на прерывание. В IBM–подобных компьютерах этому запросу присвоен тип 9 («девятое прерывание»). Обработчик «девятого прерывания» читает СКЭН-код из порта 60h, переводит его в ASCII-код, если нажата символьная клавиша (для функциональных ASCII-код берется равным 0), и помещает и СКЭН- и ASCII-код в расположенный в памяти кольцевой буфер клавиатуры (подробно буфер клавиатуры описан в Приложении). Программы же считывают информацию уже из этого буфера.

Можно напрямую работать с видеопамятью и кольцевым буфером. Например, для того чтобы очистить экран, достаточно записать во все слова видеопамяти ASCII-код пробела с соответствующими атрибутами. Допустим, так:

mov cx, 2000       ; число повторений цикла

mov ax, 0b800h

mov es, ax           ; настраиваем es на начало видеопамяти

mov si, 0              ; в si  смещение в видеопамяти

cikle:

mov word ptr es:[si], 7020h       ; записываем в текущее ;знакоместо код и атрибуты 20h – ASCII- код пробела,

;70h – «черным по белому»

add si, 2          ; переходим к следующему знакоместу

;(слову)

loop cikle.

Работа с аппаратурой напрямую, хотя и обеспечивает наиболее высокое быстродействие, чревата ошибками. Рекомендуем новичкам пользоваться сервисными подпрограммами, которые DOS и BIOS предоставляют пользователю через программные прерывания int n. Рассмотрим некоторые прерывания, обеспечивающие сервис по работе с экраном и клавиатурой.

5.11. Прерывания DOS для работы с клавиатурой

5.11.1. Функция 7 прерывания int 21h

Входные параметры: нет.

Ввод символа (его ASCII-кода) из буфера клавиатуры. Если буфер пуст (никакая клавиша не нажата), подпрограмма ждёт нажатия клавиши.

Результат: в регистре al возвращается ASCII-код символа. Если нажата функциональная клавиша (например, F2), в al вернётся ноль. При этом чтобы прочитать СКЭН-код этой клавиши, надо повторно вызвать int 21h c функцией 7.

Пример. Проверить, нажата ли клавиша q.

mov ah, 7

int 21h

cmp al, 'q'       ; сравнение полученного кода с кодом q

jne m1             ; переход на метку m1, если «не равно»

Пример. Выйти из программы по нажатию любой клавиши.

mov ah, 7

int 21h             ; здесь программа будет «висеть», пока не

;будет нажата какая-либо клавиша

mov ah, 4ch    ; выход int 21h

5.11.2. Функция 6 прерывания int 21h

Входные параметры: вdl – 0ffh

Если в буфере есть символ, то его код возвращается в регистре al, а флаг процессора zf устанавливается в 0. Если в буфере нет символа, zf устанавливается в 1, а в al – «мусор». То есть, в отличие от предыдущей функции, здесь не ждут нажатия клавиши (так называемый ввод без ожидания).

Пример. Проверка на нажатие ESC (код ESC = 1bh).

mov dl, 0ffh

mov ah, 6

int 21h

jz m1               ; никакая клавиша не нажата (zf = 1)

cmp al, 0

je m2               ; нажата функциональная клавиша (в al из

;int 21h вернулся 0)

cmp al, 1bh

je m3               ; нажата Esc

5.11.3. Функция 0ah прерывания int 21h

Входные параметры: ds:dx   начальный адрес буфера в оперативной памяти.

Ввод строки символов с клавиатуры в созданный заранее буфер. Формат буфера: в нулевом байте программист записывает ожидаемую длину строки, в первом байте подпрограмма вернет фактически набранную длину строки, далее будет записаны ASCII-коды набранных символов.

Вводимая строка набирается на клавиатуре и заканчивается нажатием клавиши ENTER. В результате в буфер помещается следующая информация:

- байт 0 ‑ ожидаемая длина строки;

- байт 1 ‑ фактическая длина строки;

- байт 2 и далее ‑ строка, заканчивающаяся ASCII-кодом клавиши ENTER (0dh).

Пример. Собираемся принять строку, состоящую из двадцати символов.

buf db 20, 0, 21 dup (0)     ; сколько ожидаем, сколько ;получим, 20 байт под ожидаемый прием, плюс байт под ;код ENTER

.

mov ah, 0ah

mov dx, offset buf

int 21h        ; здесь «висим» и ждем ввода информации.

Пусть мы ввели 56 и нажали ENTER, тогда будем иметь в буфере (если смотреть в отладчике):

14 02 35 36 0D 00 …. 00

Здесь 14h – это 20 в шестнадцатеричной системе, 02 – число принятых символов, 35h – это ASCII-код цифры 5, 36h – ASCII-код цифры 6.

5.12. Прерывания BIOS для работы с клавиатурой

5.12.1. Функция 0 прерывания int 16h

Входные параметры: нет.

Чтение символа из буфера клавиатуры. Если клавиша символьная, в al возвращается её ASCII-код,  а в ah ‑ её  СКЭН-код. Если клавиша функциональная, в al возвращается 0, а в ah ‑ её СКЭН-код. Если в буфере нет символа, то подпрограмма ждет нажатия клавиши.

Пример. Проверка на нажатие клавиши «стрелкa – вверх». Это функциональная клавиша и её СКЭН-код равен 48h.

mov ah, 0

int 16h

cmp al, 0

jne m1   ; нажата символьная клавиша

cmp ah, 48h

je m2 ; нажата «стрелка – вверх».

Примечание: прерывание int 16h имеет аналогичную функцию 10h, предназначенную для работы с расширенной (101 клавиша) клавиатурой.

5.12.2. Функция 1 прерывания int 16h

Входные параметры: нет.

Функция похожа на предыдущую, только не ждет нажатия клавиши. Если в буфере нет символов, флаг zf устанавливается в единицу. Внимание, эта функция не очищает буфер при считывании символа (от этого символа).  То есть, если мы этой функцией прочтем символ из буфера,  то в следующий раз мы прочтем оттуда тот же символ, и так до бесконечности. Решить эту проблему можно, например, так:

mov ah, 1

int 16h

jz nott         ; в буфере нет символа, уходим на метку nott

mov al, 0

int 16h        ; читаем символ и очищаем буфер.

А почему нельзя было здесь сразу использовать int 16h  с функцией 0? Дело в том, что если на экране что-то движется вне зависимости от нажатия клавиш (например, летает шарик), то не можем останавливаться и ждать нажатия клавиши (на время ожидания шарик пропадет). В приведенном выше фрагменте проверяем буфер и, если в нем нет символа, то прыгаем на дальнейшее обслуживание экранного движения. И если в буфере гарантированно есть символ, то используем int 16h с функцией 0.

Примечание: прерывание int 16h имеет аналогичную функцию 11h, предназначенную для работы с расширенной (101 клавиша) клавиатурой.  

5.13. Прерывания DOS для работы с экраном

5.13.1. Функция 2 прерывания int 21h

Входные параметры: в dl ASCII-код символа.

Выводит заданный символ в текущую позицию курсора. Курсор после вывода смещается на позицию вправо. Коды 7, 8, 0ah и 0dh (коды задаются в dl) на экран не выводятся, а управляют перемещением курсора: 8 ‑ на символ влево, 0ah ‑ на строку вниз, 0dh ‑ на начало строки, 7 – звонок.

Пример. Вывести букву А в текущую позицию курсора.

mov ah, 2

mov dl, 'A'

int 21h

5.13.2. Функция 9 прерывания INT 21h

Входные параметры: в ds:dx адрес начала строки в оперативной памяти.

Выводит, начиная с текущей позицией курсора, строку символов из оперативной памяти. Конец строки задается символом $. Коды 7, 8, 0ah и 0dh являются управляющими.

Пример.

stroka  db 'Я, ребята, студент',0dh,0ah,'$'   

.

mov ah, 9

mov dx, offset stroka

int 21h

5.14. Прерывания BIOS для работы с экраном

5.14.1. Функция 1 прерывания int 10h

Входные параметры: в ch (биты 4‑0) верхняя строка развертки курсора, в cl (биты 4‑0) нижняя строка развертки курсора.

Подпрограмма позволяет установить конфигурацию (размер по вертикали) курсора. Стандартно (для цветных мониторов) верхняя граница курсора равна 6, а нижняя – 7. Если взять верхнюю границу равной 0, а нижнюю оставить равной 7, курсор будет максимально большим. Это же прерывание позволяет гасить курсор (делать его невидимым). Для этого надо взять ch = 20, cl = 0.

Пример.

mov ah, 1

mov ch, 20

mov cl, 0

int 10h.

Фрагмент программы с такими параметрами гасит на экране курсор. Если хотите скрыть курсор, то делайте это в последний момент, когда вся программа уже отлажена. Скрытый курсор может мешать процессу отладки, так как зачастую ошибка заключается в неправильном позиционировании курсора (за пределами экрана).

5.14.2. Функция 2 прерывания int 10h

Входные параметры: в dh номер строки, в dl номер столбца, в bh номер видеостраницы (для нас всегда 0).

Подпрограмма устанавливает курсор в заданную позицию.

Пример. Установить курсор в центр экрана.

mov dx, 0c28h ; 12-я строка (0сh), 40-й столбец (28h).

mov bh, 0

mov ah, 2

int 10h.

5.14.3. Функция 3 прерывания int 10h

Входные параметры: в bh номер видеостраницы (для нас 0).

Прерывание возвращает текущие координаты и конфигурацию курсора: в dh возвращается строка, в dl – столбец, в ch – верхняя строка развертки, в cl – нижняя строка развертки.

Пример. Запомнить текущую позицию курсора в переменной cursor.

mov ah, 3

mov bh, 0

int 10h

mov cursor, dx.

5.14.4. Функция 6 прерывания int 10h

Входные параметры: сх – координаты левого верхнего угла прямоугольной области экрана (chстрока, clстолбец), dxкоординаты правого нижнего угла (dhстрока, dlстолбец), alна сколько строк прокручивать заданное окно (при al = 0 все заданное окно очищается), bh атрибуты для заполнения освобождающихся строк.

Прокрутка заданной прямоугольной области экрана (окна) на заданное число строк вверх. Такая процедура называется "скроллинг".

Пример. Очистить экран.

mov cx, 0 ; левый верхний угол экрана. Строка = 0, ;столбец = 0.

mov dx, 184fh ; правый нижний угол экрана.

;Строка = 24 (18h), столбец = 79 (4fh)

mov bh,7 ; белый по черному

mov ax, 600h ; функция 6. Очистить весь экран.

int 10h.

5.14.5. Функция 9 прерывания int 10h

Входные параметры: bh - номер видеостраницы (у нас 0), bl ‑ атрибуты символа, al ‑ ASCII-код выводимого символа, сх ‑ число повторений.

Выводит заданный символ в текущую позицию курсора. Курсор при этом не перемещается. В сх помещается число х (х³1). При выводе символ распространяется на х позиций вправо от курсора. То есть, если х = 1, то будет напечатан один символ, при х = 2 ‑ два символа (одинаковых) и т.д. Если cx = 0, то ничего выводиться не будет!!! Коды 7, 8, 0ah и 0dh являются управляющими.

Пример. Забить верхнюю строку экрана символом "*". Вывод произвести черным по белому.

mov ah, 2 ; устанавливаем курсор

mov bh, 0 ; видеостраница 0

mov dx, 0 ; левый верхний угол экрана

;(строка = столбец = 0)

int 10h

mov ah, 9 ; вывод символа

mov bh, 0 ; видеостраница 0

mov bl, 70h ; черным по белому

mov al, '*'

mov cx, 80 ; заполняем всю строку

int 10h.

5.14.6. Функция 0eh прерывания int 10h

Входные параметры: al ‑ ASCII-код выводимого символа.

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

Очень часто в программах приходится программным образом организовывать задержку. Например, если у нас по экрану летает символ, без задержки он будет летать настолько быстро, что мы увидим только некое мерцание. Есть несколько способов организации такой задержки.

Способ 1. Пустой цикл.

mov cx, 0ffffh ; в cx максимально возможное число

zero_loop: loop zero_loop.

Для современных процессоров эта задержка окажется слишком мала и ее трудно заметить. Приходится делать вложенный пустой цикл. Например, так:

mov bx, 400

m1: mov cx, 0ffffh

m2: loop m2

dec bx

jnz m1.

Недостаток заключается в том, что здесь время задержки зависит от производительности процессора. То есть, для конкретного компьютера ее приходится подбирать, уменьшая или увеличивая наше число 400.

Способ 2.

mov ah, 86h

mov cx, 4

mov dx, 0

int 15h.

Эта функция прерывания int 15h отрабатывает задержку, заданную (в микросекундах) в регистровой паре cx:dx. В приведенном выше примере задержка составит порядка 218 микросекунд, т.е. примерно четверть секунды. Недостаток заключается в том, что некоторые операционные системы (в частности, Windows NT) эту задержку «не понимают» (программа выполняется, а задержки нет).

Способ 3. Работа с системными часами, расположенными в области переменных BIOS.

Функция 0 прерывания 1ah возвращает в cx:dx текущее число тиков таймера. Таймер тикает примерно 20 раз в секунду. Отсюда, если реализуем такой фрагмент:

mov ah, 0

int 1ah

mov bx, dx

add bx, 10

mwait:

mov ah, 0

int 1ah

cmp dx, bx

jb mwait ; если меньше, то снова идем на mwait

;и получим задержку на полсекунды.

5.15. Пример программы, работающей с клавиатурой и экраном

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

code segment                   

assume cs:code, ds:code        

org 100h        

start:                                                                    

jmp begin

x db 0    ;координата курсора по Х, исходный столбец 0

simb db 0

begin:

; прячем курсор

mov ah, 1

mov ch, 20

mov cl, 0

int 10h

; чистим экран красным цветом, символ будет черным

mov cx, 0

mov dx, 184fh

mov bh, 40h

mov ax, 600h

int 10h

m1:

; ждем нажатия клавиши

mov ah, 0

int 16h

; отсекаем функциональные клавиши

cmp al, 0

je m1

; проверяем на ESC

cmp al, 1bh

je exit

; сохраняем принятый символ

mov simb, al

;выводим пробел в центр экрана (стираем старый ;символ)

mov ah, 2

mov dx, 0c28h

mov bh, 0

int 10h

mov ah, 0eh

mov al, ' '

int 10h

m2:

;устанавливаем курсор для вывода символа

mov ah, 2

mov bh, 0

mov dh, 12      ;символ летит по 12-й строке

mov dl, x

int 10h

     ; выводим символ в текущую позицию курсора

mov ah, 9

mov cx, 1

mov bl, 40h

mov bh, 0

mov al, simb

int 10h

; задержка

mov ah, 0

int 1ah

mov bx, dx

add bx, 2

m3:

mov ah, 0

int 1ah

cmp dx, bx

jb m3

; увеличиваем Х координату курсора на единицу

inc x

; проверка на выход за центр экрана (40-й столбец)

cmp x, 41

jne m4

mov x, 0     ; восстанавливаем исходное значение Х ;координаты курсора

jmp m1       ; пошли на ожидание нажатия клавиши

m4:

; стираем символ в текущей позиции (пробелом)

mov ah, 0eh

mov al, ' '

int 10h

jmp m2       ; пошли на вывод символа в следующей ;позиции

exit:

mov ah, 4ch

int 21h

code ends

end start

5.16. Задания к лабораторной работе «Клавиатура и экран»

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

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

3. Программа очищает экран и вырезает в центре экрана инверсное окно разумных размеров. Далее, при нажатии любой символьной клавиши её отображение должно появляться внутри этого окна. Таким образом, окно постепенно заполняется вводимыми с клавиатуры символами. Заполнение окна происходит в общепринятом порядке: слева – направо – сверху - вниз. Выход из программы по нажатию клавиши INS или при полном заполнении окна.

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

5. По экрану (хаотично!) летают два шарика (нули), зеркально отражаясь от его границ. К границе шарики подлетают под углом 45 градусов. Нажатие клавиши SPACE останавливает движение шариков, а последующее нажатие любой другой клавиши снова его возобновляет. Выход из программы по нажатию клавиши ESC или при столкновении шариков.

6. Программа очищает экран и вырезает в его центре инверсное окно разумных размеров. Вводимый с клавиатуры символ полностью заполняет это окно. При вводе следующего символа всё повторяется. Выход из программы по нажатию клавиши F9.

7. Программа очищает экран и вырезает в его центре инверсное окно разумных размеров. Вводимый с клавиатуры символ должен, с разумной скоростью, двигаться вокруг этого окна (по его кромке). При вводе следующего символа всё повторяется. Выход из программы по нажатию клавиши F8.

8. После запуска программы по границам очищенного экрана бежит «змей», например, из восьми звездочек или нулей. С помощью клавиш-стрелок направление движения «змея» можно менять на 90 градусов от текущего направления. При достижении границы экрана «змей» автоматически заворачивает на 90 градусов вправо, т.е. переходит на первоначальную траекторию. Выход из программы по нажатию клавиши SPACE.

9. Программа очищает экран. При вводе символа последний полностью заполняет собой границы экрана. Следующий вводимый символ полностью заполняет собой внешние границы ещё "чистой" области экрана. И так далее. Выход из программы по нажатию клавиши DEL или при полном заполнении экрана.

10. Программа очищает экран и рисует вертикальную инверсную линию, проходящую через весь экран. Эта линия должна иметь 2‑3 разрыва шириной 1‑2 строки. Затем программа просит игрока задать номер строки. После того как номер введен, по соответствующей строке из-за левой границы экрана вылетает шарик (например, ноль) и летит по горизонтали через экран (с разумной скоростью). Если шарик попал в разрыв, то программа хвалит игрока, если не попал ‑ ругает. Программа предоставляет игроку пять попыток. Выход из программы по нажатию клавиши ESC или после пяти попыток.

11. Программа очищает экран. Внешние границы экрана заполняются звездочками. Начиная с левого верхнего угла, по верхней кромке экрана выводится фамилия одного из членов бригады. Так как фамилия выводится в ту же строку, что и звездочки, то в соответствующих позициях (и только в них) звездочки отсутствуют. При нажатии клавиши R вся эта картинка начинает с разумной скоростью сдвигаться по часовой стрелке. То есть фамилия вместе со звездочками вращается вокруг экрана, при этом в любой момент времени границы экрана должны быть полностью заполнены, а остальная часть экрана должна быть чистой. Клавиша S останавливает вращение,  клавиша R ‑ возобновляет, клавиша ESC ‑ выход из программы.

12. Программа очищает экран и выводит на него набираемую на клавиатуре информацию, но не в общепринятом порядке (слева – направо – сверху – вниз), а сверху – вниз – слева ‑ направо. То есть сначала заполняется крайний левый столбец, затем следующий слева столбец и так далее. Соответственно меняется направление работы управляющих клавиш: ENTER, BACKSPACE, TAB и клавиш-стрелок. Выход из программы по нажатию клавиши ESC.

Примечание: СКЭН- и ASCII-коды для различных клавиш можно найти в различных справочниках. Очень хороша для этих целей маленькая программа code.exe.

5.17. Работа с гибкими дисками

Любой диск разбит на дорожки, а дорожки, в свою очередь, разбиты на сектора. Стандартный формат сектора – 512 байт. Сектора имеют нумерацию, при этом сектор имеет физический и логический номера. Физическая нумерация секторов более сложная и ее рассматривать не будем. При логической нумерации каждому сектору присваивается номер: 0 (сектор 0), 1 (сектор 1), …

На любом диске в секторе 0 располагается «загрузочная запись» (boot record), далее (в секторах 1, 2, …) располагаются две копии «таблицы размещения файлов» FAT (file allocate table), затем идет «корневой каталог» (root dir) и наконец собственно информация (файлы). В лабораторной работе предполагается работа с FAT и корневым каталогом дискеты, формата 1,44 Мбайта.

Помимо понятия сектор, с дисками связано еще одно понятие – «кластер». Кластер – это минимальная часть диска, которая выделяется на данном диске под запись одного файла. Один кластер занимает на диске несколько смежных секторов, например, для дискеты 1,44 Мбайта, кластер занимает два смежных сектора. То есть, если создали файл размером 1 байт и записали его на нашу дискету, то этот файл займет один кластер (1024 байта на дискете). И если есть файл размером 1000 байт, то он тоже займет на дискете один кластер. А если файл имеет размер 9,3 Кбайта? Тогда этот файл на дискете расположится в десяти кластерах, причем необязательно подряд идущих. Именно для описания таких файлов, расположенных «не в подряд идущих» кластерах, и создается FAT.

В FAT для каждого записанного на диск файла, содержится описание «цепочки кластеров», отведенных дл этого файла. Каждый кластер имеет номер, причем нумерация кластеров начинается не с нуля, а с 2 (2, 3, 4, …). FAT состоит из элементов, в первые два из которых (элементы 0 и 1) записана служебная информация, а каждый следующий элемент FAT содержит описание одного соответствующего кластера (элемент 2 описывает кластер 2, …). Если, например, в элементе 2 записано число 5, то продолжение файла находится в кластере 5. Таким образом, если наш файл располагается на дискете в кластерах 2, 3, 7 и 12 (кластер 2 – начало файла, кластер 12 – конец файла), в FAT будет записана следующая информация:

- элемент 2 – 3;

- элемент 3 – 7;

- элемент 7 – 12;

- элемент 12 – информация о том, что это последний кластер данного файла.

Для гибких дисков используется FAT, каждый элемент которой имеет формат 12 бит (FAT12). Сделано это для того, чтобы FAT занимала на дискете как можно меньше места. Работа с нестандартными 1,5-байтными элементами создает для программиста трудности. Для дискеты 1,44 Майта первая копия FAT располагается в секторах 1 – 9, вторая копия FAT – в секторах 10 – 18. Структура FAT для дискеты 1,44 Мбайта приведена на рис. 5.9. Здесь:

   ‑ служебная информация;
   ‑ четный элемент FAT;
   ‑ нечетный элемент FAT.

байт

 
 
 
 
   
 
 
   
 

……….

 
   
 

Рис. 5.9

В общем случае элемент FAT (описание кластера) может содержать следующую информацию:

- 000h ‑ кластер свободен;

- 002hff0h ‑ кластер занят, а записанное в нем число задает следующий кластер цепочки;

- ff1hff7h ‑ кластер испорчен (плохой);

- ff8hfffh ‑ кластер занят и является последним кластером цепочки.

Алгоритмы работы с FAT будут приведены ниже.

Корневой каталог для дискеты 1,44 Мбайта занимает сектора 19‑33 и содержит описания всех файлов и поддиректорий, расположенных в корневой директории. Один элемент каталога занимает 32 байта и имеет структуру, приведенную в табл. 5.1.

                                                                     Таблица 5.1

№ байта (байтов)

Описание

0 – 7

Имя файла в ASCII-кодах (большими буквами!!). Если в байте 0 записан 0, это означает,  что данный элемент каталога свободен и никогда не использовался. Соответственно и все последующие элементы каталога тоже будут свободны. Если в байте 0 записано 0e5h – это означает, что данный файл с диска удален. Примечание: данные сведения приведены для DOS, для Windows действуют иные правила, но здесь они не рассматриваются.

8 – 10

Расширение файла (большими буквами!!).

11

Атрибуты файла (формат смотри ниже)

12 – 21

Не используются

22 –23

Время создания (формат смотри ниже)

24 –25

Дата создания (формат смотри ниже)

26 –27

Номер начального кластера цепочки

28 –31

Размер файла в байтах

Формат байта атрибутов следующий:

- бит 0 = 1 ‑ файл только для чтения;

- бит 1 = 1 ‑ скрытый файл;

- бит 2 = 1 ‑ системный файл;

- бит 3 = 1 ‑ это не файл, а метка тома;

- бит 4 = 1 ‑ это поддиректория;

- бит 5 = 1 ‑ архивный файл.

Формат времени создания следующий:

- hhhhhmmmmmmsssss (байт 23: hhhhhmmm, байт 22: mmmsssss) – здесь: h – часы, m – минуты, s – пары секунд.

Формат даты создания следующий:

- yyyyyyymmmmddddd (байт 25: yyyyyyym, байт 24: mmmddddd) – здесь: yгод, вычисляемый по формуле y = текущий год – 1980, m – месяц, dдень.

Например, в 23 байте элемента каталога записано 85h, в 22 байте – 0eh, в 25 байте – 2ch и в 24 байте – 55h. Тогда:

850eh = 1000010100001110b = 16 ч 40 мин 14*2 с.

2c55h = 0010110001010101b = (22+1980) = 2002 год 2 месяц (февраль) 21 число.

Обратите внимание на то, что чем меньше число, записанное в поле год, тем более старым будет файл.

Для работы с дисками можно использовать прерывания DOS int 25h и int 26h (имеются и другие).

Прерывание int 25h считывает с указанного диска, начиная с заданного сектора, требуемое количество секторов и помещает эту информацию в созданный в программе буфер. Прерывание int 26h записывает информацию из буфера в заданные сектора на указанном диске.

Входные данные для этих прерываний одинаковы.

В dx ‑ номер начального сектора, в cx ‑ количество считываемых секторов, в ds:bx ‑ начальный адрес буфера, в al ‑ номер дисковода (0 – А, 1 – В, 2 – С,…). Прерывания int 25h и int 26h имеют особенность: возврат из обработчиков этих прерываний производится командой ret (не iret). В результате получается «неправильная вершина стека». Иногда, хотя и редко, это приводит к неправильной работе программы. Найти же в этом случае причину неправильной работы исключительно трудно. Поэтому проще сразу исключить эту причину, поставив сразу за командой int 25h (int 26h) команду выталкивания из стека (pop) в какой-нибудь ненужный регистр (фиктивное выталкивание). Таким образом, если, допустим, надо прочитать содержимое корневого каталога дискеты 1,44 Мбайта, то можно использовать такой фрагмент:

buf db 512*15 dup (0)                 ;создали буфер 15

                                                    ;секторов ;по 512 байт

.

mov dx, 19

mov cx, 15

mov bx, offset buf

mov al, 0

int 25h

pop cx.

После того как содержимое корневого каталога считано в буфер, можно работать с этим буфером, например, так:

- устанавливаем si (или di или…) на начало буфера;

- проверяем содержимое байта, расположенного по этому адресу;

- если в этом байте 0, заканчиваем проверку (больше в корневом каталоге ничего нет!) и уходим на пункт 6 (скорее всего на выход или на вывод какой-либо информации на экран);

- если в этом байте e5h – это удаленный файл, прибавляем к si (или di или…) 32 (увеличиваем адрес на 32, переходя тем самым на следующий элемент корневого каталога) и идем на пункт 2;

- если в байте не 0 и не e5h, значит это описание какого-то файла или поддиректории или метка тома. Производим необходимые действия, прибавляем к si (или di или…) 32 и идем на пункт 2.

Примечание: Этот алгоритм рассчитан на DOS. Длинные имена файлов и удаленные файлы Windows могут приводить к неправильным результатам. Самый простой способ: взять чистую дискету или отформатировать дискету под DOS и записать на нее только файлы с короткими именами.

Во многих заданиях к данной лабораторной работе требуется выводить на экран различные числа. Например, количество файлов в корневом каталоге или номер начального кластера файла. При этом у некоторых студентов возникает проблема перевода числа в форму, пригодную для вывода на экран.

Для того чтобы вывести на экран в привычном человеку десятичном виде какое-либо двоичное число, надо разбить это число на отдельные десятичные цифры и перевести каждую цифру в ASCII-код. Разбиение на отдельные десятичные цифры удобно организовать с помощью операции деления (в какой бы системе счисления не делили число на 10, в остатке получим младшую десятичную цифру этого числа). Например, 123/10 = частное 12, остаток 3; 12/10 = частное 1, остаток 2, т.е. разбили число 123 на отдельные десятичные числа.

Перевести десятичную цифру в ее ASCII-код очень просто, достаточно прибавит к этой цифре 30h (ASCII-код цифры 0). Пусть, например, в результате работы программы в регистре al получено количество файлов в корневом каталоге. Следующий фрагмент выведет это число на экран в десятичном виде (максимальное число в регистре al255, т.е. не более трех десятичных цифр):

strok db ‘Всего файлов = ’, 0, 0, 0, 0ah, 0dh,’$’        

;это заготовка под выводимую строку, вместо нулей ;подставим ASCII-коды наших трех цифр, 0ah и 0dh ;переведут курсор (после вывода) в начало следующей ;строки экрана, а «доллар в апострофах» задает конец ;выводимой строки.

.

;разбиваем на десятичные цифры и переводим их в ASCII - код

mov si, offset strok      ; в si начальный адрес заготовки

mov cl, 10

mov ah, 0

div cl           ; делим ax на cl, остаток в ah,  частное в al

add ah, ‘0’           ; в ah младшая цифра, переводим ее в

;ASCII- код

mov [si + 16], ah      ; и отправляем ее в заготовку на

;место правого нуля

mov ah, 0

div cl

add ah, ‘0’                ; в ah средняя цифра, переводим ее в

;ASCII- код

mov [si + 15], ah      ; и отправляем ее в заготовку на

;место среднего нуля

add al, ‘0’                 ; в al старшая цифра, переводим ее в

;ASCII- код

mov [si + 14], al       ; и отправляем ее в заготовку на ;место левого нуля

; выводим строку на экран

mov ah, 9

mov dx, offset strok

int 21h

Здесь программист должен быть внимателен, так как команда деления может приводить к прерыванию по ошибке деления (см. описание команды div). В приведенном выше фрагменте возникновение такого прерывания исключено.

Иногда надо выводить число на экран в шестнадцатеричном виде. Разбить двоичное число на отдельные шестнадцатеричные цифры просто достаточно разбить это число на отдельные тетрады. Для перевода шестнадцатеричной цифры в ASCII-код можно, например, исследовать эту цифру и, если она находится в диапазоне от 0 до 9 включительно, прибавить к ней ASCII-код цифры 0, в противном случае, прибавить ASCII-код буквы А (латинской) минус 10. Пусть надо вывести на экран в шестнадцатеричной форме число из регистра al:

strok db ‘Всего файлов = ’, 0, 0, 'h', 0ah, 0dh,’$’  

;это заготовка под выводимую строку, вместо нулей

;подставим ASCII – коды наших двух цифр, 0ah и 0dh ;переведут курсор (после вывода) в начало следующей ;строки экрана, а «доллар в апострофах» задает конец ;выводимой строки.

.

.

mov si, offset strok

; разбиваем на шестнадцатеричные цифры и переводим ;их в ASCII-код

mov ah, al       ; сохраняем дубликат нашего числа

shr al, 4           ; выделяем старшую тетраду (цифру), ;сдвигая число вправо на 4 разряда

cmp al, 9

ja m1

add al, ‘0’

jmp m2

m1:     add al, ‘A’-10

m2:    mov [si + 14], al      ; отправляем ASCII‑код на \

;место левого нуля заготовки

and ah, 0fh      ; выделяем младшую тетраду (цифру)

cmp ah, 9

ja m3

add ah, ‘0’

jmp m4

m3:     add ah, ‘A’-10

m4:    mov [si + 15], ah     ; отправляем ASCII-код на

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

mov ah, 9

mov dx, offset strok

int 21h

Рассмотрим алгоритм работы с FAT. Например, если надо установить цепочку кластеров, в которые записан файл, то можно действовать следующим образом.

1. Считать в буфер корневой каталог.

2. Найти в нем элемент, описывающий искомый файл. Из этого элемента взять номер начального кластера цепочки.

3. Считать в буфер (обычно другой!) FAT.

4. По известному номеру кластера найти в FAT элемент, описывающий этот кластер, и взять из него номер следующего кластера цепочки.

5. Повторять п. 5, пока не дойдем до последнего кластера цепочки.

Основное неудобство возникает при выполнении п. 5 и связано оно с тем, что формат элемента FAT (1,5 байта) плохо согласуется с форматами регистров процессора.

Пусть на диске есть какой-то довольно большой файл, занимающий кластеры 2, 3, 4, 5, … Описание цепочки кластеров в FAT для данного файла показано на рис. 5.10. Здесь во втором элементе FAT записано 003, в третьем – 004,

Если же посмотреть на эту же информацию, допустим, в отладчике, то увидим следующую картину (жирным выделено содержимое четных элементов FAT):

хх хх хх 03 40 00 05 60 00 …..

Появляется вопрос, а как по известному номеру кластера N найти в FAT соответствующий этому кластеру элемент?

байт

 
 
 

0

3

4

0

0

0

0

5

6

0

0

0

………

Рис. 5.10

Если N ‑ четный, то смещение элемента относительно начала FAT можно вычислить по формуле:

смещение элемента = N+N/2.

При нечетном N формула слегка меняется:

смещение элемента = N+ (N‑1)/2.

Отсюда можно воспользоваться, например, таким фрагментом:

; пусть N находится в регистре bp

mov si, bp       ; дубликат N в si

mov di, bp       ; дубликат N в di

mov bx, offset buf        ; в bx начальный адрес буфера, в

;который считана FAT

and bp, 1         ; если получился ноль, N ‑ четный 

jnz nechet

; четный N

shr di, 1      ; делим (сдвигом вправо на разряд) N

;пополам

add si, di              ; получаем смещение элемента в si

mov ax, [bx+si]   ; элемент в ax

and ax, 0fffh        ; убираем старшую тетраду, ;принадлежащую нечетному элементу, формируя в ax ;содержимое искомого элемента

jmp m1

nechet:

; нечетный N

dec di         ; N-1

shr di, 1      ; (N-1)/2

add si, di

mov ax, [bx+si]

shr ax, 4          ; убираем младшую тетраду, ;принадлежащую четному  элементу, формируя в ax ;содержимое искомого элемента           

m1:                     ; …………….

Приведем пример программы, выводящей на экран названия всех файлов и поддиректорий в корневом каталоге, начинающиеся с буквы t.

code segment

assume cs: code, ds: code

org 100h

start:

jmp begin

buf db 512*15 dup (0)

mess1 db 0ah, 0dh,’$’       ; для перевода курсора в начало ;новой строки экрана

nomber db 0

mess2 db ‘Нет файлов, начинающихся с t’, 0ah, 0dh, ‘ $’

mess3 db ‘ Для выхода  нажмите любую клавишу$’

begin:

; считываем корневой каталог в буфер

mov dx, 19

mov cx ,15

mov bx, offset buf

mov al, 0

int 25h

pop bx

; настраиваем si на начало буфера

mov si, offset buf

m2:

mov al, [si]      ; читаем первый байт очередного элемента ;каталога

cmp al, 0         ; файлов больше нет?

je exit1            ; да, выходим

cmp al, 0e5h   ; удаленный файл?

jne m1             ; нет, проверяем дальше

add si, 32

jmp m2            ; раз удален, переходим к следующему ;элементу каталога

m1:

cmp al, ‘T’      ; начинается с Т?

je m3               ; да, идем на вывод названия на экран

add si, 32

jmp m2            ; нет, переходим к следующему элементу ;каталога

m3:

; наращиваем счетчик

inc nomber

; выводим название файла на экран, начиная с текущей ;позиции курсора

mov cx, 11           ;число повторений цикла (8 – название, ;3 – расширение)

mov ah, 0eh         ; функция

m4:

mov al, [si]      ; в al  заносим ASCII-код очередного ;выводимого символа

int 10h             ; выводим символ, курсор сам сдвигается ;на позицию вправо

inc si               ; теперь si адресует следующий выводимый символ

loop m4   

; переводим курсор в начало следующей строки экрана

mov ah, 9

mov dx, offset mess1

int 21h

add si, 21        ; 11 мы уже прибавили к si в цикле при ;выводе названия файла

jmp m2            ; переходим к исследованию следующего ;элемента каталога

exit1:

cmp nomber, 0

jne exit            ; были файлы, начинающиеся с Т

;Выводим сообщение, что искомых файлов не было

mov ah, 9

mov dx, offset mess2

int 21h

exit:

; выводим сообщение, с просьбой нажать любую ;клавишу     

mov ah, 9

mov dx, offset mess3

int 21h

mov ah, 7

int 21h                ; ждем, когда клавишу нажмут

mov ah, 4ch

int 21h

code ends

end start

5.18. Задания к лабораторной работе «Работа с гибкими дисками»

1. Программа анализирует диск и выводит на экран общее число кластеров на диске, число свободных класте-ров, число занятых кластеров и число плохих кластеров.

2. Программа анализирует диск и выводит на экран общее количество файлов в корневом каталоге, количество системных файлов, количество скрытых файлов, количество файлов "только для чтения" и количество подкаталогов.

3. Программа выводит на экран список файлов из корневого каталога. При нажатии клавиши S программа сортирует этот список по алфавиту.

4. Программа выдает на экран цепочку кластеров, которую занимает заданный файл. Имя файла задается в программе с клавиатуры.

5. Найти самый большой файл в корневом каталоге и вывести на экран его имя и размер в килобайтах.

6. Поиск файла в корневом каталоге и, если он есть, выдача на экран номера его начального кластера. Имя файла задается в программе с клавиатуры.

7. Программа выводит на экран список файлов из корневого каталога. При нажатии клавиши U программа сортирует этот список по размеру.

8. Вывести на экран названия всех файлов из корневого каталога и номера их начальных кластеров.

9. Программа выводит на экран список файлов (без подкаталогов) из корневого каталога, отмечая для каждого файла, является ли он скрытым или нет. При нажатии клавиши С все скрытые файлы становятся "открытыми" и наоборот, причем не только на экране, но и на диске.

10. После запуска программа выводит на экран меню: "Удалить" и "Восстановить". Производится выбор одного из   пунктов этого меню, например, путем нажатия соответствующей клавиши. Затем программа запрашивает ввод буквы и, если был выбран пункт "Удалить", удаляет из корневого каталога все файлы, имя которых начинается с этой буквы.

Если был выбран пункт "Восстановить», то программа восстанавливает в корневом каталоге все удаленные файлы, используя для этого введенную букву.

11. Определить, есть ли в корневом каталоге файлы с расширением EXE и, если таковые имеются, вывести на экран их список.

12. Найти в корневом каталоге самый "старый" файл и вывести на экран его имя и дату создания.

5.19. Перехват прерываний и создание резидентных программ

Под прерыванием понимается событие, заставляющее процессор прервать выполнение текущей программы и перейти на подпрограмму обработки этого события. Прерываемая программа называется фоновой программой, а подпрограмма обработки – обработчиком. После того как обработчик заканчивает свою работу, управление возвращается фоновой программе, в ту точку (той команде фоновой программы), на которой она была прервана.

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

1. Процессор автоматически сохраняет в стеке адрес возврата, т.е. адрес той команды фоновой программы, на которую мы должны будем вернуться из обработчика. Для этого процессор заталкивает в стек содержимое трех регистров: f (регистр флагов), cs и ip. Пара cs:ip как раз и задает адрес возврата.

2. После того как адрес возврата сохранен, процессор загружает в регистры cs и ip адрес первой команды обработчика, который он берет из таблицы прерываний (см. ниже), передавая управление обработчику.

3. В конце обработчика программист пишет команду iret (возврат из прерывания). По этой команде процессор выталкивает из стека адрес возврата (в регистры cs и ip) и флаги (в регистр f). Происходит возврат в фоновую программу. Естественно, этот возврат будет правильным, только если вершина стека в момент выполнения команды iret настроена надлежащим образом, иначе в регистры вытолкнется «мусор» и компьютер «повиснет».

Рассмотрим, откуда процессор берет начальный адрес обработчика. В процессорах Intel каждому прерыванию присвоен номер (тип). Тип прерывания лежит в диапазоне 0 – 255. Возможно 256 различных прерываний. Реально в компьютерах задействованы не все 256 прерываний. Для задействованного прерывания должен быть обработчик, расположенный в известном месте памяти.

Для того чтобы по известному типу процессор мог определить начальный адрес обработчика, в младшем килобайте памяти (адреса 00000h003ffh) создается таблица прерываний, в которой последовательно записаны адреса обработчиков для различных типов (начиная с типа 0). Каждый адрес задается в виде пары сегмент: смещение (сегмент загружается в cs, смещение – в ip). Пара задает адрес ячейки памяти, в которой располагается первая команда обработчика прерывания данного типа. Пару называют вектором прерывания. Любой вектор занимает в памяти 4 байта. Чтобы по типу найти адрес вектора, надо этот тип умножить на 4. Например, тип = 2, адрес вектора прерываний для этого типа равен 2×4 = 8 ‑ искомый вектор располагается в ячейках памяти 8, 9, 10 и 11.

Пусть, например, у нас в памяти (в таблице прерываний) такая ситуация:

адрес

содержимое

00004h

22h

00005h

07h

00006h

91h

00007h

c0h

тогда пара c091:0722h представляет собой вектор для прерывания типа 1, а начальный адрес обработчика для этого типа получается из этой пары так:

A = (c091h)*16+0722h = c0910h+0722h = c1032h.

Другой пример. Пусть написан обработчик для прерывания типа 2 и расположен в памяти, начиная с адреса 55a60h. Как преобразовать этот адрес в вектор?

Надо представить этот адрес в виде пары, а это неоднозначная операция, т.е., как правило, существует несколько правильных пар, задающих один и тот же адрес. Например, наш адрес можно представить в виде пары 55a6:0000h (или 55a0:0060h или….). Теперь надо записать вектор в таблицу:

адрес

содержимое

00008h

00h

00009h

00h

0000ah

a6h

0000bh

55h

Что же такое резидентная программа и чем она отличается от обычных программ? Обычная программа при запуске загружается в память, а после того как она отработала, полностью из памяти выгружается. Физически эта программа из памяти не удаляется, DOS помечает эту область памяти как свободную. Резидентная программа остается в памяти даже после того, как она отработала. Программа постоянно находится в памяти и активизируется при наступлении определенного события, например, при нажатии определенной комбинации клавиш или истечении заданного кванта времени.

Для того чтобы резидентная программа имела возможность отслеживать «свое событие», она должна перехватывать соответствующее прерывание. Например, при реакции на определенные клавиши наиболее удобно перехватывать аппаратное прерывание от клавиатуры, которому в системе присвоен тип 9. При активации резидентной программы через заданные промежутки времени можно перехватывать аппаратное прерывание от таймера – тип 8 (лучше не тип 8, а тип 1сh).

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

Посмотрим, что происходит, когда нажимаем какую-то клавишу на клавиатуре. Контроллер клавиатуры выставляет СКЭН-код нажатой клавиши в порт 60h и формирует запрос на контроллер прерываний. Последний передает этот запрос на процессор и сообщает процессору тип данного прерывания (тип 9).

Обработчик 9-го прерывания читает порт 60h, переводит СКЭН-код в ASCII-код (если нажата символьная клавиша, для функциональных клавиш ASCI-код = 0) и помещает и СКЭН- и ASCII-коды в кольцевой буфер клавиатуры, расположенный в области переменных BIOS. Адрес элемента буфера, в который была записана информация о нажатой клавише, хранится в ячейке памяти с адресом 0041ah. Прикладные программы и операционная система считывают информацию о нажатых клавишах уже из этого буфера. Для этого они либо используют соответствующие  сервисные прерывания (например, int 16h), либо работают с буфером напрямую:

; читаем из буфера информацию о нажатой клавише

mov ax, 40h

mov es, ax                ;начальный адрес сегмента 00400h

mov bx, es:[1ah]      ; в bx смещение (относительно es) ;ячейки, в которой записана информация о клавише

mov ax, es:[bx]        ; теперь в ah – СКЭН-, а в al

;ASCII-коды

Отметим, что при нажатии клавиши происходит не одно прерывание, а два. Первое прерывание возникает, когда эту клавишу нажимаем, второе – когда отпускаем. И в том, и в другом случае контроллер клавиатуры выставляет в порт 60h соответственно СКЭН-код нажатия и СКЭН-код отжатия, после чего вызывается обработчик 9-го прерывания. Для любой клавиши справедливо:

Код отжатия = код нажатия+128

т.е. код отжатия всегда характеризуется наличием единицы в старшем разряде. Например, код нажатия клавиши 5 06h, код отжатия – 86h, код нажатия клавиши «стрелка ‑ вверх» на цифровой клавиатуре – 48h, отжатия – c8h. Иногда такая ситуация начинает мешать и, для правильной работы резидента, код отжатия приходится отсекать.

Примечание. Для некоторых функциональных клавиш, появившихся на расширенной клавиатуре (101 клавиша и более), СКЭН-код (как нажатия, так и отжатия) представляет собой последовательность байтов (два байта, а иногда (если, например, включен NUM LOCK) и четыре).

Например, когда нажимаем клавишу «стрелка – вверх», расположенную не на цифровой клавиатуре, возникают два прерывания и код нажатия имеет вид e0 48h, также два прерывания возникает и когда эту клавишу отпускаем, а код отжатия имеет вид 0e c8h. При включенном режиме NUM LOCK код нажатия этой клавиши имеет вид e0 2a e0 48h, а код отжатия – e0 c8 e0 aah (и при нажатии, и при отжатии возникают по четыре прерывания). Эту ситуацию тоже иногда приходится учитывать.

Обработчик прерывания типа 8 обрабатывает прерывания от 0-го канала таймера, который отведен для службы системного времени. Таймер тикает примерно 20 раз в секунду и по каждому «тику» обработчик прибавляет единицу к системным часам, расположенным в области переменных BIOS. Пользователю не рекомендуется перехватывать 8-е прерывание. Для пользователя в конце стандартного обработчика 8-го прерывания стоит команда int 1ch. Обработчик этого программного прерывания по сути дела фиктивен, поскольку состоит из единственной команды iret. При работе с таймером пользователю как раз и рекомендуется перехватывать прерывание 1ch.

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

- функция 35h прерывания 21h. Входные параметры: в  al – тип перехватываемого прерывания. Возвращает в паре регистров es:bx вектор для прерывания, тип которого задан в al;

- функция 25h прерывания 21h. Входные параметры: в ds:dx – новый вектор, который записываем в таблицу, в al – тип прерывания, задающий место в таблице, куда производится запись вектора.

Определить, что заносить в ds и в dx в качестве вектора, просто. В ds  должен быть начальный адрес сегмента памяти, в который загружен резидент. При запуске программы на выполнение DOS так и настраивает ds. Так как в этом регистре итак находится правильная информация, то перенастраивать его не надо. В dx должно находиться смещение первой команды резидента. Для того чтобы определить это смещение, достаточно поставить на эту команду метку (например, met:) и написать команду mov dx, offset met. После этого в dx будет нужное значение.

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

- прочитать из таблицы прерываний вектор системного обработчика и запомнить его в памяти, доступной резиденту, с тем, чтобы в дальнейшем была возможность вызвать правильный обработчик перехватываемого прерывания;

- на место старого вектора записать в таблицу новый вектор, указывающий на нашу резидентную программу.

После этого при возникновении соответствующего прерывания вместо системного обработчика будет вызвана резидентная программа, которая определяет, касается ли ее произошедшее событие или нет (например, нажата ли нужная комбинация клавиш). Если событие «наше», то резидент производит требуемую обработку, если «не наше» ‑ передает управление системному обработчику, адрес которого сохранили.

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

Обычно резидентная программа пишется в СОМ-формате и имеет структуру, показанную на рис. 5.11.

PSP

Область данных резидентной части

РЕЗИДЕНТНАЯ ЧАСТЬ

ЗАГРУЗОЧНАЯ ЧАСТЬ

Область данных загрузочной части

Рис. 5.11

При запуске программы управление передается загрузочной части, которая с помощью рассмотренных выше действий подменяет вектор системного обработчика в таблице прерываний новым вектором, указывающим на нашу резидентную программу. После этого программа завершает свою работу, оставляя PSP и резидентную часть в памяти. Загрузочную часть, как правило, для экономии места из памяти удаляют. Именно поэтому загрузочную часть и располагают после резидентной части.

Завершить работу программы, оставив ее (или ее часть) в памяти, можно с помощью прерывания int 27h. Входным параметром этого прерывания является размер (в байтах) оставляемой в памяти части программы (начиная с начала программы, т.е. с PSP). Этот входной параметр задается в dx. Определить размер оставляемой части можно, поставив метку на первую команду загрузочной части (например, start:) и написать команду mov dx, offset start.

При запуске (установке) резидентной программы желательно производить проверку на повторную установку резидента, иначе в памяти может оказаться несколько копий программы. Отсутствие такой проверки приводит, как минимум, к нерациональному использованию памяти, а в худшем случае вообще искажает работу резидента. Например, наш резидент при нажатии некоторой «горячей» клавиши инвертирует цвета экрана и в памяти установлено две копии этого резидента. Тогда при нажатии «горячей» клавиши первая копия проинвертирует цвета, а вторая их восстановит. В результате мы ничего не увидим.

Проверку на повторную установку можно производить различными способами. Рассмотрим один из них. В области данных резидентной части выделяется байт (или больше) и туда записывается «ключ». Ключ ‑ это любое число, желательно редкое, например 5555h. После того как запускается программа, и загрузочная часть считывает из таблицы прерываний вектор, относительно этого вектора (вернее, относительно его поля сегмент) в памяти определяется ячейка, соответствующая ключу. Ее содержимое сравнивается с известным нам ключом и, если сравнение произошло, значит, программа (с вероятностью 99,9..%) уже установлена, поскольку маловероятно, что другая программа имеет в соответствующей ячейке памяти число, совпадающее с нашим ключом. В этом случае загрузочная часть выводит на экран сообщение, что программа уже установлена, и завершает свою работу, ничего не оставляя в памяти. Этот способ проверки имеет недостаток: он работает, только если резидент последний, кто перехватил соответствующее прерывание. Если после нас наше прерывание перехватил «чужой» резидент, то именно в нем и будем искать наш ключ и конечно не найдем, хотя наш резидент установлен в памяти.

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

1. Считать из таблицы прерываний старый вектор (функция 35h прерывания 21h).

2. Произвести проверку на повторную установку, при положительном результате перейти к пункту 6.

3. Сохранить старый вектор в известном (резиденту!!) месте памяти.

4. Поместить в таблицу прерываний (на место старого вектора) новый вектор, указывающий на наш резидент (функция 25h прерывания 21h).

5. Завершить программу, оставив ее резидентной (прерывание 27h).

6. Вывести сообщение о том, что программа уже установлена, и завершить программу, ничего не оставляя в памяти (функция 4ch прерывания 21h).

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

Схема 1. Сохраняем (в стеке) значения всех регистров фоновой программы, в том числе и сегментных, которые портит наш резидент. Если этого не сделать, то фоновая программа при возврате получит испорченное содержимое регистров и вряд ли будет работать корректно. Выполняем требуемую обработку. Восстанавливаем значения регистров из стека (в обратном порядке!). Передаем управление системному обработчику (командой jmp far), адрес которого нам сохранила загрузочная часть. Системный обработчик впоследствии сам вернет управление фоновой программе (командой iret).

Схема 2. Сохраняем значения регистров. Передаем управление системному обработчику, выполнив команды:

pushf

call far

Первая команда заталкивает в стек содержимое регистра флагов, вторая – вызывает системный обработчик как подпрограмму. (Возврат из этой «подпрограммы» происходит по команде iret, которая выталкивает из стека адрес возврата и флаги! Именно поэтому нужна команда pushf.) При возврате из системного обработчика управление снова получает наш резидент. Он выполняет требуемые действия, восстанавливает регистры и возвращает управление фоновой программе (командой iret).

Схема 3. Сохраняем значения регистров. Выполняем требуемую обработку. Восстанавливаем регистры. Возвращаем управление фоновой программе (командой iret). То есть в этой схеме полностью игнорируем системный обработчик и, следовательно, должны работать «за него». В частности, если мы перехватываем аппаратное прерывание (от таймера, от клавиатуры и т.д.), то должны не забыть снять «штору», которую ставит контроллер прерываний, послав число 20h в порт 20h. То есть здесь должны знать и учитывать все нюансы работы аппаратуры. Именно поэтому эта схема на практике используется редко.

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

Наибольшее число ошибок, приводящих к зависанию резидента, связано с тем, что при написании резидентной части программист не учитывает одно важное обстоятельство. Когда из фоновой программы мы попадаем в наш резидент, единственным сегментным регистром, содержимое которого указывает на наш резидент, является регистр СS (в него загрузилась информация из таблицы прерываний). Все остальные сегментные регистры указывают на фоновую программу!

К чему это может привести? Пусть в области данных резидентной части программы выделен байт под переменную, которую назвали flag. Пусть при написании резидентной части программист решил присвоить этой переменной значение 5, для чего написал команду:

mov flag, 5

Программист допустил (скорее всего) грубую ошибку. Ведь в этой команде по умолчанию начальный адрес сегмента задает содержимое регистра ds, а оно, как мы помним, пришло из фоновой программы и указывает на фоновую программу. То есть реально наша пятерка запишется не в переменную flag, а в какую-то ячейку памяти фоновой программы. Велика вероятность, что после этого фоновая программа просто перестанет корректно работать и зависнет (после нашего возврата в эту фоновую программу). А как же правильно обратиться к нашей переменной? Например, это можно сделать так:

mov cs:flag, 5

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

Отметим еще одно обстоятельство. В резидентной части программы не рекомендуется использовать сервисные прерывания DOS. Использовать эти прерывания можно при соблюдении определенных правил, которые в пособии не приводятся. Бездумное использование прерываний DOS может привести к неустойчивой работе программы. Прерывания BIOS можно использовать без ограничений.

Далее приводятся примеры двух простых резидентных программ. Обе программы отслеживают нажатие комбинации клавиш ALT/t и выводят сообщение об этом «событии» на экран. Однако первая программа написана по схеме 1, а вторая по схеме 2. Приведем данные, необходимые для понимания этих программ:

- если нажата клавиша ALT, бит 3 в ячейке памяти 00417h установлен в единицу;

- скэн-код клавиши t равен 14h, ASCII равен 74h;

- скэн-код комбинации клавиш ALT/t равен 14h, ASCII равен 0.

Программа 1. Перехватывает аппаратное прерывание от клавиатуры (тип 9). Реализована схема 1. Программа предназначена для работы в текстовом режиме. Например, в графическом (не полноэкранном) режиме FAR она может работать некорректно.

; заголовок СОМ-программы

code segment

assume cs:code,ds:code

org 100h

; переход на загрузочную часть программы

f10: jmp start

;область  данных  резидентной  части.

key dw 5555h      ;  ключ расположен в ячейке со ;смещением 103h (100h байт  PSP и 3 байта jmp start)

soob db 'нажата Alt/t'

oldvect dd 0         ;  здесь запоминаем адрес системного ;обработчика

;здесь начинается резидентная часть программы. Очень ;важным является  то, что когда мы попадаем сюда из ;фоновой программы, все сегментные  регистры, кроме CS, ;содержат данные фоновой программы. Поэтому там, где ;используются команды, по умолчанию берущие базовый ;адрес из DS, необходимо использовать префикс замены ;сегмента CS:.

newvect:   

;сохраняем в стеке все регистры, которые можем ;испортить

push ax

push es

push bx

push cx

push dx

push si

;выясняем, нажата ли ALT/t

in al, 60h

cmp al, 14h          ; нажата t?

jne exit              

mov ax, 40h

mov es, ax

mov al, es:[17h]

and al, 1000b       ; нажата ALT?

jz exit

;запоминаем позицию курсора (в SI).

mov ah, 3

mov bh, 0

int 10h

mov si, dx

;выводим сообщение, начиная с текущей позиции ;курсора, что Alt/t  нажата.

mov cx, 12

mov bx, offset soob

m1:  mov ah, 0eh

mov al, cs:[bx]

int 10h

inc bx

loop m1

;вводим задержку секунд на 5, чтобы полюбоваться ;надписью.

mov ah, 0

int 1ah

mov bx, dx

add bx, 50

m2:  mov ah, 0

int 1ah

cmp bx, dx

ja m2

;восстанавливаем курсор в старой позиции. Старые ;координаты  берутся из SI.

mov dx, si

mov ah, 2

mov bh, 0

int 10h

;стираем нашу надпись пробелами. Курсор при этом не ;смещается, а остается в нужной (старой) позиции.

mov ah, 0ah

mov al, ' '

mov cx, 12

mov bh, 0

int 10h

exit:

;восстанавливаем в регистрах информацию фоновой ;программы.

pop si

pop dx

pop cx

pop bx

pop es

pop ax

;вызываем правильный системный обработчик. Так как ;ячейка oldvect описана как двойное слово, команде jmp ;автоматически  будет присвоен тип far.

jmp cs:oldvect

;здесь кончается резидентная часть и ;начинается загрузочная часть программы.

start:

;получаем вектор правильного (системного) ;обработчика (в ES:BX).

mov ah, 35h

mov al, 9

int 21h  

     ;производим проверку (сравнивая с ключом) ;на повторную установку  программы.

cmp word ptr es:[103h], 5555h

jz inst

;запоминаем вектор правильного (системного) ;обработчика.

mov word ptr oldvect, bx

mov word ptr oldvect+2, es

;устанавливаем вектор своего обработчика.

mov dx, offset newvect

mov ah, 25h

mov al, 9

int 21h       

;завершаем программу, оставляя резидентной ;в памяти ее часть, от начала PSP до метки start.

mov dx, offset start

int 27h       

;если программа уже установлена в памяти, то выдаем сообщение об этом и завершаем программу, ничего ;не оставляя в памяти.

inst:

mov ah, 9

mov dx, offset soob2

int 21h

mov ah,4ch

int 21h  

;область данных  загрузочной части.

soob2 db 'Программа уже установлена$'

code ends

end f10

Программа 2. Перехватывает прерывание BIOS int 16h. Это прерывание возвращает в ax из буфера клавиатуры СКЭН- и ASCII-коды нажатой клавиши. Основная ветвь резидентной части программы реализует схему 2. То есть, попадая в наш резидент, сразу вызываем стандартный обработчик int 16h. Этот обработчик  возвращает в ax код клавиши, с которым и работаем. Поскольку при этом возврат управления фоновой программе осуществляет наш резидент, он и должен обеспечить передачу (в регистре ax) полученного кода клавиши фоновой программе. В резидентной части программы имеется побочная ветвь, реализующая схему 1. Связано это с тем, что прерывание int 16h имеет целый ряд функций, из которых интересны только две: 0 и 10h. Остальные функции как раз и отсекаются этой побочной ветвью.

;заголовок СОМ-программы

code segment

assume cs:code,ds:code

org 100h         ;переход на загрузочную часть программы

f10: jmp start      ;область  данных  резидентной  части.

key dw 5555h      ;  ключ расположен в ячейке со ;смещением 103h (100h байт PSP и 3 байта jmp start)

soob db 'нажата Alt/T'

oldvect dd 0         ;  здесь запоминаем адрес системного ;обработчика

;здесь начинается резидентная часть программы. Очень ;важным является то, что когда попадаем сюда из фоновой ;программы, то все сегментные регистры, кроме CS, ;содержат данные фоновой программы. Поэтому там, где ;используются команды, по умолчанию берущие базовый ;адрес из DS, необходимо использовать префикс замены ;сегмента CS:.

newvect:

;сохраняем в стеке все регистры, которые можем ;испортить

push es

push bx

push cx

push dx

push si

push di

;это функция 0 ?

cmp ah, 0

je resid            ;это функция 10h ?

cmp ah, 10h

jne exit1          ;если «не наши» функции, отдаем ;управление системному обработчику

resid:          ;вызываем стандартный обработчик

pushf

call cs:oldvect

;если в AH  вернулось 14h, а в AL0 -это код Alt/t.

cmp ax, 1400h

jne exit

mov di, ax       ; сохраняем код клавиши, чтобы вернуть ;его фоновой программе

;запоминаем позицию курсора (в SI).

mov ah,3

mov bh,0

int 10h

mov si, dx

;выводим сообщение, начиная с текущей позиции ;курсора, что Alt/t  нажата.

mov cx,12

mov bx,offset soob

m1:mov ah,0eh

mov al,cs:[bx]

int 10h

inc bx

loop m1     

;вводим задержку секунд на 5, чтобы полюбоваться ;надписью

mov ah, 0

int 1ah

mov bx, dx

add bx, 50

m2:  mov ah, 0

int 1ah

cmp bx, dx

ja m2

;восстанавливаем курсор в старой позиции. Старые ;координаты  берутся из SI

mov dx, si

mov ah,2

mov bh,0

int 10h       

;стираем нашу надпись пробелами. Курсор при этом не ;смещается,  а остается в нужной (старой) позиции.

mov   ah,0ah

mov   al,' '

mov   cx,12

mov   bh,0

int 10h

mov ax, di       ; восстанавливаем код клавиши, который ;надо вернуть фоновой программе

exit:

;восстанавливаем в регистрах информацию фоновой ;программы.

pop di

pop si

pop dx

pop cx

pop bx

pop es        .

iret    ;возвращаем управление фоновой программе

;возвращаем управление стандартному обработчику

exit1:

pop di

pop si

pop dx

pop cx

pop bx

pop es

jmp cs:oldvect

;здесь кончается резидентная часть и начинается загрузочная часть  программы.

start:

;получаем вектор правильного (системного) обработчика ;(в ES:BX).

mov ah,35h

mov al, 16h

int 21h

;производим проверку (сравнивая с ключом) на ;повторную установку  программы.

cmp word ptr es:[103h],5555h

jz inst

;запоминаем вектор правильного (системного) ;обработчика.

mov word ptr oldvect,bx

mov word ptr oldvect+2,es

;устанавливаем вектор своего обработчика.

mov dx,offset newvect

mov ah,25h

mov al, 16h

int 21h

;завершаем программу, оставляя резидентной в памяти ;ее часть, от начала PSP до метки start.

mov dx,offset start

int 27h

;если программа уже установлена в памяти, выдаем ;сообщение об этом и завершаем программу, ничего не ;оставляя в памяти.

inst:

mov ah,9

mov dx,offset soob2

int 21h

mov ah,4ch

int 21h

;область данных  загрузочной части.

soob2 db 'Программа уже установлена$'

code ends

end f10

Неприятной особенностью многих резидентных программ является их критичность к операционной среде, в которой они запускаются. Зачастую резидент, успешно работающий в Volkov commander, не работает в DOS Navigator и наоборот. Это связано с тем, что мы не знаем или не учитываем специфику работы конкретной среды.

5.20. Задания к лабораторной работе «Создание резидентных программ»

1. Через временной интервал (например, 10 секунд) на экран выводится какое-либо сообщение. Через 10‑20 секунд сообщение с экрана снимается, и работа ПЭВМ продолжается обычным образом.

2. При нажатии любой клавиши на экран выдается просьба нажать эту клавишу еще раз. При повторном нажатии просьба с экрана снимается,  и работа ПЭВМ продолжается обычным образом.

3. При нажатии клавиши ENTER в центр экрана выводится сообщение: "Отдыхаю, подождите минутку". Через 10‑20 секунд сообщение снимается, и работа ПЭВМ продолжается обычным образом.

4. ПЭВМ реагирует на клавишу "стрелка ‑ вверх" как на клавишу "стрелка ‑ вниз" (и наоборот), на клавишу "стрелка ‑ влево" как на клавишу "стрелка ‑ вправо" (и наоборот).

5. При нажатии клавиши F1 программа очищает экран и безостановочно выводит на экран сообщение "Не хочу вам помогать!", прокручивая при этом экран вверх. Секунд через 10‑20 этот процесс прекращается, восстанавливается экран, и работа ПЭВМ продолжается обычным образом.

6. Через равные промежутки времени (например, 30 секунд) резидент блокирует / разблокирует клавиатуру.

7. При нажатии клавиши Т сообщается текущее системное время.

8. Нажатие клавиши D замедляет или восстанавливает реакцию системы на нажатие клавиш клавиатуры. Замедление реакции должно быть достаточным, для того чтобы его можно было заметить визуально.

9. При нажатии клавиши R очищается правая, а при нажатии L ‑ левая половина экрана. Через 10‑20 секунд после нажатия любой из этих клавиш экран восстанавливается.

10. Нажатие клавиши G меняет размер курсора. (Как правило, любая среда, в которой работаем, например, Dos Navigator, при возврате ей управления восстанавливает размер курсора. То есть, чтобы гарантированно увидеть изменение курсора, можно, например, запускать программу в Command.com).

11. Нажатие клавиши I инвертирует цвета экрана.

12. После установки резидента система перестает реагировать на нажатие клавиши F7. На другие клавиши система реагирует обычным образом.

5.21. Работа со звуком

В состав любой ПЭВМ фирмы IBM входит микросхема таймера i8254.

Микросхема таймера имеет три канала, соответственно каналы 0,1 и 2. Канал 0 отводится для службы системного времени, канал 1 ‑ для регенерации памяти, а канал 2 ‑ для управления работой динамика.

Порты таймера имеют следующие системные адреса:

- канал 0         ‑ 40h;

- канал 1         ‑ 41h;

- канал 2         ‑ 42h;

- регистр управляющего слова (РУС)  ‑ 43h.

Через РУС производится настройка каналов, через остальные порты ‑ загрузка/считывание информации в/из соответствующих каналов.

Для работы со звуком нам нужен канал 2 и РУС. Каналы 0 и 1 перезагружать и перенастраивать категорически запрещено, так как система может выйти из строя (во всяком случае, до перезапуска).

Работа канала 2 заключается в том, что он делит опорную частоту (fоп = 1,19... МГц) на коэффициент пересчета Кпр, который заранее загружается в канал. Получаемая fвых = fоп/Кпр подается на динамик. Надо учитывать, что работой канала 2 и подачей fвых на динамик управляют два младших бита порта 61h. Если бит 0 порта 61h равен единице, то работа канала 2 (счет) разрешается. Если бит 1 порта 61h равен единице, то разрешается подача fвых на динамик. Таким образом, звук будет воспроизводиться, только если оба этих бита установлены в единицу.

Коэффициент пересчета для любой ноты можно определить исходя из выражения:

Кпр = fоп/fноты.

При этом учитываются следующие соотношения:

- частота ноты "до" 1-й октавы (fдо)   = 32,625 Гц;

- частота ноты "до" 2-й октавы     = 2*fдо;

частота ноты "до" 3-й октавы = 4*fдо  и так далее;

- частота "до-диез"    = а*fдо;

- частота "ре" = а*fдо-диез и так далее,

где а = 1,06 (приблизительно).

Если необходимо сыграть на компьютере мелодию, то проще подсчитать коэффициенты всех нот заранее или взять их готовыми (см. Приложение), так как человечеству эти коэффициенты давно известны. Таким образом, для мелодии желательно подготовить следующие данные:

- коэффициент пересчета и время звучания для каждой ноты;

- длительности всех пауз между нотами.

После того как все эти числа известны, надо последовательно выполнять следующие действия:

1. Запретить звучание, для чего установить в ноль оба младших бита порта 61h (не меняя при этом остальные биты этого порта).

2. Настроить канал 2, выполнив две команды:

; настраиваем канал 2 на передачу двух байт Кпр., на ;режим 3 и на двоичный счет

mov al, 0b6h

out 43h, al.

3. За две передачи в канал 2 загрузить Кпр для текущей ноты, например:

; загружаем в канал 2 Кпр = 300

mov ax, 300

out 42h, al

mov al, ah

out 42h, al.

4. Разрешить звучание, установив в единицу оба младших бита порта 61h.

5. Ввести задержку, равную длительности ноты.

6. Запретить звучание.

7. Выдержать паузу требуемой длительности и перейти к пункту 3.

Подобная программа пишется примерно следующим образом. Создается массив нот (коэффициентов пересчета), который заканчивается «не нотой», например нулем. Эта «не нота» задает конец массива (мелодии). Если надо, то создаются массивы длительностей нот и пауз между нотами. Программа считывает из массива нот очередной элемент, проигрывает его, считывает очередной элемент и так до тех пор, пока будет считана «не нота». После этого программа заканчивает свою работу. Не забудьте при выходе из программы запретить звучание, иначе компьютер будет продолжать играть последнюю ноту. Достоинство такой программы состоит в том, что для изменения мелодии достаточно изменить числа в массивах.

Примечание. Для создания звуковых эффектов при проигрывании какой-либо ноты можно с определенной частотой сбрасывать/устанавливать бит 1 в порту 61h.

В лабораторной работе «Работа со звуком» нужно, в соответствии с музыкальными способностями и вкусом, реализовать на ПЭВМ любую известную мелодию, начиная от гаммы «до-мажор» до «Полонеза» Огинского. Единственным требованием является несовпадение вашей мелодии с мелодиями других бригад из вашего потока.

5.22. Отладка программ

Очень часто при написании ассемблерных программ приходится сталкиваться с ситуацией, когда программа работает не так, как нам этого хочется. Найти при этом ошибку в алгоритме или его реализации никак не удается. В этом случае приходится привлекать к работе специальную программу, называемую отладчиком. Отладчик позволяет выполнить программу в «пошаговом режиме», команда за командой, причем после выполнения любой команды можно следить, как меняется информация в памяти, в стеке, в регистрах и на экране. Это дает возможность локализовать ошибку и ее устранить.

Существует много различных отладчиков, однако программисты чаще всего используют отладчик td.exe (Turbo Debugger). Этот отладчик предоставляет программисту мощный инструментарий для отладки ассемблерных программ. Ограничимся минимальным набором, достаточным для начинающего программиста.

Turbo Debugger позволяет производить отладку программ в двух режимах: режиме Module и режиме CPU. Программист выбирает конкретный режим в зависимости от своих привычек. Авторы предпочитают режим CPU, который опишем. Когда попадаем в Turbo Debugger, окно CPU (оно светло-зеленое) уже открыто. Если это не так, то зайдите в меню View и выберите в нем пункт CPU.

Окно CPU состоит из пяти подокон, как показано на рис. 5.12.

Окно программы

Окно

регистров

Окно флагов

 

Окно данных

 

Окно стека

 

Рис. 5.12

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

Окно, в котором в настоящий момент находится курсор, является активным. Перемещать курсор между окнами, меняя тем самым активное окно, можно, например, с помощью клавиши TAB. Внутри любого окна можно перемещаться (перемещать курсор) с помощью клавиш‑стрелокPgUp и PgDn. Увеличить (уменьшить) размер  окон можно с помощью клавиши F5. Для активного окна можно вызвать контекстное меню, нажав ALTF10.

Сама отладка программы (ее пошаговое выполнение) производится, когда активным является окно программы. При этом нажатие клавиши F8 или F7 приводит к выполнению одной очередной команды. Выполнится при этом необязательно та команда, на которой стоит курсор. Очередной командой, которая и будет выполнена, является команда, на которую указывает пара cs:ip (содержимое этих регистров отражено в окне регистров).

Разница между клавишами F7 и F8 заключается в обработке таких команд, как loop (цикл), call (вызов подпрограммы) и int n (вызов обработчика прерывания). При нажатии F8 весь цикл (или подпрограмма, или…) выполняется как одна единственная команда, т.е. не попадаем внутрь цикла подпрограммы или обработчика, а вот при нгажатии клавиши F7 туда попадаем и выполняем цикл или подпрограмму пошагово (даже если после этого мы жмем F8). Рекомендуем всегда использовать клавишу F8, а клавишей F7 пользоваться, только если надо посмотреть, что происходит внутри цикла или подпрограммы. Не советуем нажимать F7 на командах int n. Вы попадете внутрь обработчика (который написан не вами и ничего полезного не даст) и вряд ли оттуда выберетесь.

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

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

Иногда нужно вручную изменить содержимое некоторого регистра. Сделайте активным окно регистров (клавишей TAB), установите курсор на нужный регистр и начинайте набирать новое содержимое. На экране автоматически появится окно (Enter new value), в которое Вы введете нужную информацию, после чего нажмите ENTER. Не забудьте, информация вводится в шестнадцатеричной форме, и если вводимое число начинается с буквы, перед буквой должен быть введен ноль.

Исходно в окне регистров отображается содержимое 16-разрядных регистров (ax, cx,…). А что, если работаем с 32-разрядными регистрами (eax, ecx, …), как посмотреть их содержимое? Переходим в окно регистров (TAB), вызываем контекстное меню (ALTF10), находим в нем пункт Registers 32 ‑ bit No, подгоняем на него курсор и нажимаем ENTER.

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

Очень часто в окне данных высвечивается совсем не та информация, которая нас интересует (хотим наблюдать, как меняются значения наших переменных, а отладчик показывает нам начальную область PSP). Переходим в окно данных, вызываем контекстное меню (ALTF10), выбираем в нем пункт Go To, появляется окно (Enter address to position to), в которое мы вводим ds:100h (для СОМ-программы) или ds:0 (для EXE-программы). После чего нажимаем ENTER. Отметим, что для EXE-программы данную операцию надо делать, после того как ds настроен на начало сегмента данных, т.е. после выполнения команд

mov ax, data

mov ds, ax

Изменить вручную значение любого байта в окне данных можно, например, следующим образом. Переходим в окно данных, устанавливаем курсор на нужном байте, начинаем вводить новое значение. Появляется окно Enter new data bytes. Вводим в это окно нужное число и нажимаем ENTER. Не забудьте, информация вводится в шестнадцатеричной форме, и если вводимое число начинается с буквы, то перед буквой должен быть введен ноль.

Этот минимальный инструментарий позволяет успешно производить отладку программ.

Остался неосвещенным только один важный вопрос: а как загрузить в отладчик нашу СОМ- (или EXE)-программу? Проще всего поступить следующим образом. Поместить отладчик и нашу программу (пусть, для определенности, она называется lab1.com) в одну директорию, а затем набрать в командной строке:

td.exe laba1.com

после чего нажать ENTER. Выполнив эти действия, попадаем в Turbo Debugger, причем наша программа уже загружена в окно программы. Кроме того, на экране, скорее всего, увидим надпись Program has no symbol table. Не обращайте на нее внимания, нажмите ESC и можете выполнять отладку программы.