Блочные устройства
Блочные устройства — это устройства, на которых DOS может организовать файловую систему. DOS не работает напрямую с дисками через BIOS, а только с драйверами блочных устройств, каждое из которых представляется системе как линейный массив секторов определенной длины (обычно 512 байт) с произвольным доступом (для BIOS, к примеру, диск — это четырехмерный массив секторов, дорожек, цилиндров и головок). Каждому загруженному устройству DOS присваивает один или несколько номеров логических дисков, которые соответствуют буквам, используемым для обращения к ним. Так, стандартный драйвер дисков получает буквы «А», «В», «С» и так далее, по числу видимых разделов на диске.
Рассмотрим атрибуты и команды, которые передаются блочным устройствам.
Атрибуты:
бит 15: 0 (признак блочного устройства)
бит 14: поддерживаются IOCTL-чтение и запись
бит 13: не требует копию первого сектора FAT, чтобы построить ВРВ
бит 12: сетевой диск
бит 11: поддерживает команды открыть/закрыть устройство и проверить, является ли устройство сменным
биты 10 – 8: 000
бит 7: поддерживается проверка поддержки IOCTL
бит 6: поддерживается обобщенный IOCTL и команды установить и определить номер логического диска
биты 5 – 2: 0000
бит 1: поддерживаются 32-битные номера секторов
бит 0: 0
Команды и структура переменной части буфера запроса для них (только то, что отличается от аналогичных структур для символьных устройств):
00h: Инициализация
+0Dh: байт — количество устройств, которые поддерживает драйвер
+12h: 4 байта — дальний адрес массива ВРВ-структур (по одной для каждого устройства)
ВРВ — это 25-байтная структура (53 для FAT32), которая описывает блочное устройство. Ее можно найти по смещению 0Bh от начала нулевого сектора на любом диске:
+0: 2 байта — число байт в секторе (обычно 512)
+2: байт — число секторов в кластере ( DOS выделяет пространство на диске для файлов не секторами, а обычно более крупными единицами — кластерами. Даже самый маленький файл занимает один кластер)
+3: 2 байта — число секторов до начала FAT (обычно один — загрузочный)
+5: байт — число копий FAT (обычно 2) (FAT — это список кластеров, в которых расположен каждый файл, DOS делает вторую копию, чтобы можно было восстановить диск, если произошел сбой как раз при модификации FAT)
+6: 2 байта — максимальное число файлов в корневой директории
+8: 2 байта — число секторов на устройстве (если их больше 65 536 — здесь записан 0)
+0Ah: байт — описатель носителя (F8h — для жестких дисков, F0h — для дискет на 1,2 Мб и 1,44 Мб, а также других устройств)
+0Bh: 2 байта — число секторов в одной копии FAT (0, если больше 65 535)
+0Dh: 2 байта — число секторов на дорожке (для доступа средствами BIOS)
+0Fh: 2 байта — число головок (для доступа средствами BIOS)
+11h: 4 байта — число скрытых секторов
+15h: 4 байта — 32-битное число секторов на диске
(следующие поля действительны только для дисков, использующих FAT32)
+16h: 4 байта — 32-битное число секторов в FAT
+1Dh: байт — флаги
бит 7: не обновлять резервные копии FAT
биты 3 – 0: номер активной FAT, если бит 7 = 1
+1Fh: 2 байта — версия файловой системы (0000h для Windows 95 OSR2)
+21h: 4 байта — номер кластера корневой директории
+25h: 2 байта — номер сектора с информацией о файловой системе (FFFFh, если он отсутствует)
+27h: 2 байта — номер сектора запасной копии загрузочного сектора (FFFFh, если отсутствует)
+29h: 12 байт — зарезервировано
Для всех остальных команд в поле буфера запроса со смещением +1 размещается номер логического устройства из числа обслуживаемых драйвером, к которому относится команда:
01h: Проверка носителя
+0Dh: байт
на входе — описатель носителя
на выходе
0FFh — если диск был сменен
01h — если диск не был сменен
00h — если это нельзя определить
+0Fh: 4 байта — адрес ASCIZ-строки с меткой диска (если установлен бит 11 в атрибуте)
02h: Построить ВРВ
+0Dh: описатель носителя
+0Eh: 4 байта
на входе — дальний адрес копии первого сектора FAT
на выходе — дальний адрес ВРВ
03h: IOCTL-чтение (если установлен бит 14 атрибута)
04h: Чтение из устройства
+0Dh: байт — описатель носителя
+12h: 2 байта
на входе — число секторов, которые надо прочитать
на выходе — число прочитанных секторов
+16h: 2 байта — первый сектор (если больше 65 535 — здесь FFFFh)
+18h: 4 байта — на выходе — адрес метки диска, если произошла ошибка 0Fh
+1Ch: 4 байта — первый сектор
08h: Запись в устройство. Структура буфера аналогична 04h с точностью до замены чтения на запись
09h: Запись в устройство с проверкой. Аналогично 08h
0Ch: IOCTL-запись (если установлен бит 14 атрибута)
0Dh: Открыть устройство (если установлен бит 11 атрибута)
0Eh: Закрыть устройство (если установлен бит 11 атрибута)
0Fh: Проверка наличия сменного диска (если установлен бит 11 атрибута). Драйвер должен установить бит 9 слова состояния, если диск сменный, и сбросить, если нет.
13h: Обобщенный IOCTL (если установлен бит 6 атрибута)
+0Dh: байт — категория устройства:
08h: дисковое устройство
48h: дисковое устройство с FAT32
+0Eh: код подфункции:
40h: установить параметры
60h: прочитать параметры
41h: записать дорожку
42h: отформатировать и проверить дорожку
62h: проверить дорожку
46h: установить номер тома
66h: считать номер тома
47h: установить флаг доступа
67h: прочитать флаг доступа
68h: определить тип носителя (DOS 5.0+)
4Ah: заблокировать логический диск (Windows 95)
6Ah: разблокировать логический диск (Windows 95)
4Bh: заблокировать физический диск (Windows 95)
6Bh: разблокировать физический диск (Windows 95)
6Ch: определить флаг блокировки (Windows 95)
6Dh: перечислить открытые файлы (Windows 95)
6Eh: найти файл подкачки (Windows 95)
6Fh: получить соотношение логических и физических дисков (Windows 95)
70h: получить текущее состояние блокировки (Windows 95)
71h: получить адрес первого кластера (Windows 95)
+13h: адрес структуры (аналогично INT 21h AX = 440Dh)
17h: Определить логический диск (если установлен бит 6 атрибута)
+01h: байт
на входе — номер устройства
на выходе — его номер диска (1 – А, 2 – В)
18h: Установить логический диск (если установлен бит 6 атрибута)
+01h: байт — номер устройства. (Команды 17h и 18h позволяют DOS обращаться к одному и тому же дисководу как к устройству А: и как к устройству В:)
19h: Поддержка функций IOCTL (если установлены биты 6 и 7 атрибута)
Для написания своего драйвера блочного устройства можно пользоваться схемой, аналогичной символьному драйверу из предыдущей главы. Единственное важное отличие — процедура инициализации должна будет подготовить и заполнить ВРВ, а также сообщить DOS число устройств, для которых действует этот драйвер.
Более сложные приемы программирования
Все примеры программ из предыдущей главы в первую очередь предназначались для демонстрации работы с теми или иными основными устройствами компьютера при помощи средств, предоставляемых DOS и BIOS. В этой главе рассказано о том, что и в области собственно программирования ассемблер позволяет больше, чем любой другой язык, и рассмотрены те задачи, решая которые, принято использовать язык ассемблера при программировании для DOS.
Целочисленная арифметика повышенной точности
Языки высокого уровня обычно ограничены в наборе типов данных, с которыми они могут работать, — для хранения целых чисел применяются отдельные байты, слова или двойные слова. Используя ассемблер, можно придумать тип данных совершенно любого размера (64 бита, 128 бит, 1024 бита) и легко определить все арифметические операции с такими числами.
Часы реального времени и CMOS-память
В каждом компьютере есть микросхема, отвечающая за поддержку текущей даты и времени. Для того чтобы они не сбрасывались при каждом выключении питания, на микросхеме расположена небольшая область памяти (от 64 до 128 байт), выполненная по технологии CMOS, позволяющей снизить энергопотребление до минимума (фактически энергия в таких схемах затрачивается только на зарядку паразитных емкостей при изменении состояния ячеек памяти). Вся эта микросхема получает питание от аккумулятора, расположенного на материнской плате, и не отключается при. выключении компьютера. Для хранения собственно времени достаточно всего четырнадцати байт такой энергонезависимой памяти, и остальная ее часть используется BIOS для хранения различной информации, необходимой для корректного запуска компьютера. Для общения с CMOS и регистрами RTC выделяются порты ввода-вывода от 70h до 7Fh, но только назначение портов 70h и 71h одинаково для всех материнских плат:
порт 70h для записи: индекс для выбора регистра CMOS:
бит 7: прерывание NMI запрещено на время чтения/записи
бит 6: собственно индекс
порт 71h для чтения и записи: данные CMOS
После записи в порт 70h обязательно надо выполнить запись или чтение из порта 71h, иначе RTC окажется в неопределенном состоянии. Содержимое регистров CMOS варьируется для разных BIOS, но первые 33h регистра обычно выполняют следующие функции:
00h: RTC — текущая секунда (00 – 59h или 00 – 3Bh) — формат выбирается регистром 0Bh, по умолчанию — BCD
01h: RTC — секунды будильника (00 – 59h или 00 – 3Bh или FFh (любая секунда))
02h: RTC — текущая минута (00 – 59h или 00 – 3Bh)
03h: RTC — минуты будильника (00 – 59h или 00 – 3Bh или FFh)
04h: RTC — текущий час:
0 – 23h/00 – 17h (24-часовой режим)
1 – 12h/01 – 1Ch (12-часовой режим до полудня)
81h – 92h/81 – 8Ch (12-часовой режим после полудня)
05h: RTC — часы будильника ( то же или FFh, если любой час)
06h: RTC — текущий день недели (1 – 7, 1 — воскресенье)
07h: RTC — текущий день месяца (01 – 31h/01h – 1Fh)
08h: RTC — текущий месяц (01 – 12h/01 – 0Ch)
09h: RTC — текущий год (00 – 99h/00 – 63h)
0Ah: RTC — регистр состояния А
бит 7: 1 — часы заняты (происходит обновление)
биты 4 – 6: делитель фазы (010 — 32 768 KHz — по умолчанию)
биты 3 – 0: выбор частоты периодического прерывания:
0000 — выключено
0011 — 122 микросекунды (минимум)
1111 — 500 миллисекунд
0110 — 976,562 микросекунды (1024 Hz)
0Bh: RTC — регистр состояния В
бит 7: запрещено обновление часов (устанавливают перед записью новых значений в регистры даты и часов)
бит 6: вызов периодического прерывания (IRQ8)
бит 5: вызов прерывания при срабатывании будильника
бит 4: вызов прерывания по окончании обновления времени
бит 3: включена генерация прямоугольных импульсов
бит 2: 1/0 — формат даты и времени двоичный/BCD
бит 1: 1/0 — 24-часовой/12-часовой режим
бит 0: автоматический переход на летнее время в апреле и октябре
0Ch только для чтения: RTC — регистр состояния С
бит 7: произошло прерывание
бит 6: разрешено периодическое прерывание
бит 5: разрешено прерывание от будильника
бит 4: разрешено прерывание по окончании обновления часов
0Dh только для чтения: регистр состояния D
бит 7: питание RTC/CMOS есть
0Eh: результат работы POST при последнем старте компьютера:
бит 7: RTC сбросились из-за отсутствия питания CMOS
бит 6: неверная контрольная сумма CMOS-конфигурации
бит 5: неверная конфигурация
бит 4: размер памяти не совпадает с записанным в конфигурации
бит 3: ошибка инициализации первого жесткого диска
бит 2: RTC-время установлено неверно (например, 30 февраля)
0Fh: состояние, в котором находился компьютер перед последней перезагрузкой
00h — Ctr-Alt-Del
05h — INT 19h
0Ah, 0Bh, 0Ch — jmp, iret, retf на адрес, хранящийся в 0040h:0067h
Другие значения указывают, что перезагрузка произошла в ходе POST или в других необычных условиях
10h: тип дисководов (биты 7 – 4 и 3 – 0 — типы первого и второго дисковода)
0000: отсутствует
0001: 360 Кб
0010: 1,2 Мб
0011: 720 Кб
0100: 1,44 Мб
0101: 2,88 Мб
12h: тип жестких дисков (биты 7 – 4 и 3 – 0 — типы первого и второго жестких дисков, 1111, если номер типа больше 15)
14h: байт состояния оборудования
биты 7 – 6: число установленных жестких дисков минус один
биты 5 – 4: тип монитора (00, 01, 10, 11 — EGA/VGA, 40x25 CGA, 80x25 CGA, MDA)
бит 3: монитор присутствует
бит 2: клавиатура присутствует
бит 1: FPU присутствует
бит 0: дисковод присутствует
15h: младший байт размера базовой памяти в килобайтах (80h)
16h: старший байт размера базовой памяти в килобайтах (02h)
17h: младший байт размера дополнительной памяти (выше 1 Мб) в килобайтах
18h: старший байт размера дополнительной памяти (выше 1 Мб) в килобайтах
19h: тип первого жесткого диска, если больше 15
lAh: тип второго жесткого диска, если больше 15
2Eh: старший байт контрольной суммы регистров 10h – 2Dh
2Fh: младший байт контрольной суммы регистров 10h – 2Dh
30h: младший байт найденной при POST дополнительной памяти в килобайтах
31h: старший байт найденной при POST дополнительной памяти в килобайтах
32h: первые две цифры года в BCD-формате
Данные о конфигурации, хранящиеся в защищенной контрольной суммой области, бывают нужны достаточно редко, а для простых операций с часами реального времени и будильником удобно использовать прерывание BIOS 1Ah. Однако, программируя RTC на уровне портов, можно активизировать периодическое прерывание — режим, в котором RTC вызывает прерывание IRQ8 с заданной частотой, что позволит оставить IRQ0 для работы системы, если вас удовлетворяет ограниченный выбор частот периодического прерывания. В качестве примера посмотрим, как выполняются чтение и запись в CMOS-память.
; rtctime,asm ; Вывод на экран текущей даты и времени из RTC ; .model tiny .code .186 ; для shr al,4 org 100h ; СОМ-программа start: mov al,0Bh ; CMOS OBh - управляющий регистр В out 70h,al ; порт 70h - индекс CMOS in al,71h ; порт 71h - данные CMOS and al,11111011b ; обнулить бит 2 (форма чисел - BCD) out 71h,al ; и записать обратно mov al,32h ; CMOS 32h - две старшие цифры года call print_cmos ; вывод на экран mov al,9 ; CMOS 09h - две младшие цифры года call print_cmos mov al,'-' ; минус int 29h ; вывод на экран mov al,8 ; CMOS 08h - текущий месяц call print_cmos mov al,'-' ; еще один минус int 29h mov al,7 ; CMOS 07h - день call print_cmos mov al,' ' ; пробел int 29h mov al,4 ; CMOS 04h - час call print_cmos mov al,'h' ; буква "h" int 29h mov al,' ' ; пробел int 29h mov al,2 ; CMOS 02h - минута call print_cmos mov al,':' ; двоеточие int 29h mov al,0h ; CMOS 00h - секунда call print_cmos ret
; процедура print_cmos ; выводит на экран содержимое ячейки CMOS с номером в AL ; считает, что число, читаемое из CMOS, находится в формате BCD print_cmos proc near out 70h,al ; послать AL в индексный порт CMOS in al,71h ; прочитать данные push ax shr al,4 ; выделить старшие четыре бита add al,'0' ; добавить ASCII-код цифры 0 int 29h ; вывести на экран pop ax and al,0Fh ; выделить младшие четыре бита add al,30h ; добавить ASCII-код цифры 0 int 29h ; вывести на экран ret print_cmos endp end start
Циклы
Несмотря на то что набор команд Intel включает команды организации циклов, они годятся только для одного типа циклов — FOR-циклов, которые выполняются фиксированное число раз. В общем виде любой цикл записывается в ассемблере как условный переход.
WHILE-цикл: (команды инициализации цикла) метка: IF (не выполняется условие окончания цикла) THEN (команды тела цикла) jmp метка
REPEAT/UNTIL-цикл: (команды инициализации цикла) метка: (команды тела цикла) IF (не выполняется условие окончания цикла) THEN (переход на метку)
(такие циклы выполняется быстрее на ассемблере, и всегда следует стремиться переносить проверку условия окончания цикла в конец)
LOOP/ENDLOOP-цикл: (команды инициализации цикла) метка: (команды тела цикла) IF (выполняется условие окончания цикла) THEN jmp метка2 (команды тела цикла) jmp метка метка2:
Деление
Общий алгоритм деления числа любого размера на число любого размера нельзя построить с использованием команды DIV — такие операции выполняются при помощи сдвигов и вычитаний и оказываются весьма сложными. Рассмотрим сначала менее общую операцию (деление любого числа на слово или двойное слово), которую можно легко выполнить с помощью команд DIV:
; деление 64-битного числа divident на 16-битное число divisor. ; Частное помещается в 64-битную переменную quotent, ; а остаток - в 16-битную переменную modulo mov ax,word ptr divident[6] xor dx,dx div divisor mov word ptr quotent[6],ax mov ax,word ptr divident[4] div divisor mov word ptr quotent[4],ax mov ax,word ptr divident[2] div divisor mov word ptr quotent[2],ax mov ax,word ptr divident div divisor mov word ptr quotent,ax mov modulo,dx
Деление любого другого числа полностью аналогично — достаточно только добавить нужное число троек команд mov/div/mov в начало алгоритма.
Наиболее очевидный алгоритм для деления чисел любого размера на числа любого размера — деление в столбик с помощью последовательных вычитаний делителя (сдвинутого влево на соответствующее количество разрядов) из делимого, увеличивая соответствующий разряд частного на 1 при каждом вычитании, пока не останется число, меньшее делителя (остаток):
; деление 64-битного числа в EDX:EAX на 64-битное число в ЕСХ:ЕВХ. ; Частное помещается в EDX:EAX, и остаток - в ESI:EDI mov ebp,64 ; счетчик бит xor esi,esi xor edi,edi ; остаток = 0 bitloop: shl eax,1 rcl edx,1 rcl edi,1 ; сдвиг на 1 бит влево 128-битного числа rcl esi,1 ; ESI:EDI:EDX:EAX cmp esi,ecx ; сравнить старшие двойные слова ja divide jb next cmp edi,ebx ; сравнить младшие двойные слова jb next divide: sub edi,ebx sbb esi,ecx ; ESI:EDI = EBX:ECX inc eax ; установить младший бит в ЕАХ next: dec ebp ; повторить цикл 64 раза jne bitloop
Несмотря на то что этот алгоритм не использует сложных команд, он выполняется на порядок дольше, чем одна команда DIV.
Число, записанное с фиксированной запятой в формате 16:16, можно представить как число, умноженное на 216. Если разделить такие числа друг на друга сразу — мы получим результат деления целых чисел: (А * 216)/(В * 216) = А/В. Чтобы результат имел нужный нам вид (А/В) * 216, надо заранее умножить делимое на 216:
; деление числа с фиксированной запятой в формате 16:16 ; в регистре ЕАХ на такое же число в ЕВХ, без знака: xor edx,edx ror еах,16 xchg ax,dx ; EDX:ЕАХ = ЕАХ * 216
div ebx ; ЕАХ = результат деления
; деление числа с фиксированной запятой в формате 16:16 ; в регистре ЕАХ на такое же число в ЕВХ, со знаком: cdq ror еах,16 xchg ax,dx ; EDX:ЕАХ = ЕАХ * 216
idiv ebx ; ЕАХ = результат деления
Динамик
Как сказано в главе 5.10.5, канал 2 системного таймера управляет динамиком компьютера — он генерирует прямоугольные импульсы с частотой, равной 1 193 180/начальное_значение_счетчика. При программировании динамика начальное значение счетчика таймера принято называть делителем частоты — считается, что динамик работает с частотой, равной 1 193 180/делитель герц. После программирования канала 2 таймера надо еще включить сам динамик — это делается путем установки бит 0 и 1 порта 61h в 1. Бит 0 фактически разрешает работу этого канала таймера, а бит 1 включает динамик.
; Процедура beep ; издает звук с частотой 261 Hz (нота "ми" средней октавы) ; длительностью 1/2 секунды на динамике beep proc near mov al,10110110b ; канал 2, режим 3 out 43h,al mov al,0Dh ; младший байт делителя ; частоты 11D0h out 42h,al mov al,11h ; старший байт делителя частоты out 42h,al in al,61h ; текущее состояние ; порта 61h в AL or al,00000011b ; установить биты 0 и 1 в 1 out 61h,al ; теперь динамик включен mov cx,0007h ; старшее слово числа микросекунд паузы mov dx,0A120h ; младшее слово числа микросекунд паузы mov ah,86h ; функция 86h int 15h ; пауза
in al,61h and al,11111100b ; обнулить младшие два бита out 61h,al ; теперь динамик выключен ret beep endp
В связи с повсеместным распространением звуковых плат обычный динамик PC теперь практически никем не используется или используется для выдачи сообщений об ошибках. Вернемся к звуку чуть позже, а пока вспомним, что в главе 4.7.1 рассматривалось еще одно устройство, которое использовалось для определения текущего времени и даты, — часы реального времени.
Драйверы устройств в DOS
Итак, в предыдущих разделах говорилось о том, как происходит работа с некоторыми устройствами на самом низком уровне — уровне портов ввода-вывода. Однако прикладные программы обычно никогда не используют это уровень, а обращаются ко всем устройствам через средства операционной системы. DOS, в свою очередь, обращается к средствам BIOS, которые осуществляют взаимодействие на уровне портов со всеми стандартными устройствами. Фактически процедуры BIOS и выполняют функции драйверов устройств — программ, осуществляющих интерфейс между операционной системой и аппаратной частью компьютера. BIOS обычно лучше всего известно, как управлять устройствами, которые поставляются вместе с компьютером, но, если требуется подключить новое устройство, о котором BIOS ничего не знает, появляется необходимость в специально написанном загружаемом драйвере.
Драйверы устройств в DOS — это исполнимые файлы со специальной структурой, которые загружаются на этапе запуска (при выполнении команд DEVICE или DEVICEHIGH файла config.sys) и становятся фактически частью системы. Драйвер всегда начинается с 18-байтного заголовка:
+00: 4 байта — дальний адрес следующего загружаемого драйвера DOS — так как в момент загрузки драйвер будет последним в цепочке, адрес должен быть равен FFFFh:FFFFh
+04: 2 байта — атрибуты драйвера
+06: 2 байта — адрес процедуры стратегии
+08: 2 байта — адрес процедуры прерывания
+0Ah: 8 байт — имя драйвера для символьных устройств (дополненное пробелами).
Для блочных устройств — байт по смещению 0Ah содержит число устройств, поддерживаемых этим драйвером, а остальные байты могут содержать имя драйвера.
Здесь следует заметить, что DOS поддерживает два типа драйверов — драйвер символьного устройства и драйвер блочного устройства. Первый тип используется для любых устройств — клавиатуры, принтера, сети, а второй — только для устройств, на которых могут существовать файловые системы, то есть для дисководов, RAM-дисков, нестандартных жестких дисков, для доступа к разделам диска, занятым другими операционными системами, и т.д. Чтобы обратиться к символьному устройству, программа должна открыть его при помощи функции DOS «открыть файл или устройство», а чтобы обратиться к блочному устройству — просто обратиться к соответствующему логическому диску.
Итак, код драйвера устройства представляет собой обычный код программы, как и в случае с СОМ-файлом, но в начале не надо размещать директиву org 100h для пропуска PSP. Можно также объединить драйвер и исполнимую программу, разместив в ЕХЕ-файле код драйвера с нулевым смещением от начала сегмента, а точку входа самой программы ниже.
При обращении к драйверу DOS сначала вызывает процедуру стратегии (адрес по смещению 06 в заголовке), передавая ей адрес буфера запроса, содержащий все параметры, передаваемые драйверу, а затем процедуру прерывания (адрес по смещению 08) без каких-либо параметров. Процедура стратегии должна сохранить адрес буфера запроса, а процедура прерывания — собственно выполнить все необходимые действия. Структура буфера запроса меняется в зависимости от типа команды, передаваемой драйверу, но структура его заголовка остаетсй постоянной:
+00h: байт — длина буфера запроса (включая заголовок)
+01h: байт — номер устройства (для блочных устройств)
+02h: байт — код команды (00h – 19h)
+03h: 2 байта — слово состояния драйвера — должно быть заполнено драйвером
бит 15: произошла ошибка
биты 10 – 14: 00000
бит 9: устройство занято
бит 8: команда обслужена
биты 7 – 0: код ошибки
00h: устройство защищено от записи
01h: неизвестное устройство
02h: устройство не готово
03h: неизвестная команда
04h: ошибка CRC
05h: ошибка в буфере запроса
06h: ошибка поиска
07h: неизвестный носитель
08h: сектор не найден
09h: нет бумаги
0Ah: общая ошибка записи
0Bh: общая ошибка чтения
0Ch: общая ошибка
0Fh: неожиданная смена диска
+05h: 8 байт — зарезервировано
+0Dh: отсюда начинается область данных, различающаяся для разных команд
Даже если драйвер не поддерживает запрошенную от него функцию, он обязательно должен установить бит 8 слова состояния в 1.
Рассмотрим символьные и блочные драйверы на конкретных примерах.
Джойстик
И напоследок — о программировании джойстика. Джойстик подключается к еще одному, помимо последовательного и параллельного, внешнему порту компьютера — к игровому. Для игрового порта зарезервировано пространство портов ввода-вывода от 200h до 20Fh, но для общения с джойстиком используется всего один порт 201h, чтение из которого возвращает состояние джойстика:
порт 20lh для чтения:
биты 7, 6: состояние кнопок 2, 1 джойстика В
биты 5, 4: состояние кнопок 2, 1 джойстика А
биты 3, 2: у- и х-координаты джойстика В
биты 1, 0: у- и х-координаты джойстика А
С состоянием кнопок все просто — достаточно прочитать байт из порта 201h и определить значение нужных бит. Но чтобы определить координату джойстика, придется выполнить весьма неудобную и медленную операцию: надо записать в порт 201h любое число и засечь время, постоянно считывая состояние джойстика. Сразу после записи в порт биты координат будут равны нулю, и время, за которое они обращаются в 1, пропорционально соответствующей координате (Х-координаты растут слева направо, а Y-координаты — сверху вниз).
Если джойстик отсутствует, биты координат или будут единицами с самого начала, или будут оставаться нулями неопределенно долго. Кроме того, после записи в порт 201h нельзя писать в него еще раз, пока хотя бы один из четырех координатных бит не обратился в 1.
BIOS предоставляет функцию для работы с джойстиком — функцию 84h прерывания 15h, но работа напрямую с портами оказывается гораздо быстрее и ненамного сложнее. Например, чтобы определить координаты джойстика, BIOS выполняет целых четыре цикла измерения координат, по одному на каждую.
Чтобы получить значение координаты в разумных единицах, мы будем определять, на сколько изменилось значение счетчика канала 0 системного таймера, и делить это число на 16 — это будет в точности то число, которое возвращает BIOS. Для стандартного джойстика (150 кОм) оно должно быть в пределах 0 – 416, хотя обычно максимальное значение оказывается около 150. Так как аналоговые джойстики — не точные устройства, координаты для одной и той же позиции могут изменяться на 1 – 2, и это надо учитывать, особенно при определении состояния покоя.
Покажем, как все это можно реализовать на примере чтения координат джойстика А:
; процедура read_joystick ; определяет текущие координаты джойстика А ; Вывод: ВР - Y-координата, ВХ - Х-координата (-1, если джойстик ; не отвечает), регистры не сохраняются readjoystick proc near pushf ; сохранить флаги cli ; и отключить прерывания, так как ; измеряется время выполнения кода и не ; нужно измерять еще и время выполнения ; обработчиков прерываний mov bx,-1 ; значение X, если джойстик не ответит mov bp,bx ; значение Y, если джойстик не ответит mov dx,201h ; порт
mov al,0 out 43h,al ; зафиксировать счетчик канала 0 таймера in al,40h mov ah,al in al,40h xchg ah,al ; AX - значение счетчика mov di,ax ; записать его в DI
out dx,al ; запустить измерение координат джойстика in al,dx ; прочитать начальное состояние координат and al,011b mov cl,al ; записать его в CL read_joystick_loop: mov al,0 out 43h,al ; зафиксировать счетчик канала 0 таймера in al,40h mov ah,al in al,40h xchg ah,al ; AX - значение счетчика mov si,di ; SI - начальное значение счетчика sub si,ax ; SI - разница во времени cmp si,1FF0h ; если наступил тайм-аут ; (значение взято из процедуры BIOS), ja exit_readj ; выйти из процедуры, in al,dx ; иначе: прочитать состояние джойстика and al,0011b cmp al,cl ; сравнить его с предыдущим je read_joystick_loop xchg al,cl ; поместить новое значение в CL xor al,cl ; и определить изменившийся бит, test al,01b ; если это Х-координата, jz x_same mov bx,si ; записать Х-координату в ВХ, x_same: test al,10b ; если это Y-координата, jz read_joystick_loop mov bp,si ; записать Y-координату в ВР
exit_readj: test bx,bx ; проверить, равен ли ВХ -1, js bx_bad shr bx,4 ; если нет - разделить на 16, bx_bad: test bp,bp ; проверить, равен ли ВР -1, js bp_bad shr bp,4 ; если нет - разделить на 16 bp_bad: popf ret read_joystick endp
Если вы когда-нибудь играли с помощью джойстика, то наверняка встречались с процедурой калибровки, когда игра предлагает провести джойстик по двум или четырем углам. Это нужно обязательно выполнять, чтобы определить, какие координаты возвращает данный конкретный джойстик для крайних положений, так как даже у одного и того же джойстика эти величины могут со временем изменяться.
Генераторы случайных чисел
Самый часто применяемый тип алгоритмов генерации псевдослучайных последовательностей — линейные конгруэнтные генераторы, описываемые общим рекуррентным соотношением:
Ij+1 = (aIj + с) MOD m
При правильно выбранных числах а и с эта последовательность возвращает все числа от нуля до m–1 псевдослучайным образом и ее периодичность сказывается только на последовательностях порядка m. Такие генераторы очень легко реализуются и работают быстро, но им присущи и некоторые недостатки: самый младший бит намного менее случаен, чем, например, самый старший, а также если попытаться использовать результаты работы этого генератора для заполнения k-мерного пространства, начиная с некоторого k, точки будут лежать на параллельных плоскостях. Оба эти недостатка можно устранить, используя так называемое перемешивание данных: числа, получаемые при работе последовательности, не выводятся сразу, а помещаются в случайно выбранную ячейку небольшой таблицы (8 – 16 чисел), а число, находившееся в этой ячейке раньше, возвращается как результат работы функции.
Если число а подобрано очень тщательно, может оказаться, что число с равно нулю. Так, классический стандартный генератор Льюиса, Гудмана и Миллера использует а = 16 807 (75) при m = 231–1, а генераторы Парка и Миллера используют а = 48 271 и а = 69 621 (при том же m). Любой из этих генераторов можно легко использовать в ассемблере для получения случайного 32-битного числа, достаточно всего двух команд — MUL и DIV.
; Процедура rand ; возвращает в ЕАХ случайное положительное 32-битное число ; (от 0 до 231-2) ; rand proc near push edx mov eax,dword ptr seed ; считать последнее ; случайное число test eax,eax ; проверить его, если это -1, js fetch_seed ; функция еще ни разу не ; вызывалась и надо создать ; начальное значение randomize: mul dword ptr rand_a ; умножить на число а, div dword ptr rand_m ; взять остаток от ; деления на 231-1 mov eax,edx mov dword ptr seed,eax ; сохранить для ; следующих вызовов pop edx ret
fetch_seed: push ds push 0040h pop ds mov eax,dword ptr ds:006Ch ; считать ; двойное слово из области pop ds ; данных BIOS по адресу ; 0040:0060 - текущее число jmp short randomize ; тактов таймера
rand_a dd 69621 rand_m dd 7FFFFFFFh seed dd -1 rand endp
Если период этого генератора (порядка 109) окажется слишком мал, можно скомбинировать два генератора с разными а и m, не имеющими общих делителей, например: a1 = 400 014 с m1 = 2 147 483 563 и а2 = 40 692 с m2 = 2 147 483 399. Генератор, работающий по уравнению
Ij+1 = (a1Ij + a2Ij) MOD m,
где m — любой из m1 и m2, имеет период 2,3 * 1018.
Очевидный недостаток такого генератора — команды MUL и DIV относятся к самым медленным. От DIV можно избавиться, используя один из генераторов с ненулевым числом с и с m, равным степени двойки (тогда DIV m заменяется на AND m–1), например: а = 25 173, с = 13 849, m = 216 или a = 1 664 525, с = 1 013 904 223, m = 232, но проще перейти к методам, основанным на сдвигах или вычитаниях.
Алгоритмы, основанные на вычитаниях, не так подробно изучены, как конгруэнтные, но они достаточно широко используются из-за своей скорости и, по-видимому, не имеют заметных недостатков. Подробное объяснение алгоритма этого генератора (а также алгоритмов многих других генераторов случайных чисел) приведено в книге Кнута Д.Е. «Искусство программирования» (т. 2).
; Процедура srand_init ; инициализирует кольцевой буфер для генератора, использующего вычитания ; ввод: ЕАХ - начальное значение, например из области ; данных BIOS, как в предыдущем примере srand_init proc near push bx push si push edx mov edx,1 ; засеять кольцевой буфер mov bx,216 do_0: mov word ptr ablex[bx],dx sub eax,edx xchg eax,edx sub bx,4 jge do_0
; разогреть генератор mov bx,216 do_1: push bx do_2: mov si,bx add si,120 cmp si,216 jbe skip sub si,216 skip: mov eax,dword ptr tablex[bx] sub eax,dword ptr tablex[si] mov dword ptr tablex[bx},eax sub bx,4 jge do_2 pop bx sub bx,4 jge do_1
; инициализировать индексы sub ax,ax mov word ptr index0,ax mov ax,124 mov index1, ax pop edx pop si pop bx ret srand_init endp
; процедура srand ; возвращает случайное 32-битное число в ЕАХ (от 0 до 232-1) ; перед первым вызовом этой процедуры должна быть один раз вызвана ; процедура srand_init srand proc near push bx push si mov bx,word ptr index0 mov si,word ptr index1 ; считать индексы mov eax,dword ptr tablex[bx] sub eax,dword ptr tablex[si] ; создать новое ; случайное число mov dword ptr tablex[si],eax ; сохранить его ; в кольцевом буфере sub si,4 ; уменьшить индексы, jl fix_si ; перенося их на конец буфера, fixed_si: mov word ptr index1,si ; если они выходят ; за начало sub bx,4 jl fix_bx fixed_bx: mov index0,bx pop si pop bx ret
fix_SI: mov si,216 jmp short fixed_SI fix_BX: mov bx,216 jmp short fixed_BX srand endp
tablex dd 55 dup (?) ; кольцевой буфер случайных чисел index0 dw ? ; индексы для кольцевого буфера index1 dw ?
Часто бывает, что требуется получить всего один или несколько случайных бит, и генераторы, работающие с 32-битными числами, оказываются неэффективными. В этом случае удобно применять алгоритмы, основанные на сдвигах:
; rand8 ; возвращает случайное 8-битное число в AL, ; переменная seed должна быть инициализирована заранее, ; например из области данных BIOS, как в примере ; для конгруэнтного генератора rand8 proc near mov ax, word ptr seed mov cx,8 newbit: mov bx,ax and bx,002Dh xor bh,bl clc jpe shift stc shift: rcr ax,1 loop newbit mov word ptr seed,ax mov ah,0 ret rand8 endp
Клавиатура
Контроллеру клавиатуры соответствуют порты с номерами от 60h до 6Fh, хотя для всех стандартных операций достаточно портов 60h и 61h.
64h для чтения: регистр состояния клавиатуры, возвращает следующий байт:
бит 7: ошибка четности при передаче данных с клавиатуры
бит 6: тайм-аут при приеме
бит 5: тайм-аут при передаче
бит 4: клавиатура закрыта ключом
бит 3: данные, записанные в регистр ввода, — команда
бит 2: самотестирование закончено
бит 1: в буфере ввода есть данные (для контроллера клавиатуры)
бит 0: в буфере вывода есть данные (для компьютера)
При записи в этот порт он играет роль дополнительного регистра управления клавиатурой, но его команды сильно различаются для разных плат и разных BIOS, и мы не будем его подробно рассматривать.
61h для чтения и записи — регистр управления клавиатурой. Если в старший бит этого порта записать значение 1, клавиатура будет заблокирована, если 0 — разблокирована. Другие биты этого порта менять нельзя, так как они управляют другими устройствами (в частности динамиком). Чтобы изменить состояние клавиатуры, надо считать байт из порта, изменить бит 7 и снова записать в порт 61h этот байт.
60h для чтения — порт данных клавиатуры. При чтении из него можно получить скан-код последней нажатой клавиши (см. приложение 1) — именно так лучше всего реализовывать резидентные программы, перехватывающие прерывание IRQ1, так как по этому коду можно определять момент нажатия и отпускания любой клавиши, включая такие клавиши, как Shift/Ctrl/Alt или даже Pause (скан-код отпускания клавиши равен скан-коду нажатия плюс 80h):
int09h_handler: in al,60h ; прочитать скан-код клавиши, cmp al,hot_key ; если это наша "горячая" ; клавиша, jne not_our_key ; перейти к нашему ; обработчику [...] ; наши действия здесь not_our_key: jmp old_int09h ; вызов старого обработчика
Мы пока не можем завершить обработчик просто командой IRET, потому что, во-первых, обработчик аппаратного прерывания клавиатуры должен установить бит 7 порта 61h, а затем вернуть его в исходное состояние, например так:
in al,61h push ax or al,80h out 61h,al pop ax out 61h,al
А во-вторых, он должен сообщить контроллеру прерываний, что обработка аппаратного прерывания закончилась командами
mov al,20h out 20h,al
60h для записи — регистр управления клавиатурой. Байт, записанный в этот порт (если бит 1 в порту 64h равен 0), интерпретируется как команда. Некоторые команды состоят из более чем одного байта — тогда следует дождаться обнуления зтого бита еще раз перед тем, как посылать следующий байт. Перечислим наиболее стандартные команды.
Команда EDh 0?h — изменить состояние светодиодов клавиатуры. Второй байт этой команды определяет новое состояние:
бит 0 — состояние Scroll Lock (1 — включена, 0 — выключена)
бит 1 — состояние Num Lock
бит 2 — состояние Caps Lock
При этом состояние переключателей, которое хранит BIOS в байтах состояния клавиатуры, не изменяется, и при первой возможности обработчик прерывания клавиатуры BIOS восстановит состояние светодиодов.
Команда EEh — эхо-запрос. Клавиатура отвечает скан-кодом EEh.
Команда F3h ??h — Установить параметры режима автоповтора:
бит 7 второго байта команды — 0
биты 6 – 5 устанавливают паузу перед началом автоповтора:
00 = 250ms, 01 = 500ms, 10 = 750ms, 11 = 1000ms
биты 4 – 0 устанавливают скорость автоповтора (символов в секунду):
00000 = 30,0 01111 = 8,0
00010 = 24,0 10010 = 6,0
00100 = 20,0 10100 = 5,0
00111 = 16,0 10111 = 4,0
01000 = 15,0 11010 = 3,0
01010 = 12,0 11111 = 2,0
01100 = 10,0
Все промежуточные значения также имеют смысл и соответствуют промежуточным скоростям, например 00001 = 26,7.
Команда F4h — включить клавиатуру.
Команда F5h — выключить клавиатуру.
Команда F6h — установить параметры по умолчанию.
Команда FEh — послать последний скан-код еще раз.
Команда FFh — выполнить самотестирование.
Клавиатура отвечает на все команды, кроме EEh и FEh, скан-кодом FAh (подтверждение), который поглощается стандартным обработчиком BIOS, так что, если мы не замещаем полностью стандартный обработчик, о его обработке можно не беспокоиться.
В качестве примера работы с клавиатурой напрямую рассмотрим простую программу, выполняющую переключение светодиодов.
; mig.asm ; циклически переключает светодиоды клавиатуры
.model tiny .code org 100h ; СОМ-программа start proc near mov ah,2 ; функция 02 прерывания 1Ah int 1Ah ; получить текущее время mov ch,dh ; сохранить текущую секунду в СН mov cl,0100b ; CL = состояние светодиодов клавиатуры main_loop: call change_LEDs ; установить светодиоды в соответствии с CL shl cl,1 ; следующий светодиод, test cl,1000b ; если единица вышла в бит 3, jz continue mov cl,0001b ; вернуть ее в бит 0, continue: mov ah,1 ; проверить, не была ли нажата клавиша, int 16h jnz exit_loop ; если да - выйти из программы push cx mov ah,2 ; функция 02 прерывания 1Ah int 1Ah ; получить текущее время pop сх cmp ch,dh ; сравнить текущую секунду в DH с СН, mov ch,dh ; скопировать ее в любом случае, je continue ; если это была та же самая секунда - не ; переключать светодиоды, jmp short main_loop ; иначе - переключить светодиоды exit_loop: mov ah,0 ; выход из цикла - была нажата клавиша, int 16h ; считать ее ret ; и завершить программу start endp
; процедура change_LEDs ; устанавливает состояние светодиодов клавиатуры в соответствии с числом в CL change_LEDs proc near call wait_KBin ; ожидание возможности посылки команды mov al,0EDh out 60h,al ; команда клавиатуры EDh call wait_KBin ; ожидание возможности посылки команды mov al,cl out 60h,al ; новое состояние светодиодов ret change_LEDs endp
; процедура wait_KBin ; ожидание возможности ввода команды для клавиатуры wait_KBin proc near in al,64h ; прочитать слово состояния test al,0010b ; бит 1 равен 1? jnz wait_KBin ; если нет - ждать, ret ; если да - выйти wait_KBin endp end start
Команды DSP
E1h
Определить версию DSP. После посылки этой команды в 22Ch надо выполнить чтение двух байт из 22Ah. Первый байт — старший номер версии DSP:
1 — Sound Blaster
2 — Sound Blaster 2.0
3 — Sound Blaster Pro
4 — Sound Blaster 16
10h, N
8-битный непосредственный вывод байта N (число без знака) в DSP Команда впервые появилась на Sound Blaster.
14h, LL, LH
8-битный DMA-вывод блока байт без знака длиной L + 1 (LL — младший байт длины, LH — старший). По окончании вывода блока вызывается аппаратное прерывание. Команда впервые появилась на Sound Blaster.
1Ch
8-битный DMA-вывод с автоинициализацией. Размер блоков задается командой 48h, по окончании вывода каждого блока вызывается аппаратное прерывание. Чтобы остановить DMA-вывод с автоинициализацией, надо воспользоваться командами DAh или 14h. Команда впервые появилась на Sound Blaster 2.0.
90h
8-битный ускоренный DMA-вывод с автоинициализацией. По окончании каждого блока вызывается прерывание, но DSP не будет откликаться ни на какие другие команды. Чтобы выйти из этого режима, надо выполнить сброс и инициализацию DSP заново. Команда впервые появилась на Sound Blaster Pro.
D1h
Включить звук.
Команда впервые появилась на Sound Blaster и не действует, начиная с Sound Blaster 16 (за управление линиями ввода/вывода отвечает микшер).
D3h
Выключить звук.
Команда впервые появилась на Sound Blaster и не действует, начиная с Sound Blaster 16 (за управление линиями ввода/вывода отвечает микшер).
40h, ТС
Установить скорость передачи звука. ТС — старший байт величины 256 (1 000 000/rate), где rate — частота дискретизации (умножить на 2, если используется стереозвук).
Команда впервые появилась на Sound Blaster.
41h, RH, RL
Установить частоту дискретизации (RH — старший байт, RL — младший). Частоту не надо умножать на два в случае стереозвука (допустимые значения — от 5 000 до 45 000 Hz). Частота автоматически округляется до соответствующего целого ТС, как в команде 40h. Команда впервые появилась на Sound Blaster 16.
48h, SL, SH
Установить размер блока для DMA-вывода (SL — младший байт, SH — старший. SH:SL — число байт в блоке минус 1). По окончании каждого блока будет вызываться прерывание.
Команда впервые появилась на Sound Blaster 2.0.
D0h
Приостановить 8-битный DMA. Команда впервые появилась на Sound Blaster.
D4h
Продолжить 8-битный DMA после D0h. Команда впервые появилась на Sound Blaster.
DAh
Закончить 8-битный DMA (после окончания пересылки текущего блока). Команда впервые появилась на Sound Blaster 2.0.
B?h, BM, LL, LH
16-битный DMA-режим. Младшие четыре бита команды B?h выбирают тип режима:
бит 3: 1/0 — ввод/вывод
бит 2: 1/0 — обычный/с автоинициализацией
бит 1: 1/0 — FIFO включено/выключено
бит 0: 0
Команда ВМ выбирает вариант режима:
бит 5: 1/0 — стерео/моно
бит 4: 1/0 — данные рассматриваются как числа со знаком/без знака
LL — младший байт, LH — старший байт числа слов в блоке минус один.
Команда впервые появилась на Sound Blaster 16.
C?h, BM, LL, LH
8-битный DMA-режим.
Эти команды в точности совпадают с B?h, только они опиcывают 8-битную передачу данных и LH:LL — число байт, а не слов в блоке.
Команда впервые появилась на Sound Blaster 16.
D5h
Приостановить 16-битный DMA. Команда впервые появилась на Sound Blaster 16.
D6h
Продолжить 16-битный DMA после D5h. Команда впервые появилась на Sound Blaster 16
D9h
Закончить 16-битный DMA (по окончании пересылки текущего блока). Команда впервые появилась на Sound Blaster 16.
Итак, для вывода звука через звуковую плату может использоваться один из трех режимов. Прямой вывод (команда 10h), когда программа должна сама с нужной частотой посылать отдельные байты из оцифрованного звука в DSP; простой DMA-режим, когда выводится блок данных, после чего вызывается прерывание; и DMA с автоинициализацией, когда данные выводятся непрерывно и после вывода каждого блока вызывается прерывание. Именно в этом порядке увеличивается качество воспроизводимого звука. Так как мы пока не умеем работать с DMA, рассмотрим первый способ.
Чтобы вывести оцифрованные данные с нужной частотой в DSP, придется перепрограммировать канал 0 системного таймера на требуемую частоту и установить собственный обработчик прерывания 08h. При этом будет нарушена работа системных часов, хотя можно не выключать совсем старый обработчик, а передавать ему управление примерно 18,2 раза в секунду, то есть, в частности, при каждом 604-м вызове нашего обработчика на частоте 11 025 Hz. Покажем, как это сделать на примере простой программы, которая именно таким способом воспроизведет файл c:\windows\media\tada.wav (или c:\windows\tada.wav, если вы измените соответствующую директиву EQU в начале программы).
; wavdir.asm ; воспроизводит файл c:\windows\media\tada.wav, не используя DMA, ; нормально работает только под DOS в реальном режиме ; (то есть не в окне DOS (Windows) и не под EMM386, QEMM или другими ; подобными программами)
FILESPEC equ "c:\windows\media\tada.wav" ; имя файла tada.wav с ; полным путем (замените на c:\windows\tada.wav для ; старых версий Windows) SBPORT equ 220h ; базовый порт звуковой платы (замените, если у вас он ; отличается) .model tiny .code .186 ; для pusha/popa org 100h ; СОМ-программа start: call dsp_reset ; сброс и инициализация DSP jc no_blaster mov bl,0D1h ; команда DSP D1h call dsp_write ; включить звук call open_file ; открыть и прочитать tada.wav call hook_int8 ; перехватить прерывание таймера mov bx,5 ; делитель таймера для частоты 22 050 Hz ; (на самом деле соответствует 23 867 Hz) call reprogram_pit ; перепрограммировать таймер
main_loop: ; основной цикл cmp byte ptr finished_flag,0 je main_loop ; выполняется, пока finished_flag ; равен нулю mov bx,0FFFFh ; делитель таймера для частоты 18,2 Hz call reprogram_pit ; перепрограммировать таймер call restore_int8 ; восстановить IRQ0 no_blaster: ret
buffer_addr dw offset buffer ; адрес текущего играемого байта old_int08h dd ? ; старый обработчик INT 08h (IRQ0) finished_flag db 0 ; флаг завершения filename db FILESPEC,0 ; имя файла tada.wav с полным путем
; обработчик INT 08h (IRQ0) ; посылает байты из буфера в звуковую плату int08h_handler proc far pusha ; сохранить регистры, cmp byte ptr cs:finished_flag,1 ; если флаг уже 1, je exit_handler ; ничего не делать, mov di,word ptr cs:buffer_addr ; иначе: DI = адрес текущего байта mov bl,10h ; команда DSP 10h call dsp_write ; непосредственный 8-битный вывод mov bl,byte ptr cs:[di] ; BL = байт данных для вывода call dsp_write inc di ; DI = адрес следующего байта cmp di,offset buffer+27459 ; 27 459 - длина звука в tada.wav, jb not_finished ; если весь буфер пройден, mov byte ptr cs:finished_flag,1 ; установить флаг в 1, not_finished: ; иначе: mov word ptr cs:buffer_addr,di ; сохранить текущий адрес exit_handler: mov al,20h ; завершить обработчик аппаратного ; прерывания, out 20h,al ; послав неспецифичный EOI ; (см. гл. 5.10.10) рора ; восстановить регистры iret int08h_handler endp
; процедура dsp_reset ; сброс и инициализация DSP dsp_reset proc near mov dx,SBPORT+6 ; порт 226h - регистр сброса DSP mov al,1 ; запись единицы в него начинает out dx,al ; инициализацию mov cx,40 ; небольшая пауза dsploop: in al,dx loop dsploop mov al,0 ; запись нуля завершает инициализацию out dx,al ; теперь DSP готов к работе ; проверить, есть ли DSP вообще add dx,8 ; порт 22Eh - состояние буфера чтения DSP mov cx,100 check_port: in al,dx ; прочитать состояние буфера and al,80h ; проверить бит 7 jz port_not_ready ; если ноль - порт еще не готов, sub dx,4 ; иначе: порт 22Аh - чтение данных из DSP in al,dx add dx,4 ; и снова порт 22Eh, cmp al,0AAh ; если причиталось число AAh - ; DSP присутствует и действительно ; готов к работе, je good_reset port_not_ready: loop check_port ; если нет - повторить проверку 100 раз bad_reset: stc ; и сдаться ret ; выход с CF = 1 good_reset: clc ; если инициализация прошла успешно, ret ; выход с CF = 0 dsp_reset endp
; процедура dsp_write ; посылает байт из BL в DSP dsp_write proc near mov dx,SBPORT+0Ch ; порт 22Ch - ввод данных/команд DSP write_loop: ; подождать готовности буфера записи DSP in al,dx ; прочитать порт 22Ch and al,80h ; и проверить бит 7, jnz write_loop ; если он не ноль - подождать еще, mov al,bl ; иначе: out dx,al ; послать данные ret dsp_write endp
; процедура reprogram_pit ; перепрограммирует канал 0 системного таймера на новую частоту ; Ввод: ВХ = делитель частоты reprogram_pit proc near cli ; запретить прерывания mov al,00110110b ; канал 0, запись младшего и старшего байт ; режим работы 3, формат счетчика - двоичный out 43h,al ; послать это в регистр команд первого таймера mov al,bl ; младший байт делителя - out 40h,al ; в регистр данных канала 0 mov al,bh ; и старший байт - out 40h,al ; туда же sti ; теперь IRQO вызывается с частотой ; 1 193 180/ВХ Hz ret reprogram_pit endp
; процедура hook_int8 ; перехватывает прерывание INT 08h (IRQ0) hook_int8 proc near mov ax,3508h ; AH = 35h, AL = номер прерывания int 21h ; получить адрес старого обработчика mov word ptr old_int08h,bx ; сохранить его в old_int08h mov word ptr old_int08h+2,es mov ax,2508h ; AH = 25h, AL = номер прерывания mov dx,offset int08h_handler ; DS:DX - адрес обработчика int 21h ; установить обработчик ret hook_int8 endp
; процедура restore_int8 ; восстанавливает прерывание INT 08h (IRQ0) restore_int8 proc near mov ax,2508h ; AH = 25h, AL = номер прерывания lds dx,dword ptr old_int08h ; DS:DX - адрес обработчика int 21h ; установить старый обработчик ret restore_int8 endp
; процедура open_file ; открывает файл filename и копирует звуковые данные из него, ; считая его файлом tada.wav, в буфер buffer open_file proc near mov ax,3D00h ; AH = 3Dh, AL = 00 mov dx,offset filename ; DS:DX - ASCIZ-имя файла с путем int 21h ; открыть файл для чтения, jc error_exit ; если не удалось открыть файл - выйти mov bx,ax ; идентификатор файла в ВХ mov ax,4200h ; АН = 42h, AL = 0 mov сх,0 ; CX:DX - новое значение указателя mov dx,38h ; по этому адресу начинаются ; данные в tada.wav int 21h ; переместить файловый указатель mov ah,3Fh ; АН = 3Fh mov cx,27459 ; это - длина звуковых данных ; в файле tada.wav mov dx,offset buffer ; DS:DX - адрес буфера int 21h ; чтение файла ret error_exit: ; если не удалось открыть файл mov ah,9 ; АН = 09h mov dx,offset notopenmsg ; DS:DX = сообщение об ошибке int 21h ; открыть файл для чтения int 20h ; конец программы notopenmsg db "Ошибка при открытии файла",0Dh,0Ah,'$' open_file endp
buffer: ; здесь начинается буфер длиной 27 459 байт end start
Если вы скомпилировали программу latency.asm из главы 5.10.5 и попробовали запустить ее в разных условиях, то могли заметить, что под Windows 95, а также под EMM386 и в некоторых других ситуациях пауза между реальным срабатыванием прерывания таймера и запуском обработчика может оказаться весьма значительной и варьироваться с течением времени, так что качество звука, выводимого нашей программой wavdir.asm, окажется под EMM386 очень плохим, а в DOS-задаче под Windows 95 вообще получится протяжный хрип. Чтобы этого избежать, а также чтобы указывать точную скорость оцифровки звука и выводить 16-битный звук, нам надо обратиться к программированию контроллера DMA (пример программы, выводящей звук при помощи DMA, см. в конце следующей главы).
Команды инициализации
Чтобы инициализировать контроллер, BIOS посылает последовательность из команды ICW1 в порт 20h/A0h (она отличается от OCW своим битом 4) и трех команд инициализации ICW2, ICW3, ICW4 в порт 21h/ A1h сразу после этого.
ICW1:
биты 7 – 4: 0001
бит 3: 1/0 — срабатывание по уровню/фронту сигнала IRQ (принято 0)
бит 2: 1/0 — размер вектора прерывания 4 байта/8 байт (1 для 80x86)
бит 1: каскадирования нет, ICW3 не будет послано
бит 0: ICW4 будет послано
ICW2:
номер обработчика прерывания для IRQ0/IRQ8 (кратный восьми) (08h — для первого контроллера, 70h — для второго. Некоторые операционные системы изменяют первый обработчик на 50h)
ICW3 для ведущего контроллера:
биты 7 – 0: к выходу 7 – 0 присоединен подчиненный контроллер (0100b в PC)
ICW3 для подчиненного контроллера:
биты 3 – 0: номер выхода ведущего контроллера, к которому подсоединен ведомый
ICW4:
биты 7 – 5: 0
бит 4: контроллер в режиме фиксированных приоритетов
биты 3 – 2: режим:
00, 01 — небуферированный
10 — буферированный/подчиненный
11 — буферированный/ведущий
бит 1: режим с автоматическим EOI (то есть обработчикам не надо посылать EOI в контроллер)
бит 0: 0 — режим совместимости с 8085, 1 — обычный
Повторив процедуру инициализации, программа может, например, изменить соответствие между обработчиками прерываний и реальными аппаратными прерываниями. Переместив базовый адрес первого контроллера на неиспользуемую область (например, 50h) и установив собственные обработчики на каждое из прерываний INT 50h – 58h, вызывающие обработчики INT 08h – 0Fh, вы сможете быть абсолютно уверены, что никакая программа не установит обработчик аппаратного прерывания, который получил бы управление раньше вашего.
; picinit.asm ; Выполняет полную инициализацию обоих контроллеров прерываний ; с отображением прерываний IRQ0 - IRQ7 на векторы INT 50h - 57h. ; Программа остается резидентной и издает короткий звук после каждого IRQ1. ; Восстановление старых обработчиков прерываний и переинициализация ; контроллера в прежнее состояние опущены
.model tiny .code org 100h ; СОМ-программа
PIC1_BASE equ 50h ; на этот адрес процедура pic_init ; перенесет IRQ0 - IRQ7 PIC2_BASE equ 70h ; на этот адрес процедура pic_init ; перенесет IRQ8 - IRQ15 start: jmp end_of_resident ; переход на начало инсталляционной части
irq0_handler: ; обработчик IRQ0 ; (прерывания от системного таймера) push ax in al,61h and al,11111100b ; выключение динамика out 61h,al pop ax int 08h ; старый обработчик IRQ0 iret ; он послал EOI, так что завершить ; простым iret irq1_handler: ; обработчик IRQ1 push ax ; (прерывание от клавиатуры) in al,61h or al,00000011b ; включение динамика out 61h,al pop ax int 09h ; старый обработчик IRQ1 iret irq2_handler: ; и так далее int 0Ah iret irq3_handler: int 0Bh iret irq4_handler: int 0Ch iret irq5_handler: int 0Dh iret irq6_handler: int 0Eh iret irq7_handler: int 0Fh iret
end_of_resident: ; конец резидентной части call hook_pid_ints ; установка наших обработчиков call init_pic ; переинициализация контроллера прерываний mov dx,offset end_of_resident int 27h ; оставить наши новые обработчики ; резидентными
; процедура init_pic ; выполняет инициализацию обоих контроллеров прерываний, ; отображая IRQ0 - IRQ7 на PIC1_BASE - PIC1_BASE+7, ; a IRQ8 - IRQ15 на PIC2_BASE - PIC2_BASE+7 ; для возврата в стандартное состояние вызвать с ; PIC1_BASE = 08h ; PIC2_BASE = 70h init_picproc near cli mov al,00010101b ; ICW1 out 20h,al out 0A0h,al mov al,PIC1_BASE ; ICW2 для первого контроллера out 2lh,al mov al,PIC2_BASE ; ICW2 для второго контроллера out 0A1h,al mov al,04h ; ICW.3 для первого контроллера out 21h,al mov al,02h ; ICW3 для второго контроллера out 0A1h,al mov al,00001101b ; ICW4 для первого контроллера out 21h,al mov al,00001001b ; ICW4 для второго контроллера out 0A1h,al sti ret init_pic endp
; перехват прерываний от PIC1_BASE до PIC1_BASE+7 hook_pic1_ints proc near mov ax,2500h+PIC1_BASE mov dx,offset irq0_handler int 21h mov ax,2501h+PIC1_BASE mov dx,offset irq1_handler int 21h mov ax,2502h+PIC1_BASE mov dx,offset irq2_handler int 21h mov ax,2503h+PIC1_BASE mov dx,offset irq3_handler int 21h mov ax,2504h+PIC1_BASE mov dx,offset irq4_handler int 21h mov ax,2505h+PIC1_BASE mov dx,offset irq5_handler int 21h mov ax,2506h+PIC1_BASE mov dx,offset irq6_handler int 21h mov ax,2507h+PIC1_BASE mov dx,offset irq7_handler int 21h ret hook_pic1_ints endp
end start
Команды управления
OCW1:
биты 7 – 0: прерывание 7 – 0/15 – 8 запрещено
При помощи этой команды можно временно запретить или разрешить то или иное аппаратное прерывание. Например, команды
in al,21h or al,00000010b out 21h,al
приводят к отключению IRQ1, то есть клавиатуры.
Мы пользовались OCW1 в программе term2.asm, чтобы разрешить IRQ3 — прерывание от последовательного порта COM2.
OCW2: команды конца прерывания и сдвига приоритетов
биты 7 – 5: команда
000: запрещение сдвига приоритетов в режиме без EOI
001: неспецифичный EOI (конец прерывания в режиме с приоритетами)
010: нет операции
011: специфичный EOI (конец прерывания в режиме без приоритетов)
100: разрешение сдвига приоритетов в режиме без EOI
101: сдвиг приоритетов с неспецифичным EOI
110: сдвиг приоритетов
111: сдвиг приоритетов со специфичным EOI
биты 4 – 3: 00 (указывают, что это OCW2)
биты 2 – 0: номер IRQ для команд 011lb, 110 и 111
Как упоминалось в главе 5.8.2, если несколько прерываний происходят одновременно, обслуживается в первую очередь то, которое имеет высший приоритет. При инициализации контроллера высший приоритет имеет IRQ0 (прерывание от системного таймера), а низший — IRQ7. Все прерывания второго контроллера (IRQ8 – IRQ15) оказываются в этой последовательности между IRQ1 и IRQ3, так как именно IRQ2 используется для каскадирования этих двух контроллеров. Команды сдвига приоритетов позволяют изменить эту ситуацию, присвоив завершающемуся (команды 101 или 111) или обрабатывающемуся (110) прерыванию низший приоритет, причем следующее прерывание получит наивысший, и далее по кругу.
Более того, в тот момент, когда выполняется обработчик аппаратного прерывания, другие прерывания с низшими приоритетами не происходят, даже если обработчик выполнил команду sti. Чтобы разрешить выполнение других прерываний, каждый обработчик обязательно должен послать команду EOI — конец прерывания — в соответствующий контроллер. Именно поэтому обработчики аппаратных прерываний в программах term2.asm и wavdma.asm заканчивались командами
mov al,20h ; команда "неспецифичный конец прерывания" out 20h,al ; посылается в первый контроллер прерываний
Если бы контроллер был инициализирован в режиме без приоритетов, вместо неспецифичного EOI пришлось бы посылать специфичный, содержащий в младших трех битах номер прерывания, но BIOS инициализирует контроллер именно в режиме с приоритетами. Кроме того, контроллер мог бы быть инициализирован в режиме без EOI, но тогда в ходе работы обработчика прерывания могли бы происходить все остальные прерывания, включая обрабатываемое. О способах инициализации контроллера говорится далее, а здесь рассмотрим последнюю команду управления.
OCW3: чтение состояния контроллера и режим специального маскирования
бит 7: 0
биты 6 – 5: режим специального маскирования
00 — не изменять
10 — выключить
11 — включить
биты 4 – 3: 01 — указывает, что это OCW3
бит 2: режим опроса
биты 1 – 0: чтение состояния контроллера
00 — не читать
10 — читать регистр запросов на прерывания
11 — читать регистр обслуживаемых прерываний
В режиме специального маскирования в момент выполнения обработчика прерывания разрешены все прерывания, кроме выполняющегося и маскируемых командой OCW1, что имеет смысл сделать, если обработчик прерывания с достаточно высоким приоритетом собирается выполняться слишком долго.
Чаще всего OCW3 используют для чтения состояния контроллера — младшие два бита выбирают, какой из регистров контроллера будет возвращаться при последующем чтении из порта 21h/A1h. Оба возвращаемых регистра имеют структуру, аналогичную OCW1, — каждый бит отвечает соответствующему IRQ.
Из регистра запросов на прерывания можно узнать, какие прерывания произошли, но пока не были обработаны, а из регистра обслуживаемых прерываний — какие прерывания обрабатываются в данный момент. Последнее — еще одна мера безопасности, которую применяют резидентные программы, — нельзя работать с дисководом (IRQ6), если в этот момент обслуживается прерывание от последовательного порта (IRQ3), и нельзя работать с диском (IRQ14/15), если обслуживается прерывание от системного таймера (IRQ0).
Конечные автоматы
Конечный автомат — процедура, которая помнит свое состояние и при обращении к ней выполняет различные действия для разных состояний. Например, рассмотрим процедуру, которая складывает регистры АХ и ВХ при первом вызове, вычитает при втором, умножает при третьем, делит при четвертом, снова складывает при пятом и т.д. Очевидная реализация, опять же, состоит в последовательности условных переходов:
state db 0 state_machine: cmp state,0 jne not_0 ; состояние 0: сложение add ax,bx inc state ret not_0: cmp state,1 jne not_1 ; состояние 1: вычитание sub ax,bx inc state ret not_1: cmp state,2 jne not_2 ; состояние 2: умножение push dx mul bx pop dx inc state ret : состояние 3: деление not_2: push dx xor dx,dx div bx pop dx mov state,0 ret
Оказывается, что, как и для CASE, в ассемблере есть средства для более эффективной реализации этой структуры. Это все тот же косвенный переход, использованный нами только что для CASE:
state dw offset state_0 state_machine: jmp state
state_0: add ax,bx ; состояние 0: сложение mov state,offset state_1 ret state_1: sub ax,bx ; состояние 1: вычитание mov state,offset state_2 ret state_2: push dx ; состояние 2: умножение mul bx pop dx mov state,offset state_3 ret state_3: push dx ; состояние З: деление xor dx,dx div bx рор dx mov state,offset state_0 ret
Как и в случае с CASE, использование косвенного перехода приводит к тому, что не требуется никаких проверок и время выполнения управляющей структуры остается одним и тем же для четырех или четырех тысяч состояний.
Контроллер DMA
Контроллер DMA используется для обмена данными между внешними устройствами и памятью. Он нужен в работе с жесткими дисками и дисководами, звуковыми платами и другими устройствами, работающими со значительными объемами данных. Начиная с PC AT, в компьютерах присутствуют два DMA-котроллера — 8-битный (с каналами 0, 1, 2 и 3) и 16-битный (с каналами 4, 5, 6 и 7). Канал 2 используется для обмена данными с дисководами, канал 3 — для жестких дисков, канал 4 теряется при каскадировании контроллеров, а назначение остальных каналов может варьироваться.
DMA позволяет выполнить чтение или запись блока данных, начинающегося с линейного адреса, описываемого как 20-битное число для первого DMA-контроллера и как 24-битное — для второго, то есть данные для 8-битного DMA должны располагаться в пределах первого мегабайта памяти, а для второго — в пределах первых 16 Мб. Старшие четыре бита для 20-битных адресов и старшие 8 бит для 24-битных адресов хранятся в регистрах страниц DMA, адресуемых через порты 80h – 8Fh:
порт 81h: страничный адрес для канала 2 (биты 3 – 0 = биты 19 – 16 адреса)
порт 82h: страничный адрес для канала 3 (биты 3 – 0 = биты 19 – 16 адреса)
порт 83h: страничный адрес для канала 1 (биты 3 – 0 = биты 19 – 16 адреса)
порт 87h: страничный адрес для канала 0 (биты 3 – 0 = биты 19 – 16 адреса)
порт 89h: страничный адрес для канала 6 (биты 7 – 0 = биты 23 – 17 адреса)
порт 8Ah: страничный адрес для канала 7 (биты 7 – 0 = биты 23 – 17 адреса)
порт 8Bh: страничный адрес для канала 5 (биты 7 – 0 = биты 23 – 17 адреса)
Страничный адрес определяет начало 64 Кб/128 Кб участка памяти, с которым будет работать данный канал, поэтому при передаче данных через DMA обязательно надо следить за тем, чтобы не было выхода за границы этого участка, то есть чтобы не было попытки пересечения адреса 1000h:0, 2000h:0, 3000h:0 для первого DMA или 2000h:0, 4000h:0, 6000h:0 — для второго.
Младшие 16 бит адреса записывают в следующие порты:
00h: биты 15 – 0 адреса блока данных для канала 0
01h: счетчик переданных байт канала 0
02h – 03h: аналогично для канала 1
04h – 05h: аналогично для канала 2
06h – 07h: аналогично для канала 3
(для этих портов используются две операции чтения/записи — сначала передаются биты 7 – 0, затем биты 15 – 8)
C0h: биты 8 – 1 адреса блока данных для канала 4 (бит 0 адреса всегда равен нулю)
C1h: биты 16 – 9 адреса блока данных для канала 4
C2h: младший байт счетчика переданных слов канала 4
C3h: старший байт счетчика переданных слов канала 4
C4h – C7h: аналогично для канала 5
C8h – CBh: аналогично для канала 5
CCh – CFh: аналогично для канала 5
(эти порты рассчитаны на чтение/запись целыми словами)
Каждый из этих двух DMA-контроллеров также имеет собственный набор управляющих регистров — регистры первого контроллера адресуются через порты 08h – 0Fh, а второго — через D0 – DFh:
порт 08h/D0h для чтения: регистр состояния DMA
бит 7, 6, 5, 4: установлен запрос на DMA на канале 3/7, 2/6, 1/5, 0/4
бит 3, 2, 1, 0: закончился DMA на канале 3/7, 2/6, 1/5, 0/4
порт 08h/D0h для записи: регистр команд DMA (устанавливается BIOS)
бит 7: сигнал DACK использует высокий уровень
бит 6: сигнал DREQ использует высокий уровень
бит 5: 1/0 — расширенный/задержанный цикл записи
бит 4: 1/0 — приоритеты сменяются циклически/фиксированно
бит 3: сжатие во времени
бит 2: DMA-контроллер отключен
бит 1: разрешен захват канала 0 (для режима память-память)
бит 0: включен режим память-память (канал 0 – канал 1)
порт 09h/D2h для записи: регистр запроса DMA
бит 2: 1/0 — установка/сброс запроса на DMA
биты 1 – 0: номер канала (00, 01, 10, 11 = 0/4, 1/5, 2/6, 3/7)
порт 0Ah/ D4h для записи: регистр маски канала DMA
бит 2: 1/0 — установка/сброс маскирующего бита
биты 1 – 0: номер канала (00, 01, 10, 11 = 0/4, 1/5, 2/6, 3/7)
порт 0Bh/D6h для записи: регистр режима DMA
биты 7 – 6:
00 — передача по запросу
01 — одиночная передача (используется для звука)
10 — блочная передача (используется для дисков)
11 — канал занят для каскадирования
бит 5: 1/0 — адреса уменьшаются/увеличиваются
бит 4: режим автоинициализации
биты 3 – 2:
00 — проверка
01 — запись
10 — чтение
биты 1 – 0: номер канала (00, 01, 10, 11 = 0/4, 1/5, 2/6, 3/7)
порт 0Ch/D8h для записи: сброс переключателя младший/старший байт
Для чтения/записи 16-битных значений из/в 8-битные порты 00h – 08h. Следующий байт, переданный в эти порты, будет считаться младшим, следующий за ним — старшим.
порт 0Dh/DAh для записи: сброс контроллера DMA
Любая запись сюда приводит к полному сбросу DMA-контроллера, так что его надо инициализировать заново.
порт 0Dh/DAh для чтения: последний переданный байт/слово.
порт 0Eh/DCh для записи: любая запись снимает маскирующие биты со всех каналов
порт 0Fh/DEh для записи: регистр маски всех каналов:
биты 3 – 0: маскирующие биты каналов 3/7, 2/6, 1/5, 0/4
Чаще всего внешнее устройство само инициализирует передачу данных, и все, что необходимо сделать программе, — это записать адрес начала буфера в порты, соответствующие используемому каналу, длину передаваемого блока данных минус один в регистр счетчика соответствующего канала, установить нужный режим работы канала и снять маскирующий бит.
В качестве примера вернемся к программированию звуковых плат и изменим программу wavdir. asm так, чтобы она использовала DMA.
; wavdma.asm ; Пример программы, проигрывающей файл C:\WINDOWS\MEDIA\TADA.WAV ; на звуковой карте при помощи DMA FILESPEC equ "c:\windows\media\tada.wav" ; заменить на c:\windows\tada.wav ; для старых версий Windows SBPORT equ 220h ; SBDMA equ 1 ; процедура program_dma рассчитана только на канал 1 SBIRQ equ 5 ; только IRQ0 - IRQ7 .model tiny .code .186 org 100h ; СОМ-программа start: call dsp_reset ; инициализация DSP jc no_blaster mov bl,0D1h ; команда OD1h call dsp_write ; включить звук call open_file ; прочитать файл в буфер call hook_sbirq ; перехватить прерывание mov bl,40h ; команда 40h call dsp_write ; установка скорости передачи mov bl,0B2h ; константа для 11025Hz/Stereo call dsp_write call program_dma ; начать DMA-передачу данных
main_loop: ; основной цикл cmp byte ptr finished_flag,0 je main_loop ; выход, когда байт finished_flag = 1
call restore_sbirq ; восстановить прерывание no_blaster: ret
old_sbirq dd ? ; адрес старого обработчика finished_flag db 0 ; флаг окончания работы filename db FILESPEC,0 ; имя файла
; обработчик прерывания звуковой карты ; устанавливает флаг finished_flag в 1
sbirq_handler proc far push ax mov byte ptr cs:finished_flag,1 ; установить флаг mov al,20h ; послать команду EOI out 20h,al ; в контроллер прерываний pop ax iret sbirq_handler endp
; процедура dsp_reset ; сброс и инициализация DSP dsp_reset proc near mov dx,SBPORT+6 ; порт 226h - регистр сброса DSP mov al,1 ; запись в него единицы ; запускает инициализацию out dx,al mov cx,40 ; небольшая пауза dsploop: in al,dx loop dsploop mov al,0 ; запись нуля завершает инициализацию out dx,al ; теперь DSP готов к работе add dx,8 ; порт 22Eh - бит 7 при чтении ; указывает на занятость mov сх,100 ; буфера записи DSP check_port: in al,dx ; прочитать состояние буфера записи, and al,80h ; если бит 7 ноль, jz port_not_ready ; порт еще не готов, sub dx,4 ; иначе: порт 22Аh - чтение данных из DSP in al,dx add dx,4 ; порт снова 22Eh cmp al,0AAh ; проверить, что DSP возвращает 0AAh ; при чтении - это сигнал его готовности ; к работе je good_reset port_not_ready: loop check_port ; повторить проверку на 0AAh 100 раз, bad_reset: stc ; если Sound Blaster не откликается, ret ; вернуться с CF = 1, good_reset: clc ; если инициализация прошла успешно, ret ; вернуться с CF = 0 dsp_reset endp
; процедура dsp_write ; посылает байт из BL в DSP dsp_write proc near mov dx,SBPORT+0Ch ; порт 22Ch - ввод данных/команд DSP write_loop: ; подождать готовности буфера записи DSP, in al,dx ; прочитать порт 22Ch and al,80h ; и проверить бит 7, jnz write_loop ; если он не ноль - подождать еще, mov al,bl ; иначе: out dx,al ; послать данные ret dsp_write endp
; процедура hook_sbirq ; перехватывает прерывание звуковой карты и разрешает его hook_sbirq proc near mov ax,3508h+SBIRQ ; AH = 35h, AL = номер прерывания int 21h ; получить адрес старого обработчика mov word ptr old_sbirq,bx ; и сохранить его mov word ptr old_sbirq+2,es mov ax,2508h+SBIRQ ; AH = 25h, AL = номер прерывания mov dx,offset sbirq_handler ; установить новый обработчик int 21h mov cl,1 shl cl,SBIRQ not cl ; построить битовую маску in al,21h ; прочитать OCW1 and al,cl ; разрешить прерывание out 21h,al ; запиать OCW1 ret hook_sbirq endp
; процедура restore_sbirq ; восстанавливает обработчик и запрещает прерывание restore_sbirq proc near mov ax,3508h+SBIRQ ; AH = 25h, AL = номер прерывания lds dx,dword ptr old_sbirq int 21h ; восстановить обработчик mov cl,1 shl cl,SBIRQ ; построить битовую маску in al,21h ; прочитать OCW1 or al,cl ; запретить прерывание out 21h,al ; записать OCW1 ret restore_sbirq endp
; процедура open_file ; открывает файл filename и копирует звуковые данные из него, ; считая, что это - tada.wav, в буфер buffer open_file proc near mov ax,3D00h ; AH = 3Dh, AL = 00 mov dx,offset filename ; DS:DX - ASCIZ-строка с именем файла int 21h ; открыть файл для чтения, jc error_exit ; если не удалось открыть файл - выйти mov bx,ax ; идентификатор файла в ВХ mov ax,4200h ; АН = 42h, AL = 0 mov cx,0 ; CX:DX - новое значение указателя mov dx,38h ; по этому адресу начинаются данные ; в tada.wav int 21h ; переместить файловый указатель mov ah,3Fh ; АН = 3Fh mov cx,27459 ; это - длина данных в файле tada.wav push ds mov dx,ds and dx,0F000h ; выровнять буфер на границу ; 4-килобайтной страницы add dx,1000h ; для DMA mov ds,dx mov dx,0 ; DS:DX - адрес буфера int 21h ; чтение файла pop ds ret error_exit: ; если не удалось открыть файл, mov ah,9 ; АН = 09h mov dx,offset notopenmsg ; DS:DX = адрес сообщения об ошибке int 21h ; вывод строки на экран int 20h ; конец программы
; сообщение об ошибке notopenmsg db "Ошибка при открытии файла",0Dh,0Ah,'$'
open_file endp
; процедура program_dma ; настраивает канал 1 DMA program_dma proc near mov al,5 ; замаскировать канал 1 out 0Ah,al xor al,al ; обнулить счетчик out 0Ch,al mov al,49h ; установить режим передачи ; (используйте 59h для автоинициализации) out 0Bh,al push cs pop dx and dh,0F0h add dh,10h ; вычислить адрес буфера xor ax,ax out 02h,al ; записать младшие 8 бит out 02h,al ; записать следующие 8 бит mov al,dh shr al,4 out 83h,al ; записать старшие 4 бита
mov ax,27459 ; длина данных в tada.wav dec ax ; DMA требует длину минус один out 03h,al ; записать младшие 8 бит длины mov al,ah out 03h,al ; записать старшие 8 бит длины mov al,1 out 0Ah,al ; снять маску с канала 1
mov bl,14h ; команда 14h call dsp_write ; 8-битное простое DMA-воспроизведение mov bx,27459 ; размер данных в tada.wav dec bx ; минус 1 call dsp_write ; записать в DSP младшие 8 бит длины mov bl,bh call dsp_write ; и старшие ret program_dma endp end start
В этом примере задействован обычный DMA-режим работы, в котором звуковая плата проигрывает участок данных, вызывает прерывание, и, пока обработчик прерывания подготавливает новый буфер данных, программирует DMA и звуковую плату для продолжения воспроизведения, проходит некоторое время, что может звучать как щелчок. Этого можно избежать, если воспользоваться режимом автоинициализации, который позволяет обойтись без остановок при воспроизведении.
При использовании режима DMA с автоинициализацией нужно сделать следующее: загрузить начало воспроизводимого звука в буфер длиной, например, 8 Кб и запрограммировать DMA на его передачу с автоинициализацией. Затем сообщить DSP, что проигрывается звук с автоинициализацией и размер буфера равен 4 Кб. Теперь, когда придет прерывание от звуковой платы, она не остановится и продолжит воспроизведение из вторых 4 Кб буфера, так как находится в режиме автоинициализации. Далее запишем в первые 4 Кб следующий блок данных. Когда кончится 8-килобайтный буфер, DMA начнет посылать его сначала, потому что мы его тоже запрограммировали для автоинициализации (бит 4 порта 0Bh/D6h), DSP вызовет прерывание и тоже не остановится, продолжая воспроизводить данные, которые посылает ему DMA-контроллер, а мы тем временем запишем во вторые 4 Кб буфера следующий участок проигрываемого файла и т.д.
Контроллер прерываний
Контроллер прерываний— устройство, которое получает запросы на аппаратные прерывания от всех внешних устройств. Он определяет, какие запросы следует обслужить, какие должны ждать своей очереди, а какие не будут обслуживаться вообще. Контроллеров прерываний, так же как и контроллеров DMA, два. Первый контроллер, обслуживающий запросы на прерывания от IRQ0 до IRQ7, управляется через порты 20h и 21h, а второй (IRQ8 – IRQ15) — через порты A0h и A1h.
Команды контроллеру делят на команды управления (OCW) и инициализации (ICW):
порт 20h/A0h для записи: OCW2, OCW3, ICW1
порт 20h/A0h для чтения — см. команду OCW3
порт 21h/A1h для чтения и записи: OCW1 — маскирование прерываний
порт 21h/A1h для записи — ICW2, ICW3, ICW4 сразу после ICW1
Локальные переменные
Часто процедурам требуются локальные переменные, которые не будут нужны после того, как процедура закончится. По аналогии с методами передачи параметров можно говорить о локальных переменных в регистрах — каждый регистр, который сохраняют при входе в процедуру и восстанавливают при выходе, фактически играет роль локальной переменной. Единственный недостаток регистров в роли локальных переменных — их слишком мало. Следующий вариант — хранение локальных данных в переменной в сегменте данных — удобен и быстр для большинства несложных ассемблерных программ, но процедуру, использующую этот метод, нельзя вызывать рекурсивно: такая переменная на самом деле является глобальной и находится в одном и том же месте в памяти для каждого вызова процедуры. Третий и наиболее распространенный способ хранения локальных переменных в процедуре — стек. Принято располагать локальные переменные в стеке сразу после сохраненного значения регистра ВР, так что на них можно ссылаться изнутри процедуры, как [ВР-2], [ВР-4], [ВР-б] и т.д.:
foobar proc near foobar_x equ [bp+8] ; параметры foobar_y equ [bp+6] foobar_z equ [bp+4] foobar_l equ [bp-2] ; локальные переменные foobar_m equ [bp-4] foobar_n equ [bp-6]
push bp ; сохранить предыдущий ВР mov bp,sp ; установить ВР для этой процедуры sub sp,6 ; зарезервировать 6 байт для ; локальных переменных (тело процедуры) mov sp,bp ; восстановить SP, выбросив ; из стека все локальные переменные pop bp ; восстановить ВР вызвавшей процедуры ret 6 ; вернуться, удалив параметры из стека foobar endp
Внутри процедуры foobar стек будет заполнен следующим образом (см. рис. 16).
Рис. 16. Стек при вызове процедуры foobar
Последовательности команд, используемые в начале и в конце таких процедур, оказались настолько часто применяемыми, что в процессоре 80186 были введены специальные команды ENTER и LEAVE, выполняющие эти же самые действия:
foobar proc near foobar_x equ [bp+8] ; параметры foobar_y equ [bp+6] foobar_z equ [bp+4] foobar_l equ [bp-2] ; локальные foobar_m equ [bp-4] ; переменные foobar_n equ [bp-6]
enter 6,0 ; push bp ; mov bp,sp ; sub sp,6 (тело процедуры) leave ; mov sp,bp ; pop bp ret 6 ; вернуться, ; удалив параметры ; из стека foobar endp
Область в стеке, отводимая для локальных переменных вместе с активационной записью, называется стековым кадром.
Мультиплексорное прерывание
Если вы запустите предыдущий пример несколько раз, с разными или даже одинаковыми именами дисков в командной строке, объем свободной памяти DOS каждый раз будет уменьшаться на 208 байт, то есть каждый новый запуск устанавливает дополнительную копию резидента, даже если она идентична уже установленной. Разумеется, это неправильно — инсталляционная часть обязательно должна уметь определять, загружен ли уже резидент в памяти перед его установкой. В нашем случае это не приводит ни к каким последствиям, кроме незначительного уменьшения объема свободной памяти, но во многих чуть более сложных случаях могут возникать различные проблемы, например многократное срабатывание активного резидента по каждому аппаратному прерыванию, которое он перехватывает.
Для того чтобы идентифицировать себя в памяти, резидентные программы обычно или устанавливали обработчики для неиспользуемых прерываний, или вводили дополнительную функцию в используемое прерывание. Например: наш резидент мог бы проверять в обработчике INT 21h АН на равенство какому-нибудь числу, не соответствующему функции DOS, и возвращать в, например, AL код, означающий, что резидент присутствует. Очевидная проблема, связанная с таким подходом, — вероятность того, что кто-то другой выберет то же неиспользуемое прерывание или что будущая версия DOS станет использовать ту же функцию. Именно для решения этой проблемы, начиная с версии DOS 3.3, был предусмотрен специальный механизм, позволяющий разместить до 64 резидентных программ в памяти одновременно, — мулыпиплексорное прерывание.
INT 2Fh: Мультиплексорное прерывание
Ввод: |
АН = идентификатор программы00h – 7Fh зарезервировано для DOS/Windows B8h – BFh зарезервировано для сетевых функций C0h – FFh отводится для программAL = код функции00h — проверка наличия программы остальные функции — свои для каждой программыВХ, СХ, DX = 0 (так как некоторые программы выполняют те или иные действия в зависимости от значений этих регистров) |
Вывод: |
Для подфункции AL = 00h, если установлен резидент с номером АН, он должен вернуть 0FFh в AL и какой-либо идентифицирующий код в других регистрах, например адрес строки с названием и номером версии. |
<
/p>
Оказалось, что такого уровня спецификации совершенно недостаточно и резидентные программы по-прежнему работали по-разному, находя немало способов конфликтовать между собой. Поэтому появилась новая спецификация — AMIS (альтернативная спецификация мульти-плексорного прерывания). Все резидентные программы, следующие этой спецификации, должны поддерживать базовый набор функций AMIS, а их обработчики прерываний должны быть написаны в соответствии со стандартом IBM ISP, который делает возможным выгрузку резидентных программ из памяти в любом порядке.
Начало обработчика прерывания должно выглядеть следующим образом:
+00h: 2 байта — 0EBh, 10h (команда jmp short на первый байт после этого блока)
+02h: 4 байта — адрес предыдущего обработчика: именно по адресу, хранящемуся здесь, обработчик должен выполнять call или jmp
+06h: 2 байта — 424Вh — сигнатура ISP-блока
+08h: байт — 80h, если это первичный обработчик аппаратного прерывания (то есть он посылает контроллеру прерываний сигнал EOI). 00h, если это обработчик программного прерывания или дополнительный обработчик аппаратного
+09h: 2 байта — команда jmp short на начало подпрограммы аппаратного сброса — обычно состоит из одной команды IRET
+0Bh: 7 байт — зарезервировано
Все стандартное общение с резидентной программой по спецификации AMIS происходит через прерывание 2Dh. При установке инсталляционная часть резидентной программы должна проверить, не установлена ли ее копия, просканировав все идентификаторы от 00 до 0FFh, и, если нет, установить обработчик на первый свободный идентификатор.
INT 2Dh: Мультиплексорное прерывание AMIS
Ввод: |
АН = идентификатор программы AL = 00: проверка наличия AL = 01: получить адрес точки входа AL = 02: деинсталляция AL = 03: запрос на активизацию (для «всплывающих программ») AL = 04: получить список перехваченных прерываний AL = 05: получить список перехваченных клавиш AL = 06: получить информацию о драйвере (для драйверов устройств) AL = 07 – 0Fh — зарезервировано для AMIS AL = 1Fh – 0FFh — свои для каждой программы |
Вывод: |
AL = 00h, если функция не поддерживается |
<
/p>
Рассмотрим функции, описанные в спецификации AMIS как обязательные.
INT 2Dh AL = 00h: Функция AMIS — проверка наличия резидентной программы
Ввод: |
АН = идентификатор программы AL = 00h |
Вывод: |
AL = 00h, если идентификатор не занят AL = FFh, если идентификатор занят СН = старший номер версии программы CL = младший номер версии программы DX:DI = адрес AMIS-сигнатуры, по первым шестнадцати байтам которой и происходит идентификация Первые 8 байт — имя производителя программы, следующие 8 байт — имя программы, затем или 0 или ASCIZ-строка с описанием программы, не больше 64 байт. |
INT 2Dh AL = 03h: Функция AMIS — выгрузка резидентной программы из памяти
Ввод: |
АН = идентификатор программы AL = 02h DX:BX = адрес, на который нужно передать управление после выгрузки |
Вывод: |
AL = 01h — выгрузка не удалась AL = 02h — выгрузка сейчас невозможна, но произойдет чуть позже AL = 03h — резидент не умеет выгружаться сам, но его можно выгрузить, резидент все еще активенВХ = сегментный адрес резидентаAL = 04h — резидент не умеет выгружаться сам, но его можно выгрузить, резидент больше неактивенВХ = сегментный адрес резидентаAL = 05h — сейчас выгружаться небезопасно — повторить запрос позже AL = 06h — резидент был загружен из CONFIG.SYS и выгрузиться не может, резидент больше неактивен AL = 07h — это драйвер устройства, который не умеет выгружаться самВХ = сегментный адресAL = 0FFh с передачей управления на DX:BX — успешная выгрузка |
INT 2Dh AL = 03h: Функция AMIS — запрос на активизацию
Ввод: |
АН = идентификатор программы AL = 03h |
Вывод: |
AL = 00h — резидент — «невсплывающая» программа AL = 01h — сейчас «всплывать» нельзя — повторить запрос позже AL = 02h — сейчас «всплыть» не могу, но «всплыву» при первой возможности AL = 03h — уже «всплыл» AL = 04h — «всплыть» невозможноВХ,СХ — коды ошибкиAL = 0FFh — программа «всплыла», отработала и завершиласьВХ — код завершения |
<
/p>
INT 2Dh AL = 04h: Функция AMIS — получить список перехваченных прерываний
Ввод: |
АН = идентификатор программы AL = 04h |
Вывод: |
AL = 04h DX:BX = адрес списка прерываний, состоящего из 3-байтных структур:байт 1: номер прерывания (2Dh должен быть последним) байты 2,3: смещение относительно сегмента, возвращенного в DX обработчика прерывания (по этому смещению должен находиться стандартный заголовок ISP) |
INT 2Dh AL = 05h: Функция AMIS — получить список перехваченных клавиш
Ввод: |
АН = идентификатор программы AL = 05h |
Вывод: |
AL = 0FFh — функция поддерживается
DX:BX = адрес списка клавиш:
+00h: 1 байт: тип проверки клавиши:
бит 0: проверка до обработчика INT 9
бит 1: проверка после обработчика INT 9
бит 2: проверка до обработчика INT 15h/AH = 4Fh
бит 3: проверка после обработчика INT 15h/AH = 4Fh
бит 4: проверка при вызове INT 16h/AH = 0, 1, 2
бит 5: проверка при вызове INT 16h/AH = 10h, llh, 12h
бит 6: проверка при вызове INT 16h/AH = 20h, 21h, 22h
бит 7: 0
+01h: 1 байт: количество перехваченных клавиш
+02h: массив структур по 6 байт:
байт 1: скан-код клавиши (старший бит — отпускание клавиши, 00/80h — если срабатывание только по состоянию Shift-Ctrl-Alt и т.д.)
байты 2, 3: необходимое состояние клавиатуры (формат тот же, что и в слове состояния клавиатуры, только бит 7 соответствует нажатию любой клавиши Shift)
байты 4, 5: запрещенное состояние клавиатуры (формат тот же)
байт 6: способ обработки клавиши
бит 0: клавиша перехватывается после обработчиков
бит 1: клавиша перехватывается до обработчиков
бит 2: другие обработчики не должны «проглатывать» клавишу
бит 3: клавиша не сработает, если, пока она была нажата, нажимали или отпускали другие клавиши
бит 4: клавиша преобразовывается в другую
бит 5: клавиша иногда «проглатывается», а иногда передается дальше
биты 6, 7: 0 |
<
/p>
Теперь можно написать резидентную программу, которая не загрузится дважды в память. В этой программе установим дополнительный обработчик на аппаратное прерывание от клавиатуры IRQ1 (INT 9), который будет отслеживать комбинацию клавиш Alt-А; после их нажатия программа перейдет в активное состояние, выведет на экран свое окно и среагирует уже на большее количество клавиш. Такие программы, активизирующиеся по нажатию какой-либо клавиши, часто называют «всплывающими» программами, но наша программа на самом деле будет только казаться «всплывающей». Настоящая «всплывающая» программа после активизации в обработчике INT 9h не возвращает управление, пока пользователь не закончит с ней работать. В нашем случае управление возвратится после каждого нажатия клавиши, хотя сами клавиши будут поглощаться программой, так что можно ей пользоваться одновременно с работающими программами, причем на скорости их работы активный ascii.com никак не скажется.
Так же как и с предыдущим примером, программы, не использующие средства DOS/BIOS для работы с клавиатурой, например файловый менеджер FAR, будут получать все нажатые клавиши параллельно с нашей программой, что приведет к нежелательным эффектам на экране. Кроме того, в этом упрощенном примере отсутствуют некоторые необходимые проверки (например, текущего видеорежима) и функции (например, выгрузка программы из памяти), но тем не менее это — реально используемая программа, с помощью которой легко посмотреть, какой символ соответствует какому ASCII-коду, и ввести любой символ, которого нет на клавиатуре, в частности псевдографику.
; ascii.asm ; Резидентная программа для просмотра и ввода ASCII-символов ; HCI: ; Alt-A - активация программы ; Клавиши управления курсором - выбор символа ; Enter - выход из программы с вводом символа ; Esc - выход из программы без ввода символа ; API: ; Программа занимает первую свободную функцию прерывания 2Dh ; в соответствии со спецификацией AMIS 3.6 ; Поддерживаются функции AMIS 00h, 02h, 03h, 04h и 05h ; Обработчики прерываний построены в соответствии с IBM ISP
; адрес верхнего левого угла окна (23-я позиция в третьей строке) START_POSITION equ (80*2+23)*2
.model tiny .code .186 ; для сдвигов и команд pusha/popa org 2Ch envseg dw ? ; сегментный адрес окружения DOS
org 100h ; начало СОМ-программы start: jmp initialize ; переход на инициализирующую часть hw_reset9: retf ; ISP: минимальный hw_reset
; Обработчик прерывания 09h (IRQ1) int09h_handler proc far jmp short actual_int09h_handler ; ISP: пропустить блок old_int09h dd ? ; ISP: старый обработчик dw 424Bh ; ISP: сигнатура db 00h ; ISP: вторичный обработчик jmp short hw_reset9 ; ISP: ближний jmp на hw_reset db 7 dup (0) ; ISP: зарезервировано actual_iht09h_handler: ; начало обработчика INT 09h
; Сначала вызовем предыдущий обработчик, чтобы дать BIOS возможность ; обработать прерывание и, если это было нажатие клавиши, поместить код ; в клавиатурный буфер, так как мы пока не умеем работать с портами клавиатуры ; и контроллера прерываний pushf call dword ptr cs:old_int09h
; По этому адресу обработчик INT 2Dh запишет код команды IRET ; для дезактивации программы disable_point label byte pusha ; это аппаратное прерывание - надо push ds ; сохранить все регистры push es cld ; флаг для команд строковой обработки push 0B800h pop es ; ES = сегментный адрес видеопамяти push 0040h pop ds ; DS = сегментный адрес области данных BIOS mov di,word ptr ds:001Ah ; адрес головы буфера клавиатуры cmp di,word ptr ds:001Ch ; если он равен адресу хвоста, je exit_09h_handler ; буфер пуст, и нам делать нечего ; (например, если прерывание пришло по ; отпусканию клавиши), mov ax,word ptr [di] ; иначе: считать символ из головы ; буфера cmp byte ptr cs:we_are_active,0 ; если программа уже jne already_active ; активирована - перейти ; к обработке стрелок и т.п. cmp ah,1Eh ; если прочитанная клавиша не А jne exit_09h_handler ; (скан-код 1Eh) - выйти, mov al,byte ptr ds:0017h ; иначе: считать байт ; состояния клавиатуры, test al,08h ; если не нажата любая Alt, jz exit_09h_handler ; выйти, mov word ptr ds:001Ch,di ; иначе: установить адреса ; головы и хвоста буфера одинаковыми, ; пометив его тем самым как пустой call save_screen ; сохранить область экрана, которую ; накроет всплывающее окно push cs pop ds ; DS = наш сегментный адрес call display_all ; вывести на экран окно программы mov byte ptr we_are_active,1 ; установить флаг jmp short exit_09h_handler ; и выйти из обработчика
; Сюда передается управление, если программа уже активирована. ; При этом ES = B800h, DS = 0040h, DI = адрес головы буфера клавиатуры, ; АХ = символ из головы буфера already_active: mov word ptr ds:001Ch,di ; установить адреса ; головы и хвоста буфера одинаковыми, ; пометив его тем самым как пустой push cs pop ds ; DS = наш сегментный адрес mov al,ah ; команды cmp al,? короче команд cmp ah,? mov bh,byte ptr current_char ; номер выделенного в ; данный момент ASCII-символа, cmp al,48h ; если нажата стрелка вверх (скан-код 48h), jne not_up sub bh,16 ; уменьшить номер символа на 16, not_up: cmp al,50h ; если нажата стрелка вниз (скан-код 50h), jne not_down add bh,16 ; увеличить номер символа на 16, not_down: cmp al,4Bh ; если нажата стрелка влево, jne not_left dec bh ; уменьшить номер символа на 1, not_left: cmp al,4Dh ; если нажата стрелка вправо, jne not_right inc bh ; увеличить номер символа на 1, not_right: cmp al,1Ch ; если нажата Enter (скан-код 1Ch), je enter_pressed ; перейти к его обработчику dec al ; Если не нажата клавиша Esc (скан-код 1), jnz exit_with_display ; выйти из обработчика, оставив ; окно нашей программы на экране, exit_after_enter: ; иначе: call restore_screen ; убрать наше окно с экрана, mov byte ptr we_are_active,0 ; обнулить флаг активности, jmp short exit_09h_handler ; выйти из обработчика
exit_with_display: ; выход с сохранением окна (после нажатия стрелок) mov byte ptr current_char,bh ; записать новое значение ; текущего символа call display_all ; перерисовать окно
exit_09h_handler: ; выход из обработчика INT 09h pop es pop ds ; восстановить регистры рора iret ; и вернуться в прерванную программу we_are_active db 0 ; флаг активности: равен 1, если ; программа активна current_char db 37h ; номер ASCII-символа, выделенного ; в данный момент
; сюда передается управление, если в активном состоянии была нажата Enter enter_pressed: mov ah,05h ; Функция 05h mov ch,0 ; CH = 0 mov cl,byte ptr current_char ; CL = ASCII-код int 16h ; поместить символ в буфер клавиатуры jmp short exit_after_enter ; выйти из обработчика, стерев окно
; процедура save_screen ; сохраняет в буфере screen_buffer содержимое области экрана, которую закроет ; наше окно
save_screen proc near mov si,START_POSITION push 0B800h ; DS:SI - начало этой области в видеопамяти pop ds push es push cs pop es mov di,offset screen_buffer ; ES:DI - начало буфера в программе mov dx,18 ; DX = счетчик строк save_screen_loop: mov cx,33 ; CX = счетчик символов в строке rep movsw ; скопировать строку с экрана в буфер add si,(80-33)*2 ; увеличить SI до начала следующей строки dec dx ; уменьшить счетчик строк, jnz save_screen_loop ; если он не ноль - продолжить цикл pop es ret save_screen endp
; процедура restore_screen ; восстанавливает содержимое области экрана, которую закрывало наше ; всплывающее окно данными из буфера screen_buffer
restore_screen proc near mov di,START_POSITION ; ES:DI - начало области в видеопамяти mov si,offset screen_buffer ; DS:SI - начало буфера mov dx,18 ; счетчик строк restore_screen_loop: mov cx, 33 ; счетчик символов в строке rep movsw ; скопировать строку add di,(80-33)*2 ; увеличить DI до начала следующей строки dec dx ; уменьшить счетчик строк, jnz restore_screen_loop ; если он не ноль - продолжить ret restore_screen endp
; процедура display_all ; выводит на экран текущее состояние всплывающего окна нашей программы display_all proc near
; шаг 1: вписать значение текущего выделенного байта в нижнюю строку окна mov al,byte ptr current_char ; AL = выбранный байт push ax shr al,4 ; старшие четыре байта cmp al,10 ; три команды, sbb al,69h ; преобразующие цифру в AL das ; в ее ASCII-код (0 - 9, А - F) mov byte ptr hex_byte1,al ; записать символ на его ; место в нижней строке pop ax and al,0Fh ; младшие четыре бита cmp al,10 ; то же преобразование sbb al,69h das mov byte ptr hex_byte2,al ; записать младшую цифру
; шаг 2: вывод на экран окна. Было бы проще хранить его как массив и выводить ; командой movsw, как и буфер в процедуре restore_screen, но такой массив займет ; еще 1190 байт в резидентной части. Код этой части процедуры display_all - всего ; 69 байт. ; шаг 2.1: вывод первой строки mov ah,1Fh ; атрибут белый на синем mov di,START_POSITION ; ES:DI - адрес в видеопамяти mov si,offset display_line1 ; DS:SI - адрес строки mov cx,33 ; счетчик символов в строке display_loop1: mov al,byte ptr [si] ; прочитать символ в AL stosw ; и вывести его с атрибутом из АН inc si ; увеличить адрес символа в строке loop display_loop1
; шаг 2.2: вывод собственно таблицы mov dx,16 ; счетчик строк mov аl,-1 ; выводимый символ display_loop4: ; цикл по строкам add di,(80-33)*2 ; увеличить DI до начала push ax ; следующей строки mov al,0B3h stosw ; вывести первый символ (0B3h) pop ax mov cx,16 ; счетчик символов в строке display_loop3: ; цикл по символам в строке inc al ; следующий ASCII-символ stosw ; вывести его на экран push ax mov al,20h ; вывести пробел stosw pop ax loop display_loop3 ; и так 16 раз push ax sub di,2 ; вернуться назад на 1 символ mov al,0B3h ; и вывести 0B3h на месте stosw ; последнего пробела pop ax dec dx ; уменьшить счетчик строк jnz display_loop4
; шаг 2.3: вывод последней строки add di,(80-33)*2 ; увеличить DI до начала следующей строки mov сх,33 ; счетчик символов в строке mov si,offset display_line2 ; DS:SI - адрес строки display_loop2: mov al,byte ptr [si] ; прочитать символ в AL stosw ; вывести его с атрибутом на экран inc si ; увеличить адрес символа в строке loop display_loop2
; шаг 3: подсветка (изменение атрибута) у текущего выделенного символа mov al,byte ptr current_char ; AL = текущий символ mov ah,0 mov di,ax and di,0Fh ; DI = остаток от деления на 16 ; (номер в строке) shl di,2 ; умножить его на 2, так как на экране ; используется слово на символ, ; и еще раз на 2, так как ; между символами - пробелы shr ах,4 ; АХ = частное от деления на 16 ; (номер строки) imul ax,ax,80*2 ; умножить его на длину строки на экране, add di,ax ; сложить их, add di,START_POSITION+2+80*2+1 ; добавить адрес начала окна + 2, ; чтобы пропустить первый столбец, + 80*2, ; чтобы пропустить первую строку, + 1, ; чтобы получить адрес атрибута, ; а не символа mov al,071h ; атрибут - синий на сером stosb ; вывод на экран ret display_all endp
int09h_handler endp ; конец обработчика INT 09h
; буфер для хранения содержимого части экрана, которая накрывается нашим окном screen_buffer db 1190 dup(?)
; первая строка окна display_line1 db 0DAh,11 dup (0C4h),'* ASCII *',11 dup (0C4h),0BFh
; последняя строка окна display_line2 db 0C0h,11 dup (0C4h),'* Hex ' hex_byte1 db ? ; старшая цифра текущего байта hex_byte2 db ? ; младшая цифра текущего байта db ' *',10 dup (0C4h),0D9h
hw_reset2D: retf ; ISP: минимальный hw_reset
; обработчик прерывания INT 2Dh ; поддерживает функции AMIS 3. 6 00h, 02h, 03h, 04h и 05h int2Dh_handler proc far jmp short actual_int2Dh_handler ; ISP: пропустить блок old_int2Dh dd ? ; ISP: старый обработчик dw 424Bh ; ISP: сигнатура db 00h ; ISP: программное прерывание jmp short hw_reset2D ; ISP: ближний jmp на hw_reset db 7 dup (0) ; ISP: зарезервировано actual_int2Dh_handler: ; начало собственно обработчика INT 2Dh db 80h,0FCh ; начало команды CMP АН, число mux_id db ; идентификатор программы je its_us ; если вызывают с чужим АН - это не нас jmp dword ptr cs:old_int2Dh its_us: cmp al,06 ; функции 06h и выше jae int2D_no ; не поддерживаются cbw ; AX = номер функции mov di,ax ; DI = номер функции shl di,1 ; умножить его на 2, так как jumptable - ; таблица слов jmp word ptr cs:jumptable[di] ; косвенный переход на обработчики ; функций jumptable dw offset int2D_00,offset int2D_no dw offset int2D_02,offset int2D_03 dw offset int2D_04,offset int2D_05
int2D_00: ; проверка наличия mov al,0FFh ; этот номер занят mov сх,0100h ; номер версии 1.0 push cs pop dx ; DX:DI - адрес AMIS-сигнатуры mov di,offset amis_sign iret int2D_no: ; неподдерживаемая функция mov al,00h ; функция не поддерживается iret int2D_02: ; выгрузка программы mov byte ptr cs:disable_point,0CFh ; записать код команды IRET ; по адресу disable_point ; в обработчик INT 09h mov al,04h ; программа дезактивирована, но сама ; выгрузиться не может mov bx,cs ; BX - сегментный адрес программы iret int2D_03: ; запрос на активизацию для "всплывающих" программ cmp byte ptr we_are_active,0 ; если окно уже на экране, je already_popup call save_screen ; сохранить область экрана, push cs pop ds call display_all ; вывести окно mov byte ptr we_are_active,1 ; и поднять флаг already_popup: mov al,03h ; код 03: программа активизирована iret int2D_04: ; получить список перехваченных прерываний mov dx,cs ; список в DX:BX mov bx,offset amis_hooklist iret int2D_05: ; получить список "горячих" клавиш mov al,0FFh ; функция поддерживается mov dx,cs ; список в DX:BX mov bx,offset amis_hotkeys iret int2Dh_handler endp
; AMIS: сигнатура для резидентных программ amis_sign db "Cubbi..." ; 8 байт - имя автора db "ASCII..." ; 8 байт - имя программы db "ASCII display and input utility",0 ; ASCIZ-комментарий ; не более 64 байт
; AMIS: список перехваченных прерываний amis_hooklist db 09h dw offset int09h_handler db 2Dh dw offset int2Dh_handler
; AMIS: список "горячих" клавиш amis_hotkeys db 01h ; клавиши проверяются после стандартного ; обработчика INT 09h db 1 ; число клавиш db 1Eh ; скан-код клавиши (А) dw 08h ; требуемые флаги (любая Alt) dw 0 ; запрещенные флаги db 1 ; клавиша глотается
; конец резидентной части ; начало процедуры инициализации
initialize proc near mov ah,9 mov dx,offset usage ; вывести информацию о программе int 21h
; проверить, не установлена ли уже наша программа mov ah,-1 ; сканирование номеров от FFh до 00h more_mux: mov al,00h ; Функция 00h - проверка наличия программы int 2Dh ; мультиплексорное прерывание AMIS, cmp al,00h ; если идентификатор свободен, jne not_free mov byte ptr mux_id,ah ; записать его номер прямо в код ; обработчика int 2Dh, jmp short next_mux not_free: mov es,dx ; иначе - ES:DI = адрес их сигнатуры mov si,offset amis_sign ; DS:SI = адрес нашей сигнатуры mov cx,16 ; сравнить первые 16 байт, repe cmpsb jcxz already_loaded ; если они не совпадают, next_mux: dec ah ; перейти к следующему идентификатору, jnz more_mux ; пока это не 0 ; (на самом деле в этом примере сканирование происходит от FFh до 01h, ; так как 0 мы используем в качестве признака отсутствия свободного ; номера в следующей строке) free_mux_found: cmp byte ptr mux_id,0 ; если мы ничего не записали, je no_more_mux ; идентификаторы кончились mov ax,352Dh ; АН = 35h, AL = номер прерывания int 21h ; получить адрес обработчика INT 2Dh mov word ptr old_int2Dh,bx ;и поместить его в old_int2Dh mov word ptr old_int2Dh+2,es mov ax,3509h ; AH = 35h, AL = номер прерывания int 21h ; получить адрес обработчика INT 09h mov word ptr old_int09h,bx ; и поместить его в old_int09h mov word ptr old_int09h+2,es mov ax,252Dh ; AH = 25h, AL = номер прерывания mov dx,offset int2Dh_handler ; DS:DX - адрес нашего int 21h ; обработчика mov ax,2509h ; AH = 25h, AL = номер прерывания mov dx,offset int09h_handler ; DS:DX - адрес нашего int 21h ; обработчика mov ah,49h ; AH = 49h mov es,word ptr envseg ; ES = сегментный адрес среды DOS int 21h ; освободить память mov ah,9 mov dx,offset installed_msg ; вывод строки об успешной int 21h ; инсталляции mov dx,offset initialize ; DX - адрес первого байта за ; концом резидентной части int 27h ; завершить выполнение, оставшись ; резидентом ; сюда передается управление, если наша программа обнаружена в памяти already_loaded: mov ah,9 ; АН = 09h mov dx,offset already_msg ; вывести сообщение об ошибке int 21h ret ; и завершиться нормально
; сюда передается управление, если все 255 функций мультиплексора заняты ; резидентными программами no_more_mux: mov ah,9 mov dx, offset no_more_mux_msg int 21h ret
; текст, который выдает программа при запуске: usage db "ASCII display and input program" db " v1.0",0Dh,0Ah db "Alt-A - активация",0Dh,0Ah db "Стрелки - выбор символа",0Dh,0Ah db "Enter - ввод символа",0Dh,0Ah db "Escape - выход",0Dh,0Ah db "$" ; текст, который выдает программа, если она уже загружена: already_msg db "Ошибка: программа уже загружена",0Dh,0Ah,'$' ; текст, который выдает программа, если все функции мультиплексора заняты: no_more_mux_msg db "Ошибка: Слишком много резидентных программ" db 0Dh,0Ah,'$' ; текст, который выдает программа при успешной установке: installed_msg db "Программа загружена в память",0Dh,0Ah,'$'
initialize endp end start
Резидентная часть этой программы занимает в памяти целых 2064 байта (из которых на собственно коды команд приходится только 436). Это вполне терпимо, учитывая, что обычно программа типа ascii.com запускается перед простыми текстовыми редакторами для DOS (edit, multiedit, встроенные редакторы оболочек типа Norton Commander и т.д.), которые не требуют для своей работы полностью свободной памяти. В других случаях, как, например, при создании программы, копирующей изображение с экрана в файл, может оказаться, что на счету каждый байт; такие программы часто применяют для сохранения изображений из компьютерных игр, которые задействуют все ресурсы компьютера по максимуму. Здесь резидентным программам приходится размещать данные, а иногда и часть кода, в старших областях памяти, пользуясь спецификациями HMA, UMB, EMS или XMS. В следующей главе рассмотрен простой пример именно такой программы.
Обработчики прерываний
Когда в реальном режиме выполняется команда INT, управление передается по адресу, который считывается из специального массива, таблицы векторов прерываний, начинающегося в памяти по адресу 0000h:0000h. Каждый элемент этого массива представляет собой дальний адрес обработчика прерывания в формате сегмент:смещение или 4 нулевых байта, если обработчик не установлен. Команда INT помещает в стек регистр флагов и дальний адрес возврата, поэтому, чтобы завершить обработчик, надо выполнить команды popf и retf или одну команду iret, которая в реальном режиме полностью им аналогична.
; Пример обработчика программного прерывания int_handler proc far mov ax,0 iret int_handler endp
После того как обработчик написан, следующий шаг — привязка его к выбранному номеру прерывания. Это можно сделать, прямо записав его адрес в таблицу векторов прерываний, например так:
push 0 ; сегментный адрес таблицы ; векторов прерываний pop es ; в ES pushf ; поместить регистр флагов в стек cli ; запретить прерывания ; (чтобы не произошло аппаратного прерывания между следующими ; командами, обработчик которого теоретически может вызвать INT 87h ; в тот момент, когда смещение уже будет записано, а сегментный ; адрес еще нет, что приведет к передаче управления ; в неопределенную область памяти) ; поместить дальний адрес обработчика int_handler в таблицу ; векторов прерываний, в элемент номер 87h (одно из неиспользуемых прерываний) mov word ptr es:[87h*4], offset int_handler mov word ptr es:[87h*4+2], seg int_handler popf ; восстановить исходное значение флага IF
Теперь команда INT 87h будет вызывать наш обработчик, то есть приводить к записи 0 в регистр АХ.
Перед завершением работы программа должна восстанавливать все старые обработчики прерываний, даже если это были неиспользуемые прерывания типа 87h — автор какой-нибудь другой программы мог подумать точно так же. Для этого надо перед предыдущим фрагментом кода сохранить адрес старого обработчика, так что полный набор действий для программы, перехватывающей прерывание 87h, будет выглядеть следующим образом:
push 0 pop es ; скопировать адрес предыдущего обработчика в переменную old_handler mov eax,dword ptr es:[87h*4] mov dword ptr old_handler,eax ; установить наш обработчик pushf cli mov word ptr es:[87h*4], offset int_handler mov word ptr es:[87h*4+2], seg int_handler popf ; тело программы [...] ; восстановить предыдущий обработчик push 0 pop es pushf cli mov eax,word ptr old_handler mov word ptr es:[87h*4],eax popf
Хотя прямое изменение таблицы векторов прерываний и кажется достаточно удобным, все-таки это не лучший подход к установке обработчика прерывания, и пользоваться им следует только в случаях крайней необходимости, например внутри обработчиков прерываний. Для обычных программ DOS предоставляет две системные функции: 25h и 35h — установить и считать адрес обработчика прерывания, которые и рекомендуются к использованию в обычных условиях:
; скопировать адрес предыдущего обработчика в переменную old_handler mov ax,3587h ; АН = 35h, AL = номер прерывания int 21h ; функция DOS: считать ; адрес обработчика прерывания mov word ptr old_handler,bx ; возвратить ; смещение в ВХ mov word ptr old_handler+2,es ; и сегментный ; адрес в ES, ; установить наш обработчик mov ax,2587h ; АН = 25h, AL = номер прерывания mov dx,seg int_handler ; сегментный адрес mov ds,dx ; в DS mov dx,offset int_handler ; смещение в DX int 21h ; функция DOS: установить ; обработчик ; (не забывайте, что ES изменился после вызова функции 35h!) [...] ; восстановить предыдущий обработчик lds dx,old_handler ; сегментный адрес в DS и смещение в DX mov ax,2587h ; АН = 25h, AL = номер прерывания int 21h ; установить обработчик
Обычно обработчики прерываний используют для того, чтобы обрабатывать прерывания от внешних устройств или чтобы обслуживать запросы других программ. Эти возможности рассмотрены далее, а здесь показано, как можно использовать обычный обработчик прерывания (или, в данном случае, исключения ошибки) для того, чтобы быстро найти минимум и максимум в большом массиве данных.
; Процедура minmax ; находит минимальное и максимальное значения в массиве слов ; Ввод: DS:BX = адрес начала массива ; СХ = число элементов в массиве ; Вывод: ; АХ = максимальный элемент ВХ = минимальный элемент minmax proc near ; установить наш обработчик прерывания 5 push 0 pop es mov еах,dword ptr es:[5*4] mov dword ptr old_int5,eax mov word ptr es:[5*4],offset int5_handler mov word ptr es:[5*4]+2,cs ; инициализировать минимум и максимум первым элементом массива mov ax,word ptr [bx] mov word ptr lower_bound,ax mov word ptr upper_bound,ax ; обработать массив mov di,2 ; начать со второго элемента bcheck: mov ax,word ptr [bx][di] ; считать элемент в АХ bound ax,bounds ; команда BOUND вызывает ; исключение - ошибку 5, ; если АХ не находится в пределах lower_bound/upper_bound add di,2 ; следующий элемент loop bcheck ; цикл на все элементы ; восстановить предыдущий обработчик mov eax,dword ptr old_int5 mov dword ptr es:[5*4],eax ; вернуть результаты mov ax,word ptr upper_bound mov bx,word ptr lower_bound ret
bounds: lower_bound dw ? upper_bound dw ? old_int5 dd ?
; обработчик INT 5 для процедуры minmax ; сравнить АХ со значениями upper_bound и lower_bound и копировать ; AX в один из них, обработчик не обрабатывает конфликт между ; исключением BOUND и программным прерыванием распечатки экрана INT 5. ; Нажатие клавиши PrtScr в момент работы процедуры minmax приведет ; к ошибке. Чтобы это исправить, можно, например, проверять байт, ; на который указывает адрес возврата, если это CDh ; (код команды INT), то обработчик был вызван как INT 5 int5_handler proc far cmp ax,word ptr lower_bound ; сравнить АХ с нижней границей, jl its_lower ; если не меньше - ; это было нарушение mov word ptr upper_bound,ax ; верхней границы iret its_lower: mov word ptr lower_bound,ax ; если это было нарушение iret ; нижней границы int5_handler endp minmax endp
Разумеется, вызов исключения при ошибке занимает много времени, но, если массив достаточно большой и неупорядоченный, значительная часть проверок будет происходить без ошибок и быстро.
При помощи собственных обработчиков исключений можно справиться и с другими особыми ситуациями, например обрабатывать деление на ноль и остальные исключения, которые могут происходить в программе. В реальном режиме можно столкнуться всего с шестью исключениями:
#DE (деление на ноль) — INT 0 — ошибка, возникающая при переполнении и делении на ноль. Как для любой ошибки, адрес возврата указывает на ошибочную команду.
#DB (прерывание трассировки) — INT 1 — ловушка, возникающая после выполнения каждой команды, если флаг TF установлен в 1. Используется отладчиками, действующими в реальном режиме.
#OF (переполнение) — INT 4 — ловушка, возникающая после выполнения команды INTO, если флаг OF установлен.
#ВС (переполнение при BOUND) — INT 5 — уже рассмотренная нами ошибка, возникающая при выполнении команды BOUND.
#UD (недопустимая команда) — INT 6 — ошибка, возникающая при попытке выполнить команду, отсутствующую на данном процессоре.
#NM (сопроцессор отсутствует) — INT 7 — ошибка, возникающая при попытке выполнить команду FPU, если FPU отсутствует.
Параллельный порт
BIOS автоматически обнаруживает только три параллельных порта — с адресами 0378h – 037Ah (LPT1 или LPT2), 0278h – 027Ah (LPT2 или LPT3) и 03BCh – 03BDh (LPT1, если есть) — и записывает номера их базовых портов ввода-вывода в область данных BIOS по адресам 0040h:0008h, 0040h:000Ah, 0040h:000Ch соответственно. Если в системе установлен дополнительный параллельный порт, придется дополнительно записывать его базовый номер в 0040h:000Eh, чтобы BIOS воспринимала его как LPT4. Рассмотрим назначение портов ввода-вывода, управляющих параллельными портами на примере 0278h – 027Ah.
0278h для записи — порт данных. Чтение и запись в этот порт приводят к приему или посылке байта в принтер или другое присоединенное устройство.
0279h для чтения — порт состояния
бит 7: принтер занят, находится в off line или произошла ошибка
бит 6: нет подтверждения (1 — принтер не готов к приему следующего байта)
бит 5: нет бумаги
бит 4: принтер в режиме on line
бит 3: нет ошибок
бит 2: IRQ не произошло
биты 1 – 0: 0
027Ah для чтения и записи — порт управления
бит 5: включить двунаправленный обмен данными (этот режим не поддерживается BIOS)
бит 4: включить генерацию аппаратного прерывания (по сигналу подтверждения)
бит 3: установить принтер в on line
бит 2: 0 в этом бите инициализирует принтер
бит 1: режим посылки символа LF (0Ah) после каждого CR (0Dh)
бит 0: линия STROBE
Чтобы послать байт в принтер, программа должна убедиться, что линия BUSY (бит 7 порта состояния) равна нулю, а линия АСК (бит 6 порта состояния) — единице. Затем надо послать символ на линии DATA (порт данных), не ранее чем через 0,5 мкс установить линию STROBE (бит 0 порта управления) в 0, а затем, не менее чем через 0,5 мкс, — в 1. В отличие от последовательных портов параллельные хорошо поддерживаются BIOS и DOS, так что программирование их на уровне портов ввода-вывода может потребоваться только при написании драйвера для какого-нибудь необычного устройства, подключаемого к параллельному порту, или, например, при написании драйвера принтера для новой операционной системы.
Пассивная резидентная программа
В качестве первой резидентной программы рассмотрим именно пассивный резидент, который будет активироваться при попытке программ вызывать INT 21h и запрещать удаление файлов с указанного диска.
; tsr.asm ; Пример пассивной резидентной программы. ; Запрещает удаление файлов на диске, указанном в командной строке, всем ; программам, использующим средства DOS .model tiny .code org 2Ch envseg dw ? ; сегментный адрес копии окружения DOS
org 80h cmd_len db ? ; длина командной строки cmd_line db ? ; начало командной строки
org 100h ; СОМ-программа start: old_int21h: jmp short initialize ; эта команда занимает 2 байта, так что dw 0 ; вместе с этими двумя байтами получим ; old_int21h dd ? int21h_handler proc far ; обработчик прерывания 21h pushf ; сохранить флаги cmp ah,41h ; Если вызвали функцию 41h (удалить je fn41h ; файл) cmp ax,7141h ; или 7141h (удалить файл с длинным именем), je fn41h ; начать наш обработчик, jmp short not_fn41h ; иначе - передать управление ; предыдущему обработчику fn41h: push ax ; сохранить модифицируемые push bx ; регистры mov bx,dx cmp byte ptr ds:[bx+1],':' ; если второй символ ASCIZ-строки, ; переданной INT 21h, ; двоеточие - первый символ ; должен быть именем диска, je full_spec mov ah,19h ; иначе: int 21h ; функция DOS 19h - определить текущий диск, add al,'А' ; преобразовать номер диска к заглавной букве, jmp short compare ; перейти к сравнению full_spec: mov al,byte ptr [bx] ; AL = имя диска из ASCIZ-строки and al,11011111b ; преобразовать к заглавной букве compare: cmp al,byte ptr cs:cmd_line[1] ; если диски je access_denied ; совпадают - запретить доступ, pop bx ; иначе: восстановить pop ax ; регистры not_fn41h: popf ; и флаги jmp dword ptr cs:old_int21h ; и передать управление ; предыдущему обработчику INT 21h access_denied: pop bx ; восстановить регистры pop ax popf push bp mov bp,sp or word ptr [bp+6],1 ; установить флаг переноса ; (бит 0) в регистре флагов, ; который поместила команда INT в стек ; перед адресом возврата pop bp mov ax,5 ; возвратить код ошибки "доступ запрещен" iret ; вернуться в программу int21h_handler endp
initialize proc near cmp byte ptr cmd_len,3 ; проверить размер командной строки jne not_install ; (должно быть 3 - пробел, диск, двоеточие), cmp byte ptr cmd_line[2],':' ; проверить третий символ jne not_install ; командной строки (должно быть двоеточие), mov al,byte ptr cmd_line[1] and al,11011111b ; преобразовать второй ; символ к заглавной букве, cmp al,'А' ; проверить, что это не jb not_install ; меньше "А" и не больше cmp al,'Z' ; "Z", ja not_install ; если хоть одно из этих условий ; не выполняется - выдать информацию ; о программе и выйти, иначе - начать ; процедуру инициализации mov ax,3521h ; АН = 35h, AL = номер прерывания int 21h ; получить адрес обработчика INT 21h mov word ptr old_int21h,bx ; и поместить его в old_int21h mov word ptr old_int21h+2,es
mov ax,2521h ; AH = 25h, AL = номер прерывания mov dx,offset int21h_handler ; DS:DX - адрес нашего обработчика int 21h ; установить обработчик INT 21h mov ah,49h ; AH = 49h mov es,word ptr envseg ; ES = сегментный адрес блока с нашей ; копией окружения DOS int 21h ; освободить память из-под окружения mov dx,offset initialize ; DX - адрес первого байта за концом ; резидентной части программы int 27h ; завершить выполнение, оставшись ; резидентом
not_install: mov ah,9 ; АН = 09h mov dx,offset usage ; DS:DX = адрес строки с информацией об ; использовании программы int 21h ; вывод строки на экран ret ; нормальное завершение программы
; текст, который выдает программа при запуске с неправильной командной строкой: usage db "Использование: tsr.com D:",0Dh,0Ah db "Запрещает удаление на диске D:",ODh,OAh db "$" initialize endp end start
Если запустить эту программу с командной строкой D:, никакой файл на диске D нельзя будет удалить командой Del, средствами оболочек типа Norton Commander и большинством программ для DOS. Действие этого запрета, однако, не будет распространяться на оболочку Far, которая использует системные функции Windows API, и на программы типа Disk Editor, обращающиеся с дисками при помощи функций BIOS (INT 13h). Несмотря на то что мы освободили память, занимаемую окружением DOS (а это могло быть лишних 512 или даже 1024 байта), наша программа все равно занимает в памяти 352 байта из-за того, что первые 256 байт отводятся для блока PSP. Существует возможность оставить программу резидентной без PSP — для этого инсталляционная часть программы должна скопировать резидентную часть с помощью, например, movs в начало PSP. Но при этом возникает сразу несколько проблем: во-первых, команда INT 27h, так же как и функция DOS 31h, использует данные из PSP для своей работы, во-вторых, код резидентной части должен быть написан для работы с нулевого смещения, а не со 100h, как обычно, и, в-третьих, некоторые программы, исследующие выделенные блоки памяти, определяют конец блока по адресу, находящемуся в PSP программы — владельца блока со смещением 2. С первой проблемой можно справиться вручную, создав отдельные блоки памяти для резидентной и инсталляционной частей программы, новый PSP для инсталляционной части и завершив программу обычной функцией 4Ch или INT 20h. Реальные программы, делающие это, существуют (например, программа поддержки нестандартных форматов дискет PU_1700), но мы не будем чрезмерно усложнять наш первый пример и скопируем резидентную часть не в позицию 0, а в позицию 80h, то есть, начиная с середины PSP, оставив в нем все значения, необходимые для нормальной работы функций DOS.
Прежде чем это сделать, заметим, что и номер диска, и адрес предыдущего обработчика INT 21h изменяются только при установке резидента и являются константами во время всей его работы. Более того, каждое из этих чисел используется только по одному разу. В этих условиях оказывается, что можно вписать номер диска и адрес перехода на старый обработчик прямо в код программы. Более того, после этого наш резидент не будет больше ссылаться ни на какие переменные с конкретными адресами, а значит, его код становится перемещаемым, то есть его можно выполнять, скопировав в любую область памяти.
; tsrpsp.asm ; Пример пассивной резидентной программы с переносом кода в PSP. ; Запрещает удаление файлов на диске, указанном в командной строке, ; всем программам, использующим средства DOS
.model tiny .code org 2Ch envseg dw ? ; сегментный адрес копии окружения DOS
org 80h cmd_len db ? ; длина командной строки cmd_line db ? ; начало командной строки
org 100h ; СОМ-программа start: old_int21h: jmp short initialize ; переход на инициализирующую часть
int21h_handler proc far ; обработчик прерывания 21h pushf ; сохранить флаги cmp ah,41h ; Если вызвали функцию 41h ; (удалить файл) je fn41h cmp ax,7141h ; или 7141h (удалить файл ; с длинным именем), je fn41h ; начать наш обработчик, jmp short not_fn41h ; иначе - передать ; управление предыдущему обработчику fn41h: push ax ; сохранить модифицируемые push bx ; регистры mov bx,dx ; можно было бы использовать ; адресацию [edx+1], но в старшем ; слове EDX совсем не обязательно 0, cmp byte ptr [bx+1],':' ; если второй символ ; ASCIZ-строки, переданной INT 21h, ; двоеточие, первый символ должен ; быть именем диска, je full_spec mov ah,19h ; иначе: int 21h ; функция DOS 19h - определить ; текущий диск add al,'А' ; преобразовать номер диска ; к заглавной букве jmp short compare ; перейти к сравнению full_spec: mov al,byte ptr [bx] ; AL = имя диска из ASCIZ-строки and al,11011111b ; преобразовать к заглавной букве compare: db 3Ch ; начало кода команды CMP AL,число drive_letter: db 'Z' ; сюда процедура инициализации ; впишет нужную букву pop bx ; эти регистры больше не pop ax ; понадобятся, если диски совпадают - je access_denied ; запретить доступ not_fn41h: popf ; восстановить флаги и передать ; управление предыдущему ; обработчику INT 21h: db 0EAh ; начало кода команды ; JMP, FAR-число old_int21h dd 0 ; сюда процедура инициализации ; запишет адрес предыдущего ; обработчика INT 21h access_denied: popf push bp mov bp,sp ; чтобы адресоваться в стек ; в реальном режиме, or word ptr [bp+6],1 ; установить флаг ; переноса (бит 0) в регистре ; флагов, который поместила команда ; INT в стек перед адресом возврата pop bp mov ax,5 ; возвратить код ошибки ; "доступ запрещен" iret ; вернуться в программу int21h_handler endp
tsr_length equ $-int21h_handler
initialize proc near cmp byte ptr cmd_len,3 ; проверить размер ; командной строки jne not_install ; (должно быть 3 - ; пробел, диск, двоеточие) cmp byte ptr cmd_line[2],':' ; проверить ; третий символ командной jne not_install ; строки (должно быть двоеточие) mov al,byte ptr cmd_line[1] and al,11011111b ; преобразовать второй ; символ к заглавной букве cmp al,'A' ; проверить, что это не меньше "А" jb not_install ; и не больше cmp al,'Z' ; "Z", ja not_install ; если хоть одно из ; этих условий ; не выполняется - выдать информацию о программе и выйти, ; иначе - начать процедуру инициализации mov byte ptr drive_letter,al ; вписать имя ; диска в код резидента push es mov ax,3521h ; АН = 35h, ; AL = номер прерывания int 21h ; получить адрес обработчика INT 21h mov word ptr old_int21h,bx ; и вписать его ; в код резидента mov word ptr old_int21h+2,es pop es
cld ; перенос кода резидента, mov si,offset int21h_handler ; начиная ; с этого адреса, mov di,80h ; в PSP:0080h rep movsb mov ax,2521h ; AH = 25h, ; AL = номер прерывания mov dx,0080h ; DS:DX - адрес нашего обработчика int 21h ; установить обработчик INT 21h mov ah,49h ; AH = 49h mov es,word ptr envseg ; ES = сегментный адрес блока ; с нашей копией окружения DOS int 21h ; освободить память из-под ; окружения mov dx,80h+tsr_length ; DX - адрес первого ; байта за концом резидентной части ; программы int 27h ; завершить выполнение, ; оставшись резидентом not_install: mov ah,9 ; АН = 09h mov dx,offset usage ; DS:DX = адрес строки ; с информацией об ; использовании программы int 21h ; вывод строки на экран ret ; нормальное завершение ; программы
; текст, который выдает программа при запуске ; с неправильной командной строкой: usage db "Usage: tsr.com D:",0Dh,0Ah db "Denies delete on drive D:",0Dh,0Ah db "$" initialize endp end start
Теперь эта резидентная программа занимает в памяти только 208 байт.
Передача параметров
Процедуры могут получать или не получать параметры из вызывающей процедуры и могут возвращать или не возвращать результаты (процедуры, которые что-либо возвращают, называются функциями в языке Pascal, но ассемблер не делает каких-либо различий между ними).
Параметры можно передавать с помощью одного из шести механизмов:
по значению;
по ссылке;
по возвращаемому значению;
по результату;
по имени;
отложенным вычислением.
Параметры можно передавать в одном из пяти мест:
в регистрах;
в глобальных переменных;
в стеке;
в потоке кода;
в блоке параметров.
Так что всего в ассемблере возможно 30 различных способов передачи параметров для процедур. Рассмотрим их по порядку.
Передача параметров отложенным вычислением
Как и в предыдущем случае, здесь процедура получает адрес функции, вычисляющей значение параметра. Такой механизм удобен, если вычисление значения параметра требует много ресурсов или времени, например, если функция должна выбрать один из нескольких ходов при игре в шахматы, вычисление каждого параметра может занимать несколько минут. При передаче параметров отложенным вычислением функция получает адрес заглушки, которая при первом обращении к ней вычисляет значение параметра и сохраняет его во внутренней локальной переменной, а при дальнейших вызовах возвращает ранее вычисленное значение. Если процедуре вообще не потребуются значения части параметров (например, если первый же ход приводит к мату), то использование отложенных вычислений способствует значительному выигрышу. Этот механизм чаще всего применяется в системах искусственного интеллекта и операционных системах.
Рассказав об основных механизмах того, как передавать параметры процедуре, рассмотрим применяемые в ассемблере варианты, где их передавать.
Передача параметров по имени
Это механизм, который используют макроопределения, директива EQU, а также, например, препроцессор С при обработке команды #define. При реализации этого механизма в компилирующем языке программирования (к которому относится и ассемблер) приходится заменять передачу параметра по имени другими механизмами при помощи, в частности, макроопределений.
Если определено макроопределение
pass_by_name macro parameter1 mov ax,parameter1 endm
то теперь в программе можно передавать параметр так:
pass_by_name value call procedure
Примерно так же поступают языки программирования высокого уровня, поддерживающие этот механизм: процедура получает адрес специальной функции-заглушки, которая вычисляет адрес передаваемого по имени параметра.
Передача параметров по результату
Этот механизм отличается от предыдущего только тем, что при вызове процедуры предыдущее значение параметра никак не определяется, а переданный адрес используется только для записи в него результата.
Передача параметров по ссылке
Процедуре передается не значение переменной, а ее адрес, по которому процедура должна сама прочитать значение параметра. Этот механизм удобен для передачи больших массивов данных и для тех случаев, когда процедура должна модифицировать параметры, хотя он и медленнее из-за того, что процедура будет выполнять дополнительные действия для получения значений параметров.
mov ax,offset value call procedure
Передача параметров по возвращаемому значению
Этот механизм объединяет передачу по значению и по ссылке. Процедуре передают адрес переменной, а процедура делает локальную копию параметра, затем работает с ней, а в конце записывает локальную копию обратно по переданному адресу. Этот метод эффективнее обычной передачи параметров по ссылке в тех случаях, когда процедура должна обращаться к параметру достаточно большое число раз, например, если используется передача параметров в глобальной переменной:
mov global_variable,offset value call procedure [...] procedure proc near mov dx,global_variable mov ax,word ptr [dx] (команды, работающие с АХ в цикле десятки тысяч раз) mov word ptr [dx],ax procedure endp
Передача параметров по значению
Процедуре передается собственно значение параметра. При этом фактически значение параметра копируется, и процедура использует его копию, так что модификация исходного параметра оказывается невозможной. Этот механизм применяется для передачи небольших параметров, таких как байты или слова.
Например, если параметры передаются в регистрах:
mov ax,word ptr value ; сделать копию значения call procedure ; вызвать процедуру
Передача параметров в блоке параметров
Блок параметров — это участок памяти, содержащий параметры, так же как и в предыдущем примере, но располагающийся обычно в сегменте данных. Процедура получает адрес начала этого блока при помощи любого метода передачи параметров (в регистре, в переменной, в стеке, в коде или даже в другом блоке параметров). В качестве примеров использования этого метода можно назвать многие функции DOS и BIOS, например поиск файла, использующий блок параметров DTA, или загрузка (и исполнение) программы, использующая блок параметров ЕРВ.
Передача параметров в глобальных переменных
Когда не хватает регистров, один из способов обойти это ограничение— записать параметр в переменную, к которой затем обращаться из процедуры. Этот метод считается неэффективным, и его использование может привести к тому, что рекурсия и повторная входимость станут невозможными.
Передача параметров в потоке кода
В этом необычном методе передаваемые процедуре данные размещаются прямо в коде программы, сразу после команды CALL (как реализована процедура print в одной из стандартных библиотек процедур для ассемблера UCRLIB):
call print db "This ASCIZ-line will be printed",0 (следующая команда)
Чтобы прочитать параметр, процедура должна использовать его адрес, который автоматически передается в стеке как адрес возврата из процедуры. Разумеется, функция должна будет изменить адрес возврата на первый байт после конца переданных параметров перед выполнением команды RET. Например, процедуру print можно реализовать следующим образом:
print proc near push bp mov bp,sp push ax push si mov si,[bp+2] ; прочитать адрес ; возврата/начала данных cld ; установить флаг направления ; для команды lodsb print_readchar: lodsb ; прочитать байт из строки, test al,al ; если это 0 (конец строки), jz print_done ; вывод строки закончен int 29h ; вывести символ в AL на экран jmp short print_readchar print_done: mov [bp+2],si ; поместить новый адрес возврата в стек pop si pop ax pop bp ret print endp
Передача параметров в потоке кода, так же как и передача параметров в стеке в обратном порядке (справа налево), позволяет передавать различное число параметров, но этот метод— единственный, позволяющий передать по значению параметр различной длины, что и продемонстрировал этот пример. Доступ к параметрам, переданным в потоке кода, несколько медленнее, чем к параметрам, переданным в регистрах, глобальных переменных или стеке, и примерно совпадает со следующим методом.
Передача параметров в регистрах
Если процедура получает небольшое число параметров, идеальным местом для их передачи оказываются регистры. Примерами использования этого метода могут служить практически все вызовы прерываний DOS и BIOS. Языки высокого уровня обычно используют регистр АХ (ЕАХ) для того, чтобы возвращать результат работы функции.
Передача параметров в стеке
Параметры помещаются в стек сразу перед вызовом процедуры. Именно этот метод используют языки высокого уровня, такие как С и Pascal. Для чтения параметров из стека обычно используют не команду POP, а регистр ВР, в который помещают адрес вершины стека после входа в процедуру:
push parameter1 ; поместить параметр в стек push parameter2 call procedure add sp,4 ; освободить стек от параметров [...] procedure proc near push bp mov bp,sp (команды, которые могут использовать стек) mov ax,[bp+4] ; считать параметр 2. ; Его адрес в сегменте стека ВР + 4, потому что при выполнении ; команды CALL в стек поместили адрес возврата - 2 байта для процедуры ; типа NEAR (или 4 - для FAR), а потом еще и ВР - 2 байта mov bx,[bp+6] ; считать параметр 1 (остальные команды) рор bp ret procedure endp
Параметры в стеке, адрес возврата и старое значение ВР вместе называются активационной записью функции.
Для удобства ссылок на параметры, переданные в стеке, внутри функции иногда используют директивы EQU, чтобы не писать каждый раз точное смещение параметра от начала активационной записи (то есть от ВР), например так:
push X push Y push Z call xyzzy [...] xyzzy proc near xyzzy_z equ [bp+8] xyzzy_y equ [bp+6] xyzzy_x equ [bp+4] push bp mov bp,sp (команды, которые могут использовать стек) mov ax,xyzzy_x ;считать параметр X (остальные команды) pop bp ret 6 xyzzy endp
При внимательном анализе этого метода передачи параметров возникает сразу два вопроса: кто должен удалять параметры из стека, процедура или вызывающая ее программа, и в каком порядке помещать параметры в стек. В обоих случаях оказывается, что оба варианта имеют свои «за» и «против», так, например, если стек освобождает процедура (командой RET число_байтов), то код программы получается меньшим, а если за освобождение стека от параметров отвечает вызывающая функция, как в нашем примере, то становится возможным вызвать несколько функций с одними и теми же параметрами просто последовательными командами CALL. Первый способ, более строгий, используется при реализации процедур в языке Pascal, а второй, дающий больше возможностей для оптимизации, — в языке С. Разумеется, если передача параметров через стек применяется и для возврата результатов работы процедуры, из стека не надо удалять все параметры, но популярные языки высокого уровня не пользуются этим методом. Кроме того, в языке С параметры помещают в стек в обратном порядке (справа налево), так что становятся возможными функции с изменяемым числом параметров (как, например, printf — первый параметр, считываемый из [ВР+4], определяет число остальных параметров). Но подробнее о тонкостях передачи параметров в стеке рассказано далее, а здесь приведен обзор методов.
Перехват прерываний
В архитектуре процессоров 80x86 предусмотрены особые ситуации, когда процессор прекращает (прерывает) выполнение текущей программы и немедленно передает управление программе-обработчику, специально написанной для обработки этой конкретной ситуации. Такие особые ситуации делятся на два типа: прерывания и исключения, в зависимости от того, вызвало ли эту ситуацию какое-нибудь внешнее устройство или выполняемая процессором команда. Исключения делятся далее на три типа: ошибки, ловушки и остановы, в зависимости от того, когда по отношению к вызвавшей их команде они происходят. Ошибки происходят перед выполнением команды, так что обработчик такого исключения получит в качестве адреса возврата адрес ошибочной команды (начиная с процессоров 80286), ловушки происходят сразу после выполнения команды, так что обработчик получает в качестве адреса возврата адрес следующей команды, и наконец, остановы могут происходить в любой момент и вообще не предусматривать средств возврата управления в программу.
Команда INT (а также INTO и INT3) используется в программах как раз для того, чтобы вызывать обработчики прерываний (или исключений). Фактически они являются исключениями ловушки, поскольку адрес возврата, который передается обработчику, указывает на следующую команду, но так как эти команды были введены до разделения особых ситуаций на прерывания и исключения, их практически всегда называют командами вызова прерываний. Ввиду того, что обработчики прерываний и исключений в DOS обычно не различают механизм вызова, с помощью команды INT можно передавать управление как на обработчики прерываний, так и исключений.
Как показано в главе 4, программные прерывания, то есть передача управления при помощи команды INT, являются основным средством вызова процедур DOS и BIOS, потому что в отличие от вызова через команду CALL здесь не нужно знать адреса вызываемой процедуры — достаточно только номера. С другой стороны интерфейса рассмотрим, как строится обработчик программного прерывания.
Полурезидентные программы
Полурезидентные программы — это программы, которые загружают и выполняют другую программу, оставаясь при этом в памяти, а затем, после того как загруженная программа заканчивается, они тоже заканчиваются обычным образом. Полурезидентная программа может содержать обработчики прерываний, которые будут действовать все время, пока работает загруженная из-под нее обычная программа. Так что, с точки зрения этой дочерней программы, полурезидентная программа функционирует как обычная резидентная. Эти программы удобно использовать для внесения изменений и дополнений в существующие программы, если нельзя внести исправления прямо в их исполнимый код. Так создаются загрузчики для игр, которые хранят свой код в зашифрованном или упакованном виде. Такой загрузчик может отслеживать определенные комбинации клавиш и обманывать игру, добавляя игроку те или иные ресурсы, или, например, находить код проверки пароля и выключать его.
В качестве примера напишем простой загрузчик для игры «Tie Fighter», который устранит ввод пароля, требующийся при каждом запуске игры. Разумеется, это условный пример, так как игра никак не шифрует свои файлы, и тот же эффект можно было достигнуть, изменив всего два байта в файле front.ovl. Единственное преимущество нашего загрузчика будет состоять в том, что он оказывается годен для всех версий игры (от «X-Wing» до «Tie Fighter: Defender of the Empire»).
; tieload.asm ; Пример полурезидентной программы - загрузчик, устраняющий проверку пароля ; для игр компании Lucasarts: ; "X-Wing", "X-Wing: Imperial Pursuit", "B-Wing", ; "Tie Fighter", "Tie Fighter: Defender of the Empire" ; .model tiny .code .386 ; для команды LSS org 100h ; СОМ-программа start: ; освободить память после конца программы (+ стек) mov sp,length_of_program ; перенести стек mov ah,4Ah ; функция DOS 4Ah mov bx,par_length ; размер в параграфах int 21h ; изменить размер выделенной памяти
; заполнить поля ЕРВ, содержащие сегментные адреса mov ax,cs mov word ptr EPB+4,ax mov word ptr EPB+8,ax mov word ptr EPB+0Ch,ax
; загрузить программу без выполнения mov bx,offset EPB ; ES:BX - EPB mov dx, offset filename ; DS:DX - имя файла (TIE.EXE) mov ax,4B01h ; функция DOS 4B01h int 21h ; загрузить без выполнения jnc program_loaded ; если TIE.EXE не найден, mov byte ptr XWING,1 ; установить флаг для find_passwd mov ax,4B01h mov dx,offset filename2 ; и попробовать BWING.EXE, int 21h jnc program_loaded ; если он не найден, mov ax,4B01h mov dx,offset filename3 ; попробовать XWING.EXE, int 21h jc error_exit ; если и он не найден ; (или не загружается по ; какой-нибудь другой причине) ; выйти с сообщением об ошибке program_loaded: ; Процедура проверки пароля не находится непосредственно в исполняемом файле ; tie.exe, bwing.exe или xwing.exe, а подгружается позже из оверлея front.ovl, ; bfront.ovl или fcontend.ovl соответственно. Найти команды, выполняющие чтение ; из этого оверлея, и установить на них наш обработчик find_passwd cld push cs pop ax add ax,par_length mov ds,ax xor si,si ; DS:SI - первый параграф после конца нашей ; программы (то есть начало области, в которую ; была загружена модифицируемая программа) mov di,offset read_file_code ; ES:DI - код для сравнения mov cx,rf_code_l ; CX - его длина call find_string ; поиск кода, jc error_exit2 ; если он не найден - выйти ; с сообщением об ошибке ; заменить 6 байт из найденного кода командами call find_passwd и nop mov byte ptr [si],9Ah ; CALL (дальний) mov word ptr [si+1],offset find_passwd mov word ptr [si+3],cs mov byte ptr [si+5],90h ; NOP
; запустить загруженную программу ; надо записать правильные начальные значения в регистры для ЕХЕ-программы ; и заполнить некоторые поля ее PSP mov ah,51h ; функция DOS 51h int 21h ; BX = PSP-сегмент загруженной программы mov ds,bx ; поместить его в DS mov es,bx ; и ES. Заполнить также поля PSP: mov word ptr ds:[0Ah],offset exit_without_msg mov word ptr ds:[0Ch],cs ; "адрес возврата" mov word ptr ds:[16h],cs ; и "адрес PSP - предка" lss sp,dword ptr cs:EPB_SSSP ; загрузить SS:SP jmp dword ptr cs:EPB_CSIP ; и передать управление на ; точку входа программы
XWING db 0 ; 1/0: тип защиты X-wing/Tie-fighter ЕРВ dw 0 ; запускаемый файл получает среду DOS от tieload.com, dw 0080h,? ; и командную строку, dw 005Ch,? ; и первый FCB, dw 006Ch,? ; и второй FCB EPB_SSSP dd ? ; начальный SS:SP - заполняется DOS EPB_CSIP dd ? ; начальный CS:IP - заполняется DOS
filename1 db "tie.exe",0 ; сначала пробуем запустить этот файл, filename2 db "bwing.exe",0 ; потом этот, filename3 db "xwing.exe",0 ; а потом этот
; сообщения об ошибках error_msg db "Ошибка: не найдены ни один из файлов TIE.EXE, " db "BWING.EXE, XWING. EXE",0Dh,0Ah,'$' error_msg2 db "Ошибка: участок кода не найден",0Dh,0Ah,'$'
; команды, выполняющие чтение оверлейного файла в tie.exe/bwing.exe/xwing.exe: read_file_code: db 33h,0D2h ; xor dx,dx db 0B4h,3Fh ; mov ah,3Fh db 0CDh,21h ; int 21h db 72h ; jz (на разный адрес в xwing и tie) rf_code_l = $-read_file_code
; Команды, вызывающие процедуру проверки пароля. ; Аналогичный набор команд встречается и в других местах, поэтому find_passwd ; будет выполнять дополнительные проверки passwd_code: db 89h,46h,0FCh ; mov [bp-4],ax db 89h,56h,OFEh ; mov [bp-2],dx db 52h ; push dx db 50h ; push ax db 9Ah ; call far passwd_l = $-passwd_code
error_exit: mov dx,offset error_msg ; вывод сообщения об ошибке 1 jmp short exit_with_msg error_exit2: mov dx,offset error_msg2 ; вывод сообщения об ошибке 2 exit_with_msg: mov ah, 9 ; Функция DOS 09h int 21h ; вывести строку на экран exit_without_msg: ; сюда также передается управление после ; завершения загруженной программы (этот адрес ; был вписан в поле PSP "адрес возврата") mov ah,4Ch ; Функция DOS 4Ch int 21h ; конец программы
; эту процедуру вызывает программа tie.exe/bwing.exe/xwing.exe каждый раз, когда ; она выполняет чтение из оверлейного файла find_passwd proc far ; выполнить три команды, которые мы заменили на call find_passwd xor dx,dx mov ah,3Fh ; функция DOS 3Fh int 21h ; чтение из файла или устройства deactivation_point: ; по этому адресу мы запишем код команды RETF, ; когда наша задача будет выполнена, pushf ; сохраним флаги push ds ; и регистры push es pusha push cs pop es mov si,dx ; DS:DX - начало только что прочитанного участка ; оверлейного файла mov di,offset passwd_code ; ES:DI - код для сравнения dec si ; очень скоро мы его увеличим обратно search_for_pwd: ; в этом цикле найденные вхождения эталонного кода ; проверяются на точное соответствие коду проверки пароля inc si ; процедура find_string возвращает DS:SI указывающим на ; начало найденного кода - чтобы искать дальше, надо ; увеличить SI хотя бы на 1 mov cx,passwd_l ; длина эталонного кода call find_string ; поиск его в памяти, jc pwd_not_found ; если он не найден - выйти ; find_string нашла очередное вхождение нашего эталонного кода вызова ; процедуры - проверим, точно ли это вызов процедуры проверки пароля cmp byte ptr [si+10],00h ; этот байт должен быть 00 jne search_for_pwd cmp byte ptr cs:XWING,1 ; в случае X-wing/B-wing jne check_for_tie cmp word ptr [si+53],0774h ; команда je должна быть здесь, jne search_for_pwd jmp short pwd_found check_for_tie: ; а в случае Tie Fighter - cmp word ptr [si+42],0774h ; здесь jne search_for_pwd pwd_found: ; итак, вызов процедуры проверки пароля найден - отключить его mov word ptr ds:[si+8],9090h ; NOP NOP mov word ptr ds:[si+10],9090h ; NOP NOP mov byte ptr ds:[si+12],90h ; NOP ; и деактивировать нашу процедуру find_passwd mov byte ptr cs:deactivation_point,0CBh ; RETF pwd_not_found: popa ; восстановить регистры pop es pop ds popf ; и флаги ret ; и вернуть управление в программу find_passwd endp
; процедура find_string ; выполняет поиск строки от заданного адреса до конца всей общей памяти ; ввод: ES:DI - адрес эталонной строки ; СХ - ее длина ; DS:SI - адрес, с которого начинать поиск ; вывод: CF = 1, если строка не найдена, ; иначе: CF = 0 и DS:SI - адрес, с которого начинается найденная строка find_string proc near push ax push bx push dx ; сохранить регистры do_cmp: mov dx,1000h ; поиск блоками по 1000h (4096 байт) cmp_loop: push di push si push cx repe cmpsb ; сравнить DS:SI со строкой pop cx pop si pop di je found_code ; если совпадение - выйти с CF = 0, inc si ; иначе - увеличить DS:SI на 1, dec dx ; уменьшить счетчик в DX jne cmp_loop ; и, если он не ноль, продолжить ; пройден очередной 4-килобайтный блок sub si,1000h ; уменьшить SI на 1000h mov ax,ds inc ah ; и увеличить DS на 1 mov ds,ax cmp ax,9000h ; если мы добрались до jb do_cmp ; сегментного адреса 9000h - pop dx ; восстановить регистры pop bx pop ax stc ; установить CF = 1 ret ; и выйти ; сюда передается управление, если строка найдена found_code: pop dx ; восстановить регистры pop bx pop ax clc ; установить CF = 0 ret ; и выйти find_string endp
end_of_program: lengtn_of_program = $-start+100h+100h ; длина программы в байтах par_length = length_of_program + 0Fh par_length = par_length/16 ; длина программы в параграфах end start
Последовательный порт
Каждый из последовательных портов обменивается данными с процессором через набор портов ввода-вывода: СОМ1 = 03F8h – 03FFh, COM2 = 02F8h – 02FFh, COM3 = 03E8H – 03EFh и COM4 = 02E8h – 02EFh. Имена портов СОМ1 – COM4 на самом деле никак не зафиксированы. BIOS просто называет порт СОМ1, адрес которого (03F8h по умолчанию) записан в области данных BIOS по адресу 0040h:0000h. Точно так же порт COM2, адрес которого записан по адресу 0040h:0002h, COM3 — 0040h:0004h и COM4 — 0040h:0006h. Рассмотрим назначение портов ввода-вывода на примере 03F8h – 03FFh.
03F8h для чтения и записи — если старший бит регистра управления линией = 0, это — регистр передачи данных (THR или RBR). Передача и прием данных через последовательный порт соответствуют записи и чтению именно в этот порт.
03F8h для чтения и записи — если старший бит регистра управления линией = 1, это — младший байт делителя частоты порта.
03F9h для чтения и записи — если старший бит регистра управления линией = 0, это — регистр разрешения прерываний (IER):
бит 3: прерывание по изменению состояния модема
бит 2: прерывание по состоянию BREAK или ошибке
бит 1: прерывание, если буфер передачи пуст
бит 0: прерывание, если пришли новые данные
03F9h для чтения и записи — если старший бит регистра управления линией = 1, это — старший байт делителя частоты порта. Значение скорости порта определяется по значению делителя частоты (табл. 20).
Таблица 20. Делители частоты последовательного порта
Делитель частоты |
Скорость |
0000h |
115 200 |
0001h |
57 600 |
0002h |
38 400 |
0006h |
19 200 |
000Ch |
9 600 |
0010h |
7 200 |
0018h |
4 800 |
0020h |
3 600 |
0030h |
2 400 |
03FAh для чтения — регистр идентификации прерывания. Содержит информацию о причине прерывания для обработчика:
биты 7 – 6: 00 — FIFO отсутствует, 11 — FIFO присутствует
бит 3: тайм-аут FIFO приемника
биты 2 – 1: тип произошедшего прерывания:
11 — состояние BREAK или ошибка. Сбрасывается после чтения из 03FDh
10 — пришли данные. Сбрасывается после чтения из 03F8h
01 — буфер передачи пуст. Сбрасывается после записи в 03F8h
00 — изменилось состояние модема. Сбрасывается после чтения из 03FEh
бит 0: 0, если произошло прерывание, 1, если нет
03FAh для записи — регистр управления FIFO (FCR)
биты 7 – 6: порог срабатывания прерывания о приеме данных
00 — 1 байт
01 — 4 байта
10 — 8 байт
11 — 16 байт
бит 2 — очистить FIFO приемника
бит 1 — очистить FIFO передатчика
бит 0 — включить режим работы через FIFO
03FBh для чтения и записи — регистр управления линией (LCR)
бит 7: если 1 — порты 03F8h и 03F9H работают, как делитель частоты порта
бит 6: состояние BREAK — порт непрерывно посылает нули
биты 5 – 3: четность:
? ? 0 — без четности
0 0 1 — контроль на четность
0 1 1 — контроль на нечетность
1 0 1 — фиксированная четность 1
1 1 1 — фиксированная четность 0
? ? 1 — программная (не аппаратная) четность
бит 2: число стоп-бит:
0 — 1 стоп-бит
1 — 2 стоп-бита для 6-, 7-, 8-битных, 1,5 стоп-бита для 5-битных слов
биты 1 – 0: длина слова
00 — 5 бит
01 — 6 бит
10 — 7 бит
11 — 8 бит
03FBH для чтения и записи — регистр управления модемом (MCR)
бит 4: диагностика (выход СОМ-порта замыкается на вход)
бит 3: линия OUT2 — должна быть 1, чтобы работали прерывания
бит 2: линия OUT1 — должна быть 0
бит 1: линия RTS
бит 0: линия DTR
03FCH для чтения — регистр состояния линии (LSR)
бит 6: регистр сдвига передатчика пуст
бит 5: регистр хранения передатчика пуст — можно писать в 03F8h
бит 4: обнаружено состояние BREAK (строка нулей длиннее, чем старт-бит + слово + четность + стоп-бит)
бит 3: ошибка синхронизации (получен нулевой стоп-бит)
бит 2: ошибка четности
бит 1: ошибка переполнения (пришел новый байт, хотя старый не был прочитан из 03F8h, при этом старый байт теряется)
бит 0: данные получены и готовы для чтения из 03F8h
03FDh для чтения — регистр состояния модема (MSR)
бит 7: линия DCD (несущая)
бит 6: линия RI (звонок)
бит 5: линия DSR (данные готовы)
бит 4: линия CTS (разрешение на посылку)
бит 3: изменилось состояние DCD
бит 2: изменилось состояние RI
бит 1: изменилось состояние DSR
бит 0: изменилось состояние CTS
02FFh для чтения и записи — запасной регистр. Не используется контроллером последовательного порта, любая программа может им пользоваться.
Итак, первое, что должна сделать программа, работающая с последовательным портом, — проинициализировать его, выполнив запись в регистр управления линией (03FBh) числа 80h, запись в порты 03F8h и 03F9h делителя частоты, снова запись в порт 03FBh с нужными битами, а также запись в регистр разрешения прерываний (03F9h) для выбора прерываний. Если программа вообще не пользуется прерываниями — надо записать в этот порт 0.
Перед записью данных в последовательный порт можно проверить бит 5, а перед чтением — бит 1 регистра состояния линии, но, если программа использует прерывания, эти условия выполняются автоматически. Вообще говоря, реальная серьезная работа с последовательным портом возможна только при помощи прерываний. Посмотрим, как может быть устроена такая программа на следующем примере:
; term2.asm ; Минимальная терминальная программа, использующая прерывания ; Выход - Alt-X
.model tiny .code .186 org 100h ; СОМ-программа
; следующие четыре директивы определяют, для какого последовательного порта ; скомпилирована программа (никаких проверок не выполняется - не запускайте этот ; пример, если у вас нет модема на соответствующем порту). Реальная программа ; должна определять номер порта из конфигурационного файла или из командной ; строки COM equ 02F8h ; номер базового порта (COM2) IRQ equ 0Bh ; номер прерывания (INT 0Bh для IRQ3) E_BITMASK equ 11110111b ; битовая маска для разрешения IRQ3 D_BITMASK equ 00001000b ; битовая маска для запрещения IRQ3
start: call init_everything ; инициализация линии и модема main_loop: ; основной цикл ; реальная терминальная программа в этом цикле будет выводить данные из буфера ; приема (заполняемого из обработчика прерывания) на экран, если идет обычная ; работа, в файл, если пересылается файл, или обрабатывать как-то по-другому. ; В нашем примере мы используем основной цикл для ввода символов, хотя лучше это ; делать из обработчика прерывания от клавиатуры mov ah,8 ; Функция DOS 08h int 21h ; чтение с ожиданием и без эха, test al,al ; если введен обычный символ, jnz send_char ; послать его, int 21h ; иначе - считать расширенный ASCII-код, cmp al,2Dh ; если это не Alt-X, jne main_loop ; продолжить цикл, call shutdown_everything ; иначе - восстановить все в ; исходное состояние ret ; и завершить программу
send_char: ; посылка символа в модем ; Реальная терминальная программа должна здесь только добавлять символ в буфер ; передачи и, если этот буфер был пуст, разрешать прерывания "регистр передачи ; пуст". Просто пошлем символ напрямую в порт mov dx,COM ; регистр THR out dx,al jmp short main_loop
old_irq dd ? ; здесь будет храниться адрес старого обработчика
; упрощенный обработчик прерывания от последовательного порта irq_handler proc far pusha ; сохранить регистры mov dx,COM+2 ; прочитать регистр идентификации in al,dx ; прерывания repeat_handler: and ax,00000110b ; обнулить все биты, кроме 1 и 2, mov di,ax ; отвечающие за 4 основные ситуации call word ptr cs:handlers[di] ; косвенный вызов процедуры ; для обработки ситуации mov dx,COM+2 ; еще раз прочитать регистр идентификации in al,dx ; прерывания, test al,1 ; если младший бит не 1, jz repeat_handler ; надо обработать еще одно прерывание, mov al,20h ; иначе - завершить аппаратное прерывание out 20h,al ; посылкой команды EOI (см. главу 5.10.10) рора iret ; таблица адресов процедур, обслуживающих разные варианты прерывания handlers dw offset line_h, offset trans_h dw offset recv_h, offset modem_h
; эта процедура вызывается при изменении состояния линии line_h proc near mov dx,COM+5 ; пока не будет прочитан LSR, in al,dx ; прерывание не считается завершившимся ; здесь можно проверить, что случилось, и, например, прервать связь, если ; обнаружено состояние BREAK ret line_h endp ; эта процедура вызывается при приеме новых данных recv_h proc near mov dx,COM ; пока не будет прочитан RBR, in al,dx ; прерывание не считается завершившимся ; здесь следует поместить принятый байт в буфер приема для основной программы, ; но мы просто сразу выведем его на экран int 29h ; вывод на экран ret recv_h endp ; эта процедура вызывается по окончании передачи данных trans_h proc near ; здесь следует записать в THR следующий символ из буфера передачи и, если ; буфер после этого оказывается пустым, запретить этот тип прерывания ret trans_h endp ; эта процедура вызывается при изменении состояния модема modem_h proc near mov dx,COM+6 ; пока MCR не будет прочитан, in al,dx ; прерывание не считается завершившимся ; здесь можно определить состояние звонка и поднять трубку, определить ; потерю несущей и перезвонить, и т.д. ret modem_h endp irq_handler endp
; инициализация всего, что требуется инициализировать init_everything proc near ; установка нашего обработчика прерывания mov ax,3500h+IRQ ; АН = 35h, AL = номер прерывания int 21h ; получить адрес старого обработчика mov word ptr old_irq,bx ; и сохранить в old_irq mov word ptr old_irq+2,es mov ax,2500h+IRQ ; AH = 25h, AL = номер прерывания mov dx,offset irq_handler ; DS:DX - наш обработчик int 21h ; установить новый обработчик ; сбросить все регистры порта mov dx,COM+1 ; регистр IER mov al,0 out dx,al ; запретить все прерывания mov dx,COM+4 ; MCR out dx,al ; сбросить все линии модема в О mov dx,COM+5 ; и выполнить чтение из LSR, in al,dx mov dx,COM+0 ; из RBR in al,dx mov dx,COM+6 ; и из MSR in al,dx ; на тот случай, если они недавно ; изменялись, mov dx,COM+2 ; а также послать 0 в регистр FCR, mov al,0 ; чтобы выключить FIFO out dx,al
; установка скорости СОМ-порта mov dx,COM+3 ; записать в регистр LCR mov al,80h ; любое число со старшим битом 1 out dx,al mov dx,COM+0 ; теперь записать в регистр DLL mov al,2 ; младший байт делителя скорости, out dx,al mov dx,COM+1 ; а в DLH - mov al,0 ; старший байт out dx,al ; (мы записали 0002h - ; скорость порта 57 600) ; инициализация линии mov dx,COM+3 ; записать теперь в LCR mov al,0011b ; число, соответствующее режиму 8N1 out dx,al ; (наиболее часто используемому) ; инициализация модема mov dx,COM+4 ; записать в регистр MCR mov al,1011b ; битовую маску, активирующую DTR, RTS out dx,al ; и OUT2 ; здесь следует выполнить проверку на наличие модема на этом порту (читать ; регистр MSR, пока не будут установлены линии CTS и DSR или не кончится время), ; а затем послать в модем (то есть поместить в буфер передачи) инициализирующую ; строку, например "ATZ",0Dh
; разрешение прерываний mov dx,COM+1 ; записать в IER - битовую маску, mov al,1101b ; разрешающую все прерывания, кроме ; "регистр передачи пуст" out dx,al in al,21h ; прочитать OCW1 (см. главу 5.10.10) and al,E_BITMASK ; размаскировать прерывание out 21h,al ; записать OCW1 ret init_everything endp
; возвращение всего в исходное состояние shutdown_everything proc near ; запрещение прерываний in al,21h ; прочитать OCW1 or al,D_BITMASK ; замаскировать прерывание out 21h,al ; записать OCW1 mov dx,COM+1 ; записать в регистр IER mov al,0 ; ноль out dx,al ; сброс линий модема DTR и CTS mov dx,COM+4 ; записать в регистр MCR mov al,0 ; ноль out dx,al ; восстановление предыдущего ; обработчика прерывания mov ax,2500h+IRQ ; АН = 25h, AL = номер прерывания lds dx,old_irq ; DS:DX - адрес обработчика int 21h ret shutdown_everything endp end start
Повторная входимость
Пусть у нас есть собственный обработчик программного прерывания, который вызывают обработчики двух аппаратных прерываний, и пусть эти аппаратные прерывания произошли сразу одно за другим. В этом случае может получиться так, что второе аппаратное прерывание осуществится в тот момент, когда еще не закончится выполнение нашего программного обработчика. В большинстве случаев это не приведет ни к каким проблемам, но, если обработчик обращается к каким-либо переменным в памяти, могут произойти редкие, невоспроизводимые сбои в его работе. Например, пусть в обработчике есть некоторая переменная counter, используемая как счетчик, считающий от 0 до 99:
mov al,byte ptr counter ; считать счетчик в AL, cmp al,100 ; проверить его на переполнение, jb counter_ok ; если счетчик достиг 100, ; >>> здесь произошло второе прерывание <<< sub al,100 ; вычесть 100 mov byte ptr counter,al ; и сохранить счетчик counter_ok:
Если значение счетчика было, например, 102, а второе прерывание произошло после проверки, но до вычитания 100, второй вызов обработчика получит то же значение 102 и уменьшит его на 100. Затем управление вернется, и следующая команда sub al,100 еще раз уменьшит AL на 100 и запишет полученное число на место. Если затем по значению счетчика вычисляется что-нибудь вроде адреса в памяти для записи, вполне возможно, что произойдет ошибка. О таком обработчике прерывания говорят, что он не является повторно входимым.
Чтобы защитить подобные критические участки кода, следует временно запретить прерывания, например так:
cli ; запретить прерывания mov al,byte ptr counter cmp al,100 jb counter_ok sub al,100 mov byte ptr counter,al counter_ok: sti ; разрешить прерывания
Следует помнить, что, пока прерывания запрещены, система не отслеживает изменения часов, не получает данных с клавиатуры, так что прерывания надо обязательно, при первой возможности, разрешать. Всегда лучше пересмотреть используемый алгоритм и, например, хранить локальные переменные в стеке или использовать специально разработанную команду CMPXCHG, которая позволяет одновременно провести сравнение и запись в глобальную переменную.
К сожалению, в MS- DOS самый важный обработчик прерываний в системе — обработчик INT 21h — не является повторно входимым. В отличие от прерываний BIOS, обработчики которых используют стек прерванной программы, обработчик системных функций DOS записывает в SS:SP адрес дна одного из трех внутренних стеков DOS. Если функция была прервана аппаратным прерыванием, обработчик которого вызвал другую функцию DOS, она будет пользоваться тем же стеком, затирая все, что туда поместила прерванная функция. Когда управление вернется в прерванную функцию, в стеке окажется мусор и произойдет ошибка. Лучший выход — вообще не использовать прерывания DOS из обработчиков аппаратных прерываний, но если это действительно нужно, то принять необходимые меры предосторожности. Если прерывание произошло в тот момент, когда не выполнялось никаких системных функций DOS, ими можно безбоязненно пользоваться. Чтобы определить, занята DOS или нет, надо сначала, до установки собственных обработчиков, определить адрес флага занятости DOS.
Функция DOS 34h: Определить адрес флага занятости DOS
Ввод: |
АН = 34h |
Вывод: |
ES:BX = адрес однобайтного флага занятости DOS ES:BX – 1 = адрес однобайтного флага критической ошибки DOS |
Теперь обработчик прерывания может проверять состояние этих флагов и, если оба флага равны нулю, разрешается свободно пользоваться функциями DOS.
Если флаг критической ошибки не ноль, никакими функциями DOS пользоваться нельзя. Если флаг занятости DOS не ноль, можно пользоваться только функциями 01h – 0Ch, а чтобы воспользоваться какой-нибудь другой функцией, придется отложить действия до тех пор, пока DOS не освободится. Чтобы это сделать, надо сохранить номер функции и параметры в каких-нибудь переменных в памяти и установить обработчик прерывания 8h или 1Ch. Этот обработчик будет при каждом вызове проверять флаги занятости и, если DOS освободилась, вызовет функцию с номером и параметрами, оставленными в переменных в памяти. Кроме того, участок программы после проверки флага занятости — критический, и прерывания должны быть запрещены. Это непросто, но продолжим. Не все функции DOS возвращаются быстро — функция чтения символа с клавиатуры может оставаться в таком состоянии минуты, часы или даже дни, пока пользователь не вернется и не нажмет на какую-нибудь клавишу, и все это время флаг занятости DOS будет установлен в 1. В DOS предусмотрена и такая ситуация. Все функции ввода символов с ожиданием вызывают INT 28h в том же цикле, в котором они опрашивают клавиатуру, так что, если установить обработчик прерывания 28h, из него можно вызывать все функции DOS, кроме 01h – 0Ch.
Пример вызова DOS из обработчика прерывания от внешнего устройства рассмотрен чуть ниже, в резидентных программах. А сейчас следует заметить, что функции BIOS, одну из которых мы вызывали в нашем примере timer.asm, также часто оказываются не повторно входимыми. В частности, этим отличаются обработчики программных прерываний 5, 8, 9, 0Bh, 0Ch, 0Dh, 0Eh, 10h, 13h, 14h, 16h, 17h. Так как BIOS не предоставляет какого-либо флага занятости, придется создать его самим:
int10_handler proc far inc cs:byte ptr int10_busy ; увеличить флаг занятости pushf ; передать управление старому ; обработчику INT 10h, call cs:dword ptr old_int10 ; эмулируя команду INT, dec cs:byte ptr int10_busy ; уменьшить флаг занятости iret int10_busy db 0 int10_handler endp
Теперь обработчики аппаратных прерываний могут пользоваться командой INT 10h, если флаг занятости int10_busy равен нулю, и это не приведет к ошибкам, если не найдется чужой обработчик прерывания, который тоже будет обращаться к INT 10h и не будет ничего знать о нашем флаге занятости.
Прерывания от внешних устройств
Прерывания от внешних устройств, или аппаратные прерывания — это то, что понимается под термином «прерывание». Внешние устройства (клавиатура, дисковод, таймер, звуковая карта и т.д.) подают сигнал, по которому процессор прерывает выполнение программы и передает управление на обработчик прерывания. Всего на персональных компьютерах используется 15 аппаратных прерываний, хотя теоретически возможности архитектуры позволяют довести их число до 64.
Рассмотрим их кратко в порядке убывания приоритетов (прерывание имеет более высокий приоритет, и это означает, что, пока не завершился его обработчик, прерывания с низкими приоритетами будут ждать своей очереди).
IRQ0 (INT 8) — прерывание системного таймера. Это прерывание вызывается 18,2 раза в секунду. Стандартный обработчик этого прерывания вызывает INT 1Ch при каждом вызове, так что, если программе необходимо только регулярно получать управление, а не перепрограммировать таймер, рекомендуется использовать прерывание 1Ch.
IRQ1 (INT 9) — прерывание клавиатуры. Это прерывание вызывается при каждом нажатии и отпускании клавиши на клавиатуре. Стандартный обработчик этого прерывания выполняет довольно много функций, начиная с перезагрузки по Ctrl-Alt-Del и заканчивая помещением кода клавиши в буфер клавиатуры BIOS.
IRQ2 — к этому входу на первом контроллере прерываний подключены аппаратные прерывания IRQ8 – IRQ15, но многие BIOS перенаправляют IRQ9 на INT 0Ah.
IRQ8 (INT 70h) — прерывание часов реального времени. Это прерывание вызывается часами реального времени при срабатывании будильника и если они установлены на генерацию периодического прерывания (в последнем случае IRQ8 вызывается 1024 раза в секунду).
IRQ9 (INT 0Ah или INT 71h) — прерывание обратного хода луча. Вызывается некоторыми видеоадаптерами при обратном ходе луча. Часто используется дополнительными устройствами (например, звуковыми картами, SCSI-адаптерами и т.д.).
IRQ10 (INT 72h) — используется дополнительными устройствами.
IRQ11 (INT 73h) — используется дополнительными устройствами.
IRQ12 (INT 74h) — мышь на системах PS используется дополнительными устройствами.
IRQ13 (INT 02h или INT 75h) — ошибка математического сопроцессора. По умолчанию это прерывание отключено как на FPU, так и на контроллере прерываний.
IRQ14 (INT 76h) — прерывание первого IDE-контроллера «операция завершена».
IRQ15 (INT 77h) — прерывание второго IDE-контроллера «операция завершена».
IRQ3 (INT 0Bh) — прерывание последовательного порта COM2 вызывается, если порт COM2 получил данные.
IRQ4 (INT 0Ch) — прерывание последовательного порта СОМ1 вызывается, если порт СОМ1 получил данные.
IRQ5 (INT 0Dh) — прерывание LPT2 используется дополнительными устройствами.
IRQ6 (INT 0Eh) — прерывание дисковода «операция завершена».
IRQ7 (INT 0Fh) — прерывание LPT1 используется дополнительными устройствами.
Самые полезные для программ аппаратные прерывания — прерывания системного таймера и клавиатуры. Так как их стандартные обработчики выполняют множество функций, от которых зависит работа системы, их нельзя заменять полностью, как мы делали это с обработчиком INT 5. Следует обязательно вызвать предыдущий обработчик, передав ему управление следующим образом (если его адрес сохранен в переменной old_handler, как в предыдущих примерах):
pushf call old_handler
Эти две команды выполняют действие, аналогичное команде INT (сохранить флаги в стеке и передать управление подобно команде call), так что, когда обработчик завершится командой IRET, управление вернется в нашу программу. Так удобно вызывать предыдущий обработчик в начале собственного. Другой способ — простая команда jmp:
jmp cs:old_handler
приводит к тому, что, когда старый обработчик выполнит команду IRET, управление сразу же перейдет к прерванной программе. Этот способ применяют, если нужно, чтобы сначала отработал новый обработчик, а потом он передал бы управление старому.
Посмотрим, как работает перехват прерывания от таймера на следующем примере:
; timer.asm ; демонстрация перехвата прерывания системного таймера: вывод текущего времени ; в левом углу экрана .model tiny .code .186 ; для pusha/popa и сдвигов org 100h start proc near ; сохранить адрес предыдущего обработчика прерывания 1Ch mov ax,351Ch ; АН = 35h, AL = номер прерывания int 21h ; функция DOS: определить адрес обработчика mov word ptr old_int1Ch,bx ; прерывания mov word ptr old_int1Ch+2,es ; (возвращается в ES:BX) ; установить наш обработчик mov ax,251Ch ; АН = 25h, AL = номер прерывания mov dx,offset int1Ch_handler ; DS:DX - адрес обработчика int 21h ; установить обработчик прерывания 1Ch
; здесь размещается собственно программа, например вызов command.com mov ah,1 int 21h ; ожидание нажатия на любую клавишу ; конец программы
; восстановить предыдущий обработчик прерывания 1Ch mov ax,251Ch ; АН = 25h, AL = номер прерывания mov dx,word ptr old_int1Ch+2 mov ds,dx mov dx,word ptr cs:old_int1Ch ; DS:DX - адрес обработчика int 21h
ret
old_int1Ch dd ? ; здесь хранится адрес предыдущего обработчика start_position dw 0 ; позиция на экране, в которую выводится текущее время start endp
; обработчик для прерывания 1Ch ; выводит текущее время в позицию start_position на экране ; (только в текстовом режиме) int1Ch_handler proc far pusha ; обработчик аппаратного прерывания push es ; должен сохранять ВСЕ регистры push ds push cs ; на входе в обработчик известно только pop ds ; значение регистра CS mov ah,02h ; Функция 02h прерывания 1Ah: int 1Ah ; чтение времени из RTC, jc exit_handler ; если часы заняты - в другой раз
; AL = час в BCD-формате call bcd2asc ; преобразовать в ASCII, mov byte ptr output_line[2],ah ; поместить их в mov byte ptr output_line[4],al ; строку output_line
mov al,cl ; CL = минута в BCD-формате call bcd2asc mov byte ptr output_line[10],ah mov byte ptr output_line[12],al
mov al,dh ; DH = секунда в BCD-формате call bcd2asc mov byte ptr output_line[16],ah mov byte ptr output_line[18],al
mov cx,output_line_l ; число байт в строке - в СХ push 0B800h pop es ; адрес в видеопамяти mov di,word ptr start_position ; в ES:DI mov si,offset output_line ; адрес строки в DS:SI cld rep movsb ; скопировать строку exit_handler: pop ds ; восстановить все регистры pop es popa jmp cs:old_int1Ch ; передать управление предыдущему обработчику
; процедура bcd2asc ; преобразует старшую цифру упакованного BCD-числа из AL в ASCII-символ, ; который будет помещен в АН, а младшую цифру - в ASCII-символ в AL bcd2asc proc near mov ah,al and al,0Fh ; оставить младшие 4 бита в AL shr ah,4 ; сдвинуть старшие 4 бита в АН or ах,3030h ; преобразовать в ASCII-символы ret bcd2asc endp
; строка " 00h 00:00 " с атрибутом 1Fh (белый на синем) после каждого символа output_line db ' ',1Fh,'0',1Fh,'0',1Fh,'h',1Fh db ' ',1Fh,'0',1Fh,'0',1Fh,':',1Fh db '0',1Fh,'0',1Fh,' ',1Fh output_line_l equ $ - output_line
int1Ch_handler endp
end start
Если в этом примере вместо ожидания нажатия на клавишу поместить какую-нибудь программу, работающую в текстовом режиме, например tinyshell из главы 1.3, она выполнится как обычно, но в правом верхнем углу будет постоянно показываться текущее время, то есть такая программа будет осуществлять два действия одновременно. Именно для этого и применяется механизм аппаратных прерываний — они позволяют процессору выполнять одну программу, в то время как отдельные программы следят за временем, считывают символы из клавиатуры и помещают их в буфер, получают и передают данные через последовательные и параллельные порты и даже обеспечивают многозадачность, переключая процессор между разными задачами по прерыванию системного таймера.
Разумеется, обработка прерываний не должна занимать много времени: если прерывание происходит достаточно часто (например, прерывание последовательного порта может происходить 28 800 раз в секунду), его обработчик обязательно должен выполняться за более короткое время. Если, например, обработчик прерывания таймера будет выполняться 1/32,4 секунды, то есть половину времени между прерываниями, вся система будет работать в два раза медленнее. А если еще одна программа с таким же долгим обработчиком перехватит это прерывание, система остановится совсем. Именно поэтому обработчики прерываний принято писать исключительно на ассемблере.
Процедуры и функции
Принято разделять языки программирования на процедурные (С, Pascal, Fortran, BASIC) и непроцедурные (LISP, FORTH, PROLOG), где процедуры — блоки кода программ, имеющие одну точку входа и одну точку выхода и возвращающие управление на следующую команду после команды передачи управления процедуре. Ассемблер одинаково легко можно использовать как процедурный язык и как непроцедурный, и в большинстве примеров программ до сих пор мы успешно нарушали рамки и того, и другого подхода. В этой главе рассмотрена реализация процедурного подхода как наиболее популярная.
Программирование на уровне портов ввода-вывода
Как видно из предыдущей главы, использование системных функций DOS и прерываний BIOS может быть небезопасным из-за отсутствия в них повторной входимости. Теперь самое время спуститься на следующий уровень и научиться работе с устройствами компьютера напрямую, через порты ввода-вывода, как это и делают системные функции. Кроме того, многие возможности компьютера могут быть реализованы только программированием на уровне портов.
Регистры графического контроллера (03CEh– 03CFH)
Для обращения к регистрам графического контроллера следует записать индекс нужного регистра в порт 03CEh, после чего можно будет читать и писать данные для выбранного регистра в порт 03CFh. Если требуется только запись в регистры, можно просто поместить индекс в AL, посылаемый байт — в АН и выполнить команду вывода слова в порт 03CEh. Этот контроллер, в первую очередь, предназначен для обеспечения передачи данных между процессором и видеопамятью в режимах, использующих цветовые плоскости, как, например, режим 12h (640x480x16).
00h: Регистр установки/сброса
биты 3 – 0: записывать FFh в цветовую плоскость 3 – 0 соответственно
01h: Регистр разрешения установки/сброса
биты 3 – 0: включить режим установки/сброса для цветовой плоскости 3 – 0
В этом режиме данные для одних цветовых слоев получают от CPU, а для других — из регистра установки/сброса. Режим действует только в нулевом режиме работы (см. регистр 05h).
02h: Регистр сравнения цвета
биты 3 – 0: искомые биты для цветовых плоскостей 3 – 0
Используется для поиска пикселя заданного цвета, чтобы не обращаться по очереди во все цветовые слои.
03h: Регистр циклического сдвига данных
биты 4 – 3: выбор логической операции:
00 — данные от CPU записываются без изменений
01 — операция AND над CPU и регистром-защелкой
10 — операция OR над CPU и регистром-защелкой
11 — операция XOR над CPU и регистром-защелкой
биты 2 – 0: на сколько бит выполнять вправо циклический сдвиг данных перед записью в видеопамять
04h: Регистр выбора читаемой плоскости
биты 1 – 0: номер плоскости (0 – 3)
Запись сюда изменяет номер цветовой плоскости, данные из которой получает CPU при чтении из видеопамяти.
05h: Регистр выбора режима работы
бит 6: 1/0 — 256/16 цветов
бит 4: четные адреса соответствуют плоскостям 0, 2, нечетные — 1,3
бит 3: 1 — режим сравнения цветов
биты 1 – 0: режим:
00: данные из CPU (бит на пиксель) + установка/сброс + циклический сдвиг + логические функции
01: данные в/из регистра-защелки (прочитать в него и записать в другую область памяти быстрее, чем через CPU)
10: данные из CPU, байт на пиксель, младшие 4 бита записываются в соответствующие плоскости
11: то же самое + режим битовой маски
06h: Многоцелевой регистр графического контроллера
биты 3 – 2: видеопамять:
00: A0000h – BFFFFh (128 Кб)
01: A0000h – AFFFFh (64 Кб)
10: B0000h – B7FFFh (32 Кб)
11: B8000h – BFFFFh (32 Кб)
бит 0: 1/0 — графический/текстовый режим
07h: Регистр игнорирования цветовых плоскостей
биты 3 – 0: игнорировать цветовую плоскость 3 – 0
08h: Регистр битовой маски
Если бит этого регистра 0 — соответствующий бит будет браться из регистра-защелки, а не от CPU. (Чтобы занести данные в регистр-защелку, надо выполнить одну операцию чтения из видеопамяти, при этом в каждый из четырех регистров-защелок будет помещено по одному байту из соответствующей цветовой плоскости.)
Графический контроллер предоставляет весьма богатые возможности по управлению режимами, использующими цветовые плоскости. В качестве примера напишем процедуру, выводящую точку на экран в режиме 12h (640x480x16) с использованием механизма установки/сброса:
; процедура putpixel12h ; выводит на экран точку с заданным цветом в режиме 12h (640x480x16) ; Ввод: DX = строка ; СХ = столбец ; ВР = цвет ; ES = 0A000h putpixel12h proc near pusha ; вычислить номер байта в видеопамяти xor bx,bx mov ax,dx ; AX = строка lea еах,[еах+еах*4] ; АХ = АХ * 5 shl ах,4 ; АХ = АХ * 16 ; АХ = строка * байт_в_строке ; (строка * 80) push cx shr cx,3 ; CX = номер байта в строке add ax,cx ; АХ = номер байта в видеопамяти mov di,ax ; сохранить его в DI ; вычислить номер бита в байте pop сх and cx,07h ; остаток от деления на 8 - номер ; бита в байте, считая справа налево mov bx,0080h shr bx,cl ; в BL теперь нужный бит установлен в 1 ; программирование портов mov dx,03CEh ; индексный порт ; графического контроллера mov ax,0F01h ; регистр 01h: разрешение ; установки/сброса out dx,ax ; разрешить установку/сброс для ; всех плоскостей (эту часть лучше ; сделать однажды в программе, например сразу после установки ; видеорежима, и не повторять каждый раз при вызове процедуры) mov ax,bp shl ax,8 ; регистр 00h: регистр ; установки/сброса out dx,ax ; АН = цвет mov al,08 ; порт 08h: битовая маска mov ah,bl ; записать в битовую маску нули ; всюду, кроме out dx,ax ; бита, соответствующего выводимому пикселю mov ah,byte ptr es:[di] ; заполнить ; регистры-защелки mov byte ptr es:[di],ah ; вывод на экран: ; выводится единственный бит ; в соответствии с содержимым регистра битовой маски, остальные ; биты берутся из защелки, то есть не изменяются. Цвет выводимого ; бита полностью определяется значением регистра установки/сброса рора ret putpixel12h endp
Регистры контроллера атрибутов (03C0h– 03C1h)
Контроллер атрибутов преобразовывает значения байта атрибута символа в цвета символа и фона. Для записи в эти регистры надо записать в порт 03C0h номер регистра, а затем (второй командой out) — данные для этого регистра. Чтобы убедиться, что 03C0h находится в состоянии приема номера, а не данных, надо выполнить чтение из ISR1 (порт 03DAh). Порт 03C1h можно использовать для чтения последнего записанного индекса или данных.
00h – 0Fh: Регистры палитры EGA
биты 5 – 0: номер регистра в текущей странице VGA DAC, соответствующего данному EGA-цвету.
10h: Регистр управления режимом
бит 7: разбиение регистров VGA DAC для 16-цветных режимов: 1 = 16 страниц по 16 регистров, 0 = 4 страницы по 64 регистра
бит 6: 1 = 8-битный цвет, 0 = 4-битный цвет
бит 5: горизонтальное панорамирование разрешено
бит 3: 1/0 — бит 7 атрибута управляет миганием символа/цветом фона
бит 2: девятый пиксель в каждой строке повторяет восьмой
бит 1: 1/0 — генерация атрибутов для монохромных/цветных режимов
бит 0: 1/0 — генерация атрибутов для текстовых/графических режимов
11h: Регистр цвета бордюра экрана (по умолчанию 00h)
биты 7 – 0: номер регистра VGA DAC
12h: Регистр разрешения использования цветовых плоскостей
бит 3: разрешить плоскость 3
бит 2: разрешить плоскость 2
бит 1: разрешить плоскость 1
бит 0: разрешить плоскость 0
13h: Регистр горизонтального панорамирования
биты 3 – 0: величина сдвига по горизонтали в пикселях (деленная на 2 для режима 13h)
14h: Регистр выбора цвета (по умолчанию 00h)
Функции INT 10h AX = 1000h – 1009h позволяют использовать большинство из этих регистров, но кое-что, например панорамирование, оказывается возможным только при программировании на уровне портов.
Регистры контроллера CRT (03D4h– 03D5H)
Контроллер CRT управляет разверткой и формированием кадров на дисплее. Как и для графического контроллера, для обращения к регистрам контроллера CRT следует записать индекс нужного регистра в порт 03D4h, после чего можно будет читать и писать данные для выбранного регистра в порт 03D5h. Если требуется только запись в регистры, можно просто поместить индекс в AL, посылаемый байт — в АН и выполнить команду вывода слова в порт 03D4h.
00h: Общая длина горизонтальной развертки
01h: Длина отображаемой части горизонтальной развертки минус один
02h: Начало гашения луча горизонтальной развертки
03h: Конец гашения луча горизонтальной развертки
биты 6 – 5: горизонтальное смещение в текстовых режимах
биты 4 – 0: конец импульса
04h: Начало горизонтального обратного хода луча
05h: Конец горизонтального обратного хода луча
биты 7, 4 – 0: конец импульса
биты 6 – 5: горизонтальное смещение импульса
06h: Число вертикальных линий растра без двух старших бит
07h: Дополнительный регистр
бит 7: бит 9 регистра 10h
бит 6: бит 9 регистра 12h
бит 5: бит 9 регистра 06h
бит 4: бит 8 регистра 18h
бит 3: бит 8 регистра 15h
бит 2: бит 8 регистра 10h
бит 1: бит 8 регистра 12h
бит 0: бит 8 регистра 06h
08h: Предварительная горизонтальная развертка
биты 6 – 5: биты 5 и 4 регистра горизонтального панорамирования
биты 4 – 0: номер линии в верхней строке, с которой начинается изображение
09h: Высота символов
бит 7: двойное сканирование (400 линий вместо 200)
бит 6: бит 9 регистра 18h
бит 5: бит 9 регистра 15h
биты 4 – 0: высота символов минус один (от 0 до 31)
0Ah: Начальная линия курсора (бит 5: гашение курсора)
0Bh: Конечная линия курсора (биты 6 – 5: отклонение курсора вправо)
0Ch: Старший байт начального адреса
0Dh: Младший байт начального адреса (это адрес в видеопамяти, начиная с которого выводится изображение)
0Eh: Старший байт позиции курсора
0Fh: Младший байт позиции курсора
10h: Начало вертикального обратного хода луча без старшего бита
11h: Конец вертикального обратного хода луча без старшего бита
бит 7: защита от записи в регистры 00 – 07 (кроме бита 4 в 07h)
бит 6: 1/0 — 5/3 цикла регенерации за время обратного хода луча
бит 5: 1/0 — выключить/включить прерывание по обратному ходу луча
бит 4: запись нуля сюда заканчивает обработку прерывания
биты 3 – 0: конец вертикального обратного хода луча
12h: Число горизонтальных линий минус один без двух старших бит
13h: Логическая ширина экрана (в словах/двойных словах на строку)
14h: Положение символа подчеркивания
бит 6: 1/0 — адресация словами/двойными словами
бит 5: увеличение счетчика адреса регенерации на 4
биты 4 – 0: положение подчеркивания
15h: Начало импульса гашения луча вертикальной развертки без двух старших бит
16h: Конец импульса гашения вертикальной развертки
17h: Регистр управления режимом
бит 7: горизонтальный и вертикальный ходы луча отключены
бит 6: 1/0 — адресация байтами/словами
бит 4: 1 — контроллер выключен
бит 3: 1/0 — счетчик адреса регенерации растет на 2/1 на каждый символ
бит 2: увеличение в 2 раза разрешения по вертикали
18h: Регистр сравнения линий без двух старших бит
(от начала экрана до линии с номером из этого регистра отображается начало видеопамяти, а от этой линии до конца — видеопамять, начиная с адреса, указанного в регистрах 0Ch и 0Dh)
22h: Регистр-защелка (только для чтения)
23h: Состояние контроллера атрибутов
биты 7 – 3: текущее значение индекса контроллера атрибутов
бит 2: источник адреса палитры
бит 0: состояние порта контроллера атрибутов — 0/1 = индекс/данные
BIOS заполняет регистры этого контроллера соответствующими значениями при переключении видеорежимов. Так как одного контроллера CRT мало для полного переключения в новый видеорежим, мы вернемся к этому чуть позже, а пока посмотрим, как внести небольшие изменения в действующий режим, например, как превратить текстовый режим 80x25 в 80x30:
; 80x30.asm ; переводит экран в текстовый режим 80x30 (размер символов 8x16) ; (Norton Commander 5.0 в отличие от, например, FAR восстанавливает режим по ; окончании программы, но его можно обмануть, если предварительно нажать ; Alt-F9) .model tiny .code .186 ; для команды outsw org 100h ; СОМ-программа start: mov ax,3 ; установить режим 03h (80x25), int 10h ; чтобы только внести небольшие изменения mov dx,3CCh ; порт 3CCh: регистр вывода (MOR) на чтение in al,dx mov dl,0C2h ; порт 03C2h: регистр вывода (MOR) на запись or al,0C0h ; установить полярности 1,1 - для 480 строк out dx,al mov dx,03D4h ; DX = порт 03D4h: индекс CRT mov si,offset crt480 ; DS:SI = адрес таблицы данных для CRT mov cx,crt480_l ; CX = ее размер rep outsw ; послать все устанавливаемые параметры ; в порты 03D4h и 03D5h
; нельзя забывать сообщать BIOS об изменениях в видеорежиме push 0040h pop es ; ES = 0040h mov byte ptr es:[84h],29 ; 0040h:0084h - число строк ret
; данные для контроллера CRT в формате индекс в младшем байте, данные в ; старшем - для записи при помощи команды outsw crt480 dw 0C11h ; регистр 11h всегда надо записывать первым, ; так как его бит 7 разрешает запись в другие dw 0B06h,3E07h,0EA10h,0DF12h,0E715h,0416h ; регистры crt480_l = ($-crt480)/2 end start
Еще одна интересная возможность, которую предоставляет контроллер CRT, — плавная прокрутка экрана при помощи регистра 08h:
; vscroll.asm ; Плавная прокрутка экрана по вертикали. Выход - клавиша Esc ; .model tiny .code .186 ; для push 0B400h org 100h ; СОМ-программа start: push 0B800h pop es xor si,si ; ES:SI - начало видеопамяти mov di,80*25*2 ; ES:DI - начало второй страницы видеопамяти mov cx,di rep movs es:any_label,es:any_label ; скопировать первую ; страницу во вторую mov dx,03D4h ; порт 03D4h: индекс CRT screen_loop: ; цикл по экранам mov cx,80*12*2 ; СХ = начальный адрес - адрес середины экрана line_loop: ; цикл по строкам mov al,0Ch ; регистр 0Ch - старший байт начального адреса mov ah,ch ; байт данных - СН out dx,ax ; вывод в порты 03D4, 03D5 inc ax ; регистр 0Dh - младший байт начального адреса mov ah,cl ; байт данных - CL out dx,ax ; вывод в порты 03D4, 03D5
mov bx,15 ; счетчик линий в строке sub cx,80 ; переместить начальный адрес на начало ; предыдущей строки (так как это движение вниз) pel_loop: ; цикл по линиям в строке call wait_retrace ; подождать обратного хода луча
mov al,8 ; регистр 08h - выбор номера линии в первой ; строке, с которой начинается вывод изображения mov ah,bl ; (номер линии из BL) out dx,ax
dec bx ; уменьшить число линий, jge pel_loop ; если больше или = нулю - строка еще не ; прокрутилась до конца и цикл по линиям ; продолжается in al,60h ; прочитать скан-код последнего символа, cmp al,81h ; если это 81h (отпускание клавиши Esc), jz done ; выйти из программы, cmp cx,0 ; если еще не прокрутился целый экран, jge line_loop ; продолжить цикл по строкам, jmp short screen_loop ; иначе: продолжить цикл по экранам
done: ; выход из программы mov ax,8 ; записать в регистр CRT 08h out dx,ax ; байт 00 (никакого сдвига по вертикали), add ax,4 ; а также 00 в регистр 0Ch out dx,ax inc ax ; и 0Dh (начальный адрес совпадает out dx,ax ; с началом видеопамяти) ret
wait_retrace proc near push dx mov dx,03DAh VRTL1: in al,dx ; порт 03DAh - регистр ISR1 test al,8 jnz VRTL1 ; подождать конца текущего обратного хода луча, VRTL2: in al,dx test al,8 jz VRTL2 ; а теперь начала следующего wait_retrace endp
any_label label byte ; метка для переопределения сегмента в movs end start
Горизонтальная прокрутка осуществляется аналогично, только с использованием регистра горизонтального панорамирования 13h из контроллера атрибутов.
Регистры синхронизатора (03C4h– 03C5h)
Для обращения к регистрам синхронизатора следует записать индекс нужного регистра в порт 03C4h, после чего можно будет читать и писать данные для выбранного регистра в порт 03C5h. Точно так же, если требуется только запись в регистры, можно просто поместить индекс в AL, посылаемый байт — в АН и выполнить команду вывода слова в порт 03CEh.
00h: Регистр сброса синхронизации
бит 1: запись нуля сюда вызывает синхронный сброс
бит 0: запись нуля сюда вызывает асинхронный сброс
01h: Регистр режима синхронизации
бит 5: 1 — обмен данными между видеопамятью и дисплеем выключен
бит 3: 1 — частота обновления для символов уменьшена в два раза
бит 0: 1/0 — ширина символа 8/9 точек
02h: Регистр маски записи
бит 3: разрешена запись CPU в цветовую плоскость 3
бит 2: разрешена запись CPU в цветовую плоскость 2
бит 1: разрешена запись CPU в цветовую плоскость 1
бит 0: разрешена запись CPU в цветовую плоскость 0
03h: Регистр выбора шрифта
бит 5: если бит 3 атрибута символа = 1, символ берется из шрифта 2
бит 4: если бит 3 атрибута символа = 0, символ берется из шрифта 2
биты 3 – 2: номер таблицы для шрифта 2
биты 1 – 0: номер таблицы для шрифта 1
(00, 01, 10, 11) = (0 Кб, 16 Кб, 32 Кб, 48 Кб от начала памяти шрифтов VGA)
04h: Регистр организации видеопамяти
бит 3: 1 — режим CHAIN-4 (используется только в видеорежиме 13h)
бит 2: 0 — четные адреса обращаются к плоскостям 0, 2, нечетные — к 1, 3
бит 1: объем видеопамяти больше 64 Кб
Хотя BIOS и позволяет использовать некоторые возможности этих регистров, в частности работу со шрифтами (INT 10h АН = 11h) и выключение обмена данными между видеопамятью и дисплеем (INT 10h, АН = 12h, BL = 32h), прямое программирование регистров синхронизатора вместе с регистрами контроллера CRT позволяет значительно изменять характеристики видеорежимов VGA, вплоть до установки нестандартных видеорежимов. Наиболее популярными режимами являются так называемые режимы «X» с 256 цветами и с разрешением 320 или 360 пикселей по горизонтали и 200, 240, 400 или 480 пикселей по вертикали. Так как такие режимы не поддерживаются BIOS, для их реализации нужно написать все необходимые процедуры — установку видеорежима, вывод пикселя, чтение пикселя, переключение страниц, изменение палитры, загрузку шрифтов. При этом для всех режимов из этой серии, кроме 320x240x256, приходится также учитывать измененное соотношение размеров экрана по вертикали и горизонтали, чтобы круг, выведенный на экран, не выглядел как эллипс, а квадрат — как прямоугольник.
Установка нового режима выполняется почти точно так же, как и в предыдущем примере, — путем модификации существующего. Кроме того, нам придется изменять частоту кадров (биты 3 – 2 регистра MOR), а это приведет к сбою синхронизации, если мы не выключим синхронизатор на время изменения частоты (записью в регистр 00h):
; процедура set_modex ; переводит видеоадаптер VGA в один из режимов X с 256 цветами ; ввод: DI = номер режима ; 0: 320x200, соотношение сторон 1,2:1 ; 1: 320x400, соотношение сторон 2,4:1 ; 2: 360x200, соотношение сторон 1,35:1 ; 3: 360x400, соотношение сторон 2,7:1 ; 4: 320x240, соотношение сторон 1:1 ; 5: 320x480, соотношение сторон 2:1 ; 6: 360x240, соотношение сторон 1,125:1 ; 7: 360x480, соотношение сторон 2,25:1 ; DS = CS ; Для вывода информации на экран в этих режимах ; см. процедуру putpixel_x setmode_x proc near mov ax,12h ; очистить все четыре цветовые int 10h ; плоскости видеопамяти, mov ax,13h ; установить режим 13h, который будем int 10h ; модифицировать cmp di,7 ; если нас вызвали с DI > 7, ja exit_modex ; выйти из процедуры ; (оставшись в режиме 13h), shl di,1 ; умножить на 2, так как x_modes - ; таблица слов, mov di,word ptr x_modes[di] ; прочитать ; адрес таблицы настроек для ; выбранного режима mov dx,03C4h ; порт 03C4h - индекс синхронизатора mov ax,0100h ; регистр 00h, значение 01 out dx,ax ; асинхронный сброс mov ax,0604h ; регистр 04h, значение 06h out dx,ax ; отключить режим CHAIN4 mov dl,0C2h ; порт 03C2h - регистр ; MOR на запись mov al,byte ptr [di] ; записать в него ; значение частоты кадров out dx,al ; и полярности развертки ; для выбранного режима mov dl,0D4h ; порт 03D4h - индекс ; контроллера CRT mov si,word ptr offset [di+2] ; адрес строки с настройками ; для выбранной ширины в DS:SI mov cx,8 ; длина строки настроек в СХ rep outsw ; вывод строки слов ; в порты 03D4/03D5 mov si,word ptr offset [di+4] ; настройки для ; выбранной высоты в DS:SI mov сх,7 ; длина строки настроек в СХ rep outsw mov si,word ptr offset [di+6] ; настройки ; для включения/выключения удвоения ; по вертикали (200/400 и 240/480 строк) mov сх,3 rep outsw mov ax, word ptr offset [di+8] ; число байт в строке mov word ptr x_width,ax ; сохранить ; в переменной x_width mov dl,0C4h ; порт 03C4h - индекс синхронизатора mov ах,0300h ; регистр 00h, значение 03 out dx,ax ; выйти из состояния сброса exit_modex: ret
; таблица адресов таблиц с настройками режимов x_modes dw offset mode_0,offset mode_1 dw offset mode_2,offset mode_3 dw offset mode_4,offset mode_5 dw offset mode_6,offset mode_7
; таблица настроек режимов: значение регистра MOR, адрес строки ; настроек ширины, адрес строки настроек высоты, адрес строки ; настроек удвоения по вертикали, число байт в строке mode_0 dw 63h,offset mode_320w,offset mode_200h,offset mode_double,320/4 mode_1 dw 63h,offset mode_320w,offset mode_400h,offset mode_single,320/4 mode_2 dw 67h,offset mode_360w,offset mode_200h,offset mode_double,360/4 mode_3 dw 67h,offset mode_360w,offset mode_400h,offset mode_single,360/4 mode_4 dw 0E3h,offset mode_320w,offset mode_240h,offset mode_double,320/4 mode_5 dw 0E3h,offset mode_320w,offset mode_480h,offset mode_single,320/4 mode_6 dw 0E7h,offset mode_360w,offset mode_240h,offset mode_double,360/4 mode_7 dw 0E7h,offset mode_360w,offset mode_480h,offset mode_single,360/4
; настройки CRT. В каждом слове младший байт - номер регистра, ; старший - значение, которое в этот регистр заносится mode_320w: ; настройка ширины 320 ; Первый регистр обязательно 11h, хотя он и не относится ; к ширине - он разрешает запись в остальные регистры, ; если она была запрещена (!) dw 0E11h,5F00h,4F01h,5002h,8203h,5404h,8005h,2813h mode_360w: ; настройка ширины 360 dw 0E11h,6B00h,5901h,5A02h,8E03h,5E04h,8A05h,2D13h mode_200h: mode_400h: ; настройка высоты 200/400 dw 0BF06h,1F07h,9C10h,0E11h,8F12h,9615h,0B916h mode_240h: mode_480h: ; настройка высоты 240/480 dw 0D06h,3E07h,0EA10h,0C11h,0DF12h,0E715h,0616h mode_single: ; настройка режимов без удвоения dw 4009h,0014h,0E317h mode_double: ; настройка режимов с удвоением dw 4109h,0014h,0E317h setmode_x endp
x_width dw ? ; число байт в строке ; эту переменную инициализирует setmode_x, а использует putpixel_x
; процедура putpixel_x ; выводит точку с заданным цветом в текущем режиме X ; Ввод: DX = строка ; СХ = столбец ; ВР = цвет ; ES = A000h ; DS = сегмент, в котором находится переменная x_width
putpixel_x proc near pusha mov ax, dx mul word ptr x_width ; AX = строка * число байт в строке mov di,cx ; DI = столбец shr di,2 ; DI = столбец/4 (номер байта в строке) add di,ax ; DI = номер байта в видеопамяти mov ax,0102h ; AL = 02h (номер регистра), ; АН = 01 (битовая маска) and cl,03h ; CL = остаток от деления ; столбца на 4 = номер ; цветовой плоскости shl ah,cl ; теперь в АН выставлен в 1 бит, ; соответствующий нужной ; цветовой плоскости mov dx,03C4h ; порт 03C4h - индекс ; синхронизатора out dx,ax ; разрешить запись только ; в нужную плоскость mov ax,bp ; цвет в AL stosb ; вывод байта в видеопамять рора ret putpixel_x endp
Регистры VGA DAC (03C6h– 03C9h)
Таблица цветов VGA на самом деле представляет собой 256 регистров, в каждом из которых записаны три 6-битных числа, соответствующих уровням красного, зеленого и синего цвета. Подфункции INT 10h AX =1010h – 101Bh позволяют удобно работать с этими регистрами, но, если требуется максимальная скорость, программировать их на уровне портов ввода-вывода не намного сложнее.
03C6h для чтения/записи: Регистр маскирования пикселей (по умолчанию FFh)
При обращении к регистру DAC выполняется операция AND над его номером и содержимым этого регистра.
03C7h для записи: Регистр индекса DAC для режима чтения
Запись байта сюда переводит DAC в режим чтения, так что следующее чтение из 03C9h вернет значение регистра палитры с этим индексом.
03C7h для чтения: Регистр состояния DAC
биты 1 – 0: 00/11 — DAC в режиме записи/чтения
03C8h для чтения/записи: Регистр индекса DAC для режима записи
Запись байта сюда переводит DAC в режим записи, так что следующие записи в 03C3h будут записывать новые значения в регистры палитры, начиная с этого индекса.
03C3h для чтения/записи: Регистр данных DAC
Чтение отсюда считывает значение регистра палитры с индексом, записанным предварительно в 03C8h, запись — записывает новое значение в регистр палитры с индексом, записанным в 03C8h. На каждый регистр требуются три операции чтения/записи, передающие три 6-битных значения уровня цвета: красный, зеленый, синий. После третьей операции чтения/записи индекс текущего регистра палитры увеличивается на 1, так что можно считывать/записывать сразу несколько регистров
Команды insb/outsb серьезно облегчают работу с регистрами DAC в тех случаях, когда требуется считывать или загружать значительные участки палитры или всю палитру целиком, — такие процедуры оказываются и быстрее, и меньше аналогичных, написанных с использованием прерывания INT 10h. Посмотрим, как это реализуется на примере программы плавного гашения экрана.
; fadeout.asm ; выполняет плавное гашение экрана
.model tiny .code .186 ; для команд insb/outsb org 100h ; СОМ-программа start: cld ; для команд строковой обработки mov di,offset palettes call read_palette ; сохранить текущую палитру, чтобы ; восстановить в самом конце программы, mov di,offset palettes+256*3 call read_palette ; а также записать еще одну копию ; текущей палитры, которую будем ; модифицировать mov cx,64 ; счетчик цикла изменения палитры main_loop: push cx call wait_retrace ; подождать начала обратного хода луча mov di,offset palettes+256*3 mov si,di call dec_palette ; уменьшить яркость всех цветов call wait_retrace ; подождать начала следующего mov si,offset palettes+256*3 ; обратного хода луча call write_palette ; записать новую палитру pop cx loop main_loop ; цикл выполняется 64 раза - достаточно для ; обнуления самого яркого цвета (максимальное ; значение 6-битной компоненты - 63) mov si,offset palettes call write_palette ; восстановить первоначальную палитру ret ; конец программы
; процедура read_palette ; помещает палитру VGA в строку по адресу ES:DI read_palette proc near mov dx,03C7h ; порт 03C7h - индекс DAC/режим чтения mov al,0 ; начинать с нулевого цвета out dx,al mov dl,0C9h ; порт 03C9h - данные DAC mov cx,256*3 ; прочитать 256 * 3 байта rep insb ; в строку по адресу ES:DI ret read_palette endp
; процедура write_palette ; загружает в DAC VGA палитру из строки по адресу DS:SI write_palette proc near mov dx,03C8h ; порт 03C8h - индекс DAC/режим записи mov al,0 ; начинать с нулевого цвета out dx,al mov dl,0C9h ; порт 03C9h - данные DAC mov cx,256*3 ; записать 256 * 3 байта rep outsb ; из строки в DS:SI ret write_palette endp
; процедура dec_palette ; уменьшает значение каждого байта на 1 с насыщением (то есть, после того как ; байт становится равен нулю, он больше не уменьшается из строки в DS:SI ; и записывает результат в строку в DS:SI dec_palette proc near mov cx,256*3 ; длина строки 256 * 3 байта dec_loop: lodsb ; прочитать байт, test al,al ; если он ноль, jz already_zero ; пропустить следующую команду dec ax ; уменьшить байт на 1 already_zero: stosb ; записать его обратно loop dec_loop ; повторить 256 * 3 раза ret dec_palette endp
; процедура wait_retrace ; ожидание начала следующего обратного хода луча wait_retrace proc near push dx mov dx,03DAh VRTL1: in al,dx ; порт 03DAh - регистр ISR1 test al,8 jnz VRTL1 ; подождать конца текущего обратного хода луча, VRTL2: in al,dx test al,8 jz VRTL2 ; а теперь начала следующего pop dx ret wait_retrace endp
palettes: ; за концом программы мы храним две копии ; палитры - всего 1,5 Кб end start
Резидентные программы
Программы, остающиеся в памяти, после того как управление возвращается в DOS, называются резидентными. Превратить программу в резидентную просто — достаточно вызвать специальную системную функцию DOS.
Функция DOS 31h: Оставить программу резидентной
Ввод: |
АН = 31h AL = код возврата DX = размер резидента в 16-байтных параграфах (больше 06h), считая от начала PSP |
Кроме того, существует и иногда используется предыдущая версия этой функции — прерывание 27h:
INT 27h: Оставить программу резидентной
Ввод: |
АН = 27h DX = адрес последнего байта программы (считая от начала PSP) + 1 |
Эта функция не может оставлять резидентными программы размером больше 64 Кб, но многие программы, написанные на ассемблере, соответствуют этому условию. Так как резидентные программы уменьшают объем основной памяти, их всегда пишут на ассемблере и оптимизируют для достижения минимального размера.
Никогда не известно, по каким адресам в памяти оказываются загруженные в разное время резидентные программы, поэтому единственным несложным способом получения управления является механизм программных и аппаратных прерываний. Принято разделять резидентные программы на активные и пассивные, в зависимости от того, перехватывают ли они прерывания от внешних устройств или получают управление, только если программа специально вызовет команду INT с нужным номером прерывания и параметрами.
Символьные устройства
Драйвер символьного устройства должен содержать в поле атрибутов драйвера (смещение 04 в заголовке) единицу в самом старшем бите. Тогда остальные биты трактуются следующим образом:
бит 15: 1
бит 14: драйвер поддерживает функции чтения/записи IOCTL
бит 13: драйвер поддерживает, функцию вывода до занятости
бит 12: 0
бит 11: драйвер поддерживает функции открыть/закрыть устройство
биты 10 – 8: 000
бит 7: драйвер поддерживает функцию запроса поддержки IOCTL
бит 6: драйвер поддерживает обобщенный IOCTL
бит 5: 0
бит 4: драйвер поддерживает быстрый вывод (через INT 29h)
бит 3: драйвер устройства «часы»
бит 2: драйвер устройства NUL
бит 1: драйвер устройства STDOUT
бит 0: драйвер устройства STDIN
IOCTL — это большой набор функций (свыше пятидесяти), доступных как различные подфункции INT 21h АН = 44h и предназначенных для прямого взаимодействия с драйверами. Но о IOCTL — чуть позже, а сейчас познакомимся с тем, как устроен, возможно, самый простой из реально полезных драйверов.
В качестве первого примера рассмотрим драйвер, который вообще не обслуживает никакое устройство, реальное или виртуальное, а просто увеличивает размер буфера клавиатуры BIOS до 256 (или больше) символов. Этого можно было бы добиться обычной резидентной программой,но BIOS хранит в своей области данных только ближние адреса для этого буфера, то есть смещения относительно сегментного адреса 0040h. Так как драйверы загружаются в память первыми, еще до командного интерпретатора, они обычно попадают в область линейных адресов 00400h – 10400h, в то время как с резидентными программами это может не получиться.
Наш драйвер будет обрабатывать только одну команду, команду инициализации драйвера 00h. Для нее буфер запроса выглядит следующим образом:
+00h: байт — 19h (длина буфера запроса)
+01h: байт — не используется
+02h: байт — 00h (код команды)
+03h: байт — слово состояния драйвера (заполняется драйвером)
+05h: 8 байт — не используется
+0Dh: байт — число обслуживаемых устройств (заполняется блочным драйвером)
+0Eh: 4 байта
на входе — конец доступной для драйвера памяти;
на выходе — адрес первого байта из той части драйвера, которая не будет резидентной (чтобы выйти без инсталляции — здесь надо записать адрес первого байта)
+12h: 4 байта
на входе — адрес строки в CONFIG.SYS, загрузившей драйвер;
на выходе — адрес массива ВРВ (для блочных драйверов)
+16h: байт — номер первого диска
+17h: 2 байта — сообщение об ошибке (0000h, если ошибки не было) — заполняется драйвером
Процедура инициализации может пользоваться функциями DOS 01h – 0Ch, 25h, 30h и 35h.
; kbdext.asm ; драйвер символьного устройства, увеличивающий буфер клавиатуры до BUF_SIZE ; (256 по умолчанию) символов ; BUF_SIZE equ 256 ; новый размер буфера
.model tiny .186 ; для сдвигов и push 0040h .code org 0 ; драйвер начинается с CS:0000 start: ; заголовок драйвера dd -1 ; адрес следующего драйвера - ; FFFFh:FFFFh для последнего dw 8000h ; атрибуты: символьное устройство, ; ничего не поддерживает dw offset strategy ; адрес процедуры стратегии dw offset interrupt ; адрес процедуры прерывания db "$$KBDEXT" ; имя устройства (не должно совпадать ; с каким-нибудь именем файла) request dd ? ; здесь процедура стратегии сохраняет адрес ; буфера запроса buffer db BUF_SIZE*2 dup (?) ; а это - наш новый буфер ; клавиатуры размером BUF_SIZE символов ; (два байта на символ) ; процедура стратегии ; на входе ES:BX = адрес буфера запроса strategy рroc far mov cs:word ptr request,bx ; сохранить этот адрес для mov cs:word ptr request+2,es ; процедуры прерывания ret strategy endp
; процедура прерывания interrupt proc far push ds ; сохранить регистры push bx push ax lds bx,dword ptr cs:request ; DS:BX - адрес запроса mov ah,byte ptr [bx+2] ; прочитать номер команды, or ah,ah ; если команда 00h (инициализация), jnz exit call init ; обслужить ее, ; иначе: exit: mov ax,100h ; установить бит 8 (команда обслужена) mov word ptr [bx+3],ax ; в слове состояния драйвера pop ах ; и восстановить регистры pop bx pop ds ret interrupt endp
; процедура инициализации ; вызывается только раз при загрузке драйвера init proc near push сx push dx
mov ax,offset buffer mov cx,cs ; CX:AX - адрес нашего буфера клавиатуры cmp cx,1000h ; если СХ слишком велик, jnc too_big ; не надо загружаться, shl cx,4 ; иначе: умножить сегментный адрес на 16, add cx,ax ; добавить смещение - получился ; линейный адрес, sub cx,400h ; вычесть линейный адрес начала данных BIOS push 0040h pop ds mov bx,1Ah ; DS:BX = 0040h:001Ah - адрес головы mov word ptr [bx],cx ; записать новый адрес головой буфера mov word ptr [bx+2],cx ; он же новый адрес хвоста mov bl,80h ; DS:BX = 0040h: ; 0080h - адрес начала буфера mov word ptr [bx],cx ; записать новый адрес начала, add cx,BUF_SIZE*2 ; добавить размер mov word ptr [bx+2],cx ; и записать новый адрес конца
mov ah,9 ; функция DOS 09h mov dx,offset succ_msg ; DS:DX - адрес строки push cs ; с сообщением об успешной установке pop ds int 21h ; вывод строки на экран lds bx,dword ptr cs:request ; DS:BX - адрес запроса
mov ax,offset init mov word ptr [bx+0Eh],ax ; CS:AX - следующий байт после mov word ptr [bx+10h],cs ; конца резидентной части jmp short done ; конец процедуры инициализации
; сюда передается управление, если мы загружены слишком низко в памяти too_big: mov ah,9 ; функция DOS 09h mov dx,offset fail_msg ; DS:DX - адрес строки push cs ; с сообщением о неуспешной pop ds ; установке int 21h ; вывод строки на экран lds bx,dword ptr cs:request ; DS:BX - адрес запроса mov word ptr [bx+0Eh],0 ; записать адрес начала драйвера mov word ptr [bx+10h],cs ; в поле "адрес первого ; освобождаемого байта" done: pop dx pop cx ret init endp ; сообщение об успешной установке (на английском, потому что в этот момент ; русские шрифты еще не загружены) succ_msg db "Keyboard extender loaded",0Dh,0Ah,'$' ; сообщение о неуспешной установке fail_msg db "Too many drivers in memory - " db "put kbdext.sys first " db "in config.sys",0Dh,0Ah,'$' end start
Теперь более подробно рассмотрим функции, которые должен поддерживать драйвер символьного устройства на примере драйвера устройства ROT 13. ROT 13 — это метод простой модификации английского текста, который применяется в электронной почте, чтобы текст нельзя было прочитать сразу. ROT 13 состоит в сдвиге каждой буквы латинского алфавита на 13 позиций (в любую сторону, так как всего 26 букв). Раскодирование, очевидно, выполняется такой же операцией. Когда наш драйвер загружен, команда DOS
сору encrypt.txt rot13
приведет к тому, что текст из encrypt.txt будет выведен на экран, зашифрованный или расшифрованный ROT 13, в зависимости от того, был ли он зашифрован до этого.
Рассмотрим все команды, которые может поддерживать символьное устройство, и буфера запросов, которые им передаются.
00h: Инициализация (уже рассмотрена)
03h: IOCTL-чтение (если установлен бит 14 атрибута)
+0Eh: 4 байта — адрес буфера
+12h: 2 байта
на входе — запрашиваемое число байт
на выходе — реально записанное в буфер число байт
04h: Чтение из устройства
Структура буфера для символьных устройств совпадает с 03h
05h: Чтение без удаления символа из буфера
+0Dh: на выходе — прочитанный символ, если символа нет — установить бит 9 слова состояния
06h: Определить состояние буфера чтения
Если в буфере нет символов для чтения — установить бит 9 слова состояния.
07h: Сбросить буфер ввода
08h: Запись в устройство
+0Eh: 4 байта — адрес буфера
+12h: 2 байта
на входе — число байт для записи
на выходе — число байт, которые были записаны
09h: Запись в устройство с проверкой
аналогично 08h
0Ah: Определите состояние буфера записи
Если в устройствоиельзя писать — установить бит 9 слова состояния.
0Bh: Сбросить буфер записи
0Ch: IOCTL-запись (если установлен бит 14 атрибута)
Аналогично 08h
0Dh: Открыть устройство (если установлен бит 11 атрибута)
0Eh: Закрыть устройство (если установлен бит 11 атрибута)
11h: Вывод, пока не занято (если установлен бит 13 атрибута)
Аналогично 08h, в отличие от функций записи здесь не считается ошибкой записать не все байты
13h: Обобщенный IOCTL (если установлен бит 6 атрибута)
+0Dh: байт — категория устройства (01, 03, 05 = COM, CON, LPT) 00h — неизвестная категория
+0Eh: байт — код подфункции:
45h: установить число повторных попыток
65h: определить число повторных попыток
4Ah: выбрать кодовую страницу
6Ah: определить активную кодовую страницу
4Ch: начало подготовки кодовой страницы
4Dh: конец подготовки кодовой страницы
6Bh: получить список готовых кодовых страниц
5Fh: установить информацию о дисплее
7Fh: получить информацию о дисплее
+0Fh: 4 байта — не используется
+13h: 4 байта — адрес структуры данных IOCTL — соответствует структуре, передающейся в DS:DX для INT 21h, АХ = 440Ch
19h: Поддержка функций IOCTL (если установлены биты 6 и 7 атрибута)
+0Dh: байт — категория устройства
+0Eh: код подфункции
Если эта комбинация подфункции и категории устройства не поддерживается драйвером — надо вернуть ошибку 03h в слове состояния.
Итак, теперь мы можем создать полноценный драйвер символьного устройства. Упрощая задачу, реализуем только функции чтения из устройства и будем возвращать соответствующие ошибки для других функций.
Еще одно отличие этого примера — в нем показано, как совместить в одной программе обычный исполнимый файл типа ЕХЕ и драйвер устройства. Если такую программу запустить обычным образом, она будет выполняться, начиная со своей точки входа (метка start в нашем примере), а если ее загрузить из CONFIG.SYS, DOS будет считать драйвером участок программы, начинающийся со смещения 0:
; rot13.asm ; Драйвер символьного устройства, выводящий посылаемые ему символы на экран ; после выполнения над ними преобразования ROT13 ; (каждая буква английского алфавита смещается на 13 позиций). ; Реализованы только функции записи в устройство ; ; Пример использования: ; сору encrypted.txt $rot13 ; загрузка - из CONFIG.SYS ; DEVICE=c:\rot13.exe, ; если rot13.exe находится в директории С:\ ; .model small ; модель для ЕХЕ-файла .code .186 ; для pusha/popa org 0 ; код драйвера начинается с CS:0000 dd -1 ; адрес следующего драйвера dw 0A800h ; атрибуты нашего устройства dw offset strategy ; адрес процедуры стратегии dw offset interrupt ; адрес процедуры прерывания db "$ROT13",20h,20h ; имя устройства, дополненное ; пробелами до восьми символов
request dd ? ; сюда процедура стратегии будет писать ; адрес буфера запроса
; таблица адресов обработчиков для всех команд command_table dw offset init ; 00h dw 3 dup(offset unsupported) ; 01, 02, 03 dw 2 dup(offset read) ; 04, 05 dw 2 dup(offset unsupported) ; 06, 07 dw 2 dup(offset write) ; 08h, 09h dw 6 dup(offset unsupported) ; 0Ah, 0Bh, 0Ch, ; 0Dh, 0Eh, 0Fh dw offset write ; 10h dw 2 dup(offset invalid) ; 11h, 12h dw offset unsupported ; 13h dw 3 dup(offset invalid) ; 14h, 15h, 16h dw 3 dup(offset unsupported) ; 17h, 18h, 19h
; процедура стратегии - одна и та же для всех драйверов strategy proc far mov word ptr cs:request,bx mov word ptr cs:request+2,es ret strategy endp
; процедура прерывания interrupt proc far pushf pusha ; сохранить регистры push ds ; и на всякий случай флаги push es
push cs pop ds ; DS = наш сегментный адрес les si,dword ptr request ; ES:SI = адрес буфера запроса xor bx,bx mov bl,byte ptr es:[si+2] ; BX = номер функции cmp bl,19h ; проверить, что команда jbe command_ok ; в пределах 00 - 19h, call invalid ; если нет - выйти с ошибкой jmp short interrupt_end command_ok: ; если команда находится в пределах 00 - 19h, shl bx,1 ; умножить ее на 2, чтобы получить смещение ; в таблице слов command_table, call word ptr command_table[bx] ; и вызвать обработчик interrupt_end: cmp al,0 ; AL = 0, если не было ошибок, je no_error or ah,80h ; если была ошибка - установить бит 15 в АХ, no_error: or ah,01h ; в любом случае установить бит 8 mov word ptr es:[si+3],ax ; и записать слово состояния pop es pop ds рора popf ret interrupt endp
; обработчик команд, предназначенных для блочных устройств unsupported proc near xor ax,ax ; не возвращать никаких ошибок ret unsupported endp
; обработчик команд чтения read proc near mov al,0Bh ; общая ошибка чтения ret read endp
; обработчик несуществующих команд invalid proc near mov ax,03h ; ошибка "неизвестная команда" ret invalid endp
; обработчик функций записи write proc near push si mov cx,word ptr es:[si+12h] ; длина буфера в СХ, jcxz write_finished ; если это 0 - нам делать нечего lds si,dword ptr es:[si+0Eh] ; адрес буфера в DS:SI
; выполнить ВОТ13- преобразование над буфером cld rot13_loop: ; цикл по всем символам буфера lodsb ; AL = следующий символ из буфера в ES:SI cmp al,'А' ; если он меньше "А", jl rot13_done ; это не буква, cmp al,'Z' ; если он больше "Z", jg rot13_low ; может быть, это маленькая буква, cmp al,('A'+13) ; иначе: если он больше "А" + 13, jge rot13_dec ; вычесть из него 13, jmp short rot13_inc ; а иначе - добавлять rot13_low: cmp al,'а' ; если символ меньше "а", jl rot13_done ; это не буква, cmp al,'z' ; если символ больше "z", jg rot13_done ; то же самое, cmp al,('a'+13) ; иначе: если он больше "а" + 13, jge rot13_dec ; вычесть из него 13, иначе: rot13_inc: add al,13 ; добавить 13 к коду символа, jmp short rot13_done rot13_dec: sub al,13 ; вычесть 13 из кода символа, rot13_done: int 29h ; вывести символ на экран loop rot13_loop ; и повторить для всех символов write_finished: xor ах,ах ; сообщить, что ошибок не было pop si ret write endp
; процедура инициализации драйвера init proc near mov ah,9 ; функция DOS 09h mov dx,offset load_msg ; DS:DX - сообщение об установке int 21h ; вывод строки на экран mov word ptr es:[si+0Eh],offset init ; записать адрес mov word ptr es:[si+10h],cs ; конца резидентной части xor ах,ах ; ошибок не произошло ret init endp
; сообщение об установке драйвера load_msg db "ROT13 device driver loaded",0Dh,0Ah,'$'
start: ; точка входа ЕХЕ-программы push cs pop ds mov dx,offset exe_msg ; DS:DX - адрес строки mov ah,9 ; функция DOS int 21h ; вывод строки на экран mov ah,4Ch ; функция DOS 4Ch int 21h ; завершение ЕХЕ-программы
; строка, которая выводится при запуске не из CONFIG.SYS: exe_msg db "Эту программу надо загружать как драйвер устройства из" db "CONFIG.SYS",0Dh,0Ah,'$'
.stack
end start
Сложение и вычитание
Команды ADC (сложение с учетом переноса) и SBB (вычитание с учетом займа) специально были введены для подобных операций. При сложении сначала складывают самые младшие байты, слова или двойные слова командой ADD, а затем складывают все остальное командами ADC, двигаясь от младшего конца числа к старшему. Команды SUB/SBB действуют полностью аналогично.
bigval_1 dw 0,0,0 ; 96-битное число bigval_2 dw 0,0,0 bigval_3 dw 0,0,0
; сложение 96-битных чисел bigval_1 и bigval_2 mov eax,dword ptr bigval_1 add eax,dword ptr bigval_2 ; сложить младшие слова mov dword ptr bigval_3,eax mov eax,dword ptr bigval_1[4] adc eax,dword ptr bigval_2[4] ; сложить средние слова mov dword ptr bigval_3[4],eax mov eax,dword ptr bigval_1[8] adc eax,dword ptr bigval_2[8] ; сложить старшие слова mov dword ptr bigval_3[8],eax
; вычитание 96-битных чисел bigval_1 и bigval_2 mov eax,dword ptr bigval_1 sub eax,dword ptr bigval_2 ; вычесть младшие слова mov dword ptr bigval_3,eax mov eax,dword ptr bigval_1[4] sbb eax,dword ptr bigval_2[4] ; вычесть средние слова mov dword ptr bigval_3[4],eax mov eax,dword ptr bigval_1[8] sbb eax,dword ptr bigval_2[8] ; вычесть старшие слова mov dword ptr bigval_3[8],eax
Сложение и вычитание для чисел с фиксированной запятой ничем не отличается от сложения и вычитания целых чисел:
mov ax,1080h ; AX = 1080h = 16,5 mov bx,1240h ; BX = 1240h = 18,25 add ax,bx ; AX = 22C0h = 34,75 sub ax,bx ; AX = 1080h = 16,5
Сортировки
Еще одна часто встречающаяся задача при программировании — сортировка данных. Все существующие алгоритмы сортировки можно разделить на сортировки перестановкой, в которых на каждом шаге алгоритма меняется местами пара чисел; сортировки выбором, в которых на каждом шаге выбирается наименьший элемент и дописывается в отсортированный массив; и сортировки вставлением, в которых элементы массива рассматривают последовательно и каждый вставляют на подходящее место в отсортированном массиве. Самая простая сортировка перестановкой — пузырьковая, в которой более легкие элементы «всплывают» к началу массива. Сначала второй элемент сравнивается с первым и, если нужно, меняется с ним местами. Затем третий элемент сравнивается со вторым и только в том случае, когда они переставляются, сравнивается с первым, и т.д. Этот алгоритм также является и самой медленной сортировкой — в худшем случае для сортировки массива N чисел потребуется N2/2 сравнений и перестановок, а в среднем — N2/4.
; Процедура bubble_sort ; сортирует массив слов методом пузырьковой сортировки ; ввод: DS:DI = адрес массива ; DX = размер массива (в словах) bubble_sort proc near pusha cld cmp dx,1 jbe sort_exit ; выйти, если сортировать нечего dec dx sb_loop1: mov cx,dx ; установить длину цикла xor bx,bx ; BX будет флагом обмена mov si,di ; SI будет указателем на ; текущий элемент sn_loop2: lodsw ; прочитать следующее слово cmp ax,word ptr [si] jbe no_swap ; если элементы не ; в порядке, xchg ax,word ptr [si] ; поменять их местами mov word ptr [si-2],ax inc bx ; и установить флаг в 1, no_swap: loop sn_loop2 cmp bx,0 ; если сортировка не закончилась, jne sn_loop1 ; перейти к следующему элементу sort_exit: popa ret bubble_sort endp
Пузырьковая сортировка осуществляется так медленно потому, что сравнения выполняются лишь между соседними элементами. Чтобы получить более быстрый метод сортировки перестановкой, следует выполнять сравнение и перестановку элементов, отстоящих далеко друг от друга. На этой идее основан алгоритм, который называется «быстрая сортировка». Он работает следующим образом: делается предположение, что первый элемент является средним по отношению к остальным. На основе такого предположения все элементы разбиваются на две группы — больше и меньше предполагаемого среднего. Затем обе группы отдельно сортируются таким же методом. В худшем случае быстрая сортировка массива из N элементов требует N2 операций, но в среднем случае — только 2n*log2n сравнений и еще меньшее число перестановок.
; Процедура quick_sort ; сортирует массив слов методом быстрой сортировки ; ввод: DS:BX = адрес массива ; DX = число элементов массива quicksort proc near cmp dx,1 ; Если число элементов 1 или 0, jle qsort_done ; то сортировка уже закончилась xor di,di ; индекс для просмотра сверху (DI = 0) mov si,dx ; индекс для просмотра снизу (SI = DX) dec si ; SI = DX-1, так как элементы нумеруются с нуля, shl si,1 ; и умножить на 2, так как это массив слов mov ax,word ptr [bx] ; AX = элемент X1, объявленный средним step_2: ; просмотр массива снизу, пока не встретится ; элемент, меньший или равный Х1
cmp word ptr [bx][si],ax ; сравнить XDI и Х1
jle step_3 ; если XSI больше, sub si,2 ; перейти к следующему снизу элементу jmp short step_2 ; и продолжить просмотр step_3: ; просмотр массива сверху, пока не встретится ; элемент меньше Х1 или оба просмотра не придут ; в одну точку cmp si,di ; если просмотры встретились, je step_5 ; перейти к шагу 5, add di,2 ; иначе: перейти ; к следующему сверху элементу, cmp word ptr [bx][di],ax ; если он меньше Х1, jl step_3 ; продолжить шаг 3 steр_4: ; DI указывает на элемент, который не должен быть ; в верхней части, SI указывает на элемент, ; который не должен быть в нижней. Поменять их местами mov cx,word ptr [bx][di] ; CX = XDI
xchg cx,word ptr [bx][si] ; CX = XSI, XSI = XDI
mov word ptr [bx][di],cx ; XDI = CX jmp short step_2 step_5: ; Просмотры встретились. Все элементы в нижней ; группе больше X1, все элементы в верхней группе ; и текущий - меньше или равны Х1 Осталось ; поменять местами Х1 и текущий элемент: xchg ах,word ptr [bx][di] ; АХ = XDI, XDI = X1
mov word ptr [bx],ax ; X1 = AX ; теперь можно отсортировать каждую из полученных групп push dx push di push bx
mov dx,di ; длина массива X1...XDI-1
shr dx,1 ; в DX call quick_sort ; сортировка
pop bx pop di pop dx
add bx,di ; начало массива XDI+1...XN
add bx,2 ; в BX shr di,1 ; длина массива XDI+1...XN
inc di sub dx,di ; в DX call quicksort ; сортировка qsort_done: ret quicksort endp
Кроме того, что быстрая сортировка — самый известный пример алгоритма, использующего рекурсию, то есть вызывающего самого себя, это еще и самая быстрая из сортировок «на месте», то есть сортировка, использующая только ту память, в которой хранятся элементы сортируемого массива. Можно доказать, что сортировку нельзя выполнить быстрее, чем за n*log2n операций, ни в худшем, ни в среднем случаях, и быстрая сортировка достаточно хорошо приближается к этому пределу в среднем случае. Сортировки, достигающие теоретического предела, тоже существуют — это сортировки турнирным выбором и сортировки вставлением в сбалансированные деревья, но для их работы требуется резервирование дополнительной памяти, так что, например, работа со сбалансированными деревьями будет происходить медленно из-за дополнительных затрат на поддержку сложных структур данных в памяти.
Рассмотрим в качестве примера самый простой вариант сортировки вставлением, использующий линейный поиск и затрачивающий порядка n2/2 операций. Ее так же просто реализовать, как и пузырьковую сортировку, и она тоже имеет возможность выполняться «на месте». Кроме того, из-за высокой оптимальности кода этой процедуры она может оказываться даже быстрее рассмотренной нами «быстрой» сортировки на подходящих массивах.
; Процедура linear_selection_sort ; сортирует массив слов методом сортировки линейным выбором ; Ввод: DS:SI (и ES:SI) = адрес массива ; DX = число элементов в массиве
do_swap: lea bx,word ptr [di-2] mov ax, word ptr [bx] ; новое минимальное число dec cx ; если поиск минимального закончился, jcxz tail ; перейти к концу loop1: scasw ; сравнить минимальное в АХ ; со следующим элементом массива ja do_swap ; если найденный элемент ; еще меньше - выбрать ; его как минимальный loop loop1 ; продолжить сравнения ; с минимальным в АХ tail:. xchg ax,word ptr [si-2] ; обменять минимальный элемент mov word ptr [bx],ax ; с элементом, находящимся в начале ; массива linear_selection_sort proc near ; точка входа в процедуру mov bx,si ; BX содержит адрес ; минимального элемента lodsw ; пусть элемент, адрес ; которого был в SI, минимальный, mov di,si ; DI - адрес элемента, сравниваемого ; с минимальным dec dx ; надо проверить DX-1 элементов массива mov cx,dx jg loop1 ; переход на проверку, если DX > 1 ret linear_selection_sort endp
Сравнение
Так как команда сравнения эквивалентна команде вычитания, кроме того, что она не изменяет значение приемника, можно было бы просто выполнять вычитание чисел повышенной точности и отбрасывать результат, но сравнение можно выполнить и более эффективным образом. В большинстве случаев для определения результата сравнения достаточно сравнить самые старшие слова (байты или двойные слова), и только если они в точности равны, потребуется сравнение следующих слов.
; Сравнение 96-битных чисел bigval_1 и bigval_2 mov eax,dword ptr bigval_1[8] cmp eax,dword ptr bigval_2[8] ; сравнить старшие слова jg greater jl less mov eax,dword ptr bigval_1[4] cmp eax,dword ptr bigval_2[4] ; сравнить средние слова jg greater jl less mov eax,dword ptr bigval_1 cmp eax,dword ptr bigval_2 ; сравнить младшие слова jg greater jl less equal: greater: less:
Структуры CASE
Управляющая структура типа CASE проверяет значение некоторой переменной (или выражения) и передает управление на различные участки программы. Кажется очевидным, что эта структура должна реализовываться в виде серии структур IF... THEN... ELSE, как показано в примерах, где требовались различные действия в зависимости от значения нажатой клавиши.
Пусть переменная I принимает значения от 0 до 2, и в зависимости от значения надо выполнить процедуры case0, casel и case2:
mov ax,I cmp ax,0 ; проверка на 0 jne not0 call case0 jmp endcase not0: cmp ax,1 ; проверка на 1 jne not1 call case1 jmp endcase not1: cmp ax,2 ; проверка на 2 jne not2 call case2 not2: endcase:
Но ассемблер предоставляет более удобный способ реализации таких структур — таблицу переходов.
mov bx,I shl bx,1 ; умножить ВХ на 2 (размер адреса ; в таблице переходов - 4 для 32-битных адресов) jmp cs:jump_table[bx] ; разумеется, ; в этом примере достаточно использовать call
jump_table dw foo0,foo1,foo2 ; таблица переходов
foo0: call case0 jmp endcase foo1: call case1 jmp endcase foo2: call case2 jmp endcase
Очевидно, что для большого числа значений переменной способ с таблицей переходов гораздо быстрее (не требуется многочисленных проверок), а если большая часть значений переменной — числа, следующие в точности друг за другом (так что в таблице переходов не окажется пустых участков), то эта реализация структуры CASE окажется еще и значительно меньше.
Структуры IF.. THEN... ELSE
Это часто встречающаяся управляющая структура, передающая управление на один участок программы, если некоторое условие выполняется, и на другой, если оно не выполняется, записывается на ассемблере в следующем общем виде:
; набор команд, проверяющих условие Jcc Else ; набор команд, соответствующих блоку THEN jmp Endif Else: ; набор команд, соответствующих блоку ELSE Endif:
Для сложных условий часто оказывается, что одной командой условного перехода обойтись нельзя, так что реализация проверки может значительно увеличиться; например, следующую строку на языке С
if (((х > у) && (z < t)) (a != b)) c = d;
можно представить на ассемблере как:
; проверка условия mov ax,A cmp ах,В jne then ; если а != b - условие выполнено mov ах,X cmp ax,Y jng endif ; если х <= у - условие не выполнено mov ax,Z cmp ах,Т jnl endif ; если z >= t - условие не выполнено then: ; условие выполняется mov ax,D mov С,ах endif:
Таймер
Все, что нам было известно до сих пор о системном таймере, — это устройство, вызывающее прерывание IRQ0 приблизительно 18,2 раза в секунду. На самом деле программируемый интервальный таймер — весьма сложная система, включающая в себя целых три устройства — три канала таймера, каждый из которых можно запрограммировать для работы в одном из шести режимов. И более того, на большинстве современных материнских плат располагаются два таких таймера, так что число каналов оказывается равным шести. Для своих нужд программы могут использовать канал 2 (если им не нужен динамик) и канал 4 (если присутствует второй таймер). При необходимости можно перепрограммировать и канал 0, но затем надо будет вернуть его в исходное состояние, чтобы BIOS и DOS могли продолжать работу.
В пространстве портов ввода-вывода для таймера выделена область от 40h до 5Fh:
порт 40h — канал 0 (генерирует IRQ0)
порт 41h — канал 1 (поддерживает обновление памяти)
порт 42h — канал 2 (управляет динамиком)
порт 43h — управляющий регистр первого таймера
порты 44h – 47h — второй таймер компьютеров с шиной MicroChannel
порты 48h – 4Bh — второй таймер компьютеров с шиной EISA
Все управление таймером осуществляется путем вывода одного байта в порт 43h (для первого таймера). Рассмотрим назначение бит в этом байте.
биты 7 – 6: если не 11 — это номер канала, который будет программироваться
00,01,10 = канал 0,1,2
биты 5 – 4:
00 — зафиксировать текущее значение счетчика для чтения (в этом случае биты 3 – 0 не используются)
01 — чтение/запись только младшего байта
10 — чтение/запись только старшего байта
11 — чтение/запись сначала младшего, а потом старшего байта
биты 3 – 1: режим работы канала
000: прерывание IRQ0 при достижении нуля
001: ждущий мультивибратор
010: генератор импульсов
011: генератор прямоугольных импульсов (основной режим)
100: программно запускаемый одновибратор
101: аппаратно запускаемый одновибратор
бит 0: формат счетчика:
0 — двоичное 16-битное число (0000 – FFFFh)
1 — двоично-десятичное число (0000 – 9999)
Если биты 7 – 6 равны 11, считается, что байт, посылаемый в порт 43h, — команда чтения счетчиков, формат которой отличается от команды программирования канала:
биты 7 – 6: 11 (код команды чтения счетчиков)
биты 5 – 4: режим чтения:
00: сначала состояние канала/потом значение счетчика
01: значение счетчика
10: состояние канала
биты 3 – 1: команда относится к каналам 3 – 1
Если этой командой запрашивается состояние каналов, новые команды будут игнорироваться, пока не прочтется состояние из всех каналов, которые были заказаны битами 3 – 1.
Состояние и значение счетчика данного канала получают чтением из порта, соответствующего требуемому каналу. Формат байта состояния имеет следующий вид:
бит 7: состояние входа OUTx на момент выполнения команды чтения счетчиков. Так как в режиме 3 счетчик уменьшается на 2 за каждый цикл, состояние этого бита, замороженное командой фиксации текущего значения счетчика, укажет, в каком полуцикле находился таймер
бит 6: 1/0 — состояние счетчика не загружено/загружено (используется в режимах 1 и 5, а также после команды фиксации текущего значения)
биты 5 – 0: совпадают с битами 5 – 0 последней команды, посланной в порт 43h
Для того чтобы запрограммировать таймер в режиме 3, в котором работают каналы 0 и 2 по умолчанию и который чаще всего применяют в программах, требуется выполнить следующие действия:
Вывести в регистр 43h команду ( для канала 0) 0011011h, то есть установить режим 3 для канала 0, и при чтении/записи будет пересылаться сначала младшее слово, а потом старшее.
Послать младший байт начального значения счетчика в порт, соответствующий выбранному каналу (42h для канала 2).
Послать старший байт начального значения счетчика в этот же порт.
После этого таймер немедленно начнет уменьшать введенное число от начального значения к нулю со скоростью 1 193 180 раз в секунду (четверть скорости процессора 8088). Каждый раз, когда это число достигает нуля, оно снова возвращается к начальному значению. Кроме того, при достижении счетчиком нуля таймер выполняет соответствующую функцию — канал 0 вызывает прерывание IRQO, а канал 2, если включен динамик, посылает ему начало следующей прямоугольной волны, заставляя его работать на установленной частоте. Начальное значение счетчика для канала 0 по умолчанию составляет 0FFFFh (65 535), то есть максимально возможное. Поэтому точная частота вызова прерывания IRQ0 равна 1 193 180/65 536 = 18,20648 раза в секунду.
Чтобы прочитать текущее значение счетчика, надо:
Послать в порт 43h команду фиксации значения счетчика для выбранного канала (биты 5 – 4 равны 00h).
Послать в порт 43h команду перепрограммирования канала без изменения режима его работы, если нужно изменить способ чтения/записи (обычно не требуется).
Прочитать из порта, соответствующего выбранному каналу, младший байт зафиксированного значения счетчика.
Прочитать из того же порта старший байт.
Для таймера найдется много применений, единственное ограничение здесь: таймер — это глобальный ресурс, и перепрограммировать его в многозадачных системах можно только с ведома операционной системы, если она вообще это позволяет.
Посмотрим в качестве примера, как при помощи таймера измерить, сколько времени проходит между реальным аппаратным прерыванием и моментом, когда обработчик этого прерывания получает управление (почему это важно, см. пример программ вывода звука из глав 5.10.8 и 5.10.9). Так как IRQ0 происходит при нулевом значении счетчика, нам достаточно прочитать его значение при старте обработчика и обратить его знак (потому что счетчик таймера постоянно уменьшается).
; latency.asm ; измеряет среднее время, проходящее между аппаратным прерыванием и запуском ; соответствующего обработчика. Выводит среднее время в микросекундах после ; нажатия любой клавиши (на самом деле в 1/1 193 180). ; Программа использует 16-битный сумматор для простоты, так что может давать ; неверные результаты, если подождать больше нескольких минут
.model tiny .code .386 ; для команды shld org 100h ; COM-программа start: mov ax,3508h ; AH = 35h, AL = номер прерывания int 21h ; получить адрес обработчика mov word ptr old_int08h,bx ; и записать его в old_int08h mov word ptr old_int08h+2,es mov ax,2508h ; AH = 25h, AL = номер прерывания mov dx,offset int08h_handler ; DS:DX - адрес обработчика int 21h ; установить обработчик ; с этого момента в переменной latency накапливается сумма mov ah,0 int 16h ; пауза до нажатия любой клавиши mov ax,word ptr latency ; сумма в АХ cmp word ptr counter,0 ; если клавишу нажали немедленно, jz dont_divide ; избежать деления на ноль xor dx,dx ; DX = 0 div word ptr counter ; разделить сумму на число накоплений dont_divide: call print_ax ; и вывести на экран
mov ax,2508h ; АН = 25h, AL = номер прерывания lds dx,dword ptr old_int08h ; DS:DX = адрес обработчика int 21h ; восстановить старый обработчик ret ; конец программы
latency dw 0 ; сумма задержек counter dw 0 ; число вызовов прерывания
; Обработчик прерывания 08h (IRQ0) ; определяет время, прошедшее с момента срабатывания IRQ0 int08h_handler proc far push ax ; сохранить используемый регистр mov al,0 ; фиксация значения счетчика в канале 0 out 43h,al ; порт 43h: управляющий регистр таймера ; так как этот канал инициализируется BIOS для 16-битного чтения/записи, другие ; команды не требуются in al,40h ; младший байт счетчика mov ah,al ; в АН in al,40h ; старший байт счетчика в AL xchg ah,al ; поменять их местами neg ax ; обратить его знак, так как счетчик ; уменьшается add word ptr cs:latency,ax ; добавить к сумме inc word ptr cs:counter ; увеличить счетчик накоплений pop ax db 0EAh ; команда jmp far old_int08h dd 0 ; адрес старого обработчика int08h_handler endp
; процедура print_ax ; выводит АХ на экран в шестнадцатеричном формате print_ax proc near xchg dx,ax ; DX = AX mov cx,4 ; число цифр для вывода shift_ax: shld ax,dx,4 ; получить в AL очередную цифру rol dx,4 ; удалить ее из DX and al,0Fh ; оставить в AL только эту цифру cmp al,0Ah ; три команды, переводящие sbb al,69h ; шестнадцатеричную цифру в AL das ; в соответствующий ASCII-код int 29h ; вывод на экран loop shift_ax ; повторить для всех цифр ret print_ax endp end start
Таймер можно использовать для управления динамиком, для точных измерений отрезков времени, для создания задержек, для управления переключением процессов и даже для выбора случайного числа с целью запуска генератора случайных чисел — текущее значение счетчика канала 0 представляет собой идеальный вариант такого начального числа для большинства приложений.
Трансцендентные функции
Многие операции при работе с графикой используют умножение числа на синус (или косинус) некоторого угла, например при повороте: s = sin(n) * v. При использовании арифметики с фиксированной точкой 16:16 это уравнение преобразуется в s = int(sin(n) * 65 536) * v/65 536 (где int — целая часть). Для требовательных ко времени работы участков программ, например для работы с графикой, принято вообще не вычислять значения синусов, а считывать из таблицы, содержащей значения выражения int(sin(n) * 65 535), где n меняется от 0 до 90 градусов с требуемым шагом (редко требуется шаг меньше 0,1 градуса). Затем синус любого угла от 0 до 90 градусов можно вычислить с помощью всего одного умножения и сдвига на 16 бит. Синусы других углов и косинусы вычисляются в соответствии с обычными формулами приведения:
sin(x) = sin(180-x) для 90 < х < 180 sin(x) = -sin(x-180) для 180 < х < 270 sin(x) = -sin(360-x) для 270 < х < 360 cos(x) = sin(90-x)
хотя часто используют таблицу синусов на все 360 градусов, устраняя дополнительные проверки и изменения знаков в критических участках программы.
Таблицы синусов (или косинусов), используемые в программе, можно создать заранее с помощью простой программы на языке высокого уровня в виде текстового файла с псевдокомандами DW и включить в текст программы директивой include. Другой способ, занимающий меньше места в тексте, но чуть больше времени при запуске программы, — однократное вычисление всей таблицы. Таблицу можно вычислять как с помощью команды FPU fsin и потом преобразовывать к желаемому формату, так и сразу в формате с фиксированной запятой. Существует довольно популярный алгоритм, позволяющий вычислить таблицу косинусов (или синусов, с небольшой модификацией), используя рекуррентное выражение
cos(xk) = 2cos(step)cos(xk-1) - cos(xk-2)
где step — шаг, с которым вычисляются косинусы, например 0,1 градуса.
; liss.asm ; строит фигуры Лиссажу, используя арифметику с фиксированной запятой ; и генерацию таблицы косинусов. ; Фигуры Лиссажу - семейство кривых, задаваемых параметрическими выражениями ; x(t) = cos(SCALE_V * t) ; y(t) = sin(SCALE_H * t) ; ; чтобы выбрать новую фигуру, измените параметры SCALE_H и SCALE_V, ; для построения незамкнутых фигур удалите строку add di,5l2 ; в процедуре move_point
.model tiny .code .386 ; будут использоваться 32-битные регистры org 100h ; СОМ-программа
SCALE_H equ 3 ; число периодов в фигуре по горизонтали SCALE_V equ 5 ; число периодов по вертикали
start proc near cld ; для команд строковой обработки mov di,offset cos_table ; адрес начала таблицы косинусов mov ebx,16777137 ; 224 * cos(360/2048) - заранее вычисленное mov cx,2048 ; число элементов для таблицы call build_table ; построить таблицу косинусов
mov ax,0013h ; графический режим int 10h ; 320x200x256
mov ax,1012h ; установить набор регистров палитры VGA, mov bx,70h ; начиная с регистра 70h mov cx,4 ; четыре регистра mov dx,offset palette ; адрес таблицы цветов int 10h
push 0A000h ; сегментный адрес видеопамяти pop es ; в ES
main_loop: call display_picture ; изобразить точку со следом mov dx,5000 xor cx,cx mov ah,86h int 15h ; пауза на CX:DX микросекунд mov ah,11h ; проверить, была ли нажата клавиша, int 16h jz main_loop ; если нет - продолжить основной цикл
mov ах,0003h ; текстовый режим int 10h ; 80x24
ret ; конец программы start endp
; процедура build_table ; строит таблицу косинусов в формате с фиксированной запятой 8:24 ; по рекуррентной формуле cos(xk) = 2 * cos(span/steps) * cos(xk-1) - cos(xk-2), ; где span - размер области, на которой вычисляются косинусы (например, 360), ; a steps - число шагов, на которые разбивается область ; Ввод: DS:DI = адрес таблицы ; DS:[DI] = 224 ; EBX = 224 * cos(span/steps) ; СХ = число элементов таблицы, которые надо вычислить ; Вывод: таблица размером СХ * 4 байта заполнена ; Модифицируются: DI,CX,EAX,EDX
build_table proc near mov dword ptr [di+4],ebx ; заполнить второй элемент таблицы sub ex,2 ; два элемента уже заполнены add di,8 mov eax,ebx build_table_loop: imul ebx ; умножить cos(span/steps) на cos(xk-1) shrd eax,edx,23 ; поправка из-за действий с фиксированной ; запятой 8:24 и умножение на 2 sub eax,dword ptr [di-8] ; вычитание cos(xk-2) stosd ; запись результата в таблицу loop build_table_loop ret build_table endp
; процедура display_picture ; изображает точку со следом
display_picture proc near call move_point ; переместить точку mov bp,73h ; темно-серый цвет в нашей палитре mov bx,3 ; точка, выведенная три шага назад, call draw_point ; изобразить ее dec bp ; 72h - серый цвет в нашей палитре dec bx ; точка, выведенная два шага назад, call draw_point ; изобразить ее dec bp ; 71h - светло-серый цвет в нашей палитре dec bx ; точка, выведенная один шаг назад, call draw_point ; изобразить ее dec bp ; 70h - белый цвет в нашей палитре dec bx ; текущая точка call draw_point ; изобразить ее ret display_picture endp
; процедура draw_point ; Ввод: BP - цвет ; BX - сколько шагов назад выводилась точка ; draw_point proc near movzx сx,byte ptr point_x[bx] ; Х-координата movzx dx,byte ptr point_y[bx] ; Y-координата call putpixel_13h ; вывод точки на экран ret draw_point endp
; процедура move_point ; вычисляет координаты для следующей точки, ; изменяет координаты точек, выведенных раньше
move_point proc near inc word ptr time and word ptr time,2047 ; эти две команды организуют ; счетчик в переменной time, который ; изменяется от 0 до 2047 (7FFh) mov еах,dword ptr point_x ; считать координаты точек mov ebx,dword ptr point_y ; (по байту на точку) mov dword ptr point_x[1],eax ; и записать их со сдвигом mov dword ptr point_y[1],ebx ; 1 байт mov di,word ptr time ; угол (или время) в DI imul di,di,SCALE_H ; умножить его на SCALE_H and di,2047 ; остаток от деления на 2048, shl di,2 ; так как в таблице 4 байта на косинус mov ax,50 ; масштаб по горизонтали mul word ptr cos_table[di+2] ; Умножение на косинус. ; Берется старшее слово (смещение + 2) от ; косинуса, записанного в формате 8:24, ; фактически происходит умножение на косинус ; в формате 8:8 mov dx,0A000h ; 320/2 (X центра экрана) в формате 8:8 sub dx,ax ; расположить центр фигуры в центре экрана mov byte ptr point_x,dh ; и записать новую текущую точку mov di,word ptr time ; угол (или время) в DI imul di,di,SCALE_V ; умножить его на SCALE_V add di,512 ; добавить 90 градусов, чтобы заменить ; косинус на синус. Так как у нас 2048 ; шагов на 360 градусов, ; 90 градусов - это 512 шагов and di,2047 ; остаток от деления на 2048, shl di,2 ; так как в таблице 4 байта на косинус mov ax,50 ; масштаб по вертикали mul word ptr cos_table[di+2] ; умножение на косинус mov dx,06400h ; 200/2 (Y центра экрана) в формате 8:8 sub dx,ax ; расположить центр фигуры в центре экрана mov byte ptr point_y,dh ; и записать новую текущую точку ret move_point endp
; putpixel_13h ; процедура вывода точки на экран в режиме 13h ; DX = строка, СХ = столбец, ВР = цвет, ES = A000h putpixel_13h proc near push di mov ax,dx ; номер строки shl ax,8 ; умножить на 256 mov di,dx shl di,6 ; умножить на 64 add di,ax ; и сложить - то же, что и умножение на 320 add di,cx ; добавить номер столбца mov ax,bp stosb ; записать в видеопамять pop di ret putpixel_13h endp
point_x db 0FFh,0FFh,0FFh,0FFh ; Х-координаты точки и хвоста point_y db 0FFh,0FFh,0FFh,0FFh ; Y-координаты точки и хвоста db ? ; пустой байт - нужен для команд ; сдвига координат на один байт time dw 0 ; параметр в уравнениях Лиссажу - время или угол palette db 3Fh,3Fh,3Fh ; белый db 30h,30h,30h ; светло-серый db 20h,20h,20h ; серый db 10h,10h,10h ; темно-серый cos_table dd 1000000h ; здесь начинается таблица косинусов
end start
При генерации таблицы использовались 32-битные регистры, что приводит к увеличению на 1 байт и замедлению на 1 такт каждой команды, использующей их в 16-битном сегменте, но на практике большинство программ, интенсивно работающих с графикой, — 32-битные.
Умножение
Чтобы умножить числа повышенной точности, придется вспомнить правила умножения десятичных чисел в столбик: множимое умножают на каждую цифру множителя, сдвигают влево на соответствующее число разрядов и затем складывают полученные результаты. В нашем случае роль цифр будут играть байты, слова или двойные слова, а сложение должно выполняться по правилам сложения чисел повышенной точности. Алгоритм умножения оказывается заметно сложнее, поэтому умножим для примера только 64-битные числа:
; беззнаковое умножение двух 64-битных чисел (X и Y) и сохранение ; результата в 128-битное число Z mov eax,dword ptr X mov ebx,eax mul dword ptr Y ; перемножить младшие двойные слова mov dword ptr Z,eax ; сохранить младшее слово произведения mov ecx,edx ; сохранить старшее двойное слово mov eax,ebx ; младшее слово "X" в еах mul dword ptr Y[4] ; умножить младшее слово на старшее add еах,есх adc edx,0 ; добавить перенос mov ebx,eax ; сохранить частичное произведение mov ecx,edx mov eax,dword ptr X[4] mul dword ptr Y ; умножить старшее слово на младшее add eax,ebx ; сложить с частичным произведением mov dword ptr Z[4],eax adc ecx,edx mov eax,dword ptr X[4] mul dword ptr Y[4] ; умножить старшие слова add eax,ecx ; сложить с частичным произведением adc edx,0 ; и добавить перенос mov word ptr Z[8],eax mov word ptr Z[12],edx
Чтобы выполнить умножение со знаком, потребуется сначала определить знаки множителей, изменить знаки отрицательных множителей, выполнить обычное умножение и изменить знак результата, если знаки множителей были разными.
При умножении следует просто помнить, что умножение 16-битных чисел дает 32-битный результат, а умножение 32-битных чисел — 64-битный результат. Например, пусть ЕАХ и ЕВХ содержат числа с фиксированной запятой в формате 16:16:
xor edx,edx mul ebx ; теперь EDX:EAX содержит 64-битный результат ; (EDX содержит всю целую часть, а ЕАХ - всю дробную) shrd eax,edx,16 ;теперь ЕАХ содержит ответ, если не ; произошло переполнение (то есть если результат не превысил 65 535)
аналогом IMUL в этом случае будет последовательность команд
cdq imul ebx shrd eax,edx,16
Видеоадаптеры VGA
VGA-совместимые видеоадаптеры управляются при помощи портов ввода-вывода 03C0h– 03CFh, 03B4h, 03B5h, 03D4h, 03D5h, 03DAh, причем реальное число внутренних регистров видеоадаптера, к которым можно обращаться через это окно, превышает 50. Так как BIOS предоставляет хорошую поддержку для большинства стандартных функций, мы не будем рассматривать подробно программирование видеоадаптера на уровне портов, а только рассмотрим основные действия, для которых принято обращаться к видеоадаптеру напрямую.
Вложенные процедуры
Во многих языках программирования можно описывать процедуры внутри друг друга, так что локальные переменные, объявленные в пределах одной процедуры, доступны только из этой процедуры и всех вложенных в нее. Разные языки программирования используют разные способы реализации доступа к переменным, объявленным в функциях с меньшим уровнем вложенности (уровень вложенности главной процедуры определяют как 0 и увеличивают на 1 с каждым новым вложением).
Вложенные процедуры с дисплеями
Вместо того чтобы передавать адрес только одной вышестоящей активационной записи, процедурам можно передавать набор адресов сразу для всех уровней вложенности — от нулевого до непосредственно вышестоящего. При этом доступ к любой нелокальной процедуре сводится всего к двум командам, а перед вызовом процедуры вообще не требуется каких-либо дополнительных действий (так как вызываемая процедура поддерживает дисплей самостоятельно).
proc_at_3 proc near push bp ; сохранить динамическую ссылку mov bp,sp ; установить адрес текущей записи push display[6] ; сохранить предыдущее ; значение адреса третьего ; уровня в дисплее mov display[6],bp ; инициализировать третий ; уровень в дисплее sub sp,N ; выделить место для ; локальных переменных [...] mov bx,display[4] ; получить адрес записи для уровня 2 mov ax,ss:[bx-6] ; считать значение второй ; переменной из уровня 2 [...] add sp,N ; освободить стек от ; локальных перееденных pop display[6] ; восстановить старое ; значение третьего уровня в дисплее pop bp ret proc_at_3 endp
Здесь считается, что в сегменте данных определен массив слов Display, содержащий адреса последних использованных активационных записей для каждого уровня вложенности: display[0] содержит адрес активационной записи нулевого уровня, display[2] — первого уровня и так далее (для близких адресов).
Команды ENTER и LEAVE можно использовать для организации вложенности с дисплеями, но в этой реализации дисплей располагается не в сегменте данных, а в стеке, и при вызове каждой процедуры создается его локальная копия.
; enter N,4 (уровень вложенности 4, N байт на стековый кадр) ; эквивалентно набору команд push bp ; адрес записи третьего уровня push [bp-2] push [bp-4] push [bp-6] push [bp-8] ; скопировать дисплей mov bp,sp add bp,8 ; BP = адрес начала дисплея текущей записи sub sp,N ; выделить кадр для локальных переменных
Очевидно, что такой метод оказывается крайне неэффективным с точки зрения как скорости выполнения программы, так и расходования памяти. Более того, команда ENTER выполняется дольше, чем соответствующий набор простых команд. Тем не менее существуют ситуации, когда может потребоваться создание локальной копии дисплея для каждой процедуры. Например, если процедура, адрес которой передан как параметр другой процедуре, вызывающейся рекурсивно, должна обращаться к нелокальным переменным. Но и в этом случае передачи всего дисплея через стек можно избежать — более эффективным методом оказываются простые статические ссылки, рассмотренные ранее.
Вложенные процедуры со статическими ссылками
Самый простой способ предоставить вложенной процедуре доступ к локальным переменным, объявленным во внешней процедуре, — просто передать ей вместе с параметрами адрес активационной записи, содержащей эти переменные (см. рис. 17).
Рис. 17. Стек процедуры со статическими ссылками
При этом, если процедура вызывает вложенную в себя процедуру, она просто передает ей свой ВР, например так:
push bp call nested_proc
To есть статическая и динамическая ссылки в активационной записи процедуры nested_proc в этом случае не различаются. Если процедура вызывает другую процедуру на том же уровне вложенности, она должна передать ей адрес активационной записи из общего предка:
push [bp+4] call peer_proc
Если же процедура вызывает процедуру значительно меньшего уровня вложенности, так же как если процедура хочет получить доступ к переменным, объявленным в процедуре меньшего уровня вложенности, она должна проследовать по цепочке статических ссылок наверх, вплоть до требуемого уровня. То есть, если процедура на уровне вложенности 5 должна вызвать процедуру на уровне вложенности 2, она должна поместить в стек адрес активационной записи внешней по отношению к ним обоим процедуры с уровня вложенности 1:
mov bx,[bp+4] ; адрес записи уровня 4 в ВХ mov bx,ss:[bx+4] ; адрес записи уровня 3 в ВХ mov bx,ss[bx+4] ; адрес записи уровня 2 в ВХ push ss:[bx+4] ; адрес записи уровня 1 в стек call proc_at_level2
Этот метод реализации вложенных процедур имеет как преимущества, так и недостатки. С одной стороны, вся реализация вложенности сводится к тому, что в стек помещается всего одно дополнительное число, а с другой стороны — обращение к переменным, объявленным на низких уровнях вложенности (а большинство программистов определяет все глобальные переменные на уровне вложенности 0), так же как и вызов процедур, объявленных на низких уровнях вложенности, оказывается достаточно медленным. Многие реализации языков программирования, использующих статические ссылки, помещают переменные, определяемые на уровне 0, не в стек, а в сегмент данных, но тем не менее существует способ, открывающий быстрый доступ к локальным переменным с любых уровней.
Внешние регистры контроллера VGA (03C2h – 03CFh)
Доступ к этим регистрам осуществляется прямым обращением к соответствующим портам ввода-вывода.
Регистр состояния ввода 0 (ISR0) — доступен для чтения из порта 03С2
бит 7: произошло прерывание обратного хода луча IRQ2
бит 6: дополнительное устройство 1 (линия FEAT1)
бит 5: дополнительное устройство 0 (линия FEAT0)
бит 4: монитор присутствует
Регистр вывода (MOR) — доступен для чтения из порта 3CCh и для записи как 3C2h
биты 7 – 6: полярность сигналов развертки — (01, 10, 11) = (350, 400, 480) линий
бит 5: 1/0 — нечетная/четная страница видеопамяти
биты 3 – 2: частота — (00, 01) = (25,175 MHz, 28,322 MHz)
бит 1: 1/0 — доступ CPU к видеопамяти разрешен/запрещен
бит 0: 1/0 — адрес порта контроллера CRT = 03D4h/03B4h
Регистр состояния ввода 1 (ISR1) — доступен для чтения из порта 03DAH
бит 3: происходит вертикальный обратный ход луча
бит 0: происходит любой обратный ход луча
Лучший момент для вывода данных в видеопамять — момент, когда электронный луч двигается от конца экрана к началу и экран не обновляется, то есть вертикальный обратный ход луча. Перед копированием в видеопамять полезно вызывать, например, следующую процедуру:
; процедура wait_retrace ; возвращает управление в начале обратного вертикального хода луча ; wait_retrace proc near push ax push dx mov dx,03DAh ; порт регистра ISR1 wait_retrace_end: in al,dx test al,1000b ; проверить бит 3 ; Если не ноль - jnz wait_retrace_end ; подождать конца ; текущего обратного хода wait_retrace_start: in al,dx test al,1000b ; а теперь подождать ; начала следующего jz wait_retrace_start pop dx pop ax ret wait_retrace endp
Вычисления с фиксированной запятой
Существует широкий класс задач, где требуются вычисления с вещественными числами, но не требуется высокая точность вычислений. Например, в этот класс задач попадают практически все процедуры, оперирующие с координатами и цветами точек в двух- и трехмерном пространстве. Так как в результате все будет выводиться на экран с ограниченным разрешением и каждая компонента цвета будет записываться как 6- или 8-битное целое число, не требуются все те десятки знаков после запятой, которые вычисляет FPU. А раз не требуется высокая точность, можно выполнить вычисление значительно быстрее. Чаще всего для представления вещественных чисел с ограниченной точностью используется формат чисел с фиксированной запятой: целая часть числа представляется в виде обычного целого числа, и дробная часть — точно так же в виде целого числа (как мы записываем небольшие вещественные числа на бумаге).
Наиболее распространенные форматы для чисел с фиксированной запятой — 8:8 и 16:16. В первом случае на целую и на дробную части числа отводится по одному байту, а во втором — по одному слову. Операции с этими двумя форматами можно выполнять, помещая число в регистр (16-битаый — для формата 8:8 и 32-битный — для формата 16:16). Разумеется, можно придумать и использовать совершенно любой формат, например 5:11, но некоторые операции над такими числами могут усложниться.
Вычисления с плавающей запятой
Набор команд для работы с плавающей запятой в процессорах Intel достаточно разнообразен, чтобы реализовывать весьма сложные алгоритмы, и прост в использовании. Единственное, что может представлять определенную сложность, — почти все команды FPU по умолчанию работают с его регистрами данных как со стеком, выполняя операцию над числами в ST(0) и ST(1) и помещая результат в ST(0), так что естественной формой записи математических выражений для FPU оказывается обратная польская нотация (RPN). Эта форма записи встречается в программируемых калькуляторах, языке Форт и почти всегда неявно присутствует во всех алгоритмах анализа математических выражений: они сначала преобразовывают обычные выражения в обратные и только потом начинают их анализ. В обратной польской нотации все операторы указываются после своих аргументов, так что sin(x) превращается в х sin, a а+b превращается в a b +. При этом полностью пропадает необходимость использовать скобки, например: выражение (a+b)*7-d записывается как а b + 7 * d -.
Посмотрим, как выражение, записанное в RPN, легко воплощается при помощи команд FPU на примере процедуры вычисления арксинуса.
; asin ; вычисляет арксинус числа, находящегося в st(0) (-1 <= х <= +1) ; по формуле asin(x) = atan(sqrt(x2/(1-x2))) ; (в RPN: x x * x x * 1 - / sqrt atan) ; результат возвращается в st(0), в стеке FPU должно быть ; два свободных регистра asin proc near ; комментарий показывает содержимое стека FPU: ; первое выражение - ST(0), второе - ST(1) и т.д. ; х (начальное состояние стека) fld st(0) ; х, х fmul ; x2
fld st(0) ; х2, х2
fld1 ; 1, x2, x2
fsubr ; 1-х2, x2
fdiv ; x2/(1-x2) fsqrt ; sqrt(x2/(1-x2)) fld1 ; 1, sqrt(x2/(1-x2)) fpatan ; atan(sqrt(x2/(1-x2))) ret asin endp
Теперь попробуем решить небольшое дифференциальное уравнение — уравнение Ван-дер-Поля для релаксационных колебаний:
х" = -х + m(1-х2)х', m > 0
будем двигаться по времени с малым шагом h, так что
x(t + h) = x(t) + hx(t)' x(t + h)' = x(t)' + hx(t)"
или, сделав замену у = х',
у = у + h(m(1-x2)y - х) х = х + hy
Это уравнение интересно тем, что его решение для всех m > 0 оказывается периодическим аттрактором, так что, если из-за ошибок округления решение отклоняется от истинного в любую сторону, оно тут же возвращается обратно. При m = 0, наоборот, решение оказывается неустойчивым и ошибки округления приводят к очень быстрому росту х и у до максимальных допустимых значений для вещественных чисел.
Эту программу нельзя реализовать в целых числах или числах с фиксированной запятой, потому что значения х и х' различаются на много порядков — кривая содержит почти вертикальные участки, особенно при больших m.
; vdp.asm ; решение уравнения Ван-дер-Поля ; x(t)" = -x(t) + m(1-x(t)2)x(t)' ; с m = 0, 1, 2, 3, 4, 5, 6, 7, 8 ; ; программа выводит на экран решение с m = 1, нажатие клавиш 0 - 8 изменяет m ; Esc - выход, любая другая клавиша - пауза до нажатия одной из Esc, 0 - 8
.model tiny .286 ; для команд pusha и рора .287 ; для команд FPU .code org 100h ; СОМ-программа
start proc near cld push 0A000h pop es ; адрес видеопамяти в ES mov ax,0012h int 10h ; графический режим 640x480x16 finit ; инициализировать FPU xor si, si ; SI будет содержать координату t и меняться ; от 0 до 640 fld1 ; 1 fild word ptr hinv ; 32, 1 fdiv ; h (h = 1/hinv) ; установка начальных значений для _display: ; m = 1, x = h = 1/32, у = х' = 0 again: fild word ptr m ; m, h fld st(1) ; x, m, h (x = h) fldz ; y, x, m, h (y = 0) call _display ; выводить на экран решение, пока ; не будет нажата клавиша g_key: mov ah,10h ; чтение клавиши с ожиданием int 16h ; код нажатой клавиши в AL, cmp al,1Bh ; если это Esc, jz g_out ; выйти из программы, cmp al,'0' ; если код меньше "0", jb g_key ; пауза/ожидание следующей клавиши, cmp al,'8' ; если код больше "8", ja g_key ; пауза/ожидание следующей клавиши, sub al,'0' ; иначе: AL = введенная цифра, mov byte ptr m,al ; m = введенная цифра fstp st(0) ; x, m, h fstp st(0) ; m, h fstp st(0) ; h jmp short again
g_out: mov ах,0003h ; текстовый режим int 10h ret ; конец программы start endp
; процедура _display ; пока не нажата клавиша, выводит решение на экран, ; делая паузу после каждой из 640 точек ; _display proc near dismore: mov bx,0 ; стереть предыдущую точку: цвет = 0 mov cx,si shr cx,1 ; CX - строка mov dx,240 sub dx,word ptr ix[si] ; DX - столбец call putpixel1b call next_x ; вычислить x(t) для следующего t mov bx,1 ; вывести точку: цвет = 1 mov dx,240 sub dx,word ptr ix[si] ; DX - столбец call putpixel1b inc si inc si ; SI = SI + 2 (массив слов), cmp si,640*2 ; если SI достигло конца массива IX, jl not_endscreen ; пропустить паузу sub si,640*2 ; переставить SI на начало массива IX not_endscreen: mov dx,5000 xor cx,cx mov ah,86h int 15h ; пауза на CX:DX микросекунд mov ah,11h int 16h ; проверить, была ли нажата клавиша, jz dismore ; если нет - продолжить вывод на экран, ret ; иначе - закончить процедуру _display endp
; процедура next_x ; проводит вычисления по формулам: ; y = y + h(m(1-x2)y-x) ; х = х + hy ; ввод: st = y, st(1) = х, st(2) = m, st(3) = h ; вывод: st = y, st(1) = x, st(2) = m, st(3) = h, x * 100 записывается в ix[si] next_x proc near fld1 ; 1, y, x, m, h fld st(2) ; x, 1, y, x, m, h fmul st,st(3) ; x2, 1, y, x, m, h fsub ; (1-x2), y, х, m, h fld st(3) ; m, (1-x2), y, x, m, h fmul ; M, y, x, m, h (M = m(1-x2)) fld st(1) ; y, M, y, x, m, h fmul ; My, y, x, m, h fld st(2) ; x, My, y, x, m, h fsub ; My-x, y, x, m, h fld st(4) ; h, My-x, y, x, m, h fmul ; h(My-x), y, x, m, h fld st(1) ; y, h(My-x), y, x, m, h fadd ; Y, y, x, m, h (Y = y + h(My-x)) fxch ; y, Y, x, m, h fld st(4) ; h, y, Y, x, m, h fmul ; yh, Y, x, m, h faddp st(2),st ; Y, X, m, h (X = x + hy) fld st(1) ; X, Y, X, m, h fild word ptr c_100 ; 100, X, Y, X, m, h fmul ; 100X, Y, X, m, h fistp word ptr ix[si] ; Y, X, m, h ret next_x endp
; процедура вывода точки на экран в режиме, использующем 1 бит на пиксель ; DX = строка, СХ = столбец, ES = A000h, ВХ = цвет (1 - белый, 0 - черный) ; все регистры сохраняются
putpixel1b proc near pusha ; сохранить регистры push bx xor bx,bx mov ax,dx ; AX = номер строки imul ax,ax,80 ; AX = номер строки * число байт в строке push cx shr сх,3 ; СХ = номер байта в строке add ax,cx ; АХ = номер байта в видеопамяти mov di,ax ; поместить его в DI и SI mov si,di pop сх ; СХ снова содержит номер столбца mov bx,0080h and cx,07h ; последние три бита СХ = ; остаток от деления на 8 = ; номер бита в байте, считая справа налево shr bx,cl ; теперь в BL установлен в 1 нужный бит lods es:byte ptr ix ; AL = байт из видеопамяти pop dx dec dx ; проверить цвет: js black ; если 1 - or ax,bx ; установить выводимый бит в 1, jmp short white black: not bx ; если 0 - and ax,bx ; установить выводимый цвет в 0 white: stosb ; и вернуть байт на место рора ; восстановить регистры ret ; конец putpixel1b endp
m dw 1 ; начальное значение m с_100 dw 100 ; масштаб по вертикали hinv dw 32 ; начальное значение 1/h ix: ; начало буфера для значений x(t) ; (всего 1280 байт за концом программы) end start
Выгрузка резидентной программы из памяти
Чтобы выгрузить резидентную программу из памяти, необходимо сделать три вещи: закрыть открытые программой файлы и устройства, восстановить все перехваченные векторы прерываний, и наконец, освободить всю занятую программой память. Трудность может вызвать второй шаг, так как после нашего резидента могли быть загружены другие программы, перехватившие те же прерывания. Если в такой ситуации восстановить вектор прерывания в значение, которое он имел до загрузки нашего резидента, программы, загруженные позже, не будут получать управление. Более того, они не будут получать управление только по тем прерываниям, которые у них совпали с прерываниями, перехваченными нашей программой, в то время как другие векторы прерываний будут все еще указывать на их обработчики, что почти наверняка приведет к ошибкам. Поэтому, если хоть один вектор прерывания не указывает на наш обработчик, выгружать резидентную программу нельзя. Это всегда было главным вопросом, и спецификации AMIS и IBM ISP (см. предыдущую главу) являются возможным решением этой проблемы. Если вектор прерывания не указывает на нас, имеет смысл проверить, не указывает ли он на ISP-блок (первые два байта должны быть EBh 10h, а байты 6 и 7 — «K» и «B»), и, если это так, взять в качестве вектора значение из этого блока и т.д. Кроме того, программы могут изменять порядок, в котором обработчики одного и того же прерывания вызывают друг друга.
Последний шаг в выгрузке программы — освобождение памяти — можно выполнить вручную, вызывая функцию DOS 49h на каждый блок памяти, который программа выделяла через функцию 48h, на блок с окружением DOS, если он не освобождался при загрузке, и наконец, на саму программу. Однако есть способ заставить DOS сделать все это (а также закрыть открытые файлы и вернуть код возврата) автоматически, вызвав функцию 4Ch, объявив резидент текущим процессом. Посмотрим, как это делается на примере резидентной программы, занимающей много места в памяти. Кроме того, этот пример реализует все приемы, использующиеся для вызова функций DOS из обработчиков аппаратных прерываний, о которых рассказано в главе 5.8.3.
; scrgrb.asm ; Резидентная программа, сохраняющая изображение с экрана в файл. ; Поддерживается только видеорежим 13h (320x200x256) и только один файл.
; HCI: ; Нажатие Alt-G создает файл scrgrb.bmp в текущем каталоге с изображением, ; находившимся на экране в момент нажатия клавиши. ; Запуск с командной строкой /u выгружает программу из памяти
; API: ; Программа занимает первую свободную функцию прерывания 2Dh (кроме нуля) ; в соответствии со спецификацией AMIS 3.6 ; Поддерживаемые подфункции AMIS: 00h, 02h, 03h, 04h, 05h ; Все обработчики прерываний построены в соответствии с IBM ISP
; Резидентная часть занимает в памяти 1056 байт, если присутствует EMS, ; и 66 160 байт, если EMS не обнаружен
.model tiny .code .186 ; для сдвигов и команд pusha/popa org 2Ch envseg dw ? ; сегментный адрес окружения
org 80h cmd_len db ? ; длина командной строки cmd_line db ? ; командная строка
org 100h ; COM-программа start: jmp initialize ; переход на инициализирующую часть
; Обработчик прерывания 09h (IRQ1)
int09h_handler proc far jmp short actual_int09h_handler ; пропустить ISP old_int09h dd ? dw 424Bh db 00h jmp short hw_reset db 7 dup (0) actual_int09h_handler: ; начало собственно обработчика INT 09h pushf call dword ptr cs:old_int09h ; сначала вызвать старый ; обработчик, чтобы он завершил аппаратное ; прерывание и передал код в буфер pusha ; это аппаратное прерывание - надо push ds ; сохранить все регистры push es push 0040h pop ds ; DS = сегментный адрес области данных BIOS mov di,word ptr ds:001Ah ; адрес головы буфера ; клавиатуры, cmp di,word ptr ds:001Ch ; если он равен адресу ; хвоста, je exit_09h_handler ; буфер пуст, и нам делать нечего,
mov ax,word ptr [di] ; иначе: считать символ, cmp ah,22h ; если это не G (скан-код 22h), jne exit_09h_handler ; выйти
mov al,byte ptr ds:0017h ; байт состояния клавиатуры, test al,08h ; если Alt не нажата, jz exit_09h_handler ; выйти,
mov word ptr ds:001Ch,di ; иначе: установить адреса головы ; и хвоста буфера равными, то есть ; опустошить его call do_grab ; подготовить BMP-файл с изображением mov byte ptr cs:io_needed, 1 ; установить флаг ; требующейся записи на диск cli call safe_check ; проверить, можно ли вызвать DOS, jc exit_09h_handler sti call do_io ; если да - записать файл на диск
exit_09h_handler: pop es pop ds ; восстановить регистры рора iret ; и вернуться в прерванную программу int09h_handler endp
hw_reset: retf
; Обработчик INT 08h (IRQ0)
int08h_handler proc far jmp short actual_int08h_handler ; пропустить ISP old_int08h dd ? dw 424Bh db 00h jmp short hw_reset db 7 dup (0) actual_int08h_handler: ; собственно обработчик pushf call dword ptr cs:old_int08h ; сначала вызвать стандартный ; обработчик, чтобы он завершил ; аппаратное прерывание (пока оно ; не завершено, запись на диске невозможна) pusha push ds cli ; между любой проверкой глобальной переменной ; и принятием решения по ее значению - ; не повторно входимая область, прерывания ; должны быть запрещены cmp byte ptr cs:io_needed,0 ; проверить, je no_io_needed ; нужно ли писать на диск call safe_check ; проверить, jc no_io_needed ; можно ли писать на диск sti ; разрешить прерывания на время записи call do_io ; запись на диск no_io_needed: pop ds рора iret int08h_handler endp
; Обработчик INT 13h ; поддерживает флаг занятости INT 13h, который тоже надо проверять перед ; записью на диск
int13h_handler proc far jmp short actual_int13h_handler ; пропустить ISP old_int13h dd ? dw 424Bh db 00h jmp short hw_reset db 7 dup (0) actual_int13h_handler: ; собственно обработчик pushf inc byte ptr cs:bios_busy ; увеличить счетчик занятости INT 13h cli call dword ptr cs:old_int13h pushf dec byte ptr cs:bios_busy ; уменьшить счетчик popf ret 2 ; имитация команды IRET, не восстанавливающая ; флаги из стека, так как обработчик INT 13h возвращает некоторые ; результаты в регистре флагов, а не в его копии, хранящейся ; в стеке. Он тоже завершается командой ret 2 int13h_handler endp
; Обработчик INT 28h ; вызывается DOS, когда она ожидает ввода с клавиатуры и функциями DOS можно ; пользоваться
int28h_handler proc far jmp short actual_int28h_handler ; пропустить ISP old_int28h dd ? dw 424Вh db 00h jmp short hw_reset db 7 dup (0) actual_int28h_handler: pushf push di push ds push cs pop ds cli cmp byte ptr io_needed,0 ; проверить, je no_io_needed2 ; нужно ли писать на диск lds di,dword ptr in_dos_addr cmp byte ptr [di+1],1 ; проверить, ja no_io_needed2 ; можно ли писать на диск (флаг ; занятости DOS не должен быть больше 1) sti call do_io ; запись на диск no_io_needed2: pop ds pop di popf jmp dword ptr cs:old_int28h ; переход на старый ; обработчик INT 28h int28h_handler endp
; Процедура do_grab ; помещает в буфер палитру и содержимое видеопамяти, формируя BMP-файл. ; Считает, что текущий видеорежим - 13h
do_grab proc near push cs pop ds
call ems_init ; отобразить наш буфер в окно EMS
mov dx,word ptr cs:buffer_seg mov es,dx ; поместить сегмент с буфером в ES и DS mov ds,dx ; для следующих шагов процедуры mov ax,1017h ; Функция 1017h - чтение палитры VGA mov bx,0 ; начиная с регистра палитры 0, mov сх,256 ; все 256 регистров mov dx,BMP_header_length ; начало палитры в BMP int 10h ; видеосервис BIOS
; перевести палитру из формата, в котором ее показывает функция 1017h ; (три байта на цвет, в каждом байте 6 значимых бит), ; в формат, используемый в BMP-файлах ; (4 байта на цвет, в каждом байте 8 значимых бит) std ; движение от конца к началу mov si,BMP_header_length+256*3-1 ; SI- конец 3-байтной палитры mov di,BMP_header_length+256*4-1 ; DI - конец 4-байтной палитры mov сх,256 ; СХ - число цветов adj_pal: mov al,0 stosb ; записать четвертый байт (0) lodsb ; прочитать третий байт shl al,2 ; масштабировать до 8 бит push ax lodsb ; прочитать второй байт shl al,2 ; масштабировать до 8 бит push ax lodsb ; прочитать третий байт shl al,2 ; масштабировать до 8 бит stosb ; и записать эти три байта pop ax ; в обратном порядке stosb pop ax stosb loop adj_pal
; Копирование видеопамяти в BMP. ; В формате BMP строки изображения записываются от последней к первой, так что ; первый байт соответствует нижнему левому пикселю
cld ; движение от начала к концу (по строке) push 0A000h pop ds mov si,320*200 ; DS:SI - начало последней строки на экране mov di,bfoffbits ; ES:DI - начало данных в BMP mov dx,200 ; счетчик строк bmp_write_loop: mov cx,320/2 ; счетчик символов в строке rep movsw ; копировать целыми словами, так быстрее sub si,320*2 ; перевести SI на начало предыдущей строки dec dx ; уменьшить счетчик строк, jnz bmp_write_loop ; если 0 - выйти из цикла call ems_reset ; восстановить состояние EMS ; до вызова do_grab ret do_grab endp
; Процедура do_io ; создает файл и записывает в него содержимое буфера
do_io proc near push cs pop ds mov byte ptr io_needed,0 ; сбросить флаг требующейся ; записи на диск call ems_init ; отобразить в окно EMS наш буфер mov ah,6Ch ; Функция DOS 6Ch mov bx,2 ; доступ - на чтение/запись mov cx,0 ; атрибуты - обычный файл mov dx,12h ; заменять файл, если он существует, ; создавать, если нет mov si,offset filespec ; DS:SI - имя файла int 21h ; создать/открыть файл mov bx,ax ; идентификатор файла - в ВХ
mov ah,40h ; Функция DOS 40h mov cx,bfsize ; размер BMP-файла mov ds,word ptr buffer_seg mov dx,0 ; DS:DX - буфер для файла int 21h ; запись в файл или устройство
mov ah,68h ; сбросить буфера на диск int 21h
mov ah,3Eh ; закрыть файл int 21h call ems_reset ret do_io endp
; Процедура ems_init, ; если буфер расположен в EMS, подготавливает его для чтения/записи ems_init proc near cmp dx,word ptr ems_handle ; если не используется EMS cmp dx,0 ; (EMS-идентификаторы начинаются с 1), je ems_init_exit ; ничего не делать
mov ax,4700h ; Функция EMS 47h int 67h ; сохранить EMS-контекст
mov ax,4100h ; Функция EMS 41h int 67h ; определить адрес окна EMS mov word ptr buffer_seg,bx ; сохранить его
mov ax,4400h ; Функция EMS 44h mov bx,0 ; начиная со страницы 0, int 67h ; отобразить страницы EMS в окно mov ax,4401h inc bx int 67h ; страница 1 mov ax,4402h inc bx int 67h ; страница 2 mov ax,4403h inc bx int 67h ; страница 3 ems_init_exit: ret ems_init endp
; Процедура ems_reset ; восстанавливает состояние EMS
ems_reset proc near mov dx,word ptr cs:ems_handle cmp dx,0 je ems_reset_exit mov ax,4800h ; Функция EMS 48h int 67h ; восстановить EMS-контекст ems_reset_exit: ret ems_reset endp
; Процедура safe_check ; возвращает CF = 0, если в данный момент можно пользоваться функциями DOS, ; и CF = 1, если нельзя
safe_check proc near push es push cs pop ds
les di,dword ptr in_dos_addr ; адрес флагов занятости DOS, cmp word ptr es:[di],0 ; если один из них не 0, pop es jne safe_check_failed ; пользоваться DOS нельзя,
cmp byte ptr bios_busy,0 ; если выполняется прерывание 13h, jne safe_check_failed ; тоже нельзя
clc ; CF = 0 ret safe_check_failed: stc ; CF = 1 ret safe_check endp
in_dos_addr dd ? ; адрес флагов занятости DOS io_needed db 0 ; 1, если надо записать файл на диск bios_busy db 0 ; 1, если выполняется прерывание INT 13h buffer_seg dw 0 ; сегментный адрес буфера для файла ems_handle dw 0 ; идентификатор EMS filespec db 'scrgrb.bmp',0 ; имя файла
; Обработчик INT 2Dh
hw_reset2D: retf
int2Dh_handler proc far jmp short actual_int2Dh_handler ; пропустить ISP old_int2Dh dd ? dw 424Bh db 00h jmp short hw_reset2D db 7 dup (0) actual_int2Dh_handler: ; собственно обработчик db 80h,0FCh ; начало команды CMP АН,число mux_id db ? ; идентификатор программы, je its_us ; если вызывают с чужим АН - это не нас jmp dword ptr cs:old_int2Dh its_us: cmp al,06 ; функции AMIS 06h и выше jae int2D_no ; не поддерживаются cbw ; AX = номер функции mov di,ax ; DI = номер функции shl di,1 ; * 2, так как jumptable - таблица слов jmp word ptr cs:jumptable[di] ; переход на обработчик функции jumptable dw offset int2D_00,offset int2D_no dw offset int2D_02,offset int2D_no dw offset int2D_04,offset int2D_05
int2D_00: ; проверка наличия mov al,0FFh ; этот номер занят mov cx,0100h ; номер версии программы 1.0 push cs pop dx ; DX:DI - адрес AMIS-сигнатуры mov di,offset amis_sign iret int2D_no: ; неподдерживаемая функция mov al,00h ; функция не поддерживается iret unload_failed: ; сюда передается управление, если хоть один из векторов ; прерываний был перехвачен кем-то после нас mov al,01h ; выгрузка программы не удалась iret int2D_02: ; выгрузка программы из памяти cli ; критический участок push 0 pop ds ; DS - сегментный адрес таблицы векторов прерываний mov ax,cs ; наш сегментный адрес ; проверить, все ли перехваченные прерывания по-прежнему указывают на нас, ; обычно достаточно проверить только сегментные адреса (DOS не загрузит другую ; программу с нашим сегментным адресом) cmp ax,word ptr ds:[09h*4+2] jne unload_failed cmp ax,word ptr ds:[13h*4+2] jne unload_failed cmp ax,word ptr ds:[08h*4+2] jne unload_failed cmp ax,word ptr ds:[28h*4+2] jne unload_failed cmp ax,word ptr ds:[2Dh*4+2] jne unload_failed
push bx ; адрес возврата - в стек push dx
; восстановить старые обработчики прерываний mov ax,2509h lds dx,dword ptr cs:old_int09h int 21h mov ax,2513h lds dx,dword ptr cs:old_int13h int 21h mov ax,2508h lds dx,dword ptr cs:old_int08h int 21h mov ax,2528h lds dx,dword ptr cs:old_int28h int 21h mov ax,252Dh lds dx,dword ptr cs:old_int2Dh int 21h mov dx,word ptr cs:ems_handle ; если используется EMS cmp dx,0 je no_ems_to_unhook mov ax,4500h ; функция EMS 45h int 67h ; освободить выделенную память jmp short ems_unhooked no_ems_to_unhook: ems_unhooked:
; собственно выгрузка резидента mov ah,51h ; Функция DOS 51h int 21h ; получить сегментный адрес PSP ; прерванного процесса (в данном случае ; PSP - копии нашей программы, ; запущенной с ключом /u) mov word ptr cs:[16h],bx ; поместить его в поле ; "сегментный адрес предка" в нашем PSP pop dx ; восстановить адрес возврата из стека pop bx mov word ptr cs:[0Ch],dx ; и поместить его в поле mov word ptr cs:[0Ah],bx ; "адрес перехода при ; завершении программы" в нашем PSP pop bx ; BX = наш сегментный адрес PSP mov ah,50h ; Функция DOS 50h int 21h ; установить текущий PSP ; теперь DOS считает наш резидент текущей программой, а scrgrb.com /u - ; вызвавшим его процессом, которому и передаст управление после вызова ; следующей функции mov ax,4CFFh ; Функция DOS 4Ch int 21h ; завершить программу
int2D_04: ; получить список перехваченных прерываний mov dx,cs ; список в DX:BX mov bx,offset amis_hooklist iret int2D_05: ; получить список "горячих" клавиш mov al,0FFh ; функция поддерживается mov dx,cs ; список в DX:BX mov bx,offset amis_hotkeys iret int2Dh_handler endp
; AMIS: сигнатура для резидентной программы amis_sign db "Cubbi..." ; 8 байт db "ScrnGrab" ; 8 байт db "Simple screen grabber using EMS",0
; AMIS: список перехваченных прерываний amis_hooklist db 09h dw offset int09h_handler db 08h dw offset int08h_handler db 28h dw offset int28h_handler db 2Dh dw offset int2Dh_handler ; AMIS: список "горячих" клавиш amis_hotkeys db 1 db 1 db 22h ; скан-код клавиши (G) dw 08h ; требуемые флаги клавиатуры dw 0 db 1
; конец резидентной части ; начало процедуры инициализации
initialize proc near jmp short initialize_entry_point ; пропустить различные варианты выхода без установки резидента, ; помещенные здесь потому, что на них передают управление ; команды условного перехода, имеющие короткий радиус действия
exit_with_message: mov ah,9 ; функция вывода строки на экран int 21h ret ; выход из программы
already_loaded: ; если программа уже загружена в память cmp byte ptr unloading,1 ; если мы не были вызваны с /u je do_unload mov dx,offset already_msg jmp short exit_with_message
no_more_mux: ; если свободный идентификатор INT 2Dh не найден mov dx,offset no_more_mux_msg jmp short exit_with_message
cant_unload1: ; если нельзя выгрузить программу mov dx,offset cant_unload1_msg jmp short exit_with_message
do_unload: ; выгрузка резидента: при передаче управления сюда АН содержит ; идентификатор программы - 1 inc ah mov al,02h ; AMIS-функция выгрузки резидента mov dx,es ; адрес возврата mov bx,offset exit_point ; в DX:BX int 2Dh ; вызов нашего резидента через мультиплексор
push cs ; если управление пришло сюда - ; выгрузка не произошла pop ds mov dx,offset cant_unload2_msg jmp short exit_with_message
exit_point: ; если управление пришло сюда - push cs ; выгрузка произошла pop ds mov dx,offset unloaded_msg push 0 ; чтобы сработала команда RET для выхода jmp short exit_with_message
initialize_entry_point: ; сюда передается управление в самом начале cld cmp byte ptr cmd_line[1],'/' jne not_unload cmp byte ptr cmd_line[2],'u' ; если нас вызвали с /u jne not_unload mov byte ptr unloading,1 ; выгрузить резидент not_unload: mov ah, 9 mov dx,offset usage ; вывод строки с информацией о программе int 21h mov ah,-1 ; сканирование от FFh до 01h more_mux: mov al,00h ; функция AMIS 00h - ; проверка наличия резидента int 2Dh ; мультиплексорное прерывание cmp al,00h ; если идентификатор свободен, jne not_free mov byte ptr mux_id,ah ; вписать его сразу в код обработчика, jmp short next_mux not_free: mov es,dx ; иначе - ES:DI = адрес AMIS-сигнатуры ; вызвавшей программы mov si,offset amis_sign ; DS:SI = адрес нашей сигнатуры mov cx,16 ; сравнить первые 16 байт, repe cmpsb jcxz already_loaded ; если они не совпадают, next_mux: dec ah ; перейти к следующему идентификатору, jnz more_mux ; если это 0
free_mux_found: cmp byte ptr unloading, 1 ; и если нас вызвали для выгрузки, je cant_unload1 ; а мы пришли сюда - программы нет в ; памяти, cmp byte ptr mux_id,0 ; если при этом mux_id все еще 0, je no_more_mux ; идентификаторы кончились
; проверка наличия устройства ЕММХХХХ0 mov dx,offset ems_driver mov ax,3D00h int 21h ; открыть файл/устройство jc no_emmx mov bx,ax mov ax,4400h int 21h ; IOCTL: получить состояние файла/устройства jc no_ems test dx,80h ; если старший бит DX = 0, ЕММХХХХ0 - файл jz no_ems ; выделить память под буфер в EMS mov ax,4100h ; функция EMS 41h int 67h ; получить адрес окна EMS mov bp,bx ; сохранить его пока в ВР mov ax,4300h ; Функция EMS 43h mov bx,4 ; нам надо 4 * 16 Кб int 67h ; выделить EMS-память (идентификатор в DХ), cmp ah,0 ; если произошла ошибка (нехватка памяти?), jnz ems_failed ; не будем пользоваться EMS, mov word ptr ems_handle,dx ; иначе: сохранить идентификатор ; для резидента mov ax,4400h ; Функция 44h - отобразить mov bx,0 ; EMS-страницы в окно int 67h ; страница 0 mov ax,4401h inc bx int 67h ; страница 1 mov ax,4402h inc bx int 67h ; страница 2 mov ax,4403h inc bx int 67h ; страница 3 mov dx,offset ems_msg ; вывести сообщение об установке в EMS jmp short ems_used ems_failed: no_ems: ; если EMS нет или он не работает, mov ah,3Eh int 21h ; закрыть файл/устройство ЕММХХХХ0, no_emmx: ; занять общую память mov ah,9 mov dx,offset conv_msg ; вывод сообщения об этом int 21h mov sp,length_of_program+100h+200h ; перенести стек mov ah,4Ah ; Функция DOS 4Ah
next_segment = length_of_program+100h+200h+0Fh next_segment = next_segment/16 ; такая запись нужна только для ; WASM, остальным ассемблерам это ; можно было записать в одну строчку mov bx,next_segment ; уменьшить занятую память, оставив ; текущую длину нашей программы + 100h ; на PSP +200h на стек int 21h
mov ah,48h ; Функция 48h - выделить память bfsize_p = bfsize+0Fh bfsize_p = bfsize_p/16 mov bx,bfsize_p ; размер BMP-файла 320x200x256 в 16-байтных int 21h ; параграфах
ems_used: mov word ptr buffer_seg,ax ; сохранить адрес буфера для резидента
; скопировать заголовок BMP-файла в начало буфера mov cx,BMP_header_length mov si,offset BMP_header mov di,0 mov es,ax rep movsb
; получить адреса флага занятости DOS и флага критической ошибки (считая, что ; версия DOS старше 3.0) mov ah,34л ; Функция 34h - получить флаг занятости int 21h dec bx ; уменьшить адрес на 1, чтобы он указывал ; на флаг критической ошибки, mov word ptr in_dos_addr,bx mov word ptr in_dos_addr+2,es ; и сохранить его для резидента
; перехват прерываний mov ax,352Dh ; АН = 35h, AL = номер прерывания int 21h ; получить адрес обработчика INT 2Dh mov word ptr old_int2Dh,bx ; и поместить его в old_int2Dh mov word ptr old_int2Dh+2,es mov ax,3528h ; AH = 35h, AL = номер прерывания int 21h ; получить адрес обработчика INT 28h mov word ptr old_int28h,bx ; и поместить его в old_int28h mov word ptr old_int28h+2,es mov ax,3508h ; AH = 35h, AL = номер прерывания int 21h ; получить адрес обработчика INT 08h mov word ptr old_int08h,bx ; и поместить его в old_int08h mov word ptr old_int08h+2,es mov ax,3513h ; AH = 35h, AL = номер прерывания int 21h ; получить адрес обработчика INT 13h mov word ptr old_int13h,bx ; и поместить его в old_int13h mov word ptr old_int13h+2,es mov ax,3509h ; AH = 35h, AL = номер прерывания int 21h ; получить адрес обработчика INT 09h mov word ptr old_int09h,bx ; и поместить его в old_int09h mov word ptr old_int09h+2,es mov ax,252Dh ; AH = 25h, AL = номер прерывания mov dx,offset int2Dh_handler ; DS:DX - адрес обработчика int 21h ; установить новый обработчик INT 2Dh mov ax,2528h ; AH = 25h, AL = номер прерывания mov dx,offset int28h_handler ; DS:DX - адрес обработчика int 21h ; установить новый обработчик INT 28h mov ax,2508h ; AH = 25h, AL = номер прерывания mov dx,offset int08h_handler ; DS:DX - адрес обработчика int 21h ; установить новый обработчик INT 08h mov ax,2513h ; AH = 25h, AL = номер прерывания mov dx,offset int13h_handler ; DS:DX - адрес обработчика int 21h ; установить новый обработчик INT 13h mov ax,2509h ; AH = 25h, AL = номер прерывания mov dx,offset int09h_handler ; DS:DX - адрес обработчика int 21h ; установить новый обработчик INT 09h
; освободить память из-под окружения DOS mov ah,49h ; Функция DOS 49h mov es,word ptr envseg ; ES = сегментный адрес окружения DOS int 21h ; освободить память
; оставить программу резидентной mov dx,offset initialize ; DX - адрес первого байта за концом ; резидентной части int 27h ; завершить выполнение, оставшись ; резидентом initialize endp
ems_driver db 'EMMXXXX0',0 ; имя EMS-драйвера для проверки
; текст, который выдает программа при запуске: usage db 'Простая программа для копирования экрана только из' db ' видеорежима 13h',0Dh,0Ah db ' Alt-G - записать копию экрана в scrgrb.bmp' db 0Dh,0Ah db ' scrgrb.com /u - выгрузиться из памяти',0Dh,0Ah db '$'
; тексты, которые выдает программа при успешном выполнении: ems_msg db 'Загружена в EMS',0Dh,0Ah,'$' conv_msg db 'He загружена в EMS',0Dh,0Ah,'$' unloaded_msg db 'Программа успешно выгружена из памяти',0Dh,0Ah,'$'
; тексты, которые выдает программа при ошибках: already_msg db 'Ошибка: Программа уже загружена',0Dh,0Ah,'$' no_more_mux_msg db 'Ошибка: Слишком много резидентных программ' db 0Dh,0Ah,'$' cant_unload1_msg db 'Ошибка: Программа не обнаружена в памяти',0Dh,0Ah,'$' cant_unload2_msg db 'Ошибка: Другая программа перехватила прерывания' db 0Dh,0Ah,'$' unloading db 0 ; 1, если нас запустили с ключом /u
; BMP-файл (для изображения 320x200x256) BMP_header label byte ; файловый заголовок BMP_file_header db "BM" ; сигнатура dd bfsize ; размер файла dw 0,0 ; 0 dd bfoffbits ; адрес начала BMP_data ; информационный заголовок BMP_info_header dd bi_size ; размер BMP_info_header dd 320 ; ширина dd 200 ; высота dw 1 ; число цветовых плоскостей dw 8 ; число бит на пиксель dd 0 ; метод сжатия данных dd 320*200 ; размер данных dd 0B13h ; разрешение по X (пиксель на метр) dd 0B13h ; разрешение по Y (пиксель на метр) dd 0 ; число используемых цветов (0 - все) dd 0 ; число важных цветов (0 - все) bi_size = $-BMP_info_header ; размер BMP_info_header BMP_header_length = $-BMP_header ; размер обоих заголовков bfoffbits = $-BMP_file_header+256*4 ; размер заголовков + размер палитры bfsize = $-BMP_file_header+256*4+320*200 ; размер заголовков + ; размер палитры + размер данных length_of_program = $-start end start
В этом примере, достаточно сложном из-за необходимости избегать всех возможностей повторного вызова прерываний DOS и BIOS, добавилась еще одна мера предосторожности — сохранение состояния EMS-памяти перед работой с ней и восстановление в исходное состояние. Действительно, если наш резидент активируется в тот момент, когда какая-то программа работает с EMS, и не выполнит это требование, программа будет читать/писать уже не в свои EMS-страницы, а в наши. Аналогичные предосторожности следует предпринимать всякий раз, когда вызываются функции, затрагивающие какие-нибудь глобальные структуры данных. Например: функции поиска файлов используют буфер DTA, адрес которого надо сохранить (функция DOS 2Fh), затем создать собственный (функция DOS 1Ah) и в конце восстановить DTA прерванного процесса по сохраненному адресу (функция 1Ah). Таким образом надо сохранять/восстанавливать состояние адресной линии А20 (функции XMS 07h и 03h), если резидентная программа хранит часть своих данных или кода в области HMA, сохранять состояние драйвера мыши (INT 33h, функции 17h и 18h), сохранять информацию о последней ошибке DOS (функции DOS 59h и 5D0Ah), и так с каждым ресурсом, который затрагивает резидентная программа. Писать полноценные резидентные программы в DOS сложнее всего, но, если не выходить за рамки реального режима, это — самое эффективное средство управления системой и реализации всего, что только можно реализовать в DOS.
Взаимодействие между процессами
Из того, что DOS является однозадачной операционной системой, вовсе не следует, что в ней не могут существовать одновременно несколько процессов. Это только означает, что сама система не будет предоставлять никаких специальных возможностей для их одновременного выполнения, кроме возможности оставлять программы резидентными в памяти. Так, чтобы организовать общую память для нескольких процессов, надо загрузить пассивную резидентную программу, которая будет поддерживать функции выделения блока памяти (возвращающая идентификатор), определения адреса блока (по его идентификатору) и освобождения блока — приблизительно так же, как работают драйверы EMS или XMS.
Чтобы реализовать многозадачность, придется запустить активную резидентную программу, которая перехватит прерывание IRQ0 и по каждому такту системного таймера будет по очереди отбирать управление от каждого из запущенных процессов и передавать следующему. Практически никто не реализует полноценную многозадачность в DOS, когда каждый процесс имеет собственную память и не может обращаться к памяти другого процесса, — для этого существует защищенный режим, но встречаются довольно простые реализации для облегченного варианта многозадачности — переключение нитей.
Нить — это процесс, который использует тот же код и те же данные, что и остальные такие же процессы в системе, но отличается от них содержимым стека и регистров. Тогда резидентная программа-диспетчер по каждому прерыванию таймера будет сохранять регистры прерванной нити в ее структуру, считывать регистры следующей нити в очереди и возвращать управление, а структуры и стеки всех нитей будут храниться в какой-нибудь специально выделенной общедоступной области памяти. Указанная программа также должна поддерживать несколько вызовов при помощи какого-нибудь программного прерывания — создание нити, удаление нити и, например, передача управления следующей нити, пока текущая нить находится в состоянии ожидания.
Эта простота оборачивается сложностью написания самих нитей, так как все они используют общий код, абсолютно все в коде нити должно быть повторно входимым. Кроме того, нити создают множество проблем, связанных с синхронизацией, приводящих к тому, что либо в коде всех нитей, либо в основном резиденте придется реализовывать семафоры, очереди, сигналы, барьеры и все остальные структуры, которые встречаются в реальных пакетах для работы с нитями.
Попробуем сделать простой прототип такой многозадачности в DOS (всего с двумя нитями) и посмотрим, со сколькими проблемами придется столкнуться.
; scrsvr.asm ; Пример простой задачи, реализующей нитевую многозадачность в DOS. ; Изображает на экране две змейки, двигающиеся случайным образом, каждой из ; которых управляет своя нить. ; ; Передача управления между нитями не работает в окне DOS (Windows 95)
.model tiny .code .386 ; ГСЧ использует 32-битные регистры org 100h ; СОМ-программа start: mov ax,13h ; видеорежим 13h int 10h ; 320x200x256 call init_threads ; инициализировать наш диспетчер ; с этого места и до вызова shutdown_threads исполняются две нити с одним и тем ; же кодом и данными, но с разными регистрами и стеками ; (в реальной системе здесь был бы вызов fork или аналогичной функции)
mov bx,1 ; цвет (синий) push bp mov bp,sp ; поместить все локальные переменные в стек, ; чтобы обеспечить повторную входимость push 1 ; добавка к X на каждом шаге x_inc equ word ptr [bp-2] push 0 ; добавка к Y на каждом шаге y_inc equ word ptr [bp-4] push 128-4 ; относительный адрес головы буфера line_coords coords_head equ word ptr [bp-6] push 0 ; относительный адрес хвоста буфера line_coords coords_tail equ word ptr [bp-8] sub sp,64*2 ; line_coords - кольцевой буфер координат точек mov di,sp mov cx,64 mov ax,10 ; заполнить его координатами (10, 10) push ds pop es rep stosw line_coords equ word ptr [bp-(64*2)-8]
push 0A000h pop es ; ES - адрес видеопамяти
main_loop: ; основной цикл call display_line ; изобразить текущее состояние змейки
; изменить направление движения случайным образом push bx mov ebx,50 ; вероятность смены направления 2/50 call z_random ; получить случайное число от 0 до 49 mov ax,word ptr x_inc mov bx,word ptr y_inc test dx,dx ; если это число - 0, jz rot_right ; повернем направо, dec dx ; а если 1 - jnz exit_rot ; налево
; повороты neg ax ; налево на 90 градусов xchg ax,bx ; dY = -dX, dX = dY jmp short exit_rot rot_right: neg bx ; направо на 90 градусов xchg ax,bx ; dY = dX, dX = dY exit_rot: mov word ptr x_inc,ax ; записать новые значения инкрементов mov word ptr y_inc,bx pop bx ; восстановить цвет в ВХ
; перемещение змейки на одну позицию вперед mov di,word ptr coords_head ; DI - адрес головы mov cx,word ptr line_coords[di] ; СХ-строка mov dx,word ptr line_coords[di+2] ; DX-столбец add cx,word ptr y_inc ; добавить инкременты add dx,word ptr x_inc add di,4 ; DI - следующая точка в буфере, and di,127 ; если DI > 128, DI = DI - 128 mov word ptr coords_head,di ; теперь голова здесь mov word ptr line_coords[di],cx ; записать ее координаты mov word ptr line_coords[di+2],dx mov di,word ptr coords_tail ; прочитать адрес хвоста add di,4 ; переместить его на одну and di,127 ; позицию вперед mov word ptr coords_tail,di ; и записать на место
; пауза, ; из-за особенностей нашего диспетчера (см. ниже) мы не можем пользоваться ; прерыванием BIOS для паузы, поэтому сделаем просто пустой цикл. Длину цикла ; придется изменить в зависимости от скорости процессора mov cx,-1 loop $ ; 65 535 команд loop mov cx,-1 loop $ mov cx,-1 loop $ mov ah,1 int 16h ; если не было нажато никакой клавиши, jz main_loop ; продолжить основной цикл, mov ah,0 ; иначе - прочитать клавишу int 16h leave ; освободить стек от локальных переменных call shutdown_threads ; выключить многозадачность ; с этого момента у нас снова только один процесс mov ах,3 ; видеорежим 3 int 10h ; 80x24 int 20h ; конец программы
; процедура вывода точки на экран в режиме 13h ; СХ = строка, DX = столбец, BL = цвет, ES = A000h putpixel proc near push di lea ecx,[ecx*4+ecx] ; CX = строка * 5 shl cx,6 ; CX = строка * 5 * 64 = строка * 320 add dx,cx ; DX = строка * 320 + столбец = адрес mov di,dx mov al,bl stosb ; записать байт в видеопамять pop di ret putpixel endp
; процедура display_line ; выводит на экран нашу змейку по координатам из кольцевого буфера line_coords display_line proc near mov di,word ptr coords_tail ; начать вывод с хвоста, continue_line_display: cmp di,word ptr coords_head ; если DI равен адресу головы, je line_displayed ; вывод закончился, call display_point ; иначе - вывести точку на экран, add di,4 ; установить DI на следующую точку and di,127 jmp short continue_line_display ; и так далее line_displayed: call display_point mov di,word ptr coords_tail ; вывести точку в хвосте push bx mov bx,0 ; нулевым цветом, call display_point ; то есть стереть pop bx ret display_line endp
; процедура display_point ; выводит точку из буфера line_coords с индексом DI display_point proc near mov cx,word ptr line_coords[di] ; строка mov dx,word ptr line_coords[di+2] ; столбец call putpixel ; вывод точки ret display_point endp
; процедура z_random ; стандартный конгруэнтный генератор случайных чисел (неоптимизированный) ; ввод: ЕВХ - максимальное число ; вывод: EDX - число от 0 до ЕВХ-1 z_random: push ebx cmp byte ptr zr_init_flag,0 ; если еще не вызывали, je zr_init ; инициализироваться, mov eax,zr_prev_rand ; иначе - умножить предыдущее zr_cont: mul rnd_number ; на множитель div rnd_number2 ; и разделить на делитель, mov zr_prev_rand,edx ; остаток от деления - новое число pop ebx mov eax,edx xor edx,edx div ebx ; разделить его на максимальное ret ; и вернуть остаток в EDX zr_init: push 0040h ; инициализация генератора pop fs ; 0040h:006Ch - mov eax,fs:[006Ch] ; счетчик прерываний таймера BIOS, mov zr_prev_rand,eax ; он и будет первым случайным числом mov byte ptr zr_init_flag,1 jmp zr_cont rnd_number dd 16807 ; множитель rnd_number2 dd 2147483647 ; делитель zr_init_flag db 0 ; флаг инициализации генератора zr_prev_rand dd 0 ; предыдущее случайное число
; здесь начинается код диспетчера, обеспечивающего многозадачность
; структура данных, в которой мы храним регистры для каждой нити thread_struc struc _ах dw ? _bx dw ? _cx dw ? _dx dw ? _si dw ? _di dw ? _bp dw ? _sp dw ? _ip dw ? _flags dw ? thread_struc ends
; процедура init_threads ; инициализирует обработчик прерывания 08h и заполняет структуры, описывающие ; обе нити init_threads proc near pushf pusha push es mov ax,3508h ; AH = 35h, AL = номер прерывания int 21h ; определить адрес обработчика, mov word ptr old_int08h,bx ; сохранить его mov word ptr old_int08h+2,es mov ax,2508h ; AH = 25h, AL = номер прерывания mov dx,offset int08h_handler ; установить наш int 21h pop es popa ; теперь регистры те же, что и при вызове процедуры popf
mov thread1._ax,ax ; заполнить структуры mov thread2._ax,ax ; threadl и thread2, mov thread1._bx,bx ; в которых хранится содержимое mov thread2._bx,bx ; всех регистров (кроме сегментных - mov thread1._cx,cx ; они в этом примере не изменяются) mov thread2._cx,cx mov thread1._dx,dx mov thread2._dx.dx mov thread1._si,si mov thread2._si,si mov thread1._di,di mov thread2._di,di mov thread1._bp,bp mov thread2._bp,bp mov thread1._sp,offset thread1_stack+512 mov thread2._sp,offset thread2_stack+512 pop ax ; адрес возврата (теперь стек пуст) mov thread1._ip,ax mov thread2._ip,ax pushf pop ax ; флаги mov thread1._flags,ax mov thread2._flags,ax mov sp,thread1._sp ; установить стек нити 1 jmp word ptr thread1._ip ; и передать ей управление init_threads endp
current_thread db 1 ; номер текущей нити
; Обработчик прерывания INT08h (IRQ0) переключает нити int08h_handler proc far pushf ; сначала вызвать старый обработчик db 9Ah ; код команды call far old_int08h dd 0 ; адрес старого обработчика ; Определить, произошло ли прерывание в момент исполнения нашей нити или ; какого-то обработчика другого прерывания. Это важно, так как мы не собираемся ; возвращать управление тому, кого прервал таймер, по крайней мере сейчас. ; Именно поэтому нельзя пользоваться прерываниями для задержек в наших нитях и ; поэтому программа не работает в окне DOS (Windows 95) mov save_di,bp ; сохранить ВР mov bp,sp push ax push bx pushf mov ax,word ptr [bp+2] ; прочитать сегментную часть mov bx,cs ; обратного адреса, cmp ax,bx ; сравнить ее с CS, jne called_far ; если они не совпадают - выйти, popf pop bx ; иначе - восстановить регистры pop ax mov bp,save_di mov save_di,di ; сохранить DI, SI mov save_si,si pushf ; и флаги ; определить, с какой нити на какую надо передать управление, cmp byte ptr current_thread,1 ; если с первой, je thread1_to_thread2 ; перейти на thread1_to_thread2, mov byte ptr current_thread,1 ; если с 2 на 1, записать ; в номер 1 mov si,offset thread1 ; и установить SI и DI mov di,offset thread2 ; на соответствующие структуры, jmp short order_selected thread1_to_thread2: ; если с 1 на 2, mov byte ptr current_thread,2 ; записать в номер нити 2 mov si,offset thread2 ; и установить SI и DI mov di,offset thread1 order_selected: ; записать все текущие регистры в структуру по адресу [DI] ; и загрузить все регистры из структуры по адресу [SI] ; начать с SI и DI: mov ax,[si]._si ; для MASM все выражения [reg]._reg надо push save_si ; заменить на (thread_struc ptr [reg])._reg pop [di]._si mov save_si,ax mov ax,[si]._di push save_di pop [di]._di mov save_di,ax ; теперь все основные регистры mov [di._ax],ax mov ax,[si._ax] mov [di._bx],bx mov bx,[si._bx] mov [di._cx],cx mov cx,[si._cx] mov [di._dx],dx mov dx,[si._dx] mov [di._bp],bp mov bp,[si._bp] ; флаги pop [di._flags] push [si._flags] popf ; адрес возврата pop [di._ip] ; адрес возврата из стека add sp,4 ; CS и флаги из стека - теперь он пуст ; переключить стеки mov [di._sp],sp mov sp,[si._sp] push [si._ip] ; адрес возврата в стек (уже новый) mov di,save_di ; загрузить DI и SI mov si,save_si retn ; и перейти по адресу в стеке ; управление переходит сюда, если прерывание произошло в чужом коде called_far: popf ; восстановить регистры pop bx pop ax mov bp,save_di iret ; и завершить обработчик int08h_handler endp
save_di dw ? ; переменные для временного хранения save_si dw ? ; регистров
; процедура shutdown_threads ; выключает диспетчер shutdown_threads proc near mov ax,2508h ; достаточно просто восстановить прерывание lds dx,dword ptr old_int08h int 21h ret shutdown_threads endp
; структура, описывающая первую нить thread1 thread_struc <> ; и вторую, thread2 thread_struc <> ; стек первой нити thread1_stack db 512 dup(?) ; и второй thread2_stack db 512 dup(?) end start
Как мы видим, этот пример не может работать в Windows 95 и в некоторых других случаях, когда DOS расширяют до более совершенной операционной системы. Фактически в этом примере мы именно этим и занимались — реализовывали фрагмент операционной системы, который отсутствует в DOS.
Действительно, используя механизм обработчиков прерываний, можно создать операционную систему для реального режима, аналогичную DOS, но очень быстро окажется, что для этого придется общаться напрямую с аппаратным обеспечением компьютера, то есть использовать порты ввода-вывода.
Звуковые платы
Звуковые платы, совместимые с Sound Blaster, поддерживают стандартный интерфейс для общения с компьютером через порты 220h– 22Fh для воспроизведения оцифрованного звука. Кроме того, большинство звуковых плат поддерживает порты 0388h – 038Fh для совместимости с Adlib — одной из первых звуковых плат, в которой не было возможности вывода оцифрованного звука, а присутствовал только частотный синтез. Возможности частотного синтеза значительно расширились со времени появления Adlib, но речь не о ней, потому что средства MIDI-интерфейса позволяют получать более качественную музыку, a Sound Blaster — звук.
Базовый порт для звуковой платы может не быть равен 220h, в этом случае все следующие адреса надо изменить:
порт 220h для чтения: состояние левого канала FM
порт 220h для записи: индексный регистр левого канала FM
порт 221h: регистр данных левого канала FM
порт 222h для чтения: состояние правого канала FM
порт 222h для записи: индексный регистр правого канала FM
порт 223h: регистр данных правого канала FM
порт 224h для записи: индексный регистр микшера
порт 225h: регистр данных микшера
порт 226h: сброс и инициализация DSP
порт 228h для чтения: состояние FM
порт 228h для записи: индексный регистр FM
порт 229h для записи: регистр данных FM
порт 22Ah для чтения: чтение данных из DSP
порт 22Ch для записи: вывод данных/команд DSP
порт 22Ch для чтения: состояние буфера записи DSP (бит 7)
порт 22Eh для чтения: состояние буфера чтения DSP (бит 7)
Программирование современных звуковых плат — весьма сложное занятие, поэтому в качестве примера рассмотрим одну часто применяемую операцию — воспроизведение оцифрованного звука. С этой целью потребуется программировать только DSP, для которого и команды, и данные (которые фактически являются аргументами команд) посылают в один и тот же порт 22Ch. Кроме того, стандартный DSP вызывает одно аппаратное прерывание (чаще всего IRQ5) и использует один канал DMA (чаще всего канал 1). DMA — конструкция, позволяющая внешним устройствам работать с памятью компьютера без вмешательства центрального процессора, рассмотрена в следующей главе, а здесь остановимся на основных командах DSP и попробуем воспроизвести звук без использования DMA и IRQ.