Что потребуется для работы с ассемблером
Биты, байты и слова
Минимальная единица информации называется битом. Бит может принимать только два значения - обычно 0 и 1. На самом деле эти значения совершенно необязательны - один бит может принимать значения "да" и "нет", показывать присутствие и отсутствие жесткого диска, является ли персонаж игры магом или воином - важно лишь то, что бит имеет только два значения. Но далеко не все величины принимают только два значения, а значит, для их описания нельзя обойтись одним битом.
Рис. 1. Байт
Единица информации размером восемь бит называется байтом. Байт - это минимальный объем данных, который реально может использовать компьютерная программа. Даже чтобы изменить значение одного бита в памяти, надо сначала считать байт, содержащий его. Биты в байте нумеруют справа налево, от нуля до семи, нулевой бит часто называют младшим битом, а седьмой - старшим.
Так как всего в байте восемь бит, байт может принимать до 28 = 256 разных значений. Байт используют для представления целых чисел от 0 до 255 (тип unsigned char в С), целых чисел со знаком от -128 до +127 (тип signed char в С), набора символов ASCII (тип char в С) или переменных, принимающих менее 256 значений, например для представления десятичных чисел от 0 до 99. Следующий по размеру базовый тип данных - слово. Размер одного слова в процессорах Intel - два байта.
Рис. 2. Слово
Биты с 0 по 7 составляют младший байт слова, а биты с 8 по 15 - старший. В слове содержится 16 бит, а значит, оно может принимать до 216 = 65 536 разных значений. Слова используют для представления целых чисел без знака со значениями 0 - 65 535 (тип unsigned short в С), целых чисел со знаком со значениями от -32 768 до +32 767 (тип short int в С), адресов сегментов и смещений при 16-битной адресации. Два слова подряд образуют двойное слово, состоящее из 32 бит, а два двойных слова составляют одно учетверенное слово (64 бита). Байты, слова и двойные слова - основные типы данных, с которыми мы будем работать.
Числа со знаком
Легко использовать байты, слова или двойные слова для представления целых положительных чисел - от 0 до 255, 65 535 или 4 294 967 295 соответственно. Чтобы использовать те же самые байты или слова для представления отрицательных чисел, существует специальная операция, известная как дополнение до двух. Для изменения знака числа выполняют инверсию, то есть заменяют в двоичном представлении числа все единицы нулями и нули единицами, а затем прибавляют 1. Например, пусть используются переменные типа слова:
150 = 0096h = 0000 0000 1001 0110b инверсия дает: 1111 1111 0110 1001b +1 = 1111 1111 0110 1010b = 0FF6Ah
Проверим, что полученное число на самом деле -150: сумма с +150 должна, быть равна нулю:
+150 + (-150) = 0096h + FF6Ah = 10000h ;
Единица в l6-м разряде не помещается в слово, и значит, мы действительно получили 0. В этом формате старший (7-й, 15-й, 31-й для байта, слова, двойного слова соответственно) бит всегда соответствует знаку числа 0 - для положительных и 1 - для отрицательных. Таким образом, схема с использованием дополнения до двух выделяет для положительных и отрицательных чисел равные диапазоны: -128 - +127 - для байта, -32 768 - +32 767 - для слов, -2 147 483 648 - +2 147 483 647 - для двойных слов.
Что потребуется для работы с ассемблером
Прежде всего вам потребуется ассемблер. Здесь самое время сказать, что на самом деле язык программирования, которым мы собираемся заниматься, называется "язык ассемблера" (assembly language). Ассемблер - это программа, которая переводит текст с языка, понятного человеку, в язык, понятный процессору, то есть говорят, что она переводит язык ассемблера в машинный код. Однако сначала в повседневной речи, а затем и в литературе слово "ассемблер" стало также и названием самого языка программирования. Понятно, что, когда говорят "программа на ассемблере", имеют в виду язык, а когда говорят "макроассемблер версии 6.13", имеют в виду программу. Вместе с ассемблером обязательно должна быть еще одна программа - компоновщик (linker), которая и создает исполнимые файлы из одного или нескольких объектных модулей, полученных после запуска ассемблера. Помимо этого для разных целей могут потребоваться дополнительные вспомогательные программы - компиляторы ресурсов, расширители DOS и тому подобное (см. табл. 1).
|
Microsoft |
Borland |
Watcom |
| DOS, 16 бит |
masm или ml, link (16 бит) |
tasm tlink |
wasm wlink |
| DOS, 32 бита |
masm или ml, link (32 бита) и dosx link (16 бит) и dos32 |
tasm tlink wdosx или dos32 |
wasm wlink dos4gw, pmodew, zrdx или wdosx |
| Windows EXE |
masm386 или ml, link (32 бита) rc |
tasm tlink32 brcc32 |
wasm wlink wrc |
| Windows DLL |
masm386 или ml, link (32 бита) |
tasm tlink32 implib |
wasm wlink wlib |
Таблица 1. Ассемблеры и сопутствующие программы
Трудно говорить о том, продукция какой из этих трех компаний однозначно лучше. С точки зрения удобства компиляции TASM лучше подходит для создания 16-битных программ для DOS, WASM - для 32-битных программ для DOS, MASM - для Windows. С точки зрения удобства программирования развитость языковых средств растет в ряду WASM-MASM-TASM. Все примеры программ в этой книге построены так, что можно использовать любой из этих компиляторов.
Разумеется, существуют и другие компиляторы, например бесплатно распространяемый в сети Internet NASM или условно бесплатный А86, но пользоваться ими проще, если вы уже знаете турбо- или макроассемблер. Бесплатно распространяемый GNU ассемблер, gas, вообще использует совершенно непохожий синтаксис, который будет рассмотрен в главе 11, рассказывающей о программировании для UNIX.
Во всех программах встречаются ошибки. Если вы собираетесь не только попробовать примеры из книги, но и написать что-то свое, то вам рано или поздно обязательно потребуется отладчик. Кроме поиска ошибок отладчики иногда применяют и для того, чтобы исследовать работу существующих программ. Безусловно, самый мощный отладчик на сегодняшний день - SoftICE от NuMega Software. Это фактически единственный отладчик для Windows 95/NT, позволяющий исследовать все - от ядра Windows до программ на С++, поддерживающий одновременно 16- и 32-битный код и многое другое. Другие популярные отладчики, распространяемые вместе с соответствующими ассемблерами, - Codeview (MS), Turbo Debugger (Borland) и Watcom Debugger (Watcom).
Еще одна особенность ассемблера, отличающая его от всех остальных языков программирования, - возможность дизассемблирования. То есть, имея исполнимый файл, с помощью специальной программы (дизассемблера) почти всегда можно получить исходный текст на ассемблере. Например, можно дизассемблировать BIOS вашего компьютера и узнать, как выполняется переключение видеорежимов, или драйвер для DOS, чтобы написать такой же для Windows. Дизассемблер не необходим, но иногда оказывается удобно иметь его под рукой. Лучшие дизассемблеры на сегодняшний день - Sourcer от V Communications и IDA.
И наконец, последняя необязательная, но крайне полезная утилита - шестнадцатеричный редактор. Многие такие редакторы (hiew, proview, Iview, hexit) тоже имеют встроенный дизассемблер, так что можно, например, открыв в таком редакторе свою программу, посмотреть, как скомпилировался тот или иной участок программы, поправить какую-нибудь команду ассемблера или изменить значения констант и тут же, без перекомпиляции, запустить программу, чтобы посмотреть на результат изменений.
Двоичная система счисления
Практически все существующие сейчас компьютерные системы, включая Intel, используют для всех вычислений двоичную систему счисления. В их электрических цепях напряжение может принимать два значения, и эти значения назвали нулем и единицей. Двоичная система счисления как раз и использует только эти две цифры, а вместо степеней десяти, как в обычной десятичной системе, здесь используют степени двойки. Чтобы перевести двоичное число в десятичное, надо сложить двойки в степенях, соответствующих позициям, где в двоичном стоят единицы. Например:
10010110b = 1*27+0*26+0*25+1*24+0*23+1*22+1*21+0*20 =
= 128+16+4+2 = 150
Чтобы перевести десятичное число в двоичное, можно, например, просто делить его на 2, записывая 0 каждый раз, когда число делится на два, и 1, когда не делится (табл. 2).
|
Остаток |
Разряд |
150/2 = 75 75/2 = 37 37/2 = 18 18/2 = 9 9/2 = 4 4/2 = 2 2/2 = 1 1/2 = 0 |
0 1 1 0 1 0 0 1 |
0 1 2 3 4 5 6 7 |
| Результат: 10010110b |
Таблица 2. Перевод числа из десятичной системы в двоичную
Чтобы отличать двоичные числа от десятичных, в ассемблерных программах в конце каждого двоичного числа ставят букву "b".
Коды символов
Для представления всех букв, цифр и знаков, появляющихся на экране компьютера, обычно используется всего один байт. Символы, соответствующие значениям от 0 до 127, то есть первой половине всех возможных значений байта, были стандартизованы и названы символами ASCII (хотя часто кодами ASCII называют всю таблицу из 256 символов). Сюда входят некоторые управляющие коды (символ с кодом 0Dh - конец строки), знаки препинания, цифры (символы с кодами 30h – 39h), большие (41h – 5Ah) и маленькие (61h – 7Ah) латинские буквы. Вторая половина символьных кодов используется для алфавитов других языков и псевдографики, и набор и порядок символов в ней различаются в разных странах и даже в пределах одной страны. Например, для букв одного только русского языка существует пять разных вариантов размещения во второй половине таблицы символов ASCII. Эти таблицы символов приведены в приложении 1. Существует также стандарт, использующий слова для хранения кодов символов, известный как UNICODE или UCS-2, и даже двойные слова (UCS-4), но мы пока не будем на нем останавливаться.
Логические операции
Один из широко распространенных вариантов значений, которые может принимать один бит, - это значения "правда" и "ложь", используемые в логике, откуда происходят так называемые "логические операции" над битами. Так, если объединить "правду" и "правду" - получится "правда", а если объединить "правду" и "ложь" - "правды" не получится. В ассемблере нам встретятся четыре основные операции - И (AND), ИЛИ (OR), исключающее ИЛИ (XOR) и отрицание (NOT), действие которых приводится в таблице 4.
| И |
ИЛИ |
ИСКЛЮЧАЮЩЕЕ ИЛИ |
НЕ |
0 AND 0 = 0 0 AND 1 = 0 1 AND 0 = 0 1 AND 1 = 1 |
0 OR 0 = 0 0 OR 1 = 1 1 OR 0 = 1 1 OR 1 = 1 |
0 XOR 0 = 0 0 XOR 1 = 1 1 XOR 0 = 1 1 XOR 1 = 0 |
NOT 0 = 1 NOT 1 = 0 |
Таблица 4. Логические операции
Все эти операции побитовые, поэтому, чтобы выполнить логическую операцию над числом, надо перевести его в двоичный формат и выполнить операцию над каждым битом, например:
96h AND 0Fh = 10010110b AND 00001111b = 00000110b = 06h
Организация памяти
Память с точки зрения процессора представляет собой последовательность байт, каждому из которых присвоен уникальный адрес. Он может принимать значения от 0 до 232-1 (4 гигабайта). Программы же могут работать с памятью как с одним непрерывным массивом (модель памяти flat) или как с несколькими массивами (сегментированные модели памяти). Во втором случае для задания адреса любого байта требуется два числа - адрес начала массива и адрес искомого байта внутри массива. Помимо основной памяти программы могут использовать регистры - специальные ячейки памяти, расположенные физически внутри процессора, доступ к которым осуществляется не по адресам, а по именам. Но здесь мы вплотную подходим к рассмотрению собственно работы процессора, подробнее о чем - в следующей главе.
Представление данных в компьютерах
Для того чтобы освоить программирование на ассемблере, неизбежно приходится знакомиться с двоичными и шестнадцатеричными числами. В некоторых случаях в тексте программы можно обойтись и обычными десятичными числами, но без понимания того, как на самом деле хранятся данные в памяти компьютера, очень трудно использовать логические и битовые операции, упакованные форматы данных и многое другое.
Шестнадцатеричная система счисления
Главное неудобство двоичной системы счисления - это размеры чисел, с которыми приходится обращаться. На практике с двоичными числами работают, только если необходимо следить за значениями отдельных бит, а когда размеры переменных превышают хотя бы четыре бита, используется шестнадцатеричная система. Эта система хороша тем, что она гораздо более компактна, компактнее десятичной, и тем, что перевод в двоичную систему и обратно происходит очень легко. В шестнадцатеричной системе используется 16 "цифр": 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, А, В, С. D, E, F, и номер позиции цифры в числе соответствует степени, в которую надо возвести число 16, так что:
96h = 9 * 16 + 6 = 150
Перевод в двоичную систему и обратно осуществляется крайне просто - вместо каждой шестнадцатеричной цифры подставляют соответствующее четырехзначное двоичное число:
9h = 1001b, 6h = 0110b, 96h = 10010110b
В ассемблерных программах при записи чисел, начинающихся с А, В, С, D, E, F, в начале приписывается цифра 0, чтобы нельзя было спутать такое число с названием переменной или другим идентификатором. После шестнадцатеричных чисел ставится буква "h" (см. табл. 3).
| Десятичное |
Двоичное |
Шестнадцатиричное |
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
0000b 0001b 0010b 0011b 0100b 0101b 0110b 0111b 1000b 1001b 1010b 1011b 1100b 1101b 1110b 1111b 10000b |
00h 01h 02h 03h 04h 05h 06h 07h 08h 09h 0Ah 0Bh 0Ch 0Dh 0Eh 0Fh 10h |
Таблица 3. Двоичные и шестнадцатеричные числа
Процессоры Intel в реальном режиме
Адресация по базе с индексированием и масштабированием
Это самая полная возможная схема адресации, в которую входят все случаи, рассмотренные ранее, как частные. Полный адрес операнда можно записать как выражение, представленное на рис. 6.
Рис. 6. Полная форма адресации
Смещение может быть байтом или двойным словом. Если ESP или EBP используются в роли базового регистра, селектор сегмента операнда берется по умолчанию из регистра SS, во всех остальных случаях - из DS.
Адресация по базе с индексированием
В этом методе адресации смещение операнда в памяти вычисляется как сумма чисел, содержащихся в двух регистрах, и смещения, если оно указано. Все следующие команды - это разные формы записи одного и того же действия:
mov ax,[bx+si+2] mov ax,[bx][si]+2 mov ax,[bx+2][si] mov ax,[bx][si+2] mov ax,2[bx][si]
В регистр AX помещается слово из ячейки памяти со смещением, равным сумме чисел, содержащихся в BX и SI, и числа 2. Из шестнадцатибитных регистров так можно складывать только BX + SI, BX + DI, BP + SI и BP + DI, а из 32-битных - все восемь регистров общего назначения. Так же как и для прямой адресации, вместо непосредственного указания числа можно использовать имя переменной, заданной одной из директив определения данных. Так можно прочитать, например, число из двумерного массива: если задана таблица 10x10 байт, 2 - смещение ее начала от начала сегмента данных (на практике будет использоваться имя этой таблицы), BX = 20, а SI = 7, приведенные команды прочитают слово, состоящее из седьмого и восьмого байт третьей строки. Если таблица состоит не из одиночных байт, а из слов или двойных слов, удобнее использовать следующую, наиболее полную форму адресации.
Адресация по базе со сдвигом
Теперь скомбинируем два предыдущих метода адресации: следующая команда
mov ax,[bx+2]
помещает в регистр AX слово, находящееся в сегменте, указанном в DS, со смещением на 2 большим, чем число, находящееся в BX. Так как слово занимает ровно два байта, эта команда поместила в AX слово, непосредственно следующее за тем, которое есть в предыдущем примере. Такая форма адресации используется в тех случаях, когда в регистре находится адрес начала структуры данных, а доступ надо осуществить к какому-нибудь элементу этой структуры. Другое важное применение адресации по базе со сдвигом - доступ из подпрограммы к параметрам, переданным в стеке, используя регистр BP (EBP) в качестве базы и номер параметра в качестве смещения, что детально разобрано в параграфе 5.2. Другие допустимые формы записи этого способа адресации:
mov ax,[bp]+2 mov ax,2[bp]
До 80386 в качестве базового регистра можно было использовать только BX, BP, SI или DI и сдвиг мог быть только байтом или словом (со знаком). Начиная с 80386 и старше, процессоры Intel позволяют дополнительно использовать EAX, EBX, ECX, EDX, EBP, ESP, ESI и EDI, так же как и для обычной косвенной адресации. С помощью этого метода можно организовывать доступ к одномерным массивам байт: смещение соответствует адресу начала массива, а число в регистре - индексу элемента массива, который надо считать. Очевидно, что, если массив состоит не из байт, а из слов, придется умножать базовый регистр на два, а если из двойных слов - на четыре. Для этого предусмотрен следующий специальный метод адресации.
Арифметические операции ММХ
| Команда: |
PADDB приемник,источник PADDW приемник,источник PADDD приемник,источник |
| Назначение: |
Сложение |
| Процессор: |
ММХ |
Команды выполняют сложение отдельных элементов данных (байт - для PADDB, слов - для PADDW, двойных слов - для PADDD) источника (регистр ММХ или переменная) и соответствующих элементов приемника (регистр ММХ). Если при сложении возникает перенос, он не влияет ни на следующие элементы, ни на флаг переноса, а просто игнорируется (так что, например, для PADDB 255 + 1 = 0, если это числа без знака, или -128 + -1 = +127, если со знаком).
| Команда: |
PADDSB приемник,источник PADDSW приемник,источник |
| Назначение: |
Сложение с насыщением |
| Процессор: |
ММХ |
Команды выполняют сложение отдельных элементов данных (байт - для PADDSB и слов - для PADDSW) источника (регистр ММХ или переменная) и соответствующих элементов приемника (регистр ММХ). Если результат сложения выходит за пределы байта со знаком для PADDSB (больше +127 или меньше -128) или слова со знаком для PADDSW (больше +32 767 или меньше -32 768), в качестве результата используется соответствующее максимальное или минимальное число, так что, например, для PADDSB -128 + -1 = -128.
| Команда: |
PADDUSB приемник,источник PADDUSW приемник,источник |
| Назначение: |
Беззнаковое сложение с насыщением |
| Процессор: |
ММХ |
Команды выполняют сложение отдельных элементов данных (байт - для PADDUSB и слов - для PADDUSW) источника (регистр ММХ или переменная) и соответствующих элементов приемника (регистр ММХ). Если результат сложения выходит за пределы байта без знака для PADDUSB (больше 255 или меньше 0) или слова без знака для PADDUSW (больше 65 535 или меньше 0), в качестве результата используется соответствующее максимальное или минимальное число, так что, например, для PADDUSB 255 + 1 = 255.
| Команда: |
PSUBB приемник,источник PSUBW приемник,источник PSUBD приемник,источник |
| Назначение: |
Вычитанние |
| Процессор: |
ММХ |
/p>
Команды выполняют вычитание отдельных элементов данных (байт - для PSUBB, слов - для PSUBW, двойных слов - для PSUBD) источника (регистр ММХ или переменная) и соответствующих элементов приемника (регистр ММХ). Если при вычитании возникает заем, он игнорируется (так что, например, для PSUBB -128 - 1 = +127 - для чисел со знаком или 0 - 1 = 255 - для чисел без знака).
| Команда: |
PSUBSB приемник,источник PSUBSW приемник,источник |
| Назначение: |
Вычитание с насыщением |
| Процессор: |
ММХ |
Команды выполняют вычитание отдельных элементов данных (байт - для PSUBSB и слов - для PSUBSW) источника (регистр ММХ или переменная) и соответствующих элементов приемника (регистр ММХ). Если результат вычитания выходит за пределы байта или слова со знаком, в качестве результата используется соответствующее максимальное или минимальное число, так что, например, для PSUBSB -128 - 1 = -128.
| Команда: |
PSUBUSB приемник,источник PSUBUSW приемник,источник |
| Назначение: |
Беззнаковое вычитание с насыщением |
| Процессор: |
ММХ |
Команды выполняют вычитание отдельных элементов данных (байт - для PSUBUSB и слов - для PSUBUSW) источника (регистр ММХ или переменная) и соответствующих элементов приемника (регистр ММХ). Если результат вычитания выходит за пределы байта или слова без знака, в качестве результата используется соответствующее максимальное или минимальное число, так что, например, для PSUBUSB 0 - 1 = 0.
| Команда: |
PMULHW приемник,источник |
| Назначение: |
Старшее умножение |
| Процессор: |
ММХ |
Команда умножает каждое из четырех слов со знаком из источника (регистр ММХ или переменная) на соответствующее слово со знаком из приемника (регистр ММХ). Старшее слово каждого из результатов записывается в соответствующую позицию приемника.
| Команда: |
PMULLW приемник,источник |
| Назначение: |
Младшее умножение |
| Процессор: |
ММХ |
<
/p>
Умножает каждое из четырех слов со знаком из источника (регистр ММХ или переменная) на соответствующее слово со знаком из приемника (регистр ММХ). Младшее слово каждого из результатов записывается в соответствующую позицию приемника.
| Команда: |
PMADDWD приемник,источник |
| Назначение: |
Умножение и сложение |
| Процессор: |
ММХ |
Умножает каждое из четырех слов со знаком из источника (регистр ММХ или переменная) на соответствующее слово со знаком из приемника (регистр ММХ). Произведения двух старших пар слов складываются между собой, и их сумма записывается в старшее двойное слово приемника. Сумма произведений двух младших пар слов записывается в младшее двойное слово.
Базовая арифметика FPU
| Команда: |
FADD приемник,источник |
| Назначение: |
Сложение вещественных чисел |
| Команда: |
FADDP приемник,источник |
| Назначение: |
Сложение с выталкиванием из стека |
| Команда: |
FIADD источник |
| Назначение: |
Сложение целых чисел |
| Процессор: |
8087 |
Команда выполняет сложение источника и приемника и помещает результат в приемник. Команда FADDP после этого выталкивает ST(0) из стека (помечает ST(0) как пустой и увеличивает ТОР на один). Команды сложения могут принимать следующие формы:
FADD источник, когда источником является 32- или 64-битная переменная, а приемником - ST(0);
FADD ST(0),ST(n), FADD ST(n),ST(0), FADDP ST(n),ST(0), когда источник и приемник заданы явно в виде регистров FPU;
FADD без операндов - эквивалентно FADD ST(0),ST(1); FADDP без операндов - эквивалентно FADDP ST(1),ST(0);
FIADD источник, когда источником является 16- или 32-битная переменная, содержащая целое число, а приемником - ST(0).
| Команда: |
FSUB приемник,источник |
| Назначение: |
Вычитание вещественных чисел |
| Команда: |
FSUBP приемник,источник |
| Назначение: |
Вычитание с выталкиванием из стека |
| Команда: |
FISUB источник |
| Назначение: |
Вычитание целых чисел |
| Процессор: |
8087 |
Выполняет вычитание источника из приемника и сохраняет результат в приемнике. Команда FSUBP после этого выталкивает ST(0) из стека (помечает ST(0) как пустой и увеличивает ТОР на один). Команды вычитания могут принимать следующие формы:
FSUB источник, когда источником является 32- или 64-битная переменная, содержащая вещественное число, а приемником - ST(0);
FSUB ST(0),ST(n), FSUB ST(n),ST(0), FSUBP ST(n),ST(0), когда источник и приемник заданы явно в виде регистров FPU;
FSUB без операндов - эквивалентно FSUB ST(0),ST(1); FSUBP без операндов - эквивалентно FSUBP ST(1),ST(0);
FISUB источник, когда источником является 16- или 32-битная переменная, содержащая целое число, а приемником - ST(0).
Числа с плавающей запятой
В процессорах Intel все операции с плавающей запятой выполняет специальное устройство, FPU (Floating Point Unit), с собственными регистрами и собственным набором команд, поставлявшееся сначала в виде сопроцессора (8087, 80287, 80387, 80487), а начиная с 80486DX - встраивающееся в основной процессор. FPU полностью соответствует стандартам IEEE 754 и (начиная с 80486) IEEE 854.
Десятичная арифметика
Процессоры Intel поддерживают операции с двумя форматами десятичных чисел: неупакованное двоично-десятичное число - байт, принимающий значения от 00 до 09, и упакованное двоично-десятичное число - байт, принимающий значения от 00 до 99h. Все обычные арифметическиe операции над такими числами приводят к неправильным результатам. Например, если увеличить 19h на 1, то получится число 1Ah, а не 20h. Для коррекции результатов арифметических действий над двоично-десятичными числами используются следующие команды.
| Команда: |
DAA |
| Назначение: |
BCD-коррекция после сложения |
| Процессор: |
8086 |
Если эта команда выполняется сразу после ADD (ADC, INC или XADD) и в регистре AL находится сумма двух упакованных двоично-десятичных чисел, то в результате в AL записывается упакованное двоично-десятичное число, которое должно было быть результатом сложения. Например, если AL содержит число 19h, последовательность команд
inc al daa
приведет к тому, что в AL окажется 20h (а не 1Ah, как было бы после INC).
 |
|
DAA выполняет следующие действия:
Если младшие четыре бита AL больше 9 или флаг AF = 1, то AL увеличивается на 6, CF устанавливается, если при этом сложении произошел перенос, и AF устанавливается в 1.
Иначе AF = 0.
Если теперь старшие четыре бита AL больше 9 или флаг CF = 1, то AL увеличивается на 60h и CF устанавливается в 1.
Иначе CF = 0.
| |
|
Флаги AF и CF устанавливаются, если в ходе коррекции происходил перенос из первой или второй цифры соответственно, SF, ZF и PF устанавливаются в соответствии с результатом, флаг OF не определен.
| Команда: |
DAS |
| Назначение: |
BCD-коррекция после вычитания |
| Процессор: |
8086 |
Если эта команда выполняется сразу после SUB (SBB или DEC) и в регистре AL находится разность двух упакованных двоично-десятичных чисел, то в результате в AL записывается упакованное двоично-десятичное число, которое должно было быть результатом вычитания. Например, если AL содержит число 20h, последовательность команд
dec al das
приведет к тому, что в AL окажется 19h ( а не 1Fh, как было бы после DEC).
 |
|
DAS выполняет следующие действия:
Если младшие четыре бита AL больше 9 или флаг AF = 1, то AL уменьшается на 6, CF устанавливается, если при этом вычитании произошел заем, и AF устанавливается в 1.
Иначе AF = 0.
Если теперь старшие четыре бита AL больше 9 или флаг CF = 1, то AL уменьшается на 60h и CF устанавливается в 1.
Иначе CF = 0.
Известный пример необычного использования этой команды - самый компактный вариант преобразования шестнадцатеричной цифры в ASCII-код соответствующего символа (более длинный и очевидный вариант этого преобразования рассматривался в описании команды XLAT):
cmp al,10 sbb al,96h das
После SBB числа 0 – 9 превращаются в 96h – 9Fh, а числа 0Ah – 0Fh - в 0А1h – 0A6h. Затем DAS вычитает 66h из первой группы чисел, переводя их в 30h – 39h, и 60h из второй группы чисел, переводя их в 41h – 46h. |
|
Флаги AF и CF устанавливаются, если в ходе коррекции происходил заем из первой или второй цифры соответственно, SF, ZF и PF устанавливаются в соответствии с результатом, флаг OF не определен.
| Команда: |
AAA |
| Назначение: |
ASCII-коррекция после сложения |
| Процессор: |
8086 |
Корректирует сумму двух неупакованных двоично-десятичных чисел в AL. Если коррекция приводит к десятичному переносу, АН увеличивается на 1. Эта команда имеет смысл сразу после команды сложения двух таких чисел. Например, если при сложении 05 и 06 в АХ окажется число 000Bh, то команда ААА скорректирует его в 0101h (неупакованное десятичное 11). Флаги CF и OF устанавливаются в 1, если произошел перенос из AL в АН, иначе они равны нулю. Значения флагов OF, SF, ZF и PF не определены.
| Команда: |
AAS |
| Назначение: |
ASCII-коррекция после вычитания |
| Процессор: |
8086 |
Корректирует разность двух неупакованных двоично-десятичных чисел в AL сразу после команды SUB или SBB. Если коррекция приводит к займу, АН уменьшается на 1. Флаги CF и OF устанавливаются в 1, если произошел заем из AL в АН, и в ноль - в противном случае. Значения флагов OF, SF, ZF и PF не определены.
| Команда: |
AAM |
| Назначение: |
ASCII-коррекция после умножения |
| Процессор: |
8086 |
<
/p>
Корректирует результат умножения неупакованных двоично-десятичных чисел, находящийся в АХ после выполнения команды MUL, преобразовывая полученный результат в пару неупакованных двоично-десятичных чисел (в АН и AL). Например:
mov al,5 mov bl,5 ; умножить 5 на 5 mul bl ; результат в АХ - 0019h aam ; теперь АХ содержит 0205h
ААМ устанавливает флаги SF, ZF и PF в соответствии с результатом и оставляет OF, AF и CF неопределенными.
 |
|
Код команды ААМ - D4h 0Ah, где 0Ah - основание системы счисления, по отношению к которой выполняется коррекция. Этот байт можно заменить на любое другое число (кроме нуля), и ААМ преобразует АХ к двум неупакованным цифрам любой системы счисления. Такая обобщенная форма ААМ работает на всех процессорах (начиная с 8086), но появляется в документации Intel только с процессоров Pentium. Фактически действие, которое выполняет ААМ, - целочисленное деление AL на 0Ah (или любое другое число в общем случае), частное помещается в AL, и остаток - в АН, так что эту команду часто используют для быстрого деления в высокооптимизированных алгоритмах. |
|
| Команда: |
AAD |
| Назначение: |
ASCII-коррекция перед делением |
| Процессор: |
8086 |
Выполняет коррекцию неупакованного двоично-десятичного числа, находящегося в регистре АХ, так, чтобы последующее деление привело к корректному десятичному результату. Например, разделим десятичное 25 на 5:
mov ax,0205h ; 25 в неупакованном формате mov bl,5 aad ; теперь в АХ находится 19h div bl ; АХ = 0005
Флаги SF, ZF и PF устанавливаются в соответствии с результатом, OF, AF и CF не определены.
 |
|
Так же как и команда ААМ, AAD используется с любой системой счисления: ее код - D5h 0Ah, и второй байт можно заменить на любое другое число. Действие AAD состоит в том, что содержимое регистра АН умножается на второй байт команды (0Ah по умолчанию) и складывается с AL, после чего АН обнуляется, так что AAD можно использовать для быстрого умножения на любое число. |
|
Другие команды
| Команда: |
NOP |
| Назначение: |
Отсутствие операции |
| Процессор: |
8086 |
NOP - однобайтная команда (код 90h), которая не выполняет ничего, только занимает место и время. Код этой команды фактически соответствует команде XCHG AL,AL. Можно многие команды записать так, что они не будут приводить ни к каким действиям, например:
mov ax,ax ; 2 байта xchg ax,ax ; 2 байта lea bx,[bx+0] ; 3 байта (8Dh, 5Fh, 00h, но многие ; ассемблеры, встретив такую команду, ; реально используют более короткую команду ; lea bx,[bx] с кодом 8Dh 1Fh) shl eax,0 ; 4 байта shrd еах,еах,0 ; 5 байт
| Префикс: |
LOCK |
| Назначение: |
Префикс блокировки шины данных |
| Процессор: |
8086 |
На все время выполнения команды, снабженной таким префиксом, будет заблокирована шина данных, и если в системе присутствует другой процессор, он не сможет обращаться к памяти, пока не закончится выполнение команды с префиксом LOCK. Команда XCHG автоматически всегда выполняется с блокировкой доступа к памяти, даже если префикс LOCK не указан. Этот префикс можно использовать только с командами ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD и XCHG.
| Команда: |
UD2 |
| Назначение: |
Неопределенная операция |
| Процессор: |
P6 |
Эта команда всегда вызывает ошибку "неопределенная операция" (исключение #UD). Впервые она описана как таковая для Pentium Pro, но во всех предыдущих процессорах эта команда (код 0Fh 0Bh) не была определена и, естественно, приводила к такой же ошибке. UD2 предназначена для тестирования программного обеспечения, в частности операционных систем, которые должны уметь корректно обрабатывать такую ошибку. Название команды происходит от команды UD (код 0Fh 0FFh), которая была определена AMD для процессоров AMD K5.
| Команда: |
CPUID |
| Назначение: |
Идентификация процессора |
| Процессор: |
80486 |
CPUID сообщает информацию о производителе, типе и модификации процессора, о наличии и поддержке различных расширений. Команда CPUID поддерживается Intel, начиная с процессоров Intel 80486DX/SX/DX2 SL, UMC U5S, Cyrix M1, AMD 80486DX4. Чтобы проверить, поддерживает ли процессор эту команду, попробуйте установить флаг ID в 1 (бит 21 в регистре EFLAGS) - если это получается, значит, команда CPUID поддерживается.
Результат работы CPUID зависит от значения регистра ЕАХ. Если ЕАХ = 0, CPUID возвращает в ЕАХ максимальное значение, с которым ее можно вызывать (2 для Р6, 1 для Р5), а регистры EBX:ECX:EDX содержат 12-байтную строку - идентификатор производителя (табл. 8).
Таблица 8. Строки производителей в CPUID
| Производитель |
Строка в ЕВХ:ЕСХ:ЕРХ |
| Intel |
GenuineIntel |
| UMC |
UMC UMC UMC |
| Cyrix |
CyrixInstead |
| AMD |
AuthenticAMD |
| NexGen |
NexGenDriven |
| Centaur Technology |
CentaurHalls |
Например, для процессоров Intel регистр ЕВХ содержит "Genu" (756E6547h), ЕСХ содержит "ineI" (49656E69h), a EDX - "ntel" (6C65746Eh).
Если ЕАХ = 1, CPUID возвращает в ЕАХ информацию о версии процессора, а в EDX - информацию о поддерживаемых расширениях. Многие понятия в этом описании относятся к работе процессора в защищенном режиме и рассмотрены ниже.
Биты 3 – 0 ЕАХ - Модификация.
Биты 7 – 4 ЕАХ - Модель.
Биты 11 – 8 ЕАХ - Семейство (3 для 386, 4 для 486, 5 для Pentium, 6 для Pentium Pro).
Биты 13 – 12 ЕАХ - Тип (0 - OEM, 1 - Overdrive, 2 - Dual).
Биты 31 – 14 ЕАХ зарезервированы и равны нулю.
Бит 0 EDX - "FPU" Процессор содержит FPU и может выполнять весь набор команд 80387.
Бит 1 EDX - "VME" Процессор поддерживает усовершенствованный режим V86 (флаги VIF и VIP в EFLAGS, биты VME и PVI в CRO).
Бит 2 EDX - "DE" Процессор поддерживает точки останова по вводу/выводу, бит DE в CR0.
Бит 3 EDX - "PSE" Процессор поддерживает страницы до 4 Мб, бит PSE в CR4, модифицированные биты в элементах списков страниц (РDЕ) и таблиц страниц (РТЕ).
Бит 4 EDX - "TSC" Процессор поддерживает команду RDTSC и бит TSC в CR4.
Бит 5 EDX - "MSR" Процессор поддерживает команды RDMSR и WRMSR и машинно-специфичные регистры, совместимые с Pentium.
Бит 6 EDX - "РАЕ" Процессор поддерживает физические адреса больше 32 бит, дополнительный уровень в таблицах трансляции страниц, страницы по 2 Мб и бит РАЕ в CR4. Число бит для физических адресов зависит от модели процессора. Так, Pentium Pro поддерживает 36 бит.
Бит 6 EDX - "РТЕ" (только для Cyrix).
Бит 7 EDX - "МСЕ" Процессор поддерживает бит МСЕ в CR4.
Бит 8 EDX - "СХ8" Процессор поддерживает команду CMPXCHG8B.
Бит 9 EDX - "APIC" Процессор содержит встроенный контроллер прерываний (APIC), и он активирован и доступен.
Бит 9 EDX - "PGE" (только для AMD).
Бит 10 EDX зарезервирован.
Бит 11 EDX - "SEP" Процессор поддерживает быстрые системные вызовы, команды SYSENTER и SYSEXIT (Pentium II).
Бит 12 EDX - "MTRR" Процессор поддерживает машинно-специфичные регистры MTRR.
Бит 13 EDX - "PGE" Процессор поддерживает бит PGE в CR4 и глобальные флаги в PTDE и РТЕ, указывающие элементы TLB, которые принадлежат сразу нескольким задачам.
Бит 14 EDX - "МСА" Процессор поддерживает машинно-специфичный регистр MCG_CAP.
Бит 15 EDX - "CMOV" Процессор поддерживает команды CMOVcc и (если бит 0 EDX установлен) FCMOVcc (Pentium Pro).
Бит 16 EDX - "PAT" Процессор поддерживает таблицу атрибутов страниц.
Биты 17 – 22 зарезервированы.
Бит 23 EDX - "ММХ" Процессор поддерживает набор команд ММХ.
Бит 24 EDX - "FXSR" Процессор поддерживает команды быстрого чтения/записи (ММХ2).
Биты 31 – 25 EDX зарезервированы.
Если ЕАХ = 2, CPUID на процессорах семейства Р6 возвращает в регистрах ЕАХ, ЕВХ, ЕСХ и EDX информацию о кэшах и TLB. Самый младший байт ЕАХ (регистр AL) указывает, сколько раз надо вызвать CPUID с ЕАХ = 2, чтобы получить информацию обо всех кэшах (1 для Pentium Pro и Pentium II). Самый старший бит (бит 31) каждого регистра указывает, содержит ли этот регистр правильную информацию (бит 31 = 0) или он зарезервирован (бит 31 = 1). В первом случае регистр содержит информацию в 1-байтных дескрипторах со следующими значениями:
00h - Пустой дескриптор.
01h - TLB команд, 4-килобайтные страницы, 4-сторонняя ассоциативность, 32 элемента.
02h - TLB команд, 4-мегабайтные страницы, 4-сторонняя ассоциативность, 4 элемента.
03h - TLB данных, 4-килобайтные страницы, 4-сторонняя ассоциативность, 64 элемента.
04h - TLB данных, 4-мегабайтные страницы, 4-сторонняя ассоциативность, 8 элементов.
06h - Кэш команд, 8 Кб, 4-сторонняя ассоциативность, 32 байта в строке.
08h - Кэш команд, 16 Кб, 4-сторонняя ассоциативность, 32 байта в строке.
0Ah - Кэш данных, 8 Кб, 2-сторонняя ассоциативность, 32 байта в строке.
0Ch - Кэш данных, 16 Кб, 2-сторонняя ассоциативность, 32 байта в строке.
41h - Унифицированный кэш, 128 Кб, 4-сторонняя ассоциативность, 32 байта в строке.
42h - Унифицированный кэш, 256 Кб, 4-сторонняя ассоциативность, 32 байта в строке.
43h - Унифицированный кэш, 512 Кб, 4-сторонняя ассоциативность, 32 байта в строке.
44h - Унифицированный кэш, 1 Мб, 4-сторонняя ассоциативность, 32 байта в строке.
Совместимые с Intel процессоры AMD и Cyrix поддерживают вызов "расширенных функций" CPUID со значениями ЕАХ, в которых самый старший бит всегда установлен в 1.
ЕАХ = 80000000h: Возвращает в ЕАХ максимальный номер расширенной функции CPUID, поддерживаемой данным процессором.
ЕАХ = 80000001h: Возвращает в ЕАХ 051Xh для AMD K5 (X - номер модификации) или 061Хh для AMD К6. В EDX эта функция возвращает информацию о поддерживаемых расширениях (указаны только флаги, отличающиеся от CPUID с ЕАХ = 1).
Бит 5 EDX "MSR" - Процессор поддерживает машинно-специфичные регистры, совместимые с К5.
Бит 10 EDX - Процессор поддерживает команды SYSCALL и SYSRET.
Бит 16 EDX - Процессор поддерживает команды FCMOVcc.
Бит 24 EDX - Процессор поддерживает ММХ с расширениями от Cyrix.
Бит 25 EDX - Процессор поддерживает набор команд AMD 3D.
ЕАХ = 80000002h, 80000003h и 80000004h - последовательный вызов CPUID с этими значениями в ЕАХ возвращает в EAX:EBX:ECX:EDX последовательно четыре 16-байтные части строки - имени процессора. Например: "AMD-K5(tm) Processor".
ЕАХ = 80000005h - Команда возвращает информацию о TLB в регистре ЕВХ (старшее слово - TLB данных, младшее слово - TLB команд, старший байт - ассоциативность, младший байт - число элементов), о кэше данных в регистре ЕСХ и о кэше команд в регистре EDX (биты 31 – 24 - размер в килобайтах, биты 23 – 16 - ассоциативность, биты 15 – 8 - число линий на тэг, биты 7 – 0 - число байт на линию.
Двоичная арифметика
Все команды из этого раздела, кроме команд деления и умножения, изменяют флаги OF, SF, ZF, AF, CF, PF в соответствии с назначением каждого из этих флагов (см. главу 2.1.4).
| Команда: |
ADD приемник, источник |
| Назначение: |
Сложение |
| Процессор: |
8086 |
Команда выполняет арифметическое сложение приемника и источника, помещает сумму в приемник, не изменяя содержимое источника. Приемник может быть регистром или переменной, источник может быть числом, регистром или переменной, но нельзя использовать переменную одновременно и для источника, и для приемника. Команда ADD никак не различает числа со знаком и без знака, но, употребляя значения флагов CF (перенос при сложении чисел без знака), OF (перенос при сложении чисел со знаком) и SF (знак результата), можно использовать ее и для тех, и для других.
| Команда: |
ADC приемник, источник |
| Назначение: |
Сложение с переносом |
| Процессор: |
8086 |
Эта команда во всем аналогична ADD, кроме того, что она выполняет арифметическое сложение приемника, источника и флага СF. Пара команд ADD/ADC используется для сложения чисел повышенной точности. Сложим, например, два 64-битных целых числа: пусть одно из них находится в паре регистров EDX:EAX (младшее двойное слово (биты 0 – 31) - в ЕАХ и старшее (биты 32 – 63) - в EDX), а другое - в паре регистров ЕВХ:ЕСХ:
add eax,ecx adc edx,ebx
Если при сложении младших двойных слов произошел перенос из старшего разряда (флаг CF = 1), то он будет учтен следующей командой ADC.
| Команда: |
XADD приемник, источник |
| Назначение: |
Обменять между собой и сложить |
| Процессор: |
80486 |
Выполняет сложение, помещает содержимое приемника в источник, - сумму операндов - в приемник. Источник всегда регистр, приемник может быть регистром и переменной.
| Команда: |
SUB приемник, источник |
| Назначение: |
Вычитание |
| Процессор: |
8086 |
Вычитает источник из приемника и помещает разность в приемник. Приемник может быть регистром или переменной, источник может быть числом, регистром или переменной, но нельзя использовать переменную одновременно и для источника, и для приемника. Точно так же, как и команда ADD, SUB не делает различий между числами со знаком и без знака, но флаги позволяют использовать ее как для тех, так и для других.
| Команда: |
SBB приемник, источник |
| Назначение: |
Вычитание с займом |
| Процессор: |
8086 |
<
/p>
Эта команда во всем аналогична SUB, кроме того, что она вычитает из приемника значение источника и дополнительно вычитает значение флага CF. Так, можно использовать эту команду для вычитания 64-битных чисел в EDX:EAX и ЕВХ:ЕСХ аналогично ADD/ADC:
sub eax,ecx sbb edx,ebx
Если при вычитании младших двойных слов произошел заем, то он будет учтен при вычитании старших.
| Команда: |
IMUL источник IMUL приемник, источник IMUL приемник, источник1, источник2 |
| Назначение: |
Умножение чисел со знаком |
| Процессор: |
8086 80386 80186 |
Эта команда имеет три формы, различающиеся числом операндов:
IMUL источник: источник (регистр или переменная) умножается на AL, АХ или ЕАХ (в зависимости от размера операнда), и результат располагается в АХ, DX:AX или EDX:EAX соответственно.
IMUL приемник,источник: источник (число, регистр или переменная) умножается на приемник (регистр), и результат заносится в приемник.
IMUL приемник,источник1,источник2: источник 1 (регистр или переменная) умножается на источник 2 (число), и результат заносится в приемник (регистр).
Во всех трех вариантах считается, что результат может занимать в два раза больше места, чем размер источника. В первом случае приемник автоматически оказывается достаточно большим, но во втором и третьем случаях могут произойти переполнение и потеря старших бит результата. Флаги OF и CF будут равны единице, если это произошло, и нулю, если результат умножения поместился целиком в приемник (во втором и третьем случаях) или в младшую половину приемника (в первом случае).
Значения флагов SF, ZF, AF и PF после команды IMUL не определены.
| Команда: |
MUL источник |
| Назначение: |
Умножение чисел без знака |
| Процессор: |
8086 |
Выполняет умножение содержимого источника (регистр или переменная) и регистра AL, АХ, ЕАХ (в зависимости от размера источника) и помещает результат в АХ, DX:AX, EDX:EAX соответственно. Если старшая половина результата (АН, DX, EDX) содержит только нули (результат целиком поместился в младшую половину), флаги CF и OF устанавливаются в 0, иначе - в 1. Значение остальных флагов (SF, ZF, AF и PF) не определено.
| Команда: |
IDIV источник |
| Назначение: |
Целочисленное деление со знаком |
| Процессор: |
8086 |
<
/p>
Выполняет целочисленное деление со знаком AL, АХ или ЕАХ (в зависимости от размера источника) на источник (регистр или переменная) и помещает результат в AL, АХ или ЕАХ, а остаток - в АН, DX или EDX соответственно. Результат всегда округляется в сторону нуля, знак остатка всегда совпадает со знаком делимого, абсолютное значение остатка всегда меньше абсолютного значения делителя. Значения флагов CF, OF, SF, ZF, AF и PF после этой команды не определены, а переполнение или деление на ноль вызывает исключение #DE (ошибка при делении) в защищенном режиме и прерывание 0 - в реальном.
| Команда: |
DIV источник |
| Назначение: |
Целочисленное деление без знака |
| Процессор: |
8086 |
Выполняет целочисленное деление без знака AL, АХ или ЕАХ (в зависимости от размера источника) на источник (регистр или переменная) и помещает результат в AL, АХ или ЕАХ, а остаток - в АН, DX или EDX соответственно. Результат всегда округляется в сторону нуля, абсолютное значение остатка всегда меньше абсолютного значения делителя. Значения флагов CF, OF, SF, ZF, AF и PF после этой команды не определены, а переполнение или деление на ноль вызывает исключение #DE (ошибка при делении) в защищенном режиме и прерывание 0 - в реальном.
| Команда: |
INC приемник |
| Назначение: |
Инкремент |
| Процессор: |
8086 |
Увеличивает приемник (регистр или переменная) на 1. Единственное отличие этой команды от ADD приемник,1 состоит в том, что флаг CF не затрагивается. Остальные арифметические флаги (OF, SF, ZF, AF, PF) устанавливаются в соответствии с результатом сложения.
| Команда: |
DEC приемник |
| Назначение: |
Декремент |
| Процессор: |
8086 |
Уменьшает приемник (регистр или переменная) на 1. Единственное отличие этой команды от SUB приемник,1 состоит в том, что флаг CF не затрагивается. Остальные арифметические флаги (OF, SF, ZF, AF, PF) устанавливаются в соответствии с результатом вычитания.
| Команда: |
NEG приемник |
| Назначение: |
Изменение знака |
| Процессор: |
8086 |
<
/p>
Выполняет над числом, содержащимся в приемнике (регистр или переменная), операцию дополнения до двух. Эта операция эквивалентна обращению знака операнда, если рассматривать его как число со знаком. Если приемник равен нулю, флаг CF устанавливается в 0, иначе - в 1. Остальные флаги (OF, SF, ZF, AF, PF) устанавливаются в соответствии с результатом операции.
 |
|
Красивый пример применения команды NEG - получение абсолютного значения числа, используя всего две команды - изменение знака и переход на первую команду еще раз, если знак отрицательный:
label0: neg eax js label0
|
|
| Команда: |
CMP приемник, источник |
| Назначение: |
Сравнение |
| Процессор: |
8086 |
Сравнивает приемник и источник и устанавливает флаги. Сравнение осуществляется путем вычитания источника (число, регистр или переменная) из приемника (регистр или переменная; приемник и источник не могут быть переменными одновременно), причем результат вычитания никуда не записывается, единственным результатом работы этой команды оказывается изменение флагов CF, OF, SF, ZF, AF и PF. Обычно команду СМР используют вместе с командами условного перехода (Jcc), условной пересылки данных (CMOVcc) или условной установки байт (SETcc), которые позволяют использовать результат сравнения, не обращая внимания на детальное значение каждого флага. Так, команды CMOVE, JE и SETE выполнят соответствующие действия, если значения операндов предшествующей команды СМР были равны.
 |
|
Несмотря на то что условные команды почти всегда применяются сразу после СМР, не надо забывать, что точно так же их можно использовать после любой команды, модифицирующей флаги, например: проверить равенство АХ нулю можно более короткой командой
test ax,ax
а равенство единице - однобайтной командой
dec ax
|
|
| Команда: |
CMPXCHG приемник, источник |
| Назначение: |
Сравнить и обменять между собой |
| Процессор: |
80486 |
Сравнивает значение, содержащееся в AL, АХ, ЕАХ (в зависимости от размера операндов), с приемником (регистром). Если они равны, содержимое источника копируется в приемник и флаг ZF устанавливается в 1. Если они не равны, содержимое приемника копируется в AL, АХ, ЕАХ и флаг ZF устанавливается в 0. Остальные флаги устанавливаются по результату операции сравнения, как после СМР. Источник всегда регистр, приемник может быть регистром и переменной.
| Команда: |
CMPXCHG8B приемник |
| Назначение: |
Сравнить и обменять восемь байт |
| Процессор: |
Р5 |
Выполняет сравнение содержимого регистров EDX:EAX как 64-битного числа (младшее двойное слово в ЕАХ, старшее - в EDX) с приемником (восьмибайтная переменная в памяти). Если они равны, содержимое регистров ЕСХ:ЕВХ как 64-битное число (младшее двойное слово в ЕВХ, старшее - в ЕСХ) копируется в приемник. Иначе содержимое приемника копируется в EDX:EAX.
Исключения FPU
При выполнении команд FPU могут возникать шесть типов особых ситуаций, называемых исключениями. При возникновении исключения соответствующий флаг в регистре SR устанавливается в 1 и, если маска этого исключения в регистре CR не установлена, вызывается обычное прерывание INT 10h (если бит NE в регистре центрального процессора CR0 установлен в 1) или IRQ13 (INT 75h), обработчик которого может прочитать регистр SR, чтобы определить тип исключения (и FIP, и FDP) и команду, которая его вызвала, а затем попытаться исправить ситуацию. Если бит маски наступившего исключения в регистре CR установлен в 1, выполняются следующие действия по умолчанию:
неточный результат: результат округляется в соответствии с битами RC (на самом деле это исключение происходит очень часто. Например: дробь 1/6 не может быть представлена точно десятичным вещественным числом любой точности и округляется). При этом флаг С1 показывает, в какую сторону произошло округление: 0 - вниз, 1 - вверх;
антипереполнение: результат слишком мал, чтобы быть представленным обычным числом, - он преобразуется в денормализованное число;
переполнение: результат преобразуется в бесконечность соответствующего знака;
деление на ноль: результат преобразуется в бесконечность соответствующего знака (учитывается и знак нуля);
денормализованный операнд: вычисление продолжается, как обычно;
недействительная операция: результат определяется из таблицы 12.
Таблица 12. Результаты операций, приводящих к исключениям
| Операция |
Результат |
| Ошибка стека |
Неопределенность |
| Операция с неподдерживаемым числом |
Неопределенность |
| Операция с SNAN |
QNAN |
| Сравнение числа с NAN |
C0 = C2 = C3 = 1 |
| Сложение бесконечностей с одним знаком или вычитание - с разным |
Неопределенность |
| Умножение нуля на бесконечность |
Неопределенность |
| Деление бесконечности на бесконечность или 0/0 |
Неопределенность |
| Команды FPREM и FPREM1, если делитель - 0 или делимое - бесконечность |
Неопределенность и C2 = 0 |
| Тригонометрическая операция над бесконечностью |
Неопределенность и C2 = 0 |
| Корень или логарифм, если x < 0, log(x+1), если x < -1 |
Неопределенность |
| FBSTP, если регистр-источник пуст, содержит NAN, бесконечность или превышает 18 десятичных знаков |
Десятичная неопределенность |
| FXCH, если один из операндов пуст |
Неопределенность |
Команды передачи управления
| Команда: |
JMP операнд |
| Назначение: |
Безусловный переход |
| Процессор: |
8086 |
JMP передает управление в другую точку программы, не сохраняя какой-либо информации для возврата. Операндом может быть непосредственный адрес для перехода (в программах используют имя метки, установленной перед командой, на которую выполняется переход), а также регистр или переменная, содержащая адрес.
В зависимости от типа перехода различают:
переход типа short (короткий переход) - если адрес перехода находится в пределах от -127 до +128 байт от команды JMP;
переход типа near (ближний переход) - если адрес перехода находится в том же сегменте памяти, что и команда JMP;
переход типа far (дальний переход) - если адрес перехода находится в другом сегменте. Дальний переход может выполняться и в тот же самый сегмент, если в сегментной части операнда указано число, совпадающее с текущим значением CS;
переход с переключением задачи - передача управления другой задаче в многозадачной среде. Этот вариант будет рассмотрен в главе, посвященной защищенному режиму.
При выполнении переходов типа short и near команда JMP фактически изменяет значение регистра EIP (или IP), изменяя тем самым смещение следующей исполняемой команды относительно начала сегмента кода. Если операнд - регистр или переменная в памяти, то его значение просто копируется в EIP, как если бы это была команда MOV. Если операнд для JMP - непосредственно указанное число, то его значение суммируется с содержимым EIP, приводя к относительному переходу. В ассемблерных программах в качестве операнда обычно указывают имена меток, но на уровне исполнимого кода ассемблер вычисляет и записывает именно относительные смещения.
Выполняя дальний переход в реальном режиме, виртуальном режиме и в защищенном режиме (при переходе в сегмент с теми же привилегиями), команда JMP просто загружает новое значение в EIP и новый селектор сегмента кода в CS, используя старшие 16 бит операнда как новое значение для CS и младшие 16 или 32 - как значение IP или EIP.
| Команда: |
Jcc метка |
| Назначение: |
Условный переход |
| Процессор: |
8086 |
<
/p>
Это набор команд, каждая из которых выполняет переход (типа short или near), если удовлетворяется соответствующее условие. Условием в каждом случае реально является состояние тех или иных флагов, но, если команда из набора Jcc используется сразу после СМР, условия приобретают формулировки, соответствующие отношениям между операндами СМР (см. табл. 7). Например, если операнды СМР были равны, то команда JE, выполненная сразу после этого СМР, осуществит переход. Операнд для всех команд из набора Jcc - 8-битное или 32-битное смешение относительно текущей команды.
Слова "выше" и "ниже" в таблице относятся к сравнению чисел без знака, слова "больше" и "меньше" учитывают знак.
Таблица 7. Варианты команды Jcc
| Код команды |
Реальное условие |
Условие для CMP |
JA JNBE |
CF = 0 и ZF = 0 |
если выше если не ниже или равно |
JAE JNB JNC |
CF = 0 |
если выше или равно если не ниже если нет переноса |
JB JNAE JC |
CF = 1 |
если ниже если не выше или равно если перенос |
JBE JNA |
CF = 1 и ZF = 1 |
если ниже или равно если не выше |
JE JZ |
ZF = 1 |
если равно если ноль |
JG JNLE |
ZF = 0 и SF = OF |
если больше если не меньше или равно |
JGE JNL |
SF = OF |
если больше или равно если не меньше |
JL JNGE |
SF <> OF |
если меньше если не больше или равно |
JLE JNG |
ZF = 1 и SF <> OF |
если меньше или равно если не больше |
JNE JNZ |
ZF = 0 |
если не равно если не ноль |
| JNO |
OF = 0 |
если нет переполнения |
| JO |
OF = 1 |
если есть переполнение |
JNP JPO |
PF = 0 |
если нет четности если нечетное |
JP JPE |
PF = 1 |
если есть четность если четное |
| JNS |
SF = 0 |
если нет знака |
| JS |
SF = 1 |
если есть знак |
Команды Jcc не поддерживают дальних переходов, так что, если требуется выполнить условный переход на дальнюю метку, необходимо использовать команду из набора Jcc с обратным условием и дальний JMP, как, например:
cmp ах,0 jne local_1 jmp far_label ; переход, если АХ = 0 lосаl_1:
| Команда: |
JCXZ метка |
| Назначение: |
Переход, если СХ = 0 |
| Процессор: |
8086 |
| Команда: |
JECXZ метка |
| Назначение: |
Переход, если EСХ = 0 |
| Процессор: |
80386 |
<
/p>
Выполняет ближний переход на указанную метку, если регистр CX или ECX (для JCXZ и JECXZ соответственно) равен нулю. Так же как и команды из серии Jcc, JCXZ и JECXZ не могут выполнять дальних переходов. Проверка равенства СХ нулю, например, может потребоваться в начале цикла, организованного командой LOOPNE, - если в него войти с СХ = 0, то он будет выполнен 65 535 раз.
| Команда: |
LOOP метка |
| Назначение: |
Цикл |
| Процессор: |
8086 |
Уменьшает регистр ЕСХ на 1 и выполняет переход типа short на метку (которая не может быть дальше, чем на расстоянии от -128 до +127 байт от команды LOOP), если ЕСХ не равен нулю. Эта команда используется для организации циклов, в которых регистр ЕСХ (или СХ при 16-битной адресации) играет роль счетчика. Так, в следующем фрагменте команда ADD выполнится 10 раз:
mov cx,0Ah loop_start: add ax,cx loop loop_start
Команда LOOP полностью эквивалентна паре команд
dec ecx jz метка
Но LOOP короче этих двух команд на один байт и не изменяет значения флагов.
| Команда: |
LOOPE метка |
| Назначение: |
Цикл, пока равно |
| Команда: |
LOOPZ метка |
| Назначение: |
Цикл, пока ноль |
| Команда: |
LOOPNE метка |
| Назначение: |
Цикл, пока не равно |
| Команда: |
LOOPNZ метка |
| Назначение: |
Цикл, пока не ноль |
| Процессор: |
8086 |
Все эти команды уменьшают регистр ЕСХ на один, после чего выполняют переход типа short, если ЕСХ не равен нулю и если выполняется условие. Для команд LOOPE и LOOPZ условием является равенство единице флага ZF, для команд LOOPNE и LOOPNZ - равенство флага ZF нулю. Сами команды LOOPcc не изменяют значений флагов, так что ZF должен быть установлен (или сброшен) предшествующей командой. Например, следующий фрагмент копирует строку из DS:SI в строку в ES:DI (см. описание команд работы со строками), пока не кончится строка (СХ = 0) или пока не встретится символ с ASCII-кодом 13 (конец строки):
mov cx,str_length move_loop: stosb lodsb cmp al,13 loopnz move_loop
| Команда: |
CALL операнд |
| Назначение: |
Вызов процедуры |
| Процессор: |
8086 |
<
/p>
Сохраняет текущий адрес в стеке и передает управление по адресу, указанному в операнде. Операндом может быть непосредственное значение адреса (метка в ассемблерных программах), регистр или переменная, содержащие адрес перехода. Если в качестве адреса перехода указано только смещение, считается, что адрес расположен в том же сегменте, что и команда CALL. При этом, так же как и в случае с JMP, выполняется ближний вызов процедуры. Процессор помещает значение регистра EIP (IP при 16-битной адресации), соответствующее следующей за CALL команде, в стек и загружает в EIP новое значение, осуществляя тем самым передачу управления. Если операнд CALL - регистр или переменная, то его значение рассматривается как абсолютное смещение, если операнд - метка в программе, то ассемблер указывает ее относительное смещение. Чтобы осуществить дальний CALL в реальном режиме, режиме V86 или в защищенном режиме при переходе в сегмент с теми же привилегиями, процессор помещает в стек значения регистров CS и EIP (IP при 16-битной адресации) и выполняет дальний переход аналогично команде JMP.
| Команда: |
RET число RETN число RETF число |
| Назначение: |
Возврат из процедуры |
| Процессор: |
8086 |
RETN считывает из стека слово (или двойное слово, в зависимости от режима адресации) и загружает его в IP (или EIP), выполняя тем самым действия, обратные ближнему вызову процедуры командой, CALL. RETF соответственно загружает из стека IP (EIP) и CS, возвращаясь из дальней процедуры. Если в ассемблерной программе указана команда RET, ассемблер заменит ее на RETN или RETF в зависимости от того, как была описана процедура, которую эта команда завершает. Операнд для RET необязателен, но, если он присутствует, после считывания адреса возврата из стека будет удалено указанное количество байт - это бывает нужно, если при вызове процедуры ей передавались параметры через стек.
| Команда: |
INT число |
| Назначение: |
Вызов прерывания |
| Процессор: |
8086 |
INT помещает в стек содержимое регистров EFLAGS, CS и EIP, после чего передает управление программе, называемой "обработчик прерывания" с указанным в качестве операнда номером (число от 0 до 0FFh), аналогично команде CALL. В реальном режиме адреса обработчиков прерываний считываются из таблицы, начинающейся в памяти по адресу 0000h:0000h. Адрес каждого обработчика занимает 4 байта, так что, например, адрес обработчика прерывания 10h находится в памяти по адресу 0000h:0040h. В защищенном режиме адреса обработчиков прерываний находятся в таблице IDT и обычно недоступны для прямого чтения или записи, так что для установки собственного обработчика программа должна обращаться к операционной системе. В DOS вызовы прерываний используются для выполнения большинства системных функций - работы с файлами, вводом/выводом и т.д. Например, следующий фрагмент кода завершает выполнение программы и возвращает управление DOS:
mov ax,4C01h int 21h
| Команда: |
IRET IRETD |
| Назначение: |
Возврат из обработчика прерывания |
| Процессор: |
8086 |
Возврат управления из обработчика прерывания или исключения. IRЕТ загружает из стека значения IP, CS и FLAGS, a IRETD - EIP, CS и EFLAGS соответственно. Единственное отличие IRET от RETF состоит в том, что восстанавливается значение регистра флагов, из-за чего многим обработчикам прерываний приходится изменять значение EFLAGS, находящегося в стеке, чтобы, например, вернуть флаг CF установленным в случае ошибки.
| Команда: |
INT3 |
| Назначение: |
Вызов прерывания 3 |
| Процессор: |
8086 |
Размер этой команды - один байт (код 0CCh), что делает ее удобной для отладки программ отладчиками, работающими в реальном режиме. Такие отладчики записывают этот байт вместо первого байта команды, перед которой требуется точка останова, и переопределяют адрес обработчика прерывания 3 на соответствующую процедуру внутри отладчика.
| Команда: |
INTO |
| Назначение: |
Вызов прерывания 4 при переполнении |
| Процессор: |
8086 |
INTO - еще одна специальная форма команды INT. Эта команда вызывает обработчик прерывания 4, если флаг OF установлен в 1.
| Команда: |
BOUND индекс, границы |
| Назначение: |
Проверка выхода за границы массива |
| Процессор: |
80186 |
BOUND проверяет, не выходит ли значение первого операнда (регистр), взятое как число со знаком, за границы, указанные во втором операнде (переменная). Границы - два слова или двойных слова (в зависимости от разрядности операндов), рассматриваемые как целые со знаком, расположенные в памяти подряд. Первая граница считается нижней, вторая - верхней. Если индекс меньше нижней границы или больше верхней, вызывается прерывание 5 (или исключение #BR), причем адрес возврата указывает не на следующую команду, а на BOUND, так что обработчик должен исправить значение индекса или границ, прежде чем выполнять команду IRET.
| Команда: |
ENTER размер, уровень |
| Назначение: |
Вход в процедуру |
| Процессор: |
80186 |
<
/p>
Команда ENTER создает стековый кадр заданного размера и уровня вложенности (оба операнда - числа; уровень вложенности может принимать значения только от 0 до 31) для вызова процедуры, использующей динамическое распределение памяти в стеке для своих локальных переменных. Так, команда
enter 2048,3
помещает в стек указатели на стековый кадр текущей процедуры и той, из которой вызывалась текущая, создает стековый кадр размером 2 килобайта для вызываемой процедуры и помещает в ЕВР адрес начала кадра. Пусть процедура MAIN имеет уровень вложенности 0, процедура PROCA запускается из MAIN и имеет уровень вложенности 1, и PROCB запускается из PROCA с уровнем вложенности 2. Тогда стек при входе в процедуру MAIN имеет вид, показанный на рис. 10.
Рис. 10. Стековый кадр процедуры 0-го уровня (MAIN)
Теперь процедура MAIN может определять свои локальные переменные в памяти, используя текущее значение ЕВР.
На первом уровне вложенности процедура PROCA, как показано на рис. 11, может создавать свои локальные переменные, используя текущее значение EBP, и получает доступ к локальным переменным процедуры MAIN, используя значение ЕВР для MAIN, помещенное в стек командой ENTER.
Рис. 11. Стековый кадр процедуры 1-го уровня (PROCA)
Процедура PROCB на втором уровне вложенности (рис. 12) получает доступ как к локальным переменным процедуры PROCA, используя значение ЕВР для PROCA, так и к локальным переменным процедуры МАIN, используя значение ЕВР для MAIN.
Рис. 12. Стековый кадр процедуры 2-го уровня (PROCB)
| Команда: |
LEAVE |
| Назначение: |
Выход из процедуры |
| Процессор: |
80186 |
Команда LEAVE выполняет действия, обратные команде ENTER. Фактически эта команда только копирует содержимое ЕВР в ESP, тем самым выбрасывая из стека весь кадр, созданный последней выполненной командой ENTER, и считывает из стека ЕВР для предыдущей процедуры, что одновременно восстанавливает и значение, которое имел ESP до вызова последней команды ENTER.
Команды пересылки данных FPU
| Команда: |
FLD источник |
| Назначение: |
Загрузить вещественное число в стек |
| Процессор: |
8087 |
Команда помещает содержимое источника (32-, 64- или 80-битная переменная или ST(n)) и уменьшает ТОР на 1. Команда FLD ST(0) делает копию вершины стека.
| Команда: |
FST приемник |
| Назначение: |
Скопировать вещественное число из стека |
| Команда: |
FSTP приемник |
| Назначение: |
Считать вещественное число из стека |
| Процессор: |
8087 |
Копирует ST(0) в приемник (32- или 64-битную переменную или пустой ST(n) в случае FST; 32-, 64- или 80-битную переменную или пустой ST(n) в случае FSTP). FSTP после этого выталкивает число из стека (помечает ST(0) как пустой и увеличивает ТОР на один).
| Команда: |
FILD источник |
| Назначение: |
Загрузить целое число в стек |
| Процессор: |
8087 |
Преобразовывает целое число со знаком из источника (16-, 32- или 64-битная переменная) в вещественный формат, помещает в вершину стека и уменьшает ТОР на 1.
| Команда: |
FIST приемник |
| Назначение: |
Скопировать целое число из стека |
| Команда: |
FISTP приемник |
| Назначение: |
Считать целое число из стека |
| Процессор: |
8087 |
Преобразовывает число из вершины стека в целое со знаком и записывает его в приемник (16- или 32-битная переменная для FIST; 16-, 32- или 64-битная переменная для FISTP). FISTP после этого выталкивает число из стека (помечает ST(0) как пустой и увеличивает ТОР на один). Попытка записи слишком большого числа, бесконечности или не-числа приводит к исключению "недопустимая операция" (и записи целой неопределенности, если IM = 1).
| Команда: |
FBLD источник |
| Назначение: |
Загрузить десятичное число в стек |
| Процессор: |
8087 |
Преобразовывает BCD число из источника (80-битная переменная в памяти), помещает в вершину стека и уменьшает ТОР на 1.
| Команда: |
FBSTP приемник |
| Назначение: |
Считать десятичное число из стека |
| Процессор: |
8087 |
Преобразовывает число из вершины стека в 80-битное упакованное десятичное, записывает его в приемник (80-битная переменная) и выталкивает это число из стека (помечает ST(0) как пустой и увеличивает ТОР на один). Попытка записи слишком большого числа, бесконечности или не-числа приводит к исключению "недопустимая операция" (и записи десятичной неопределенности, если IM = 1).
| Команда: |
FXCH приемник |
| Назначение: |
Обменять местами два регистра стека |
| Процессор: |
8087 |
<
/p>
Обмен местами содержимого регистра ST(0) и источника (регистр ST(n)). Если операнд не указан, обменивается содержимое ST(0) и ST(1).
| Команда: |
FCMOVcc приемник, источник |
| Назначение: |
Условная пересылка данных |
| Процессор: |
P6 |
Это набор команд, каждая из которых копирует содержимое источника (регистр ST(n)) в приемник (только ST(0)), если выполняется соответствующее условие. Реально каждое условие соответствует тем или ным значениям флагов регистра FLAGS, но после команд
fcom (или другие команды сравнения) fstsw ax sahf
в регистр FLAGS загружаются флаги С0, С1 и С3, и последующая команда из набора FCMOVcc приобретает смысл обработки результата предыдущего сравнения (табл. 13).
Таблица 13. Команды FCMOVcc
| Команда |
Значения флагов |
Действие после FCOM |
| FCMOVE |
ZF = 1 |
если равно |
| FCMOVNE |
ZF = 0 |
если не равно |
| FCMOVB |
CF = 1 |
если меньше |
| FCMOVBE |
CF = 1 и ZF = 1 |
если меньше или равно |
| FCMOVNB |
CF = 0 |
если не меньше |
| FCMOVNBE |
CF = 0 и ZF = 0 |
если не меньше или равно |
| FCMOVU |
PF = 1 |
если несравнимы |
| FCMOVNU |
PF = 0 |
если сравнимы |
Команды пересылки данных ММХ
| Команда: |
MOVD приемник,источник |
| Назначение: |
Пересылка двойных слов |
| Процессор: |
ММХ |
Команда копирует двойное слово из источника (регистр ММХ, обычный регистр или переменная) в приемник (регистр ММХ, обычный регистр или переменная, но хотя бы один из операндов обязательно должен быть регистром ММХ). Если приемник - регистр ММХ, двойное слово записывается в его младшую половину (биты 31 – 0), а старшая заполняется нулями. Если источник - регистр ММХ, в приемник записывается младшее двойное слово этого регистра.
| Команда: |
MOVQ приемник,источник |
| Назначение: |
Пересылка учетверенных слов |
| Процессор: |
ММХ |
Копирует учетверенное слово (64 бита) из источника (регистр ММХ или переменная) в приемник (регистр ММХ или переменная, оба операнда не могут быть переменными).
Команды преобразования типов ММХ
| Команда: |
PACKSSWB приемник,источник PACKSSDW приемник,источник |
| Назначение: |
Упаковка со знаковым насыщением |
| Процессор: |
ММХ |
Команды упаковывают и насыщает слова со знаком в байты (PACKSSWB) или двойные слова со знаком в слова (PACKSSDW). Команда PACKSSWB копирует четыре слова (со знаком), находящиеся в приемнике (регистр ММХ), в 4 младших байта (со знаком) приемника и копирует четыре слова (со знаком) из источника (регистр ММХ или переменная) в старшие четыре байта (со знаком) приемника. Если значение какого-нибудь слова больше +127 (7Fh) или меньше -128 (80h), в байты помещаются числа +127 и -128 соответственно. Команда PACKSSDW аналогично копирует два двойных слова из приемника в два младших слова приемника и два двойных слова из источника в два старших слова приемника. Если значение какого-нибудь двойного слова больше +32 767 (7FFFh) или меньше -32 768 (8000h), в слова помещаются числа +32 767 и -32 768 соответственно.
| Команда: |
PACKUSWB приемник,источник |
| Назначение: |
Упаковка с беззнаковым насыщением |
| Процессор: |
ММХ |
Копирует четыре слова (со знаком), находящиеся в приемнике (регистр ММХ), в 4 младших байта (без знака) приемника и копирует четыре слова (со знаком) из источника (регистр ММХ или переменная) в старшие четыре байта (без знака) приемника. Если значение какого-нибудь слова больше 255 (FFh) или меньше 0 (00h), в байты помещаются числа 255 и 0 соответственно.
| Команда: |
PUNPCKHBW приемник,источник PUNPCKHWD приемник,источник PUNPCKHDQ приемник,источник |
| Назначение: |
Распаковка и объединение старших элементов |
| Процессор: |
ММХ |
Команды распаковывают старшие элементы источника (регистр ММХ или переменная) и приемника (регистр ММХ) и записывают их в приемник через один (см. рис. 15).
Команда PUNPCKHBW объединяет по 4 старших байта источника и приемника, команда PUNPCKHWD объединяет по 2 старших слова, и команда PUNPCKHDQ копирует в приемник по одному старшему двойному слову из источника и приемника.
Если источник содержит нули, эти команды фактически переводят старшую половину приемника из одного формата данных в другой, дополняя увеличиваемые элементы нулями. PUNPCKHBW переводит упакованные байты в упакованные слова, PUNPCKHWD переводит слова в двойные слова, и PUNPCKHDQ переводит единственное старшее двойное слово приемника в учетверенное.
Рис. 15. Действие команды PUNPCKHBW
| Команда: |
PUNPCKLBW приемник,источник PUNPCKLWD приемник,источник PUNPCKLDQ приемник,источник |
| Назначение: |
Распаковка и объединение младших элементов |
| Процессор: |
ММХ |
Команды распаковывают младшие элементы источника (регистр ММХ или переменная) и приемника (регистр ММХ) и записывают их в приемник через один аналогично предыдущим командам. Команда PUNPCKLBW объединяет по 4 младших байта источника и приемника, команда PUNPCKLWD объединяет по 2 младших слова, и команда PUNPCKLDQ копирует в приемник по одному младшему двойному слову из источника и приемника. Если источник содержит только нули, эти команды, аналогично PUNPCKH*, фактически переводят младшую половину приемника из одного формата данных в другой, дополняя увеличиваемые элементы нулями.
Команды сравнения FPU
| Команда: |
FCOM источник |
| Назначение: |
Сравнить вещественные числа |
| Команда: |
FCOMP источник |
| Назначение: |
Сравнить и вытолкнуть из стека |
| Команда: |
FCOMPP источник |
| Назначение: |
Сравнить и вытолкнуть из стека два числа |
| Процессор: |
8087 |
Команды выполняют сравнение содержимого регистра ST(0) с источником (32- или 64-битная переменная или регистр ST(n), если операнд не указан - ST(1)) и устанавливают флаги С0, С2 и С3 в соответствии с таблицей 14.
Таблица 14. Флаги сравнения FPU
| Условие |
C3 |
C2 |
C0 |
| ST(0) > источник |
0 |
0 |
0 |
| ST(0) < источник |
0 |
0 |
1 |
| ST(0) = источник |
1 |
0 |
0 |
| Не сравнимы |
1 |
1 |
1 |
Если один из операндов - не-число или неподдерживаемое число, происходит исключение "недопустимая операция", а если оно замаскировано (флаг IM = 1), все три флага устанавливаются в 1. После команд сравнения с помощью команд FSTSW и SAHF можно перевести флаги С3, С2 и С0 в соответственно ZF, PF и CF, после чего все условные команды (Jcc, CMOVcc, FCMOVcc, SETcc) могут использовать результат сравнения, как после команды СМР.
Команда FCOMP после выполнения сравнения выталкивает из стека содержимое ST(0) (помечает его как пустой и увеличивает ТОР на 1), а команда FCOMPP выталкивает из стека и ST(0), и ST(1).
| Команда: |
FUCOM источник |
| Назначение: |
Сравнить вещественные числа без учета порядков |
| Команда: |
FUCOMP источник |
| Назначение: |
Сравнить без учета порядков и вытолкнуть из стека |
| Команда: |
FUCOMPP источник |
| Назначение: |
Сравнить без учета порядков и вытолкнуть из стека два числа |
| Процессор: |
80387 |
Эти команды аналогичны FCOM/FCOMP/FCOMPP во всем, кроме того, что в роли источника могут выступать только регистры ST(n), и если один из операндов - QNAN ("тихое" не-число), флаги С3, С2, С0 устанавливаются в единицы, но исключение "недопустимая операция" не вызывается. Если один из операндов - SNAN или неподдерживаемое число, эти команды ведут себя так же, как и обычное сравнение.
| Команда: |
FICOM источник |
| Назначение: |
Сравнить целые числа |
| Команда: |
FICOMP источник |
| Назначение: |
Сравнить целые и вытолкнуть из стека |
| Процессор: |
8087 |
<
/p>
Эти команды сравнивают содержимое регистра ST(0) и источника (16- или 32-битная переменная), причем считается, что источник содержит целое число. В остальном действие FICOM/FICOMP полностью эквивалентно FCOM/FCOMP.
| Команда: |
FCOMI источник |
| Назначение: |
Сравнить и установить EFLAGS |
| Команда: |
FCOMIP источник |
| Назначение: |
Сравнить, установить EFLAGS и вытолкнуть |
| Команда: |
FUCOMI источник |
| Назначение: |
Сравнить без учета порядков и установить EFLAGS |
| Команда: |
FUCOMIP источник |
| Назначение: |
Сравнить без учета порядков, установить EFLAGS и вытолкнуть из стека |
| Процессор: |
P6 |
Выполняет сравнение регистра ST(0) и источника (регистр ST(n)) и устанавливает флаги регистра EFLAGS соответственно таблице 15.
Таблица 15. Флаги после соманд FCOMI
| Условие |
ZF |
PF |
CF |
| ST(0) > источник |
0 |
0 |
0 |
| ST(0) < источник |
0 |
0 |
1 |
| ST(0) = источник |
1 |
0 |
0 |
| Не сравнимы |
1 |
1 |
1 |
Эти команды эквивалентны командам FCOM/FCOMP/FUCOM/FUCOMP, вслед за которыми исполняются FSMSW АХ и SAHF, но они не изменяют содержимого регистра АХ и выполняются быстрее.
| Команда: |
FTST |
| Назначение: |
Проверить, не содержит ли SP(0) ноль |
| Процессор: |
8087 |
Сравнивает содержимое ST(0) с нулем и выставляет флаги С3, С2 и С0 аналогично другим командам сравнения.
| Команда: |
FXAM |
| Назначение: |
Проанализировать содержимое ST(0) |
| Процессор: |
8087 |
Устанавливает флаги С3, С2 и С0 в зависимости от типа числа, находящегося в ST(0), в соответствии с правилами, приведенными в таблице 16.
Флаг С1 устанавливается равным знаку числа в ST(0) независимо от типа числа (на самом деле он устанавливается, даже если регистр помечен как пустой).
Таблица 16. Результаты действия команды FXAM
| Тип числа |
C3 |
C2 |
C0 |
| Неподдерживаемое |
0 |
0 |
0 |
| Не-число |
0 |
0 |
1 |
| Нормальное конечное число |
0 |
1 |
0 |
| Бесконечность |
0 |
1 |
1 |
| Ноль |
1 |
0 |
0 |
| Регистр пуст |
1 |
0 |
1 |
| Денормализованное число |
1 |
1 |
0 |
Команды сравнения ММХ
| Команда: |
PCMPEQB приемник,источник PCMPEQW приемник,источник PCMPEQD приемник,источник |
| Назначение: |
Проверка на равенство |
| Процессор: |
ММХ |
Команды сравнивают индивидуальные элементы данных (байты - в случае PCMPEQB, слова - в случае PCMPEQW, двойные слова - в случае PCMPEQD) источника (регистр ММХ или переменная) с соответствующими элементами приемника (регистр ММХ). Если пара сравниваемых элементов равна, соответствующий элемент приемника заполняется единицами, если они не равны - элемент заполняется нулями.
| Команда: |
PCMPGTB приемник,источник PCMPGTW приемник,источник PCMPGTD приемник,источник |
| Назначение: |
Сравнение |
| Процессор: |
ММХ |
Команды сравнивают индивидуальные элементы данных (байты - в случае PCMPGTB, слова - в случае PCMPGTW, двойные слова - в случае PCMPGTD) источника (регистр ММХ или переменная) с соответствующими элементами приемника (регистр ММХ). Если элемент приемника больше, чем соответствующий элемент источника, все биты в этом элементе приемника устанавливаются в единицы. Если элемент приемника меньше или равен элементу источника, он обнуляется.
Команды управления FPU
| Команда: |
FINCSTP |
| Назначение: |
Увеличить указатель вершины стека |
| Процессор: |
8087 |
Поле ТОР регистра состояния FPU увеличивается на 1. Если ТОР было равно семи, оно обнуляется. Эта команда не эквивалентна выталкиванию ST(0) из стека, потому что регистр данных, который назывался ST(0) и стал ST(7), не помечается как пустой.
| Команда: |
FDECSTP |
| Назначение: |
Уменьшить указатель вершины стека |
| Процессор: |
8087 |
Поле ТОР регистра состояния FPU уменьшается на 1. Если ТОР было равно нулю, оно устанавливается в 7. Содержимое регистров данных и TW не изменяется.
| Команда: |
FFREE операнд |
| Назначение: |
Освободить регистр данных |
| Процессор: |
8087 |
Команда отмечает в регистре TW, что операнд (регистр данных ST(n)) - пустой. Содержимое регистра и ТОР не изменяются.
| Команда: |
FINIT |
| Назначение: |
Инициализировать FPU |
| Команда: |
FNINIT |
| Назначение: |
Инициализировать FPU без ожидания |
| Процессор: |
8087 |
Команды FINIT и FNINIT восстанавливают значения по умолчанию в регистрах CR, SR, TW, а начиная с 80387 - FIP и FDP Управляющий регистр инициализируется значением 037Fh (округление к ближайшему, 64-битная точность, все исключения замаскированы). Регистр состояния обнуляется (ТОР = 0, никакие флаги исключений не установлены). Регистры данных никак не изменяются, но все они помечаются пустыми в регистре TW. Регистры FIP и FDP обнуляются. Команда FINIT, в отличие от FNINIT, проверяет наличие произошедших и необработанных исключений и обрабатывает их до инициализации. Команда FINIT полностью эквивалентна (и на самом деле является) WAIT FNINIT.
| Команда: |
FCLEX |
| Назначение: |
Обнулить флаги исключений |
| Команда: |
FNCLEX |
| Назначение: |
Обнулить флаги исключений без ожидания |
| Процессор: |
8087 |
Команды обнуляют флаги исключений (РЕ, UE, OF, ZE, DE, IE), а также флаги ES, SF и В в регистре состояния FPU. Команда FCLEX, в отличие от FNCLEX, проверяет наличие произошедших и необработанных исключений и обрабатывает их до выполнения. Команда FCLEX полностью эквивалентна (и на самом деле является) WAIT FNCLEX.
| Команда: |
FSTCW приемник |
| Назначение: |
Сохранить регистр CR |
| Команда: |
FNSTCW приемник |
| Назначение: |
Сохранить регистр CR без ожидания |
| Процессор: |
8087 |
<
/p>
Команды копируют содержимое CR в приемник (16-битная переменяя). Команда FSTCW, в отличие от FNSTCW, проверяет наличие произошедших и необработанных исключений и обрабатывает их до выполнения. Команда FSTCW полностью эквивалентна (и на самом деле является) WAIT FNSTCW.
| Команда: |
FLDCW источник |
| Назначение: |
Загрузить регистр CR |
| Процессор: |
8087 |
Копирует содержимое источника (16-битная переменная) в регистр CR. Если один или несколько флагов исключений установлены в регистре SR и замаскированы в CR, а команда FLDCW эти маски удалила, исключения будут обработаны перед началом выполнения следующей команды FPU (кроме команд без ожидания). Чтобы этого не происходило, обычно перед FLDCW выполняют команду FCLEX.
| Команда: |
FSTENV приемник |
| Назначение: |
Сохранить вспомогательные регистры |
| Команда: |
FNSTENV приемник |
| Назначение: |
Сохранить вспомогательные регистры без ожидания |
| Процессор: |
8087 |
Сохраняет все вспомогательные регистры FPU в приемник (14 или 28 байт в памяти, в зависимости от разрядности операндов) и маскирует все исключения. Сохраняет содержимое регистров CR, SR, TW, FIP, FDP и последнюю команду в формате, зависящем от текущей разрядности операндов и адресов (7 двойных слов для 32-битных операндов и 7 слов для 16-битных операндов). Первое слово (или младшая половина первого двойного слова в 32-битном случае) всегда содержит CR, второе слово - SR, третье слово - TW, четвертое - FIP. Использование последних трех слов варьируется в зависимости от текущей разрядности адресации и операндов.
32-битные операнды и 32-битная адресация:
Двойное слово 5: биты 10 – 0 старшего слова - код последней команды, младшее слово - селектор для FIP.
Двойное слово 6: FDP (32-битный).
Двойное слово 7: младшее слово содержит селектор для FDP.
32-битные операнды и 16-битная адресация:
Двойное слово 5: биты 31 – 16 - FIP, биты 10 – 0 - код последней команды.
Двойное слово 6: биты 15 – 0 - FDP.
Двойное слово 7: биты 31 – 16 - FDP.
16-битные операнды и 32-битная адресация:
Слово 5: селектор для FIP.
Слово 6: FDP.
Слово 7: селектор для FDP.
16-битные операнды и 16-битная адресация:
Слово 5: биты 15 – 12 - биты 19 – 16 20-битного FIP, биты 10 – 0 - код последней команды.
Слово 6: FDP.
Слово 7: биты 15 – 12 - биты 19 – 16 20-битного FDP.
Из кода последней выполненной FPU- команды сохраняются первые два байта без префиксов и без первых пяти бит, которые одинаковы для всех команд FPU, то есть всего 11 бит. Команда FSTENV, в отличие от FNSTENV, проверяет наличие произошедших и необработанных исключений и обрабатывает их до выполнения. Команда FSTENV полностью эквивалентна (и на самом деле является) WAIT FNSTENV.
| Команда: |
FLDENV источник |
| Назначение: |
Загрузить вспомогательные регистры |
| Процессор: |
8087 |
Команда загружает все вспомогательные регистры FPU (регистры CR, SR, TW, FIP, FDP) из источника (область памяти в 14 или 28 байт, в зависимости от разрядности операндов), сохраненные ранее командой FSTENV/FNSTENV. Если в загружаемом SW установлены несколько (или один) флагов исключений, которые одновременно не замаскированы флагами CR, эти исключения будут выполнены перед следующей командой FPU (кроме команд без ожидания).
| Команда: |
FSAVE приемник |
| Назначение: |
Сохранить состояние FPU |
| Команда: |
FNSAVE приемник |
| Назначение: |
Сохранить состояние FPU без ожидания |
| Процессор: |
8087 |
Сохраняет состояние FPU (регистры данных и вспомогательные регистры) в приемник (область памяти размером 94 или 108 байт, в зависимости от разрядности операндов) и инициализирует FPU аналогично командам FINIT/FNINIT. Команда FSAVE, в отличие от FNSAVE, проверяет наличие произошедших и необработанных исключений и обрабатывает их до выполнения. Команда FSAVE полностью эквивалентна (и на самом деле является) WAIT FNSAVE. Эта команда обычно используется операционной системой при переключении задач или программами, которые должны передавать вызываемым процедурам чистый FPU.
| Команда: |
FXSAVE приемник |
| Назначение: |
Быстрое сохранение состояния FPU |
| Процессор: |
PII |
<
/p>
Команда FXSAVE сохраняет все текущее состояние FPU, включая все регистры, в приемник (512-байтную область памяти с адресом, кратным 16), не проверяя на необработанные исключения, аналогично команде FNSAVE. Кроме того, в отличие от FSAVE/FNSAVE, эта команда не переинициализирует FPU после сохранения состояния. Эта команда несовместима с FSAVE/FRSTOR.
| Команда: |
FRSTOR источник |
| Назначение: |
Восстановить состояние FPU |
| Процессор: |
8087 |
Загружает состояние FPU (вспомогательные регистры и регистры данных) из источника (область в памяти размером в 94 или 108 байт, в зависимости от разрядности операндов).
| Команда: |
FXRSTOR источник |
| Назначение: |
Быстрое восстановление состояния FPU |
| Процессор: |
PII |
Команда FXRSTOR восстанавливает все текущее состояние FPU, включая все регистры, из источника (512-байтной области памяти с адресом, кратным 16), который был заполнен командой FXSAVE.
| Команда: |
FSTSW приемник |
| Назначение: |
Сохранить регистр SR |
| Команда: |
FNSTSW приемник |
| Назначение: |
Сохранить регистр SR без ожидания |
| Процессор: |
80287 |
Сохраняет текущее значение регистра SR в приемник (регистр АХ или 16-битная переменная). Команда FSTSW АХ обычно используется после команд сравнения и FPREM/FPREM1/FXAM, чтобы выполнять условные переходы.
| Команда: |
WAIT FWAIT |
| Назначение: |
Ожидание готовности FPU |
| Процессор: |
8087 |
Процессор проверяет, присутствуют ли необработанные и немаскированные исключения FPU, и обрабатывает их. Эту команду можно указывать в критических ситуациях после команд FPU, чтобы убедиться, что возможные исключения будут обработаны. WAIT и FWAIT - разные названия для одной и той же команды.
| Команда: |
FNOP |
| Назначение: |
Отсутствие операции |
| Процессор: |
8087 |
Эта команда занимает место и время, но не выполняет никакого действия. Устаревшие команды FPU - FENI (разрешить исключения, 8087), FDISI (запретить исключения, 8087) и FSETPM (80287) выполняются, как FNOP, всеми более старшими процессорами.
Команды управления состоянием ММХ
| Команда: |
EMMS |
| Назначение: |
Освободить регистры ММХ |
| Процессор: |
ММХ |
Если выполнялись какие-нибудь команды ММХ (кроме EMMS), все регистры FPU помечаются как занятые (в регистре TW). Команда EMMS помечает все регистры FPU как пустые для того, чтобы после завершения работы с ММХ можно было передать управление процедуре, использующей FPU.
Константы FPU
| Команда: |
FLD1 |
| Назначение: |
Поместить в стек 1,0 |
| Команда: |
FLDZ |
| Назначение: |
Поместить в стек +0,0 |
| Команда: |
FLDPI |
| Назначение: |
Поместить в стек число  |
| Команда: |
FLDL2E |
| Назначение: |
Поместить в стек log2(e) |
| Команда: |
FLDL2T |
| Назначение: |
Поместить в стек log2(10) |
| Команда: |
FLDLN2 |
| Назначение: |
Поместить в стек ln(2) |
| Команда: |
FLDLG2 |
| Назначение: |
Поместить в стек lg(2) |
| Процессор: |
8087 |
Все эти команды помещают в стек (то есть уменьшают ТОР на один и помещают в ST(0)) соответствующую часто используемую константу. Начиная с сопроцессора 80387, все константы хранятся в более точном формате, чем 80-битный формат, используемый в регистрах данных, и при загрузке в стек происходит округление в соответствии с полем RC.
Косвенная адресация с масштабированием
Этот метод адресации полностью идентичен предыдущему, за исключением того, что с его помощью можно прочитать элемент массива слов, двойных слов или учетверенных слов, просто поместив номер элемента в регистр:
mov ax,[esi*2]+2
Множитель, который может быть равен 1, 2, 4 или 8, соответствует размеру элемента массива - байту, слову, двойному слову, учетверенному слову соответственно. Из регистров в этом варианте адресации можно использовать только EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, но не SI, DI, BP или SP, которые можно было использовать в предыдущих вариантах.
Косвенная адресация
По аналогии с регистровыми и непосредственными операндами адрес операнда в памяти также можно не указывать непосредственно, а хранить в любом регистре. До 80386 для этого можно было использовать только BX, SI, DI и BP, но потом эти ограничения были сняты и адрес операнда разрешили считывать также и из EAX, EBX, ECX, EDX, ESI, EDI, EBP и ESP (но не из AX, CX, DX или SP напрямую - надо использовать EAX, ECX, EDX, ESP соответственно или предварительно скопировать смещение в BX, SI, DI или BP). Например, следующая команда помещает в регистр AX слово из ячейки памяти, селектор сегмента которой находится в DS, а смещение - в BX:
mov ax,[bx]
Как и в случае прямой адресации, DS используется по умолчанию, но не во всех случаях: если смещение берут из регистров ESP, EBP или BP, то в качестве сегментного регистра используется SS. В реальном режиме можно свободно пользоваться всеми 32-битными регистрами, надо только следить, чтобы их содержимое не превышало границ 16-битного слова.
Логические операции
| Команда: |
AND приемник, источник |
| Назначение: |
Логическое И |
| Процессор: |
8086 |
Команда выполняет побитовое "логическое И" над приемником (регистр или переменная) и источником (число, регистр или переменная; источник и приемник не могут быть переменными одновременно) и помещает результат в приемник. Любой бит результата равен 1, только если соответствующие биты обоих операндов были равны 1, и равен 0 в остальных случаях. Наиболее часто AND применяют для выборочного обнуления отдельных бит, например, команда
and al,00001111b
обнулит старшие четыре бита регистра AL, сохранив неизменными четыре младших.
Флаги OF и CF обнуляются, SF, ZF и PF устанавливаются в соответствии с результатом, AF не определен.
| Команда: |
OR приемник, источник |
| Назначение: |
Логическое ИЛИ |
| Процессор: |
8086 |
Выполняет побитовое "логическое ИЛИ" над приемником (регистр или переменная) и источником (число, регистр или переменная; источник и приемник не могут быть переменными одновременно) и помещает результат в приемник. Любой бит результата равен 0, только если соответствующие биты обоих операндов были равны 0, и равен 1 в остальных случаях. Команду OR чаще всего используют для выборочной установки отдельных бит, например, команда
or al,00001111b
приведет к тому, что младшие четыре бита регистра AL будут установлены в 1.
При выполнении команды OR флаги OF и CF обнуляются, SF, ZF и PF устанавливаются в соответствии с результатом, AF не определен.
| Команда: |
XOR приемник, источник |
| Назначение: |
Логическое исключающее ИЛИ |
| Процессор: |
8086 |
Выполняет побитовое "логическое исключающее ИЛИ" над приемником (регистр или переменная) и источником (число, регистр или переменная; источник и приемник не могут быть переменными одновременно) и помещает результат в приемник. Любой бит результата равен 1, если соответствующие биты операндов различны, и нулю, если одинаковы. XOR используется для самых разных операций, например:
xor ах,ах ; обнуление регистра АХ
или
xor ах,bх xor bх,ах xor ах,bх ; меняет местами содержимое АХ и ВХ
Оба этих примера могут выполняться быстрее, чем соответствующие очевидные команды
mov ax,0
или
xchg ax,bx
| Команда: |
NOT приемник |
| Назначение: |
Инверсия |
| Процессор: |
8086 |
Каждый бит приемника (регистр или переменная), равный нулю, устанавливается в 1, и каждый бит, равный 1, сбрасывается в 0. Флаги не затрагиваются.
| Команда: |
TEST приемник, источник |
| Назначение: |
Логическое сравнение |
| Процессор: |
8086 |
Вычисляет результат действия побитового "логического И" над приемником (регистр или переменная) и источником (число, регистр или переменная; источник и приемник не могут быть переменными одновременно) и устанавливает флаги SF, ZF и PF в соответствии с полученным результатом, не сохраняя результат (флаги OF и CF обнуляются, значение AF не определено). TEST, так же как и СМР, используется в основном в сочетании с командами условного перехода (Jcc), условной пересылки данных (CMOVcc) и условной установки байт (SETcc).
Логические операции ММХ
| Команда: |
PAND приемник,источник |
| Назначение: |
Логическое И |
| Процессор: |
ММХ |
Команда выполняет побитовое "логическое И" над источником (регистр ММХ или переменная) и приемником (регистр ММХ) и сохраняет результат в приемнике. Каждый бит результата устанавливается в 1, если соответствующие биты в обоих операндах равны 1, иначе бит сбрасывается в 0.
| Команда: |
PANDN приемник,источник |
| Назначение: |
Логическое НЕ-И (штрих Шеффера) |
| Процессор: |
ММХ |
Выполняет побитовое "логическое НЕ" (то есть инверсию бит) над приемником (регистр ММХ) и затем побитовое "логическое И" над приемником и источником (регистр ММХ или переменная). Результат сохраняется в приемнике. Каждый бит результата устанавливается в 1, только если соответствующий бит источника был равен 1, а приемника - 0, иначе бит сбрасывается в 0. Эта логическая операция носит также название "штрих Шеффера".
| Команда: |
POR приемник,источник |
| Назначение: |
Логическое ИЛИ |
| Процессор: |
ММХ |
Выполняет побитовое "логическое ИЛИ" над источником (регистр ММХ или переменная) и приемником (регистр ММХ) и сохраняет результат в приемнике. Каждый бит результата сбрасывается в 0, если соответствующие биты в обоих операндах равны 0, иначе бит устанавливается в 1.
| Команда: |
PXOR приемник,источник |
| Назначение: |
Логическое исключающее ИЛИ |
| Процессор: |
ММХ |
Выполняет побитовое "логическое исключающее ИЛИ" над источником (регистр ММХ или переменная) и приемником, (регистр ММХ) и сохраняет результат в приемнике. Каждый бит результата устанавливается в 1, если соответствующие биты в обоих операндах равны, иначе бит сбрасывается в 0.
Непосредственная адресация
Некоторые команды (все арифметические команды, кроме деления) позволяют указывать один из операндов непосредственно в тексте программы, например команда
mov ax,2
помещает в регистр AX число 2.
Операции над битами и байтами
| Команда: |
BT база, смещение |
| Назначение: |
Проверка бита |
| Процессор: |
80386 |
Команда ВТ считывает во флаг CF значение бита из битовой строки, указанной первым операндом, битовой базой (регистр или переменная), со смещением, указанным во втором операнде, битовом смещении (число или регистр). Если первый операнд - регистр, то битовой базой считается бит 0 в указанном регистре и смещение не может превышать 15 или 31 (в зависимости от размера регистра); если оно превышает эти границы, в качестве смещения будет использован остаток от деления его на 16 или 32 соответственно. Если первый операнд - переменная, то в качестве битовой базы используется бит 0 указанного байта в памяти, а смещение может принимать значения от 0 до 31, если оно указано непосредственно (старшие биты процессором игнорируются), и от -231 до 231–1, если оно указано в регистре.
 |
|
Несмотря на то что эта команда считывает единственный бит из памяти, процессор считывает целое двойное слово по адресу База+(4*(Смещение/32)) или слово по адресу База+(2*(Смещение/16)), в зависимости от разрядности адреса, так что не следует пользоваться ВТ вблизи от не доступных для чтения областей памяти. | |
|
После выполнения команды ВТ флаг CF равен значению считанного бита, флаги OF, SF, ZF, AF и PF не определены.
| Команда: |
BTS база, смещение |
| Назначение: |
Проверка и установка бита |
| Команда: |
BTR база, смещение |
| Назначение: |
Проверка и сброс бита |
| Команда: |
BTC база, смещение |
| Назначение: |
Проверка и инверсия бита |
| Процессор: |
80386 |
Эти три команды соответственно устанавливают в 1 (BTS), сбрасывают в 0 (ВТR) и инвертируют (ВТС) значение бита, который находится в битовой строке с началом, указанным в базе (регистр или переменная), и смещением, указанным во втором операнде (число от 0 до 31 или регистр). Если битовая база - регистр, то смещение не может превышать 15 или 31 в зависимости от разрядности этого регистра. Если битовая база - переменная в памяти, то смещение может принимать значения от -231 до 231–1 (если оно указано в регистре).
После выполнения команд BTS, BTR и ВТС флаг CF равен значению считанного бита до его изменения в результате действия команды, флаги OF, SF, ZF, AF и PF не определены.
| Команда: |
BSF приемник, источник |
| Назначение: |
Прямой поиск бита |
| Команда: |
BSR база, смещение |
| Назначение: |
Обратный поиск бита |
| Процессор: |
80386 |
BSF сканирует источник (регистр или переменная), начиная с самого младшего бита, и записывает в приемник (регистр) номер первого встретившегося бита, равного 1. Команда BSR сканирует источник, начиная с самого старшего бита, и возвращает номер первого встретившегося ненулевого бита, считая от нуля, то есть, если источник равен 0100 0000 0000 0010b, то BSF возвратит 1 a BSR - 14.
Если весь источник равен нулю, значение приемника не определено и флаг ZF устанавливается в 1, иначе ZF всегда сбрасывается. Флаги CF, OF, SF, AF и PF не определены.
| Команда: |
SETcc приемник |
| Назначение: |
Установка байта по условию |
| Процессор: |
80386 |
Это набор команд, которые устанавливают приемник (восьмибитный регистр или переменная размером в один байт) в 1 или 0, если удовлетворяется или не удовлетворяется определенное условие. Условием в каждом случае реально является состояние тех или иных флагов, но, если команда из набора SETcc используется сразу после СМР, условия приобретают формулировки, соответствующие отношениям между операндами СМР (см. табл. 6). Скажем, если операнды СМР были неравны, то команда SETNE, выполненная сразу после этого СМР, установит значение своего операнда в 1.
Слова "выше" и "ниже" в таблице относятся к сравнению чисел без знака, слова "больше" и "меньше" учитывают знак.
Таблица 6. Команды SETcc
| Код команды |
Реальное условие |
Условие для CMP |
SETA SETNBE |
CF = 0 и ZF = 0 |
если выше если не ниже или равно |
SETAE SETNB SETNC |
CF = 0 |
если выше или равно если не ниже если нет переноса |
SETB SETNAE SETC |
CF = 1 |
если ниже если не выше или равно если перенос |
SETBE SETNA |
CF = 1 и ZF = 1 |
если ниже или равно если не выше |
SETE SETZ |
ZF = 1 |
если равно если ноль |
SETG SETNLE |
ZF = 0 и SF = OF |
если больше если не меньше или равно |
SETGE SETNL |
SF = OF |
если больше или равно если не меньше |
SETL SETNGE |
SF <> OF |
если меньше если не больше или равно |
SETLE SETNG |
ZF = 1 и SF <> OF |
если меньше или равно если не больше |
SETNE SETNZ |
ZF = 0 |
если не равно если не ноль |
| SETNO |
OF = 0 |
если нет переполнения |
| SETO |
OF = 1 |
если есть переполнение |
SETNP SETPO |
PF = 0 |
если нет четности если нечетное |
SETP SETPE |
PF = 1 |
если есть четность если четное |
| SETNS |
SF = 0 |
если нет знака |
| SETS |
SF = 1 |
если есть знак |
Основные непривилегированные команды
В этой главе описаны все непривилегированные команды процессоров Intel серии х86, включая команды расширений IA NPX (чаще называемый FPU - расширение для работы с числами с плавающей запятой) и IA MMX (мультимедийное расширение). Для каждой команды указана форма записи, название и модель процессоров Intel, начиная с которой она поддерживается: 8086, 80186, 80286, 80386, 80486, Р5 (Pentium), MMX, P6 (Pentium Pro и Pentium II).
Пересылка данных
| Команда: |
MOV приемник, источник |
| Назначение: |
Пересылка данных |
| Процессор: |
8086 |
Базовая команда пересылки данных. Копирует содержимое источника в приемник, источник не изменяется. Команда MOV действует аналогично операторам присваивания из языков высокого уровня, то есть команда
mov ax,bx
эквивалентна выражению
ах := bх;
языка Паскаль или
ах = bх;
языка С, за исключением того, что команда ассемблера позволяет работать не только с переменными в памяти, но и со всеми регистрами процессора.
В качестве источника для MOV могут использоваться: число (непосредственный операнд), регистр общего назначения, сегментный регистр или переменная (то есть операнд, находящийся в памяти). В качестве приемника - регистр общего назначения, сегментный регистр (кроме CS) или переменная. Оба операнда должны быть одного и того же размера - байт, слово или двойное слово.
Нельзя выполнять пересылку данных с помощью MOV из одной переменной в другую, из одного сегментного регистра в другой и нельзя помещать в сегментный регистр непосредственный операнд - эти операции выполняют двумя командами MOV (из сегментного регистра в обычный и уже из него в другой сегментный) или парой команд PUSH/POP.
 |
|
Загрузка регистра SS командой MOV автоматически запрещает прерывания до окончания следующей за этим команды MOV, так что можно загрузить SS и ESP двумя последовательными командами MOV, не опасаясь, что в этот момент произойдет прерывание, обработчик которого получит неправильный стек. В любом случае для загрузки значения в регистр SS предпочтительнее команда LSS. | |
|
| Команда: |
CMOVcc приемник, источник |
| Назначение: |
Условная пересылка данных |
| Процессор: |
P6 |
Это набор команд, которые копируют содержимое источника в приемник, если удовлетворяется то или иное условие (см. табл. 5). Источником может быть регистр общего назначения или переменная, а приемником - только регистр. Условие, которое должно удовлетворяться, - просто равенство нулю или единице тех или иных флагов из регистра FLAGS, но, если использовать команды CMOVcc сразу после команды СМР (сравнение) с теми же операндами, условия приобретают особый смысл, например:
cmp ах,bх ; сравнить ах и bх cmovl ax,bx ; если ах < bх, скопировать bх в ах
Слова "выше" и "ниже" в таблице 5 относятся к сравнению чисел без знака, слова "больше" и "меньше" учитывают знак.
Таблица 5. Разновидности команды CMOVcc
| Код команды |
Реальное условие |
Условие для CMP |
CMOVA CMOVNBE |
CF = 0 и ZF = 0 |
если выше если не ниже или равно |
CMOVAE CMOVNB CMOVNC |
CF = 0 |
если выше или равно если не ниже если нет переноса |
CMOVB CMOVNAE CMOVC |
CF = 1 |
если ниже если не выше или равно если перенос |
CMOVBE CMOVNA |
CF = 1 и ZF = 1 |
если ниже или равно если не выше |
CMOVE CMOVZ |
ZF = 1 |
если равно если ноль |
CMOVG CMOVNLE |
ZF = 0 и SF = OF |
если больше если не меньше или равно |
CMOVGE CMOVNL |
SF = OF |
если больше или равно если не меньше |
CMOVL CMOVNGE |
SF <> OF |
если меньше если не больше или равно |
CMOVLE CMOVNG |
ZF = 1 и SF <> OF |
если меньше или равно если не больше |
CMOVNE CMOVNZ |
ZF = 0 |
если не равно если не ноль |
| CMOVNO |
OF = 0 |
если нет переполнения |
| CMOVO |
OF = 1 |
если есть переполнение |
CMOVNP CMOVPO |
PF = 0 |
если нет четности если нечетное |
CMOVP CMOVPE |
PF = 1 |
если есть четность если четное |
| CMOVNS |
SF = 0 |
если нет знака |
| CMOVS |
SF = 1 |
если есть знак |
| Команда: |
XCHG операнд1, операнд2 |
| Назначение: |
Обмен операндов между собой |
| Процессор: |
8086 |
Содержимое операнда 2 копируется в операнд 1, а старое содержимое операнда 1 - в операнд 2. XCHG можно выполнять над двумя регистрами или над регистром и переменной.
xchg eax,ebx ; то же, что три команды на языке С: ; temp = eax; eax = ebx; ebx = temp; xchg al,al ; а эта команда не делает ничего
| Команда: |
BSWAP регистр32 |
| Назначение: |
Обмен байт внутри регистра |
| Процессор: |
80486 |
Обращает порядок байт в 32-битном регистре. Биты 0 – 7 (младший байт младшего слова) меняются местами с битами 24 – 31 (старший байт старшего слова), а биты 8 – 15 (старший байт младшего слова) меняются местами с битами 16 – 23 (младший байт старшего слова).
mov eax,12345678h bswap eax ; теперь в еах находится 78563412h
Чтобы обратить порядок байт в 16- битном регистре, следует использовать команду XCHG:
xchg al,ah ; обратить порядок байт в АХ
В процессорах Intel команду BSWAP можно использовать и для обращения порядка байт в 16-битных регистрах, но в некоторых совместимых процессорах других фирм этот вариант BSWAP не реализован.
| Команда: |
PUSH источник |
| Назначение: |
Поместить данные в стек |
| Процессор: |
8086 |
Помещает содержимое источника в стек. Источником может быть регистр, сегментный регистр, непосредственный операнд или переменная. Фактически эта команда копирует содержимое источника в память по адресу SS:[ESP] и уменьшает ESP на размер источника в байтах (2 или 4). Команда PUSH практически всегда используется в паре с POP (считать данные из стека). Так, например, чтобы скопировать содержимое одного сегментного регистра в другой (что нельзя выполнить одной командой MOV), можно использовать такую последовательность команд:
push cs pop ds ; теперь DS указывает на тот же сегмент, что и CS
Другое частое применение команд PUSH/POP - временное хранение переменных, например:
push eax ; сохраняет текущее значение ЕАХ ... ; здесь располагаются какие-нибудь команды, ; которые используют ЕАХ, например CMPXCHG pop eax ; восстанавливает старое значение ЕАХ
Начиная с 80286, команда PUSH ESP (или SP) помещает в стек значение ESP до того, как эта же команда его уменьшит, в то время как на 8086 SP помещался в стек уже уменьшенным на два.
| Команда: |
POP приемник |
| Назначение: |
Считать данные из стека |
| Процессор: |
8086 |
Помещает в приемник слово или двойное слово, находящееся в вершине стека, увеличивая ESP на 2 или 4 соответственно. POP выполняет действие, полностью обратное PUSH. Приемником может быть регистр общего назначения, сегментный регистр, кроме CS (чтобы загрузить CS из стека, надо воспользоваться командой RET), или переменная. Если в роли приемника выступает операнд, использующий ESP для косвенной адресации, команда POP вычисляет адрес операнда уже после того, как она увеличивает ESP.
| Команда: |
PUSHA PUSHAD |
| Назначение: |
Поместить в стек все регистры общего назначения |
| Процессор: |
80186 80386 |
<
/p>
PUSHA помещает в стек регистры в следующем порядке: АХ, СХ, DX, ВХ, SP, ВР, SI и DI. PUSHAD помещает в стек ЕАХ, ЕСХ, EDX, ЕВХ, ESP, EBP, ESI и EDI. (В случае SP и ESP используется значение, которое находилось в этом регистре до начала работы команды.) В паре с командами POPA/POPAD, считывающими эти же регистры из стека в обратном порядке, это позволяет писать подпрограммы (обычно обработчики прерываний), которые не должны изменять значения регистров по окончании своей работы. В начале такой подпрограммы вызывают команду PUSHA, а в конце - РОРА.
 |
|
На самом деле PUSHA и PUSHAD - одна и та же команда с кодом 60h. Ее поведение определяется тем, выполняется ли она в 16- или в 32-битном режиме. Если программист использует команду PUSHAD в 16-битном сегменте или PUSHA в 32-битном, ассемблер просто записывает перед ней префикс изменения размерности операнда (66h).
Это же будет распространяться на некоторые другие пары команд: РОРА/POPAD, POPF/POPFD, PUSHF/PUSHFD, JCXZ/JECXZ, CMPSW/CMPSD, INSW/INSD, LODSW/LODSD, MOVSW/MOVSD, OUTSW/OUTSD, SCASW/SCASD и STOSW/STOSD. |
|
| Команда: |
POPA POPAD |
| Назначение: |
Загрузить из стека все регистры общего назначения |
| Процессор: |
80186 80386 |
Эти команды выполняют действия, полностью обратные действиям PUSHA и PUSHAD, за исключением того, что помещенное в стек значение SP или ESP игнорируется. РОРА загружает из стека DI, SI, BP, увеличивает SP на два, загружает ВХ, DX, CX, AX, a POPAD загружает EDI, ESI, ЕВР, увеличивает ESP на 4 и загружает ЕВХ, EDX, ЕСХ, ЕАХ.
| Команда: |
IN приемник, источник |
| Назначение: |
Считать данные из порта |
| Процессор: |
8086 |
Копирует число из порта ввода-вывода, номер которого указан в источнике, в приемник. Приемником может быть только AL, АХ или ЕАХ. Источник - или непосредственный операнд, или DX, причем можно указывать только номера портов не больше 255.
| Команда: |
OUT приемник, источник |
| Назначение: |
Записать данные в порт |
| Процессор: |
8086 |
Копирует число из источника (AL, АХ или ЕАХ) в порт ввода-вывода, номер которого указан в приемнике. Приемник может быть либо непосредственным номером порта, либо регистром DX. На командах IN и OUT строится все общение процессора с устройствами ввода-вывода - клавиатурой, жесткими дисками, различными контроллерами, и используются они, в первую очередь, в драйверах устройств. Например, чтобы включить динамик PC, достаточно выполнить команды:
in al,61h or al,3 out 61h,al
Программирование портов ввода-вывода рассмотрено подробно в главе 5.10.
| Команда: |
CWD |
| Назначение: |
Конвертирование слова в двойное слово |
| Процессор: |
8086 |
| Команда: |
CDQ |
| Назначение: |
Конвертирование двойного слова в учетверенное |
| Процессор: |
80386 |
Команда CWD превращает слово в AХ в двойное слово, младшая половина которого (биты 0 – 15) остается в АХ, а старшая (биты 16 – 31) располагается в DX. Команда CDQ выполняет аналогичное действие по отношению к двойному слову в ЕАХ, расширяя его до учетверенного слова в EDX:EAX. Эти команды всего лишь устанавливают все биты регистра DX или EDX в значение, равное значению старшего бита регистра АХ или ЕАХ, сохраняя таким образом его знак.
| Команда: |
CBW |
| Назначение: |
Конвертирование байта в слово |
| Процессор: |
8086 |
| Команда: |
CWDE |
| Назначение: |
Конвертирование слова в двойное слово |
| Процессор: |
80386 |
CBW расширяет байт, находящийся в регистре AL, до слова в АХ, CWDE расширяет слово в АХ до двойного слова в ЕАХ. CWDE и CWD отличаются тем, что CWDE располагает свой результат в ЕАХ, в то время как CWD, команда, выполняющая точно такое же действие, располагает результат в паре регистров DX:AX. Так же как и команды CWD/CDQ, расширение выполняется путем установки каждого бита старшей половины результата равным старшему биту исходного байта или слова, то есть:
mov al,0F5h ; AL = 0F5h = 245 = -11 cbw ; теперь АХ = 0FFF5h = 65 525 = -11
 |
|
Так же как и в случае с командами PUSHA/PUSHAD, пара команд CWD/CDQ - это одна команда с кодом 99h, и пара команд CBW/CWDE - одна команда с кодом 98h. Интерпретация этих команд зависит от того, в каком (16-битном или в 32-битном) сегменте они исполняются, и точно так же, если указать CDQ или CWDE в 16-битном сегменте, ассемблер поставит префикс изменения разрядности операнда. |
|
| Команда: |
MOWSX приемник, источник |
| Назначение: |
Пересылка с расширением знака |
| Процессор: |
80386 |
Копирует содержимое источника (регистр или переменная размером в байт или слово) в приемник (16- или 32-битный регистр) и расширяет знак аналогично командам CBW/CWDE.
| Команда: |
MOWZX приемник, источник |
| Назначение: |
Пересылка с расширением нулями |
| Процессор: |
80386 |
<
/p>
Копирует содержимое источника (регистр или переменная размером в байт или слово) в приемник (16- или 32-битный регистр) и расширяет нулями, то есть команда
movzx ax,bl
эквивалентна паре команд
mov al,bl mov ah,0
| Команда: |
XLAT адрес XLATB |
| Назначение: |
Трансляция в соответствии с таблицей |
| Процессор: |
8086 |
Помещает в AL байт из таблицы в памяти по адресу ES:BX (или ES:EBX) со смещением относительно начала таблицы, равным AL. В качестве аргумента для XLAT в ассемблере можно указать имя таблицы, но эта информация никак не используется процессором и служит только как комментарий. Если этот комментарий не нужен, можно применить форму записи XLATB. В качестве примера использования XLAT можно написать следующий вариант преобразования шестнадцатеричного числа в ASCII-код соответствующего ему символа:
mov al,0Ch mov bx, offset htable xlatb
если в сегменте данных, на который указывает регистр ES, было записано
htable db "0123456789ABCDEF"
то теперь AL содержит не число 0Сh, а ASCII-код буквы "С". Разумеется, это преобразование можно выполнить, используя гораздо более компактный код всего из трех арифметических команд, который будет рассмотрен в описании команды DAS, но с XLAT можно выполнять любые преобразования такого рода.
| Команда: |
LEA приемник, источник |
| Назначение: |
Вычисление эффективного адреса |
| Процессор: |
8086 |
Вычисляет эффективный адрес источника (переменная) и помещает его в приемник (регистр). С помощью LEA можно вычислить адрес переменной, которая описана сложным методом адресации, например по базе с индексированием. Если адрес 32-битный, а регистр-приемник 16-битный, старшая половина вычисленного адреса теряется, если наоборот, приемник 32-битный, а адресация 16-битная, то вычисленное смещение дополняется нулями.
 |
|
Команду LEA часто используют для быстрых арифметических вычислений, например умножения:
lea bx,[ebx+ebx*4] ; ВХ=ЕВХ*5
или сложения:
lea ebx,[eax+12] ; ЕВХ=ЕАХ+12
(эти команды меньше, чем соответствующие MOV и ADD, и не изменяют флаги) |
|
Прямая адресация
Если известен адрес операнда, располагающегося в памяти, можно использовать этот адрес. Если операнд - слово, находящееся в сегменте, на который указывает ES, со смещением от начала сегмента 0001, то команда
mov ax,es:0001
поместит это слово в регистр AX. В реальных программах обычно для задания статических переменных используют директивы определения данных (глава 3.3), которые позволяют ссылаться на статические переменные не по адресу, а по имени. Тогда, если в сегменте, указанном в ES, была описана переменная word_var размером в слово, можно записать ту же команду как
mov ax,es:word_var
В таком случае ассемблер сам заменит слово "word_var" на соответствующий адрес. Если селектор сегмента данных находится в DS, имя сегментного регистра при прямой адресации можно не указывать, DS используется по умолчанию. Прямая адресация иногда называется адресацией по смещению.
Адресация отличается для реального и защищенного режимов. В реальном режиме (так же как и в режиме V86) смещение всегда 16-битное, это значит, что ни непосредственно указанное смещение, ни результат сложения содержимого разных регистров в более сложных методах адресации не могут превышать границ слова. При программировании для Windows, для DOS4G, PMODE и в других ситуациях, когда программа будет запускаться в защищенном режиме, смещение не может превышать границ двойного слова.
Процессоры Intel в реальном режиме
Процессор Intel x86 после включения питания оказывается в так называемом режиме реальной адресации памяти, или просто реальном режиме. Большинство операционных систем сразу же переводят его в защищенный режим, позволяющий им обеспечивать многозадачность, распределение памяти и другие функции. Пользовательские программы в таких операционных системах часто работают еще в одном режиме, режиме V86, из которого им доступно все то же, что и из реального, кроме команд, относящихся к управлению защищенным режимом. Таким образом, эта глава описывает не только реальный режим, но и V86, то есть все то, что доступно программисту, если он не проектирует операционную систему или DPMI-сервер, в подавляющем большинстве случаев.
Расширение AMD 3D
Процессоры AMD, начиная с AMD К6 3D, поддерживают дополнительное расширение набора команд ММХ. В AMD 3D вводится новый тип данных - упакованные 32-битные вещественные числа, определяются новые команды (начинающиеся с PF) и несколько дополнительных команд для работы с обычными ММХ-типами данных:
PI2FD приемник, источник - преобразовывает упакованные 32-битные целые со знаком (двойные слова) в упакованные вещественные числа;
PF2ID приемник, источник - преобразовывает упакованные вещественные в упакованные целые числа со знаком (преобразование с насыщением);
PAVGUSB приемник, источник - вычисляет средние арифметические для упакованных 8-битных целых чисел без знака;
PMULHRW приемник, источник - перемножает упакованные 16-битные целые со знаком и сохраняет результаты как 16-битные целые в приемнике (при переполнениях выполняется насыщение);
PFACC приемник, источник - сумма вещественных чисел в приемнике помещается в младшую половину приемника, сумма вещественных чисел из источника помещается в старшую половину приемника;
PFADD приемник, источник - сложение упакованных вещественных чисел;
PFSUB приемник, источник - вычитание упакованных вещественных чисел;
PFSUBR приемник, источник - обратное вычитание (приемник из источника) упакованных вещественных чисел;
PFMUL приемник,источник - умножение упакованных вещественных чисел.
Набор команд для быстрого вычисления по итерационным формулам:
Быстрое деление:
xi+1 = хi(2 - bхi)
х0 = PFRCP(b)
х1 = PFRCPIT1(b, x0)
х2 = PFRCPIT2(x1, x0)
х4 = PFMUL(b, x3)
Быстрое вычисление квадратного корня:
хi+1 = хi(3 - bxi2)/2
х0 = PFRSQRT(b)
х1 = PFMUL(x0, x0)
х2 = PFRSQIT(b, x1)
х3 = PFRCPIT2(x2, x0)
х4 = PFMUL(b, x3)
PFCMPEQ приемник, источник - проверка равенства для упакованных вещественных чисел (полностью аналогично PCMPEQW);
PFCMPGE приемник, источник - сравнение упакованных вещественных чисел: если число в приемнике больше или равно числу в источнике, все его биты устанавливаются в 1;
PFCMPGT приемник, источник - сравнение упакованных вещественных чисел: если число в приемнике больше числа в источнике, все его биты устанавливаются в 1;
PFMAX приемник, источник - сохраняет в приемнике максимальное из каждой пары сравниваемых вещественных чисел;
PFMIN приемник, источник - сохраняет в приемнике минимальное из каждой пары сравниваемых вещественных чисел;
FEMMS - более быстрая версия команды EMMS;
PREFETCH источник - заполняет строку кэша L1 из памяти по адресу, указанному источником;
PREFETCHW источник - заполняет строку кэша L1 из памяти по адресу, указанному источником, и помечает как модифицированную.
Расширение IА ММХ
Начиная с модификации процессора Pentium Р54С, все процессоры Intel содержат расширение ММХ, предназначенное для увеличения эффективности программ, работающих с большими потоками данных (обработка изображений, видео, синтез и обработка звука), то есть для всех тех случаев, когда нужно выполнить несложные операции над большими массивами однотипных чисел. ММХ предоставляет несколько новых типов данных, регистров и команд, позволяющих выполнять арифметические и логические операции над несколькими числами одновременно.
Регистр флагов
Еще один важный регистр, использующийся при выполнении большинства команд, - регистр флагов EFLAGS. Как и раньше, его младшие 16 бит, представлявшие из себя весь этот регистр до 80386, называются FLAGS. В этом регистре каждый бит является флагом, то есть устанавливается в 1 при определенных условиях или установка его в 1 изменяет поведение процессора. Все флаги, расположенные в старшем слове регистра EFLAGS, имеют отношение к управлению защищенным режимом, поэтому здесь рассмотрен только регистр FLAGS (рис. 5).
Рис. 5. Регистр флагов FLAGS
CF - флаг переноса. Устанавливается в 1, если результат предыдущей операции не уместился в приемнике и произошел перенос из старшего бита или если требуется заем (при вычитании), иначе устанавливается в 0. Например, после сложения слова 0FFFFh и 1, если регистр, в который надо поместить результат, - слово, в него будет записано 0000h и флаг CF = 1.
PF - флаг четности. Устанавливается в 1, если младший байт результата предыдущей команды содержит четное число бит, равных 1; устанавливается в 0, если число единичных бит нечетное. (Это не то же самое, что делимость на два. Число делится на два без остатка, если его самый младший бит равен нулю, и не делится, если он равен 1.)
AF - флаг полупереноса или вспомогательного переноса. Устанавливается в 1, если в результате предыдущей операции произошел перенос (или заем) из третьего бита в четвертый. Этот флаг используется автоматически командами двоично-десятичной коррекции.
ZF - флаг нуля. Устанавливается в 1, если результат предыдущей команды - ноль.
SF - флаг знака. Этот флаг всегда равен старшему биту результата.
TF - флаг ловушки. Этот флаг был предусмотрен для работы отладчиков, не использующих защищенный режим. Установка его в 1 приводит к тому, что после выполнения каждой команды программы управление временно передается отладчику (вызывается прерывание 1 - см. описание команды INT).
IF - флаг прерываний. Установка этого флага в 1 приводит к тому, что процессор перестает обрабатывать прерывания от внешних устройств (см. описание команды INT). Обычно его устанавливают на короткое время для выполнения критических участков кода.
DF - флаг направления. Этот флаг контроллирует поведение команд обработки строк - когда он установлен в 1, строки обрабатываются в сторону уменьшения адресов, а когда DF = 0 - наоборот.
OF - флаг переполнения. Этот флаг устанавливается в 1, если результат предыдущей арифметической операции над числами со знаком выходит за допустимые для них пределы. Например, если при сложении двух положительных чисел получается число со старшим битом, равным единице (то есть отрицательное) и наоборот.
Флаги IOPL (уровень привелегий ввода-вывода) и NT (вложенная задача) применяются в защищенном режиме.
Регистровая адресация
Операнды могут располагаться в любых регистрах общего назначения и сегментных регистрах. В этом случае в тексте программы указывается название соответствующего регистра, например команда, копирующуя в регистр AX содержимое регистра BX, записывается как
mov ax,bx
Регистры FPU
FPU предоставляет восемь регистров для хранения данных и пять вспомогательных регистров.
Регистры данных (R0 – R7) не адресуются по именам, как регистры основного процессора. Вместо этого эти восемь регистров рассматриваются как стек, вершина которого называется ST, а более глубокие элементы - ST(1), ST(2) и так далее до ST(7). Если, например, в какой-то момент времени регистр R5 называется ST (рис. 13), то после записи в этот стек числа оно будет записано в регистр R4, который станет называться ST, R5 станет называться ST(1) и т.д.
Рис. 13. Регистры FPU
 |
|
К регистрам R0 – R7 нельзя обращаться напрямую, по именам, но если процессор поддерживает расширение ММХ, то мантиссы, находящиеся в этих регистрах, становятся доступны, как ММ0 – ММ7. | |
|
Регистр состояний SR содержит слово состояния FPU:
Бит 15: В - занятость FPU - этот флаг существует для совместимости с 8087, и его значение всегда совпадает с ES.
Бит 14: С3 - условный флаг 3.
Биты 13 – 11: ТОР - число от 0 до 7, показывающее, какой из регистров данных R0 – R7 в настоящий момент является вершиной стека.
Бит 10: С2 - условный флаг 2.
Бит 9: С1 - условный флаг 1.
Бит 8: С0 - условный флаг 0.
Бит 7: ES - общий флаг ошибки - равен 1, если произошло хотя бы одно немаскированное исключение.
Бит 6: SF - ошибка стека. Если С1 = 1, произошло переполнение (команда пыталась писать в непустую позицию в стеке), если С1 = 0, произошло антипереполнение (команда пыталась считать число из пустой позиции в стеке).
Бит 5: РЕ - флаг неточного результата - результат не может быть представлен точно.
Бит 4: UE - флаг антипереполнения - результат слишком маленький.
Бит 3: ОЕ - флаг переполнения - результат слишком большой.
Бит 2: ZE - флаг деления на ноль - выполнено деление на ноль.
Бит 1: DE - флаг денормализованного операнда - выполнена операция над денормализованным числом.
Бит 0: IE - флаг недопустимой операции - произошла ошибка стека (SF = 1) или выполнена недопустимая операция.
Биты С0 – С3 употребляются так же, как и биты, состояния в основном процессоре, - их значения отражают результат выполнения предыдущей команды и используются для условных переходов; команды
fstsw ax sahf
копируют их значения в регистр FLAGS так, что флаг С0 переходит в CF, С2 - в PF, а С3 - в ZF (флаг С2 теряется).
Биты 0 – 5 отражают различные ошибочные ситуации, которые могут возникать при выполнении команд FPU. Они рассмотрены в описании управляющих регистров.
Регистр управления CR:
Биты 15 – 13 - зарезервированы.
Бит 12 "IC" - управление бесконечностью (поддерживается для совместимости с 8087 и 80287 - вне зависимости от значения этого бита +

> -

).
Биты 11 – 10 "RC" - управление округлением.
Биты 9 – 8 "PC" - управление точностью.
Биты 7 – 6 - зарезервированы.
Бит 5 "РМ" - маска неточного результата.
Бит 4 "UM" - маска антипереполнения.
Бит 3 "ОМ" - маска переполнения.
Бит 2 "ZM" - маска деления на ноль.
Бит 1 "DM" - маска денормализованного операнда.
Бит 0 "IM" - маска недействительной операции.
Биты RC определяют способ округления результатов команд FPU до заданной точности (табл. 10).
Таблица 10. Способы округления
| Значение RC |
Способ округления |
| 0 |
к ближайшему числу |
| 1 |
к отрицательной бесконечности |
| 2 |
к положительной бесконечности |
| 3 |
к нулю |
Биты PC определяют точность результатов команд FADD, FSUB, FSUBR, FMUL, FDIV, FDIVR и FSQRT (табл. 11).
Таблица 11. Точность результатов
| Значение PC |
Точность результатов |
| 0 |
одинарная точность (32-битные числа) |
| 1 |
зарезервировано |
| 2 |
двойная точность (64-битные числа) |
| 3 |
расширенная точность (80-битные числа) |
Биты 0 – 5 регистра CR маскируют соответствующие исключения - если маскирующий бит установлен, исключения не происходит, а результат вызвавшей его команды определяется правилами для каждого исключения специально.
Регистр тегов TW содержит восемь пар бит, описывающих содержание каждого регистра данных, - биты 15 – 14 описывают регистр R7, 13 – 12 - R6 и т.д. Если пара бит (тег) равна 11, соответствующий регистр пуст. 00 означает, что регистр содержит число, 01 - ноль, 10 - нечисло, бесконечность, денормализованное число, неподдерживаемое число.
Регистры FIP и FDP содержат адрес последней выполненной команды (кроме FINIT, FCLEX, FLDCW, FSTCW, FSTSW, FSTSWAX, FSTENV, FLDENV, FSAVE, FRSTOR и FWAIT) и адрес ее операнда соответственно и используются в обработчиках исключений для анализа вызвавшей его команды.
Регистры ММХ
Расширение ММХ включает в себя восемь 64-битных регистров общего пользования ММ0 – ММ7, показанных на рис. 14.
Рис. 14. Регистры ММХ
Физически никаких новых регистров с введением ММХ не появилось - ММ0 – ММ7 - это в точности мантиссы восьми регистров FPU, от R0 до R7. При записи числа в регистр ММХ оно появляется в битах 63 – 0 соответствующего регистра FPU, а экспонента (биты 78 – 64) и ее знаковый бит (бит 79) заполняются единицами. Запись числа в регистр FPU также приводит к изменению соответствующего регистра ММХ. Любая команда ММХ, кроме EMMS, приводит к тому, что поле ТОР регистра SR и весь регистр TW в FPU обнуляются. Команда EMMS заполняет регистр TW единицами. Таким образом, нельзя одновременно пользоваться командами для работы с числами с плавающей запятой и командами ММХ, а если это необходимо - следует пользоваться командами FSAVE/FRSTQR,каждый раз перед переходом от использования FPU к ММХ и обратно (эти команды сохраняют состояние регистров ММХ точно так же, как и FPU).
Регистры общего назначения
32-битные регистры EAX (аккумулятор), EBX (база), ECX (счетчик), EDX (регистр данных) могут использоваться без ограничений для любых целей - временного хранения данных, аргументов или результатов различных операций. Названия этих регистров происходят от того, что некоторые команды применяют их специальным образом: так, аккумулятор часто используется для хранения результата действий, выполняемых над двумя операндами, регистр данных в этих случаях получает старшую часть результата, если он не умещается в аккумулятор, регистр-счетчик используется как счетчик в циклах и строковых операциях, а регистр-база используется при так называемой адресации по базе. Младшие 16 бит каждого из этих регистров могут использоваться как самостоятельные регистры и имеют имена (соответственно AX, BX, CX, DX). На самом деле в процессорах 8086 – 80286 все регистры имели размер 16 бит и назывались именно так, а 32-битные EAX – EDX появились с введением 32-битной архитектуры в 80386. Кроме этого, отдельные байты в 16-битных регистрах AX – DX тоже имеют свои имена и могут использоваться как 8-битные регистры. Старшие байты этих регистров называются AH, BH, CH, DH, а младшие - AL, BL, CL, DL (рис. 3).
Рис. 3. Регистры общего назначения
Другие четыре регистра общего назначения - ESI (индекс источника), EDI (индекс приемника), EBP (указатель базы), ESP (указатель стека) - имеют более конкретное назначение и могут применяться для хранения всевозможных временных переменных, только когда они не используются по назначению. Регистры ESI и EDI используются в строковых операциях, EBP и ESP используются при работе со стеком (см. параграф 2.1.3). Так же, как и с регистрами EAX – EDX, младшие половины этих четырех регистров называются SI, DI, BP и SP соответственно, и в процессорах до 80386 только они и присутствовали.
Регистры процессора
Начиная с 80386, процессоры Intel предоставляют 16 основных регистров для пользовательских программ плюс еще 11 регистров для работы с числами с плавающей запятой (FPU/NPX) и мультимедийными приложениями (MMX). Все команды так или иначе изменяют значения регистров, и всегда быстрее и удобнее обращаться к регистру, чем к памяти.
Помимо основных регистров из реального (но не из виртуального) режима доступны также регистры управления памятью (GDTR, IDTR, TR, LDTR) регистры управления (CR0, CR1 – CR4), отладочные регистры (DR0 – DR7) и машинно-специфичные регистры, но они не применяются для повседневных задач и будут рассматриваться в соответствующих главах позже.
Сдвиговые операции ММХ
| Команда: |
PSLLW приемник,источник PSLLD приемник,источник PSLLQ приемник,источник |
| Назначение: |
Логический сдвиг влево |
| Процессор: |
ММХ |
Команды сдвигают влево биты в каждом элементе (в словах - для PSLLW, в двойных словах - для PSLLD, во всем регистре - для PSLLQ) приемника (регистр ММХ) на число бит, указанное в источнике (8-битное число, регистр ММХ или переменная). При сдвиге младшие биты заполняются нулями, так что, например, команды
psllw mm0,15 pslld mm0,31 psllq mm0,63
обнуляют регистр ММ0.
| Команда: |
PSRLW приемник,источник PSRLD приемник,источник PSRLQ приемник,источник |
| Назначение: |
Логический сдвиг вправо |
| Процессор: |
ММХ |
Команды сдвигают вправо биты в каждом элементе (в словах - для PSRLW, в двойных словах - для PSRLD, во всем регистре - для PSRLQ) приемника (регистр ММХ) на число бит, указанное в источнике (8-битное число, регистр ММХ или переменная). При сдвиге старшие биты заполняются нулями.
| Команда: |
PSRAW приемник,источник PSRAD приемник,источник |
| Назначение: |
Арифметический сдвиг вправо |
| Процессор: |
ММХ |
Команды сдвигают вправо биты в каждом элементе (в словах - для PSRAW и в двойных словах - для PSRAD) приемника (регистр ММХ) на число бит, указанное в источнике (8-битное число, регистр ММХ или переменная). При сдвиге самый старший (знаковый) бит используется для заполнения пустеющих старших бит, так что фактически происходит знаковое деление на 2 в степени, равной содержимому источника.
Сдвиговые операции
| Команда: |
SAR приемник, счетчик |
| Назначение: |
Арифметический сдвиг вправо |
| Команда: |
SAL приемник, счетчик |
| Назначение: |
Арифметический сдвиг влево |
| Команда: |
SHR приемник, счетчик |
| Назначение: |
Логический сдвиг вправо |
| Команда: |
SHL приемник, счетчик |
| Назначение: |
Логический сдвиг влево |
| Процессор: |
8086 |
Эти четыре команды выполняют двоичный сдвиг приемника (регистр или переменная) вправо (в сторону старшего бита) или влево (в сторону младшего бита) на значение счетчика (число или регистр CL, из которого учитываются только младшие пять бит, которые могут принимать значения от 0 до 31), Операция сдвига на 1 эквивалентна умножению (сдвиг влево) или делению (сдвиг вправо) на 2. Так, число 0010b (2) после сдвига на 1 влево превращается в 0100b (4). Команды SAL и SHL выполняют одну и ту же операцию (на самом деле это одна и та же команда) - на каждый шаг сдвига старший бит заносится в CF, все биты сдвигаются влево на одну позицию, и младший бит обнуляется. Команда SHR выполняет прямо противоположную операцию: младший бит заносится в CF, все биты сдвигаются на 1 вправо, старший бит обнуляется. Эта команда эквивалентна беззнаковому целочисленному делению на 2. Команда SAR действует по аналогии с SHR, только старший бит не обнуляется, а сохраняет предыдущее значение, так что, например, число 11111100b (-4) перейдет в 11111110b (-2). SAR, таким образом, эквивалентна знаковому делению на 2, но, в отличие от IDIV, округление происходит не в сторону нуля, а в сторону отрицательной бесконечности. Так, если разделить -9 на 4 с помощью IDIV, результат будет -2 (и остаток -1), а если выполнить арифметический сдвиг вправо числа -9 на 2, результат будет -3. Сдвиги больше чем на 1 эквивалентны соответствующим сдвигам на 1, выполненным последовательно. Схема всех сдвиговых операций приведена на рис. 7.
Рис. 7. Сдвиговые операции
Сдвиги на 1 изменяют значение флага OF: SAL/SHL устанавливают его в 1, если после сдвига старший бит изменился (то есть старшие два бита исходного числа не были одинаковыми), и в 0, если старший бит остался тем же. SAR устанавливает OF в 0, и SHR устанавливает OF в значение старшего бита исходного числа. Для сдвигов на несколько бит значение OF не определено. Флаги SF, ZF, PF устанавливаются всеми сдвигами в соответствии с результатом, значение AF не определено (кроме случая, если счетчик сдвига равен нулю, в котором ничего не происходит и флаги не изменяются).
В процессорах 8086 непосредственно можно было задавать в качестве второго операнда только число 1 и при использовании CL учитывать все биты, а не только младшие 5, но уже начиная с 80186 эти команды приняли свой окончательный вид.
| Команда: |
SHRD приемник, источник, счетчик |
| Назначение: |
Сдвиг повышенной точности вправо |
| Команда: |
SHLD приемник, источник, счетчик |
| Назначение: |
Сдвиг повышенной точности влево |
| Процессор: |
80386 |
Приемник (регистр или переменная) сдвигается влево (в случае SHLD) или вправо (в случае SHRD) на число бит, указанное в счетчике (число или регистр CL, откуда используются только младшие 5 бит, которые могут принимать значения от 0 до 31). Старший (для SHLD) или младший (в случае SHRD) бит не обнуляется, а считывается из источника (регистр), значение которого не изменяется. Например, если приемник содержал 00101001b, источник 1010b, счетчик равен 3, SHRD даст в результате 01000101b, a SHLD - 01001101b (см. рис. 8).
Рис. 8. Сдвиги двойной точности
Флаг OF устанавливается при сдвигах на 1 бит, если изменился знак приемника, и сбрасывается, если знак не изменился; при сдвигах на несколько бит флаг OF не определен. Во всех случаях SF, ZF и PF устанавливаются в соответствии с результатом и AF не определен, кроме случая со сдвигом на 0 бит, в котором значения флагов не изменяются. Если счетчик больше, чем разрядность приемника, - результат и все флаги не определены.
| Команда: |
ROR приемник, счетчик |
| Назначение: |
Циклический сдвиг вправо |
| Команда: |
ROL приемник, счетчик |
| Назначение: |
Циклический сдвиг влево |
| Команда: |
RCR приемник, счетчик |
| Назначение: |
Циклический сдвиг вправо через флаг переноса |
| Команда: |
RCL приемник, счетчик |
| Назначение: |
Циклический сдвиг влево через флаг переноса |
| Процессор: |
8086 |
Эти команды осуществляют циклический сдвиг приемника (регистр или переменная) на число бит, указанное в счетчике (число или регистр CL, из которого учитываются только младшие пять бит, принимающие значения от 0 до 31). При выполнении циклического сдвига на 1 команды ROR (ROL) сдвигают каждый бит приемника вправо (влево) на одну позицию, за исключением самого младшего (старшего), который записывается в позицию самого старшего (младшего) бита. Команды RCR и RCL выполняют аналогичное действие, но включают флаг CF в цикл, как если бы он был дополнительным битом в приемнике (рис. 9).
Рис. 9. Циклические сдвиги
После выполнения команд циклического сдвига флаг CF всегда равен последнему вышедшему за пределы приемника биту, флаг OF определен только для сдвигов на 1 - он устанавливается, если изменилось значение самого старшего бита, и сбрасывается, если старший бит не изменился. Флаги SF, ZF, AF и PF не изменяются.
Сегментные регистры
При использовании каждой из сегментированных моделей памяти для формирования любого адреса применяются два числа - адрес начала сегмента и смещение искомого байта относительно этого начала (в бессегментной модели памяти flat адреса начал всех сегментов равны). Операционные системы (кроме DOS) могут размещать сегменты, с которыми работает программа пользователя, в разных местах в памяти, и даже могут временно записывать их на диск, если памяти не хватает. Так как сегменты могут оказаться где угодно, программа обращается к ним, используя вместо настоящего адреса начала сегмента 16-битное число, называемое селектором. В процессорах Intel предусмотрено шесть шестнадцатибитных регистров - CS, DS, ES, FS, GS, SS, используемых для хранения селекторов. (Регистры FS и GS отсутствовали в 8086, но появились уже в 80286.) Это не значит, что программа не может одновременно работать с большим количеством сегментов памяти, - в любой момент времени можно изменить значения, записанные в этих регистрах.
 |
|
В реальном режиме селектор любого сегмента равен адресу его начала, деленому на 16. Чтобы получить адрес в памяти, 16-битное смещение складывают с этим селектором, сдвинутым предварительно влево на 4 разряда. Таким образом, оказывается, что максимальный доступный адрес в реальном режиме 220-1 = 1 048 575. Для сравнения, в защищенном режиме адрес начала для каждого сегмента хранится отдельно, так что возможно 246 (64 терабайта) различных логических адресов в формате сегмент:смещение (программа может определить до 16384 сегментов, каждый из которых до 4 Гб), хотя реально процессор может адресоваться только к 4 или 64 (для Pentium Pro) гигабайтам памяти. | |
|
В отличие от регистров DS, ES, GS, FS, которые называются регистрами сегментов данных, регистры CS и SS отвечают за сегменты двух особенных типов - сегмент кода и сегмент стека. Сегмент кода содержит программу, исполняющуюся в данный момент, так что запись нового селектора в этот регистр приводит к тому, что далее будет исполнена не следующая по тексту программы команда, а команда из кода, находящегося в другом сегменте, с тем же смещением. Смещение следующей выполняемой команды всегда хранится в специальном регистре - EIP (указатель инструкции, шестнадцатибитная форма IP), запись в который также приведет к тому, что следующей будет исполнена какая-нибудь другая команда. На самом деле все команды передачи управления - перехода, условного перехода, цикла, вызова подпрограммы и т.п. - и осуществляют эту самую запись в CS и EIP.
Способы адресации
Большинство команд процессора вызывается с аргументами, которые в ассемблере принято называть операндами. Например: команда сложения содержимого регистра с числом требует задания двух операндов - содержимого регистра и числа. Далее рассмотрены все существующие способы задания адреса хранения операндов - способы адресации.
Стек
Стек - это специальным образом организованный участок памяти, используемый для временного хранения переменных, для передачи параметров вызываемым подпрограммам и для сохранения адреса возврата при вызове процедур и прерываний. Легче всего представить стек в виде стопки листов бумаги (это одно из значений слова "stack" в английском языке) - вы можете класть и забирать листы бумаги только с вершины стопки. Таким образом, если записать в стек числа 1, 2, 3, то при чтении они будут получаться в обратном порядке - 3, 2, 1. Стек располагается в сегменте памяти, описываемом регистром SS, а текущее смещение вершины стека записано в регистре ESP, причем при записи в стек значение этого смещения уменьшается, то есть стек растет вниз от максимально возможного адреса (рис. 4). Такое расположение стека "вверх ногами" может быть необходимо, например в бессегментной модели памяти, когда все сегменты, включая сегмент стека и сегмент кода, занимают одну и ту же область - всю память. Тогда программа исполняется в нижней области памяти, в области малых адресов, и EIP растет, а стек располагается в верхней области памяти, и ESP уменьшается.
Рис. 4. Стек
При вызове подпрограммы параметры в большинстве случаев помещают в стек, а в EBP записывают текущее значение ESP. Тогда, если подпрограмма использует стек для хранения локальных переменных, ESP изменится, но EBP можно будет использовать для того, чтобы считывать значения параметров напрямую из стека (их смещения будут записываться как EBP + номер параметра). Более подробно вызовы подпрограмм и все возможные способы передачи параметров рассмотрены в главе 4.3.2.
Строковые операции
Все команды для работы со строками считают, что строка-источник находится по адресу DS:SI (или DS:ESI), то есть в сегменте памяти, указанном в DS со смещением в SI, а строка-приемник - соответственно в ES:DI (или ES:EDI). Кроме того, все строковые команды работают только с одним элементом строки (байтом, словом или двойным словом) за один раз. Для того чтобы команда выполнялась над всей строкой, необходим один из префиксов повторения операций.
| Префикс: |
REP |
| Назначение: |
Повторять |
| Префикс: |
REPE |
| Назначение: |
Повторять, пока равно |
| Префикс: |
REPNE |
| Назначение: |
Повторять, пока не равно |
| Префикс: |
REPZ |
| Назначение: |
Повторять, пока ноль |
| Префикс: |
REPNZ |
| Назначение: |
Повторять, пока не ноль |
| Процессор: |
8086 |
Все эти команды - префиксы для операций над строками. Любой из префиксов выполняет следующую за ним команду строковой обработки столько раз, сколько указано в регистре ЕСХ (или СХ, в зависимости от разрядности адреса), уменьшая его при каждом выполнении команды на 1. Кроме того, префиксы REPZ и REPE прекращают повторения команды, если флаг ZF сброшен в 0, и префиксы REPNZ и REPNE прекращают повторения, если флаг ZF установлен в 1. Префикс REP обычно используется с командами INS, OUTS, MOVS, LODS, STOS, а префиксы REPE, REPNE, REPZ и REPNZ - с командами CMPS и SCAS. Поведение префиксов не с командами строковой обработки не определено.
| Команда: |
MOVS приемник, источник |
| Назначение: |
Копирование строки |
| Процессор: |
8086 |
| Команда: |
MOVSB |
| Назначение: |
Копирование строки байт |
| Процессор: |
8086 |
| Команда: |
MOVSW |
| Назначение: |
Копирование строки слов |
| Процессор: |
8086 |
| Команда: |
MOVSD |
| Назначение: |
Копирование строки двойных слов |
| Процессор: |
80386 |
Копирует один байт (MOVSB), слово (MOVSW) или двойное слово (MOVSD) из памяти по адресу DS:ESI (или DS:SI, в зависимости от разрядности адреса) в память по адресу ES:EDI (или ES:DI). При использовании формы записи MOVS ассемблер сам определяет из типа указанных операндов (принято указывать имена копируемых строк, но можно использовать любые два операнда подходящего типа), какую из трех форм этой команды (MOVSB, MOVSW или MOVSD) выбрать. Используя MOVS с операндами, можно заменить регистр DS на другой с помощью префикса замены сегмента (ES:, GS:, FS:, CS:, SS:), регистр ES заменить нельзя. После выполнения команды регистры ESI (SI) и EDI (DI) увеличиваются на 1, 2 или 4 (если копируются байты, слова или двойные слова), если флаг DF = 0, и уменьшаются, если DF = 1. При использовании с префиксом REP команда MOVS выполняет копирование строки длиной в ЕСХ (или СХ) байт, слов или двойных слов.
| Команда: |
CMPS приемник, источник |
| Назначение: |
Сравнение строк |
| Процессор: |
8086 |
| Команда: |
CMPSB |
| Назначение: |
Сравнение строк байт |
| Процессор: |
8086 |
| Команда: |
CMPSW |
| Назначение: |
Сравнение строк слов |
| Процессор: |
8086 |
| Команда: |
CMPSD |
| Назначение: |
Сравнение строк двойных слов |
| Процессор: |
80386 |
<
/p>
Сравнивает один байт (CMPSB), слово (CMPSW) или двойное слово (CMPSD) из памяти по адресу DS:ESI (или DS:SI, в зависимости от разрядности адреса) с байтом, словом или двойным словом по адресу ES:EDI (или ES:DI) и устанавливает флаги аналогично команде СМР. При использовании формы записи CMPS ассемблер сам определяет из типа указанных операндов (принято указывать имена сравниваемых строк, но можно использовать любые два операнда подходящего типа), какую из трех форм этой команды (CMPSB, CMPSW или CMPSD) выбрать. Используя CMPS с операндами, можно заменить регистр DS на другой, применяя префикс замены сегмента (ES:, GS:, FS:, CS:, SS:), регистр ES заменить нельзя. После выполнения команды регистры ESI (SI) и EDI (DI) увеличиваются на 1, 2 или 4 (если сравниваются байты, слова или двойные слова), если флаг DF = 0, и уменьшаются, если DF = 1. При использовании с префиксом REP команда CMPS выполняет сравнение строки длиной в ЕСХ (или СХ) байт, слов или двойных слов, но чаще ее используют с префиксами REPNE/REPNZ или REPE/REPZ. В первом случае сравнение продолжается до первого несовпадения в сравниваемых строках, а во втором - до первого совпадения.
| Команда: |
SCAS приемник |
| Назначение: |
Сканирование строки |
| Процессор: |
8086 |
| Команда: |
SCASB |
| Назначение: |
Сканирование строки байт |
| Процессор: |
8086 |
| Команда: |
SCASW |
| Назначение: |
Сканирование строки слов |
| Процессор: |
8086 |
| Команда: |
SCASD |
| Назначение: |
Сканирование строки двойных слов |
| Процессор: |
80386 |
Сравнивает содержимое регистра AL (SCASB), AX (SCASW) или ЕАХ (SCASD) с байтом, словом или двойным словом из памяти по адресу ES:EDI (или ES:DI, в зависимости от разрядности адреса) и устанавливает флаги аналогично команде СМР. При использовании формы записи SCAS ассемблер сам определяет из типа указанного операнда (принято указывать имя сканируемой строки, но можно использовать любой операнд подходящего типа), какую из трех форм этой команды (SCASB, SCASW или SCASD) выбрать. После выполнения команды регистр EDI (DI) увеличивается на 1, 2 или 4 (если сканируются байты, слова или двойные слова), если флаг DF = 0, и уменьшается, если DF = 1. При использовании с префиксом REP команда SCAS выполняет сканирование строки длиной в ЕСХ (или СХ) байт, слов или двойных слов, но чаще ее используют с префиксами REPNE/REPNZ или REPE/REPZ. В первом случае сканирование продолжается до первого элемента строки, отличного от содержимого аккумулятора, а во втором - до первого совпадающего.
| Команда: |
LODS источник |
| Назначение: |
Чтение из строки |
| Процессор: |
8086 |
| Команда: |
LODSB |
| Назначение: |
Чтение байта из строки |
| Процессор: |
8086 |
| Команда: |
LODSW |
| Назначение: |
Чтение слова из строки |
| Процессор: |
8086 |
| Команда: |
LODSD |
| Назначение: |
Чтение двойного слова из строки |
| Процессор: |
80386 |
<
/p>
Копирует один байт (LODSB), слово (LODSW) или двойное слово (LODSD) из памяти по адресу DS:ESI (или DS:SI, в зависимости от разрядности адреса) в регистр AL, АХ или ЕАХ соответственно. При использовании формы записи LODS ассемблер сам определяет из типа указанного операнда (принято указывать имя строки, но можно использовать любой операнд подходящего типа), какую из трех форм этой команды (LODSB, LODSW или LODSD) выбрать. Используя LODS с операндом, можно заменить регистр DS на другой с помощью префикса замены сегмента (ES:, GS:, FS:, CS:, SS:). После выполнения команды регистр ESI (SI) увеличивается на 1, 2 или 4 (если считывается байт, слово или двойное слово), если флаг DF = 0, и уменьшается, если DF = 1. При использовании с префиксом REP команда LODS выполнит копирование строки длиной в ЕСХ (или СХ), что приведет к тому, что в аккумуляторе окажется последний элемент строки. На самом деле эту команду используют без префиксов, часто внутри цикла в паре с командой STOS, так что LODS считывает число, другие команды выполняют над ним какие-нибудь действия, а затем STOS записывает измененное число в то же место в памяти.
| Команда: |
STOS приемник |
| Назначение: |
Запись в строку |
| Процессор: |
8086 |
| Команда: |
STOSB |
| Назначение: |
Запись байта в строку |
| Процессор: |
8086 |
| Команда: |
STOSW |
| Назначение: |
Запись слова в строку |
| Процессор: |
8086 |
| Команда: |
STOSD |
| Назначение: |
Запись двойного слова в строку |
| Процессор: |
80386 |
Копирует регистр AL (STOSB), AX (STOSW) или ЕАХ (STOSD) в память по адресу ES:EDI (или ES:DI, в зависимости от разрядности адреса). При использовании формы записи STOS ассемблер сам определяет из типа указанного операнда (принято указывать имя строки, но можно использовать любой операнд подходящего типа), какую из трех форм этой команды (STOSB, STOSW или STOSD) выбрать. После выполнения команды регистр EDI (DI) увеличивается на 1, 2 или 4 (если копируется байт, слово или двойное слово), если флаг DF = 0, и уменьшается, если DF = 1. При использовании с префиксом REP команда STOS заполнит строку длиной в ЕСХ (или СХ) числом, находящимся в аккумуляторе.
| Команда: |
INS источник, DX |
| Назначение: |
Чтение строки из порта |
| Процессор: |
80186 |
| Команда: |
INSB |
| Назначение: |
Чтение строки байт из порта |
| Процессор: |
80186 |
| Команда: |
INSW |
| Назначение: |
Чтение строки слов из порта |
| Процессор: |
80186 |
| Команда: |
INSD |
| Назначение: |
Чтение строки двойных слов из порта |
| Процессор: |
80386 |
<
/p>
Считывает из порта ввода-вывода, номер которого указан в регистре DX, байт (INSB), слово (INSW) или двойное слово (INSD) в память по адресу ES:EDI (или ES:DI, в зависимости от разрядности адреса). При использовании формы записи INS ассемблер определяет из типа указанного операнда, какую из трех форм этой команды (INSB, INSW или INSD) употребить. После выполнения команды регистр EDI (DI) увеличивается на 1, 2 или 4 (если считывается байт, слово или двойное слово), если флаг DF = 0, и уменьшается, если DF = 1. При использовании с префиксом REP команда INS считывает блок данных из порта длиной в ЕСХ (или СХ) байт, слов или двойных слов.
| Команда: |
OUTS DX, приемник |
| Назначение: |
Запись строки в порт |
| Процессор: |
80186 |
| Команда: |
OUTSB |
| Назначение: |
Запись строки байт в порт |
| Процессор: |
80186 |
| Команда: |
OUTSW |
| Назначение: |
Запись строки слов в порт |
| Процессор: |
80186 |
| Команда: |
OUTSD |
| Назначение: |
Запись строки двойных слов в порт |
| Процессор: |
80386 |
Записывает в порт ввода-вывода, номер которого указан в регистре DX, байт (OUTSB), слово (OUTSW) или двойное слово (OUTSD) из памяти по адресу DS:ESI (или DS:SI, в зависимости от разрядности адреса). При использовании формы записи OUTS ассемблер определяет из типа указанного операнда, какую из трех форм этой команды (OUTSB, OUTSW или OUTSD) употребить. Используя OUTS с операндами, также можно заменить регистр DS на другой с помощью префикса замены сегмента (ES:, GS:, FS:, CS:, SS:). После выполнения команды регистр ESI (SI) увеличивается на 1, 2 или 4 (если считывается байт, слово или двойное слово), если флаг DF = 0, и уменьшается, если DF = 1. При использовании с префиксом REP команда OUTS записывает блок данных размером в ЕСХ (или СХ) байт, слов или двойных слов в указанный порт. Все процессоры вплоть до Pentium не проверяли готовность порта принять новые данные в ходе выполнения команды REP OUTS, так что, если порт не успевал обрабатывать информацию с той скоростью, с которой ее поставляла эта команда, часть данных терялась.
Типы данных FPU
Числовой процессор может выполнять операции с семью разными типами данных, представленными в таблице 9, - три целых двоичных, один целый десятичный и три типа данных с плавающей запятой.
Таблица 9. Типы данных FPU
| Тип данных |
Бит |
Количество значащих цифр |
Пределы |
| Целое слово |
16 |
4 |
-32768 - 32767 |
| Короткое целое |
32 |
9 |
-2*109 - 2*109 |
| Длинное целое |
64 |
18 |
-9*1018 - 9*1018 |
| Упакованное десятичное |
80 |
18 |
-99..99 - +99..99 (18 цифр) |
| Короткое вещественное |
32 |
7 |
1.18*10-38 - 3.40*1038 |
| Длинное вещественное |
64 |
15-16 |
2.23*10-308 - 1.79*10308 |
| Расширенное вещественное |
80 |
19 |
3.37*10-4932 - 1.18*104932 |
Вещественные числа хранятся, как и все данные, в форме двоичных чисел. Двоичная запись числа с плавающей запятой аналогична десятичной, только позиции справа от запятой соответствуют не делению на 10 в соответствующей степени, а делению на 2. Переведем для примера в двоичный вид число 0,625:
0,625 - 1/2 = 0,125
1/4 больше, чем 0,125
0,125 - 1/8 = 0
Итак, 0,625 = 0,101b. При записи вещественных чисел всегда выполняют нормализацию - умножают число на такую степень двойки, чтобы перед десятичной точкой стояла единица, в нашем случае
0,625 = 0,101b = 1,01b * 2-1
Говорят, что число имеет мантиссу 1,01 и экспоненту -1. Как можно заметить, при использовании этого алгоритма первая цифра мантиссы всегда равна 1, так что ее можно не писать, увеличивая тем самым точность представления числа дополнительно на 1 бит. Кроме того, значение экспоненты хранят не в виде целого со знаком, а в виде суммы с некоторым числом так, чтобы хранить всегда только положительное число и чтобы было легко сравнивать вещественные числа - в большинстве случаев достаточно сравнить экспоненту. Теперь мы можем рассмотреть вещественные форматы IEEE, используемые в процессорах Intel:
короткое вещественное: бит 31 - знак мантиссы, биты 30 – 23 - 8-битная экспонента + 127, биты 22 – 0 - 23-битная мантисса без первой цифры;
длинное вещественное: бит 63 - знак мантиссы, биты 62 – 52 - 11-битная экспонента + 1024, биты 51 – 0 - 52-битная мантисса без первой цифры;
расширенное вещественное: бит 79 - знак мантиссы, биты 78 – 64 - 15-битная экспонента + 16 383, биты 63 – 0 - 64-битная мантисса с первой цифрой ( то есть бит 63 равен 1).
FPU выполняет все вычисления в 80-битном расширенном формате, а 32- и 64-битные числа используются для обмена данными с основным процессором и памятью.
 |
|
Кроме обычных чисел формат IEEE предусматривает несколько специальных случаев, которые могут получаться в результате математических операций и над которыми также можно выполнять некоторые операции:
положительный ноль: все биты числа сброшены в ноль;
отрицательный ноль: знаковый бит - 1, все остальные биты - нули;
положительная бесконечность: знаковый бит - 0, все биты мантиссы - 0, все биты экспоненты - 1;
отрицательная бесконечность: знаковый бит - 1, все биты мантиссы - 0, все биты экспоненты - 1;
денормализованные числа: все биты экспоненты - 0 (используются для работы с очень маленькими числами - до 10-16445 для расширенной точности);
неопределенность: знаковый бит - 1, первый бит мантиссы (первые два для 80-битных чисел) - 1, а остальные - 0, все биты экспоненты - 1;
не-число типа SNAN (сигнальное): все биты экспоненты - 1, первый бит мантиссы - 0 (для 80-битных чисел первые два бита мантиссы - 10), а среди остальных бит есть единицы;
не-число типа QNAN (тихое): все биты экспоненты - 1, первый бит мантиссы (первые два для 80-битных чисел) - 1, среди остальных бит есть единицы. Неопределенность - один из вариантов QNAN;
неподдерживаемое число: все остальные ситуации.
|
|
Остальные форматы данных FPU также допускают неопределенность - единица в старшем бите и нули в остальных для целых чисел, и старшие 16 бит - единицы для упакованных десятичных чисел.
Типы данных ММХ
ММХ использует четыре новых типа данных:
учетверенное слово - простое 64-битное число;
упакованные двойные слова - два 32-битных двойных слова, упакованные в 64-битный тип данных. Двойное слово 1 занимает биты 63 – 32, и двойное слово 0 занимает биты 31 – 0;
упакованные слова - четыре 16-битных слова, упакованные в 64-битный тип данных. Слово 3 занимает биты 63 – 48, слово 0 занимает биты 15 – 0;
упакованные байты - восемь байт, упакованных в 64-битный тип данных. Байт 7 занимает биты 63 – 56, байт 0 занимает биты 7 – 0.
Команды ММХ перемещают упакованные данные в память или в обычные регистры как целое, но выполняют арифметические и логические операции над каждым элементом по отдельности.
Арифметические операции в ММХ могут использовать специальный способ обработки переполнений и антипереполнений - насыщение. Если результат операции больше, чем максимальное значение для его типа данных (+127 для байта со знаком), то результат считают равным этому максимальному значению. Если он меньше минимального значения - соответственно его полагают равным минимально допустимому значению. Например, при операциях с цветом насыщение позволяет ему превращаться в чисто белый при переполнении и в чисто черный при антипереполнении, в то время как обычная арифметика привела бы к нежелательной инверсии цвета.
Трансцендентные операции FPU
| Команда: |
FSIN |
| Назначение: |
Синус |
| Процессор: |
80387 |
Вычисляет синус числа, находящегося в ST(0), и сохраняет результат в этом же регистре. Операнд считается заданным в радианах и не может быть больше 263 или меньше -263 (можно воспользоваться FPREM с делителем 2

, если операнд слишком велик). Если операнд выходит за эти пределы, флаг С2 устанавливается в 1 и значение ST(0) не изменяется.
| Команда: |
FCOS |
| Назначение: |
Косинус |
| Процессор: |
80387 |
Вычисляет косинус числа, находящегося в ST(0), и сохраняет результат в этом же регистре. Операнд считается заданным в радианах и не может быть больше 263 или меньше -263 (так же, как и в случае синуса, можно воспользоваться FPREM с делителем 2

, если операнд слишком велик). Если операнд выходит за эти пределы, флаг С2 устанавливается в 1 и значение ST(0) не изменяется.
| Команда: |
FSINCOS |
| Назначение: |
Синус и косинус |
| Процессор: |
80387 |
Вычисляет синус и косинус числа, находящегося в ST(0), помещает синус в ST(0) и затем помещает косинус в стек (так что синус оказывается в ST(1), косинус - в ST(0), и ТОР уменьшается на 1). Операнд считается заданным в радианах и не может быть больше 263 или меньше -263. Если операнд выходит за эти пределы, флаг С2 устанавливается в 1 и значение ST(0) и стек не изменяются.
| Команда: |
FPTAN |
| Назначение: |
Тангенс |
| Процессор: |
8087 |
Вычисляет тангенс числа, находящегося в регистре ST(0), заменяет его на вычисленное значение и затем помещает 1 в стек, так что результат оказывается в ST(1), ST(0) содержит 1, а ТОР уменьшается на единицу. Как и для остальных тригонометрических команд, операнд считается заданным в радианах и не может быть больше 263 или меньше -263. Если операнд выходит за эти пределы, флаг С2 устанавливается в 1 и значение ST(0) и стек не изменяются. Единица помещается в стек для того, чтобы можно было получить котангенс вызовом команды FDIVR сразу после FPTAN.
| Команда: |
FPATAN |
| Назначение: |
Арктангенс |
| Процессор: |
8087 |
Вычисляет арктангенс числа, получаемого при делении ST(1) на ST(0), сохраняет результат в ST(1) и выталкивает ST(0) из стека (помечает ST(0) как пустой и увеличивает TOP на 1). Рeзyльтaт всегда имеет тот же знак, что и ST(1), и меньше

по абсолютной величине. Смысл этой операции в том, что FPATAN вычисляет угол между осью абсцисс и линией, проведенной из центра координат в точку ST(1),ST(0).
FPATAN может выполняться над любыми операндами (кроме не-чисел), давая результаты для различных нулей и бесконечностей, определенные в соответствии со стандартом IEEE (как показано в табл. 17).
Таблица 17. Результаты работы команды FPATAN1
| |
- |
-F |
0 |
0 |
+F |
+ |
- |
-3 /4 |
- /2 |
- /2 |
- /2 |
- /2 |
- /4 |
| -F |
- |
от - до - /2 |
- /2 |
- /2 |
от - /2 до -0 |
0 |
| 0 |
- |
- |
- |
0 |
0 |
0 |
| 0 |
+ |
+ |
+ |
0 |
0 |
0 |
| +F |
+ |
от + до + /2 |
+ /2 |
+ /2 |
от + /2 до +0 |
0 |
+ |
+3 /4 |
+ /2 |
+ /2 |
+ /2 |
+ /2 |
+ /4 |
1 F в этой таблице - конечное вещественное число.
| Команда: |
F2XMI |
| Назначение: |
Вычисление 2х-1 |
| Процессор: |
8087 |
Возводит 2 в степень, равную ST(0), и вычитает 1. Результат сохраняется в ST(0). Значение ST(0) должно лежать в пределах от -1 до +1, иначе результат не определен.
| Команда: |
FYL2X |
| Назначение: |
Вычисление у*log2(x) |
| Процессор: |
8087 |
Вычисляет ST(1)*log2(ST(0)), помещает результат в ST(1) и выталкивает ST(0) из стека, так что после этой операции результат оказывается в ST(0). Первоначальное значение ST(0) должно быть неотрицательным. Если регистр ST(0) содержал ноль, результат (если ZM = 1) будет равен бесконечности со знаком, обратным ST(1).
| Команда: |
FYL2XP1 |
| Назначение: |
Вычисление у*log2(x+1) |
| Процессор: |
8087 |
Вычисляет ST(1)*log2(ST(0)+1), помещает результат в ST(1) и выталкивает ST(0) из стека, так что после этой операции результат оказывается в ST(0). Первоначальное значение ST(0) должно быть в пределах от -(1 -

/2) до (1 +

/2), иначе результат не определен. Команда FYL2XP1 дает большую точность для ST(0), близких к нулю, чем FYL2X для суммы того же ST(0) и 1.
Управление флагами
| Команда: |
STC |
| Назначение: |
Установить флаг переноса |
| Процессор: |
8086 |
Устанавливает флаг CF в 1.
| Команда: |
CLC |
| Назначение: |
Сбросить флаг переноса |
| Процессор: |
8086 |
Сбрасывает флаг CF в 0.
| Команда: |
CMC |
| Назначение: |
Инвертировать флаг переноса |
| Процессор: |
8086 |
Инвертирует флаг СF.
| Команда: |
STD |
| Назначение: |
Установить флаг направления |
| Процессор: |
8086 |
Устанавливает флаг DF в 1, так что при последующих строковых операциях регистры DI и SI будут уменьшаться.
| Команда: |
CLD |
| Назначение: |
Сбросить флаг направления |
| Процессор: |
8086 |
Сбрасывает флаг DF в 0, так что при последующих строковых операциях регистры DI и SI будут увеличиваться.
| Команда: |
LAHF |
| Назначение: |
Загрузить флаги состояния в АН |
| Процессор: |
8086 |
Копирует младший байт регистра FLAGS в АН, включая флаги SF (бит 7), ZF (бит 6), AF (бит 4), PF (бит 2) и CF (бит 0). Бит 1 устанавливается в 1, биты 3 и 5 - в 0.
| Команда: |
SAHF |
| Назначение: |
Загрузить флаги состояния из АН |
| Процессор: |
8086 |
Загружает флаги SF, ZF, AF, PF и CF из регистра АН значениями бит 7, 6, 4, 2 и 0 соответственно. Зарезервированные биты 1, 3 и 5 регистра флагов не изменяются.
| Команда: |
PUSHF |
| Назначение: |
Поместить FLAGS в стек |
| Процессор: |
8086 |
| Команда: |
PUSHFD |
| Назначение: |
Поместить ЕFLAGS в стек |
| Процессор: |
80386 |
Эти команды копируют содержание регистра FLAGS или EFLAGS в стек (уменьшая SP или ESP на 2 или 4 соответственно). При копировании регистра EFLAGS флаги VM и RF (биты 16 и 17) не копируются, соответствующие биты в двойном слове, помещенном в стек, обнуляются.
| Команда: |
POPF |
| Назначение: |
Загрузить FLAGS из стека |
| Процессор: |
8086 |
| Команда: |
POPFD |
| Назначение: |
Загрузить EFLAGS из стека |
| Процессор: |
80386 |
Считывает из вершины стека слово (POPF) или двойное слово (POPFD) и помещает в регистр FLAGS или EFLAGS. Эффект этих команд зависит от режима, в котором выполняется программа: в реальном режиме и в защищенном режиме с уровнем привилегий 0 модифицируются все незарезервированные флаги в EFLAGS, кроме VIP, VIF и VM. VIP и VIF обнуляются, и VM не изменяется. В защищенном режиме c уровнем привилегий, большим нуля, но меньшим или равным IOPL, модифицируются все флаги, кроме VIP, VIF, VM и IOPL. В режиме V86 не модифицируются флаги VIF, VIP, VM, IOPL и RF.
| Команда: |
CLI |
| Назначение: |
Запретить прерывания |
| Процессор: |
8086 |
<
/p>
Сбрасывает флаг IF в 0. После выполнения этой команды процессор игнорирует все прерывания от внешних устройств (кроме NMI). В защищенном режиме эта команда, так же как и все другие команды, модифицирующие флаг IF (POPF или IRET), выполняется, только если программе даны соответствующие привилегии (CPL < IOPL).
| Команда: |
STI |
| Назначение: |
Разрешить прерывания |
| Процессор: |
8086 |
Устанавливает флаг IF в 1, отменяя тем самым действие команды CLI.
| Команда: |
SALC |
| Назначение: |
Установить AL в соответствии с CF |
| Процессор: |
8086 |
Устанавливает AL в 0FFh, если флаг CF = 1, и сбрасывает в 00h, если CF = 0. Это недокументированная команда с кодом 0D6h, присутствующая во всех процессорах Intel и совместимых с ними (начиная с 8086). В документации на Pentium Pro эта команда упоминается в общем списке команд, но ее действие не описывается. Действие SALC аналогично SBB AL,AL, но SALC не изменяет значений флагов.
Загрузка сегментных регистров
| Команда: |
LDS приемник, источник |
| Назначение: |
Загрузить адрес, используя DS |
| Процессор: |
8086 |
| Команда: |
LES приемник, источник |
| Назначение: |
Загрузить адрес, используя ES |
| Процессор: |
8086 |
| Команда: |
LFS приемник, источник |
| Назначение: |
Загрузить адрес, используя FS |
| Процессор: |
80386 |
| Команда: |
LGS приемник, источник |
| Назначение: |
Загрузить адрес, используя GS |
| Процессор: |
80386 |
| Команда: |
LSS приемник, источник |
| Назначение: |
Загрузить адрес, используя SS |
| Процессор: |
8086 |
Второй операнд (источник) для всех этих команд - переменная в памяти размером в 32 или 48 бит (в зависимости от разрядности операндов). Первые 16 бит из этой переменной загружаются в соответствующий сегментный регистр (DS для LDS, ES для LES и т.д.), а следующие 16 или 32 - в регистр общего назначения, указанный в качестве первого операнда. В защищенном режиме значение, загружаемое в сегментный регистр, всегда должно быть правильным селектором сегмента (в реальном режиме любое число может использоваться как селектор).
Директивы и операторы ассемблера
Блоки повторений
Простейший блок повторений REPT (не поддерживается WASM) выполняет ассемблирование участка программы заданное число раз. Например, если требуется создать массив байтов, проинициализированный значениями от 0 до 0FFh, это можно сделать путем повтора псевдокоманды DB следующим образом:
hexnumber = 0 hextable label byte ; Имя массива rept 256 ; Начало блока db hexnumber ; Эти две строки ассемблируются hexnumber = hexnumber+1 ; 256 раз. endm
Блоки повторений, так же как макроопределения, могут вызываться с параметрами. Для этого используются директивы IRP и IRPC:
irp параметр,<значение1,значение2...> ... endm
irpc параметр,строка ... endm
Блок, описанный директивой IRP, будет вызываться столько раз, сколько значений указано в списке (в угловых скобках), и при каждом повторении будет определена метка с именем параметр, равная очередному значению из списка. Например, следующий блок повторений сохранит в стек регистры AX, BX, CX и DX:
irp reg,
push reg endm
Директива IRPC (FORC в WASM) описывает блок, который выполняется столько раз, сколько символов содержит указанная строка, и при каждом повторении будет определена метка с именем параметр, равная очередному символу из строки. Если строка содержит пробелы или другие символы, отличные от разрешенных для меток, она должна быть заключена в угловые скобки. Например, следующий блок задает строку в памяти, располагая после каждого символа строки атрибут 0Fh (белый символ на черном фоне), так что эту строку впоследствии можно будет скопировать прямо в видеопамять.
irpc character,<строка символов> db ’&character&’,0Fh endm
В этом примере используются амперсанды, чтобы вместо параметра character было подставлено его значение даже внутри кавычек. Амперсанд - это один из макрооператоров - специальных операторов, которые действуют только внутри макроопределений и блоков повторений.
Директивы управления программным счетчиком
Программный счетчик - внутренняя переменная ассемблера, равная смещению текущей команды или данных относительно начала сегмента. Для преобразования меток в адреса используется именно значение этого счетчика. Значением счетчика можно управлять с помощью следующих директив.
org выражение
Устанавливает значение программного счетчика. Директива ORG с операндом 100h обязательно используется при написании файлов типа COM, которые загружаются в память после блока параметров размером 100h.
even
Директива EVEN делает текущее значение счетчика кратным двум, вставляя команду NOP, если оно было нечетным. Это увеличивает скорость работы программы, так как для доступа к слову, начинающемуся с нечетного адреса, процессор должен считать два слова из памяти. Если при описании сегмента не использовалось выравнивание типа BYTE, счетчик в начале сегмента всегда четный.
align значение
Округляет значение программного счетчика до кратного указанному значению. Оно может быть любым четным числом. Если счетчик некратен указанному числу, эта директива вставляет необходимое количество команд NOP.
Директивы задания набора допустимых команд
По умолчанию ассемблеры используют набор команд процессора 8086 и выдают сообщения об ошибках, если выбирается команда, которую этот процессор не поддерживал. Для того чтобы ассемблер разрешил использование команд, появившихся в более новых процессорах, и команд расширений, предлагаются следующие директивы:
.8086 - используется по умолчанию. Разрешены только команды 8086;
.186 - разрешены команды 80186;
.286 и .286c - разрешены непривилегированные команды 80286;
.286p - разрешены все команды 80286;
.386 и .386c - разрешены непривилегированные команды 80386;
.386p - разрешены все команды 80386;
.486 и .486c - разрешены непривилегированные команды 80486;
.486p - разрешены все команды 80486;
.586 и .586c - разрешены непривилегированные команды P5 (Pentium);
.586p - разрешены все команды P5 (Pentium);
.686 - разрешены непривилегированные команды P6 (Pentium Pro, Pentium II);
.686p - разрешены все команды P6 (Pentium Pro, Pentium II);
.8087 - разрешены команды NPX 8087;
.287 - разрешены команды NPX 80287;
.387 - разрешены команды NPX 80387;
.487 - разрешены команды FPU 80486;
.587 - разрешены команды FPU 80586;
.MMX - разрешены команды IA MMX;
.K3D - разрешены команды AMD 3D.
Не все ассемблеры поддерживают каждую директиву, например MASM и WASM не поддерживают .487 и .587, так как их действие не отличается от .387. Естественно, ассемблеры, вышедшие до появления последних процессоров и расширений, не в состоянии выполнять соответствующие им команды.
Если присутствует директива .386 или выше, ассемблер WASM всегда определяет все сегменты как 32-битные при условии, что не указан явно операнд USE16. MASM и TASM действуют так же, только если директива задания набора команд указана перед директивой .model.
Другие директивы, используемые в макроопределениях
Директива EXITM (не поддерживается WASM) выполняет преждевременный выход из макроопределения или блока повторений. Например, следующее макроопределение не выполнит никаких действий, то есть не будет расширено в команды процессора, если параметр не указан:
pushreg macro reg ifb exitm endif push reg endm
LOCAL метка... - перечисляет метки, которые будут применяться внутри макроопределения, чтобы не возникало ошибки "метка уже определена" при использовании макроса более одного раза или если та же метка присутствует в основном тексте программы (в WASM директива LOCAL позволяет использовать макрос с метками несколько раз, но не разрешает применять метку с тем же именем в программе). Операнд для LOCAL - метка или список меток, которые будут использоваться в макросе.
PURGE имя_макроса - отменяет определенный ранее макрос (не поддерживается WASM). Эта директива часто применяется сразу после INCLUDE, включившей в текст программы файл с большим количеством готовых макроопределений.
Директивы и операторы ассемблера
Каждая программа на языке ассемблера помимо команд процессора содержит еще и специальные инструкции, указывающие самому ассемблеру, как организовывать различные секции программы, где располагаются данные, а где команды, позволяющие создавать макроопределения, выбирать тип используемого процессора, организовывать связи между процедурами и т.д. К сожалению, пока нет единого стандарта на эти команды (он существует для UNIX, о чем рассказано в главе 11). Разные ассемблеры используют различные наборы директив, но TASM и MASM (два самых популярных ассемблера для DOS и Windows) поддерживают общий набор, или, точнее, TASM поддерживает набор директив MASM наряду с несовместимым собственным, известным как Ideal Mode. Все примеры программ в книге написаны так, чтобы для их компиляции можно было воспользоваться TASM, MASM или WASM - еще одним популярным ассемблером, поэтому в данной главе рассмотрены те предопределенные идентификаторы, операторы и директивы, которые поддерживаются этими тремя ассемблерами одновременно.
Глобальные объявления
public язык метка... ; Для TASM и MASM
или
public метка язык... ; для WASM
Метка, объявленная директивой PUBLIC, становится доступной для других модулей программы. Так, можно объявлять имена процедур, переменные и константы, определенные директивой EQU. Необязательный операнд языка (C, PASCAL, BASIC, FORTRAN, SYSCALL или STDCALL) указывает, что метка будет вызываться из модуля, написанного на соответствующем языке, и при необходимости изменяет ее (например, добавляет _ перед первым символом метки).
comm расст язык метка:тип... ; для TASM comm язык расст метка:тип... ; для TASM comm расст метка:тип язык... ; для WASM
Директива COMM описывает общую переменную. Такие переменные доступны из всех модулей, и их размещение в программе определяется на этапе компоновки. Обязательные аргументы директивы COMM - метка (собственно имя общей переменной) и тип (BYTE, WORD, DWORD, FWORD, QWORD, TBYTE или имя структуры). Необязательный операнд "расстояние" (NEAR или FAR) указывает, находится ли переменная в группе сегментов DGROUP (ближняя переменная, для доступа достаточно смещения) или вне этих сегментов (дальняя переменная, для доступа потребуется сегментный адрес). Для моделей памяти TINY, SMALL и COMPACT по умолчанию значение этого операнда принимается за NEAR. И наконец, операнд "язык" действует аналогично такому же операнду для PUBLIC.
extrn язык метка:тип... ; для MASM и TASM extrn метка:тип язык... ; для WASM
Описывает метку, определенную в другом модуле (с помощью PUBLIC). Тип (BYTE, WORD, DWORD, FWORD, QWORD, TBYTE, имя структуры, FAR, NEAR, ABS) должен соответствовать типу метки в том модуле, где она была установлена (тип ABS используется для констант из других модулей, определенных директивой EQU). Необязательный операнд языка действует так же, как и для директивы PUBLIC.
global язык метка:тип... ; для MASM и TASM global метка:тип язык... ; для WASM
Директива GLOBAL действует, как PUBLIC и EXTRN одновременно. Когда указанная метка находится в этом же модуле, она становится доступной для других модулей, как если бы выполнилась директива PUBLIC. Если метка не описана - она считается внешней и выполняется действие, аналогичное действию директивы EXTRN.
в конце строки, возможны большие
Кроме обычных комментариев, начинающихся с символа ; (точка с запятой) и заканчивающихся в конце строки, возможны большие блоки комментариев, описываемых специальной директивой COMMENT.
comment @ любой текст @
Операнд для COMMENT - любой символ, который будет считаться концом комментария. Весь участок текста, вплоть до следующего появления этого символа, ассемблером полностью игнорируется.
Конец программы
end start_label
Этой директивой завершается любая программа на ассемблере. В роли необязательного операнда здесь выступает метка (или выражение), определяющая адрес, с которого начинается выполнение программы. Если программа состоит из нескольких модулей, только один файл может содержать начальный адрес, так же как в C только один файл может содержать функцию main().
Макрооператоры
Макрооператор & (амперсанд) нужен для того, чтобы параметр, переданный в качестве операнда макроопределению или блоку повторений, заменялся значением до обработки строки ассемблером. Так, например, следующий макрос выполнит команду PUSH EAX, если его вызвать как PUSHREG A:
pushreg macro letter push e&letter&x endm
Иногда можно использовать только один амперсанд - в начале параметра, если не возникает неоднозначностей. Например, если передается номер, а требуется создать набор переменных с именами, оканчивающимися этим номером:
irp number,<1,2,3,4> msg&number db ? endm
Макрооператор <> (угловые скобки) действует так, что весь текст, заключенный в эти скобки, рассматривается как текстовая строка, даже если он содержит пробелы или другие разделители. Как мы уже видели, этот макрооператор используется при передаче текстовых строк в качестве параметров для макросов. Другое частое применение угловых скобок - передача списка параметров вложенному макроопределению или блоку повторений.
Макрооператор ! (восклицательный знак) используется аналогично угловым скобкам, но действует только на один следующий символ, так что, если этот символ - запятая или угловая скобка, он все равно будет передан макросу как часть параметра.
Макрооператор % (процент) указывает, что находящийся за ним текст является выражением и должен быть вычислен. Обычно это требуется для того, чтобы передавать в качестве параметра в макрос не само выражение, а его результат.
Макрооператор ;; (две точки с запятой) - начало макрокомментария. В отличие от обычных комментариев текст макрокомментария не попадает в листинг и в текст программы при подстановке макроса. Это сэкономит память при ассемблировании программы с большим количеством макроопределений.
Макроопределения
Одно из самых мощных языковых средств ассемблера - макроопределения. Макроопределением (или макросом) называется участок программы, которому присвоено имя и который ассемблируется всякий раз, когда ассемблер встречает это имя в тексте программы. Макрос начинается директивой MACRO и заканчивается ENDM. Например: пусть описано макроопределение hex2ascii, переводящее шестнадцатеричное число, находящееся в регистре AL, в ASCII-код соответствующей шестнадцатеричной цифры:
hex2ascii macro cmp al,10 sbb al,69h das endm
Теперь в программе можно использовать слово hex2ascii, как если бы это было имя команды, и ассемблер заменит каждое такое слово на три команды, содержащиеся в макроопределении. Разумеется, можно оформить этот же участок кода в виде процедуры и вызывать его командой CALL - если процедура вызывается больше одного раза, этот вариант программы займет меньше места, но вариант с макроопределением станет выполняться быстрее, так как в нем не будет лишних команд CALL и RET. Однако скорость выполнения - не главное преимущество макросов. В отличие от процедур макроопределения могут вызываться с параметрами, следовательно, в зависимости от ситуации, включаемый код будет немного различаться, например:
s_mov macro register1,register2 push register1 pop register2 endm
Теперь можно использовать S_MOV вместо команды MOV для того, чтобы скопировать значение из одного сегментного регистра в другой.
Следующее важное средство, использующееся в макроопределениях, - директивы условного ассемблирования. Например: напишем макрос, выполняющий умножение регистра AX на число, причем, если множитель - степень двойки, то умножение будет выполняться более быстрой командой сдвига влево.
fast_mul macro number if number eq 2 shl ax,1 ; Умножение на 2 elseif number eq 4 shl ax,2 ; Умножение на 4 elseif number eq 8 shl ax,3 ; Умножение на 8 ... ; Аналогично вплоть до: elseif number eq 32768 shl ax,15 ; Умножение на 32768 else mov dx,number ; Умножение на число, не являющееся mul dx ; степенью двойки. endif endm
Можно, конечно, усложнить этот макрос, применяя особые свойства команды LEA и ее комбинации, сдвиги и сложения, однако в нынешнем виде он чрезмерно громоздкий. Проблема решается с помощью третьего средства, постоянно использующегося в макросах, - блоков повторений.
Модели памяти и упрощенные директивы определения сегментов
Модели памяти задаются директивой .MODEL
.model модель,язык,модификатор
где модель - одно из следующих слов:
TINY - код, данные и стек размещаются в одном и том же сегменте размером до 64 Кб. Эта модель памяти чаще всего используется при написании на ассемблере небольших программ;
SMALL - код размещается в одном сегменте, а данные и стек - в другом (для их описания могут применяться разные сегменты, но объединенные в одну группу). Эту модель памяти также удобно использовать для создания программ на ассемблере;
COMPACT - код размещается в одном сегменте, а для хранения данных могут использоваться несколько сегментов, так что для обращения к данным требуется указывать сегмент и смещение (данные дальнего типа);
MEDIUM - код размещается в нескольких сегментах, а все данные - в одном, поэтому для доступа к данным используется только смещение, а вызовы подпрограмм применяют команды дальнего вызова процедуры;
LARGE и HUGE - и код, и данные могут занимать несколько сегментов;
FLAT - то же, что и TINY, но используются 32-битные сегменты, так что максимальный размер сегмента, содержащего и данные, и код, и стек, - 4 Мб.
Язык - необязательный операнд, принимающий значения C, PASCAL, BASIC, FORTRAN, SYSCALL и STDCALL. Если он указан, подразумевается, что процедуры рассчитаны на вызов из программ на соответствующем языке высокого уровня, следовательно, если указан язык C, все имена ассемблерных процедур, объявленных как PUBLIC, будут изменены так, чтобы начинаться с символа подчеркивания, как это принято в C.
Модификатор - необязательный операнд, принимающий значения NEARSTACK (по умолчанию) или FARSTACK. Во втором случае сегмент стека не будет объединяться в одну группу с сегментами данных.
После того как модель памяти установлена, вступают в силу упрощенные директивы определения сегментов, объединяющие действия директив SEGMENT и ASSUME. Кроме того, сегменты, объявленные упрощенными директивами, не требуется закрывать директивой ENDS - они закрываются автоматически, как только ассемблер обнаруживает новую директиву определения сегмента или конец программы.
Директива .CODE описывает основной сегмент кода
.code имя_сегмента
эквивалентно
_TEXT segment word public ’CODE’
для моделей TINY, SMALL и COMPACT и
name_TEXT segment word public ’CODE’
для моделей MEDIUM, HUGE и LARGE (name - имя модуля, в котором описан данный сегмент). В этих моделях директива .CODE также допускает необязательный операнд - имя определяемого сегмента, но все сегменты кода, описанные так в одном и том же модуле, объединяются в один сегмент с именем NAME_TEXT.
.stack размер
Директива .STACK описывает сегмент стека и эквивалентна директиве
STACK segment para public ’stack’
Необязательный параметр указывает размер стека. По умолчанию он равен 1 Кб.
.data
Описывает обычный сегмент данных и соответствует директиве
_DATA segment word public ’DATA’
.data?
Описывает сегмент неинициализированных данных:
_BSS segment word public ’BSS’
Этот сегмент обычно не включается в программу, а располагается за концом памяти, так что все описанные в нем переменные на момент загрузки программы имеют неопределенные значения.
.const
Описывает сегмент неизменяемых данных:
CONST segment word public ’CONST’
В некоторых операционных системах этот сегмент будет загружен так, что попытка записи в него может привести к ошибке.
.fardata имя_сегмента
Сегмент дальних данных:
имя_сегмента segment para private ’FAR_DATA’
Доступ к данным, описанным в этом сегменте, потребует загрузки сегментного регистра. Если не указан операнд, в качестве имени сегмента используется FAR_DATA.
.fardata? имя_сегмента
Сегмент дальних неинициализированных данных:
имя_сегмента segment para private ’FAR_BSS’
Как и в случае с FARDATA, доступ к данным из этого сегмента потребует загрузки сегментного регистра. Если имя сегмента не указано, используется FAR_BSS.
Во всех моделях памяти сегменты, представленные директивами .DATA, .DATA?, .CONST, .FARDATA и .FARDATA?, а также сегмент, описанный директивой .STACK, если не был указан модификатор FARSTACK, и сегмент .CODE в модели TINY автоматически объединяются в группу с именем FLAT - для модели памяти FLAT или DGROUP - для всех остальных моделей. При этом сегментный регистр DS (и SS, если не было FARSTACK, и CS в модели TINY) настраивается на всю эту группу, как если бы была выполнена команда ASSUME.
Порядок загрузки сегментов
Обычно сегменты загружаются в память в том порядке, в котором они описываются в тексте программы, причем, если несколько сегментов объединяются в один, порядок определяется по началу первого из объединяемых сегментов. Этот порядок можно изменить с помощью одной из специальных директив.
.alpha
Эта директива устанавливает алфавитный порядок загрузки сегментов.
.dosseg ; для MASM и WASM
или
dosseg ; для MASM и TASM
Устанавливает порядок загрузки сегментов, существующий в MS DOS и часто требуемый для взаимодействия программ на ассемблере с программами на языках высокого уровня. DOSSEG устанавливает следующий порядок загрузки сегментов:
1. Все сегменты класса 'CODE'.
2. Все сегменты, не принадлежащие группе DGROUP и классу 'CODE'.
3. Группа сегментов DGROUP:
3.1. Все сегменты класса 'BEGDATA'.
3.2. Все сегменты, кроме классов 'BEGDATA', 'BSS' и 'STACK'.
3.3. Все сегменты класса 'BSS'.
3.4. Все сегменты класса 'STACK'.
.seq
Устанавливает загрузку сегментов в том порядке, в котором они описаны в тексте программы. Этот режим устанавливается по умолчанию, так что директива .SEQ просто отменяет действие .ALPHA или .DOSSEG.
Знание порядка загрузки сегментов необходимо, например, для вычисления длины программы или адреса ее конца. Для этого надо знать, какой сегмент будет загружен последним, и смещение последнего байта в нем.
Процедуры
Процедурой в ассемблере является все то, что в других языках называют подпрограммами, функциями, процедурами и т.д. Ассемблер не накладывает на процедуры никаких ограничений - на любой адрес программы можно передать управление командой CALL, и оно вернется к вызвавшей процедуре, как только встретится команда RET. Такая свобода выражения легко может приводить к трудночитаемым программам, и в язык ассемблера были включены директивы логического оформления процедур.
метка proc язык тип USES регистры ; TASM
или
метка proc тип язык USES регистры ; MASM/WASM ... ret метка endp
Все операнды PROC необязательны.
Тип может принимать значения NEAR и FAR, и если он указан, все команды RET в теле процедуры будут заменены соответственно на RETN и RETF. По умолчанию подразумевается, что процедура имеет тип NEAR в моделях памяти TINY, SMALL и COMPACT.
Операнд язык действует аналогично такому же операнду директивы .MODEL, определяя взаимодействие процедуры с языками высокого уровня. В некоторых ассемблерах директива PROC позволяет также считать параметры, передаваемые вызывающей программой. В этом случае указание языка необходимо, так как различные языки высокого уровня используют разные способы передачи параметров.
USES - список регистров, значения которых изменяет процедура. Ассемблер помещает в начало процедуры набор команд PUSH, а перед командой RET - набор команд POP, так что значения перечисленных регистров будут восстановлены.
Псевдокоманды определения переменных
Псевдокоманда - это директива ассемблера, которая приводит к включению данных или кода в программу, хотя сама она никакой команде процессора не соответствует. Псевдокоманды определения переменных указывают ассемблеру, что в соответствующем месте программы располагается переменная, определяют тип переменной (байт, слово, вещественное число и т.д.), задают ее начальное значение и ставят в соответствие переменной метку, которая будет использоваться для обращения к этим данным. Псевдокоманды определения данных записываются в общем виде следующим образом:
имя_переменной d* значение
где D* - одна из нижеприведенных псевдокоманд:
DB - определить байт;
DW - определить слово (2 байта);
DD - определить двойное слово (4 байта);
DF - определить 6 байт (адрес в формате 16-битный селектор: 32-битное смещение);
DQ - определить учетверенное слово (8 байт);
DT - определить 10 байт (80-битные типы данных, используемые FPU).
Поле значения может содержать одно или несколько чисел, строк символов (взятых в одиночные или двойные кавычки), операторов ? и DUP, разделенных запятыми. Все установленные таким образом данные окажутся в выходном файле, а имя переменной будет соответствовать адресу первого из указанных значений. Например, набор директив
text_string db 'Hello world!' number dw 7 table db 1,2,3,4,5,6,7,8,9,0Ah,0Bh,0Ch,0Dh,0Eh,0Fh float_number dd 3.5e7
заполняет данными 33 байта. Первые 12 байт содержат ASCII-коды символов строки "Hello world!", и переменная text_string указывает на первую букву в этой строке, так что команда
mov al,text_string
считает в регистр AL число 48h (код латинской буквы H). Если вместо точного значения указан знак ?, переменная считается неинициализированной и ее значение на момент запуска программы может оказаться любым. Если нужно заполнить участок памяти повторяющимися данными, используется специальный оператор DUP, имеющий формат счетчик DUP (значение). Например, вот такое определение:
table_512w dw 512 dup(?)
создает массив из 512 неинициализированных слов, на первое из которых указывает переменная table_512w. В качестве аргумента в операторе DUP могут выступать несколько значений, разделенных запятыми, и даже дополнительные вложенные операторы DUP.
Сегменты
Каждая программа, написанная на любом языке программирования, состоит из одного или нескольких сегментов. Обычно область памяти, в которой находятся команды, называют сегментом кода, область памяти с данными - сегментом данных и область памяти, отведенную под стек, - сегментом стека. Разумеется, ассемблер позволяет изменять устройство программы как угодно - помещать данные в сегмент кода, разносить код на множество сегментов, помещать стек в один сегмент с данными или вообще использовать один сегмент для всего.
Сегмент программы описывается директивами SEGMENT и ENDS.
имя_сегмента segment readonly выравн. тип разряд 'класс' ... имя_сегмента ends
Имя сегмента - метка, которая будет использоваться для получения сегментного адреса, а также для комбинирования сегментов в группы.
Все пять операндов директивы SEGMENT необязательны.
READONLY. Если этот операнд присутствует, MASM выдаст сообщение об ошибке на все команды, выполняющие запись в данный сегмент. Другие ассемблеры этот операнд игнорируют.
Выравнивание. Указывает ассемблеру и компоновщику, с какого адреса может начинаться сегмент. Значения этого операнда:
BYTE - с любого адреса;
WORD - с четного адреса;
DWORD - с адреса, кратного 4;
PARA - с адреса, кратного 16 (граница параграфа);
PAGE - с адреса, кратного 256.
По умолчанию используется выравнивание по границе параграфа.
Тип. Выбирает один из возможных типов комбинирования сегментов:
тип PUBLIC (иногда используется синоним MEMORY) означает, что все такие сегменты с одинаковым именем, но разными классами будут объединены в один;
тип STACK - то же самое, что и PUBLIC, но должен использоваться для сегментов стека, потому что при загрузке программы сегмент, полученный объединением всех сегментов типа STACK, будет использоваться как стек;
сегменты типа COMMON с одинаковым именем также объединяются в один, но не последовательно, а по одному и тому же адресу, следовательно, длина суммарного сегмента будет равна не сумме длин объединяемых сегментов, как в случае PUBLIC и STACK, а длине максимального. Таким способом иногда можно формировать оверлейные программы;
тип AT - выражение указывает, что сегмент должен располагаться по фиксированному абсолютному адресу в памяти. Результат выражения, использующегося в качестве операнда для AT, равен этому адресу, деленному на 16. Например: segment at 40h - сегмент, начинающийся по абсолютному адресу 0400h. Такие сегменты обычно содержат только метки, указывающие на области памяти, которые могут потребоваться программе;
PRIVATE (значение по умолчанию) - сегмент такого типа не объединяется с другими сегментами.
Разрядность. Этот операнд может принимать значения USE16 и USE32. Размер сегмента, описанного как USE16, не может превышать 64 Кб, и все команды и адреса в этом сегменте считаются 16-битными. В этих сегментах все равно можно применять команды, использующие 32-битные регистры или ссылающиеся на данные в 32-битных сегментах, но они будут использовать префикс изменения разрядности операнда или адреса и окажутся длиннее и медленнее. Сегменты USE32 могут занимать до 4 Гб, и все команды и адреса в них по умолчанию 32-битные. Если разрядность сегмента не указана, по умолчанию используется USE16 при условии, что перед директивой .MODEL не применялась директива задания допустимого набора команд .386 или старше.
Класс сегмента - это любая метка, взятая в одинарные кавычки. Все сегменты с одинаковым классом, даже сегменты типа PRIVATE, будут расположены в исполняемом файле непосредственно друг за другом.
Для обращения к любому сегменту следует сначала загрузить его сегментный адрес (или селектор в защищенном режиме) в какой-нибудь сегментный регистр. Если в программе определено много сегментов, удобно объединить несколько сегментов в группу, адресуемую с помощью одного сегментного регистра:
имя_группы group имя_сегмента...
Операнды этой директивы - список имен сегментов (или выражений, использующих оператор SEG), которые объединяются в группу. Имя группы теперь можно применять вместо имен сегментов для получения сегментного адреса и для директивы ASSUME.
assume регистр:связь...
Директива ASSUME указывает ассемблеру, с каким сегментом или группой сегментов связан тот или иной сегментный регистр. В качестве операнда "связь" могут использоваться имена сегментов, имена групп, выражения с оператором SEG или слово "NOTHING", означающее отмену действия предыдущей ASSUME для данного регистра. Эта директива не изменяет значений сегментных регистров, а только позволяет ассемблеру проверять допустимость ссылок и самостоятельно вставлять при необходимости префиксы переопределения сегментов, если они необходимы.
Перечисленные директивы удобны для создания больших программ на ассемблере, состоящих из разнообразных модулей и содержащих множество сегментов. В повседневном программировании обычно используется ограниченный набор простых вариантов организации программы, известных как модели памяти.
Структура программы
Программа на языке ассемблера состоит из строк, имеющих следующий вид:
метка команда/директива операнды ; комментарий
Причем все эти поля необязательны. Метка может быть любой комбинацией букв английского алфавита, цифр и символов _, $, @, ?, но цифра не может быть первым символом метки, а символы $ и ? иногда имеют специальные значения и обычно не рекомендуются к использованию. Большие и маленькие буквы по умолчанию не различаются, но различие можно включить, задав ту или иную опцию в командной строке ассемблера. Во втором поле, поле команды, может располагаться команда процессора, которая транслируется в исполняемый код, или директива, которая не приводит к появлению нового кода, а управляет работой самого ассемблера. В поле операндов располагаются требуемые командой или директивой операнды (то есть нельзя указать операнды и не указать команду или директиву). И наконец, в поле комментариев, начало которого отмечается символом ; (точка с запятой), можно написать все что угодно - текст от символа ";" до конца строки не анализируется ассемблером.
Для облегчения читаемости ассемблерных текстов принято, что метка начинается на первой позиции в строке, команда - на 17-й (две табуляции), операнды - на 25-й (три табуляции) и комментарии - на 41-й или 49-й. Если строка состоит только из комментария, его начинают с первой позиции.
Если метка располагается перед командой процессора, сразу после нее всегда ставится символ ":" (двоеточие), который указывает ассемблеру, что надо создать переменную с этим именем, содержащую адрес текущей команды:
some_loop: lodsw ; cчитать слово из строки, cmp ax,7 ; если это 7 - выйти из цикла loopne some_loop
Когда метка стоит перед директивой ассемблера, она обычно оказывается одним из операндов этой директивы и двоеточие не ставится. Рассмотрим директивы, работающие напрямую с метками и их значениями, - LABEL, EQU и =.
метка label тип
Директива LABEL определяет метку и задает ее тип. Тип может быть одним из: BYTE (байт), WORD (слово), DWORD (двойное слово), FWORD (6 байт), QWORD (учетверенное слово), TBYTE (10 байт), NEAR (ближняя метка), FAR (дальняя метка). Метка получает значение, равное адресу следующей команды или следующих данных, и тип, указанный явно. В зависимости от типа команда
mov метка,0
запишет в память байт (слово, двойное слово и т.д.), заполненный нулями, а команда
call метка
выполнит ближний или дальний вызов подпрограммы.
С помощью директивы LABEL удобно организовывать доступ к одним и тем же данным, как к байтам, так и к словам, определив перед данными две метки с разными типами.
метка equ выражение
Директива EQU присваивает метке значение, которое определяется как результат целочисленного выражения в правой части. Результатом этого выражения может быть целое число, адрес или любая строка символов:
truth equ 1 message1 equ 'Try again$' var2 equ 4[si] cmp ax,truth ; cmp ax,1 db message1 ; db 'Try again$' mov ax,var2 ; mov ax, 4[si]
Директива EQU чаще всего используется с целью введения параметров, общих для всей программы, аналогично команде #define препроцессора языка С.
метка = выражение
Директива = эквивалентна EQU, но определяемая ею метка может принимать только целочисленные значения. Кроме того, метка, указанная этой директивой, может быть переопределена.
Каждый ассемблер предлагает целый набор специальных предопределенных меток - это может быть текущая дата (@date или ??date), тип процессора (@cpu) или имя того или иного сегмента программы, но единственная предопределенная метка, поддерживаемая всеми рассматриваемыми нами ассемблерами, - $. Она всегда соответствует текущему адресу. Например, команда
jmp $
выполняет безусловный переход на саму себя, так что создается вечный цикл из одной команды.
Структуры
Директива STRUC позволяет определить структуру данных аналогично структурам в языках высокого уровня. Последовательность директив
имя struc поля
имя ends
где поля - любой набор псевдокоманд определения переменных или структур, устанавливает, но не инициализирует структуру данных. В дальнейшем для ее создания в памяти используют имя структуры как псевдокоманду:
метка имя <значения>
И наконец, для чтения или записи в элемент структуры используется оператор "." (точка). Например:
point struc ; Определение структуры x dw 0 ; Три слова со значениями y dw 0 ; по умолчанию 0,0,0 z dw 0 color db 3 dup(?) ; и три байта point ends
cur_point point <1,1,1,255,255,255> ; Инициализация mov ax,cur_point.x ; Обращение к слову "x"
Если была определена вложенная структура, доступ к ее элементам осуществляется через еще один оператор "." (точка).
color struc ; Определить структуру color. red db ? green db ? blue db ? color ends
point struc x dw 0 y dw 0 z dw 0 clr color <> point ends
cur_point point <> mov cur_point.clr.red,al ; Обращение к красной компоненте ; цвета точки cur_point.
Управление файлами
INCLUDE имя_файла - директива, вставляющая в текст программы текст файла аналогично команде препроцессора C #include. Обычно используется для включения файлов, содержащих определения констант, структур и макросов.
INCLUDELIB имя_файла - директива, указывающая компоновщику имя дополнительной библиотеки или объектного файла, который потребуется при составлении данной программы. Например, если используются вызовы процедур или обращение к данным, определенным в других модулях. Использование этой директивы позволяет не указывать имена дополнительных библиотек при вызове компоновщика.
Управление листингом
Обычно ассемблеры, помимо создания объектного файла, предоставляют возможность создания листинга программы (TASM /L - для TASM, ml /Fl - для MASM). Листинг - это файл, содержащий текст ассемблерной программы, код каждой ассемблированной команды, список определенных меток, перекрестных ссылок, сегментов и групп. Формат файла листинга отличается для разных ассемблеров, и директивы управления форматом этого файла также сильно различаются, но несколько наиболее общих директив все-таки поддерживаются всеми тремя ассемблерами, рассмотренными в этой книге.
TITLE текст - определяет заголовок листинга. Заголовок появляется в начале каждой страницы;
SUBTTL текст - определяет подзаголовок листинга. Подзаголовок появляется на следующей строке после заголовка;
PAGE высота,ширина - устанавливает размеры страниц листинга (высота 10-255, ширина 59-255). Директива PAGE без аргументов начинает новую страницу, директива PAGE + начинает новую секцию, и нумерация страниц ведется с самого начала;
NAME текст - определяет имя модуля программы. Если NAME не указан, в качестве имени используются первые 6 символов из TITLE; если нет ни NAME, ни TITLE, за имя берется название файла;
.XLIST - отменить выдачу листинга;
.LIST - разрешить выдачу листинга;
.SALL - запретить листинг макроопределений;
.SFCOND - запретить листинг неассемблированных условных блоков;
.LFCOND - разрешить листинг неассемблированных условных блоков;
.TFCOND - изменить режим листинга условных блоков на противоположный;
.CREF - разрешить листинг перекрестных ссылок;
.XCREF - запретить листинг перекрестных ссылок.
Условное ассемблирование
В большинстве языков программирования присутствуют средства, позволяющие игнорировать тот или иной участок программы в зависимости от выполнения условий, например: в языке C это осуществляется командами препроцессора #if, #ifdef, #ifndef и т.д. Ассемблер тоже предоставляет такую возможность.
if выражение ... endif
Если значение выражения - ноль (ложь), весь участок программы между IF и ENDIF игнорируется. Директива IF может также сочетаться с ELSE и ELSEIF:
if выражение ... else ... endif
Если значение выражения - ноль, ассемблируется участок программы от ELSE до ENDIF, в противном случае - от IF до ELSE.
if выражение1 ... elseif выражение2 ... elseif выражение3 ... else ... endif
Так, если, например, выражение 2 не равно нулю, будет ассемблироваться участок программы между первой и второй директивой ELSEIF. Если все три выражения равны нулю, ассемблируется фрагмент от ELSE до ENDIF. Данная структура директив может использоваться в частном случае аналогично операторам switch/case языков высокого уровня, если выражения - проверки некоторой константы на равенство.
Кроме общих директив IF и ELSEIF ассемблеры поддерживают набор специальных команд, каждая из которых проверяет специальное условие:
IF1/ELSEIF1 - если ассемблер выполняет первый проход ассемблирования;
IF2/ELSEIF2 - если ассемблер выполняет второй проход ассемблирования (часто не работает на современных ассемблерах);
IFE выражение/ELSEIFE выражение - если выражение равно нулю (ложно);
IFDEF метка/ELSEIFDEF метка - если метка определена;
IFNDEF метка/ELSEIFNDEF метка - если метка не определена;
IFB <аргумент>/ELSEIFB <аргумент> - если значение аргумента - пробел (эти и все следующие директивы используются в макроопределениях для проверки параметров);
IFNB <аргумент>/ELSEIFNB <аргумент> - если значение аргумента - не пробел (используется в макроопределениях для проверки переданных параметров);
IFDIF <арг1>,<арг2>/ELSEIFDIF <арг1>,<арг2> - если аргументы отличаются (с различием больших и маленьких букв);
IFDIFI <арг1>,<арг2>/ELSEIFDIFI <арг1>,<арг2> - если аргументы отличаются (без различия больших и маленьких букв);
IFIDN <арг1>,<арг2>/ELSEIFIDN <арг1>,<арг2> - если аргументы одинаковы (с различием больших и маленьких букв);
IFIDNI <арг1>,<арг2>/ELSEIFIDNI <арг1>,<арг2> - если аргументы одинаковы (без различия больших и маленьких букв).
Иногда директивы условного ассемблирования используются для того, чтобы прервать ассемблирование программы, если обнаружилась какая-нибудь ошибка. Для таких случаев предназначены директивы условной генерации ошибок.
if $ gt 65535 ; Если адрес вышел за пределы сегмента. .err endif
Встретив директиву .ERR, ассемблер прекратит работу с сообщением об ошибке. Аналогично командам условного ассемблирования существуют модификации команды .ERR:
.ERR1 - ошибка при первом проходе ассемблирования;
.ERR2 - ошибка при втором проходе ассемблирования;
.ERRE выражение - ошибка, если выражение равно нулю (ложно);
.ERRNZ выражение - ошибка, если выражение не равно нулю (истинно);
.ERRDEF метка - ошибка, если метка определена;
.ERRNDEF метка - ошибка, если метка не определена;
.ERRB <аргумент> - ошибка, если аргумент пуст (эта и все следующие директивы используются в макроопределениях для проверки параметров);
.ERRNB <аргумент> - ошибка, если аргумент не пуст;
.ERRDIF <арг1>,<арг2> - ошибка, если аргументы различны;
.ERRDIFI <арг1>,<арг2> - ошибка, если аргументы отличаются (сравнение не различает большие и маленькие буквы);
.ERRIDN <арг1>,<арг2> - ошибка, если аргументы совпадают;
.ERRIDNI <арг1>,<арг2> - ошибка, если аргументы совпадают (сравнение не различает большие и маленькие буквы).
Выражения
Мы уже упоминали выражения при описании многих директив ассемблера. Выражение - это набор чисел, меток или строк, связанных друг с другом операторами. Например: 2 + 2 - выражение, состоящее из двух чисел (2 и 2) и оператора +. Каждое выражение имеет значение, которое определяется как результат действия операторов. Так, значение выражения 2 + 2 - число 4. Все выражения вычисляются в ходе ассемблирования программы, следовательно, в полученном коде используются только значения.
Оператор <> (угловые скобки). Часть выражения, заключенная в угловые скобки, не вычисляется, а применяется как строка символов, например:
message1 equ
Оператор () (круглые скобки). Часть выражения, заключенная в круглые скобки, вычисляется в первую очередь.
mov al, 2*(3+4) ; mov al,14
Арифметические операторы: + (плюс), – (минус), * (умножение), / (целочисленное деление), MOD (остаток от деления). Они выполняют соответствующие арифметические действия.
mov al,90 mod 7 ; mov al,6
Кроме того, к арифметическим операторам относится унарный минус - минус, который ставят перед отрицательным числом.
Логические операторы: AND (И), NOT (НЕ), OR (ИЛИ), XOR (исключающее ИЛИ), SHL (сдвиг влево), SHR (сдвиг вправо). Эти операторы выполняют соответствующие логические действия.
mov ax,1234h AND 4321h ; mov ax,0220h
Операторы сравнения: EQ (равно), GE (больше или равно), GT (больше), LE (меньше или равно), LT (меньше), NE (не равно). Результат действия каждого из этих операторов - единица, если условие выполняется, и ноль - если не выполняется.
.errnz $ gt 65535 ; Если адрес больше 64 Кб – ошибка
Операторы адресации:
SEG выражение - сегментный адрес;
OFFSET выражение - смещение;
THIS тип - текущий адрес (MASM и TASM);
Тип PTR выражение - переопределение типа;
LARGE выражение - 32-битное смещение (TASM и WASM);
SMALL выражение - 16-битное смещение (TASM и WASM);
SHORT выражение - 8-битное смещение.
SEG и OFFSET возвращают соответствующую часть адреса своего аргумента:
mov dx, offset msg ; Занести в DX смещение переменной msg
THIS создает операнд, адресом которого является текущее значение счетчика:
mov al, this byte-1 ; Занести в AX последний байт кода ; предыдущей команды.
PTR создает аргумент, адресом которого является значение выражения, а тип указан явно:
mov dword ptr [si],0 ; Записать 4 байта нулей по адресу DS:SI
LARGE, SMALL и SHORT используются с командами передачи управления, если возникают двусмысленности при косвенных переходах:
jmp large dword ptr old_address ; Переменная old_address содержит 32-битное смещение jmp small dword ptr old_address ; Переменная old_address содержит 16-битный сегментный адрес ; и 16-битное смещение. jmp short short_label ; Метка short_label находится ; ближе, чем +128/-127 байт от этой команды, так что можно ; использовать короткую форму команды JMP.
Другие операторы:
. (точка) - ссылка на элемент структуры;
: (двоеточие) - переопределение сегмента;
[] (прямые скобки) - косвенная адресация;
? - неинициализированное значение;
число DUP (значение) - повторяющееся значение.
Эти пять операторов описаны ранее, когда говорилось о структурах данных, методах адресации и псевдокомандах определения данных.
LENGTH метка – число элементов данных table dw 0,1,2,3,4,5,6,7 ; Определить таблицу из 8 слов. table_count = length table ; table_count = 8 SIZE метка – размер данных table_size = size table ; table_size = 16
Основы программирования для MS-DOS
Чтение и запись в файл
Функция DOS 3Fh - Чтение из файла или устройства
| Ввод: |
АН = 3Fh ВХ = идентификатор СХ = число байт DS:DX = адрес буфера для приема данных |
| Вывод: |
CF = 0 и АХ = число считанных байт, если не произошла ошибка CF = 1 и АХ = 05h, если доступ запрещен, 06h, если неправильный идентификатор |
Если при чтении из файла число фактически считанных байт в АХ меньше, чем заказанное число в СХ, при чтении был достигнут конец файла. Каждая следующая операция чтения, так же как и записи, начинается не с начала файла, а с того байта, на котором остановилась предыдущая операция чтения/записи. Если требуется считать (или записать) произвольный участок файла, используют функцию 42h (функция lseek в С).
Функция DOS 42h - Переместить указатель чтения/записи
| Ввод: |
АН = 42h ВХ = идентификатор CX:DX = расстояние, на которое надо переместить указатель (со знаком) AL = перемещение от:0 - от начала файла 1 - от текущей позиции 2 - от конца файла |
| Вывод: |
CF = 0 и CX:DX = новое значение указателя (в байтах от начала файла), если не произошла ошибка CF = 1 и АХ = 06h, если неправильный идентификатор |
Указатель можно установить за реальными пределами файла: если указатель устанавливается в отрицательное число, следующая операция чтения/записи вызовет ошибку; если указатель устанавливается в положительное число, большее длины файла, следующая операция записи увеличит размер файла. Эта функция также часто используется для определения длины файла - достаточно вызвать ее с СХ = 0, DX = 0, AL = 2, и в CX:DX будет возвращена длина файла в байтах.
Функция DOS 40h - Запись в файл или устройство
| Ввод: |
АН = 40h ВХ = идентификатор СХ = число байт DS:DX = адрес буфера с данными |
| Вывод: |
CF = 0 и АХ = число записанных байт, если не произошла ошибка CF = 1 и АХ = 05h, если доступ запрещен, 06h, если неправильный идентификатор |
Если при записи в файл указать СХ = 0, файл будет обрезан по текущему значению указателя. При записи в файл на самом деле происходит запись в буфер DOS, данные из которого сбрасываются на диск при закрытии файла или если их количество превышает размер сектора диска. Для немедленного сброса буфера можно использовать функцию 68h (функция fflush в С).
Функция DOS 68h - Сброс файловых буферов DOS на диск
| Ввод: |
АН = 68h ВХ = идентификатор |
| Вывод: |
CF = 0, если операция выполнена CF = 1, если произошла ошибка (АХ = код ошибки) |
Для критических участков программ использовать более эффективную функцию 0Dh.
Функция DOS 0Dh - Сброс всех файловых буферов на диск
| Ввод: |
АН = 0Dh |
| Вывод: |
никакого |
Интерфейс EMS
Дополнительная память (EMS) - способ для программ, запускающихся в реальном режиме (или в режиме V86), обращаться к памяти, находящейся за пределами первого мегабайта. EMS позволяет отобразить сегмент памяти, начинающийся обычно с 0D000h, на любые участки памяти, аналогично тому, как осуществляется доступ к видеопамяти в SVGA-режимах. Вызывать функции EMS (прерывание 67h) можно, только если в системе присутствует драйвер с именем ЕММХХХХ0. Для проверки его существования можно, например, вызвать функцию 3Dh (открыть файл или устройство), причем на тот случай, если драйвер EMS отсутствует, а в текущей директории есть файл с именем ЕММХХХХ0, следует дополнительно вызвать функцию IOCTL - INT 21h с АХ = 4400h и ВХ = идентификатор файла или устройства, полученный от функции 3Dh. Если значение бита 7 в DX после вызова этой функции 1, то драйвер EMS наверняка присутствует в системе.
Основные функции EMS:
INT 67h, АН = 46h - Получить номер версии
| Ввод: |
AH = 46h |
| Вывод: |
АН = 0 и AL = номер версии в упакованном BCD (40h для 4.0) |
Во всех случаях, если АН не ноль, произошла ошибка.
INT 67h, АН = 41h - Получить сегментный адрес окна
| Ввод: |
AH = 41h |
| Вывод: |
АН = 0 и ВХ = сегментный адрес окна |
INT 67h, АН = 42h - Получить объем памяти
| Ввод: |
AH = 42h |
| Вывод: |
АН = 0 DX = объем EMS-памяти в 16-килобайтных страницах ВХ = объем свободной EMS-памяти в 16-килобайтных страницах |
INT 67h, АН = 43h - Выделить идентификатор и EMS-память
| Ввод: |
АН = 43h ВХ = требуемое число 16-килобайтных страниц |
| Вывод: |
АН = 0, DX = идентификатор |
Теперь указанный в этой функции набор страниц в EMS-памяти описывается как занятый и другие программы не смогут его выделить для себя.
INT 67h, АН = 44h - Отобразить память
| Ввод: |
АН = 44h AL = номер 16-килобайтной страницы в 64-килобайтном окне EMS (0 – 3) ВХ = номер 16-килобайтной страницы в EMS-памяти DX = идентификатор |
| Вывод: |
АН = 0 |
<
/p>
Теперь запись/чтение в указанную страницу в реальном адресном пространстве приведет к записи/чтению в указанную страницу в EMS-памяти.
INT 67h, АН = 45h - Освободить идентификатор и EMS-память
| Ввод: |
АН = 45h DX = идентификатор |
| Вывод: |
АH = 00h |
Спецификация EMS была разработана для компьютеров IBM XT, снабжавшихся специальной платой, на которой и находилась дополнительная память. С появлением процессора 80286 появилась возможность устанавливать больше одного мегабайта памяти на материнской плате и для работы с ней была введена новая спецификация - XMS. Тогда же появились менеджеры памяти, эмулировавшие EMS поверх XMS, для совместимости со старыми программами, причем работа через EMS оказывалась значительно медленнее. Позже, когда в процессорах Intel появился механизм страничной адресации, оказалось, что теперь уже EMS можно реализовать значительно быстрее XMS. Большинство программ для DOS, которым требуется дополнительная память, поддерживают обе спецификации.
Интерфейс XMS
Спецификация доступа к дополнительной памяти (XMS) - еще один метод, позволяющий программам, запускающимся под управлением DOS в реальном режиме (или в режиме V86), использовать память, расположенную выше границы первого мегабайта.
INT 2Fh, АН = 43 - XMS- и DPMS-сервисы
| Ввод: |
АХ = 4300h: проверить наличие XMS |
| Вывод: |
АН = 80h, если HIMEM.SYS или совместимый драйвер загружен |
| Ввод: |
АХ = 4310h: получить точку входа XMS |
| Вывод: |
ES:BX = дальний адрес точки входа XMS |
После определения точки входа все функции XMS вызываются с помощью команды CALL на указанный дальний адрес.
Функция XMS 00h - Определить номер версии
| Ввод: |
AH = 00h |
| Вывод: |
АХ = номер версии, не упакованный BCD (0300h для 3.0) ВХ = внутренний номер модификации DX = 1, если HMA существует, 0, если нет |
Функция XMS 08h - Определить объем памяти
| Ввод: |
АН = 08h BL = 00h |
| Вывод: |
АХ = размер максимального доступного блока в килобайтах DX = размер всей XMS-памяти в килобайтах BL = код ошибки (A0h, если вся XMS-память занята, 00, если нет ошибок) |
Так как возвращаемый размер памяти оказывается ограниченным размером слова (65 535 Кб), начиная с версии XMS 3.0, введена более точная функция 88h.
Функция XMS 88h - Определить объем памяти
| Ввод: |
AH = 88h |
| Вывод: |
ЕАХ = размер максимального доступного блока в килобайтах BL = код ошибки (A0h, если вся XMS-память занята, 00 - если нет ошибок) ЕСХ = физический адрес последнего байта памяти (верный для ошибки А0h) EDX = размер XMS-памяти всего в килобайтах (0 для ошибки А0h) |
Функция XMS 09h - Выделить память
| Ввод: |
АН = 09h DX = размер запрашиваемого блока (в килобайтах) |
| Вывод: |
АХ = 1, если функция выполнена ВХ = идентификатор блока АХ = 0: BL = код ошибки (A0h, если не хватило памяти) |
Функция XMS 0Ah - Освободить память
| Ввод: |
АН = 0Ah DX = идентификатор блока |
| Вывод: |
АХ = 1, если функция выполнена Иначе - АХ = 0 и BL = код ошибки (A2h - неправильный идентификатор, ABh - участок заблокирован) |
<
/p>
Функция XMS 0Bh - Пересылка данных
| Ввод: |
АН = 0Bh DS:SI = адрес структуры для пересылки памяти |
| Вывод: |
АХ = 1, если функция выполнена Иначе - АХ = 0 и BL = код ошибки |
Структура данных, адрес которой передается в DS:SI:
+00h: 4 байта - число байт для пересылки
+04h: слово - идентификатор источника (0 для обычной памяти)
+06h: 4 байта - смещение в блоке-источнике или адрес в памяти
+0Ah: слово - идентификатор приемника (0 для обычной памяти)
+0Ch: 4 байта - смещение в блоке-приемнике или адрес в памяти
Адреса в обычной памяти записываются в соответствующие двойные слова в обычном виде - сегмент:смещение. Копирование происходит быстрее, если данные выровнены на границы слова или двойного слова; если области данных перекрываются, адрес начала источника должен быть меньше адреса начала приемника.
Функция XMS 0Fh - Изменить размер XMS-блока
| Ввод: |
АН = 0Fh ВХ = новый размер DX = идентификатор блока |
| Вывод: |
АХ = 1, если функция выполнена Иначе - АХ = 0 и BL = код ошибки |
Кроме того, XMS позволяет программам использовать область НМА и блоки UMB, если они не заняты DOS при запуске (так как в CONFIG.SYS не было строк DOS = HIGH или DOS = UMB).
; mem.asm ; сообщает размер памяти, доступной через EMS и XMS ; .model tiny .code .186 ; для команд сдвига на 4 org 100h ; СОМ-программа start: cld ; команды строковой обработки ; будут выполняться вперед
; проверка наличия EMS
mov dx,offset ems_driver ; адрес ASCIZ-строки "EMMXXXX0" mov ax,3D00h int 21h ; открыть файл или устройство jc no_emmx ; если не удалось открыть - EMS нет mov bx,ax ; идентификатор в ВХ mov ax,4400h int 21h ; IOCTL: проверить состояние файла/устройства jc no_ems ; если не произошла ошибка, test dx,80h ; проверить старший бит DX, jz no_ems ; если он - 0, EMMXXXXO - файл в текущем ; каталоге
; определение версии EMS
mov ah,46h int 67h ; получить версию EMS test ah,ah jnz no_ems ; если EMS выдал ошибку - не стоит продолжать ; с ним работать mov ah,al and al,0Fh ; AL = старшая цифра shr ah,4 ; AH = младшая цифра call output_version ; выдать строку о номере версии EMS
; определение доступной EMS-памяти
mov ah,42h int 67h ; получить размер памяти ; в 16-килобайтных страницах shl dx,4 ; DX = размер памяти в килобайтах shl bx,4 ; ВХ = размер свободной памяти в килобайтах mov ax,bx mov si,offset ems_freemem ; адрес строки для output_info call output_info ; выдать строки о размерах памяти
no_ems: mov ah,3Eh int 21h ; закрыть файл/устройство EMMXXXX0 no_emmx:
; проверка наличия XMS
mov ax,4300h int 2Fh ; проверка XMS, cmp al,80h ; если AL не равен 80h, jne no_xms ; XMS отсутствует, mov ax,4310h ; иначе: int 2Fh ; получить точку входа XMS mov word ptr entry_pt,bx ; и сохранить ее в entry_pt mov word ptr entry_pt+2,es ; (старшее слово - по старшему адресу!) push ds pop es ; восстановить ES
; определение версии XMS
mov ah,00 call dword ptr entry_pt ; Функция XMS 00h - номер версии mov byte ptr mem_version,'X' ; изменить первую букву строки ; "EMS версии" на "X" call output_version ; выдать строку о номере версии XMS
; определение доступной XMS-памяти
mov ah,08h xor bx,bx call dword ptr entry_pt ; Функция XMS 08h mov byte ptr totalmem,'X' ; изменить первую букву строки ; "EMS-памяти" на "X" mov si,offset xms_freemem ; строка для output_info
; вывод сообщений на экран: ; DX - объем всей памяти ; АХ - объем свободной памяти ; SI - адрес строки с сообщением о свободной памяти (разный для EMS и XMS)
output_info: push ax mov ax,dx ; объем всей памяти в АХ mov bp,offset totalmem ; адрес строки - в ВР call output_info1 ; вывод pop ах ; объем свободной памяти - в АХ mov bp,si ; адрес строки - в ВР
output_info1: ; вывод mov di,offset hex2dec_word
; hex2dec ; преобразует целое двоичное число в АХ ; в строку десятичных ASCII-цифр в ES:DI, заканчивающуюся символом "$" mov bx,10 ; делитель в ВХ xor сх,сх ; счетчик цифр в 0 divlp: xor dx,dx div bx ; разделить преобразуемое число на 10 add dl,'0' ; добавить к остатку ASCII-код нуля push dx ; записать полученную цифру в стек inc cx ; увеличить счетчик цифр test ax,ax ; и, если еще есть, что делить, jnz divlp ; продолжить деление на 10 store: pop ax ; считать цифру из стека stosb ; дописать ее в конец строки в ES:DI loop store ; продолжить для всех СХ-цифр mov byte ptr es:[di],'$' ; дописать '$' в конец строки mov dx,bp ; DX - адрес первой части строки mov ah,9 int 21h ; Функция DOS 09h - вывод строки mov dx,offset hex2dec_word ; DX - адрес строки с десятичным числом int 21h ; вывод строки mov dx,offset eol ; DX - адрес последней части строки int 21h ; вывод строки
no_xms: ret ; конец программы и процедур ; output_info и output_info1
; вывод версии EMS/XMS ; АХ - номер в неупакованном BCD-формате
output_version: or ax,3030h ; преобразование неупакованного BCD в ASCII mov byte ptr major,ah ; старшая цифра в major mov byte ptr minor,al ; младшая цифра в minor mov dx,offset mem_version ; адрес начала строки - в DX mov ah,9 int 21h ; вывод строки ret
ems_driver db "EMMXXXX0",0 ; имя драйвера для проверки EMS mem_version db "EMS версии " ; сообщение о номере версии major db "0." ; первые байты этой minor db "0 обнаружен ",0Dh,0Ah,"$" ; и этой строк будут ; заменены реальными номерами версий totalmem db "EMS-памяти: $" ems_freemem db "EMS-памяти: $" eol db "K",0Dh,0Ah,'$' ; конец строки xms_freemem db "Наибольший свободный блок XMS: $"
entry_pt: ; сюда записывается точка входа XMS hex2dec_word equ entry_pt+4 ; буфер для десятичной строки end start
Командные параметры и переменные среды
В случае если команда не передавалась бы интерпретатору DOS, а выполнялась нами самостоятельно, то оказалось бы: чтобы запустить любую программу из-под shell.com, потребовалось бы предварительно переходить в каталог с этой программой или вводить ее с полным путем. Дело в том, что COMMAND.COM при запуске файла ищет его по очереди в каждом из каталогов, указанных в переменной среды PATH. DOS создает копию всех переменных среды (так называемое окружение DOS) для каждого запускаемого процесса. Сегментный адрес копии окружения для текущего процесса располагается в PSP по смещению 2Ch. В этом сегменте записаны все переменные подряд в форме ASCIZ-строк вида "COMSPEC=C:/WINDOWS/COMMAND.COM",0. По окончании последней строки стоит дополнительный нулевой байт, затем слово (обычно 1) - количество дополнительных строк окружения, а потом - дополнительные строки. Первая дополнительная строка - всегда полный путь и имя текущей программы - также в форме ASCIZ-строки. При запуске новой программы с помощью функции 4Bh можно создать полностью новое окружение и передать его сегментный адрес запускаемой программе в блоке ЕРВ или просто указать 0, позволив DOS скопировать окружение текущей программы.
Кроме того, в предыдущем примере мы передавали запускаемой программе (command.com) параметры (/с команда), но пока не объяснили, как программа может определить, что за параметры были переданы ей при старте. При запуске программы DOS помещает всю командную строку (включая последний символ 0Dh) в блок PSP запущенной программы по смещению 81h и ее длину в байт 80h (таким образом, длина командной строки не может быть больше 7Eh (126) символов). Под Windows 95 и 4DOS, если командная строка превышает эти размеры, байт PSP:0080h (длина) устанавливается в 7Fh, в последний байт PSP (PSP:00FFh) записывается 0Dh, первые 126 байт командной строки размещаются в PSP, а вся строка целиком - в переменной среды CMDLINE.
; cat.asm ; копирует объединенное содержимое всех файлов, указанных в командной строке, ; в стандартный вывод. Можно как указывать список файлов, так и использовать ; маски (символы "*" и "?") в одном или нескольких параметрах, ; например: ; cat header *.txt footer > all-texts помещает содержимое файла ; header, всех файлов с расширением .txt в текущем каталоге и файла ; footer - в файл all-texts ; длинные имена файлов не используются, ошибки игнорируются ; .model tiny .code org 80h ; по смещению 80h от начала PSP находятся: cmd_length db ? ; длина командной строки cmd_line db ? ; и сама командная строка org 100h ; начало СОМ-программы - 100h от начала PSP start: cld ; для команд строковой обработки mov bp,sp ; сохранить текущую вершину стека в ВР mov cl,cmd_length cmp cl,1 ; если командная строка пуста - jle show_usage ; вывести информацию о программе и выйти
mov ah,1Ah ; функция DOS 1Ah mov dx,offset DTA int 21h ; переместить DTA (по умолчанию она совпадает ; с командной строкой PSP)
; преобразовать список параметров в PSP:81h следующим образом: ; каждый параметр заканчивается нулем (ASCIZ-строка) ; адреса всех параметров помещаются в стек в порядке обнаружения ; в переменную argc записывается число параметров
mov cx,-1 ; для команд работы со строками mov di,offset cmd_line ; начало командной строки в ES:DI
find_param: mov al,' ' ; искать первый символ, repz scasb ; не являющийся пробелом dec di ; DI - адрес начала очередного параметра push di ; поместить его в стек inc word ptr argc ; и увеличить argc на один mov si,di ; SI = DI для следующей команды lodsb
scan_params: lodsb ; прочитать следующий символ из параметра, cmp al,0Dh ; если это 0Dh - это был последний параметр je params_ended ; и он кончился, cmp al,20h ; если это 20h - этот параметр кончился, jne scan_params ; но могут быть еще
dec si ; SI - первый байт после конца параметра mov byte ptr [si],0 ; записать в него 0 mov di,si ; DI = SI для команды scasb inc di ; DI - следующий после нуля символ jmp short find_param ; продолжить разбор командной строки
params_ended: dec si ; SI - первый байт после конца последнего mov byte ptr [si],0 ; параметра - записать в него 0
; каждый параметр воспринимается как файл или маска для поиска файлов, ; все найденные файлы выводятся на stdout. Если параметр - не имя файла, ; то ошибка игнорируется
mov si,word ptr argc ; SI - число оставшихся параметров
next_file_from_param: dec bp dec bp ; BP - адрес следующего адреса параметра dec si ; уменьшить число оставшихся параметров, js no_more_params ; если оно стало отрицательным - все mov dx,word ptr [bp] ; DS:DX - адрес очередного параметра mov ah,4Eh ; Функция DOS 4Eh mov cx,0100111b ; искать все файлы, кроме каталогов ; и меток тома int 21h ; найти первый файл, jc next_file_from_param ; если произошла ошибка - файла нет call output_found ; вывести найденный файл на stdout
find_next: mov ah,4Fh ; Функция DOS 4Fh mov dx,offset DTA ; адрес нашей области DTA int 21h ; найти следующий файл, jc next_file_from_param ; если ошибка - файлы кончились call output_found ; вывести найденный файл на stdout jmp short find_next ; продолжить поиск файлов
no_more_params: mov ax,word ptr argc shl ax,1 add sp,ax ; удалить из стека 2 * argc байт ; (то есть весь список адресов ; параметров командной строки) ret ; конец программы
; процедура show_usage ; выводит информацию о программе
show_usage: mov ah,9 ; Функция DOS 09h mov dx,offset usage int 21h ; вывести строку на экран ret ; выход из процедуры
; процедура output_found ; выводит в stdout файл, имя которого находится в области DTA
output_found: mov dx,offset DTA+1Eh ; адрес ASCIZ-строки с именем файла mov ax,3D00h ; Функция DOS 3Dh int 21h ; открыть файл (al = 0 - только на чтение), jc skip_file ; если ошибка - не трогать этот файл mov bx,ax ; идентификатор файла - в ВХ mov di,1 ; DI будет хранить идентификатор STDOUT
do_output: mov cx,1024 ; размер блока для чтения файла mov dx,offset DTA+45 ; буфер для чтения/записи располагается ; за концом DTA mov ah,3Fh ; Функция DOS 3Fh int 21h ; прочитать 1024 из файла, jc file_done ; если ошибка - закрыть файл mov cx,ax ; число реально прочитанных байт в СХ, jcxz file_done ; если это не ноль - закрыть файл mov ah,40h ; Функция DOS 40h xchg bx,di ; BX = 1 - устройство STDOUT int 21h ; вывод прочитанного числа байт в STDOUT xchg di,bx ; вернуть идентификатор файла в ВХ, jc file_done ; если ошибка - закрыть файл jmp short do_output ; продолжить вывод файла
file_done: mov ah,3Eh ; Функция DOS 3Eh int 21h ; закрыть файл
skip_file: ret ; конец процедуры output_found
usage db "cat.com v1.0",0Dh,0Ah db "объединяет и выводит файлы на stdout",0Dh,0Ah db "использование: cat имя_файла, ...",0Dh,0Ah db "(имя файла может содержать маски * и ?)",0Dh,0Ah,'$' argc dw 0 ; число параметров (должен быть 0 при старте программы!)
DTA: ; область DTA начинается сразу за концом файла, ; а сразу за областью DTA начинается ; 1024-байтный буфер для чтения файла end start
Размер блока для чтения файла можно значительно увеличить, но в этом случае почти наверняка потребуется проследить за объемом памяти, доступным для программы.
Область памяти HMA
Область памяти от FFFFh:0010h (конец первого мегабайта) до FFFFh:FFFFh (конец адресного пространства в реальном режиме), 65 520 байт, может использоваться на компьютерах, начиная с 80286. Доступ к этой области осуществляется с помощью спецификации XMS, причем вся она выделяется целиком одной программе. Обычно, если загружен драйвер HIMEM.SYS и если в файле CONFIG.SYS присутствует строка DOS = HIGH, DOS занимает эту область, освобождая почти 64 Кб в основной памяти. При этом DOS может оставить небольшой участок HMA (16 Кб или меньше) для пользовательских программ, которые обращаются к нему с помощью недокументированной функции мультиплексора 4Ah.
INT 2Fh, AX= 4A01h - Определить размер доступной части HMA (DOS 5.0+)
| Ввод: |
АХ = 4A01h |
| Вывод: |
ВХ = размер доступной части HMA в байтах, 0000h, если DOS не в HMA ES:DI = адрес начала доступной части НМА (FFFFh:FFFFh, если DOS не в НМА) |
INT 2Fh, АХ= 4A02h - Выделить часть НМА (DOS 5.0+)
| Ввод: |
АХ = 4А02h ВХ = размер в байтах |
| Вывод: |
ES:DI = адрес начала выделенного блока ВХ = размер выделенного блока в байтах |
В версиях DOS 5.0 и 6.0 нет функций освобождения выделенных таким образом блоков НМА. В DOS 7.0 (Windows 95) выделение памяти в НМА было организовано аналогично выделению памяти в обычной памяти и UMB, с функциями изменения размера и освобождения блока.
INT 2Fh, АХ = 4A03h - Управление распределением памяти в НМА (DOS 7.0+)
| Ввод: |
АХ = 4A03h DL = 0 - выделить блок (ВХ = размер в байтах) DL = 1 - изменить размер блока (ES:DI = адрес, ВХ = размер) DL = 2 - освободить блок (ES:DI = адрес) СХ = сегментный адрес владельца блока |
| Вывод: |
DI = FFFFh, если не хватило памяти, ES:DI = адрес блока (при выделении) |
 |
|
Следует помнить, что область НМА доступна для программ только в том случае, если адресная линия процессора А20 разблокирована. Если DOS не занимает НМА, она почти всегда заблокирована для совместимости с программами, написанными для процессора 8086/8088, которые считают, что адреса FFFFh:0010h - FFFFh:FFFFh всегда совпадают с 0000h:0000h - 0000h:FFEFh. Функции XMS 01 – 07 предоставляют возможность управления состоянием этой адресной линии. | |
|
Область памяти UMB
Функция DOS 58h - Считать/изменить стратегию выделения памяти
| Ввод: |
АН = 58h AL = 00h - считать стратегию AL = 01h - изменить стратегию ВХ = новая стратегия
биты 2 – 0:
00 - первый подходящий блок
01 - наиболее подходящий блок
11 - последний подходящий блок
биты 4 – 3:
00 - обычная память
01 - UMB (DOS 5.0+)
10 - UMB, затем обычная память (DOS 5.0+)
AL = 02h - считать состояние UMB AL = 03h - установить состояние UMB ВХ = новое состояние: 00 - не используются, 01 - используются |
| Вывод: |
CF = 0, АХ = текущая стратегия для AL = 0, состояние UMB для AL = 2 CF = 1, AX = 01h, если функция не поддерживается (если не запущен менеджер памяти (например, EMM386) или нет строки DOS = UMB в CONFIG.SYS |
Если программа изменяла стратегию выделения памяти или состояние UMB, она обязательно должна их восстановить перед окончанием работы.
Обычная память
До сих пор, если требовалось создать массив данных в памяти, мы просто обращались к памяти за концом программы, считая, что там есть еще хотя бы 64 килобайта свободной памяти. Разумеется, как и во всех операционных системах, в DOS есть средства управления распределением памяти - выделение блока (аналог стандартной функции языка С malloc), изменение его размеров (аналог realloc) и освобождение (free).
Функция DOS 48h - Выделить память
| Ввод: |
АН = 48h ВХ = размер блока в 16-байтных параграфах |
| Вывод: |
CF = 0, если блок выделен АХ = сегментный адрес выделенного блока CF = 1, если произошла ошибка:АХ = 7 - блоки управления памятью разрушены АХ = 8 - недостаточно памяти:ВХ = размер максимального доступного блока |
Эта функция с большим значением в ВХ (обычно FFFFh) используется для определения размера самого большого доступного блока памяти.
Функция DOS 49h - Освободить память
| Ввод: |
АН = 49h ES = сегментный адрес освобождаемого блока |
| Вывод: |
CF = 0, если блок освобожден CF = 1:АХ = 7, если блоки управления памятью разрушены, АХ = 9, если в ES содержится неверный адрес |
Эта функция не позволит освободить блок памяти, которым текущая программа не владеет, но с помощью функции DOS 50h (AX = 50h, ВХ = сегментный адрес PSP процесса) программа может "притвориться" любым другим процессом.
Функция DOS 4Ah - Изменить размер блока памяти
| Ввод: |
АН = 4Ah ВХ = новый размер в 16-байтных параграфах ES = сегментный адрес модифицируемого блока |
| Вывод: |
CF = 0, если размер блока изменен CF = 1:
АХ = 7, если блоки управления памятью разрушены, АХ = 8, если не хватает памяти (при увеличении), АХ = 9, если ES содержит неверный адрес
ВХ = максимальный размер, доступный для этого блока |
Если для увеличения блока не хватило памяти, DOS увеличивает его до возможного предела.
При запуске СОМ-программы загрузчик DOS выделяет самый большой доступный блок памяти для этой программы, так что при работе с основной памятью эти функции требуются редко (в основном для того, чтобы сократить выделенный программе блок памяти до минимума перед загрузкой другой программы), но уже в MS-DOS 5.0 и далее с помощью этих же функций можно выделять память в областях UMB - неиспользуемых участках памяти выше 640 Кб и ниже 1 Мб, для этого требуется сначала подключить UMB к менеджеру памяти и изменить стратегию выделения памяти с помощью функции DOS 58h.
Основы программирования для MS-DOS
Программа, написанная на ассемблере, так же как и программа, написанная на любом другом языке программирования, выполняется не сама по себе, а при помощи операционной системы. Операционная система выделяет области памяти для программы, загружает ее, передает ей управление и обеспечивает взаимодействие программы с устройствами ввода-вывода, файловыми системами и другими программами (разумеется, кроме тех случаев, когда эта программа сама является операционной системой или ее частью). Способы взаимодействия программы с внешним миром различны для разных операционных систем, так что программа, написанная для Windows, не будет работать в DOS, а программа для Linux - в Solaris/x86, хотя все эти системы могут работать на одном и том же компьютере.
Самая простая и распространенная операционная система для компьютеров, основанных на процессорах Intel, - DOS (Дисковая Операционная Система). Она распространяется как сама по себе несколькими производителями - Microsoft (MS-DOS), IBM (PC-DOS), Novell (Novell DOS), Caldera (Open DOS) и др., так и в виде части систем Microsoft Windows 95 и старше. DOS предоставляет программам полную свободу действий, никак не ограничивая доступ к памяти и внешним устройствам, позволяя им самим управлять процессором и распределением памяти. По этой причине DOS лучше всего подходит для того, чтобы близко познакомиться с устройством компьютера и возможностями, которые может использовать программа на ассемблере, но которые часто скрываются компиляторами с языков высокого уровня и более совершенными операционными системами.
Итак, чтобы программа выполнилась любой ОС, она должна быть скомпилирована в исполнимый файл. Основные два формата исполнимых файлов в DOS - СОМ и ЕХЕ. Файлы типа СОМ содержат только скомпилированный код без какой-либо дополнительной информации о программе. Весь код, данные и стек такой программы располагаются в одном сегменте и не могут превышать 64 килобайта. Файлы типа ЕХЕ содержат заголовок, в котором описывается размер файла, требуемый объем памяти, список команд в программе, использующих абсолютные адреса, которые зависят от расположения программы в памяти, и т.д. ЕХЕ-файл может иметь любой размер. Формат ЕХЕ также используется для исполнимых файлов в различных версиях DOS-расширителей и Windows, но со значительными изменениями.
 |
|
Несмотря на то что файлам типа СОМ принято давать расширение .com, а файлам типа EXE - .exe, DOS не использует расширения для определения типа файла. Первые два байта заголовка ЕХЕ-файла - символы "MZ" или "ZM", и если файл начинается с этих символов и длиннее некоторого порогового значения, разного для разных версий DOS, он загружается как ЕХЕ, если нет - как СОМ. | |
|
Кроме обычных исполнимых программ DOS может загружать драйверы устройств - специальные программы, используемые для упрощения доступа к внешним устройствам. Например, драйвер устройства LPT, входящий в IO.SYS, позволяет посылать тексты на печать из DOS простым копированием файла в LPT, а драйвер RAMDISK.SYS позволяет выделить область памяти и обращаться к ней, как к диску. Написание драйверов значительно сложнее, чем написание обычных программ, и рассмотрено далее.
Параллельный порт
Параллельные порты используются в первую очередь для подключения принтеров, хотя встречаются и другие устройства, например переносные жесткие диски, которые могут подключаться к этим портам. Базовые средства DOS и BIOS для работы с параллельными портами аналогичны соответствующим средствам для работы с последовательными портами: DOS инициализирует стандартное устройство PRN, соответствующее первому порту LPT1, которое может быть переопределено командой MODE, и предоставляет прерывание для вывода в это устройство.
Функция DOS 05h - Вывод символа в стандартное устройство PRN
| Ввод: |
АН = 05h DL = символ |
Кроме того, можно пользоваться функцией записи в файл или устройство, поместив в ВХ число 4, соответствующее устройству PRN. BIOS, в свою очередь, предоставляет базовый набор из трех функций для работы с принтером.
INT 17h, АН = 00 - Вывести символ в принтер
| Ввод: |
АН = 00h AL = символ DX = номер параллельного порта (00 - LPT1, 01 - LPT2, 02 - LPT3) |
| Вывод: |
АН = состояние принтера:
бит 7: принтер не занят
бит 6: подтверждение
бит 5: нет бумаги
бит 4: принтер в состоянии on-line
бит 3: ошибка ввода-вывода
бит 0: тайм-аут |
INT 17h, АН = 01 - Выполнить аппаратный сброс принтера
| Ввод: |
АН = 01h DX = номер порта (00h - 02h) |
| Вывод: |
АН = состояние принтера |
INT 17h, AH = 02 - Получить состояние принтера
| Ввод: |
АН = 02h DX = номер порта (00h – 02h) |
| Вывод: |
АН = состояние принтера |
Например, чтобы распечатать содержимое экрана на принтере, можно написать такую программу:
; prtscr.asm ; распечатать содержимое экрана на принтере ; .model tiny .code .186 ; для команды push 0B800h org 100h ; начало СОМ-файла start: mov ah,1 mov dx,0 ; порт LPT1 int 17h ; инициализировать принтер, cmp ah,90h ; если принтер не готов, jne printer_error ; выдать сообщение об ошибке, push 0B800h ; иначе: pop ds ; DS = сегмент видеопамяти в текстовом режиме xor si,si ; SI = 0 mov cx,80*40 ; CX = число символов на экране cld ; строковые операции вперед main_loop: lodsw ; AL - символ, АН - атрибут, SI = SI + 2 mov ah,0 ; АН - номер функции int 17h ; вывод символа из AL на принтер loop main_loop ret ; закончить программу
printer_error: mov dx,offset msg ; адрес сообщения об ошибке в DS:DX mov ah,9 int 21h ; вывод строки на экран ret
msg db "Принтер на LPT1 находится в состоянии offline или занят$" end start
Чтобы распечатать экран в текстовом режиме на LPT1, достаточно всего лишь одной команды INT 05h, что в точности эквивалентно нажатию клавиши PrtScr.
Поиск файлов
Найти нужный файл на диске намного сложнее, чем просто открыть его, - для этого требуются две функции при работе с короткими именами (найти первый файл и найти следующий файл) и три - при работе с длинными именами в DOS 7.0 (найти первый файл, найти следующий файл, прекратить поиск).
Функция DOS 4Eh - Найти первый файл
| Ввод: |
АН = 4Eh AL используется при обращении к функции APPEND СХ = атрибуты, которые должен иметь файл (биты 0 (только для чтения) и 5 (архивный бит) игнорируются, если бит 3 (метка тома) установлен, все остальные биты игнорируются) DS:DX = адрес ASCIZ-строки с именем файла, которое может включать путь и маски для поиска (символы * и ?) |
| Вывод: |
CF = 0 и область DTA заполняется данными, если файл найден CF = 1 и АХ = 02h, если файл не найден, 03h - если путь не найден, 12h - если неправильный режим доступа |
Вызов этой функции заполняет данными область памяти DTA (область передачи данных), которая начинается по умолчанию со смещения 0080h от начала блока данных PSP (при запуске СОМ- и ЕХЕ-программ сегменты DS и ES содержат сегментный адрес начала PSP), но ее можно переопределить с помощью функции 1Ah.
Функция DOS 1Ah - Установить область DTA
| Ввод: |
АН = 1Ah DS:DX = адрес начала DTA (128-байтный буфер) |
Функции поиска файлов заполняют DTA следующим образом:
+00h: байт - биты 0 – 6: ASCII-код буквы диска; бит 7: диск сетевой
+01h: 11 байт - маска поиска (без пути)
+0СН: байт - атрибуты для поиска
+0Dh: слово - порядковый номер файла в каталоге
+0Fh: слово - номер кластера начала внешнего каталога
+11h: 4 байта - зарезервировано
+15h: байт - атрибут найденного файла
+16h: слово - время создания файла в формате DOS:
биты 15 – 11: час (0 - 23)
биты 10 – 5: минута
биты 4 – 0: номер секунды, деленный на 2 (0 – 30)
+18h: слово - дата создания файла в формате DOS:
биты 15 – 9: год, начиная с 1980
биты 8 – 5: месяц
биты 4 – 0: день
+1Ah: 4 байта - размер файла
+1Eh: 13 байт - ASCIZ-имя найденного файла с расширением
После того как DTA заполнена данными, для продолжения поиска следует вызывать функцию 4Fh, пока не будет возвращена ошибка.
Функция DOS 4Fh - Найти следующий файл
| Ввод: |
АН = 4Fh DTA - содержит данные от предыдущего вызова функции 4Е или 4F |
| Вывод: |
CF = 0 и DTA содержит данные о следующем найденном файле, если не произошла ошибка CF = 1 и АХ = код ошибки, если произошла ошибка |
Для случая длинных имен файлов (LFN) употребляется набор из трех подфункций функции DOS 71h, которые можно использовать, только если запущен IFSmgr (всегда запускается при обычной установке Windows 95, но не запускается, например, с загрузочной дискеты MS-DOS 7.0).
Функция LFN 4Eh - Найти первый файл с длинным именем
| Ввод: |
АХ = 714Eh CL = атрибуты, которые файл может иметь (биты 0 и 5 игнорируются) СН = атрибуты, которые файл должен иметь SI = 0: использовать Windows-формат даты/времени SI = 1: использовать DOS-формат даты/времени DS:DX = адрес ASCIZ-строки с маской для поиска (может включать * и ?. Для совместимости маска *.* ищет все файлы в каталоге, а не только файлы, содержащие точку в имени) ES:DI = адрес 318-байтного буфера для информации о файле |
| Вывод: |
CF = 0 АХ = поисковый идентификатор СХ = Unicode-флаг:
бит 0: длинное имя содержит подчеркивания вместо непреобразуемых Unicode-символов бит 1: короткое имя содержит подчеркивания вместо непреобразуемых Unicode-символовCF = 1, АХ = код ошибки, если произошла ошибка (7100h - функция не поддерживается) |
Если файл, подходящий под маску и атрибуты поиска, найден, область данных по адресу ES:DI заполняется следующим образом:
+00h: 4 байта - атрибуты файла
биты 0 – 6: атрибуты файла DOS
бит 8: временный файл
+04h: 8 байт - время создания файла
+0Ch: 8 байт - время последнего доступа к файлу
+14h: 8 байт - время последней модификации файла
+1Ch: 4 байта - старшее двойное слово длины файла
+20h: 4 байта - младшее двойное слово длины файла
+24h: 8 байт - зарезервировано
+2Ch: 260 байт - ASCIZ-имя файла длинное
+130h: 14 байт - ASCIZ-имя файла короткое
Причем даты создания/доступа/модификации записываются в одном из двух форматов, в соответствии со значением SI при вызове функции. Windows-формат - 64-битное число 100-наносекундных интервалов с 1 января 1601 года, а если используется DOS-формат - в старшее двойное слово записывается DOS-дата, а в младшее - DOS-время.
Функция LFN 4Fh - Найти следующий файл
| Ввод: |
АХ = 714Fh ВХ = поисковый идентификатор (от функции 4Eh) SI = формат даты/времени ES:DI = адрес буфера для информации о файле |
| Вывод: |
CF = 0 и СХ = Unicode-флаг, если следующий файл найден CF = 1, АХ = код ошибки, если произошла ошибка (7100h - функция не поддерживается) |
Функция LFN A1h - Закончить поиск файла
| Ввод: |
АХ = 71A1h ВХ = поисковый идентификатор |
| Вывод: |
CF = 0, если операция выполнена CF = 1 и АХ = код ошибки, если произошла ошибка (7100h - функция не поддерживается) |
В качестве примера программы, использующей многие из функций работы с файлами, рассмотрим программу, заменяющую русские буквы "Н" на латинские "Н" во всех файлах с расширением .ТХТ в текущем каталоге (такая замена требуется для всех текстов, которые будут пересылаться через сеть Fidonet, программное обеспечение которой воспринимает русскую букву "Н" как управляющий символ).
; fidoh.asm ; заменяет русские "Н" на латинские "Н" во всех файлах с расширением .ТХТ ; в текущем каталоге .model tiny .code org 100h ; СОМ-файл start: mov ah,4Eh ; поиск первого файла xor cx,cx ; не системный, не каталог и т.д. mov dx,offset filespec ; маска для поиска в DS:DX file_open: int 21h jc no_more_files ; если CF = 1 - файлы кончились
mov ax,3D02h ; открыть файл для чтения и записи mov dx,80h+1Eh ; смещение DTA + смещение имени файла int 21h ; от начала DTA jc find_next ; если файл не открылся - перейти ; к следующему mov bx,ax ; идентификатор файла в ВХ mov cx,1 ; считывать один байт mov dx,offset buffer ; начало буфера - в DX read_next: mov ah,3Fh ; чтение файла int 21h jc find_next ; если ошибка - перейти к следующему dec ах ; если АХ = 0 - файл кончился - js find_next ; перейти к следующему cmp byte ptr buffer,8Dh ; если не считана русская "Н", jne read_next ; считать следующий байт, mov byte ptr buffer,48h ; иначе - записать в буфер ; латинскую букву "Н" mov ax,4201h ; переместить указатель файла от текущей dec cx ; позиции назад на 1 dec cx ; CX = FFFFh mov dx,cx ; DX = FFFFh int 21h mov ah,40h ; записать в файл inc cx inc cx ; один байт (СХ = 1) mov dx,offset buffer ; из буфера в DS:DX int 21h jmp short read_next ; считать следующий байт
find_next: mov ah,3Eh ; закрыть предыдущий файл int 21h mov ah,4Fh ; найти следующий файл mov dx,80h ; смещение DTA от начала PSP jmp short file_open
no_more_files: ; если файлы кончились, ret ; выйти из программы
filespec db "*.txt",0 ; маска для поиска buffer label byte ; буфер для чтения/записи - end start ; за концом программы
Последовательный порт
Каждый компьютер обычно оборудован, по крайней мере, двумя последовательными портами, которые чаще всего используются для подключения мыши и модема, но также могут использоваться и для подключения других дополнительных устройств или соединения компьютеров между собой. Для работы с устройствами, подключенными к портам, такими как мышь, используются драйверы, которые общаются с последовательным портом непосредственно на уровне портов ввода-вывода и предоставляют программам некоторый набор функций более высокого уровня, так что прямая работа с последовательными портами оказывается необходимой только при написании таких драйверов, работе с нестандартными устройствами или с модемами.
DOS всегда инициализирует первый порт СОМ1 как 2400 бод, 8N1 (8 бит в слове, 1 стоп-бит, четность не проверяется) и связывает с ним устройство STDAUX. В это устройство можно записывать и считывать один байт функциями 3 и 4.
Функция DOS 03h - Считать байт из STDAUX
| Ввод: |
АН = 03h |
| Вывод: |
AL = считанный байт |
Функция DOS 04h - Записать байт в STDAUX
Или же можно воспользоваться функциями записи в файл (40h) и чтения из файла (3Fh), поместив в ВХ число 3, как это было показано ранее для вывода на экран.
Хотя установленную DOS скорость работы порта (2400 бод) и можно изменить командой MODE, все равно отсутствие обработки ошибок, буферизации и гибкого управления состоянием порта делает эти функции DOS практически неприменимыми. BIOS позволяет управлять любым из портов, писать и читать один байт и считывать состояние порта с помощью функций прерывания 14h, но BIOS (так же как и DOS) не позволяет инициализировать порт на скорость выше, чем 9600 бод. Таким образом выясняется, что большинство программ вынуждено программировать порты напрямую, но, если в системе присутствует драйвер, предоставляющий набор сервисов FOSSIL (такие как Х00 или BNU), оказывается возможным пользоваться для полноценного буферированного обмена данными с последовательными портами только функциями прерывания 14h.
INT 14h АН = 04 - Инициализация FOSSIL-драйвера
| Ввод: |
АН = 04h DX = номер порта (0 - для СОМ1, 1 - для COM2 и т.д.) |
| Вывод: |
АХ = 1954h BL = максимальный поддерживаемый номер функции ВН = версия спецификации FOSSIL |
INT 14h АН = 05 - Деинициализация FOSSIL-драйвера
| Ввод: |
АН = 05h DX = номер порта (00h – 03h) |
INT 14h АН = 00 - Инициализация последовательного порта
| Ввод: |
АН = 00h
AL = параметры инициализации:
биты 7 – 5:
000 - 19 200 бод (110 бод без FOSSIL)
001 - 38 400 бод (150 бод без FOSSIL)
010 - 300 бод
011 - 600 бод
100 - 1200 бод
101 - 2400 бод
110 - 4800 бод
111 - 9600 бод
биты 4 – 3: четность (01 - четная, 11 - нечетная, 00 или 10 - нет)
бит 2: число стоп-бит (0 - один, 1 - два)
биты 1 – 0: длина слова (00 - 5, 01 - 6, 10 - 7, 11 - 8)
DX = номер порта (00h – 03h) |
| Вывод: |
АН = состояние порта
бит 7: тайм-аут
бит 6: буфер вывода пуст ( без FOSSIL: регистр сдвига передатчика пуст)
бит 5: в буфере вывода есть место (без FOSSIL: регистр хранения передатчика пуст)
бит 4: обнаружено состояние BREAK
бит 3: ошибка синхронизации
бит 2: ошибка четности
бит 1: ошибка переполнения - данные потеряны
бит 0: в буфере ввода есть данные
AL = состояние модема
бит 7: обнаружена несущая (состояние линии DCD)
бит 6: обнаружен звонок (состояние линии RI)
бит 5: запрос для передачи (состояние линии DSR)
бит 4: сброс для передачи (состояние линии CTS)
бит 3: линия DCD изменила состояние
бит 2: линия RI изменила состояние
бит 1: линия DSR изменила состояние
бит 0: линия CTS изменила состояние
|
<
/p>
INT 14h АН =01 - Запись символа в последовательный порт
| Ввод: |
АН = 01h AL = символ DX = номер порта (00h – 03h) |
| Вывод: |
АН = состояние порта |
INT 14h АН = 02 - Чтение символа из последовательного порта с ожиданием
| Ввод: |
АН = 02h DX = номер порта |
| Вывод: |
АН = состояние порта AL = считанный символ, если бит 7 АН равен нулю (не было тайм-аута) |
INT 14h AH = 03 - Получить текущее состояние порта
| Ввод: |
АН = 03h DX = номер порта (00h – 03h) |
| Вывод: |
АН = состояние линии AL = состояние модема |
Воспользуемся этими функциями, чтобы написать короткую терминальную программу:
; term.asm ; Простая терминальная программа для модема на COM2. Выход по Alt-X ; .model tiny .code org 100h ; Начало СОМ-файла start: mov ah,0 ; инициализировать порт mov al,11100011b ; 9600/8n1 mov dx,1 ; порт COM2 int 14h
main_loop: mov ah,2 int 14h ; получить байт от модема, test ah,ah ; если что-нибудь получено, jnz no_input int 29h ; вывести его на экран no_input: ; иначе: mov ah,1 int 16h ; проверить, была ли нажата клавиша, jz main_loop ; если да: mov ah,8 int 21h ; считать ее код (без отображения на экране), test al,al ; если это нерасширенный ASCII-код, jnz send_char ; перейти к посылке его в модем, int 21h ; иначе - получить расширенный ASCII-код, cmp al,2Dh ; если это Alt-X, jne send_char ret ; завершить программу send_char: mov ah, 1 int 14h ; послать введенный символ в модем jmp short main_loop ; продолжить основной цикл
end start
Этот терминал тратит чрезмерно много процессорного времени на постоянные вызовы прерываний 14h и 16h. Более эффективным оказывается подход, состоящий в перехвате прерываний от внешних устройств, о котором рассказано далее.
Прямая работа с видеопамятью
Все, что изображено на мониторе - и графика, и текст, одновременно присутствует в памяти, встроенной в видеоадаптер. Для того чтобы изображение появилось на мониторе, оно должно быть записано в память видеоадаптера. Для этого отводится специальная область памяти, начинающаяся с абсолютного адреса B800h:0000h (для текстовых режимов) и заканчивающаяся на B800h:FFFFh. Все, что программы пишут в эту область памяти, немедленно пересылается в память видеоадаптера. В текстовых режимах для хранения каждого изображенного символа используются два байта: байт с ASCII-кодом символа и байт с его атрибутом, так что по адресу B800h:0000h лежит байт с кодом символа, находящимся в верхнем левом углу экрана; по адресу B800h:0001h лежит атрибут этого символа; по адресу B800h:0002h лежит код второго символа в верхней строке экрана и т.д.
Таким образом, любая программа может вывести текст на экран простой командой пересылки данных, не прибегая ни к каким специальным функциям DOS или BIOS.
; dirout.asm ; Выводит на экран все ASCII-символы без исключения, ; используя прямой вывод на экран .model tiny .code .386 ; будет использоваться регистр ЕАХ ; и команда STOSD org 100h ; начало СОМ-файла start: mov ax,0003h int 10h ; видеорежим 3 (очистка экрана) cld ; обработка строк в прямом направлении ; подготовка данных для вывода на экран mov еах,1F201F00h ; первый символ 00 с атрибутом 1Fh, ; затем пробел (20h) с атрибутом 1Fh mov bx,0F20h ; пробел с атрибутом 0Fh mov cx,255 ; число символов минус 1 mov di,offset ctable ; ES:DI - начало таблицы cloop: stosd ; записать символ и пробел в таблицу ctable inc al ; AL содержит следующий символ test cx,0Fh ; если СХ не кратен 16, jnz continue_loop ; продолжить цикл, push cx ; иначе: сохранить значение счетчика mov cx,80-32 ; число оставшихся до конца строки символов xchg ax,bx rep stosw ; заполнить остаток строки пробелами ; с атрибутом 0F xchg bx.ax ; восстановить значение ЕАХ pop cx ; восстановить значение счетчика continue_loop: loop cloop
stosd ; записать последний (256-й) символ и пробел
; собственно вывод на экран mov ax,0B800h ; сегментный адрес видеопамяти mov es,ax xor di,di ; DI = 0, адрес начала видеопамяти в ES:DI mov si,offset ctable ; адрес таблицы в DS:SI mov cx,15*80+32 ; 15 строк по 80 символов, последняя строка - 32 rep movsw ; скопировать таблицу ctable в видеопамять ret ; завершение СОМ-файла ctable: ; Данные для вывода на экран начинаются сразу ; за концом файла. В ЕХЕ-файле такие данные ; определяют в сегменте .data? end start
При подготовке данных для копирования в видеопамять в этой программе использовался тот факт, что в архитектуре Intel при записи слова (или двойного слова) в память старший байт располагается по старшему адресу. Так что при записи в память двойного слова 1F201F00h сначала записывается самый младший байт 00h (ASCII-код текущего символа), потом 1Fh, используемый в этом примере атрибут, потом 20h (код пробела) и потом, по самому старшему адресу, - самый старший байт, 1Fh, атрибут для этого пробела. Кроме того, в этом примере использовались некоторые 32-битные команды (MOV и STOSD). Этими командами можно пользоваться из 16-битной программы (разумеется, если процессор 80386 и выше), но не стоит этим злоупотреблять, так как каждая такая команда оказывается длиннее на 1 байт и выполняется дольше на 1 такт.
Программа типа ЕХЕ
ЕХЕ-программы немного сложнее в исполнении, но для них отсутствует ограничение размера в 64 килобайта, так что все достаточно большие программы используют именно этот формат. Конечно, ассемблер позволяет уместить и в 64 килобайтах весьма сложные и большие алгоритмы, а все данные хранить в отдельных файлах, но ограничение размера все равно очень серьезно, и даже чисто ассемблерные программы могут с ним сталкиваться.
; hello-2.asm ; Выводит на экран сообщение "Hello World!" и завершается .model small ; модель памяти, используемая для ЕХЕ .stack 100h ; сегмент стека размером в 256 байт .code start: mov ax,DGROUP ; сегментный адрес строки message mov ds,ax ; помещается в DS mov dx,offset message mov ah,9 int 21h ; функция DOS "вывод строки" mov ax,4C00h int 21h ; функция DOS "завершить программу" .data message db "Hello World!",0Dh,0Ah,'$' end start
В этом примере определяются три сегмента - сегмент стека директивой .STACK размером в 256 байт, сегмент кода, начинающийся с директивы .CODE, и сегмент данных, начинающийся с .DATA и содержащий строку. При запуске ЕХE-программы регистр DS уже не содержит адреса сегмента со строкой message (он указывает на сегмент, содержащий блок данных PSP), а для вызова используемой функции DOS этот регистр должен иметь сегментный адрес строки. Команда MOV AX,DGROUP загружает в АХ сегментный адрес группы сегментов данных DGROUP, a MOV DS,AX копирует его в DS. Для ассемблеров MASM и TASM можно использовать вместо DGROUP предопределенную метку "@data", но единственная модель памяти, в которой группа сегментов данных называется иначе, - FLAT (ей мы пока пользоваться не будем). И наконец, программы типа ЕХЕ должны завершаться системным вызовом DOS 4Ch: в регистр АН помещается значение 4Ch, в регистр AL помещается код возврата (в данном примере код возврата 0 и регистры АН и AL загружаются одной командой MOV AX,4C00h), после чего вызывается прерывание 21h.
Компиляция hello-2.asm:
Для TASM:
tasm hello-2.asm tlink /x hello-2.obj
Размер получаемого файла hello-2.exe - 559 байт.
Для MASM:
ml /с hello-2.asm link hello-2.obj
Размер получаемого файла hello-2.exe - 545 байт.
Для WASM:
wasm hello-2.asm wlink file hello-2.obj form DOS
Размер получаемого файла hello-2.exe - 81 байт.
Расхождения в размерах файлов вызваны различными соглашениями о выравнивании сегментов программы по умолчанию. Все примеры программ для DOS в этой книге рассчитаны на компиляцию в СОМ-файлы, так как идеология работы с памятью в них во многом совпадает с идеологией, применяемой при программировании под расширители DOS, DPMI и Windows.
Программа типа СОМ
Традиционно первая программа для освоения нового языка программирования - программа, выводящая на экран текст "Hello world!". He будет исключением и эта книга, так как такая программа всегда была удобной отправной точкой для дальнейшего освоения языка.
Итак, наберите в любом текстовом редакторе, который может записывать файлы как обычный текст (например: EDIT.COM в DOS, встроенный редактор в Norton Commander или аналогичной программе, NOTEPAD в Windows), следующий текст:
; hello-l.asm ; Выводит на экран сообщение "Hello World!" и завершается .model tiny ; модель памяти, используемая для СОМ .code ; начало сегмента кода org 100h ; начальное значение счетчика - 100h start: mov ah,9 ; номер функции DOS - в АН mov dx,offset message ; адрес строки - в DX int 21h ; вызов системной функции DOS ret ; завершение СОМ-программы message db "Hello World!",0Dh,0Ah,'$' ; строка для вывода end start ; конец программы
и сохраните его как файл hello-l.asm. Можно также использовать готовый файл с этим именем. (Все программы, использующиеся в этой книге в качестве примеров, вы можете найти в Internet: .) Чтобы превратить программу в исполнимый файл, сначала надо вызвать ассемблер, для того чтобы скомпилировать ее в объектный файл с именем hello-1.obj, набрав в командной строке следующую команду:
Для TASM:
tasm hello-1.asm
Для MASM:
ml /c hello-1.asm
Для WASM:
wasm hello-1.asm
С ассемблерными программами также можно работать из интегрированных сред разработки, как обычно работают с языками высокого уровня, но в них обычно удобнее создавать процедуры на ассемблере, вызываемые из программ на языке, для которого предназначена среда, а создание полноценных программ на ассемблере требует некоторой перенастройки.
Формат объектных файлов, используемых всеми тремя рассматриваемыми ассемблерами по умолчанию (OMF-формат), совпадает, так что можно пользоваться ассемблером из одного пакета и компоновщиком из другого.
Для TLINK:
tasm /t /x hello-1.obj
Для MASM (команда link должна вызывать 16-битную версию LINK.EXE):
link hello-1.obj,,NUL,,, exe2bin hello-1.exe hello-1.com
Для WASM:
wlink file hello-1.obj form DOS COM
Теперь получился файл HELLO-1. COM размером 23 байта. Если его выполнить, на экране появится строка "Hello World!" и программа завершится.
Рассмотрим исходный текст программы, чтобы понять, как она работает.
Первая строка определяет модель памяти TINY, в которой сегменты кода, данных и стека объединены. Эта модель предназначена для создания файлов типа СОМ.
Директива .CODE начинает сегмент кода, который в нащем случае также должен содержать и данные.
ORG 100h устанавливает значение программного счетчика в 100h, так как при загрузке СОМ-файла в память DOS занимает первые 256 байт (100h) блоком данных PSP и располагает код программы только после этого блока. Все программы, которые компилируются в файлы типа СОМ, должны начинаться с этой директивы.
Метка START располагается перед первой командой в программе и будет использоваться в директиве END, чтобы указать, с какой команды начинается программа.
Команда MOV АН,9 помещает число 9 в регистр АН. Это - номер функции DOS "вывод строки".
Команда MOV DX,OFFSET MESSAGE помещает в регистр DX смешение метки MESSAGE относительно начала сегмента данных, который в нашем случае совпадает с сегментом кода.
Команда INT 21h вызывает системную функцию DOS. Эта команда - основное средство взаимодействия программ с операционной системой. В нашем примере вызывается функция DOS номер 9 - вывести строку на экран. Эта функция выводит строку от начала, адрес которого задается в регистрах DS:DX, до первого встреченного символа $. При загрузке СОМ-файла регистр DS автоматически загружается сегментным адресом программы, а регистр DX был загружен предыдущей командой.
Команда RET используется обычно для возвращения из процедуры. DOS вызывает СОМ-программы так, что команда RET корректно завершает программу.
 |
|
DOS при вызове СОМ-файла помещает в стек сегментный адрес программы и ноль, так что RET передает управление на нулевой адрес текущего сегмента, то есть на первый байт PSP. Там находится код команды INT 20h, которая и используется для возвращения управления в DOS. Можно сразу заканчивать программу командой INT 20h, хотя это длиннее на 1 байт. |
|
Следующая строка программы HELLO-1.ASM определяет строку данных, содержащую текст "Hello World!", управляющий символ ASCII "возврат каретки" с кодом 0Dh, управляющий символ ASCII "перевод строки" с кодом 0Ah и символ "$", завершающий строку. Эти два управляющих символа переводят курсор на первую позицию следующей строки точно так же, как в строках на языке С действует последовательность "\n".
И наконец, директива END завершает программу, одновременно указывая, с какой метки должно начинаться выполнение программы.
Работа с файлами
Возможно, основная функция DOS как операционной системы - организация доступа к дискам как к набору файлов и каталогов. DOS поддерживает только один тип файловой системы - FAT и, начиная с версии 7.0 (Windows 95), его модификацию VFAT с длинными именами файлов. Первоначальный набор функций для работы с файлами, предложенный в MS-DOS 1.0, оказался очень неудобным: каждый открытый файл описывался 37-байтной структурой FCB (блок управления файлом), адрес которой требовался для всех файловых операций, а передача данных осуществлялась через структуру данных DTA (область передачи данных). Уже в MS-DOS 2.0, вместе с усовершенствованием FAT (например, появлением вложенных каталогов), появился набор UNIX-подобных функций работы с файлами, использующих для описания файла всего одно 16-битное число, идентификатор файла или устройства. Все остальные функции работы с файлами используют затем только это число. Первые пять идентификаторов инициализируются системой следующим образом:
0: STDIN - стандартное устройство ввода (обычно клавиатура),
1: STDOUT - стандартное устройство вывода (обычно экран),
2: STDERR - устройство вывода сообщений об ошибках (всегда экран),
3: AUX - последовательный порт (обычно СОМ1),
4: PRN - параллельный порт (обычно LPT1),
так что функции чтения/записи (а также сброс буферов на диск) файлов можно точно так же применять и к устройствам.
Работа с мышью
Все общение с мышью в DOS выполняется через прерывание 33h, обработчик которого устанавливает драйвер мыши, загружаемый обычно при запуске системы. Современные драйверы поддерживают около 60 функций, позволяющих настраивать разрешение мыши, профили ускорений, виртуальные координаты, настраивать дополнительные обработчики событий и т.п. Большинство этих функций требуются редко, сейчас рассмотрим основные:
INT 33h, AX = 0 - Инициализация мыши
| Ввод: |
AX = 0000h |
| Вывод: |
АХ = 0000h, если мышь или драйвер мыши не установлены АХ = FFFFh, если драйвер и мышь установлены ВХ = число кнопок:0002 или FFFF - две 0003 - три 0000 - другое количество |
Выполняется аппаратный и программный сброс мыши и драйвера.
INT 33h, AX = 1 - Показать курсор
INT 33h, AX = 2 - Спрятать курсор
Драйвер мыши поддерживает внутренний счетчик, управляющий видимостью курсора мыши. Функция 2 уменьшает значение счетчика на единицу, а функция 1 увеличивает его, но только до значения 0. Если значение счетчика - отрицательное число, он спрятан, если ноль - показан. Это позволяет процедурам, использующим прямой вывод в видеопамять, вызывать функцию 2 в самом начале и 1 - в самом конце, не заботясь о том, в каком состоянии был курсор мыши у вызвавшей эту процедуру программы.
INT 33h, AX = 3 - Определить состояние мыши
| Ввод: |
AX = 0003h |
| Вывод: |
ВХ = состояние кнопок:
бит 0 - нажата левая кнопка
бит 1 - нажата правая кнопка
бит 2 - нажата средняя кнопкаСХ = Х-координата DX = Y-координата |
Возвращаемые координаты совпадают с пиксельными координатами соответствующей точки на экране в большинстве графических режимов, кроме 04, 05, 0Dh, 13h, в которых Х-координату мыши нужно разделить на 2, чтобы получить номер столбца соответствующей точки на экране. В текстовых режимах обе координаты надо разделить на 8, чтобы получить номер строки и столбца соответственно.
В большинстве случаев эта функция не используется в программах, так как для того, чтобы реагировать на нажатие кнопки или перемещение мыши в заданную область, требуется вызывать это прерывание постоянно, что приводит к трате процессорного времени. Функции 5 (определить положение курсора при последнем нажатии кнопки), 6 (определить положение курсора при последнем отпускании кнопки) и 0Bh (определить расстояние, пройденное мышью) могут помочь оптимизировать работу программы, самостоятельно следящей за всеми передвижениями мыши, но гораздо эффективнее указать драйверу самому следить за ее передвижениями (чем он, собственно, и занимается постоянно) и передавать управление в программу, как только выполнится заранее определенное условие, например пользователь нажмет на левую кнопку мыши. Такой сервис обеспечивает функция 0Ch - установить обработчик событий.
INT 33h, AX = 0Ch - Установить обработчик событий
| Ввод: |
AX = 000Ch ES:DX = адрес обработчика СХ = условие вызова
бит 0 - любое перемещение мыши
бит 1 - нажатие левой кнопки
бит 2 - отпускание левой кнопки
бит 3 - нажатие правой кнопки
бит 4 - отпускание правой кнопки
бит 5 - нажатие средней кнопки
бит 6 - отпускание средней кнопки
СХ = 0000h - отменить обработчик |
Обработчик событий должен быть оформлен, как дальняя процедура (то есть завершаться командой RETF). На входе в процедуру обработчика АХ содержит условие вызова, ВХ - состояние кнопок, СХ, DX - X- и Y-координаты курсора, SI, DI - счетчики последнего перемещения по горизонтали и вертикали (единицы измерения для этих счетчиков - мики, 1/200 дюйма), DS - сегмент данных драйвера мыши. Перед завершением программы установленный обработчик событий должен быть обязательно удален (вызов функции 0Ch с СХ = 0), так как иначе при первом же выполнении условия управление будет передано по адресу в памяти, с которого начинался обработчик.
Функция 0Ch используется так часто, что у нее появилось несколько модификаций - функция 14h, позволяющая установить одновременно три обработчика с разными условиями, и функция 18h, также позволяющая установить три обработчика и включающая в условие вызова состояние клавиш Shift, Ctrl и Alt. Воспользуемся обычной функцией 0Ch, чтобы написать простую программу для рисования.
; mousedr.asm ; Рисует на экране прямые линии с концами в позициях, указываемых мышью ; .model tiny .code org 100h ; СОМ-файл .186 ; для команды shr cx,3 start: mov ax,12h int 10h ; видеорежим 640x480 mov ax,0 ; инициализировать мышь int 33h mov ax,1 ; показать курсор мыши int 33h mov ax,000Ch ; установить обработчик событий мыши mov cx,0002h ; событие - нажатие левой кнопки mov dx,offset handler ; ES:DX - адрес обработчика int 33h mov ah,0 ; ожидание нажатия любой клавиши int 16h mov ax,000Ch mov cx,0000h ; удалить обработчик событий мыши int 33h mov ax,3 ; текстовый режим int 10h ret ; конец программы
; Обработчик событий мыши: при первом нажатии выводит точку на экран, ; при каждом дальнейшем вызове проводит прямую линию от предыдущей ; точки к текущей
handler: push 0A000h pop es ; ES - начало видеопамяти push cs pop ds ; DS - сегмент кода и данных этой программы push сх ; СХ (Х-координата) и push dx ; DX (Y-координата) потребуются в конце
mov ax, 2 ; спрятать курсор мыши перед выводом на экран int 33h cmp word ptr previous_X,-1 ; если это первый вызов, je first_point ; только вывести точку,
call line_bresenham ; иначе - провести прямую exit_handler: pop dx ; восстановить СХ и DX pop сх mov previous_X,cx ; и запомнить их как предыдущие mov previous_Y,dx ; координаты
mov ax,1 ; показать курсор мыши int 33h retf ; выход из обработчика - команда RETF
first_point: call putpixel1b ; вывод одной точки (при первом вызове) jmp short exit_handler
; Процедура рисования прямой линии с использованием алгоритма Брезенхама ; Ввод: СХ, DX - X, Y конечной точки ; previous_X,previous_Y - X, Y начальной точки
line_bresenham: mov ax, сх sub ax,previous_X ; AX = длина проекции прямой на ось X jns dx_pos ; если АХ отрицательный - neg ax ; сменить его знак, причем mov word ptr X_increment,1 ; координата X при выводе jmp short dx_neg ; прямой будет расти, dx_pos: mov word ptr X_increment,-1 ; а иначе - уменьшаться
dx_neg: mov bx,dx sub bx,previous_Y ; BX = длина проекции прямой на ось Y jns dy_pos ; если ВХ отрицательный - neg bx ; сменить его знак, причем mov word ptr Y_increment,1 ; координата Y при выводе jmp short dy_neg ; прямой будет расти, dy_pos: mov word ptr Y_increment,-1 ; а иначе - уменьшаться
dy_neg: shl ax,1 ; удвоить значения проекций, shl bx,1 ; чтобы избежать работы с полуцелыми числами
call putpixel1b ; вывести первую точку (прямая рисуется от ; CX,DX к previous_X,previous_Y) cmp ax,bx ; если проекция на ось X больше, чем на Y: jna dx_le_dy mov di,ax ; DI будет указывать, в какую сторону мы shr di,1 ; отклонились от идеальной прямой neg di ; оптимальное начальное значение DI: add di,bx ; DI = 2 * dy - dx cycle: cmp ex,word ptr previous_X ; основной цикл выполняется, je exit_bres ; пока Х не станет равно previous_X cmp di,0 ; если DI > 0, jl fractlt0 add dx,word ptr Y_increment ; перейти к следующему Y sub di,ax ; и уменьшить DI на 2 * dx fractlt0: add cx,word ptr X_increment ; следующий Х (на каждом шаге) add di,bx ; увеличить DI на 2 * dy call putpixel1b ; вывести точку jmp short cycle ; продолжить цикл dx_le_dy: ; если проекция на ось Y больше, чем на X mov di,bx shr di,1 neg di ; оптимальное начальное значение DI: add di,ax ; DI = 2 * dx - dy cycle2: cmp dx,word ptr previous_Y ; основной цикл выполняется, je exit_bres ; пока Y не станет равным previous_Y, cmp di,0 ; если DI > 0, jl fractlt02 add cx,word ptr X_increment ; перейти к следующему X sub di,bx ; и уменьшить DI на 2 * dy fractlt02: add dx,word ptr Y_increment ; следующий Y (на каждом шаге) add di,ax ; увеличить DI на 2 * dy call putpixel1b ; вывести точку jmp short cycle2 ; продолжить цикл exit_bres: ret ; конец процедуры
; Процедура вывода точки на экран в режиме, использующем один бит для ; хранения одного пикселя. ; DХ = строка, СХ = столбец ; Все регистры сохраняются
putpixel1b: pusha ; сохранить регистры xor bx,bx mov ax,dx ; AX = номер строки imul ах,ах,80 ; АХ = номер строки * число байт в строке push cx shr сх,3 ; СХ = номер байта в строке add ах,сх ; АХ = номер байта в видеопамяти mov di,ax ; поместить его в SI и DI для команд mov si,di ; строковой обработки
pop cx ; СХ снова содержит номер столбца mov bx,0080h and cx,07h ; последние три бита СХ = ; остаток от деления на 8 = номер бита в байте, считая справа налево shr bx,cl ; теперь в BL установлен в 1 нужный бит lods es:byte ptr some_label ; AL = байт из видеопамяти or ax,bx ; установить выводимый бит в 1, ; чтобы стереть пиксель с экрана, эту команду OR можно заменить на ; not bx ; and ax,bx ; или лучше инициализировать ВХ не числом 0080h, а числом FF7Fh и использовать ; только and stosb ; и вернуть байт на место рора ; восстановить регистры ret ; конец
previous_X dw -1 ; предыдущая Х-координата previous_Y dw -1 ; предыдущая Y-координата Y_increment dw -1 ; направление изменения Y X_increment dw -1 ; направление изменения X some_label: ; метка, используемая для переопределения ; сегмента-источника для lods с DS на ES end start
Алгоритм Брезенхама, использованный в этой программе, является самым распространенным алгоритмом рисования прямой. Существуют, конечно, и более эффективные алгоритмы, например алгоритм Цаолинь By, работающий на принципе конечного автомата, но алгоритм Брезенхама стал стандартом де-факто.
 |
|
Приведенную реализацию этого алгоритма можно значительно ускорить, использовав самомодифицирующийся код, то есть после проверки на направление прямой в начале алгоритма вписать прямо в дальнейший текст программы команды INС СХ, DEC CX, INС DX и DEC DX вместо команд сложения этих регистров с переменными X_increment и Y_increment. Самомодифицирующийся код часто применяется при программировании для DOS, но во многих многозадачных системах текст программы загружается в область памяти, защищенную от записи, так что в последнее время область применения этого приема становится ограниченной. |
|
Работа с SVGA-режимами
В режиме VGA 320x200 с 256 цветами для отображения видеопамяти на основное адресное пространство используется 64 000 байт, располагающихся с адреса A000h:0000h. Дальнейшее увеличение разрешения или числа цветов приводит к тому, что объем видеопамяти превышает максимальные границы сегмента в реальном режиме (65 535 байт), а затем и размер участка адресного пространства, отводимого для видеопамяти (160 Кб, от A000h:0000h до B800h:FFFFh. С адреса C800h:0000h начинается область ROM BIOS). Чтобы вывести изображение, используются два механизма - переключение банков видеопамяти для реального режима и LFB (линейный кадровый буфер) для защищенного.
Во втором случае вся видеопамять отображается на непрерывный кусок адресного пространства, но начинающегося не с адреса 0A0000h, а с какого-нибудь другого адреса, так чтобы весь образ видеопамяти, который может занимать несколько мегабайтов, отобразился в один непрерывный массив. В защищенном режиме максимальный размер сегмента составляет 4 гигабайта, поэтому никаких сложностей с адресацией этого буфера не возникает. Буфер LFB можно использовать, только если видеоадаптер поддерживает спецификацию VBE 2.0 (см. пример в главе 6.4).
В реальном режиме вывод на экран осуществляется по-прежнему копированием данных в 64-килобайтный сегмент, обычно начинающийся с адреса A000h:0000h, но эта область памяти соответствует только части экрана. Чтобы вывести изображение в другую часть экрана, требуется вызвать функцию перемещения окна (или, что то же самое, переключения банка видеопамяти), изменяющую область видеопамяти, которой соответствует сегмент A000h. Например, в режиме 640x480 с 256 цветами требуется 307 200 байт для хранения всего видеоизображения. Заполнение сегмента A000h:0000h – A000h:FFFFh приводит к закраске приблизительно 1/5 экрана, перемещение окна А на позицию 1 (или переключение на банк 1) и повторное заполнение этой же области приводит к закраске следующей 1/5 экрана, и т.д. Перемещение окна осуществляется подфункцией 05 видеофункции 4Fh или передачей управления прямо на процедуру, адрес которой можно получить, вызвав подфункцию 01, как будет показано ниже. Некоторые видеорежимы позволяют использовать сразу два таких 64-килобайтных окна, окно А и окно В, так что можно записать 128 Кб данных, не вызывая прерывания.
Стандартные графические режимы SVGA могут быть 4-, 8-, 15-, 16-, 24- и 32-битными.
4-битные режимы (16 цветов):
VGA
012h: 640x480 (64 Кб)
VESA VBE 1.0
102h: 800x600 (256 Кб)
104h: 1024x768 (192 Кб)
106h: 1280x1024 (768 Кб)
Каждый пиксель описывается одним битом, для вывода цветного изображения требуется программирование видеоадаптера на уровне портов ввода-вывода (глава 5.10.4).
8-битные режимы (256 цветов):
VGA
013h: 320x200 (64 Кб)
VBE 1.0
100h: 640x400 (256 Кб)
101h: 640x480 (320 Кб)
103h: 800x600 (512 Кб)
105h: 1024x768 (768 Кб)
107h: 1280x1024 (1,3 Мб)
VBE 2.0
120h: 1600x1200 (1,9 Мб)
Каждый пиксель описывается ровно одним байтом. Значение байта - нoмер цвета из палитры, значения цветов которой можно изменять, например вызывая подфункцию 09 видеофункции 4Fh.
15-битные режимы (32 К цветов):
VBE 1.2
10Dh: 320x200 (128 Кб)
110h: 640x480 (768 Кб)
113h: 800x600 (1 Мб)
116h: 1024x768 (1,5 Мб)
119h: 1280x1024 (2,5 Мб)
VBE 2.0
121h: 1600x1200 (3,8 Мб)
Каждый пиксель описывается ровно одним словом (16 бит), в котором биты 0 – 4 содержат значение синей компоненты цвета, биты 5 – 9 - зеленой, а биты 10 – 14 - красной. Бит 15 не используется.
16-битные режимы (64 К цветов):
VBE 1.2
10Eh: 320x200 (128 Кб)
111h: 640x480 (768 Кб)
114h: 800x600 (1 Мб)
117h: 1024x768 (1,5 Мб)
11Ah: 1280x1024 (2,5 Мб)
VBE 2.0
121h: 1600x1200 (3,8 Мб)
Так же как и в 15-битных режимах, каждый пиксель описывается ровно одним словом. Обычно биты 0 – 4 (5 бит) содержат значение синей компоненты, биты 5 – 10 (6 бит) - зеленой, а биты 11 – 15 (5 бит) - красной. В нестандартных режимах число бит, отводимое для каждого цвета, может отличаться, так что при их использовании следует вызвать подфункцию 01 видеофункции 4Fh и получить информацию о видеорежиме, включающую битовые маски и битовые смещения для цветов.
24-битные и 32-битные режимы (16 М цветов):
VBE 1.2
10Fh: 320x200 (192 Кб)
112h: 640x480 (1 Мб)
115h: 800x600 (1,4 Мб)
118h: 1024x768 (2,3 Мб)
11Bh: 1280x1024 (3,7 Мб)
VBE 2.0
122h: 1600x1200 (7,7 Мб)
В режимах с 24-битным и 32- битным цветом каждому пикселю на экране соответствуют три байта и одно двойное слово (4 байта). Если видеорежим использует модель памяти 6 (Direct Color), то младший байт (байт 0) содержит значение синего цвета, байт 1 содержит значение зеленого, байт 2 - значение красного, а байт 3 - в 32-битных режимах резервный и используется либо для выравнивания, либо содержит значение для альфа-канала. Некоторые видеорежимы могут использовать не Direct Color, a YUV (модель памяти 7) - здесь младший байт соответствует насыщенности красного, байт 1 - насыщенности синего, а байт 2 - яркости.
Видеоадаптер может поддерживать и собственные нестандартные видеорежимы. Список их номеров можно получить, вызвав подфункцию 00h, а получить информацию о режиме по его номеру - вызвав подфункцию 01h видеофуикции 4Fh. Более того, для стандартных режимов также следует вызывать подфункцию 01h, чтобы проверить реальную доступность режима (например, режим может быть в списке, но не поддерживаться из-за нехватки памяти). VBE 2.0 разрешает видеоадаптерам не поддерживать никаких стандартных режимов вообще.
INT 10h АН = 4Fh, AL = 00 - Получить общую SVGA-информацию
| Ввод: |
AX = 4F00h ES:DI = адрес буфера (512 байт) |
| Вывод: |
AL = 4Fh, если функция поддерживается АН = 01, если произошла ошибка АН = 00, если данные получены и записаны в буфер |
Буфер для общей SVGA-информации:
| +00h: |
4 байта - будет содержать "VESA" после вызова прерывания, чтобы получить поля, начиная с 14h, здесь надо предварительно записать строку "VBE2" |
| +04h: |
слово - номер версии VBE в двоично-десятичном формате (0102h - для 1.2, 0200h - для 2.0) |
| +06h: |
4 байта - адрес строки-идентификатора производителя |
| +0Ah: |
4 байта - флаги:
бит 0 - АЦП поддерживает 8-битные цветовые компоненты (см. подфункцию 08h)
бит 1 - видеоадаптер несовместим с VGA
бит 2 - АЦП можно программировать только при обратном ходе луча
бит 3 - поддерживается спецификация аппаратного ускорения графики VBE/AF 1.0
бит 4 - требуется вызов EnableDirectAccess перед использованием LFB
бит 5 - поддерживается аппаратный указатель мыши
бит 6 - поддерживается аппаратное отсечение
бит 7 - поддерживается аппаратное копирование блоков
биты 8 – 31 зарезервированы |
| +0Eh: |
4 байта - адрес списка номеров поддерживаемых видеорежимов (массив слов, последнее слово = FFFFh, после которого обычно следует список нестандартных режимов, также заканчивающийся словом FFFFh) |
| +12h: |
слово - объем видеопамяти в 64-килобайтных блоках |
| +14h: |
слово - внутренняя версия данной реализации VBE |
| +16h: |
4 байта - адрес строки с названием производителя |
| +1Ah: |
4 байта - адрес строки с названием видеоадаптера |
| +1Eh: |
4 байта - адрес строки с версией видеоадаптера |
| +22h: |
слово - версия VBE/AF (BCD, то есть 0100h для 1.0) |
| +24h: |
4 байта - адрес списка номеров режимов, поддерживающих аппаратное ускорение (если бит поддержки VBE/AF установлен в 1) |
| +28h: |
216 байт - зарезервировано VESA |
| +100h: |
256 байт - зарезервировано для внутренних данных VBE. Так, например, в эту область копируются строки с названиями производителя, видеоадаптера, версии и т.д. |
<
/p>
INT 10h АН = 4Fh, AL = 01 - Получить информацию о режиме
| Ввод: |
AX = 4F01h СХ = номер SVGA-режима ( бит 14 соответствует использованию LFB, бит 13 - аппаратному ускорению) ES:DI = адрес буфера для информации о режиме (256 байт) |
| Вывод: |
AL = 4Fh, если функция поддерживается АН = 01, если произошла ошибка АН = 00, если данные получены и записаны в буфер |
Буфер для информации о SVGA-режиме:
| +00h: |
слово - атрибуты режима:
бит 0 - режим присутствует
бит 1 - дополнительная информация (смещения 12h – 1Eh) присутствует (для VBE 2.0 эта информация обязательна и бит всегда установлен)
бит 2 - поддерживается вывод текста на экран средствами BIOS
бит 3 - режим цветной
бит 4 - режим графический
бит 5 - режим несовместим с VGA
бит 6 - переключение банков не поддерживается
бит 7 - LFB не поддерживается
бит 8 - не определен
бит 9 - (для VBE/AF) приложения должны вызвать EnableDirectAccess, прежде чем переключать банки |
| +02h: |
байт - атрибуты окна А:
бит 1 - окно существует
бит 2 - чтение из окна разрешено
бит 3 - запись в окно разрешена |
| +03h: |
байт - атрибуты окна В |
| +04h: |
слово - гранулярность окна - число килобайтов, которому всегда кратен адрес начала окна в видеопамяти (обычно 64) |
| +06h: |
слово - размер окна в килобайтах (обычно 64) |
| +08h: |
слово - сегментный адрес окна А (обычно A000h) |
| +0Ah: |
слово - сегментный адрес окна В |
| +0Ch: |
4 байта - адрес процедуры перемещения окна (аналог подфункции 05h, но выполняется быстрее) |
| +10h: |
слово - число целых байт в логической строке |
| +12h: |
слово - ширина в пикселях (для графики) или символах (для текста) |
| +14h: |
слово - высота в пикселях (для графики) или символах (для текста) |
| +16h: |
байт - высота символов в пикселях |
| +17h: |
байт - ширина символов в пикселях |
| +18h: |
байт - число плоскостей памяти (4 - для 16-цветных режимов, 1 - для обычных, число переключений банков, требуемое для доступа ко всем битам (4 или 8), - для модели памяти 5) |
| +19h: |
байт - число бит на пиксель |
| +1Ah: |
байт - число банков для режимов, в которых строки группируются в банки (2 - для CGA, 4 - для HGC) |
| +1Bh: |
байт - модель памяти:
00h - текст
01h - CGA-графика
02h - HGC-графика
03h - EGA-графика (16 цветов)
04h - VGA-графика (256 цветов в одной плоскости)
05h - Режим X (256 цветов в разных плоскостях)
06h - RGB (15-битные и выше)
07h - YUV
08h – 0Fh - зарезервированы VESA
10h – FFh - нестандартные модели |
| +1Ch: |
байт - размер банка в килобайтах (8 - для CGA и HGC, 0 - для остальных) |
| +1Dh: |
байт - число видеостраниц |
| +1Eh: |
байт - зарезервирован |
| +1Fh: |
байт - битовая маска красной компоненты |
| +20h: |
байт - первый бит красной компоненты |
| +21h: |
байт - битовая маска зеленой компоненты |
| +22h: |
байт - первый бит зеленой компоненты |
| +23h: |
байт - битовая маска синей компоненты |
| +24h: |
байт - первый бит синей компоненты |
| +25h: |
байт - битовая маска зарезервированной компоненты |
| +26h: |
байт - первый бит зарезервированной компоненты |
| +27h: |
байт - дополнительные флаги:
бит 0: -поддерживается перепрограммирование цветов (подфункция 09h)
бит 1 - приложение может использовать биты в зарезервированной компоненте |
| +28h: |
4 байта - физический адрес начала LFB |
| +2Ch: |
4 байта - смещение от начала LFB, указывающее на первый байт после конца участка памяти, отображающейся на экране |
| +30h: |
слово - размер памяти в LFB, не отображающейся на экране, в килобайтах |
| +32h: |
206 байт - зарезервировано |
<
/p>
INT 10h АН = 4FH, AL = 02 - Установить режим
| Ввод: |
AX=4F02h ВХ = номер режима:
биты 0 – 6 - собственно номер режима
бит 7 - видеопамять не очищается при установке режима, если все следующие биты - нули
бит 8 - стандартный VBE SVGA-режим
бит 9 - нестандартный SVGA-режим
биты 10 – 12 - зарезервированы
бит 13 - режим использует аппаратное ускорение
бит 14 - режим использует LFB
бит 15 - видеопамять не очищается при установке режима
Кроме того, специальный номер режима 81FFh соответствует доступу ко всей видеопамяти и может использоваться для сохранения ее содержимого. |
| Вывод: |
AL = 4Fh, если функция поддерживается АН = 00, если режим установлен АН = 01 или 02, если произошла ошибка |
INT 10h АН = 4Fh, AL = 03 - Узнать номер текущего видеорежима
| Ввод: |
АХ = 4F03h |
| Вывод: |
AL = 4Fh, если функция поддерживается ВХ = номер режима |
INT 10h АН = 4Fh AL = 05 - Перемещение окна (переключение банка видеопамяти)
| Ввод: |
АХ = 4F03h ВН = 00 - установить окно ВН = 01 - считать окно BL = 00 - окно А BL = 01 - окно В DX = адрес окна в видеопамяти в единицах гранулярности (номер банка), если ВН = 0 |
| Вывод: |
AL = 4Fh, если функция поддерживается DX = адрес окна в единицах гранулярности (номер банка), если ВН = 1 АН = 03, если функция была вызвана в режиме, использующем LFB |
Всегда предпочтительнее переключать банки прямым дальним вызовом процедуры, адрес которой возвращается подфункцией 01h в блоке информации о видеорежиме. Все параметры передаются в процедуру точно так же, как и в подфункцию 05h, но содержимое регистров АХ и DX по возвращении не определено.
INT 10h АН = 4Fh AL = 07 - Установка начала изображения
| Ввод: |
АХ = 4F07h ВН = 00 BL = 00 - считать начало изображения BL = 80h - установить начало изображения (в VBE 2.0 автоматически выполняется при следующем обратном ходе луча) СХ = первый изображаемый пиксель в строке (для BL = 80h) DX = первая изображаемая строка (для BL = 80h) |
| Вывод: |
AL = 4Fh, если функция поддерживается АН = 01, если произошла ошибка АН = 00, если функция выполнилась успешно ВН = 00 (для BL = 00) СХ = первый изображаемый пиксель в строке (для BL = 00) DX = первая изображаемая строка (для BL = 00) |
<
/p>
С помощью этой функции можно выполнять как плавный сдвиг экрана, перемещая начало изображения на одну строку за один раз, так и быстро отображать на экране два разных изображения, изменяя одно, пока на экране показано другое, так что создается эффект плавной анимации.
: scrolls.asm ; Изображает в разрешении 1024x768x64К окрашенный конус, который можно ; плавно перемещать по экрану стрелками вверх и вниз. ; .model tiny .code .386 ; используется команда shrd org 100h ; СОМ-файл start: mov ax,4F01h ; получить информацию о видеорежиме mov cx,116h ; 1024x768x64К mov di,offset vbe_mode_buffer int 10h ; здесь для простоты опущена проверка наличия режима mov ax,4F02h ; установить режим mov bx,116h int 10h push word ptr [vbe_mode_buffer+8] pop es ; поместить в ES адрес начала видеопамяти ; (обычно A000h) cld
; вывод конуса на экран
mov cx,-1 ; начальное значение цвета (белый) mov si,100 ; начальный радиус mov bx,300 ; номер столбца mov ax,200 ; номер строки main_loop: inc si ; увеличить радиус круга на 1 inc ax ; увеличить номер строки inc bx ; увеличить номер столбца call fast_circle ; нарисовать круг sub cx,0000100000100001b ; изменить цвет cmp si,350 ; если еще не нарисовано 250 кругов, jb main_loop ; продолжить, xor сх,сх ; иначе: выбрать черный цвет, call fast_circle ; нарисовать последний круг
; плавное перемещение изображения по экрану с помощью функции 4F07
xor bx,bx ; ВХ = 0 - установить начало экрана xor dx,dx ; номер строки = 0 ; номер столбца в СХ уже ноль main_loop_2: mov ax,4F07h int 10h ; переместить начало экрана mov ah,7 ; считать нажатую клавишу с ожиданием, без эха int 21h ; и без проверки на Ctrl-Break, test al,al ; если это обычная клавиша - jnz exit_loop_2 ; завершить программу, int 21h ; иначе: получить расширенный ASCII-код, cmp al,50h ; если это стрелка вниз je key_down cmp al,48h ; или вверх - вызвать обработчик, je key_up exit_loop_2: ; иначе - завершить программу mov ах,З ; текстовый режим int 10h ret ; завершить СОМ-программу
key_down: ; обработчик нажатия стрелки вниз dec dx ; уменьшить номер строки начала экрана, jns main_loop_2 ; если знак не изменился - продолжить цикл, ; иначе (если номер был 0, а стал -1) - ; увеличить номер строки key_up: ; обработчик нажатия стрелки вверх inc dx ; увеличить номер строки начала экрана jmp short main_loop_2
; Процедура вывода точки на экран в 16-битном видеорежиме ; Ввод: DX = номер строки, ВХ = номер столбца, ES = А000, СХ = цвет ; модифицирует АХ
putpixel16b: push dx push di хоr di,di shrd di,dx,6 ; DI = номер строки * 1024 mod 65 536 shr dx,5 ; DX = номер строки / 1024 * 2 inc dx cmp dx,current_bank ; если номер банка для выводимой точки jne bank_switch ; отличается от текущего - переключить банки switched: add di,bx ; добавить к DI номер столбца mov ax,cx ; цвет в АХ shl di,1 ; DI = DI * 2, так как адресация идет в словах stosw ; вывести точку на экран pop di ; восстановить регистры pop dx ret bank_switch: ; переключение банка push bx xor bx,bx ; BX = 0 -> Установить начало экрана mov current_bank,dx ; сохранить новый номер текущего банка call dword ptr [vbe_mode_buffer+0Ch] ; переключить ; банк pop bx jmp short switched
; Алгоритм рисования круга, используя только сложение, вычитание и сдвиги ; (упрощенный алгоритм промежуточной точки). ; Ввод: SI = радиус, СХ = цвет, АХ = номер столбца центра круга, ; ВХ = номер строки центра круга модифицирует DI, DX
fast_circle: push si push ax push bx xor di,di ; DI - относительная Х-координата текущей точки dec di ; (SI - относительная Y-координата, начальное mov ax,1 ; значение - радиус) sub ax,si ; AX - наклон (начальное значение 1-Радиус) circle_loop: inc di ; следующий X (начальное значение - 0) cmp di,si ; цикл продолжается, пока X < Y ja exit_main_loop
pop bx ; BX = номер строки центра круга pop dx ; DX = номер столбца центра круга push dx push bx
push ax ; сохранить АХ (putpixel16b его изменяет) add bx,di ; вывод восьми точек на окружности: add dx,si call putpixel16b ; центр_Х + X, центр_Y + Y sub dx,si sub dx,si call putpixel16b ; центр_X + X, центр_Y - Y sub bx,di sub bx,di call putpixel16b ; центр_Х - X, центр_Y - Y add dx,si add dx,si call putpixel16b ; центр_Х - X, центр_Y + Y sub dx,si add dx,di add bx,di add bx,si call putpixel16b ; центр_Х + Y, центр_Y + X sub dx,di sub dx,di call putpixel16b ; центр_Х + Y, центр_Y - X sub bx,si sub bx,si call putpixel16b ; центр_Х - Y, центр_Y - X add dx,di add dx,di call putpixel16b ; центр_Х - Y, центр_Y + X pop ax
test ax,ax ; если наклон положительный js slop_negative mov dx,di sub dx,si shl dx,1 inc dx add ax,dx ; наклон = наклон + 2(Х - Y) + 1 dec si ; Y = Y - 1 jmp circle_loop slop_negative: ; если наклон отрицательный mov dx,di shl dx,1 inc dx add ax,dx ; наклон = наклон + 2X + 1 jmp circle_loop ; и Y не изменяется exit_main_loop: pop bx pop ax pop si ret
current_bank dw 0 ; номер текущего банка vbe_mode_buffer: ; начало буфера данных о видеорежиме end start
В этом примере для наглядности отсутствуют необходимые проверки на поддержку VBE (все прерывания должны возвращать 4Fh в AL), на поддержку видеорежима (атрибут видеорежима в первом байте буфера, заполняемого подфункцией 02) или на объем видеопамяти (должно быть как минимум 2 Мб) и на другие ошибки (все прерывания должны возвращать 0 в АН).
Для вывода точки на экран используется выражение типа
номер_банка = номер_строки * байт_в_строке / байт_в_банке смещение = номер_строки * байт_в_строке MOD байт_в_банке
Но так как и число байт в строке, и число байт в банке являются степенями двойки, умножение, деление и вычисление остатка от деления можно заменить более быстрыми операциями сдвига, как это сделано в процедуре putpixel16b.
Переключение банков всегда отнимает значительное время, так что по возможности программированием для SVGA-режимов лучше всего заниматься в 32-битном режиме с линейным кадровым буфером, например используя DOS-расширители, как показано в главе 6.4.
Работа с VGA-режимами
Функция 00 прерывания BIOS 10h позволяет переключаться не только в текстовые режимы, использовавшиеся в предыдущих главах, но и в некоторые графические. Эти видеорежимы стандартны и поддерживаются всеми видеоадаптерами (начиная с VGA), см. табл. 19.
Таблица 19. Основные графические режимы VGA
| Номер режима |
Разрешение |
Число цветов |
| 11h |
640x480 |
2 |
| 12h |
640x480 |
16 |
| 13h |
320x200 |
256 |
Существуют еще несколько видеорежимов, использовавшихся более старыми видеоадаптерами CGA и EGA (с номерами от 4 до 10h); их список приведен в приложении 2.
BIOS также предоставляет видеофункции чтения и записи точки на экране в графических режимах, но эти функции настолько медленно исполняются, что никогда не используются в реальных программах.
INТ 10h АН = 0Ch - Вывести точку на экран
| Ввод: |
АН = 0Ch ВН = номер видеостраницы (игнорируется для режима 13h, поддерживающего только одну страницу) DX = номер строки СХ = номер столбца AL = номер цвета (для режимов 10h и llh, если старший бит 1, номер цвета точки на экране будет результатом операции "исключающее ИЛИ") |
| Вывод: |
Никакого |
INТ 10h AH = 0Dh - Считать точку с экрана
| Ввод: |
АН = 0Dh ВН = номер видеостраницы (игнорируется для режима 13h, поддерживающего только одну страницу) DX = номер строки СХ = номер столбца |
| Вывод: |
AL = номер цвета |
Попробуем тем не менее воспользоваться средствами BIOS для вывода на экран. Следующая программа переводит экран в графический режим 13h (320x200), заселяет его точками случайным образом, после чего эти точки эволюционируют согласно законам алгоритма "Жизнь": если у точки меньше двух или больше трех соседей, она погибает, а если у пустой позиции есть три соседа, в ней появляется новая точка. Мы будем использовать очень простой, но неоптимальный способ реализации этого алгоритма: сначала для каждой точки вычисляется число соседей, затем каждая точка преобразуется в соответствии с полученным числом соседей, и затем каждая точка выводится на экран.
; lifebios.asm ; Игра "Жизнь" на поле 320x200, использующая вывод на экран средствами BIOS .model small .stack 100h ; явное задание стека - для ЕХЕ-программ .code .186 ; для команд shl al,4 и shr al,4 start: push FAR_BSS ; сегментный адрес буфера в DS pop ds
; заполнение массива ячеек псевдослучайными значениями xor ах,ах int 1Ah ; Функция АН = 0 INT 1Ah: получить текущее ; время DX теперь содержит число секунд, ; прошедших с момента включения компьютера, ; которое используется как начальное значение ; генератора случайных чисел mov di,320*200+1 ; максимальный номер ячейки fill_buffer: imul dx,4E35h ; простой генератор случайных чисел inc dx ; из двух команд mov ax,dx ; текущее случайное число копируется в АХ, shr ax,15 ; от него оставляется только один бит, mov byte ptr [di],al ; и в массив копируется 00, если ячейка ; пуста, и 01, если заселена dec di ; следующая ячейка jnz fill_buffer ; продолжить цикл, если DI не стал равен нулю
mov ах,0013h ; графический режим 320x200, 256 цветов int 10h
; основной цикл
new_cycle:
; Шаг 1: для каждой ячейки вычисляется число соседей ; и записывается в старшие 4 бита этой ячейки
mov di,320*200+1 ; максимальный номер ячейки step_1: mov al,byte ptr [di+1] ; в AL вычисляется сумма add al,byte ptr [di-1] ; значений восьми соседних ячеек, add al,byte ptr [di+319] ; при этом в младших четырех add al,byte ptr [di-319] ; битах накапливается число add al,byte ptr [di+320] ; соседей add al,byte ptr [di-320] add al,byte ptr [di+321] add al,byte ptr [di-321] shl al,4 ; теперь старшие четыре бита AL - число ; соседей текущей ячейки or byte ptr [di],al ; поместить их в старшие четыре бита ; текущей ячейки dec di ; следующая ячейка jnz step_1 ; продолжить цикл, если DI не стал равен нулю
; Шаг 2: изменение состояния ячеек в соответствии с полученными в шаге 1 ; значениями числа соседей
mov di,320*200+1 ; максимальный номер ячейки flip_cycle: mov al,byte ptr [di] ; считать ячейку из массива shr al,4 ; AL = число соседей cmp al,3 ; если число соседей = 3, je birth ; ячейка заселяется, cmp al,2 ; если число соседей = 2, je f_c_continue ; ячейка не изменяется, mov byte ptr [di],0 ; иначе - ячейка погибает jmp short f_c_continue birth: mov byte ptr [di],1 f_c_continue: and byte ptr [di],0Fh ; обнулить число соседей в старших ; битах ячейки dec di ; следующая ячейка jnz flip_cycle ; ; Вывод массива на экран средствами BIOS ; mov si,320*200+1 ; максимальный номер ячейки mov сх,319 ; максимальный номер столбца mov dx,199 ; максимальный номер строки zdisplay: mov al,byte ptr [si] ; цвет точки (00 - черный, 01 - синий) mov ah,0Ch ; номер видеофункции в АН int 10h ; вывести точку на экран dec si ; следующая ячейка dec cx ; следующий номер столбца jns zdisplay ; если столбцы не закончились - продолжить, mov сх,319 ; иначе: снова максимальный номер столбца в СХ dec dx ; и следующий номер строки в DX, jns zdisplay ; если и строки закончились - выход из цикла mov ah,1 ; если не нажата клавиша int 16h jz new_cycle ; следующий шаг жизни
mov ах,0003h ; восстановить текстовый режим int 10h mov ax,4C00h ; и завершить программу int 21h
.fardata? ; сегмент дальних неинициализированных данных db 320*200+1 dup(?) ; содержит массив ячеек end start
Этот пример оформлен как ЕХЕ-программа, так как используется массив, близкий по размерам к размеру сегмента, и если разместить его в одном сегменте с СОМ-программой, стек, растущий от самых старших адресов, может затереть область данных. Наш пример не использует стек, но это делает обработчик прерывания BIOS 10h.
Скорость работы этой программы - в среднем 200 тактов процессора Pentium на точку (измерения выполнены с помощью команды RDTSC, см. главу 10.2), то есть всего 16 поколений в секунду для Pentium-200 (200 миллионов тактов в секунду разделить на 200 тактов на точку и на 320x200 точек). Разумеется, используемый алгоритм крайне нерационален и кажется очевидным, что его оптимизация приведет к значительному выигрышу во времени. Но если измерить скорость выполнения каждого из трех циклов, то окажется, что первый цикл выполняется в среднем за 20,5 такта на точку, второй - за 13, а третий - за 170,5!
Исправить эту ситуацию весьма просто - достаточно отказаться от видеофункций BIOS для работы с графикой и перейти к прямому копированию в видеопамять.
В видеорежиме 13h каждый байт в области памяти, начинающейся с адреса A000h:0000h, соответствует одной точке на экране, а значение, которое может принимать этот байт (0 – 255), соответствует номеру цвета этой точки. (Цвета, которые соответствуют этим номерам, могут быть перепрограммированы с помощью видеофункции 10h BIOS.) В видеорежимах 11h и 12h каждый бит соответствует одной точке на экране, так что простым копированием в видеопамять можно получить только черно-белое изображение (для вывода цветного изображения в режиме 12h необходимо перепрограммировать видеоадаптер, об этом см. в главе 5.10.4).
В нашем примере для хранения информации о каждой ячейке также используется один байт, так что для вывода данных на экран в режиме 13h достаточно выполнить простое копирование. Переименуем программу LIFEBIOS.ASM в LIFEDIR.ASM, заменив цикл вывода на экран от команды
mov si,320*200+1
до команды
jns zdisplay
следующим фрагментом кода:
push 0A000h ; сегментный адрес видеопамяти pop es ; в ES mov cx,320*200 ; максимальный номер точки mov di,cx ; в видеопамяти - 320 * 200 mov si,cx ; а в массиве - inc si ; 320 * 200 + 1 rep movsb ; выполнить копирование в видеопамять
Теперь программа обрабатывает одну точку приблизительно за 61,5 такта процессора Pentium, что дает 51 поколение в секунду на Pentium-200. Кроме того, теперь эту программу можно переписать в виде СОМ-файла, так как и код, и массив, и стек точно умещаются в одном сегменте размером 64 Кб. Такая СОМ-программа (LIFECOM.ASM) займет 143 байта.
Оптимизация программы "Жизнь" - хорошее упражнение для программирования на ассемблере. В 1997 году проводился конкурс на самую короткую и на самую быструю программу, выполняющую в точности то же, что и наш пример, - заполнение экрана случайными точками, их эволюция и выход по нажатию любой клавиши. Самой короткой тогда оказалась программа размером в 72 байта, которая с тех пор была усовершенствована до 64 байт (ее скорость 52 такта на точку), а самая быстрая из 16-битных программ тратит на каждую точку в среднем всего 6 тактов процессора Pentium и имеет размер 689 байт. В ней состояния ячеек описываются отдельными битами массива, а для их обработки используются команды логических операций над целыми словами, так что одна команда обрабатывает сразу 16 точек. Использование 32-битных команд с тем же алгоритмом позволяет ускорить программу до 1,5 такта на точку.
Системный таймер
Начиная с IBM AT, персональные компьютеры содержат два устройства для управления процессами - часы реального времени (RTC) и собственно системный таймер. Часы реального времени получают питание от аккумулятора на материнской плате и работают даже тогда, когда компьютер выключен. Это устройство можно использовать для определения/установки текущих даты и времени, установки будильника с целью выполнения каких-либо действий и для вызова прерывания IRQ8 (INT 4Ah) каждую миллисекунду. Системный таймер используется одновременно для управления контроллером прямого доступа к памяти, для управления динамиком и как генератор импульсов, вызывающий прерывание IRQ0 (INT 8h) 18,2 раза в секунду. Таймер предоставляет богатые возможности для препрограммирования на уровне портов ввода-вывода, но на уровне DOS и BIOS и часы реального времени, и системный таймер используются только как средство определения/установки текущего времени и организации задержек.
Функция DOS 2Ah - Определить дату
| Ввод: |
AX = 2Ah |
| Вывод: |
СХ = год (1980 – 2099) DH = месяц DL = день AL = день недели (0 - воскресенье, 1 - понедельник...) |
Функция DOS 2Ch - Определить время
| Ввод: |
AX = 2Ch |
| Вывод: |
СН = час CL = минута DH = секунда DL = сотая доля секунды |
Эта функция использует системный таймер, так что время изменяется только 18,2 раза в секунду и число в DL увеличивается сразу на 5 или 6.
Функция DOS 2Bh - Установить дату
| Ввод: |
АН = 2Bh СХ = год (1980 – 2099) DH = месяц DL = день |
| Вывод: |
АН = FFh, если введена несуществующая дата, АН = 00h, если дата установлена |
Функция DOS 2Dh - Установить время
| Ввод: |
АН = 2Dh СН = час CL = минута DH = секунда DL = сотая доля секунды |
| Вывод: |
AL = FFh, если введено несуществующее время, AL = 00h, если время установлено |
Функции 2Bh и 2Dh устанавливают одновременно как внутренние часы DOS, которые управляются системным таймером и обновляются 18,2 раза в секунду, так и часы реального времени. BIOS позволяет управлять часами напрямую:
INT 1Ah АН = 04h - Определить дату RTC
| Ввод: |
АН = 04h |
| Вывод: |
CF = 0, если дата прочитана СХ = год ( в формате BCD, то есть 1998h для 1998-го года) DH = месяц (в формате BCD) DL = день (в формате BCD) CF = 1, если часы не работают или попытка чтения пришлась на момент обновления |
INT 1Ah АН = 02h - Определить время RTC
| Ввод: |
АН = 02h |
| Вывод: |
CF = 0, если время прочитано СН = час (в формате BCD) CL = минута (в формате BCD) DH = секунда (в формате BCD) DL = 01h, если действует летнее время, 00h, если нет CF = 1, если часы не работают или попытка чтения пришлась на момент обновления |
INT 1Ah АН = 05h - Установить дату RTC
| Ввод: |
АН = 05h СХ = год (в формате BCD) DH = месяц DL = день |
INT 1Ah АН = 03h - Установить время RTC
| Ввод: |
АН = 03h СН = час (в формате BCD) CL = минута (в формате BCD) DH = секунда (в формате BCD) DL = 01h, если используется летнее время, 0 - если нет |
Кроме того, BIOS позволяет использовать RTC для организации будильников и задержек:
INT 1Ah АН = 06h - Установить будильник
| Ввод: |
АН = 06h СН = час (BCD) CL = минута (BCD) DH = секунда (BCD) |
| Вывод: |
CF = 1, если произошла ошибка (будильник уже установлен или прерывание вызвано в момент обновления часов) CF = 0, если будильник установлен |
Теперь каждые 24 часа, когда время совпадет с заданным, часы реального времени вызовут прерывание IRQ8 (INT 4Ah), которое должна обрабатывать установившая будильник программа. Если при вызове СН = FFh, CL*nbsp;= FFh, a DH = 00h, то будильник начнет срабатывать раз в минуту.
INT 1Ah АН = 07 - Отменить будильник
Эта функция позволяет отменить будильник, например для того, чтобы установить его на другое время.
BIOS отслеживает каждый отсчет системного таймера с помощью своего обработчика прерывания IRQ0 (INT 8h) и увеличивает на 1 значение 32-битного счетчика, который располагается в памяти по адресу 0000h:046Ch, причем при переполнении этого счетчика байт по адресу 0000h:0470h увеличивается на 1.
INT 1Ah АН = 00h - Считать значение счетчика времени
| Ввод: |
АН = 00h |
| Вывод: |
CX:DX = значение счетчика AL = байт переполнения счетчика |
INT 1Ah АН = 01h - Изменить значение счетчика времени
| Ввод: |
АН = 01h CX:DX = значение счетчика |
Программа может считывать значение этого счетчика в цикле (через прерывание или просто командой MOV) и организовывать задержки, например пока счетчик не увеличится на 1. Но так как этот счетчик использует системный таймер, минимальная задержка будет равна приблизительно 55 микросекундам. Частоту таймера можно изменить, программируя его на уровне портов, но BIOS предоставляет для этого специальные функции.
INT 15h АН = 86h - Формирование задержки
| Ввод: |
АН = 86h CX:DX = длительность задержки в микросекундах (миллионных долях секунды!) |
| Вывод: |
AL = маска, записанная обработчиком в регистр управления прерываниями CF = 0, если задержка выполнена CF = 1, если таймер был занят |
Если нужно запустить счетчик времени и продолжить выполнение программы, можно воспользоваться еще одной функцией.
INT 15h АН = 83h - Запуск счетчика времени
| Ввод: |
АН = 83h AL = 0 - запустить счетчик AL = 1 - прервать счетчик CX:DX = длительность задержки в микросекундах ES:BX = адрес байта, старший бит которого по окончании работы счетчика будет установлен в 1 |
| Вывод: |
AL = маска, записанная обработчиком в регистр управления прерываниями CF = 0, если задержка выполнена CF = 1, если таймер был занят |
Минимальный интервал для этих функций на большинстве систем обычно составляет около 1000 микросекунд. Воспользуемся функцией организации задержки для небольшой интерактивной игры:
; worm.asm ; Игра "Питон" (или "Змея", или "Червяк"). Управление осуществляется клавишами ; управления курсором, питон погибает, если он выходит за верхнюю или нижнюю ; границу экрана или самопересекается. .model tiny .code .186 ; для команды push 0A000h org 100h ; СОМ-файл start: mov ax,cs ; текущий сегментный адрес плюс add ax,1000h ; 1000h = следующий сегмент, mov ds,ax ; который будет использоваться ; для адресов головы и хвоста push 0A000h ; 0A000h - сегментный адрес pop es ; видеопамяти (в ES) mov ax,13h ; графический режим 13h int 10h mov di,320*200 mov cx,600h ; заполнить часть видеопамяти, ; остающуюся за пределами rep stosb ; экрана, ненулевыми значениями ; (чтобы питон не смог выйти ; за пределы экрана) xor si,si ; начальный адрес хвоста в DS:SI mov bp,10 ; начальная длина питона - 10 jmp init_food ; создать первую еду main_cycle: ; использование регистров в этой программе: ; АХ - различное ; ВХ - адрес головы, хвоста или еды на экране ; СХ - 0 (старшее слово числа микросекунд для функции задержки) ; DX - не используется (модифицируется процедурой random) ; DS - сегмент данных программы (следующий после сегмента кода) ; ES - видеопамять ; DS:DI - адрес головы ; DS:SI - адрес хвоста ; ВР - добавочная длина (питон растет, пока ВР > 0, ВР уменьшается ; на каждом шаге, пока не станет нулем)
mov dx,20000 ; пауза - 20 000 микросекунд mov ah,86h ; (СХ = 0 после REP STOSB ; и больше не меняется) int 15h ; задержка mov ah,1 ; проверка состояния клавиатуры int 16h jz short no_keypress ; если клавиша не нажата - xor ah,ah ; АН = 0 - считать скан-код int 16h ; нажатой клавиши в АН, cmp ah,48h ; если это стрелка вверх, jne short not_up mov word ptr cs:move_direction,-320 ; изменить ; направление движения на "вверх", not_up: cmp ah,50h ; если это стрелка вниз, jne short not_down mov word ptr cs:move_direction,320 ; изменить ; направление движения на "вниз", not_down: cmp ah,4Bh ; если это стрелка влево, jne short not_left mov word ptr cs:move_direction,-1 ; изменить ; направление движения на "влево", not_left: cmp ah,4Dh ; если это стрелка вправо, jne short no_keypress mov word ptr cs:move_direction,1 ; изменить ; направление движения на "вправо", no_keypress: and bp,bp ; если питон растет (ВР > 0), jnz short advance_head ; пропустить стирание хвоста, lodsw ; иначе: считать адрес хвоста из ; DS:SI в АХ и увеличить SI на 2 xchg bx,ax mov byte ptr es:[bx],0 ; стереть хвост на экране, mov bx,ax inc bp ; увеличить ВР, чтобы следующая ; команда вернула его в 0, advance_head: dec bp ; уменьшить ВР, так как питон ; вырос на 1, если стирание хвоста было пропущено, ; или чтобы вернуть его в 0 - в другом случае add bx,word ptr cs:move_direction ; bx = следующая координата головы mov al,es:[bx] ; проверить содержимое экрана в точке ; с этой координатой, and al,al ; если там ничего нет, jz short move_worm ; передвинуть голову, cmp al,0Dh ; если там еда, je short grow_worm ; увеличить длину питона, mov ax,3 ; иначе - питон умер, int 10h ; перейти в текстовый режим retn ; и завершить программу
move_worm: mov [di],bx ; поместить адрес головы в DS:DI inc di inc di ; и увеличить DI на 2, mov byte ptr es:[bx],09 ; вывести точку на экран, cmp byte ptr cs:eaten_food,1 ; если предыдущим ; ходом была съедена еда, je if_eaten_food ; создать новую еду, jmp short main_cycle ; иначе - продолжить основной цикл
grow_worm: push bx ; сохранить адрес головы mov bx,word ptr cs:food_at ; bx - адрес еды xor ах,ах ; АХ = 0 call draw_food ; стереть еду call random ; AX - случайное число and ax,3Fh ; AX - случайное число от 0 до 63 mov bp,ax ; это число будет добавкой ; к длине питона mov byte ptr cs:eaten_food,1 ; установить флаг ; для генерации еды на следующем ходе pop bx ; восстановить адрес головы ВХ jmp short move_worm ; перейти к движению питона
if_eaten_food: ; переход сюда, если еда была съедена mov byte ptr cs:eaten_food,0 ; восстановить флаг init_food: ; переход сюда в самом начале push bx ; сохранить адрес головы make_food: call random ; AX - случайное число and ax,0FFFEh ; AX - случайное четное число mov bx,ax ; BX - новый адрес для еды xor ах,ах cmp word ptr es:[bx],ax ; если по этому адресу ; находится тело питона, jne make_food ; еще раз сгенерировать случайный адрес, cmp word ptr es:[bx+320],ax ; если на строку ниже ; находится тело питона - jne make_food ; то же самое, mov word ptr cs:food_at,bx ; поместить новый адрес ; еды в food_at, mov ax,0D0Dh ; цвет еды в АХ call draw_food ; нарисовать еду на экране pop bx jmp main_cycle
; процедура draw_food ; изображает четыре точки на экране - две по адресу ВХ и две на следующей ; строке. Цвет первой точки из пары - AL, второй - АН
draw_food: mov es:[bx],ax mov word ptr es:[bx+320],ax retn
; генерация случайного числа ; возвращает число в АХ, модифицирует DX
random: mov ах,word ptr cs:seed, mov dx,8E45h mul dx inc ax mov cs:word ptr seed,ax retn
; переменные
eaten_food db 0 move_direction dw 1 ; направление движения: 1 - вправо, ; -1 - влево, 320 - вниз, -320 - вверх seed: ; это число хранится за концом программы, food_at equ seed+2 ; а это - за предыдущим end start
Создание и открытие файлов
Функция DOS 3Ch - Создать файл
| Ввод: |
AX = 3Ch СХ = атрибут файла
бит 7: файл можно открывать разным процессам в Novell Netware
бит 6: не используется
бит 5: архивный бит (1, если файл не сохранялся)
бит 4: каталог (должен быть 0 для функции 3Ch)
бит 3: метка тома (игнорируется функцией 3Ch)
бит 2: системный файл
бит 1: скрытый файл
бит 0: файл только для чтения
DS:DX = адрес ASCIZ-строки с полным именем файла (ASCIZ-строка ASCII-символов, оканчивающаяся нулем) |
| Вывод: |
CF = 0 и АХ = идентификатор файла, если не произошла ошибка CF = 1 и АХ = 03h, если путь не найден CF = 1 и АХ = 04h, если слишком много открытых файлов CF = 1 и АХ = 05h, если доступ запрещен |
Если файл уже существует, функция 3Ch все равно открывает его, присваивая ему нулевую длину. Чтобы этого не произошло, следует пользоваться функцией 5Bh.
Функция DOS 3Dh - Открыть существующий файл
| Ввод: |
AX = 3Dh AL = режим доступа
биты 0 – : права доступа
00: чтение
01: запись
10: чтение и запись
бит 1: открыть для записи
биты 2 – 3: зарезервированы (0)
биты 6 – 4: режим доступа для других процессов
000: режим совместимости (остальные процессы также должны открывать этот файл в режиме совместимости)
001: все операции запрещены
010: запись запрещена
011: чтение запрещено
100: запрещений нет
бит 7: файл не наследуется порождаемыми процессами
DS:DX = адрес ASCIZ-строки с полным именем файла
CL = маска атрибутов файлов |
| Вывод: |
CF = 0 и АХ = идентификатор файла, если не произошла ошибка CF = 1 и АХ = код ошибки (02h - файл не найден, 03h - путь не найден, 04h - слишком много открытых файлов, 05h - доступ запрещен, 0Ch - неправильный режим доступа) |
Функция DOS 5Bh - Создать и открыть новый файл
| Ввод: |
AX = 5Bh СХ = атрибут файла DS:DX = адрес ASCIZ-строки с полным именем файла |
| Вывод: |
CF = 0 и АХ = идентификатор файла, открытого для чтения/записи в режиме совместимости, если не произошла ошибка CF = 1 и АХ = код ошибки (03h - путь не найден, 04h - слишком много открытых файлов, 05h - доступ запрещен, 50h - файл уже существует) |
Функция DOS 5Ah - Создать и открыть временный файл
| Ввод: |
AX = 5Ah СХ = атрибут файла DS:DX = адрес ASCIZ-строки с путем, оканчивающимся символом "\", и тринадцатью нулевыми байтами в конце |
| Вывод: |
CF = 0 и АХ = идентификатор файла, открытого для чтения/записи в режиме совместимости, если не произошла ошибка (в строку по адресу DS:DX дописывается имя файла) CF = 1 и АХ = код ошибки (03h - путь не найден, 04h - слишком много открытых файлов, 05h - доступ запрещен) |
Функция 5Ah создает файл с уникальным именем, который не является на самом деле временным, его следует специально удалять, для чего его имя и записывается в строку в DS:DX.
Во всех случаях строка с полным именем файла имеет вид типа
filespec db 'с:\data\filename.ext',0
причем, если диск или путь опущены, используются их текущие значения.
Для работы с длинными именами файлов в DOS 7.0 (Windows 95) и старше используется еще один дополнительный набор функций, которые вызываются как функция DOS 71h.
Функция LFN 6Ch - Создать или открыть файл с длинным именем
| Ввод: |
AX = 716Сh ВХ = режим доступа Windows 95
биты 2 – 0: доступ
000 - только для чтения
001 - только для записи
010 - для чтения и записи
100 - только для чтения, не изменять время последнего обращения к файлу
биты 6 – 4: доступ для других процессов (см. функцию 3Dh)
бит 7: файл не наследуется порождаемыми процессами
бит 8: данные не буферизуются
бит 9: не архивировать файл, если используется архивирование файловой системы (DoubleSpace)
бит 10: использовать число в DI для записи в конец короткого имени файла
бит 13: не вызывать прерывание 24h при критических ошибках
бит 14: сбрасывать буфера на диск после каждой записи в файл
СХ = атрибут файла
DX = действие
бит 0: открыть файл (ошибка, если файл не существует)
бит 1: заменить файл (ошибка, если файл не существует)
бит 4: создать файл (ошибка, если файл существует)
DS:SI = адрес ASCIZ-строки с именем файла
DI = число, которое будет записано в конце короткого варианта имени файла |
| Вывод: |
CF = 0 АХ = идентификатор файла СХ = 1, если файл открыт СХ = 2, если файл создан СХ = 3, если файл заменен CF = 1, если произошла ошибка АХ = код ошибки (7100h, если функция не поддерживается) |
<
/p>
Если функции открытия файлов возвращают ошибку "слишком много открытых файлов" (АХ = 4), следует увеличить число допустимых идентификаторов с помощью функции 67h.
Функция DOS 67h - Изменить максимальное число идентификаторов файлов
| Ввод: |
AX = 67h ВХ = новое максимальное число идентификаторов (20 – 65 535) |
| Вывод: |
CF = 0, если не произошла ошибка CF = 1 и АХ = код ошибки, если произошла ошибка (например: 04h, если заданное число меньше, чем количество уже открытых файлов, или 08h, если DOS не хватает памяти для новой таблицы идентификаторов) |
Следует помнить, что все дочерние процессы будут наследовать только первые 20 идентификаторов и должны вызывать функцию 67h сами, если им требуется больше.
Средства BIOS
Функции DOS вывода на экран позволяют перенаправлять вывод в файл, но не позволяют вывести текст в любую позицию экрана и не позволяют изменить цвет текста. DOS предполагает, что для более тонкой работы с экраном программы должны использоваться видеофункции BIOS. BIOS (базовая система ввода-вывода) - это набор программ, расположенных в постоянной памяти компьютера, которые выполняют его загрузку сразу после включения и обеспечивают доступ к некоторым устройствам, в частности к видеоадаптеру. Все функции видеосервиса BIOS вызываются через прерывание 10h. Рассмотрим функции, которые могут быть полезны для вывода текстов на экран (полностью видеофункции BIOS описаны в приложении 2).
Выбор видеорежима
BIOS предоставляет возможность переключения экрана в различные текстовые и графические режимы. Режимы отличаются друг от друга разрешением (для графических) и количеством строк и столбцов (для текстовых), а также количеством возможных цветов.
INT 10h, АН = 00 - Установить видеорежим
| Ввод: |
AL = номер режима в младших 7 битах |
| Вывод: |
Обычно никакого, но некоторые BIOS (Phoenix и AMI) помещают в AL 30Н для текстовых режимов и 20h для графических |
Вызов этой функции приводит к тому, что экран переводится в выбранный режим. Если старший бит AL не установлен в 1, экран очищается. Номера текстовых режимов - 0, 1, 2, 3 и 7. 0 и 1 - 16-цветные режимы 40x25 (с 25 строками по 40 символов в строке), 2 и 3 - 16-цветные режимы 80x25, 7 - монохромный режим 80x25. Мы не будем пока рассматривать графические режимы, хотя функции вывода текста на экран DOS и BIOS могут работать и в них. Существует еще много текстовых режимов с более высоким разрешением (80x43, 80x60, 132x50 и т.д.), но их номера для вызова через эту функцию различны для разных видеоадаптеров (например, режим 61h - 132x50 для Cirrus 5320 и 132x29 для Genoa 6400). Однако, если видеоадаптер поддерживает стандарт VESA BIOS Extention, в режимы с высоким разрешением можно переключаться, используя функцию 4Fh.
INT 10h, АН = 4Fh, AL = 02 - Установить SuperVGA-видеорежим
| Ввод: |
ВХ = номер режима в младших 13 битах |
| Вывод: |
AL = 4Fh, если эта функция поддерживается АН = 0, если переключение произошло успешно АН = 1, если произошла ошибка |
Если бит 15 регистра ВХ установлен в 1, видеопамять не очищается. Текстовые режимы, которые можно вызвать с использованием этой функции: 80x60 (режим 108h), 132x25 (109h), 132x43 (10Ah), 132x50 (10Bh), 132x60 (10Ch).
Видеорежим, используемый в DOS по умолчанию, - текстовый режим 3.
Управление положением курсора
INT 10h, АН = 02 - Установить положение курсора
| Ввод: |
АН = 02 ВН = номер страницы DH = строка DL = столбец |
С помощью этой функции можно установить курсор в любую позицию экрана, и дальнейший вывод текста будет происходить из этой позиции. Отсчет номера строки и столбца ведется от верхнего левого угла экрана (символ в левой верхней позиции имеет координаты 0, 0). Номера страниц 0 – 3 (для режимов 2 и 3)и 0 – 7 (для режимов 1 и 2) соответствуют области памяти, содержимое которой в данный момент отображается на экране. Можно вывести текст в неактивную в настоящий момент страницу, а затем переключиться на нее, чтобы изображение изменилось мгновенно.
INТ 10h, АН = 03 - Считать положение и размер курсора
| Ввод: |
АН = 03 ВН = номер страницы |
| Вывод: |
DH, DL = строка и столбец текущей позиции курсора СН, CL = первая и последняя строки курсора |
Возвращает текущее состояние курсора на выбранной странице (каждая страница использует собственный независимый курсор).
Вывод символов на экран
Каждый символ на экране описывается двумя байтами - ASCII-кодом символа и байтом атрибута, указывающим цвет символа и фона, а также является ли символ мигающим.
Атрибут символа:
Бит 7: символ мигает (по умолчанию) или фон яркого цвета (если его действие было переопределено видеофункцией 10h).
Биты 6 – 4: цвет фона.
Бит 3: символ яркого цвета ( по умолчанию) или фон мигает (если его действие было переопределено видеофункцией 11h).
Биты 2 – 0: цвет символа.
Цвета кодируются в битах, как показано в таблице 18.
Таблица 18. Атрибуты символов
| |
Обычный цвет |
Яркий цвет |
| 000b |
черный |
темно-серый |
| 001b |
синий |
светло-синий |
| 010b |
зеленый |
светло-зеленый |
| 011b |
голубой |
светло-голубой |
| 100b |
красный |
светло-красный |
| 101b |
пурпурный |
светло-пурпурный |
| 110b |
коричневый |
желтый |
| 111b |
светло-серый |
белый |
INT 10h, АН = 08 - Считать символ и атрибут символа в текущей позиции курсора
| Ввод: |
АН = 08 ВН = номер страницы |
| Вывод: |
АН = атрибут символа AL = ASCII-код символа |
INT 10h, АН = 09 - Вывести символ с заданным атрибутом на экран
| Ввод: |
АН = 09 ВН = номер страницы AL = ASCII-код символа BL = атрибут символа СХ = число повторений символа |
С помощью этой функции можно вывести на экран любой символ, включая даже символы CR и LF, которые обычно интерпретируются как конец строки. В графических режимах СХ не должен превышать число позиций, оставшееся до правого края экрана.
INT 10h, АН = 0Ah - Вывести символ с текущим атрибутом на экран
| Ввод: |
АН = 0Ah ВН = номер страницы AL = ASCII-код символа СХ = число повторений символа |
Эта функция также выводит любой символ на экран, но в качестве атрибута символа используется атрибут, который имел символ, находившийся ранее в этой позиции.
INT 10h, АН = 0Eh - Вывести символ в режиме телетайпа
| Ввод: |
АН = 0Eh ВН = номер страницы AL = ASCII-код символа |
Символы CR (0Dh), LF (0Ah), BEL (7) интерпретируются как управляющие символы. Если текст при записи выходит за пределы нижней строки, экран прокручивается вверх. В качестве атрибута используется атрибут символа, находившегося в этой позиции.
INT 10h, AH = 13h - Вывести строку символов с заданными атрибутами
| Ввод: |
АН = 13h AL = режим вывода:бит 0 - переместить курсор в конец строки после вывода бит 1 - строка содержит не только символы, но также и атрибуты, так что каждый символ описывается двумя байтами: ASCII-код и атрибут биты 2 – 7 зарезервированыСХ = длина строки (только число символов) BL = атрибут, если строка содержит только символы DH,DL = строка и столбец, начиная с которых будет выводиться строки ES:BP = адрес начала строки в памяти |
<
/p>
Функция 13h выводит на экран строку символов, интерпретируя управляющие символы CR (0Dh), LF (0Ah), BS (08) и BEL (07). Если строка подготовлена в формате символ,атрибут - гораздо быстрее просто скопировать ее в видеопамять, о чем рассказано в следующем разделе.
Воспользуемся теперь функциями BIOS, чтобы усовершенствовать программу DOSOUT1 и вывести на экран все 256 символов, включая даже символы перевода строки. Кроме того, для лучшей читаемости таблицы после каждого символа будет выводиться пробел.
; biosout.asm ; Выводит на экран все ASCII-символы без исключения .model tiny .code org 100h ; Начало СОМ-файла start: mov ax,0003h int 10h ; Видеорежим 3 (очистка экрана ; и установка курсора в 0, 0) mov dx,0 ; DH и DL будут использоваться ; для хранения положения курсора. ; Начальное положение - 0,0 mov si,256 ; SI будет счетчиком цикла mov al,0 ; Первый символ - с кодом 00h mov ah,9 ; Номер видеофункции "вывод символа с атрибутом" mov cx,1 ; Выводится один символ за раз mov bl,00011111b ;атрибут символа - белый на синем cloop: int 10h ; Вывести символ на экран push ax ; Сохранить текущий символ и номер функции mov ah,2 ; Номер видеофункции 2 - ; изменить положение курсора inc dl ; Увеличить текущий столбец на 1 int 10h ; Переместить курсор mov ax,0920h ; АН = 09, AL = 20h (ASCII-код пробела) int 10h ; Вывести пробел mov ah,2 ; Номер видеофункции 2 inc dl ; Увеличить столбец на 1 int 10h ; Переместить курсор pop ax ; Восстановить номер функции в ah ; и текущий символ в al inc al ; Увеличить AL на 1 - следующий символ test al,0Fh ; Если AL не кратен 16, jnz continue_loop ; продолжить цикл, push ax ; иначе - сохранить номер функции ; и текущий символ mov ah,2 ; Номер видеофункции 2 inc dh ; Увеличить номер строки на 1 mov dl,0 ; Столбец = 0 int 10h ; Установить курсор на начало следующей строки pop ax ; Восстановить номер видеофункции ; и текущий символ continue_loop: dec si ; Уменьшить SI на 1, ; если он не стал нулем - продолжить jnz cloop ; CX используется внутри цикла, ; так что нельзя использовать команду LOOP ; для его организации ret ; Завершение СОМ-файла end start
Так как функция 09 выводит символ в позиции курсора, но не перемещает сам курсор, это приходится делать каждый раз специально.
Функции BIOS удобны для переключения и настройки видеорежимов, но часто оказывается, что вывод текста на экран гораздо быстрее и проще выполнять просто копированием изображения в видеопамять.
Так же как и для вывода на экран, BIOS предоставляет больше возможностей по сравнению с DOS для считывания данных и управления клавиатурой. Например, функциями DOS нельзя определить нажатие комбинаций клавиш типа Ctrl-Alt-Enter или нажатие двух клавиш Shift одновременно, DOS не может определить момент отпускания нажатой клавиши, и наконец, в DOS нет аналога функции С ungetch(), помещающей символ в буфер клавиатуры, как если бы его ввел пользователь. Все это можно осуществить, используя различные функции прерывания 16h и операции с байтами состояния клавиатуры.
INT 16h, АН = 0, 10h, 20h - Чтение символа с ожиданием
| Ввод: |
АН = 00h (83/84-key), 10h (101/102-key), 20h (122-key) |
| Вывод: |
AL = ASCII-код символа, 0 или префикс скан-кода АН = скан-код нажатой клавиши или расширенный ASCII-код |
Каждой клавише на клавиатуре соответствует так называемый скан-код (см. приложение 1), соответствующий только этой клавише. Этот код посылается клавиатурой при каждом нажатии и отпускании клавиши и обрабатывается BIOS (обработчиком прерывания INT 9). Прерывание 16h дает возможность получить код нажатия, не перехватывая этот обработчик. Если нажатой клавише соответствует ASCII-символ, то в АН возвращается код этого символа, а в AL - скан-код клавиши. Если нажатой клавише соответствует расширенный ASCII-код, в AL возвращается префикс скан-кода (например, Е0 для серых клавиш) или 0, если префикса нет, а в АН - расширенный ASCII-код. Функция 00Н обрабатывает только комбинации, использующие клавиши 84-клавишной клавиатуры, l0h обрабатывает все 101 – 105-клавишные комбинации, 20h - 122-клавишные. Тип клавиатуры можно определить с помощью функции 09h прерывания 16h, если она поддерживается BIOS (поддерживается ли эта функция, можно узнать с помощью функции C0h прерывания 15h).
INT 16h, АН = 1, 11h, 21h - Проверка символа
| Ввод: |
АН = 01h (83/84-key), 11h (101/102-key), 21h (122-key) |
| Вывод: |
ZF = 1, если буфер пуст ZF = 0, если в буфере присутствует символ, в этом случае AL = ASCII-код символа, 0 или префикс скан-кода АН = скан-код нажатой клавиши или расширенный ASCII-код |
<
/p>
Символ остается в буфере клавиатуры, хотя некоторые BIOS удаляют символ из буфера при обработке функции 01h, если он соответствует расширенному ASCII-коду, отсутствующему на 84-клавишных клавиатурах.
INT 16h, АН = 05h - Поместить символ в буфер клавиатуры
| Ввод: |
АН = 05h СН = скан-код CL = ASCII-код |
| Вывод: |
AL = 00, если операция выполнена успешно AL = 01h, если буфер клавиатуры переполнен АН модифицируется многими BIOS |
Обычно можно поместить 0 вместо скан-кода в СН, если функция, которая будет выполнять чтение из буфера, будет использовать именно ASCII-код. Например, следующая программа при запуске из DOS вызывает команду DIR (но при запуске из некоторых оболочек, например FAR, этого не произойдет).
; ungetch.asm ; заносит в буфер клавиатуры команду DIR так, чтобы она ; выполнилась сразу после завершения программы ; .model tiny .code org 100h ; СОМ-файл start: mov cl,'d' ; CL = ASCII-код буквы "d" call ungetch mov cl,'i' ; ASCII-код буквы "i" call ungetch mov cl,'r' ; ASCII-код буквы "r" call ungetch mov cl,0Dh ; перевод строки ungetch: mov ah,5 ; AH = номер функции mov ch,0 ; CH = 0 (скан-код неважен) int 16h ; поместить символ в буфер ret ; завершить программу
end start
INT 16h, AH = 02h, 12h, 22h - Считать состояние клавиатуры
| Ввод: |
АН = 02h (83/84-key), 12h (101/102-key), 22h (122-key) |
| Вывод: |
AL = байт состояния клавиатуры 1 АН = байт состояния клавиатуры 2 (только для функций 12h и 22h) |
Байт состояния клавиатуры 1 (этот байт всегда расположен в памяти по адресу 0000h:0417h или 0040h:0017h):
Бит 7: Ins включена
Бит 6: CapsLock включена
Бит 5: NumLock включена
Бит 4: ScrollLock включена
Бит 3: Alt нажата (любая Alt для функции 02h, часто только левая Alt для 12h/22h)
Бит 2: Ctrl нажата (любая Ctrl)
Бит 1: Левая Shift нажата
Бит 0: Правая Shift нажата
Байт состояния клавиатуры 2 (этот байт всегда расположен в памяти по адресу 0000h:0418h или 0040h:0018h):
Средства DOS
На примере первой программы на ассемблере мы уже познакомились с одним из способов вывода текста на экран - вызовом функции DOS 09h. Это далеко не единственный способ вывода текста - DOS предоставляет для этого несколько функций.
Функция DOS 02h - Записать символ в STDOUT с проверкой на Ctrl-Break
| Ввод: |
АН = 02h DL = ASCII-код символа |
| Вывод: |
Никакого, согласно документации, но на самом деле: AL = код последнего записанного символа (равен DL, кроме случая, когда DL = 09h (табуляция), тогда в AL возвращается 20h). |
Эта функция при выводе на экран обрабатывает некоторые управляющие символы - вывод символа BEL (07h) приводит к звуковому сигналу, символ BS (08h) приводит к движению курсора влево на одну позицию, символ НТ (09h) заменяется на несколько пробелов, символ LF (0Ah) опускает курсор на одну позицию вниз, и CR (0Dh) приводит к переходу на начало текущей строки.
Если в ходе работы этой функции была нажата комбинация клавиш Ctrl-Break, вызывается прерывание 23h, которое по умолчанию осуществляет выход из программы.
Например, напишем программу, выводящую на экран все ASCII-символы, 16 строк по 16 символов в строке.
; dosoutl.asm ; Выводит на экран все ASCII-символы ; .model tiny .code org 100h ; начало СОМ-файла start: mov ex,256 ; вывести 256 символов mov dl,0 ; первый символ - с кодом 00 mov ah,2 ; номер функции DOS "вывод символа" cloop: int 21h ; вызов DOS inc dl ; увеличение DL на 1 - следующий символ test dl,0Fh ; если DL не кратен 16, jnz continue_loop ; продолжить цикл, push dx ; иначе: сохранить текущий символ mov dl,0Dh ; вывести CR int 21h mov dl,0Ah ; вывести LF int 21h pop dx ; восстановить текущий символ continue_loop: loop cloop ; продолжить цикл ret ; завершение СОМ-файла end start
Это программа типа СОМ, и компилироваться она должна точно так же, как hello-1.asm в разделе 4.1. Здесь с помощью команды LOOP оформляется цикл, выполняющийся 256 раз (значение регистра СХ в начале цикла). Регистр DL содержит код символа, который равен нулю в начале цикла и увеличивается каждый раз на 1 командой INC DL. Если значение DL сразу после увеличения на 1 кратно 16, оно временно сохраняется в стеке и на экран выводятся символы CR и LF, выполняющие переход на начало новой строки. Проверка выполняется командой TEST DL,0Fh - результат операции AND над DL и 0Fh будет нулем, только если младшие четыре бита DL равны нулю, что и соответствует кратности шестнадцати.
Все функции DOS вывода на экран используют устройство STDOUT, стандартный вывод. Это позволяет перенаправлять вывод программы в файл или на стандартный ввод другой программы. Например, если написать в командной строке
hello-1.com > hello-1.out
то на экран ничего выдано не будет, а в текущем каталоге появится файл hello-1.out, содержащий строку "Hello World!". Точно так же, если написать
dosout1.com > dosout1.out
то в файле dosout1.out окажутся все символы ASCII, причем символы BEL и BS не будут интерпретироваться и запишутся в файл как есть. Символы CR и LF тоже запишутся как есть, но так как они отмечают конец строки, редакторы и просмотрщики текстовых файлов будут разрывать первую строку символов.
Функция DOS 06h - Записать символ в STDOUT без проверки на Ctrl-Break
| Ввод: |
АН = 06h DL = ASCII-код символа (кроме FFh) |
| Вывод: |
Никакого, согласно документации, но на самом деле: AL = код записанного символа (копия DL) |
Эта функция не обрабатывает управляющие символы (CR, LF, HT и BS выполняют свои функции при выводе на экран, но сохраняются при перенаправлении вывода в файл) и не проверяет нажатие Ctrl-Break. Можно заменить в программе dosoutl.asm команду MOV АН,2 на MOV АН,6 и перекомпилировать этот пример, чтобы получить более полную таблицу символов.
Функция DOS 09h - Записать строку в STDOUT с проверкой на Ctrl-Break
| Ввод: |
АН = 09h DS:DX = адрес строки, заканчивающейся символом $ (24h) |
| Вывод: |
Никакого, согласно документации, но на самом деле: AL = 24h (код последнего символа) |
Действие этой функции полностью аналогично действию функции 02h, но выводится не один символ, а целая строка, как в программах hello-1.asm и hello-2.asm.
Функция DOS 40h - Записать в файл или устройство
| Ввод: |
АН = 40h ВХ = 1 для STDOUT или 2 для STDERR DS:DX = адрес начала строки СХ = длина строки |
| Вывод: |
CF = 0, АХ = число записанных байт |
Эта функция предназначена для записи в файл, но, если в регистр ВХ поместить число 1, функция 40h будет выводить данные на STDOUT, а если ВХ = 2 - на устройство STDERR. STDERR всегда выводит данные на экран и не перенаправляется в файлы. На этой функции основаны используемые в С функции стандартного вывода - фактически функция С fputs() просто вызывает это прерывание, помещая свой первый аргумент в ВХ, адрес строки (второй аргумент) - в DS:DX и длину - в СХ.
; dosout2.asm ; Выводит на экран строку "This function can print $", ; используя вывод в STDERR, так что ее нельзя перенаправить в файл. .model tiny .code org 100h ; начало СОМ-файла start: mov ah,40h ; номер функции DOS mov bx,2 ; устройство STDERR mov dx,offset message ; DS:DX - адрес строки mov cx, message_length ; CX - длина строки int 21h ret ; завершение СОМ-файла message db "Эта функция может выводить знак $" message_length = $-message ; длина строки = текущий адрес ; минус адрес начала строки end start
Если скомпилировать эту программу и запустить ее командой
dosout2.com > dosout2.out
то сообщение появится на экране, а файл dosout2.out окажется пустым.
И наконец, последняя функция DOS вывода на экран - недокументированное прерывание 29h.
INT 29h: Быстрый вывод символа на экран
| Ввод: |
AL = ASCII-код символа |
В большинстве случаев INT 29h просто немедленно вызывает функцию BIOS "вывод символа на экран в режиме телетайпа", так что никаких преимуществ, кроме экономии байт при написании как можно более коротких программ, она не имеет.
/p>
При чтении с помощью этой функции введенный символ автоматически немедленно отображается на экране (посылается в устройство STDOUT - так что его можно перенаправить в файл). При нажатии Ctrl-C или Ctrl-Break выполняется команда INT 23h. Если нажата клавиша, не соответствующая какому-нибудь символу (стрелки, функциональные клавиши Ins, Del и т.д.), то в AL возвращается 0 и функцию надо вызвать еще один раз, чтобы получить расширенный ASCII-код (см. приложение 1).
В трех следующих вариантах этой функции код символа возвращается в AL по такому же принципу.
Функция DOS 08h - Считать символ из STDIN без эха, с ожиданием и проверкой на Ctrl-Break
| Ввод: |
АН = 08h |
| Вывод: |
AL = код символа |
Функция DOS 07h - Считать символ из STDIN без эха, с ожиданием и без проверки на Ctrl-Break
| Ввод: |
АН = 07h |
| Вывод: |
AL = код символа |
Функция DOS 06h - Считать символ из STDIN без эха, без ожидания и без проверки на Ctrl-Break
| Ввод: |
АН = 07h DL = 0FFh |
| Вывод: |
ZF = 1, если не была нажата клавиша, и AL = 00 ZF = 0, если клавиша была нажата. В этом случае AL = код символа |
Кроме перечисленных функций могут потребоваться и некоторые служебные функции DOS для работы с клавиатурой.
Функция DOS 0Bh - Проверить состояние клавиатуры
| Ввод: |
АН = 0Bh |
| Вывод: |
AL = 0, если не была нажата клавиша AL = 0FFh, если была нажата клавиша |
Эту функцию удобно использовать перед функциями 01, 07 и 08, чтобы не ждать нажатия клавиши. Кроме того, вызов этой функции позволяет проверить, не считывая символ с клавиатуры, была ли нажата комбинация клавиш Ctrl-Break; если это произошло, выполнится прерывание 23h.
Функция DOS 0Ch - Очистить буфер и считать символ
| Ввод: |
АН = 0Ch AL = Номер функции DOS (01, 06, 07, 08, 0Ah) |
| Вывод: |
Зависит от вызванной функции |
Функция 0Ch очищает буфер клавиатуры, так что следующая функция чтения символа будет ждать ввода с клавиатуры, а не использовать нажатый ранее и еще не обработанный символ. Например, именно эта функция используется для считывания ответа на вопрос "Уверен ли пользователь в том, что он хочет отформатировать диск?".
Функции посимвольного ввода без эха можно использовать для интерактивного управления программой, как в следующем примере.
; dosin2.asm ; Изображает пентамино F, которое можно перемещать по экрану клавишами ; управления курсором и вращать клавишами X и Z. Выход из программы - Esc. ; line_length = 3 ; число символов в строке изображения number_of_lines = 3 ; число строк
.model tiny .code org 100h ; начало СОМ-файла start: cld ; будут использоваться команды ; строковой обработки mov ax,0B800h ; адрес начала текстовой видеопамяти mov es,ax ; в ES mov ax,0003h int 10h ; текстовый режим 03 (80x25) mov ah,02h ; установить курсор mov bh,0 mov dh,26 ; на строку 26, то есть за пределы экрана mov dl,1 int 10h ; теперь курсора на экране нет call update_screen ; вывести изображение
; основной цикл опроса клавиатуры main_loop: mov ah,08h ; считать символ с клавиатуры int 21h ; без эха, с ожиданием, с проверкой на Ctrl-Break test al,al ; если AL = 0 jz eASCII_entered ; введен символ расширенного ASCII cmp al,1Bh ; иначе: если введен символ 1Bh (Esc), je key_ESC ; выйти из программы, cmp al,'Z' ; если введен символ Z, je key_Z ; перейти на его обработчик cmp al,'z' ; то же для z je key_Z cmp al,'X' ; если введен символ X, je key_X ; перейти на его обработчик cmp al,'х' ; то же для х je key_X jmp short main_loop ; считать следующую клавишу
eASCII_entered: ; был введен расширенный ASCII-символ int 21h ; получить его код (повторный вызов функции) cmp al,48h ; стрелка вверх je key_UP cmp al,50h ; стрелка вниз je key_DOWN cmp al,4Bh ; стрелка влево je key_LEFT cmp al,4Dh ; стрелка вправо je key_RIGHT jmp short main_loop ; считать следующую клавишу ; ; обработчики нажатий клавиш ; key_ESC: ; Esc ret ; завершить СОМ-программу
key_UP: ; стрелка вверх cmp byte ptr start_row,0 ; если изображение на верхнем ; краю экрана, jna main_loop ; считать следующую клавишу, dec byte ptr start_row ; иначе - уменьшить номер строки, call update_screen ; вывести новое изображение jmp short main_loop ; и считать следующую клавишу
key_DOWN: ; стрелка вниз cmp byte ptr start_row,25-number_of_lines ; если ; изображение на нижнем краю экрана, jnb main_loop ; считать следующую клавишу, inc byte ptr start_row ; иначе - увеличить номер строки, call update_screen ; вывести новое изображение jmp short main_loop ; и считать следующую клавишу
key_LEFT: ; стрелка влево cmp byte ptr start_col,0 ; если изображение на левом краю ; экрана, jna main_loop ; считать следующую клавишу, dec byte ptr start_col ; иначе - уменьшить номер столбца, call update_screen ; вывести новое изображение jmp short main_loop ; и считать следующую клавишу
key_RIGHT: ; стрелка вправо cmp byte ptr start_col,80-line_length ; если ; изображение на правом краю экрана, jnb main_loop ; считать следующую клавишу, inc byte ptr start_col ; иначе - увеличить номер столбца, call update_screen ; вывести новое изображение jmp short main_loop ; и считать следующую клавишу
key_Z: ; клавиша Z (вращение влево) mov ax,current_screen ; считать номер текущего изображения ; (значения 0, 1, 2, 3), dec ax ; уменьшить его на 1, jns key_Z_ok ; если получился -1 (поменялся знак), mov ах,3 ; АХ = 3 key_Z_ok: mov current_screen,ax ; записать номер обратно, call update_screen ; вывести новое изображение jmp main_loop ; и считать следующую клавишу
key_X: ; клавиша X (вращение вправо) mov ax,current_screen ; считать номер текущего изображения ; (значения 0, 1, 2, 3), inc ax ; увеличить его на 1, cmp ax,4 ; если номер стал равен 4, jne key_X_ok xor ах,ах ; АХ = 0 key_X_ok: mov current_screen,ax ; записать номер обратно, call update_screen ; вывести новое изображение jmp main_loop ; и считать следующую клавишу
; процедура update_screen ; очищает экран и выводит текущее изображение ; модифицирует значения регистров АХ, ВХ, СХ, DX, SI, DI update_screen: mov cx,25*80 ; число символов на экране mov ax,0F20h; ; символ 20h (пробел) с атрибутом 0Fh ; (белый на черном) xor di,di ; ES:DI = начало видеопамяти rep stosw ; очистить экран mov bx,current_screen ; номер текущего изображения в ВХ shl bx,1 ; умножить на 2, так как screens - массив слов mov si,screens[bx] ; поместить в ВХ смещение начала ; текущего изображения из массива screens, mov ax,start_row ; вычислить адрес начала mul row_length ; изображения в видеопамяти add ax,start_col ; (строка * 80 + столбец) * 2 shl ax,1 mov di,ax ; ES:DI - начало изображения в видеопамяти mov ah,0Fh ; используемый атрибут - белый на черном mov dx,number_of_lines ; число строк в изображении сору_lines: mov cx,line_length ; число символов в строке copy_1: lodsb ; считать ASCII-код в AL, stosw ; записать его в видеопамять ; (AL - ASCII, АН - атрибут), loop copy_1 ; вывести так все символы в строке, add di,(80-line_length)*2 ; перевести DI на начало ; следующей строки экрана, dec dx ; если строки не закончились - jnz copy_lines ; вывести следующую ret ; конец процедуры update_screen
; изображение пентамино F screen1 db " XX" ; выводимое изображение db "XX " db " X "
screen2 db " X " ; поворот на 90 градусов вправо db "XXX" db " X"
screen3 db " X " ; поворот на 180 градусов db " XX" db "XX "
screen4 db "X " ; поворот на 90 градусов влево db "XXX" db " X " ; массив, содержащий адреса всех вариантов изображения screens dw screen1,screen2,screen3,screen4 current_screen dw 0 ; текущий вариант изображения start_row dw 10 ; текущая верхняя строка изображения start_col dw 37 ; текущий левый столбец row_length db 80 ; длина строки экрана для команды MUL
end start
В этом примере для вывода на экран используется прямое копирование в видеопамять, так как вызов функции BIOS вывода строки (INT 10h, АН = 13h) прокручивает экран вверх на одну строку при выводе символа в нижнем правом углу экрана.
Как и в случае вывода на экран, DOS предоставляет набор функций для чтения данных с клавиатуры, которые используют стандартное устройство ввода STDIN, так что можно использовать в качестве источника данных файл или стандартный вывод другой программы.
Функция DOS 0Ah - Считать строку символов из STDIN в буфер
| Ввод: |
АН = 0Ah DS:DX = адрес буфера |
| Вывод: |
Буфер содержит введенную строку |
Для вызова этой функции надо подготовить буфер, первый байт которого содержит максимальное число символов для ввода (1 – 254), а содержимое, если оно задано, может использоваться как подсказка для ввода. При наборе строки обрабатываются клавиши Esc, F3, F5, BS, Ctrl-C/Ctrl-Break и т.д., как при наборе команд DOS (то есть Esc начинает ввод сначала, F3 восстанавливает подсказку для ввода, F5 запоминает текущую строку как подсказку, Backspace стирает предыдущий символ). После нажатия клавиши Enter строка (включая последний символ CR (0Dh)) записывается в буфер, начиная с третьего байта. Во второй байт записывается длина реально введенной строки без учета последнего CR.
Рассмотрим пример программы, выполняющей преобразование десятичного числа в шестнадцатеричное.
; dosinl.asm ; Переводит десятичное число в шестнадцатеричное ; .model tiny .code .286 ; для команды shr al,4 org 100h ; начало СОМ-файла start: mov dx,offset message1 mov ah,9 int 21h ; вывести приглашение ко вводу message1 mov dx,offset buffer mov ah,0Ah int 21h ; считать строку символов в буфер mov dx,offset crlf mov ah,9 int 21h ; перевод строки
; перевод числа в ASCII-формате из буфера в бинарное число в АХ xor di,di ; DI = 0 - номер байта в буфере xor ах,ах ; АХ = 0 - текущее значение результата mov cl,blength xor ch,ch xor bx,bx mov si,cx ; SI - длина буфера mov cl,10 ; CL = 10, множитель для MUL asc2hex: mov bl,byte ptr bcontents[di] sub bl,'0' ; цифра = код цифры - код символа "0", jb asc_error ; если код символа был меньше, чем код "0", cmp bl,9 ; или больше, чем "9", ja asc_error ; выйти из программы с сообщением об ошибке, mul cx ; иначе: умножить текущий результат на 10, add ax,bx ; добавить к нему новую цифру, inc di ; увеличить счетчик cmp di,si ; если счетчик+1 меньше числа символов - jb asc2hex ; продолжить (счетчик считается от 0)
Управление файловой системой
Начиная с MS-DOS 2.0, файловая система организована в виде каталогов, которые могут содержать файлы и другие каталоги. Функции поиска файлов действуют только в пределах текущего каталога, а функции создания и удаления файлов не действуют на каталоги, несмотря на то, что на самом низком уровне каталог - тот же файл, в атрибуте которого бит 4 установлен в 1 и который содержит список имен вложенных файлов, их атрибутов и физических адресов на диске.
Функция DOS 39h - Создать каталог
| Ввод: |
АН = 39h DS:DX = адрес ASCIZ-строки с путем, в котором все каталоги, кроме последнего, существуют. Для версии DOS 3.3 и более ранних длина всей строки не должна превышать 64 байта |
| Вывод: |
CF = 0, если каталог создан CF = 1 и АХ = 3, если путь не найден, 5 - если доступ запрещен |
Функция LFN 39h - Создать каталог с длинным именем
| Ввод: |
АХ = 7139h DS:DX = адрес ASCIZ-строки с путем |
| Вывод: |
CF = 0, если каталог создан CF = 1 и АХ = код ошибки (7100h, если функция не поддерживается) |
Функция DOS 3Ah - Удалить каталог
| Ввод: |
АН = 3Ah DS:DX = адрес ASCIZ-строки с путем, последний каталог в котором будет удален (только если он пустой, не является текущим, не занят командой SUBST) |
| Вывод: |
CF = 0, если каталог удален CF = 1 и АХ = 3, если путь не найден, 5 - если доступ запрещен, 10h - если удаляемый каталог - текущий |
Функция LFN 3Ah - Удалить каталог с длинным именем
| Ввод: |
АХ = 713Ah DS:DX = адрес строки с путем |
| Вывод: |
CF = 0, если каталог удален, иначе CF = 1 и АХ = код ошибки |
Функция DOS 47h - Определить текущий каталог
| Ввод: |
АН = 47h DL = номер диска (00h - текущий, 01h - А: и т.д.) DS:SI = 64-байтный буфер для текущего пути (ASCIZ-строка без имени диска, первого и последнего символа "\") |
| Вывод: |
CF = 0 и АХ = 0100h, если операция выполнена CF = 1 и АХ = 0Fh, если указан несуществующий диск |
<
/p>
Функция LFN 47h - Определить текущий каталог с длинным именем
| Ввод: |
АХ = 7147h DL = номер диска DS:SI = буфер для пути (ASCIZ- строка без имени диска, первого и последнего символа "\". Необязательно содержит только длинные имена - возвращается тот путь, который использовался при последней смене текущего каталога) |
| Вывод: |
CF = 0, если операция выполнена, иначе CF = 1 и АХ = код ошибки |
Функция DOS 3Bh - Сменить каталог
| Ввод: |
АН = 3Bh DS:DX = адрес 64-байтного ASCIZ-буфера с путем, который станет текущим каталогом |
| Вывод: |
CF = 0, если произошла смена каталога, иначе CF = 1 и АХ = 3 (путь не найден) |
Функция LFN 3Вh - Сменить каталог с длинным именем
| Ввод: |
АХ = 713ВН DS:DX = адрес ASCIZ-буфера с путем |
| Вывод: |
CF = 0, если произошла смена каталога, иначе CF = 1 и АХ = код ошибки |
Перед работой с любыми функциями LFN следует один раз вызвать подфункцию A0h, чтобы определить размеры буферов для имен файлов и путей.
Функция LFN A0h - Получить информацию о разделе файловой системы VFAT
| Ввод: |
АХ = 71A0Н DS:DX = адрес ASCIZ-строки с именем раздела (например: db "С:\",0) ES:DI = адрес буфера для имени файловой системы (FAT, NTFS, CDFS) СХ = размер буфера в ES:DI (обычно достаточно 32 байта) |
| Вывод: |
СХ = 0, АХ = 0000h или 0200h
ВХ = флаги файловой системы:
бит 0: функции поиска учитывают регистр символов
бит 1: регистр символов сохраняется для имен каталогов
бит 2: используются символы Unicode
бит 14: поддерживаются функции LFN
бит 15: включено сжатие раздела (DoubleSpace)
СХ = максимальная длина имени файла (обычно 255) DX = максимальная длина пути (обычно 260) в Windows 95 SP1 возвращает 0000h для CD-ROM CF = 1 и АХ = код ошибки, если произошла ошибка (7100h, если функция не поддерживается) |
Кроме того, при вызове любой функции LFN следует устанавливать CF в 1 для совместимости с ранними версиями DOS. Старые версии DOS не изменяли CF, так что в результате, если функция не поддерживается, CF останется равным 1.
Загрузка и выполнение программ
Как и любая операционная система, DOS загружает и выполняет программы. При загрузке программы в начале отводимого для нее блока памяти (для СОМ-программ это вся свободная на данный момент память) создается структура данных PSP (префикс программного сегмента) размером 256 байт (100h). Затем DOS создает копию текущего окружения для загружаемой программы, помещает полный путь и имя программы в конец окружения, заполняет поля PSP следующим образом:
+00h: слово - CDh 20h - команда INT 20h. Если СОМ-программа завершается командой RETN, управление передается на эту команду. Введено для совместимости с командой СР/М CALL 0.
+02h: слово - сегментный адрес первого байта после области памяти, выделенной для программы
+04h: байт - не используется DOS
+05h: 5 байт - 9Ah F0h FEh 1Dh F0h - команда CALL FAR на абсолютный адрес 000C0h, записанная так, чтобы второй и третий байты составляли слово, равное размеру первого сегмента для СОМ-файлов (в этом примере FEF0h). Введено для совместимости с командой СР/М CALL 5.
+0Ah: 4 байта - адрес обработчика INT 22h (выход из программы)
+0Eh: 4 байта - адрес обработчика INT 23h (обработчик нажатия Ctrl-Break).
+12h: 4 байта - адрес обработчика INT 24h (обработчик критических ошибок)
+16h: слово - сегментный адрес PSP процесса, из которого был запущен текущий.
+18h: 20 байт - JFT - список открытых идентификаторов, один байт на идентификатор, FFh - конец списка.
+2Ch: слово - сегментный адрес копии окружения для процесса.
+2Eh: 2 слова - SS:SP процесса при последнем вызове INT 21h.
+32h: слово - число элементов JFT (по умолчанию 20).
+34h: 4 байта - дальний адрес JFT (по умолчанию PSP:0018).
+38h: 4 байта - дальний адрес предыдущего PSP.
+3Ch: байт - флаг, указывающий, что консоль находится в состоянии ввода 2-байтного символа.
+3Dh: байт - флаг, устанавливаемый функцией В711h прерывания 2Fh (при следующем вызове INT 21h для работы с файлом имя файла будет замечено на полное).
+3Eh: слово - не используется в DOS.
+40h: слово - версия DOS, которую вернет функция DOS 30h (DOS 5.0+).
+42h: 12 байт - не используется в DOS.
+50h: 2 байта - CDh 21h - команда INT 21h.
+54h: 7 байт - область для расширения первого FCB.
+5Ch: 16 байт - первый FCB, заполняемый из первого аргумента командной строки.
+6Ch: 16 байт - второй FCB, заполняемый из второго аргумента командной строки.
+7Ch: 4 байта - не используется в DOS.
+80h: 128 байт - командная строка и область DTA по умолчанию.
и записывает программу в память, начиная с адреса PSP:0100h. Если загружается ЕХЕ-программа, использующая дальние процедуры или сегменты данных, DOS модифицирует эти команды так, чтобы используемые в них сегментные адреса соответствовали сегментным адресам, которые получили эти процедуры и сегменты данных при загрузке программы в память. При запуске СОМ-программы регистры устанавливаются следующим образом:
AL = FFh, если первый параметр командной строки содержит неправильное имя диска (например, z:/something), иначе - 00h.
АН = FFh, если второй параметр содержит неправильное имя диска, иначе 00h.
CS = DS = ES = SS = сегментный адрес PSP.
SP = адрес последнего слова в сегменте (обычно FFFEh; меньше, если не хватает памяти).
При запуске ЕХЕ-программы регистры SS:SP устанавливаются в соответствии с сегментом стека, определенным в программе, затем в любом случае в стек помещается слово 0000h и выполняется переход на начало программы (PSP:0100h для СОМ, собственная точка входа для ЕХЕ).
Все эти действия выполняет одна функция DOS - загрузить и выполнить программу.
Функция DOS 4Bh - Загрузить и выполнить программу
| Ввод: |
АН = 4Bh
AL = 00h - загрузить и выполнить
AL = 01h - загрузить и не выполнять
DS:DX - адрес ASCIZ-строки с полным именем программы
ES:BX - адрес блока параметров ЕРВ:
+00h: слово - сегментный адрес окружения, которое будет скопировано для нового процесса (или 0, если используется текущее окружение)
+02h: 4 байта - адрес командной строки для нового процесса
+06h: 4 байта - адрес первого FCB для нового процесса
+0Ah: 4 байта - адрес второго FCB для нового процесса
+0Eh: 4 байта - здесь будет записан SS:SP нового процесса после его завершения (только для AL = 01)
+12h: 4 байта - здесь будет записан CS:IP (точка входа) нового процесса после его завершения (только для AL = 01)
AL = 03h - загрузить как оверлей
DS:DX - адрес ASCIZ-строки с полным именем программы
ES:BX - адрес блока параметров:
+00h: слово - сегментный адрес для загрузки оверлея
+02h: слово - число, которое будет использовано в командах, использующих непосредственные сегментные адреса, - обычно то же самое число, что и в предыдущем поле. 0 для СОМ-файлов
AL = 05h - подготовиться к выполнению (DOS 5.0+)
DS:DX - адрес следующей структуры:
+00h: слово - 00h
+02h: слово:бит 0 - программа - ЕХЕ бит 1 - программа - оверлей
+04h: 4 байта - адрес ASCIZ-строки с именем новой программы
+08h: слово - сегментный адрес PSP новой программы
+0Ah: 4 байта - точка входа новой программы
+0Eh: 4 байта - размер программы, включая PSP |
| Вывод: |
CF = 0, если операция выполнена, ВХ и DX модифицируются, CF = 1, если произошла ошибка, АХ = код ошибки (2 - файл не найден, 5 - доступ к файлу запрещен, 8 - не хватает памяти, 0Ah - неправильное окружение, 0Bh - неправильный формат) |
<
/p>
Для подфункций 00 и 01 требуется, чтобы было достаточно свободной памяти для загрузки программы, так что СОМ-программы должны воспользоваться функцией DOS 4Ah для уменьшения отведенного им блока памяти до минимально необходимого. При вызове подфункции 03 DOS загружает оверлей в память, выделенную текущим процессом, так что ЕХЕ-програмаш должны убедиться, что ее достаточно.
Эта функция игнорирует расширение файла и различает ЕХЕ- и СОМ-файлы по первым двум байтам заголовка ("MZ" для ЕХЕ-файлов).
Подфункция 05 должна вызываться после загрузки и перед передачей управления на программу, причем никакие прерывания DOS и BIOS нельзя вызывать после возвращения из этой подфункции и до перехода на точку входа новой программы.
Загруженной и вызванной таким образом программе предоставляется несколько способов завершения работы. Способ, который чаще всего применяется для СОМ-файлов, - команда RETN. При этом управление передается на адрес PSP:0000, где располагается код команды INT 20h. Соответственно можно завершить программу сразу, вызвав INT 20h, но оба эти способа требуют, чтобы CS содержал сегментный адрес PSP текущего процесса. Кроме того, они не позволяют вернуть код возврата, который может передать предыдущему процессу информацию о том, как завершилась запущенная программа. Рекомендованный способ завершения программы - функция DOS 4Ch.
Функция DOS 4Ch - Завершить программу
| Ввод: |
АН = 4Ch AL = код возврата |
Значение кода возврата можно использовать в пакетных файлах DOS как переменную ERRORLEVEL и определять из программы с помощью функции DOS 4Dh.
Функция DOS 4Dh - Определить код возврата последнего завершившегося процесса
| Ввод: |
АН = 4Dh |
| Вывод: |
АН = способ завершения:00h - нормальный 01h - Ctrl-Break 02h - критическая ошибка 03h - программа осталась в памяти как резидентнаяAL = код возврата CF = 0 |
Воспользуемся функциями 4Ah и 4Bh в следующем примере программы, которая ведет себя как командный интерпретатор, хотя на самом деле единственная команда, которую она обрабатывает, - команда exit. Все остальные команды передаются настоящему COMMAND.COM с ключом /С (выполнить команду и вернуться).
; shell.asm ; программа, выполняющая функции командного интерпретатора ; (вызывающая command.com для всех команд, кроме exit). .model tiny .code .186 org 100h ; СОМ-программа prompt_end equ "$" ; последний символ в приглашении ко вводу
start: mov sp,length_of_program+100h+200h ; перемещение стека на 200h ; после конца программы ; (дополнительные 100h - для PSP) mov ah,4Ah
stack_shift = length_of_program + 100h + 200h
mov bx,stack_shift shr 4+1 int 21h ; освободить всю память после конца ; программы и стека
; Заполнить поля ЕРВ, содержащие сегментные адреса mov ax,cs mov word ptr EPB+4,ax ; сегментный адрес командной строки mov word ptr EPB+8,ax ; сегментный адрес первого FCB mov word ptr EPB+0Ch,ax ; сегментный адрес второго FCB
main_loop:
; построение и вывод приглашения для ввода
mov ah,19h ; Функция DOS 19h int 21h ; определить текущий диск add al,'A' ; теперь AL = ASCII-код диска (А, В, С,) mov byte ptr drive_letter,al ; поместить его в строку mov ah,47h ; Функция DOS 47h mov dl,00 mov si,offset pwd_buffer int 21h ; определить текущий каталог mov al,0 ; найти ноль (конец текущего каталога) mov di,offset prompt_start ; в строке с приглашением mov cx,prompt_l repne scasb dec di ; DI - адрес байта с нулем mov dx,offset prompt_start ; DS:DX - строка приглашения sub di,dx ; DI - длина строки приглашения mov cx,di mov bx,1 ; stdout mov ah,40h int 21h ; вывод строки в файл или устройство mov al,prompt_end int 29h ; вывод последнего символа в приглашении
; получить команду от пользователя
mov ah,0Ah ; Функция DOS 0Ah mov dx,offset command_buffer int 21h ; буферированный ввод mov al,0Dh ; вывод символа CR int 29h mov al,0Ah ; вывод символа LF int 29h ; (CR и LF вместе - перевод строки) cmp byte ptr command_buffer+1,0 ; если введена пустая строка, je main_loop ; продолжить основной цикл
; проверить, является ли введенная команда командой "exit"
mov di,offset command_buffer+2 ; адрес введенной строки mov si,offset cmd_exit ; адрес эталонной строки "exit",0Dh mov ex,cmd_exit_l ; длина эталонной строки repe cmpsb ; сравнить строки jcxz got_exit ; если строки идентичны - выполнить exit
; передать остальные команды интерпретатору DOS (COMMAND.COM)
xor сх,сх mov si,offset command_buffer+2 ; адрес введенной строки mov di,offset command_text ; параметры для command.com mov cl,byte ptr command_buffer+1 ; размер введенной строки inc cl ; учесть 0Dh в конце rep movsb ; скопировать строку mov ax,4B00h ; функция DOS 4Bh mov dx,offset command_com ; адрес ASCIZ-строки с адресом mov bx,offset EPB int 21h ; исполнить программу jmp short main_loop ; продолжить основной цикл got_exit: int 20h ; выход из программы (ret нельзя, ; потому что мы перемещали стек)
cmd_exit db "exit",0Dh ; команда "exit" cmd_exit_l equ $-cmd_exit ; ее длина prompt_start db "tinyshell:" ; подсказка для ввода drive_letter db "С:" pwd_buffer db 64 dup (?) ; буфер для текущего каталога prompt_l equ $-prompt_start ; максимальная длина подсказки command_com db "С:\COMMAND.COM",0 ; имя файла EPB dw 0000 ; использовать текущее окружение dw offset commandline,0 ; адрес командной строки dw 005Ch,0,006Ch,0 ; адреса FCB, переданных DOS ; нашей программе при запуске ; (на самом деле они не используются) commandline db 125 ; максимальная длина командной строки db " /С" ; ключ /С для COMMAND.COM command_text db 122 dup (?) ; буфер для командной строки command_buffer db 122 ; здесь начинается буфер для ввода length_of_program equ 124+$-start ; длина программы + длина ; буфера для ввода end start
Для краткости в этом примере используются функции для работы с обычными короткими именами файлов. Достаточно заменить строку
mov ah,47h
на
mov ax,7147h
и увеличить размер буфера для текущего каталога (pwd_buffer) с 64 до 260 байт, чтобы каталоги с длинными именами отображались корректно в подсказке для ввода. Но для совместимости следует также добавить проверку на поддержку функции 71h (LFN) и определить размер буфера для каталога с помощью подфункции LFN A0h.
Закрытие и удаление файла
Функция DOS 3Eh - Закрыть файл
| Ввод: |
АН = 3Eh ВХ = идентификатор |
| Вывод: |
CF = 0, если не произошла ошибка CF = 1 и АХ = 6, если неправильный идентификатор |
Если файл был открыт для записи, все файловые буфера сбрасываются на диск, устанавливается время модификации файла и записывается его новая длина.
Функция DOS 41h - Удаление файла
| Ввод: |
АН = 41h DS:DX = адрес ASCIZ-строки с полным именем файла |
| Вывод: |
CF = 0, если файл удален CF = 1 и АН = 02h, если файл не найден, 03h - если путь не найден, 05h - если доступ запрещен |
Удалить файл можно только после того, как он будет закрыт, так как DOS будет продолжать выполнять запись в несуществующий файл, что может привести к разрушению файловой системы. Функция 41h не позволяет использовать маски (символы * и ? в имени файла) для удаления сразу нескольких файлов, хотя этого можно добиться, вызывая ее через недокументированную функцию 5D00h. Но, начиная с DOS 7.0 (Windows 95), официальная функция удаления файла может работать сразу с несколькими файлами.
Функция LFN 41h - Удаление файлов с длинным именем
| Ввод: |
АХ = 7141h DS:DX = адрес ASCIZ-строки с длинным именем файла SI = 0000h: маски не разрешены и атрибуты в СХ игнорируются SI = 0001h: маски в имени файла и атрибуты в СХ разрешены:CL = атрибуты, которые файлы могут иметь СН = атрибуты, которые файлы должны иметь |
| Вывод: |
CF = 0, если файл или файлы удалены CF = 1 и АХ = код ошибки, если произошла ошибка. Код 7100h означает, что функция не поддерживается |
Более сложные приемы программирования
Блочные устройства
Блочные устройства - это устройства, на которых 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 байт.
Передача параметров отложенным вычислением
Как и в предыдущем случае, здесь процедура получает адрес функции, вычисляющей значение параметра. Такой механизм удобен, если вычисление значения параметра требует много ресурсов или времени, например, если функция должна выбрать один из нескольких ходов при игре в шахматы, вычисление каждого параметра может занимать несколько минут. При передаче параметров отложенным вычислением функция получает адрес заглушки, которая при первом обращении к ней вычисляет значение параметра и сохраняет его во внутренней локальной переменной, а при дальнейших вызовах возвращает ранее вычисленное значение. Если процедуре вообще не потребуются значения части параметров (например, если первый же ход приводит к мату), то использование отложенных вычислений способствует значительному выигрышу. Этот механизм чаще всего применяется в системах искусственного интеллекта и операционных системах.
Рассказав об основных механизмах того, как передавать параметры процедуре, рассмотрим применяемые в ассемблере варианты, где их передавать.
Передача параметров по имени
Это механизм, который используют макроопределения, директива 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], определяет число остальных параметров). Но подробнее о тонкостях передачи параметров в стеке рассказано далее, а здесь приведен обзор методов.
Передача параметров
Процедуры могут получать или не получать параметры из вызывающей процедуры и могут возвращать или не возвращать результаты (процедуры, которые что-либо возвращают, называются функциями в языке Pascal, но ассемблер не делает каких-либо различий между ними).
Параметры можно передавать с помощью одного из шести механизмов:
по значению;
по ссылке;
по возвращаемому значению;
по результату;
по имени;
отложенным вычислением.
Параметры можно передавать в одном из пяти мест:
в регистрах;
в глобальных переменных;
в стеке;
в потоке кода;
в блоке параметров.
Так что всего в ассемблере возможно 30 различных способов передачи параметров для процедур. Рассмотрим их по порядку.
Перехват прерываний
В архитектуре процессоров 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 предоставляет хорошую поддержку для большинства стандартных функций, мы не будем рассматривать подробно программирование видеоадаптера на уровне портов, а только рассмотрим основные действия, для которых принято обращаться к видеоадаптеру напрямую.
Вложенные процедуры с дисплеями
Вместо того чтобы передавать адрес только одной вышестоящей активационной записи, процедурам можно передавать набор адресов сразу для всех уровней вложенности - от нулевого до непосредственно вышестоящего. При этом доступ к любой нелокальной процедуре сводится всего к двум командам, а перед вызовом процедуры вообще не требуется каких-либо дополнительных действий (так как вызываемая процедура поддерживает дисплей самостоятельно).
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, не в стек, а в сегмент данных, но тем не менее существует способ, открывающий быстрый доступ к локальным переменным с любых уровней.
Вложенные процедуры
Во многих языках программирования можно описывать процедуры внутри друг друга, так что локальные переменные, объявленные в пределах одной процедуры, доступны только из этой процедуры и всех вложенных в нее. Разные языки программирования используют разные способы реализации доступа к переменным, объявленным в функциях с меньшим уровнем вложенности (уровень вложенности главной процедуры определяют как 0 и увеличивают на 1 с каждым новым вложением).
Внешние регистры контроллера 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.
Программирование в защищенном режиме
Адресация в защищенном режиме
Прежде чем познакомиться с программированием в защищенном режиме, рассмотрим механизм адресации, применяющийся в нем. Так же как и в реальном режиме, адрес складывается из адреса начала сегмента и относительного смещения, но если в реальном режиме адрес начала сегмента просто лежал в соответствующем сегментном регистре, деленый на 16, то в защищенном режиме не все так просто. В сегментных регистрах находятся специальные 16-битные структуры, называемые селекторами и имеющие следующий вид:
биты 15 – 3: номер дескриптора в таблице
бит 2: индикатор таблицы 0/1 - использовать GDT/LDT
биты 1 – 0: уровень привилегий запроса (RPL)
Уровень привилегий запроса - это число от 0 до 3, указывающее уровень защиты сегмента, для доступа к которому используется данный селектор. Если программа имеет более высокий уровень привилегий, при использовании этого сегмента привилегии понизятся до RPL. Уровни привилегий и весь механизм защиты в защищенном режиме нам пока не потребуется.
GDT и LDT - таблицы глобальных и локальных дескрипторов соответственно. Это таблицы восьмибайтных структур, называемых дескрипторами сегментов, в которых и находится начальный адрес сегмента вместе с другой важной информацией:
слово 3 (старшее):
биты 15 – 8: биты 31 – 24 базы
бит 7: бит гранулярности (0 - лимит в байтах, 1 - лимит в 4-килобайтных единицах)
бит 6: бит разрядности (0/1 - 16-битный/32-битный сегмент)
бит 5: 0
бит 4: зарезервировано для операционной системы
биты 3 – 0: биты 19 – 16 лимита
слово 2:
бит 15: бит присутствия сегмента
биты 14 – 13: уровень привилегий дескриптора (DPL)
бит 12: тип дескриптора (0 - системный, 1 - обычный)
биты 11 – 8: тип сегмента
биты 7 – 0: биты 23 – 16 базы
слово 1: биты 15 – 0 базы
слово 0 (младшее): биты 15 – 0 лимита
Два основных поля в этой структуре, которые нам интересны, - это база и лимит сегмента. База представляет линейный 32-битный адрес начала сегмента, а лимит - это 20-битное число, которое равно размеру сегмента в байтах (от 1 байта до 1 мегабайта), если бит гранулярности сброшен в ноль, или в единицах по 4096 байт (от 4 Кб до 4 Гб), если он установлен в 1. Числа отсчитываются от нуля, так что лимит 0 соответствует сегменту длиной 1 байт, точно так же, как база 0 соответствует первому байту памяти.
Остальные элементы дескриптора выполняют следующие функции:
Бит разрядности: для сегмента кода этот бит указывает на разрядность операндов и адресов по умолчанию. То есть в сегменте с этим битом, установленным в 1, все команды будут интерпретироваться как 32-битные, а префиксы изменения разрядности адреса или операнда будут превращать их в 16-битные, и наоборот. Для сегментов данных этот бит управляет тем, какой регистр (SP или ESP) используют команды, работающие с этим сегментом данных как со стеком.
Поле DPL определяет уровень привилегий сегмента.
Бит присутствия указывает, что сегмент реально есть в памяти. Операционная система может выгрузить содержимое сегмента из памяти на диск и сбросить этот бит, а когда программа попытается к нему обратиться, произойдет исключение, обработчик которого снова загрузит содержимое этого сегмента в память.
Бит типа дескриптора - если он равен 1, сегмент является обычным сегментом кода или данных. Если этот бит - 0, дескриптор является одним из 16 возможных видов, определяемых полем типа сегмента.
Тип сегмента: для системных регистров в этом поле находится число от 0 до 15, определяющее тип сегментов (LDT, TSS, различные шлюзы), которые рассмотрены в главе 9. Для обычных сегментов кода и данных эти четыре бита выполняют следующие функции:
бит 11: 0 - сегмент данных, 1 - сегмент кода
бит 10: для данных - бит направления роста сегмента
для кода - бит подчинения
бит 9: для данных - бит разрешения записи
для кода - бит разрешения чтения
бит 8: бит обращения
Бит обращения устанавливается в 1 при загрузке селектора этого сегмента в регистр.
Бит разрешения чтения/записи выбирает разрешаемые операции с сегментом - для сегмента кода это могут быть выполнение или выполнение/чтение, а для сегмента данных - чтение или чтение/запись.
Бит подчинения указывает, что данный сегмент кода является подчиненным. Это значит, что программа с низким уровнем привилегий может передать управление в этот сегмент и текущий уровень привилегий не изменится.
Бит направления роста сегмента обращает смысл лимита сегмента. В сегментах с этим битом, сброшенным в ноль, допустимы все смещения от 0 до лимита, а если этот бит - 1, то допустимы все смещения, кроме смещений от 0 до лимита. Про такой сегмент говорят, что он растет сверху вниз, так как если лимит, например, равен –100, допустимы смещения от –100 до 0, а если лимит увеличить, станут допустимыми еще меньшие смещения.
Для обычных задач программирования нам не потребуется все многообразие возможностей адресации. Все, что нам нужно, - это удобный неограниченный доступ к памяти. Поэтому мы будем рассматривать простую модель памяти - так называемую модель flat, в которой базы всех регистров установлены в ноль, а лимиты - в 4 Гб. Именно в такой ситуации окажется, что можно забыть о сегментации и пользоваться только 32-битными смещениями.
Для создания flat-памяти нам потребуются два дескриптора с нулевой базой и максимальным лимитом - один для кода и один для данных.
Дескриптор кода:
лимит FFFFFh
база 000000000h
тип сегмента FAh
бит присутствия = 1
уровень привилегий = 3 (минимальный)
бит типа дескриптора = 1
тип сегмента: 1010b (сегмент кода, для выполнения/чтения)
бит гранулярности = 1
бит разрядности = 1
db 0FFh, 0FFh, 0h, 0h, 0h, 0FAh, 0CFh, 0h
Дескриптор данных:
лимит FFFFFh
база 00000000h
бит присутствия = 1
уровень привилегий = 3 (минимальный)
бит типа дескриптора = 1
тип сегмента: 0010b (сегмент данных, растет вверх, для чтения/записи)
бит гранулярности = 1
бит разрядности = 1
db 0FFh, 0FFh, 0h, 0h, 0h, 0F2h, 0CFh, 0h
Для того чтобы процессор знал, где искать дескрипторы, операционная система собирает их в таблицы, которые называются GDT (таблица глобальных дескрипторов - может быть только одна) и LDT (таблица локальных дескрипторов - по одной на каждую задачу), и загружает их при помощи привилегированных команд процессора. Так как мы пока не собираемся создавать операционные системы, нам потребуется только подготовить дескриптор и вызвать соответствующую функцию VCPI или DPMI.
Заметим также, что адрес, который получается при суммировании базы сегмента и смещения, называется линейным адресом и может не совпадать с физическим, если дело происходит в операционной системе, реализующей виртуальную память при помощи специально предусмотренного в процессорах Intel страничного механизма виртуализации памяти.
Функции DPMI управления дескрипторами
INT 31h, AX = 0 - Выделить локальные дескрипторы
| Ввод: |
АХ = 0 СХ = количество необходимых дескрипторов |
| Вывод: |
если CF = 0, АХ = селектор для первого из заказанных дескрипторов |
Эта функция только выделяет место в таблице LDT, создавая в ней дескриптор сегмента данных с нулевыми базой и лимитом, так что пользоваться им пока нельзя.
INT 31h, AX = 1 - Удалить локальный дескриптор
| Ввод: |
АХ = 1 ВХ = селектор |
| Вывод: |
CF = 0, если не было ошибки |
Эта функция действует на дескрипторы, созданные при переключении в защищенный режим, и на дескрипторы, созданные функцией 0, но не на дескрипторы, созданные функцией 2.
INT 31h, АХ = 2 - Преобразовать сегмент в дескриптор
| Ввод: |
АХ = 2 ВХ = сегментный адрес (A000h - для видеопамяти, 0040h - для данных BIOS) |
| Вывод: |
если CF = 0, АХ = готовый селектор на сегмент, начинающийся с указанного адреса, и с лимитом 64 Кб |
Так, программы в защищенном режиме могут обращаться к различным областям памяти ниже границы 1 Мб, например для прямого вывода на экран.
INT 31h, AX = 6 - Определить базу сегмента
| Ввод: |
АХ = 6 ВХ = селектор |
| Вывод: |
если CF = 0, CX:DX = 32-битный линейный адрес начала сегмента |
INT 31h, AX = 7 - Сменить базу сегмента
| Ввод: |
АХ = 7 ВХ = селектор CX:DX = 32-битная база |
| Вывод: |
CF = 0, если не было ошибок |
INT 31h, AX = 8 - Сменить лимит сегмента
| Ввод: |
АХ = 8 ВХ = селектор CX:DX = 32-битный лимит (длина сегмента минус 1) |
| Вывод: |
CF = 0, если не было ошибок (чтобы определить лимит сегмента, можно пользоваться командой LSL) |
INT 31h, AX = 9 - Сменить права доступа сегмента
| Ввод: |
АХ = 9 ВХ = селектор CL = права доступа/тип (биты 15 – 8 слова 2 дескриптора) СН = дополнительные права (биты 7 – 4 соответствуют битам 7 – 4 слова 3 дескриптора, биты 3 – 0 игнорируются) |
| Вывод: |
CF = 0, если не было ошибок (чтобы определить права доступа сегмента, можно пользоваться командой LAR) |
<
/p>
INT 31h, АХ = 0Ah - Создать копию дескриптора
| Ввод: |
АХ = 000Ah ВХ = селектор (сегмента кода или данных) |
| Вывод: |
если CF = 0, АХ = селектор на сегмент данных с теми же базой и лимитом |
INT 31h, AX = 0Bh - Прочитать дескриптор
| Ввод: |
АХ = 000Bh ВХ = селектор ES:EDI = селектор:смещение 8-байтного буфера |
| Вывод: |
если CF = 0, в буфер помещен дескриптор |
INT 31h, AX = 0Ch - Загрузить дескриптор
| Ввод: |
АХ = 000Ch ВХ = селектор ES:EDI = адрес 8-байтного дескриптора |
| Вывод: |
CF = 0, если не было ошибок |
INT 31h, AX = 0Dh - Выделить определенный дескриптор
| Ввод: |
АХ = 000Dh ВХ = селектор на один из первых 16 дескрипторов (значения селектора 04h – 7Ch) |
| Вывод: |
CF = 0, если нет ошибок (CF = 1 и АХ = 8011h, если этот дескриптор занят) |
Этого набора функций, а точнее пары функций 00 и 0Ch, достаточно для того, чтобы полностью настроить режим flat (или любой другой) после переключения в защищенный режим. Но прежде чем это осуществить, нам надо познакомиться с тем, как в DMPI сделан вызов обработчиков прерываний реального режима, иначе наша программа просто не сможет себя проявить.
Интерфейс DPMI
Спецификация DPMI создана в 1990 – 1991 годах и представляет собой развитую систему сервисов, позволяющих программам переключаться в защищенный режим, вызывать обработчики прерываний BIOS и DOS, передавать управление другим процедурам, работающим в реальном режиме, устанавливать обработчики аппаратных и программных прерываний и исключений, работать с памятью с разделяемым доступом, устанавливать точки останова и т.д. Операционные системы, такие как Windows 95 или, например, Linux, предоставляют DPMI-интерфейс для программ, запускаемый в DOS-задачах; многочисленные расширители DOS, о которых говорится в следующей главе, предоставляют DPMI для DOS-программ, запускаемых в любых условиях, так что сейчас можно считать DPMI основным интерфейсом, на который следует ориентироваться при программировании 32-битных приложений для DOS.
Интерфейс VCPI
Спецификация этого интерфейса была создана в 1989 году, вскоре после появления процессора 80386, компаниями Phar Lap Software и Quaterdeck Office Systems. Программа, пользующаяся VCPI для переключения в защищенный режим, должна поддерживать полный набор системных таблиц - GDT, LDT, IDT, таблицы страниц и т.д., то есть фактически VCPI-сервер обеспечивает следующее - процессор находится в защищенном режиме, и различные программы, пользующиеся им, не будут конфликтовать между собой.
VCPI является своего рода расширением интерфейса EMS, и все обращения к нему выполняются при помощи прерывания EMS, INT 67h с АН = 0DEh и кодом подфункции VCPI в AL.
INT 67h АХ = DE00h - Проверка наличия VCPI
| Ввод: |
AX = 0DE00h |
| Вывод: |
АН = 00, если VCPI есть ВН, BL - версия (старшая, младшая цифры) |
INT 67h АХ = DE01h - Получить точку входа VCPI
| Ввод: |
AX = 0DE01h ES:DI = адрес 4-килобайтного буфера для таблицы страницDS:SI = адрес таблицы дескрипторов |
| Вывод: |
АН = 0, если нет ошибок DS:SI - первые три дескриптора заполняются дескрипторами VCPI-сервера ES:SI - адрес первой не используемой сервером записи в таблице страниц ЕВХ - адрес точки входа VCPI относительно сегмента, дескриптор которого лежит первым в таблице DS:SI. Можно делать far call из 32-битного защищенного режима на этот адрес с АХ = DE00h – DE05h, чтобы пользоваться функциями VCPI из защищенного режима |
INT 67h AX = DE0Ch - VCPI: переключиться в защищенный режим (для вызова из V86)
| Ввод: |
AX = 0DE0Ch ESI = линейный адрес таблицы со значениями для системных регистров (в первом мегабайте)
+00h: 4 байта - новое значение CR3
+04h: 4 байта - адрес 6-байтного значения для GDTR
+08h: 4 байта - адрес 6-байтного значения для IDTR
+0Ch: 2 байта - LDTR
+0Eh: 2 байта - TR
+10h: 6 байт - CS:EIP - адрес точки входа
прерывания должны быть запрещены |
| Вывод: |
Загружаются регистры GDTR, IDTR, LDTR, TR. Теряются регистры ЕАХ, ESI, DS, ES, FS, GS. SS:ESP указывает на стек размером в 16 байт, и его надо изменить, прежде чем снова разрешать прерывания |
<
/p>
Точка входа VCPI, АХ = DE0Ch - Переключиться в режим V86 (для вызова из РМ)
| Ввод: |
Перед передачей управления командой call в стек надо поместить регистры в следующем порядке (все значения - двойные слова): GS, FS, DS, ES, SS, ESP, 0, CS, EIP. Прерывания должны быть запрещены |
| Вывод: |
Сегментные регистры загружаются, значение ЕАХ не определено, прерывания запрещены |
Остальные функции VCPI:
INT 67h AX = DE02h - Определить максимальный физический адрес
| Ввод: |
АХ = 0DE02h |
| Вывод: |
АН = 0, если нет ошибок EDX = физический адрес самой старшей 4-килобайтной страницы, которую можно выделить |
INT 67h AX = DE03h - Определить число свободных страниц
| Ввод: |
АХ = 0DE03h |
| Вывод: |
АН = 0, если нет ошибок EDX = число свободных 4-килобайтных страниц для всех задач |
INT 67h AX = DE04h - Выделить 4-килобайтную страницу (обязательно надо вызвать DE05h)
| Ввод: |
АХ = 0DE04h |
| Вывод: |
АН = 0, если нет ошибок EDX = физический адрес выделенной страницы |
INT 67h AX = DE05h - Освободить 4-килобайтную страницу
| Ввод: |
АХ = 0DE05h EDX = физический адрес страницы |
| Вывод: |
АН = 0, если нет ошибок |
INT 67h AX = DE06h - Определить физический адрес 4-килобайтной страницы в первом мегабайте
| Ввод: |
АХ = 0DE06h СХ = линейный адрес страницы, сдвинутый вправо на 12 бит |
| Вывод: |
АН = 0, если нет ошибок EDX = физический адрес страницы |
INT 67h AX = DE07h - Прочитать регистр CR0
| Ввод: |
АХ = 0DE07h |
| Вывод: |
АН = 0, если нет ошибок ЕВХ = содержимое регистра CR0 |
INT 67h АХ = DE08h - Прочитать регистры DR0 – DR7
| Ввод: |
АХ = 0DE08h ES:DI = буфер на 8 двойных слов |
| Вывод: |
АН = 0, если нет ошибок, в буфер не записываются DR4 и DR5 |
INT 67h AX = DE09h - Записать регистры DR0 – DR7
| Ввод: |
АХ = 0DE09h ES:DI = буфер на 8 двойных слов с новыми значениями для регистров |
| Вывод: |
АН = 0, если нет ошибок (DR4 и DR5 не записываются) |
<
/p>
INT 67h AX = DE0Ah - Определить отображение аппаратных прерываний
| Ввод: |
АХ = 0DE0Ah |
| Вывод: |
АН = 0, если нет ошибок ВХ = номер обработчика для IRQ0 СХ = номер обработчика для IRQ8 |
INT 67h AX = DE0Bh - Сообщить VCPI- серверу новое отображение аппаратных прерываний (вызывается после перепрограммирования контроллера прерываний)
| Ввод: |
АХ = 0DE0Bh ВХ = номер обработчика для IRQ0 СХ = номер обработчика для IRQ8 |
| Вывод: |
АН = 0, если нет ошибок |
Итак, чтобы использовать защищенный режим с VCPI, фактически надо уметь программировать его самостоятельно. Например, чтобы вызвать прерывание DOS или BIOS, нам пришлось бы переключаться в режим V86, вызывать прерывание и затем возвращаться обратно. Естественно, этот интерфейс не получил широкого развития и был практически повсеместно вытеснен более удобным DPMI.
Обработчики прерываний
Прежде чем мы рассмотрим первый пример программы, использующей DPMI, остановимся еще на одной группе его функций - операции с обработчиками прерываний. Когда происходит прерывание или исключение, управление передается сначала по цепочке обработчиков прерываний в защищенном режиме, последний обработчик - стандартный обработчик DPMI - переходит в режим V86, а затем управление проходит по цепочке обработчиков прерывания в реальном режиме (в реальном режиме обработчики прерываний и исключений совпадают).
INT 31h, AX = 0200h - Определить адрес реального обработчика прерывания
| Ввод: |
АХ = 0200h BL = номер прерывания |
| Вывод: |
CF = 0 всегда, CX:DX - сегмент:смещение обработчика прерывания в реальном режиме |
INT 31h, АХ = 0201h - Установить реальный обработчик прерывания
| Ввод: |
АХ = 0201h BL = номер прерывания CX:DX = сегмент:смещение обработчика прерывания в реальном режиме |
| Вывод: |
CF = 0 всегда |
INT 31h, АХ = 0204h - Определить адрес защищенного обработчика прерывания
| Ввод: |
АХ = 0204h BL = номер прерывания |
| Вывод: |
CF = 0 всегда, CX:EDX = селектор:смещение обработчика |
INT 31h, АХ = 0205h - Установить защищенный обработчик прерывания
| Ввод: |
АХ = 0205h BL = номер прерывания CX:EDX = селектор:смещение обработчика |
| Вывод: |
CF = 0 |
INT 31h, АХ = 0202h - Определить адрес обработчика исключения
| Ввод: |
АХ = 0202h BL = номер исключения (00h – 1Fh) |
| Вывод: |
если CF = 0, CX:EDX = селектор:смещение обработчика исключения |
INT 31h, АХ = 0203h - Установить обработчик исключения
| Ввод: |
АХ = 0203h BL = номер исключения (00h – 1Fh) CX:EDX = селектор:смещение обработчика исключения |
| Вывод: |
CF = 0, если не было ошибок |
Если обработчик исключения передает управление дальше по цепочке на стандартный обработчик DPMI-сервера, следует помнить, что только исключения 0, 1, 2, 3, 4, 5 и 7 передаются обработчикам из реального режима, а остальные исключения приводят к прекращению работы программы.
Передача управления между режимами в DPMI
Вызов любого программного прерывания, кроме INT 31h и INT 21h/АН = 4Ch (функция DPMI: завершение программы), приводит к тому, что DPMI-сервер переключается в режим V86 и вызывает это же самое прерывание, скопировав все регистры, кроме сегментных регистров и стека (состояние сегментных регистров не определено, а стек предоставляет сам DPMI-сервер). После того как обработчик прерывания возвратит управление, DPMI-сервер возвратиться в защищенный режим и вернет управление программе. Таким способом можно вызывать все прерывания, передающие параметры только в регистрах, например проверку нажатия клавиши или вывод символа на экран. Чтобы вызвать прерывание, использующее сегментные регистры, например вывод строки на экран, а также в других ситуациях, требующих вызова процедуры, работающей в другом режиме, применяются следующие функции.
INT 31h, AX = 0300h - Вызов прерывания в реальном режиме
| Ввод: |
АХ = 0300h ВН = 0, BL = номер прерывания СХ = число слов из стека защищенного режима, которое будет скопировано в стек реального режима и обратно ES:EDI = селектор:смещение структуры регистров v86_regs (см. ниже) |
| Вывод: |
если CF = 0, структура в ES:EDI модифицируется |
Значения регистров CS и IP в структуре v86_regs игнорируются. Вызываемый обработчик прерывания должен восстанавливать стек в исходное состояние (например, INT 25h и INT 26h этого не делают).
INT 31h, АХ = 0301Н Вызов дальней процедуры в реальном режиме
| Ввод: |
АХ = 0301h ВН = 0 СХ = число слов из стека защищенного режима, которое будет скопировано в стек реального режима и обратно ES:EDI = селектор:смещение структуры регистров v86_regs (см. ниже) |
| Вывод: |
если CF = 0, структура в ES:EDI модифицируется |
Вызываемая процедура должна заканчиваться командой RETF.
INT 31h, AX = 0302h Вызов обработчика прерывания в реальном режиме
| Ввод: |
АХ = 0302h ВН = 0 СХ = число слов из стека защищенного режима, которое будет скопировано в стек реального режима и обратно ES:EDI = селектор:смещение структуры регистров v86_regs (см. ниже) |
| Вывод: |
если CF = 0, структура в ES:EDI модифицируется |
<
/p>
Вызываемая процедура должна заканчиваться командой IRET.
Эти три функции используют следующую структуру данных для передачи значений регистров в реальный режим и обратно:
+00h: 4 байта - EDI
+04h: 4 байта - ESI
+08h: 4 байта - ЕВР
+0Ch: 4 байта - игнорируются
+10h: 4 байта - ЕВХ
+14h: 4 байта - EDX
+18h: 4 байта - ЕСХ
+1Сh: 4 байта - ЕАХ
+20h: 2 байта - FLAGS
+22h: 2 байта - ES
+24h: 2 байта - DS
+26h: 2 байта - FS
+28h: 2 байта - GS
+2Ah: 2 байта - IP
+2Ch: 2 байта - CS
+2Eh: 2 байта - SP
+30h: 2 байта - SS
Значения SS и SP могут быть нулевыми, тогда DPMI-сервер сам предоставит стек для работы прерывания.
Кроме этих трех функций, которые передают управление процедурам в реальном режиме из защищенного, существует механизм, позволяющий делать в точности обратное действие - передавать управление процедурам в защищенном режиме из реального.
INT 31h, АХ = 0303h - Выделить точку входа для вызова из реального режима
| Ввод: |
АХ = 0303h DS:ESI = селектор:смещение процедуры в защищенном режиме (заканчивающейся IRET), которую будут вызывать из реального режима ES:EDI = селектор:смещение структуры v86_regs, которая будет использоваться для передачи регистров |
| Вывод: |
если CF = 0, CX:DX = сегмент:смещение точки входа |
При передаче управления в такую процедуру DS:ESI указывает на стек реального режима, ES:EDI - на структуру v86_regs, SS:ESP - на стек, предоставленный DPMI-сервером, и остальные регистры не определены.
Количество точек входа, которыми располагает DPMI-сервер, ограничено, и неиспользуемые точки входа должны быть удалены при помощи следующей функции DPMI.
INT 31h, AX = 0304h - Освободить точку входа для вызова из реального режима
| Ввод: |
АХ = 0304h CX:DX = сегмент:смещение точки входа |
| Вывод: |
CF = 0, если точка входа удалена |
Переключение в защищенный режим
Все основные сервисы DPMI доступны только в защищенном режиме через прерывание INT 31h, так что переключение режимов - первое, что должна сделать программа.
INT 2Fh AX = 1687h - Функция DPMI: получить точку входа в защищенный режим
| Ввод: |
АХ = 1687h |
| Вывод: |
АХ = 0, если DPMI присутствует ВХ: бит 0 = 1, если поддерживаются 32-битные программы, 0 - если нет CL: тип процессора (02 - 80286, 03 - 80386 и т.д.) DH:DL - версия DPMI в двоичном виде (обычно 00:90 (00:5Ah) или 01:00) SI = размер временной области данных, требуемой для переключения в 16-байтных параграфах ES:DI = адрес процедуры переключения в защищенный режим |
Вызвав эту функцию, программа должна выделить область памяти размером SI * 16 байт и выполнить дальний CALL на указанный адрес. Единственные входные параметры - регистр ES, который должен содержать сегментный адрес области данных для DPMI и бит 0 регистра АХ. Если этот бит 1 - программа собирается стать 32-битным приложением, а если 0 - 16-битным. Если по возвращении из процедуры установлен флаг CF, переключения не произошло и программа все еще в реальном режиме. Если CF = 0, программа переключилась в защищенный режим и в сегментные регистры загружены следующие селекторы:
CS: 16-битный селектор с базой, совпадающей со старым CS, и лимитом 64 Кб
DS: селектор с базой, совпадающей со старым DS, и лимитом 64 Кб
SS: селектор с базой, совпадающей со старым SS, и лимитом 64 Кб
ES: селектор с базой, совпадающей с началом блока PSP, и лимитом 100h
FS и GS = 0
Разрядность сегментов данных определяется заявленной разрядностью программы. Остальные регистры сохраняются, а для 32-битных программ старшее слово ESP обнуляется. По адресу ES:[002Ch] записывается селектор сегмента, содержащего переменные окружения DOS.
После того как произошло переключение, можно загрузить собственные дескрипторы для сегментов кода и данных и перейти в режим с моделью памяти flat.
Перечислим теперь основные функции, предоставляемые DPMI для работы с дескрипторами и селекторами.
Пример программы
Теперь воспользуемся интерфейсом DPMI для переключения в защищенный режим и вывода строки на экран. Этот пример будет работать только в системах, предоставляющих DPMI для запускаемых в них DOS-программ, например в Windows 95.
; dpmiex.asm ; Выполняет переключение в защищенный режим средствами DPMI .386 ; 32-битный защищенный режим появился в 80386 ; 16-битный сегмент - в нем выполняется подготовка и переход в защищенный режим RM_seg segment byte public use16 assume cs:RM_seg, ds:PM_seg, ss:RM_stack
; точка входа программы RM_entry: ; проверить наличие DPMI mov ax,1687h ; номер 1678h int 2Fh ; прерывание мультиплексора, test ax,ax ; если АХ не ноль, jnz DPMI_error ; произошла ошибка (DPMI отсутствует), test bl,1 ; если не поддерживается 32-битный режим, jz DPMI_error ; нам тоже нечего делать
; подготовить базовые адреса для будущих дескрипторов mov eax,PM_seg mov ds,ax ; DS - сегментный адрес PM_seg shl eax,4 ; EAX - линейный адрес начала сегмента PM_seg mov dword ptr PM_seg_addr,eax or dword ptr GDT_flatCS+2,eax ; дескриптор для CS or dword ptr GDT_flatDS+2,eax ; дескриптор для DS
; сохранить адрес процедуры переключения DPMI mov word ptr DPMI_ModeSwitch,di mov word ptr DPMI_ModeSwitch+2,es
; ES должен указывать на область данных для переключения режимов, ; у нас она будет совпадать с началом будущего 32-битного стека add eax,offset DPMI_data ; добавить к EAX смещение shr eax,4 ; и превратить в сегментный адрес inc ax mov es,ax
; перейти в защищенный режим mov ах, 1 ; АХ = 1 - мы будем 32 приложением ifdef _WASM_ db 67h ; поправка для wasm endif call dword ptr DPMI_ModeSwitch jc DPMI_error ; если переключения не произошло - выйти
; теперь мы находимся в защищенном режиме, но лимиты всех сегментов ; установлены на 64 Кб, а разрядности сегментов - на 16 бит. ; Нам надо подготовить два ; 32-битных селектора с лимитом 4 Гб - один для кода и один для данных push ds pop es ; ES вообще был сегментом PSP с лимитом 100h mov edi,offset GDT ; EDI - адрес таблицы GDT ; цикл по всем дескрипторам в нашей GDT, mov edx,1 ; которых всего два (0, 1) sel_loop: xor ах,ах ; функция DPMI 00 mov cx,1 int 31h ; создать локальный дескриптор mov word ptr selectors[edx*2],ax ; сохранить селектор mov bx,ax ; в таблицу selectors mov ax,000Ch ; функция DPMI OCh int 31h ; установить селектор add di,8 ; EDI - следующий дескриптор dec dx jns sel_loop
; загрузить селектор сегмента кода в CS при помощи команды RETF push dword ptr Sel_flatCS ; селектор для CS ifdef _WASM_ db 066h endif push offset PM_entry ; EIP db 066h ; префикс размера операнда retf ; выполнить переход в 32-битный сегмент
; сюда передается управление, если произошла ошибка при инициализации DPMI ; (обычно, если DPMI просто нет) DPMI_error: push cs pop ds mov dx,offset nodpmi_msg mov ah,9h ; вывод строки на экран int 21h mov ah,4Ch ; конец ЕХЕ-программы int 21h nodpmi_msg db "Ошибка DPMI$" RM_seg ends
; сегмент PM_seg содержит код, данные и стек для защищенного режима PM_seg segment byte public use32 assume cs:PM_seg,ds:PM_seg,ss:PM_seg ; таблица дескрипторов GDT label byte ; дескриптор для CS GDT_flatCS db 0FFh,0FFh,0h,0h,0h,0FAh,0CFh,0h ; дескриптор для DS GDT_flatDS db 0FFh,0FFh,0h,0h,0h,0F2h,0CFh,0h
; точка входа в 32-битный режим - загружен только CS PM_entry: mov ax,word ptr Sel_flatDS ; селектор для данных mov ds,ax ; в DS mov es,ax ; в ES mov ss,ax ; и в SS mov esp,offset PM_stack_bottom ; и установить стек ; отсюда начинается текст собственно программы, ; программа работает в модели памяти flat с ненулевой базой, ; база CS, DS, ES и SS совпадает и равна линейному адресу начала PM_seg ; все лимиты - 4 Гб mov ах,0300h ; функция DPMI 0300h mov bx,0021h ; прерывание DOS 21h xor есх,есх ; стек не копировать mov edi,offset v86_regs ; ES:EDI - адрес v86_regs int 31h ; вызвать прерывание
mov ah,4Ch ; Это единственный способ int 21h ; правильно завершить DPMI-программу
hello_msg db "Hello world из 32-битного защищенного режима!$"
v86_regs: ; значения регистров для функции DPMI ОЗООп dd 0,0,0,0,0 ; EDI, ESI, EBP, 0, ЕВХ v_86_edx dd offset hello_msg ; EDX dd 0 ; ЕСХ v86_eax dd 0900h ; EAX (AH = 09h, вывод строки на экран) dw 0,0 ; FLAGS, ES v86_ds dw PM_seg ; DS dw 0,0,0,0,0,0 ; FS, GS, 0, 0, SP, SS
; различные временные переменные, нужные для переключения режимов DPMI_ModeSwitch dd ? ; точка входа DPMI PM_seg_addr dd ? ; линейный адрес сегмента PM_seg
; значения селекторов selectors: Sel_flatDS dw ? Sel_flatCS dw ?
; стек для нашей 32-битной программы DPMI_data: ; и временная область данных DPMI одновременно db 16384 dup (?) PM_stack_bottom: PM_seg ends
; стек 16-битной программы, который использует DPMI-сервер при переключении режимов ; Windows 95 требует 16 байт ; CWSDPMI требует 32 байта ; QDPMI требует 96 байт ; мы выберем по максимуму RM_stack segment byte stack "stack" use16 db 96 dup (?) RM_stack ends end RM_entry ; точка входа для DOS - RM_entry
Несмотря на то что DPMI разрешает пользоваться многими прерываниями напрямую и всеми через функцию 0300h, он все равно требует некоторой подготовки для переключения режимов. Кроме того, программа, использующая DPMI для переключения режимов, должна сочетать в себе 16-битный и 32-битный сегменты, что неудобно с точки зрения практического программирования. На самом деле для написания приложений, идущих в защищенном режиме под DOS, никто не применяет переключение режимов вообще - это делают специальные программы, называющиеся расширителями DOS.
Программирование в защищенном режиме
Все, о чем рассказано до этой главы, рассчитано на работу под управлением DOS в реальном режиме процессора (или в режиме V86), унаследованном еще с семидесятых годов. В этом режиме процессор неспособен адресоваться к памяти выше границы первого мегабайта. Кроме того, из-за того, что для адресации используются 16-битные смещения, невозможно работать с массивами больше 65 536 байт. Защищенный режим лишен этих недостатков, в нем можно адресоваться к участку памяти размером 4 Гб как к одному непрерывному массиву и вообще забыть о сегментах и смещениях. Этот режим намного сложнее реального, поэтому, чтобы переключить в него процессор и поддерживать работу в этом режиме, надо написать небольшую операционную систему. Кроме того, если процессор уже находится под управлением какой-то операционной системы, которая перевела его в защищенный режим, например Windows 95, она, скорее всего, не разрешит программе устранить себя от управления компьютером. С этой целью были разработаны специальные интерфейсы, позволяющие программам, запущенным в режиме V86 в DOS, переключаться в защищенный режим простым вызовом соответствующего прерывания - VCPI и DPMI.
Расширители DOS
Расширитель DOS (DOS Extender) - это средство разработки (программа, набор программ, часть компоновщика или просто объектный файл, с которым нужно компоновать свою программу), позволяющее создавать 32-битные приложения, запускающиеся в защищенном режиме с моделью памяти flat и с работающими функциями DPMI. Расширитель сам выполняет переключение в защищенный режим, причем, если в системе уже присутствует DPMI, VCPI или другие средства переключения в защищенный режим из V86, он пользуется ими, а если программа запускается в настоящем реальном режиме, DOS-расширитель сам переводит процессор в защищенный режим и выполняет все необходимые действия для поддержки его работы. Кроме полного или частичного набора функций DPMI расширители DOS обычно поддерживают некоторые функции прерывания 21h, за что и получили свое название. В частности, практически всегда поддерживается функция DOS 09h вывода строки на экран: в DS:EDX помещают селектор:смещение начала строки, и расширитель это правильно интерпретирует (многие DPMI-серверы, включая встроенный сервер Windows 95, тоже эмулируют эту функцию DOS).
Так как расширитель должен первым получить управление при старте программы, для того чтобы выполнить переключение режимов, код расширителя нужно объединить с нашей программой на стадии компиляции или компоновки.
Способы объединения программы с расширителем
Первые популярные DOS-расширители, такие как Start32, Raw32, System64, 386Power, PMODE и другие, распространяются в виде исходных текстов (Start32 и PMODE оказали решающее влияние на развитие DOS-расширителей в целом). Чтобы использовать такой расширитель, надо скомпилировать его любым ассемблером в объектный файл, который необходимо скомпоновать вместе со своей программой. В большинстве случаев надо назвать точку входа своей программы main или _main и закончить модуль директивой end без параметра, тогда DOS-расширитель получит управление первым и передаст его на метку main после того, как будут настроены все сегменты для модели памяти flat.
Самым популярным из профессиональных компиляторов, поддерживающих расширители DOS, стал компилятор Watcom C/C++, использующий модификацию коммерческого DOS-расширителя DOS4G, названную DOS/4GW. Дело в том, что компоновщик wlink.exe поддерживает, среди большого числа различных форматов вывода, формат линейных исполнимых файлов LE, применяющийся в операционной системе OS/2 (а также, с небольшими модификациями, для драйверов в Windows). Оказалось, что достаточно просто дописать файл в формате OS/2 LE в конец загрузчика DOS-расширителя, написанного соответствующим образом, чтобы потом его запускать. Загрузчик расширителя можно указать прямо в командной строке wlink (командой op stub) или скопировать позже. В комплект поставки расширителей часто входит специальная утилита, которая заменяет загрузчик, находящийся в начале такой программы, на свой.
Чтобы скомпилировать, например, программу lfbfire.asm, которую мы рассмотрим далее, следует воспользоваться следующими командами:
Компиляция:
wasm lfbfire.asm
Компоновка с DOS/4GW (стандартный расширитель, распространяемый с Watcom С):
wlink file lfbfire.obj form os2 le op stub=wstub.exe
Компоновка с PMODE/W (самый популярный из бесплатных расширителей):
wlink file lfbfire.obj form os2 le op stub=pmodew.exe
Компоновка с ZRDX (более строгий с точки зрения реализации):
wlink file lfbfire.obj form os2 le op stub=zrdx.exe
Компоновка с WDOSX (самый универсальный расширитель):
wlink file lfbfire. obj form os2 le op stub=wdosxle.exe
И так далее.
К сожалению, формат исполнимых файлов DOS (так называемый формат MZ), который по умолчанию создают другие компиляторы, крайне неудобен для объединения с расширителями, хотя универсальный расширитель WDOSX способен обработать и такой файл, и даже просто файл с 32-битным кодом без всяких заголовков (какой можно получить, создав СОМ-файл с директивой org 0), и файл в формате РЕ (см. главу 7), хотя и не во всех случаях такие программы будут работать успешно.
И наконец, третий подход к объединению расширителя и программы можно видеть на примере DOS32, в состав которого входит программа dlink.exe, являющаяся компоновщиком, который вполне подойдет вместо link, tlink или wlink, чтобы получить исполнимый файл, работающий с этим расширителем.
Тем не менее популярность подхода, используемого в Watcom, настолько высока, что подавляющее большинство программ, применяющих идею расширителей DOS, написано именно на Watcom С или на ассемблере для WASM.
Прежде чем мы сможем написать обещанный в главе 4.5.2 пример программы, работающей с линейным кадровым буфером SVGA, познакомимся еще с двумя группами функций DPMI, которые нам потребуются.
Управление памятью в DPMI
INT 31h, AX = 0100h - Выделить память ниже границы 1 Мб
| Ввод: |
АХ = 0100h ВХ = требуемый размер в 16-байтных параграфах |
| Вывод: |
если CF = 0, АХ = сегментный адрес выделенного блока для использования в реальном режиме; DX = селектор выделенного блока для применения в защищенном режиме |
Обработчик этой функции выходит в V86 и вызывает функцию DOS 48h для выделения области памяти, которую потом можно использовать для передачи данных между нашей программой и обработчиками прерываний, возвращающими структуры данных в памяти.
INT 31h, АХ = 0101h - Освободить память ниже границы 1 Мб
| Ввод: |
АХ = 0101h DX = селектор освобождаемого блока |
| Вывод: |
CF = 0, если не было ошибок |
INT 31h, АХ = 0102h - Изменить размер блока, выделенного функцией 0100h
| Ввод: |
АХ = 0102h ВХ = новый размер блока в 16-байтных параграфах DX = селектор модифицируемого блока |
| Вывод: |
CF = 0, если не было ошибок |
INT 31h, АХ = 0500h - Получить информацию о свободной памяти
| Ввод: |
АХ = 0500h ES:EDI = адрес 48-байтного буфера |
| Вывод: |
CF = 0 всегда, буфер заполняется следующей структурой данных:
+00h: 4 байта - максимальный доступный блок в байтах
+04h: 4 байта - число доступных нефиксированных страниц
+08h: 4 байта - число доступных фиксированных страниц
+0Ch: 4 байта - линейный размер адресного пространства в страницах
+10h: 4 байта - общее число нефиксированных страниц
+14h: 4 байта - общее число свободных страниц
+18h: 4 байта - общее число физических страниц
+1Ch: 4 байта - свободное место в линейном адресном пространстве
+20h: 4 байта - размер swap-файла или раздела в страницах
+24h: 0Ch байт - все байты равны FFh |
INT 31h, AX = 0501h - Выделить блок памяти
| Ввод: |
АХ = 0501h ВХ:СХ = размер блока в байтах, больше нуля |
| Вывод: |
если CF = 0, ВХ:СХ = линейный адрес блока; SI:DI = идентификатор блока для функций 0502 и 0503 |
<
/p>
INT 31h, АХ = 0502h - Освободить блок памяти
| Ввод: |
АХ = 0502h SI:DI = идентификатор блока |
| Вывод: |
CF = 0, если не было ошибки |
INT 31h, AX = 0503h - Изменить размер блока памяти
| Ввод: |
АХ = 0503h ВХ:СХ = новый размер в байтах SI:DI = идентификатор блока |
| Вывод: |
если CF = 0, ВХ:СХ = новый линейный адрес блока; SI:DI = новый идентификатор |
Нам потребуются еще две функции DPMI для работы с устройством, которое отображает свою память в физическое пространство адресов.
INT 31h, АХ = 0800h - Отобразить физический адрес выше границы 1 Мб на линейный
| Ввод: |
АХ = 0800h ВХ:СХ = физический адрес SI:DI = размер области в байтах |
| Вывод: |
если CF = 0, ВХ:СХ = линейный адрес, который можно использовать для доступа к этой памяти |
INT 31h, AX = 0801h - Отменить отображение, выполненное функцией 0800h
| Ввод: |
АХ = 0801h ВХ:СХ = линейный адрес, возвращенный функцией 0800h |
| Вывод: |
CF = 0, если не было ошибок |
Вывод на экран через линейный кадровый буфер
; lfbfire.asm ; Программа, работающая с SVGA при помощи линейного кадрового буфера (LFB), ; демонстрирует стандартный алгоритм генерации пламени. ; Требуется поддержка LFB видеоплатой (или загруженный эмулятор univbe), ; для компиляции необходим DOS-расширитель ; .486p ; для команды xadd .model flat ; основная модель памяти в защищенном режиме .code assume fs:nothing ; нужно только для MASM _start: jmp short _main db "WATCOM" ; нужно только для DOS/4GW
; начало программы ; на входе обычно CS, DS и SS указывают на один и тот же сегмент с лимитом 4 Гб, ; ES указывает на сегмент с PSP на 100h байт, ; остальные регистры не определены _main: sti ; даже флаг прерываний не определен, cld ; не говоря уже о флаге направления
mov ax,0100h ; функция DPMI 0100h mov bx,100h ; размер в 16-байтных параграфах int 31h ; выделить блок памяти ниже 1 Мб jc DPMI_error mov fs,dx ; FS - селектор для выделенного блока
; получить блок общей информации о VBE 2.0 (см. главу 4.5.2) mov dword ptr fs:[0],'2EBV' ; сигнатура VBE2 в начало блока mov word ptr v86_eax,4F00h ; функция VBE 00h mov word ptr v86_es,ax ; сегмент, выделенный DPMI mov ax,0300h ; функция DPMI 300h mov bx,0010h ; эмуляция прерывания 10h xor есх,есх mov edi,offset v86_regs push ds pop es ; ES:EDI - структура v86_regs int 31h ; получить общую информацию VBE2 jc DPMI_error cmp byte ptr v86_eax,4Fh ; проверка поддержки VBE jne VBE_error movzx ebp,word ptr fs:[18] ; объем SVGA-памяти в EBP shl ebp,6 ; в килобайтах
; получить информацию о видеорежиме 101h mov word ptr v86_eax,4F01h ; номер функции INT 10h mov word ptr v86_ecx,101h ; режим 101h - 640x480x256 ; ES:EDI те же самые mov ax,0300h ; функция DPMI 300h - эмуляция mov bx,0010h ; прерывания INT 10h xor ecx,ecx int 31h ; получить данные о режиме jc DPMI_error cmp byte ptr v86_eax,4Fh jne VBE_error test byte ptr fs:[0],80h ; бит 7 байта атрибутов = 1 - LFB есть jz LFB_error
; построение дескриптора сегмента, описывающего LFB ; лимит mov eax,ebp ; видеопамять в килобайтах shl eax,10 ; теперь в байтах dec еах ; лимит = размер - 1 shr еах,12 ; лимит в 4-килобайтных единицах mov word ptr videodsc+0,ax ; записать биты 15-0 лимита shr еах,8 and ah,0Fh or byte ptr videodsc+6,ah ; и биты 19 - 16 лимита mov eax,ebp ; видеопамять в килобайтах shl eax,10 ; в байтах mov edx,dword ptr fs:[40] ; физический адрес LFB shld ebx,edx,16 ; поместить его в CX:DX, shld esi,eax,16 ; а размер - в SI:DI mov ax,800h ; и вызвать функцию DPMI 0800h int 31h ; отобразить физический адрес в линейный shrd edx,ebx,16 ; перенести полученный линейный адрес mov dx,cx ; из BS:CX в EDX mov word ptr videodsc+2,dx ; и записать биты 15-0 базы mov byte ptr videodsc+4,dl ; биты 23 - 16 mov byte ptr videodsc+7,dh ; и биты 31 -24 ; права mov bx,cs lar cx,bx ; прочитать права нашего сегмента and cx,6000h ; и перенести биты DPL or byte ptr videodsc+5,ch ; в строящийся дескриптор
; поместить наш дескриптор в LDT и получить селектор mov ex,1 ; получить один новый дескриптор mov ах,0 ; у DPMI int 31h jc DPMI_error mov word ptr videosel,ax ; записать его селектор push ds pop es mov edi,offset videodsc ; ES:EDI - адрес нашего дескриптора mov bx,ax ; BX - выданный нам селектор mov ax,0Ch ; функция DPMI 0Ch int 31h ; загрузить дескриптор в LDT jc DPMI_error ; теперь в videosel лежит селектор на LFB ; переключение в режим 4101h (101h + LFB) mov word ptr v86_eax,4F02h ; 4F02h - установить SVGA-режим mov word ptr v86_ebx,4101h ; режим 4101h = 101h + LFB mov edi,offset v86_regs ; ES:EDI - структура v86_regs mov ах,0300h ; функция DPMI 300h mov bx,0010h ; эмуляция прерывания 10h xor ecx,ecx int 31h
mov ax,word ptr videosel ; AX - наш селектор enter_flame: ; сюда придет управление с селектором ; в ах на A000h:0000, если произошла ; ошибка в любой VBE-функции mov es,ax ; ES - селектор видеопамяти или LFB
; отсюда начинается процедура генерации пламени
; генерация палитры для пламени xor edi,edi ; начать писать палитру с адреса ES:0000 xor есх,есх palette_gen: xor еах,еах ; цвета начинаются с 0, 0, 0 mov cl,63 ; число значений для одной компоненты palette_11: stosb ; записать байт inc eax ; увеличить компоненту cmpsw ; пропустить два байта loop palette_11 ; и так 64 раза
push edi mov cl,192 palette_12: stosw ; записать два байта inc di ; и пропустить один loop palette_12 ; и так 192 раза (до конца палитры) pop edi ; восстановить EDI inc di jns palette_gen
; палитра сгенерирована, записать ее в регистры VGA DAC (см. главу 5.10.4) mov al,0 ; начать с регистра 0 mov dx,03C8h ; индексный регистр на запись out dx,al push es push ds ; поменять местами ES и DS pop es pop ds xor esi,esi mov ecx,256*3 ; писать все 256 регистров mov edx,03C9h ; в порт данных VGA DAC rep outsb push es push ds ; поменять местами ES и DS pop es pop ds
; основной цикл - анимация пламени, пока не будет нажата любая клавиша, xor edx,edx ; должен быть равен нулю mov ebp,7777h ; любое число mov ecx,dword ptr scr_width ; ширина экрана main_loop: push es ; сохранить ES push ds pop es ; ES = DS - мы работаем только с буфером ; анимация пламени (классический алгоритм) inc ecx mov edi,offset buffer mov ebx,dword ptr scr_height shr ebx,1 dec ebx mov esi,scr_width animate_flame: mov ax,[edi+esi*2-1] ; вычислить среднее значение цвета add al,ah ; в данной точке (EDI) из значений setc ah ; цвета в точке слева и на две строки mov dl,[edi+esi*2+1] ; вниз, справа и на две строки вниз add ax,dx mov dl,[edi+esi*4] ; и на четыре строки вниз. add ax,dx ; причем первое значение shr ax,2 ; модифицировать jz already_zero ; уменьшить яркость цвета dec ax already_zero: stosb ; записать новый цвет в буфер add eax,edx shr eax,1 mov byte ptr [edi+esi-1],al loop animate_flame mov ecx,esi add edi,ecx dec ebx jnz animate_flame
; псевдослучайная полоска внизу экрана, которая служит генератором пламени generator_bar: xadd bp,ax stosw stosw loop generator_bar pop es ; восстановить ES для вывода на экран
;вывод пламени на экран xor edi,edi ; ES:EDI - LFB push esi add esi,offset buffer ; DS:ESI - буфер mov ecx,dword ptr scr_size ; размер буфера в двойных словах rep movsd ; скопировать буфер на экран pop esi mov ah,1 ; если не была нажата int 16h ; никакая клавиша, jz main_loop ; продолжить основной цикл, mov ah,0 ; иначе - int 16h ; считать эту клавишу
exit_all: mov ах,03h ; восстановить текстовый режим int 10h mov ax,4C00h ; AH = 4Ch int 21h ; выход из программы под расширителем DOS
; различные обработчики ошибок DPMI_error: ; ошибка при выполнении ; одной из функций DPMI mov edx,offset DPMI_error_msg mov ah,9 int 21h ; вывести сообщение об ошибке jmp short exit_all ; и выйти VBE_error: ; не поддерживается VBE mov edx,offset VBE_error_msg mov ah,9 int 21h ; вывести сообщение об ошибке jmp short start_with_vga ; и использовать VGA LFB_error: ; не поддерживается LFB mov edx,offset LFB_error_msg mov ah,9 ; вывести сообщение об ошибке int 21h start_with_vga: mov ah,0 ; подождать нажатия любой клавиши int 16h mov ax,13h ; переключиться в видеорежим 13h int 10h ; 320x200x256 mov ax,2 ; функция DPMI 0002h mov bx,0A000h ; построить дескриптор для реального int 31h ; сегмента mov dword ptr scr_width,320 ; установить параметры режима mov dword ptr scr_height,200 mov dword ptr scr_size,320*200/4 jmp enter_flame ; и перейти к пламени
.data ; различные сообщения об ошибках VBE_error_msg db "Ошибка VBE 2.0",0Dh,0Ah db "Будет использоваться режим VGA 320x200",0Dh,0Ah,'$' DPMI_error_msg db "Ошибка DPMI$" LFB_error_msg db "LFB недоступен",0Dh,0Ah db "Будет использоваться режим VGA 320x200",0Dh,0Ah,'$' ; параметры видеорежима scr_width dd 640 scr_height dd 480 scr_size dd 640*480/4 ; структура, используемая функцией DPMI 0300h v86_regs label byte v86_edi dd 0 v86_esi dd 0 v86_ebp dd 0 v86_res dd 0 v86_ebx dd 0 v86_edx dd 0 v86_ecx dd 0 v86_eax dd 0 v86_flags dw 0 v86_es dw 0 v86_ds dw 0 v86_fs dw 0 v86_gs dw 0 v86_ip dw 0 v86_cs dw 0 v86_sp dw 0 v86_ss dw 0 ; дескриптор сегмента, соответствующего LFB videodsc dw 0 ; биты 15-0 лимита dw 0 ; биты 15-0 базы db 0 ; биты 16-23 базы db 10010010b ; доступ db 10000000b ; биты 16-19 лимита и другие биты db 0 ; биты 24 - 31 базы ; селектор сегмента, описывающего LFB videosel dw 0
.data? ; буфер для экрана buffer db 640*483 dup(?)
; стек .stack 1000h end _start
Программирование с DOS-расширителями - один из лучших выходов для приложений, которые должны работать в любых условиях, включая старые версии DOS, и в то же время требуют 32-битный режим. Еще совсем недавно большинство компьютерных игр, в частности знаменитые Doom и Quake, выпускались именно как программы, использующие расширители DOS. Сейчас, с повсеместным распространением операционных систем для PC, работающих в 32-битном защищенном режиме, требование работы в любых версиях DOS становится менее актуальным и все больше программ выходят только в версиях для Windows 95 или NT, чему и будет посвящена следующая глава.
Программирование для Windows 95 и Windows NT
Диалоги
Диалоги
Графические программы для Windows практически никогда не ограничиваются одним меню, потому что меню не позволяет ввести никакой реальной информации - только выбрать тот или иной пункт из предложенных. Конечно, можно в цикле после GetMessage или PeekMessage обрабатывать события передвижения мыши и нажатия клавиш, и так делают в интерактивных программах, например в играх, но, если требуется ввод текста с возможностью его редактирования, выбор файла на диске и любые другие нетривиальные действия, основным средством ввода информации в программах для Windows оказываются диалоги.
Диалог описывается, так же как и меню, в файле ресурсов, но, если меню было очень просто написать вручную, ради диалогов, скорее всего, придется пользоваться каким-нибудь редактором диалогов, идущим в комплекте с вашим любимым компилятором, если, конечно, вы не знаете в точности, по каким координатам будет располагаться каждый контрол (активный элемент диалога).
// windlg.rc // Файл ресурсов, описывающий диалог, используемый в программе windlg.asm. // Все следующие определения можно заменить на #include // , если он есть:
// стили для диалогов #define DS_CENTER 0x0800L #define DS_MODALFRAME 0x80L #define DS_3DLOOK 0x0004L
// стили для окон #define WS_MINIMIZEBOX 0x00020000L #define WS_SYSMENU 0x00080000L #define WS_VISIBLE 0x10000000L #define WS_OVERLAPPED 0x00000000L #define WS_CAPTION 0xC00000L
// стили для редактора #define ES_AUTOHSCROLL 0x80L #define ES_LEFT 0 #define ZDLG_MENU 7
// идентификаторы контролов диалога #define IDC_EDIT 0 #define IDC_BUTTON 1 #define IDC_EXIT 2
// идентификаторы пунктов меню #define IDM_GETTEXT 10 #define IDM_CLEAR 11 #define IDM_EXIT 12
ZZZ_Dialog DIALOG 10,10,205,30 // x, у, ширина, высота STYLE DS_CENTER | DS_MODALFRAME | DS_3DLOOK | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED CAPTION "Win32 assembly dialog example" // заголовок MENU ZDLG_MENU // меню BEGIN // начало списка контролов EDITTEXT IDC_EDIT,15,7,111,13,ES_AUTOHSCROLL | ES_LEFT PUSHBUTTON "E&xit",IDC_EXIT,141,8,52,13 END
ZDLG_MENU MENU // меню BEGIN POPUP "Test" BEGIN MENUITEM "Get Text",IDM_GETTEXT MENUITEM "Clear Text",IDM_CLEAR MENUITEM SEPARATOR MENUITEM "E&xit",IDM_EXIT END END
В качестве простого примера использования диалога покажем, как можно его применять, даже не регистрируя новый класс. Для этого надо просто создать диалог командой CreateDialog или одним из ее вариантов, не создавая никакого окна-предка. Все сообщения от диалога и окон, которые он создает, будут посылаться в процедуру-обработчик типа DialogProc, аналогичную процедуре WindowProc.
; windlg.asm ; Графическое win32-приложение, демонстрирующее работу с диалогом
; идентификаторы контролов (элементов диалога) IDC_EDIT equ 0 IDC_BUTTON equ 1 IDC_EXIT equ 2
; идентификаторы элементов меню IDM_GETTEXT equ 10 IDM_CLEAR equ 11 IDM_EXIT equ 12
include def32.inc include kernel32.inc include user32.inc
.386 .model flat .data dialog_name db "ZZZ_Dialog",0 ; имя диалога в ресурсах .data? buffer db 512 dup(?) ; буфер для введенного текста .code _start: xor ebx,ebx ; в EBX будет 0 для команд push 0 ; (короче в 2 раза) ; определить идентификатор нашей программы push ebx call GetModuleHandle ; запустить диалог push ebx ; значение, которое перейдет как параметр WM_INITDIALOG push offset dialog_proc ; адрес процедуры типа DialogProc push ebx ; идентификатор окна-предка (0 - ничей диалог) push offset dialog_name ; адрес имени диалога в ресурсах push eax ; идентификатор программы, в ресурсах которой ; находится диалог (наш идентификатор в ЕАХ) call DialogBoxParam ; выход из программы push ebx call ExitProcess ; ; процедура dialog_proc ; вызывается диалогом каждый раз, когда в нем что-нибудь происходит ; именно здесь будет происходить вся работа программы ; ; процедура не должна изменять регистры EBP,EDI,ESI и ЕВХ! ; dialog_proc proc near ; так как мы получаем параметры в стеке, построим стековый кадр push ebp mov ebp,esp ; процедура типа DialogProc вызывается со следующими параметрами: dp_hWnd equ dword ptr [ebp+08h] ; идентификатор диалога dp_uMsg equ dword ptr [ebp+0Ch] ; номер сообщения dp_wParam equ dword ptr [ebp+10h] ; первый параметр dp_lParam equ dword ptr [ebp+14h] ; второй параметр
mov ecx,dp_hWnd ; ECX будет хранить идентификатор диалога, mov eax,dp_uMsg ; a EAX - номер сообщения, cmp eax,WM_INITDIALOG ; если мы получили WM_INITDIALOG jne not_initdialog push IDC_EDIT push dp_hWnd call GetDlgItem ; определить идентификатор push eax ; окна редактирования текста call SetFocus ; и передать ему фокус, not_initdialog: cmp eax,WM_CLOSE ; если мы получили WM_CLOSE, jne not_close push 0 push ecx call EndDialog ; закрыть диалог, not_close: cmp eax,WM_COMMAND ; если мы получили WM_COMMAND, jne not_command mov eax,dp_wParam ; EAX = wParam (номер сообщения), cmp dp_lParam,0 ; если lparam ноль - сообщение от меню, jne lParam_not_0 cmp ax,IDM_GETTEXT ; если это пункт меню Get Text jne not_gettext push 512 ; размер буфера push offset buffer ; адрес буфера push IDC_EDIT ; номер конрола редактирования push ecx call GetDlgItemText ; считать текст в buffer push MB_OK push offset dialog_name push offset buffer push dp_hWnd call MessageBox ; и показать его в MessageBox, not_gettext: cmp eax,IDM_CLEAR ; если это пункт меню Clear jne not_clear push 0 ; NULL push IDC_EDIT ; номер контрола push ecx call SetDlgItemText ; установить новый текст, not_clear: cmp eax,IDM_EXIT ; если это пункт меню Exit jne not_exit push 0 ; код возврата push ecx ; идентификатор диалога call EndDialog ; закрыть диалог lParam_not_0: ; lParam не ноль - сообщение от контрола, cmp eax,IDC_EXIT ; если сообщение от кнопки Exit, jne not_exit shr eax,16 cmp eax,BN_CLICKED ; если ее нажали push 0 ; код возврата push ecx ; идентификатор диалога call EndDialog ; закрыть диалог not_exit: xor eax,eax ; после обработки команды inc eax ; DialogProc должен возвращать TRUE (eax = 1) leave ret 16 ; конец процедуры not_command: ; сюда передается управление, если мы получили ; какое-то незнакомое сообщение xor еах,еах ; код возврата FALSE (eax = 0) leave ret 16 ; конец процедуры dialog_proc endp end _start
Добавления в наш user32.inc:
между ifdef _TASM_ и else:
extrn DialogBoxParamA:near extrn GetDlgItem:near extrn SetFocus:near extrn GetDlgItemTextA:near extrn SetDlgItemTextA:near extrn EndDialog:near DialogBoxParam equ DialogBoxParamA GetDlgltemText equ GetDlgItemTextA SetDlgltemText equ SetDlgItemTextA
между else и endif:
extrn __imp__DialogBoxParamA@20:dword extrn __imp__GetDlgItem@8:dword extrn __imp__SetFocus@4:dword extrn __imp__GetDlgItemTextA@16:dword extrn __imp__SetDlgItemTextA@12:dword extrn __imp__EndDialog@8:dword DialogBoxParam equ __imp__DialogBoxParamA@20 GetDlgItem equ __imp__GetDlgItem@8 SetFocus equ __imp__SetFocus@4 GetDlgItemText equ __imp__GetDlgItemTextA@16 SetDlgItemText equ __imp__SetDlgItemTextA@12 EndDialog equ __imp__EndDialog@8
Добавления к файлу def32.inc:
; из winuser.h WM_INITDIALOG equ 110h WM_CLOSE equ 10h BN_CLICKED equ 0
Динамические библиотеки
Кроме обычных приложений в Windows появился специальный тип файла - динамические библиотеки (DLL). DLL - это файл, содержащий процедуры и данные, которые доступны программам, обращающимся к нему. Например, все системные функции Windows, которыми мы пользовались, на самом деле были процедурами, входящими в состав таких библиотек, - kernel32.dll, user32.dll, comdlg32.dll и т.д. Динамические библиотеки позволяют уменьшить использование памяти и размер исполнимых файлов для тех случаев, когда несколько программ (или даже несколько копий одной и той же программы) используют одну и ту же процедуру. Можно считать, что DLL - это аналог пассивной резидентной программы, с тем лишь отличием, что DLL не находится в памяти, если ни одна программа, его использующая, не загружена.
С точки зрения программирования на ассемблере DLL - это самый обычный исполнимый файл формата РЕ, отличающийся только тем, что при входе в него в стеке находятся три параметра (идентификатор DLL-модуля, причина вызова процедуры и зарезервированный параметр), которые надо удалить, например командой ret 12. Кроме этой процедуры в DLL входят и другие, часть которых можно вызывать из других программ. Список этих экспортируемых процедур должен быть задан во время компиляции DLL, и поэтому команды для компиляции нашего следующего примера будут отличаться от обычных.
Компиляция MASM:
ml /с /coff /Cp /D_MASM_ dllrus.asm link dllrus.obj @dllrus.lnk
Содержимое файла dllrus.lnk:
/DLL /entry:start /subsystem:windows /export:koi2win_asm /export:koi2win /export:koi2wins_asm /export:koi2wins
Компиляция TASM:
tasm /m /x /ml /D_TASM_ dllrus.asm tlink32 -Tpd -c dllrus.obj,,,,dllrus.def
Содержимое файла dllrus.def:
EXPORTS koi2win_asm koi2win koi2wins koi2wins_asm
Компиляция WASM:
wasm dllrus.asm wlink @dllrus.dir
Содержимое dllrus.dir:
file dllrus.obj form windows nt DLL exp koi2win_asm,koi2win,koi2wins_asm,koi2wins
; dllrus.asm ; DLL для Win32 - перекодировщик из koi8 в ср1251 .386 .model flat ; функции, определяемые в этом DLL ifdef _MASM_ public _koi2win_asm@0 ; koi2win_asm - перекодирует символ в AL public _koi2win@4 ; CHAR WINAPI koi2win(CHAR symbol) public _koi2wins_asm@0 ; koi2wins_asm - перекодирует строку в [ЕАХ] public _koi2wins@4 ; VOID WINAPI koi2win(CHAR * string) else public koi2win_asm ; те же функции для TASM и WASM public koi2win public koi2wins_asm public koi2wins endif
.const ; таблица для перевода символа из кодировки KOI8-r (RFC1489) ; в кодировку Windows (cp1251), ; таблица только для символов 80h - FFh ; (то есть надо будет вычесть 80h из символа, преобразовать его командой xlat ; и снова добавить 80h) k2w_tbl db 16 dup(0) ; символы, не существующие в ср1251, db 16 dup(0) ; перекодируются в 80h db 00h, 00h, 00h, 38h, 00h, 00h, 00h, 00h db 00h, 00h, 00h, 00h, 00h, 00h, 00h, 00h db 00h, 00h, 00h, 28h, 00h, 00h, 00h, 00h db 00h, 00h, 00h, 00h, 00h, 00h, 00h, 00h db 7Eh, 60h, 61h, 76h, 64h, 65h, 74h, 63h db 75h, 68h, 69h, 6Ah, 6Bh, 6Ch, 6Dh, 6Eh db 6Fh, 7Fh, 70h, 71h, 72h, 73h, 66h, 62h db 7Ch, 7Bh, 67h, 78h, 7Dh, 79h, 77h, 7Ah db 5Eh, 40h, 41h, 56h, 44h, 45h, 54h, 43h db 55h, 48h, 49h, 4Ah, 4Bh, 4Ch, 4Dh, 4Eh db 4Fh, 5Fh, 50h, 51h, 52h, 53h, 46h, 42h db 5Ch, 5Bh, 47h, 58h, 5Dh, 59h, 57h, 5Ah
.code ; процедура DLLEntry. Получает три параметра - идентификатор, причину вызова ; и зарезервированный параметр. Нам не нужен ни один из них _start@12: mov al,1 ; надо вернуть ненулевое число в ЕАХ ret 12
; процедура BYTE WINAPI koi2win (BYTE symbol) - ; точка входа для вызова из С ifdef _MASM_ _koi2win@4 proc else koi2win proc endif pop ecx ; обратный адрес в ЕСХ pop eax ; параметр в ЕСХ (теперь стек очищен ; от параметров!) push ecx ; обратный адрес вернуть в стек для RET ; здесь нет команды RET - управление передается следующей процедуре ifdef _MASM_ _koi2win@4 endp else koi2win endp endif
; процедура koi2win_asm ; точка входа для вызова из ассемблерных программ: ; ввод: AL - код символа в KOI ; вывод: AL - код этого же символа в WIN ifdef _MASM_ _koi2win_asm@0 proc else koi2win_asm proc endif test al,80h ; если символ меньше 80h (старший бит 0), jz dont_decode ; не перекодировать, push ebx ; иначе - mov ebx,offset k2w_tbl sub al,80h ; вычесть 80h, xlat ; перекодировать add al,80h ; и прибавить 80h pop ebx dont_decode: ret ; выйти ifdef _MASM_ _koi2win_asm@0 endp else koi2win_asm endp endif
; VOID koi2wins(BYTE * koistring) - ; точка входа для вызова из С ifdef _MASM_ _koi2wins@4 proc else koi2wins proc endif pop ecx ; адрес возврата из стека pop eax ; параметр в ЕАХ push ecx ; адрес возврата в стек ifdef _MASM_ _koi2wins@4 endp else koi2wins endp endif ; точка входа для вызова из ассемблера: ; ввод: ЕАХ - адрес строки, которую надо преобразовать из KOI в WIN ifdef _MASM_ _koi2wins_asm@0 proc else koi2wins_asm proc endif push esi ; сохранить регистры, которые ; нельзя изменять push edi push ebx mov esi,eax ; приемник строк mov edi,eax ; и источник совпадают mov ebx,offset k2w_tbl decode_string: lodsb ; прочитать байт, test al,80h ; если старший бит 0, jz dont_decode2 ; не перекодировать, sub al,80h ; иначе - вычесть 80h, xlat ; перекодировать add al,80h ; и добавить 80h dont_decode2: stosb ; вернуть байт на место, test al,al ; если байт - не ноль, jnz decode_string ; продолжить pop ebx pop edi pop esi ret ifdef _MASM_ _koi2wins_asm@0 endp else koi2wins_asm endp endif end _start@l2
Как видно из примера, нам пришлось назвать все процедуры по-разному для различных ассемблеров. В случае MASM понятно, что все функции должны иметь имена типа _start@12, а иначе программам, использующим их, придется обращаться к функциям с именами типа _Jmp_start, то есть такой DLL нельзя будет использовать из программы, написанной на Microsoft С. В случае TASM и WASM процедуры могут иметь неискаженные имена (и более того, wlink.exe не позволяет экспортировать имя переменной, содержащее символ @), так как их компиляторы берут имена процедур не из библиотечного файла, а прямо из DLL при помощи соответствующей программы - implib или wlib.
Итак, чтобы воспользоваться полученным DLL, напишем простую программу, которая перекодирует одну строку из КОI-8r в Windows ср1251.
; dlldemo.asm ; Графическое приложение для Win32, демонстрирующее работу с dllrus.dll, ; выводит строку в KOI8 и затем в ср1251, перекодированную функцией koi2wins include def32.inc include user32.inc include kernel32.inc includelib dllrus.lib ifndef _MASM_ extrn koi2win__asm:near ; определения для функций из DLL для extrn koi2win:near ; TASM и WASM extrn koi2wins_asm:near ; (хотя для WASM было бы эффективнее extrn koi2wins:near ; использовать __imp__koi2win, выделив else ; его в отдельный условный блок), extrn __imp__koi2win_asm@0:dword ; а это для MASM extrn __imp__koi2win@4:dword extrn __imp__koi2wins_asm@0:dword extrn __imp__koi2wins@4: dword koi2win_asm equ __imp__koi2win_asm@0 koi2win equ __imp__koi2win@4 koi2wins_asm equ __imp__koi2wins_asm@0 koi2wins equ __imp__koi2wins@4 endif
.386 .model flat .const title_string1 db "koi2win demo: string in KOI8",0 title_string2 db "koi2win demo: string in cp1251",0
.data koi_string db 0F3h,0D4h,0D2h,0CFh,0CBh,0C1h,20h,0CEh,0C1h db 20h,0EBh,0EFh,0E9h,2Dh,28h,0 .code _start: push MB_OK push offset title_string1 ; заголовок окна MessageBox push offset koi_string ; строка на KOI push 0 call MessageBox mov eax,offset koi_string push eax call koi2wins push MB_OK push offset title_string2 push offset koi_string push 0 call MessageBox push 0 ; код выхода call ExitProcess ; конец программы
end _start
Этот небольшой DLL может оказаться очень полезным для расшифровки текстов, приходящих из сети Internet или других систем, в которых используется кодировка KOI8. Воспользовавшись таблицами из приложения 1, вы можете расширить набор функций dllrus.dll, вплоть до перекодировки из любого варианта в какой угодно.
Драйверы устройств
В Windows, так же как и в DOS, существует еще один вид исполнимых файлов - драйверы устройств. Windows 3.x и Windows 95 используют одну модель драйверов, Windows NT - другую, a Windows 98 - уже третью, хотя и во многом близкую к модели Windows NT. В Windows 3.x/Windows 95 используются два типа драйверов устройств - виртуальные драйверы (VxD), выполняющиеся с уровнем привилегий 0 (обычно имеют расширение .386 для Windows 3.x и .VXD для Windows 95), и непривилегированные драйверы, исполняющиеся, как и обычные программы, с уровнем привилегий 3 (обычно имеют расширение .DRV). Windows NT использует несовместимую модель драйверов, так называемую kernel-mode (режим ядра). На основе модели kernel-mode с добавлением поддержки технологии PNP и понятия потоков данных в 1996 году была основана модель WDM (win32 driver model), которая теперь используется в Windows 98 и NT и, по-видимому, будет играть главную роль в дальнейшем.
Как и следовало ожидать, основным средством создания драйверов является ассемблер, и хотя применение С здесь возможно (в отличие от случая драйверов DOS), оно оказывается гораздо сложнее, чем применение ассемблера, - функции, к которым обращается драйвер, могут передавать параметры в регистрах, сегменты, из которых состоит драйвер, должны называться определенным образом, и т.д. Практически при создании драйверов часто пользуются одновременно С и ассемблером или С и специальным пакетом, который включает в себя все необходимые для инициализации действия.
Чтобы самостоятельно создавать драйверы для любой версии Windows, необходим комплект программ, документации, включаемых файлов и библиотек, распространяемый Microsoft, который называется DDK (Drivers Development Kit). (DDK для Windows NT и Windows 98 распространяются бесплатно.)
Мы не будем рассматривать программирование драйверов для Windows в деталях, так как этой теме посвящено достаточно много литературы, не говоря уже о документации, прилагающейся к DDK. Чтобы создать драйвер, в любом случае лучше всего начать с одного из прилагающихся примеров и изменять/добавлять процедуры инициализации, обработчики сообщений, прерываний и исключений, обработчики для API, предоставляемого драйвером, и т.д. Рассмотрим только, как выглядит исходный текст драйвера, потому что он несколько отличается от привычных нам ассемблерных программ.
Любой драйвер начинается с директивы include vmm.inc, которая включает файл, содержащий определения используемых сегментов и макроопределений. Макроопределения вида VXD_LOCKED_CODE_SEG/VXD_LOCKED_CODE_ENDS соответствуют директивам начала и конца соответствующих сегментов (в данном случае сегмента _LTEXT). Другие два важных макроопределения Declare_Virtual_Device и VMMCall/WDMCall. Первое - это просто определение, получающее в качестве параметров идентификатор драйвера, название, версию, порядок загрузки и адреса основных процедур драйвера, из которых строится заголовок драйвера. Второе - это замена команды call, получающая в качестве параметра имя функции VMM или WDM, к которой надо обратиться. Например, элемент кода драйвера BIOSXLAT, перехватывающий прерывание 10h, выглядит следующим образом:
VxD_ICODE_SEG ; начало сегмента _ITEXT (сегмент кода ; инициализации, исполняющийся ; в защищенном режиме, который удаляется ; из памяти после сообщения Init_Complete) BeginProc BIOSXlat_Sys_Critical_Init ; процедура, которая вызывается ; для обработчика сообщения ; Sys_Critical_Init - первого сообщения, ; которое получает драйвер. ; Обычно обработчики сообщений должны ; сохранять регистры ЕВХ, EDI, ESI и ЕВР, ; хотя в этом случае этого можно не делать mov esi,OFFSET32 BIOSXlat_Int10 ; адрес обработчика INT 10h ; в регистр ESI. Важно использовать ; макроопределение OFFSET32 всюду ; вместо offset mov edx,10h ; любое число, которое будет ; помещаться в EDX при вызове ; регистрируемого обработчика VMMCall Allocate_PM_Call_Back ; зарегистрировать ; точку входа, обращаясь ; к которой, программы из защищенного ; режима в VM будут передавать ; управление процедуре в драйвере ;(но не win32-nporpaммa) - ; они должны использовать ; DeviceIOControl для работы ; с драйверами, jc BXLSCI_NoPM ; если CF = 1 - произошла ошибка xchg edx,eax ; точку входа - в EDX, ; число 10h - в ЕАХ ; (теперь это - номер перехватываемого прерывания для Set_PM_Int_Vector) mov ecx,edx shr ecx,10h ; селектор точки входа movzx edx,dx ; смещение точки входа VMMCall Set_PM_Int_Vector ; установить обработчик ; прерывания INT 10h. ; Если эта функция вызвана до Sys_VM_Init, установленный обработчик ; становится звеном цепочки обработчиков для всех виртуальных машин. ; После того как прерывание проходит по цепочке обработчиков ; в защищенном режиме, оно отображается в V86, точно так же, как в DPMI
; [код перехвата других прерываний]
EndProc BIOSXlat_Sys_Critical_Init ; конец процедуры VxD_ICODE_ENDS ; конец сегмента инициализации
Соответственно, чтобы эта процедура была вызвана для сообщения Sys_Critical_Init, в сегменте фиксированного кода _LTEXT должна быть следующая запись:
VxD_LQCKED_CODE_SEG ; начало сегмента _LTEXT BeginProc BIOSXlat_Control ; начало процедуры Control_Dispatch Sys_Critical_Init,\BIOSXlat_Sys_Critical_Init ; при помощи еще одного макроопределения из vmm.inc ; зарегистрировать процедуру BIOSXlat_Sys_Critical_Init ; как обработчик сообщения Sys_Critical_Init clc ; процедура-обработчик управляющих ; сообщений должна возвращать CF = 0 ret EndProc BIOSXlat_Control ; конец процедуры VxD_LOCKED_CODE_ENDS ; конец сегмента _LTEXT
И наконец, процедура BIOSXlat_Control регистрируется как процедура, получающая управляющие сообщения, в заголовке драйвера:
; первая строка после .386р и include vmm.inc: Declare_Virtual_Device BlOSXlat, 1, 0, BIOSXlat_Control,\ BIOSXiat_Device_ID, BIOSXlat_Init_Order
Это не слишком сложно и, пользуясь примерами и документацией из DDK и отладчиком SoftICE, можно справиться практически с любой задачей, для которой имеет смысл создавать драйвер.
Программирование для Windows 95 и Windows NT
Несмотря на то что Windows 95 и Windows NT кажутся более сложными операционными системам по сравнению с DOS, программировать для них на ассемблере намного проще. С одной стороны, Windows-приложение запускается в 32-битном режиме (мы не рассматриваем Windows 3.11 и более старые версии, которые работали в 16-битном режиме) с моделью памяти flat, так что программист получает все те преимущества, о которых говорилось в предыдущей главе, а с другой стороны - нам больше не нужно изучать в деталях, как программировать различные устройства компьютера на низком уровне. В настоящих операционных средах приложения пользуются только системными вызовами, число которых здесь превышает 2000 (около 2200 для Windows 95 и 2434 для Windows NT). Все Windows-приложения используют специальный формат исполнимых файлов - формат PE (Portable Executable). Такие файлы начинаются как обычные EXE-файлы старого образца (их также называют MZ по первым двум символам заголовка), и, если такой файл запустить из DOS, он выполнится и выдаст сообщение об ошибке (текст сообщения зависит от используемого компилятора), в то время как Windows заметит, что после обычного MZ-заголовка файла идет PE-заголовок, и запустит приложение. Ёто будет означать только то, что для компиляции программ потребуются другие параметры в командной строке.
Консольные приложения
Исполнимые программы для Windows делятся на два основных типа - консольные и графические приложения. При запуске консольного приложения открывается текстовое окно, с которым программа может общаться функциями WriteConsole()/ReadConsole() и другими (соответственно при запуске из другого консольного приложения, например, файлового менеджера FAR, программе отводится текущая консоль и управление не возвращается к FAR, пока программа не закончится). Графические приложения соответственно не получают консоли и должны открывать окна, чтобы вывести что-нибудь на экран.
Для компиляции консольных приложений мы будем пользоваться следующими командами:
MASM:
ml /с /coff /Cp winurl.asm link winurl.asm /subsystem console
TASM:
tasm /m /ml /D_TASM_ winurl.asm tlink32 /Тре /ар /с /x winurl.obj
WASM:
wasm winurl.asm wlink file winurl.obj form windows nt runtime console op с
Попробуйте скомпилировать программу winurl.asm этим способом, чтобы увидеть, как отличается работа консольного приложения от графического.
В качестве примера полноценного консольного приложения напишем программу, которая перечислит все подключенные сетевые ресурсы (диски и принтеры), используя системные функции WNetOpenEnum(), WNetEnumResource() и WNetCloseEnum().
; netenum.asm ; Консольное приложение для win32, перечисляющее сетевые ресурсы include def32.inc include kernel32.inc include mpr.inc
.386 .model flat .const greet_message db 'Example win32 console program',0Dh,0Ah,0Dh,0Ah,0 error1_message db 0Dh,0Ah,'Could not get current user name',0Dh,0Ah,0 error2_message db 0Dh,0Ah,'Could not enumerate',0Dh,0Ah,0 good_exit_msg db 0Dh,0Ah,0Dh,0Ah,'Normal termination',0Dh,0Ah,0 enum_msg1 db 0Dh,0Ah,'Local ',0 enum_msg2 db ' remote - ',0 .data user_name db 'List of connected resources for user ' user_buff db 64 dup (?) ; буфер для WNetGetUser user_buff_l dd $-user_buff ; размер буфера для WNetGetUser enum_buf_l dd 1056 ; длина enum_buf в байтах enum_entries dd 1 ; число ресурсов, которые в нем помещаются .data? enum_buf NTRESOURCE ,?,?,?,?,?,?,?> ; буфер для WNetEnumResource dd 256 dup (?) ; 1024 байт для строк message_l dd ? ; переменная для WriteConsole enum_handle dd ? ; идентификатор для WNetEnumResource .code _start: ; получим от системы идентификатор буфера вывода stdout push STD_OUTPUT_HANDLE call GetStdHandle ; возвращает идентификатор STDOUT в eax mov ebx,eax ; а мы будем хранить его в EBX
; выведем строку greet_message на экран mov esi,offset greet_message call output_string
; определим имя пользователя, которому принадлежит наш процесс mov esi,offset user_buff push offset user_buff_l ; адрес переменной с длиной буфера push esi ; адрес буфера push 0 ; NULL call WNetGetUser cmp eax,NO_ERROR ; если произошла ошибка jne error_exit1 ; выйти mov esi,offset user_name ; иначе - выведем строку на экран call output_string
; начнем перечисление сетевых ресурсов push offset enum_handle ; идентификатор для WNetEnumResource push 0 push RESOURCEUSAGE_CONNECTABLE ; все присоединяемые ресурсы push RESOURCETYPE_ANY ; ресурсы любого типа push RESOURCE_CONNECTED ; только присоединенные сейчас call WNetOpenEnum ; начать перечисление cmp eax,NO_ERROR ; если произошла ошибка jne error_exit2 ; выйти
; цикл перечисления ресурсов enumeration_loop: push offset enum_buf_l ; длина буфера в байтах push offset enum_buf ; адрес буфера push offset enum_entries ; число ресурсов push dword ptr enum_handle ; идентификатор от WNetOpenEnum call WNetEnumResource cmp eax,ERROR_NO_MORE_ITEMS ; если они закончились je end_enumeration ; завершить перечисление cmp eax,NO_ERROR ; если произошла ошибка jne error_exit2 ; выйти с сообщением об ошибке
; вывод информации ресурсе на экран mov esi,offset enum_msg1 ; первая часть строки call output_string ; на консоль mov esi,dword ptr enum_buf.lpLocalName ; локальное имя устройства call output_string ; на консоль mov esi,offset enum_msg2 ; вторая часть строки call output_string ; на консоль mov esi,dword ptr enum_buf.lpRemoteName ; удаленное имя устройства call output_string ; туда же
jmp short enumeration_loop ; продолжим перечисление ; конец цикла end_enumeration: push dword ptr enum_handle call WNetCloseEnum ; конец перечисления
mov esi,offset good_exit_msg exit_program: call output_string ; выведем строку push 0 ; код выхода call ExitProcess ; конец программы ; выходы после ошибок error_exit1: mov esi,offset error1_message jmp short exit_program error_exit2: mov esi,offset error2_message jmp short exit_program
; процедрура output_string ; выводит на экран строку ; ввод: esi - адрес строки ; ebx - идентификатор stdout или другого консольного буфера
output_string proc near
; определим длину строки cld xor eax,eax mov edi,esi repne scasb dec edi sub edi,esi ; пошлем ее на консоль push 0 push offset message_l ; сколько байт выведено на консоль push edi ; сколько байт надо вывести на консоль push esi ; адрес строки для вывода на консоль push ebx ; идентификатор буфера вывода call WriteConsole ; WriteConsole(hConsoleOutput,lpvBuffer,cchToWrite, ; lpcchWritten,lpvReserved) ret output_string endp
end _start
В файл kernel32.inc надо добавить между ifdef _TASM_ и else строки:
extrn GetStdHandle:near extrn WriteConsoleA:near WriteConsole equ WriteConsoleA
и между else и endif:
extrn __imp__GetStdHandle@4:dword extrn __imp__WriteConsoleA@20:dword GetStdHandle equ __imp__GetStdHandle@4 WriteConsole equ __imp__WriteConsoleA@20
Кроме того, надо создать файл mpr.inc:
; mpr.inc ; включаемый файл с определениями функций из mpr.dll ; ifdef _TASM_ includelib import32.lib ; имена используемых функций extrn WNetGetUserA:near extrn WNetOpenEnumA:near extrn WNetEnumResourceA:near extrn WNetCloseEnum:near ; присваивания для облегчения читаемости кода WNetGetUser equ WNetGetUserA WNetOpenEnum equ WNetOpenEnumA WNetEnumResource equ WNetEnumResourceA else includelib mpr.lib ; истинные имена используемых функций extrn __imp__WNetGetUserA@12:dword extrn __imp__WNetOpenEnumA@20:dword extrn __imp__WNetEnumResourceA@16:dword extrn __imp__WNetCloseEnum@4:dword ; присваивания для облегчения читаемости кода WNetGetUser equ __imp__WNetGetUserA@12 WNetOpenEnum equ __imp__WNetOpenEnumA@20 WNetEnumResource equ __imp__WNetEnumResourceA@16 WNetCloseEnum equ __imp__WNetCloseEnum@4 endif
Еще потребуется файл def32.inc, в который поместим определения констант и структур из разных включаемых файлов для языка С. Существует утилита h2inc, преобразующая эти файлы целиком, но мы создадим собственный включаемый файл, в который будем добавлять новые определения по мере надобности.
; def32.inc ; файл с определениями констант и типов для примеров программ под win32
; из winbase.h STD_OUTPUT_HANDLE equ -11
; из winerror.h NO_ERROR equ 0 ERROR_NO_MORE_ITEMS equ 259
; из winnetwk.h RESOURCEUSAGE_CONNECTABLE equ 1 RESOURCETYPE_ANY equ 0 RESOURCE_CONNECTED equ 1 NTRESOURCE struc dwScope dd ? dwType dd ? dwDisplayType dd ? dwUsage dd ? lpLocalName dd ? lpRemoteName dd ? lpComment dd ? lpProvider dd ? NTRESOURCE ends
Ётот пример, разумеется, можно было построить более эффективно, выделив большой буфер для WNetEnumResource(), например при помощи LocalAlloc() или GlobalAlloc() (в Win32 это одно и то же), и затем, прочитав информацию обо всех ресурсах из него, пришлось бы следить за тем, кончились ресурсы или нет, и вызывать WNetEnumResource() еще раз.
Меню
Меню - это один из краеугольных камней идеологии Windows. Похожие друг на друга меню позволяют пользоваться совершенно незнакомыми программами, не читая инструкций, и знакомиться с их возможностями, просто посмотрев содержание различных пунктов меню. Давайте добавим меню и в нашу программу window.asm.
Первое, что мы должны будем сделать, - это само меню. Меню, так же как и иконки, диалоги и другая информация (вплоть до версии программы), записывают в файлы ресурсов. Файл ресурсов имеет расширение *.RC для текстового файла или *.RES для бинарного файла, скомпилированного специальным компилятором ресурсов (RC, BRCC32 или WRC). И те, и другие файлы ресурсов можно редактировать специальными программами, входящими в дистрибутивы C/C++ или других средств разработки для Windows, но мы не будем делать слишком сложное меню и напишем RC-файл вручную, например так:
// winmenu.rc // файл ресурсов для программы winmenu.asm // #define ZZZ_TEST 0 #define ZZZ_OPEN 1 #define ZZZ_SAVE 2 #define ZZZ_EXIT 3
ZZZ_Menu MENU { POPUP "&File" { MENUITEM "&Open",ZZZ_OPEN MENUITEM "&Save", ZZZ_SAVE MENUITEM SEPARATOR MENUITEM "E&xit",ZZZ_EXIT } MENUITEM "&Edit",ZZZ_TEST }
Чтобы добавить этот файл в программу, его надо скомпилировать и указать имя скомпилированного *.RES-файла для компоновщика:
MASM:
ml /c /coff /Cp winmenu.asm rc /r winmenu.rc link winmenu.obj winmenu.res /subsystem:windows
TASM:
tasm /m /ml /D_TASM_ winmenu.asm brcc32 winmenu.rc tlink32 /Tpe /aa /c /x winmenu.obj,,,,,winmenu.res
WASM:
wasm winmenu.rc wrc /r /bt=nt winmenu.rc wlink file winmenu.obj res winmenu.res form windows nt
А теперь сам текст программы. Чтобы показать, как мало надо внести изменений в программу window.asm, комментарии для всех строк, перенесенных оттуда без изменений заменены, на символ "*".
; winmenu.asm ; Графическое win32-приложение, демонстрирующее работу с меню ; звездочками отмечены строки, скопированные из файла window.asm ;
ZZZ_TEST equ 0 ; сообщения от нашего меню ZZZ_OPEN equ 1 ; должны совпадать с определениями из winmenu.rc ZZZ_SAVE equ 2 ; кроме того в нашем примере их номера важны ZZZ_EXIT equ 3 ; потому что они используются как индекс для ; таблицы переходов к обработчикам
include def32.inc ;* include kernel32.inc ;* include user32.inc ;* .386 ;* .model flat ;* .data ;* class_name db "window class 1",0 ;* window_name db "win32 assembly example",0 ;* menu_name db "ZZZ_Menu",0 ; имя меню в файле ресурсов test_msg db "You selected menu item TEST",0 ; строки для open_msg db "You selected menu item OPEN",0 ; демонстрации работы save_msg db "You selected menu item SAVE",0 ; меню wc WNDCLASSEX <4*12,CS_HREDRAW or CS_VREDRAW,offset win_proc,0,0,?,?,?,\ COLOR_WINDOW+1,0,offset class_name,0> ;* .data? ;* msg_ MSG ,?,?,?,?,?> ;* .code ;* _start: ;* xor ebx,ebx ;* push ebx ;* call GetModuleHandle ;* mov esi,eax ;* mov dword ptr wc.hInstance,eax ;* push IDI_APPLICATION ;* push ebx ;* call LoadIcon ;* mov wc.hIcon,eax ;* push IDC_ARROW ;* push ebx ;* call LoadCursor ;* mov wc.hCursor,eax ;* push offset wc ;* call RegisterClassEx ;* push offset menu_name ; имя меню push esi ; наш идентификатор call LoadMenu ; загрузим меню из ресурсов mov ecx,CW_USEDEFAULT ;* push ebx ;* push esi ;* push eax ; идентификатор меню или окна-потомка push ebx ;* push ecx ;* push ecx ;* push ecx ;* push ecx ;* push WS_OVERLAPPEDWINDOW ;* push offset window_name ;* push offset class_name ;* push ebx ;* call CreateWindowEx ;* push eax ;* push SW_SHOWNORMAL ;* push eax ;* call ShowWindow ;* call UpdateWindow ;* mov edi,offset msg_ ;* message_loop: ;* push ebx ;* push ebx ;* push ebx ;* push edi ;* call GetMessage ;* test eax,eax ;* jz exit_msg_loop ;* push edi ;* call TranslateMessage ;* push edi ;* call DispatchMessage ;* jmp short message_loop ;* exit_msg_loop: ;* push ebx ;* call ExitProcess ;*
; процедура win_proc ; вызывается окном каждый раз, когда окно получает какое-нибудь сообщение ; именно здесь будут происходить вся работа программы ; ; процедура не должна изменять регистры EBP,EDI,ESI и EBX ! win_proc proc ;* push ebp ;* mov ebp,esp ;* wp_hWnd equ dword ptr [ebp+08h] ;* wp_uMsg equ dword ptr [ebp+0Ch] ;* wp_wParam equ dword ptr [ebp+10h] ;* wp_lParam equ dword ptr [ebp+14h] ;* cmp wp_uMsg,WM_DESTROY ;* jne not_wm_destroy ;* push 0 ;* call PostQuitMessage ;* jmp short end_wm_check ;* not_wm_destroy: ;* cmp wp_uMsg,WM_COMMAND ; если мы получили WM_COMMAND jne not_wm_command ; это от нашего меню mov eax,wp_wParam ; и в wParam лежит наше подсообщение jmp dword ptr menu_handlers[eax*4] ; косвенный переход ; (в 32-битном режиме можно делать переход по любому регистру)
menu_handlers dd offset menu_test,offset menu_open dd offset menu_save,offset menu_exit
; обработчики событий test, open и save выводят MessageBox ; обработчик exit выходит из программы
menu_test: mov eax,offset test_msg ; сообщение для MessageBox jmp short show_msg menu_open: mov eax,offset open_msg ; сообщене для MessageBox jmp short show_msg menu_save: mov eax,offset save_msg ; сообщение для MessageBox show_msg: push MB_OK ; стиль для MessageBox push offset menu_name ; заголовок push eax ; сообщение push wp_hWnd ; идентификатор окна-предка call MessageBox ; вызов функции jmp short end_wm_check ; выход из win_proc menu_exit: ; если выбрали пункт EXIT push wp_hWnd call DestroyWindow ; уничтожим наше окно end_wm_check: leave ;* xor eax,eax ; вернем 0 как результат работы процедуры ret 16 ;* not_wm_command: ; not_wm_command, чтобы избавиться от лишнего jmp leave ;* jmp DefWindowProc ;* win_proc endp ;* end _start ;*
Итого: из 120 строк программы новыми оказались всего 36, в то время как программа, с точки зрения пользователя, стала намного сложнее. Так и выглядит все программирование под Windows на ассемблере - берется одна написанная раз и навсегда шаблонная программа, модифицируются ресурсы и пишутся обработчики для различных событий меню и диалогов. Фактически все программирование оказывается сосредоточенным именно в этих процедурах-обработчиках.
Добавления к включаемым файлам в этом примере тоже оказываются незначительными по сравнению с window.asm.
В user32.inc между ifdef _TASM_ и else:
extrn LoadMenuA:near extrn DestroyWindow:near LoadMenu equ LoadMenuA
и между else и endif:
extrn __imp__LoadMenuA@8:dword extrn __imp__DestroyWindow@4:dword LoadMenu equ __imp__LoadMenuA@8 DestroyWindow equ __imp__DestroyWindow@4
и в def32.inc:
; из winuser.h WM_COMMAND equ 111h MB_OK equ 0
Окна
Теперь, когда мы знаем, как просто выводится окно с предопределенным классом, возьмемся за вывод собственного окна - процедуры, на которой будут базироваться все последующие примеры, познакомимся с понятием сообщения. В DOS основным средством передачи управления программам в различных ситуациях служат прерьшания. В Windows прерывания используются системой для своих нужд, а для приложений существует аналогичный механизм - механизм событий. Так, нажатие клавиши на клавиатуре, если эта клавиша не используется Windows, генерирует сообщение WM_KEYDOWN или WM_KEYUP, которое можно перехватить, добавив в цепь обработчиков события собственное при помощи SetWindowHookEx(). События затем преобразуются в сообщения, которые рассылаются функциям - обработчикам сообщений и которые можно прочитать из основной программы при помощи вызовов GetMessage() и PeekMessage().
Нам пока потребуется только обработка сообщения закрытия окна (WM_DESTROY и WM_QUIT), по которому программа будет завершаться.
; window.asm ; Графическое win32-приложение, демонстрирующее базовый вывод окна ; include def32.inc include kernel32.inc include user32.inc .386 .model flat .data class_name db "window class 1",0 window_name db "win32 assembly example",0 ; структура, описывающая класс окна. wc WNDCLASSEX <4*12,CS_HREDRAW or CS_VREDRAW,offset win_proc,0,0,?,?,?, COLOR_WINDOW+1,0,offset class_name,0> ; здесь находятся следующие поля ; wc.cbSize = 4*12 - размер этой структуры ; wc.style - стиль окна (перерисовывать при изменении размера) ; wc.lpfnWndProc - обработчик событий окна (win_proc) ; wc.cbClsExtra - число дополнительных байтов после структуры (0) ; wc.cbWndExtra - число дополнительных байтов после окна (0) ; wc.hInstance - идентификатор нашего процесса (?) ; wc.hIcon - идентификатор иконки (?) ; wc.hCursor - идентификатор курсора (?) ; wc.hbrBackground - идентификатор кисти или цвет фона+1 ; (COLOR_WINDOW+1) ; wc.lpszMenuName - ресурс с основным меню (в этом примере - 0) ; wc.lpszClassName - имя класса (строка class_name) ; wc.hIconSm - идентификатор маленькой иконки (только в windows 95, ; для NT должен быть 0) .data? msg_ MSG ,?,?,?,?,?> ; а это - структура, в которой возвращается ; сообщение после GetMessage .code _start: xor ebx,ebx ; в EBX будет 0 для команд push 0 ; (короче в 2 раза) ; определим идентификатор нашей программы push ebx call GetModuleHandle mov esi,eax ; и сохраним его в ESI ; заполним и зарегестрируем класс mov dword ptr wc.hInstance,eax ; идентификатор предка ; выберем иконку push IDI_APPLICATION ; стандартная иконка приложения push ebx ; идентификатор модуля с иконкой call LoadIcon mov wc.hIcon,eax ; идентификатор иконки для нашего класса ; выберем форму курсора push IDC_ARROW ; стандартная стрелка push ebx ; идентификатор модуля с курсором call LoadCursor mov wc.hCursor,eax ; идентификатор курсора для нашего класса push offset wc call RegisterClassEx ; зарегистрируем класс ; создадим окно mov ecx,CW_USEDEFAULT ; push ecx короче push N в пять раз push ebx ; адрес структуры CREATESTRUCT (здесь NULL) push esi ; идентификатор процесса, который будет получать ; сообщения от окна (то есть, наш) push ebx ; идентификатор меню или окна-потомка push ebx ; идентификатор окна-предка push ecx ; высота (CW_USEDEFAULT - по умолчанию) push ecx ; ширина (по умолчанию) push ecx ; y-координата (по умолчанию) push ecx ; x-координата (по умолчанию) push WS_OVERLAPPEDWINDOW ; стиль окна push offset window_name ; заголовок окна push offset class_name ; любой зарегистрированный класс push ebx ; дополнительный стиль call CreateWindowEx ; создать окно (eax - идентификатор окна) push eax ; идентификатор для UpdateWindow push SW_SHOWNORMAL ; тип показа для для ShowWindow push eax ; идентификатор для ShowWindow ; больше идентификатор окна нам не потребуется call ShowWindow ; показать окно call UpdateWindow ; и послать ему сообщение WM_PAINT
; основной цикл - проверка сообщений от окна и выход по WM_QUIT mov edi,offset msg_ ; push edi короче push N в 5 раз message_loop: push ebx ; последнее сообщение push ebx ; первое сообщение push ebx ; идентификатор окна (0 - любое наше окно) push edi ; адрес структуры MSG call GetMessage ; получить сообщение от окна с ожиданием ; - не забывайте использовать PeekMessage ; если нужно в этом цикле что-то выполнять test eax,eax ; если получено WM_QUIT jz exit_msg_loop ; выйти push edi ; иначе - преобразовать сообщения типа call TranslateMessage ; WM_KEYUP в сообщения типа WM_CHAR push edi call DispatchMessage ; и послать их процедуре окна (иначе его просто ; нельзя будет закрыть) jmp short message_loop ; продолжить цикл exit_msg_loop: ; выход из программы push ebx call ExitProcess
; процедура win_proc ; вызывается окном каждый раз, когда окно получает какое-нибудь сообщение ; именно здесь будут происходить вся работа программы ; ; процедура не должна изменять регистры EBP,EDI,ESI и EBX ! ; win_proc proc ; так как мы получаем параметры в стеке, построим стековый кадр push ebp mov ebp,esp ; процедура типа WindowProc вызывается со следующими параметрами wp_hWnd equ dword ptr [ebp+08h] ; идентификатор окна wp_uMsg equ dword ptr [ebp+0Ch] ; номер сообщения wp_wParam equ dword ptr [ebp+10h] ; первый параметр wp_lParam equ dword ptr [ebp+14h] ; второй параметр ; если мы получили сообщение WM_DESTROY (оно означает что окно уже удалили ; с экрана, нажав alt-F4 или кнопку в верхнем правом углу) ; то пошлем основной программе сообщение WM_QUIT cmp wp_uMsg,WM_DESTROY jne not_wm_destroy push 0 ; код выхода call PostQuitMessage ; послать WM_QUIT jmp short end_wm_check ; и выйти из процедуры not_wm_destroy: ; если мы получили другое сообщение - вызовем его обработчик по умолчанию leave ; восстановим ebp jmp DefWindowProc ; и вызовем DefWindowProc с нашими параметрами ; и адресом возврата в стеке end_wm_check: leave ; восстановим ebp ret 16 ; и вернемся сами, очистив стек от параметров win_proc endp end _start
Необходимые добавления в файл def32.inc:
; из winuser. h IDI_APPLICATION equ 32512 WM_DESTROY equ 2 CS_HREDRAW equ 2 CS_VREDRAW equ 1 CW_USEDEFAULT equ 80000000h WS_OVERLAPPEDWINDOW equ 0CF0000h IDC_ARROW equ 32512 SW_SHOWNORMAL equ 1 COLOR_WINDOW equ 5 WNDCLASSEX struc cbSize dd ? style dd ? lpfnWndProc dd ? cbClsExtra dd ? cbWndExtra dd ? hInstance dd ? hIcon dd ? hCursor dd ? hbrBackground dd ? lpszMenuName dd ? lpszClassName dd ? hIconSm dd ? WNDCLASSEX ends MSG struc hwnd dd ? message dd ? wParam dd ? lParam dd ? time dd ? pt dd ? MSG ends
Добавления в файл user32.inc:
между ifdef _TASM_ и else:
extrn DispatchMessageA:near extrn TranslateMessage:near extrn GetMessageA:near extrn LoadIconA:near extrn UpdateWindow:near extrn ShowWindow:near extrn CreateWindowExA:near extrn DefWindowProcA:near extrn PostQuitMessage:near extrn RegisterClassExA:near extrn LoadCursorA:near ; присваивания для облегчения читаемости кода DispatchMessage equ DispatchMessageA GetMessage equ GetMessageA LoadIcon equ LoadIconA CreateWindowEx equ CreateWindowExA DefWindowProc equ DefWindowProcA RegisterClassEx equ RegisterClassExA LoadCursor equ LoadCursorA
и между else и endif:
extrn __imp__DispatchMessageA@4:dword extrn __imp__TranslateMessage@4:dword extrn __imp__GetMessageA@16:dword extrn __imp__LoadIconA@8:dword extrn __imp__UpdateWindow@4:dword extrn __imp__ShowWindow@8:dword extrn __imp__CreateWindowExA@48:dword extrn __imp__DefWindowProcA@16:dword extrn __imp__PostQuitMessage@4:dword extrn __imp__RegisterClassExA@4:dword extrn __imp__LoadCursorA@8:dword ; присваивания для облегчения читаемости кода DispatchMessage equ __imp__DispatchMessageA@4 TranslateMessage equ __imp__TranslateMessage@4 GetMessage equ __imp__GetMessageA@16 LoadIcon equ __imp__LoadIconA@8 UpdateWindow equ __imp__UpdateWindow@4 ShowWindow equ __imp__ShowWindow@8 CreateWindowEx equ __imp__CreateWindowExA@48 DefWindowProc equ __imp__DefWindowProcA@16 PostQuitMessage equ __imp__PostQuitMessage@4 RegisterClassEx equ __imp__RegisterClassExA@4 LoadCursor equ __imp__LoadCursorA@8
а в файл kernel32.inc между ifdef _TASM_ и else:
extrn GetModuleHandleA:near GetModuleHandle equ GetModuleHandleA
и между else и endif:
extrn __imp__GetModuleHandleA@4:dword GetModuleHandle equ __imp__GetModuleHandleA@4
В начале главы говорилось, что программировать под Windows просто, а в то же время текст обычной программы вывода пустого окна на экран уже занимает больше места, чем, например, программа проигрывания wav-файла из главы 5.10.8. Где же обещанная простота? Так вот, оказывается, что, написав window.asm, мы уже создали большую часть всех последующих программ, а когда мы дополним этот текст полноценным диалогом, обнаружится, что больше не нужно писать все эти громоздкие конструкции, достаточно просто копировать отдельные участки текста.
Окно типа MessageBox
Для того чтобы вывести на экран любое окно, программа обычно должна сначала описать его внешний вид и все свойства, то есть то, что называется классом окна. О том, как это сделать, - немного позже, а для начала выведем одно из окон с предопределенным классом - окно типа MessageBox. MessageBox - это маленькое окно с указанным текстовым сообщением и одной или несколькими кнопками. В нашем примере сообщением будет традиционное "Hello world!", и кнопка будет всего одна - ОК.
; winhello.asm ; Графическое win32-приложениe ; Выводит окно типа mesagebox с текстом "Hello world!" ; include def32.inc include kernel32.inc include user32.inc
.386 .model flat .const ; заголовок окна hello_title db "First win32 GUI program",0 ; сообщение hello_message db "Hello world!",0 .code _start: push MB_ICONINFORMATION ; стиль окна push offset hello_title ; адрес строки с заголовком push offset hello_message ; адрес строки с сообщением push 0 ; идентификатор предка call MessageBox
push 0 ; код выхода call ExitProcess ; завершение программы end _start
Естественно, нам потребуется добавить к файлу def32.inc строку:
; из winuser.h MB_ICONINFORMATION equ 40h
и создать новый файл, user32.inc, в который будут входить определения функций из user32.dll - библиотеки, куда входят все основные функции, отвечающие за оконный интерфейс:
; user32.inc ; включаемый файл с определениями функций из user32.dll ; ifdef _TASM_ includelib import32.lib ; имена используемых функций extrn MessageBoxA:near ; присваивания для облегчения читаемости кода MessageBox equ MessageBoxA else includelib user32.lib ; истинные имена используемых функций extrn __imp__MessageBoxA@16:dword ; присваивания для облегчения читаемости кода MessageBox equ __imp__MessageBoxA@16
Теперь можно скомпилировать эту программу аналогично тому, как мы компилировали winurl.asm, и запустить - на экране появится маленькое окно с нашим сообщением и кнопкой ОК, которое пропадет после того, как будет нажата эта кнопка. Если скомпилировать winhello.asm как консольное приложение, ничего не изменится, текстовое окно с именем программы будет открыто до тех пор, пока окно с нашим сообщением не будет закрыто.
Первая программа
В качестве нашего первого примера посмотрим, насколько проще написать под Windows программу, которая загружает другую программу. В DOS (см. главу 4.10) нам приходилось изменять распределение памяти, заполнять специальный блок данных EPBВ и только затем вызывать DOS. Здесь же не только вся процедура сокращается до одного вызова функции, а еще оказывается, что можно точно так же загружать не только программы, но и документы, графические и текстовые файлы и даже почтовые и WWW-адреса - все, для чего в реестре Windows записано действие, выполняющееся при попытке открытия.
; winurl.asm ; Пример програмы для win32. ; Запускает установленный по умолчанию броузер на адрес, указанный в строке URL ; аналогично можно запускать любую программу, документ, и любой другой файл, ; для которого определена операция open ; include shell32.inc include kernel32.inc
.386 .model flat .const URL db 'http://www.lionking.org/~cubbi/',0 .code _start: ; метка точки входа должна начинаться с подчеркивания xor ebx,ebx push ebx ; для исполнимых файлов - способ показа push ebx ; рабочий каталог push ebx ; командная строка push offset URL ; имя файла с путем push ebx ; операция open или print (если NULL - open) push ebx ; идентификатор окна, которое получит сообщения call ShellExecute ; ShellExecute(NULL,NULL,url,NULL,NULL,NULL) push ebx ; код выхода call ExitProcess ; ExitProcess(0) end _start
Итак, в этой программе выполняется вызов двух системных функций Win32 - ShellExecute() (открыть файл) и ExitProcess() (завершить процесс). Чтобы вызвать системную функцию Windows, программа должна поместить в стек все параметры от последнего к первому и передать управление дальней командой CALL. Все эти функции сами освобождают стек (завершаясь командой RET N) и возвращают результат работы в регистре ЕАХ. Такая договоренность о передаче параметров называется STDCALL. С одной стороны, это позволяет вызывать функции с нефиксированным числом параметров, а с другой - вызывающая сторона не должна заботиться об освобождении стека. Кроме того, функции Windows сохраняют значение регистров ЕВР, ESI, EDI и EBX, этим мы пользовались в нащем примере - хранили 0 в регистре EBX и применили 1-байтную команду PUSH EBX вместо 2-байтной PUSH 0.
Прежде чем мы сможем скомпилировать winurl.asm, нужно создать файлы kernel32.inc и shell32.inc, в которые поместим директивы, описывающие вызываемые системные функции:
; kernel32.inc ; включаемый файл с определениями функций из kernel32.dll ; ifdef _TASM_ includelib import32.lib ; имена используемых функций extrn ExitProcess:near else includelib kernel32.lib ; истинные имена используемых функций extrn __imp__ExitProcess@4:dword ; присваивания для облегчения читаемости кода ExitProcess equ __imp__ExitProcess@4 endif
; shell32.inc ; включаемый файл с определениями функций из shell32.dll ifdef _TASM_ includelib import32.lib ; имена используемых функций extrn ShellExecuteA:near ; присваивания для облегчения читаемости кода ShellExecute equ ShellExecuteA else includelib shell32.lib ; истинные имена используемых функции extrn __imp__ShellExecuteA@24:dword ; присваивания для облегчения читаемости кода ShellExecute equ __imp__ShellExecuteA@24 endif
Имена всех системных функций Win32 модифицируются так, что перед именем функции ставится подчеркивание, а после - знак "@" и число байт, которое занимают параметры, передаваемые ей в стеке, так ExitProcess() превращается в _ExitProcess@4(). Компиляторы с языков высокого уровня часто останавливаются на этом и вызывают функции по имени _ExitProcess@4(), но реально вызывается небольшая процедура-заглушка, которая ничего не делает, а только передает управление на такую же метку, но с добавленным "__imp_" - __imp__ExitProcess@4(). Во всех наших примерах мы будем обращаться напрямую к __imp__ExitProcess@4(). К сожалению, TASM (а точнее TLINK32) использует собственный способ вызова системных функций, который нельзя так обойти, и программы, скомпилированные с его помощью, оказываются намного больше и в некоторых случаях работают медленнее. Мы отделили описания функций для TASM во включаемых файлах при помощи директив условного ассемблирования, которые будут использовать их, если в командной строке ассемблера указать /D_TASM_.
Кроме этого, все функции, работающие со строками (как, например, ShellExecute()), существуют в двух вариантах. Если строка рассматривается в обычном смысле, как набор символов ASCII, к имени функции добавляется "A" (ShellExecuteA()). Другой вариант функции, использующий строки в формате UNICODE (два байта на символ), заканчивается буквой "U". Во всех наших примерах будем использовать обычные ASCII-функции, но, если вам потребуется перекомпилировать программы на UNICODE, достаточно только поменять "А" на "U" во включаемых файлах.
Итак, теперь, когда у нас есть все необходимые файлы, можно скомпилировать первую программу для Windows.
Компиляция MASM:
ml /с /coff /Cp winurl.asm link winurl.obj /subsystem:windows
(здесь и далее используется 32-битная версия link.exe)
Компиляция TASM:
tasm /m /ml /D_TASM_ winurl.asm tlink32 /Tpe /aa /c /x winurl.obj
Компиляция WASM:
wasm winurl.asm wlink file winurl.obj form windows nt op с
Также для компиляции потребуются файлы kernel32.lib и shell32.lib в первом и третьем случае и import32.lib - во втором. Ёти файлы входят в дистрибутивы любых средств разработки для Win32 от соответствующих компаний - Microsoft, Watcom (Sybase) и Borland (Inprise), хотя их всегда можно воссоздать из файлов kernel32.dll и shell32.dll, находящихся в каталоге WINDOWS/SYSTEM.
Иногда вместе с дистрибутивами различных средств разработки для Windows идет файл windows.inc, в котором дано макроопределение Invoke или заменена макросом команда call так, что они принимают список аргументов, первым из которых идет имя вызываемой функции, а затем через запятую - все параметры. С использованием этих макроопределений наша программа выглядела бы так:
_start: xor ebx,ebx Invoke SnellExecute, ebx, ebx, offset URL, ebx, \ ebx, ebx Invoke ExitProcess, ebx end _start
И этот текст компилируется в точно такой же код, что и у нас, но выполняется вызов не функции __imp__ExitProcess@4(), а промежуточной функции _ExitProcess@4(). Использование этой формы записи не позволяет применять отдельные эффективные приемы оптимизации, которые мы будем приводить в наших примерах, - помещение параметров в стек заранее и вызов функции командой JMP. И наконец, файла windows.inc у вас может просто не оказаться, так что будем писать push перед каждым параметром вручную.
Полноценное приложение
Теперь, когда мы знаем, как строятся программы с меню и диалогами, напишем одно настоящее полноценное приложение, включающее в себя все то, что требуется от программы, - меню, диалоги, комбинации клавиш для быстрого доступа к элементам меню и т.д. В качестве примера создадим простой текстовый редактор, аналогичный Notepad. В этом примере мы увидим, как получить параметры из командной строки, прочитать и записать файл, выделить и освободить память.
// winpad95.rc // Файл ресурсов для программы winpad95.asm // идентификаторы сообщений от пунктов меню #define IDM_NEW 0x1001 #define IDM_OPEN 0x101L #define IDM_SAVE 0x102L #define IDM_SAVEAS 0x103L #define IDM_EXIT 0x104L #define IDM_ABOUT 0x105L #define IDM_UNDO 0x106L #define IDM_CUT 0x107L #define IDM_COPY 0x108L #define IDM_PASTE 0x109L #define IDM_CLEAR 0x10AL #define IDM_SETSEL 0x10BL
// идентификаторы основных ресурсов #define IDJ1ENU 0x700L #define ID_ACCEL 0x701L #define ID_ABOUT 0x702L
// если есть иконка - можно раскомментировать эти две строки // #define ID_ICON 0x703L // ID_ICON ICON "winpad95.ico"
// основное меню ID_MENU MENU DISCARDABLE { POPUP "&File" { MENUITEM "&New\tCtrl+N", IDM_NEW MENUITEM "&Open...\tCtrl+O", IDM_OPEN MENUITEM "&Save\tCtrl+S", IDM_SAVE MENUITEM "Save &As...\tCtrl+Shift+S", IDM_SAVEAS MENUITEM SEPARATOR MENUITEM "E&xit\tCtrl+Q", IDM_EXIT } POPUP "&Edit" { MENUITEM "&Undo\tCtrl-Z", IDM_UNDO MENUITEM SEPARATOR MENUITEM "Cu&t\tCtrl-X", IDM_CUT MENUITEM "&Copy\tCtrl-C", IDM_COPY MENUITEM "&Paste\tCtrl-V", IDM_PASTE MENUITEM "&Delete\tDel", IDM_CLEAR MENUITEM SEPARATOR MENUITEM "Select &All\tCtrl-A", IDM_SETSEL } POPUP "&Help" { MENUITEM "About", IDM_ABOUT } }
// комбинации клавиш ID_ACCEL ACCELERATORS DISCARDABLE { "N", IDM_NEW, CONTROL, VIRTKEY "O", IDM_OPEN, CONTROL, VIRTKEY "S", IDM_SAVE, CONTROL, VIRTKEY "S", IDM_SAVEAS, CONTROL, SHIFT, VIRTKEY "Q", IDM_EXIT, CONTROL, VIRTKEY "Z", IDM_UNDO, CONTROL, VIRTKEY "A", IDM_SETSEL, CONTROL, VIRTKEY }
// все эти определения можно заменить на #include #define DS_MODALFRAME 0x80L #define DS_3DLOOK 4 #define WS_POPUP 0x80000000L #define WS_CAPTION 0xC00000L #define WS_SYSMENU 0x80000L #define IDOK 1 #define IDC_STATIC -1 #define IDI_APPLICATION 32512 #define WS_BORDER 0x800000L
// стандартный диалог "About" ID_ABOUT DIALOG DISCARDABLE 0, 0, 125, 75 STYLE DS_MODALFRAME | DS_3DLOOK | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "About Asmpad95" { ICON IDI_APPLICATION,IDC_STATIC,12,15,21,20 CTEXT "Asmpad95",IDC_STATIC,0,30,40,8 CTEXT "Prototype notepad-style editor for Windows 95 written entirely in assembly language",IDC_STATIC,45,10,70,45,WS_BORDER DEFPUSHBUTTON "OK",IDOK,35,60,40,10 }
Теперь рассмотрим текст программы:
; winpad95.asm ; графическое win32-приложение - текстовый редактор ; include def32.inc include user32.inc include kernel32.inc include comdlg32.inc
ID_MENU equ 700h ID_ACCEL equ 701h ID_ABOUT equ 702h
MAXSIZE equ 260 ; максимальное имя файла MEMSIZE equ 65535 ; максимальный размер временного буфера в памяти
EditID equ 1
.386 .model flat .const c_w_name db "Asmpad95",0 ; это и имя класса, и имя основного окна edit_class db "edit",0 ; предопределенное имя класса для редактора changes_msg db "Save changes?",0 filter_string db "All Files",0,'*.*',0 ; маски для Get * FileName db "Text Files",0, '*.txt',0,0
.data ; структура, использующаяся Get * FileName ofn OPENFILENAME ; структура, описывающая наш основной класс wc WNDCLASSEX flag_untitled db 1 ; = 1, если имя файла не определено (новый файл)
.data? h_editwindow dd ? ; идентификатор окна редактора h_accel dd ? ; идентификатор массива акселераторов p_memory dd ? ; адрес буфера в памяти SizeReadWrite dd ? msg_ MSG <> rec RECT <> buffer db MAXSIZE dup(?) ; имя файла window_title db MAXSIZE dup(?), 12 dup(?)
.code _start: call GetCommandLine ; получить нашу командную строку mov edi,eax mov al,' ' mov ecx,MAXSIZE repne scasb ; найти конец имени нашей программы cmp byte ptr [edi],0 je cmdline_empty mov esi,edi mov edi,offset buffer rep movsb mov flag_untitled,0 cmdline_empty: ; подготовить и зарегистрировать класс xor ebx,ebx call GetModuleHandle ; определить наш идентификатор mov esi,eax mov wc.hInstance,eax ; и сохранить его в wc.hInstance mov ofn._hInstance,eax push IDI_APPLICATION ; или IDI_ICON, если иконка есть в ресурсах, push ebx ; или esi, если иконка есть в ресурсах call LoadIcon mov wc.hIcon,eax push IDC_ARROW ; предопределенный курсор (стрелка) push ebx call LoadCursor mov wc.hCursor,eax push offset wc call RegisterClassEx ; создать основное окно push ebx push esi push ebx push ebx push 200 push 300 push CW_USEDEFAULT push CW_USEDEFAULT push WS_OVERLAPPEDWINDOW push offset c_w_name push offset c_w_name push WS_EX_CLIENTEDGE call СreateWindowEx push eax ; для pop esi перед message_loop push eax push SW_SHOWNORMAL push eax call ShowWindow call UpdateWindow ; инициализировать акселераторы push ID_ACCEL push esi call LoadAcceleratоrs mov h_accel,eax ; цикл ожидания сообщения pop esi ; ESI - идентификатор основного окна mov edi,offset msg_ ; EDI - структура с сообщением от него message_loop: push ebx push ebx push ebx push edi call GetMessage ; получить сообщение, test eax,eax ; если это WM_OUIT, - jz exit_msg_loop ; выйти из цикла push edi push h_accel push esi ; hWnd call TranslateAccelerator ; преобразовать акселераторы в IDM* test eax,eax jnz message_loop push edi call TranslateMessage ; преобразовать сообщения от клавиш push edi call DispatchMessage ; и отослать обратно jmp short message_loop exit_msg_loop: push msg_.wParam call ExitProcess ; конец программы
; процедура win_proc ; ; процедура не должна изменять регистры EBP,EDI,ESI и ЕВХ! win_proc proc near ; параметры (с учетом push ebp) wp_hWnd equ dword ptr [ebp+08h] wp_uMsg equ dword ptr [ebp+0Ch] wp_wParam equ dword ptr [ebp+10h] wp_lParam equ dword ptr [ebp+14h] ; инициализируем стековый кадр push ebp mov ebp,esp ; создать стековый кадр pusha ; сохранить все регистры xor ebx,ebx ; 0 для команд push 0 mov esi,wp_hWnd ; для команд push hWnd mov eax,wp_uMsg ; обработать пришедшее сообщение cmp eax,WM_CREATE je h_wm_create cmp eax,WM_SIZE je h_wm_size cmp eax,WM_DESTROY je h_wm_destroy cmp eax,WM_COMMAND je h_wm_command cmp eax,WM_ACTIVATE je h_wm_activate cmp eax,WM_CLOSE je h_wm_close def_proc: popa leave ; если это ненужное сообщение, jmp DefWindowProc ; оставить его обработчику по умолчанию
; обработчик WM_CLOSE, ; если нужно, спрашивает, сохранить ли файл h_wm_close: call save_contents jmp short def_proc
; обработчик WM_CREATE ; h_wm_create: ; здесь также можно создать toolbar и statusbar ; создать окно редактора push ebx push wc.hInstance ; идентификатор основной программы push EditID push esi ; hWnd push ebx ; 0 push ebx ; 0 push ebx ; 0 push ebx ; 0 push WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or \ ES_AUTOHSCROLL or ES_AUTOVSCROLL push ebx ; 0 push offset edit_class push ebx ; 0 call CreateWindowEx mov h_editwindow,eax ; передать ему фокус push eax call SetFocus cmp flag_untitled,1 je continue_create call skip_getopen ; открыть файл, указанный в командной строке continue_create: call set_title jmp end_wm_check
; обработчик WM_COMMAND ; h_wm_command: mov eax,wp_wParam cwde ; младшее слово содержит IDM_* sub eax,100h jb def_proc ; обработать сообщения от пунктов меню call dword ptr menu_handlers[eax*4] jmp end_wm_check
menu_handlers dd offset h_idm_new,offset h_idm_open,offset h_idm_save dd offset h_idm_saveas,offset h_idm_exit,offset h_idm_about dd offset h_idm_undo, offset h_idm_cut, offset h_idm_copy dd offset h_idm_paste, offset h_idm_clear, offset h_idm_setsel ; сообщения от пунктов меню должны быть описаны в win95pad.rc именно в таком ; порядке - от IDM_NEW 100h до IDM_CLEAR 10Ah
h_idm_setsel: push -1 ; -1 push ebx ; 0 push EM_SETSEL ; выделить весь текст push h_editwindow call SendMessage ret
; обработчики сообщений из меню EDIT: h_idm_clear: mov eax,WM_CLEAR jmp short send_to_editor h_idm_paste: mov eax,WM_PASTE jmp short send_to_editor h_idm_copy: mov eax,WM_COPY jmp short send_to_editor h_idm_cut: mov eax,WM_CUT jmp short send_to_editor h_idm_undo: mov eax,EM_UNDO send_to_editor: push ebx ; 0 push ebx ; 0 push eax push h_editwindow call SendMessage ret
; обработчик IDM_NEW h_idm_new: call save_contents ; записать файл, если нужно mov byte ptr flag_untitled,1 call set_title ; отметить, что файл не назван push ebx push ebx push WM_SETTEXT push h_editwindow call SendMessage ; послать пустой WM_SETTEXT редактору ret
; обработчик IDM_ABOUT h_idm_about: push ebx ; 0 push offset about_proc push esi ; hWnd push ID_ABOUT push wc.hInstance call DialogBoxParam ret
; обработчик IDM_SAVEAS и IDM_SAVE h_idm_save: cmp flag_untitled, 1 ; если файл назван, jne skip_getsave ; пропустить вызов GetSaveFileName h_idm_saveas: ; спросить имя файла mov ofn.Flags,OFN_EXPLORER or OFN_OVERWRITEPROMPT push offset ofn call GetSaveFileName test eax,eax jz file_save_failed skip_getsave: ; создать его push ebx push FILE_ATTRIBUTE_ARCHIVE push CREATE_ALWAYS push ebx push FILE_SHARE_READ or FILE_SHARE_WRITE push GENERIC_READ or GENERIC_WRITE push offset buffer call CreateFile mov edi,eax ; выделить память push MEMSIZE push GMEM_MOVEABLE or GMEM_ZEROINIT call GlobalAlloc push eax ; hMemory для GlobalFree push eax ; hMemory для GlobalLock call GlobalLock mov esi,eax ; адрес буфера в ESI ; забрать текст из редактора push esi push MEMSIZE-1 push WM_GETTEXT push h_editwindow call SendMessage ; записать в файл push esi ; pMemory call lstrlen push ebx push offset SizeReadWrite push eax ; размер буфера push esi ; адрес буфера push edi ; идентификатор файла call WriteFile push esi ; pMemory call GlobalUnlock call GlobalFree ; hMemory уже в стеке push edi ; идентификатор файла call CloseHandle ; сбросить флаг модификации в редакторе push ebx push ebx push EM_SETMODIFY push h_editwindow call SendMessage mov byte ptr flag_untitled,0 call set_title file_save_failed: push h_editwindow call SetFocus ret
; обработчик IDM_OPEN h_idm_open: call save_contents ; вызвать стандартный диалог выбора имени файла mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or \ OFN_EXPLORER push offset ofn call GetOpenFileName test eax,eax jz file_open_failed skip_getopen: ; открыть выбранный файл push ebx push FILE_ATTRIBUTE_ARCHIVE push OPEN_EXISTING push ebx push FILE_SHARE_READ or FILE_SHARE_WRITE push GENERIC_READ or GENERIC_WRITE push offset buffer call CreateFile mov edi,eax ; идентификатор для ReadFile ; выделить память push MEMSIZE push GMEM_MOVEABLE or GMEM_ZEROINIT call GlobalAlloc push eax ; hMemory для GlobalFree push eax ; hMemory для GlobalLock call GlobalLock ; получить адрес выделенной памяти push eax ; pMemory для GlobalUnlock push eax ; pMemory для SendMessage ; прочитать файл push ebx push offset SizeReadWrite push MEMSIZE-1 push eax ; pMemory для ReadFile push edi call ReadFile ; послать окну редактора сообщение wm_settext, чтобы он забрал текст из буфера push ebx ; pMemory уже в стеке push WM_SETTEXT push h_editwindow call SendMessage ; а теперь можно закрыть файл и освободить память call GlobalUnlock ; pMemory уже в стеке call GlobalFree ; hMemory уже в стеке push edi ; hFile call CloseHandle mov byte ptr flag_untitled,0 call set_title file_open_failed: push h_editwindow call SetFocus ret
; обработчик IDM_EXIT h_idm_exit: call save_contents push esi ; hWnd call DestroyWindow ; уничтожить наше окно ret
; обработчик WM_SIZE h_wm_size: ; здесь также надо послать WM_SIZE окнам toolbar и statusbar, ; изменить размер окна редактора так, чтобы оно по-прежнему было на все окно push offset rec push esi ; hWnd call GetClientRect push 1 ; true push rec. bottom ; height push rec. right ; width push ebx ; у push ebx ; x push h_editwindow call MoveWindow jmp short end_wm_check
; обработчик WM_ACTIVATE h_wm_activate: push h_editwindow call SetFocus jmp short end_wm_check
; обработчик WM_DESTROY h_wm_destroy: push ebx call PostQuitMessage ; послать WM_QUIT основной программе
; конец процедуры window_proc end_wm_check: рора xor еах,еах ; вернуть 0 leave ret 16
; процедура set_title ; устанавливает новый заголовок для основного окна set_title: push esi push edi mov edi, offset window_title cmp byte ptr flag_untitled,1 ; если у файла нет имени, je untitled ; использовать Untitled mov esi,ofn.lpstrFile ; [ESI] - имя файла с путем movzx eax,ofn.nFileOffset ; eax - начало имени файла add esi,eax copy_filename: lodsb ; скопировать файл побайтно в название окна, test al,al jz add_progname ; пока не встретится ноль stosb jmp short copy_filename add_progname: mov dword ptr [edi],'-' ; приписать минус add edi,3 mov esi,offset c_w_name mov ecx,9 ; и название программы rep movsb pop edi pop esi push offset window_title push esi ; идентификатор окна call SetWindowText ret untitled: mov dword ptr [edil'itnU' ; дописать "Unti" mov dword ptr [edi+4],'delt' ; дописать "tled" add edi,8 jmp short add_progname
; процедура save_contents ; EBX = 0, ESI = hWnd save_contents: ; спросить редактор, изменялся ли текст push ebx push ebx push EM_GETMODIFY push h_editwindow call SendMessage test eax,eax jz not_modified ; спросить пользователя, сохранять ли его push MB_YESNO + MB_ICONWARNING push offset c_w_name push offset changes_msg push esi call MessageBox cmp eax,IDYES jne not_modified ; сохранить его call h_idm_save not_modified: ret
win_proc endp
about_proc proc near ; параметры ( с учетом push ebp) ap_hDlg equ dword ptr [ebp+08h] ap_uMsg equ dword ptr [ebp+0Ch] ap_wParam equ dword ptr [ebp+10h] ap_lParam equ dword ptr [ebp+14h] push ebp mov ebp,esp ; создать стековый кадр cmp ap_uMsg,WM_COMMAND jne dont_proceed cmp ap_wParam,IDOK jne dont_proceed push 1 push ap_hDlg call EndDialog dont_proceed: xor eax,eax ; не обрабатывается leave ret 16 about_proc endp
end _start
Размер этой программы - 6,5 Кб (скомпилированной ml/link), и даже версия, в которую добавлено все, что есть в Notepad (вывод файла на печать и поиск по тексту), все равно более чем в три раза меньше notepad.exe. Чем большее Windows-приложение создается, тем сильнее сказывается выигрыш в размерах при использовании ассемблера, даже несмотря на то, что мы только вызываем системные функции, практически не занимаясь программированием.
Прежде чем можно будет скомпилировать winpad95.asm, конечно, надо внести необходимые дополнения в наши включаемые файлы.
Добавления в файл def32.inc:
; из winuser.h WM_CREATE equ 1 WM_ACTIVATE equ 6 WM_SETTEXT equ 0Ch WM_GETTEXT equ 0Dh WM_CUT equ 300h WM_COPY equ 301h WM_PASTE equ 302h WM_CLEAR equ 303h WM_UNDO equ 304h WM_SIZE equ 5 WS_VISIBLE equ 10000000h WS_CHILD equ 40000000h WS_EX_CLIENTEDGE equ 200h ES_LEFT equ 0 ES_MUITILINE equ 4 ES_AUTOHSCROLL equ 80h ES_AUTOVSCROLL equ 40h EM_GETHANDLE equ 0BDh EM_GETMODIFY equ 0B8h EM_SETMODIFY equ 0B9h EM_UNDO equ 0C7h EM_SETSEL equ 0B1h MB_YESNO equ 4 MB_ICONWARNING equ 30h IDOK equ 1 IDYES equ 6
; из winnt.h GENERIC_READ equ 80000000h GENERIC_WRITE equ 40000000h FILE_SHARE_READ equ 1 FILE_SHARE_WRITE equ 2 FILE_ATTRIBUTE_ARCHIVE equ 20h
; из commdlg.h OFN_PATHMUSTEXIST equ 800h OFN_FILEMUSTEXIST equ 1000h OFN_EXPLORER equ 80000h OFN_OVERWRITEPROMPT equ 2 OPENFILENAME struc IStructSize dd ? hwndOwner dd ? _hInstance dd ? lpstrFilter dd ? lpstrCustomFilter dd ? nMaxCustFilter dd ? nFilterlndex dd ? lpstrFile dd ? nMaxFile dd ? lpstrFileTitle dd ? nMaxFileTitle dd ? lpstrInitialDir dd ? lpstrTitle dd ? Flags dd ? nFileOffset dw ? nFileExtension dw ? lpstrDefExt dd ? lCustData dd ? lpfnHook dd ? lpTemplateName dd ? OPENFILENAME ends
; из windef. h RECT struc left dd ? top dd ? right dd ? bottom dd ? RECT ends
; из winbase.h GMEM_MOVEABLE equ 2 GMEM_ZEROINIT equ 40h OPEN_EXISTING equ 3 CREATE_ALWAYS equ 2
Добавления в файл kernel32.inc между ifdef _TASM_ и else:
extrn lstrlen:near extrn GetCommandLineA:near extrn CloseHandle:near extrn GlobalAlloc:near extrn GlobalLock:near extrn GlobalFree:near extrn CreateFileA:near extrn ReadFile:near extrn WriteFile:near GetCommandLine equ GetCommandLineA CreateFile equ CreateFileA
и между else и endif:
extrn __imp__lstrlen@4:dword extrn __imp__GetCommandLineA@0:dword extrn __imp__CloseHandle@4:dword extrn __imp__GlobalAlloc@8:dword extrn __imp__GlobalLock@4:dword extrn __imp__GlobalFree@4:dword extrn __imp__CreateFileA@28:dword extrn __imp__ReadFile@20:dword extrn __imp__WriteFile@20:dword lstrlen equ __imp__lstrlen@4 GetCommandLine equ __imp__GetCommandLineA@0 CloseHandle equ __imp__CloseHandle@4 GlobalAlloc equ __imp__GlobalAlloc@8 GlobalLock equ __imp__GlobalLock@4 GlobalFree equ __imp__GlobalFree@4 CreateFile equ __imp__CreateFileA@28 ReadFile equ __imp__ReadFile@20 WriteFile equ __imp__WriteFile@20
Добавления в файл user32.inc:
extrn LoadAcceleratorsA:near extrn TranslateAccelerator:near extrn SendMessageA:near extrn SetWindowTextA:near extrn MoveWindow:near extrn GetClientRect:near extrn GlobalUnlock:near LoadAccelerators equ LoadAcceleratorsA SendMessage equ SendMessageA SetWindowText equ SetWindowTextA
и между else и endif:
extrn __imp__LoadAcceleratorsA@8:dword extrn __imp__ТranslateAccelerator@12:dword extrn __imp__SendMessageA@16:dword extrn __imp__SetWindowTextA@8:dword extrn __imp__MoveWindow@24:dword extrn __imp__GetClientRect@8:dword extrn __imp__GlobalUnlock@4:dword LoadAccelerators equ __imp__LoadAcceleratorsA@8 TranslateAccelerator equ __imp__TranslateAccelerator@12 SendMessage equ __imp__SendMessageA@16 SetWindowText equ __imp__SetWindowTextA@8 MoveWindow equ __imp__MoveWindow@24 GetClientRect equ __imp__GetClientRect@8 GlobalUnlock equ __imp__GlobalUnlock@4
Кроме того, нам потребуется новый включаемый файл, comdlg32.inc, описывающий функции, связанные с вызовами стандартных диалогов (выбор имени файла, печать документа, выбор шрифта и т.д.):
; comdlg32.inc ; включаемый файл с функциями из comdlg32.dll ifdef _TASM_ includelib import32.lib extrn GetOpenFileNameA:near extrn GetSaveFileNameA:near GetOpenFileName equ GetOpenFileNameA GetSaveFileName equ GetSaveFileNameA else includelib comdlg32.lib ; истинные имена используемых функций extrn __imp__GetOpenFileNameA@4:dword extrn __imp__GetSaveFileNameA@4:dword ; присваивания для удобства использования GetOpenFileName equ __imp__GetOpenFileNameA@4 GetSaveFileName equ __imp__GetSaveFileNameA@4 endif
Конечно, эту программу можно еще очень долго развивать - добавить toolbar и statusbar, написать документацию, можно сделать так, чтобы выделялось не фиксированное небольшое количество памяти для переноса файла в редактор, а равное его длине. Можно также воспользоваться функциями отображения части файла в память (CreateFileMapping, OpenFileMapping, MapViewOfFile, UnmapViewOfFile), позволив работать с неограниченно большими файлами. Win32 API настолько богат функциями, что можно довольно долго заниматься только их изучением, а это относится к теме программирования на ассемблере ровно настолько, насколько относится к программированию на любом другом языке.
Ассемблер и языки высокого уровня
Ассемблер и языки высокого уровня
В предыдущем разделе, занимаясь программированием для Windows, мы уже обращались к процедурам, написанным на языке высокого уровня из программ на ассемблере, и создавали процедуры на ассемблере, к которым можно обращаться из языков высокого уровня. Для этого нужно было соблюдать определенные договоренности о передаче параметров - параметры помещались в стек справа налево, результат возвращался в ЕАХ, стек освобождался от переданных параметров самой процедурой. Эта договоренность, известная как STDCALL, конечно, не единственная, и разные языки высокого уровня используют разнообразные способы передачи параметров.
Искажение имен
Компиляторы Microsoft С (а также многие компиляторы в UNIX, как мы увидим далее) изменяют названия процедур, чтобы отразить используемый способ передачи параметров. Так, к названиям всех процедур, использующих С-конвенцию, приписывается символ подчеркивания. То есть, если в С-программе записано
some_proc();
то реально компилятор пишет
call _some_proc
и это означает, что, если эта процедура написана на ассемблере, она должна называться именно _some_proc (или использовать сложную форму записи директивы proc).
Названия процедур, использующих STDCALL, как можно было видеть из примера DLL-программы в разделе 7.4, искажаются еще более сложным образом: спереди к называнию процедуры добавляется символ подчеркивания, а сзади - символ @ и размер занимаемой параметрами области стека в байтах, (то есть в точности число, стоящее после команды ret в конце процедуры).
some_proc(a:word);
превращается в
push a call _some_proc@4
Конвенция Pascal
Самый очевидный способ выражения вызова процедуры или функции языка высокого уровня, после того как решено, что параметры передаются в стеке и возвращаются в регистре АХ/ЕАХ, - это способ, принятый в языке PASCAL (а также в BASIC, FORTRAN, ADA, OBERON, MODULA2), - просто поместить параметры в стек в естественном порядке. В этом случае запись
some_proc(a,b,c,d,e)
превращается в
push a push b push с push d push e call some_proc
Это значит, что процедура some_proc, во-первых, должна очистить стек по окончании работы (например, завершившись командой ret 10) и, во-вторых, параметры, переданные ей, находятся в стеке в обратном порядке:
some_proc proc push bp mov bp,sp ; создать стековый кадр a equ [bp+12] ; определения для простого ; доступа к параметрам b equ [bp+10] c equ [bp+8] d equ [bp+6] e equ [bp+4]
; текст процедуры, использующей параметры а, Ь, с, d, e
ret 10 some_proc endp
Этот код в точности соответствует усложненной форме директивы proc, которую поддерживают все современные ассемблеры:
some_proc proc PASCAL,а:word,b:word,с:word,d:word,e:word
; текст процедуры, использующей параметры а, Ь, с, d, e. ; Так как ВР используется в качестве указателя стекового кадра, ; его использовать нельзя!
ret ; эта команда RET будет заменена на RET 10 some_proc endp
Главный недостаток этого подхода - сложность создания функции с изменяемым числом параметров, аналогичных функции языка С printf. Чтобы определить число параметров, переданных printf, процедура должна сначала прочитать первый параметр, но она не знает его расположения в стеке. Эту проблему решает подход, используемый в С, где параметры передаются в обратном порядке.
Конвенция С
Этот способ передачи параметров используется в первую очередь в языках С и C++, а также в PROLOG и других. Параметры помещаются в стек в обратном порядке, и, в противоположность PASCAL-конвенции, удаление параметров из стека выполняет вызывающая процедура. Запись
some_proc(a,b,c,d,e)
превращается в
push e push d push с push b push a call some_proc add sp,10 ; освободить стек
Вызванная таким образом процедура может инициализироваться так: some_proc proc push bp mov bp,sp ; создать стековый кадр a equ [bp+4] ; определения для простого доступа к параметрам b equ [bp+6] с equ [bp+8] d equ [bp+10] e equ [bp+12]
; текст процедуры, использующей параметры a, b, с, d, e
pop bp ret some_proc endp
Ассемблеры поддерживают и такой формат вызова при помощи усложненной формы директивы proc с указанием языка С:
some_proc proc С,а:word,b:word,с:word,d:word,e:word
; текст процедуры, использующей параметры a, b, с, d, e. ; Так как BP применяется как указатель стекового кадра, ; его использовать нельзя!
ret some_proc endp
Мы не пользовались до сих пор этими формами записи процедур в ассемблере потому, что они скрывают от нас тот факт, что регистр ВР используется для хранения параметров и его ни в коем случае нельзя изменять, и, в случае PASCAL, что команда ret на самом деле - команда ret N.
Преимущество по сравнению с PASCAL-конвенцией заключается в том, что освобождение стека от параметров в С возлагается на вызывающую процедуру, что позволяет лучше оптимизировать код программы. Например, если мы должны вызвать несколько функций, принимающих одни и те же параметры подряд, можно не заполнять стек каждый раз заново:
push param2 push param1 call proc1 call proc2 add sp,4
эквивалентно
proc1(param1,param2); proc2(param1,param2);
и это - одна из причин, почему компиляторы с языка С создают более компактный и быстрый код по сравнению с компиляторами с других языков.
Передача параметров
Большинство языков высокого уровня передают параметры вызываемой процедуре в стеке и ожидают возвращения параметров в регистре АХ (ЕАХ) (иногда используется DX:AX (EDX:EAX), если результат не умещается в одном регистре, и ST(0), если результат число с плавающей запятой).
Смешанные конвенции
В главе 7 мы встречались с договоренностью о передаче параметров STDCALL, отличавшейся и от С, и от PASCAL-конвенций, которая применяется для всех системных функций Win32 API. Здесь параметры помещаются в стек в обратном порядке, как в С, но процедуры должны очищать стек сами, как в PASCAL.
Еще одно интересное отклонение от С-конвенции можно наблюдать в Watcom С. Этот компилятор активно использует регистры для ускорения работы программы, и параметры в функции также передаются по возможности через регистры. Например, при вызове функции с шестью параметрами
some_proc(a,b,с,d,e,f);
первые четыре параметра передаются соответственно в (Е)АХ, (E)DX, (Е)ВХ, (Е)СХ, а только начиная с пятого, параметры помещают в стек в обычном обратном порядке:
e equ [bp+4] f equ [bp+6]
Встроенный ассемблер в Pascal
function get_seed:longint var seed:longint begin asm push es mov ax,0040h mov es,ax mov ax,es:[006Ch] mov seed,ax pop es end; get_seed:=seed; end;
Встроенный ассемблер в С
int get_seed() int seed; { _asm { push es mov ax,0040h mov es,ax mov ax,es:[006Ch] mov seed,ax pop es }; return(seed); };
В этих ситуациях ассемблерная программа может свободно пользоваться переменными из языка высокого уровня, так как они автоматически преобразуются в соответствующие выражения типа word ptr [bp+4].
Встроенный ассемблер
Если требуется выполнить совсем небольшую операцию на ассемблере, например вызвать какое-то прерывание или преобразовать сложную битовую структуру, часто нерационально создавать отдельный файл ради нескольких строк на ассемблере. Чтобы этого избежать, многие языки высокого уровня поддерживают возможность вставки ассемблерного кода непосредственно в программу. Например, напишем процедуру, возвращающую слово, находящееся по адресу 0040h:006Ch, в BIOS - счетчик сигналов системного таймера, который удобно использовать для инициализации генераторов случайных чисел.
AGI
AGI- это ситуация, при которой регистр, используемый командой для генерации адреса как базовый или индексный, был приемником предыдущей команды. В этой ситуации процессор тратит один дополнительный такт. Последовательность команд
add edx,4 mov esi,[edx]
выполняется с AGI на любом процессоре.
Последовательность команд
add esi,4 ; U-конвейер - 1 такт (на Pentium) pop ebx ; V-конвейер - 1 такт inc ebx ; V-конвейер - 1 такт mov edi,[esi] ; в U-конвейер - *AGI*, затем 1 такт
выполняется с AGI на Pentium за три такта процессора.
Кроме того, AGI может происходить неявно, например при изменении регистра ESP и обращении к стеку:
sub esp,24 push ebx ; *AGI*
или
mov esp,ebp pop ebp ; *AGI*
но изменение ESP, производимое командами PUSH и POP, не приводит к AGI, если следующая команда тоже обращается к стеку.
Процессоры Pentium Pro и Pentium II не подвержены AGI.
Кэш-память
Процессор Pentium включает в себя два 8-килобайтных блока кэш-памяти, один для кода и один для данных с длиной линейки 32 байта. Кэш данных состоит из восьми банков, причем он доступен из обоих конвейеров одновременно, только если обращения происходят к разным банкам. Если данные или код не находятся в кэше, минимальная дополнительная задержка составляет 4 такта.
Если происходит два кэш-промаха одновременно при записи в память из обоих конвейеров и обе записи попадают в одно учетверенное слово (например, при записи двух слов в последовательные адреса), процессор затрачивает столько же времени, сколько и на один кэш-промах.
Процессоры Pentium Pro включают в себя 8-килобайтный кэш L1 для данных и 8-килобайтный кэш L1 для кода, а процессоры Pentium II соответственно по 16 Кб, но не все кэш-промахи приводят к чтению из памяти - существует кэш второго уровня - L2, который маскирует промахи L1. Минимальная задержка при промахе в оба кэша составляет 10 – 14 тактов в зависимости от состояния цикла обновления памяти.
Микрооперации чтения и записи могут произойти одновременно, если они обращаются к разным банкам кэша L1.
Команда LEA
LEA можно использовать (кроме прямого назначения- вычисления адреса сложно адресуемой переменной) для следующих двух ситуаций:
быстрое умножение
lea еах,[еах*2] ; ЕАХ = ЕАХ * 2 (shl eax,1 ; лучше) lea еах,[еах+еах*2] ; ЕАХ = ЕАХ * 3 lea еах,[еах*4] ; ЕАХ = ЕАХ * 4 (shl eax,2 ; лучше) lea еах,[еах+еах*4] ; ЕАХ = ЕАХ * 5 lea еах,[еах+еах*8] ; ЕАХ = ЕАХ * 9
трехоперандное сложение
lea ecx,[eax+ebx] ; ЕСХ = ЕАХ * ЕВХ
Единственный недостаток LEA - увеличивается вероятность AGI с предыдущей командой (см. ниже).
Конвейер FPU
Конвейер исполнения команд FPU состоит из трех участков, на каждом из которых команда тратит по крайней мере один такт. Многие команды, однако, построены таким образом, что позволяют другим командам выполняться на ранних участках конвейера, пока эти команды выполняются на более поздних. Кроме того, параллельно с длинными командами FPU, например FDIV, могут выполняться команды в целочисленных конвейерах.
Команда FXCH может выполняться одновременно почти с любой командой FPU, что позволяет использовать ST(n) как неупорядоченный набор регистров практически без потерь в производительности.
Конвейер ММХ
Команды ММХ, так же как команды FPU, используют дополнительный конвейер, содержащий два блока целочисленной арифметики (и логики), один блок умножения, блок сдвигов, блок доступа к памяти и блок доступа к целочисленным регистрам. Все блоки, кроме умножителя, выполняют свои стадии команды за один такт, умножение требует трех тактов, но имеет собственный буфер, позволяющий принимать по одной команде каждый такт. Так как блоков арифметики два, соответствующие операции могут выполняться одновременно в U- или V-конвейере. Команды, использующие блок сдвигов или умножитель, способны осуществляться в любом конвейере, но не одновременно с другими командами, использующими тот же самый блок. А команды, обращающиеся к памяти или обычным регистрам, в состоянии выполняться только в U-конвейере и только одновременно с ММХ-командами.
Если перед командой, копирующей ММХ-регистр в память или в обычный регистр, происходила запись в этот ММХ-регистр, затрачивается один лишний такт.
Обращение к частичному регистру
Если команда обращается к 32-битному регистру, например ЕАХ, сразу после команды, выполнявшей запись в соответствующий частичный регистр (АХ, AL, АН), происходит пауза минимум в 7 тактов на Pentium Pro и Pentium II и в 1 такт - на 80486, но не на Pentium:
mov ax,8 add ecx,eax ; пауза
На Pentium Pro и Pentium II эта пауза не появляется, если сразу перед командой записи в АХ была команда XOR ЕАХ,ЕАХ или SUB ЕАХ,ЕАХ.
Общие принципы низкоуровневой оптимизации
Так как процессоры Intel используют весьма сложный набор команд, большинство операций можно выполнить на низком уровне очень многими способами. При этом иногда оказывается, что наиболее очевидный способ- не самый быстрый или короткий. Часто простыми перестановками команд, зная механизм выполнения команд на современных процессорах, реально заставить ту же процедуру выполняться на 50 – 200% быстрее. Разумеется, переходить к этому уровню оптимизации можно только после того, как текст программы окончательно написан и максимально оптимизирован на среднем уровне.
Перечислим основные рекомендации, которым нужно следовать при оптимальном программировании для процессоров Intel Pentium, Pentium MMX, Pentium Pro и Pentium II.
Очередь предвыборки
Перед тем как команды распределяются по конвейерам, они загружаются из памяти в одну из четырех 32-байтных очередей предвыборки. Если загружается команда перехода и блок предсказания переходов утверждает, что переход произойдет, начинает работать следующая очередь предвыборки. Это сделано для того, чтобы, если предсказание было неверным, первая очередь продолжила бы выборку команд после невыполненной команды перехода.
Если условный переход не был предугадан, затрачивается 3 такта, если команда перехода находилась в U-конвейере, и 4 такта, если в V.
Если безусловный переход или вызов процедуры не был предугадан, затрачивается 3 такта в любом случае.
Очередь предвыборки считывает прямую линию кода 16-байтными выровненными блоками. Это значит, что следует организовывать условные переходы так, чтобы наиболее частым исходом было бы отсутствие перехода, и что полезно выравнивать команды на границы слова. Кроме того, желательно располагать редко используемый код в конце процедуры, чтобы он не считывался в очередь предвыборки впустую.
Оптимизация на среднем уровне
Реализация алгоритма на данном конкретном языке программирования - самая ответственная стадия оптимизации. Именно здесь можно получить выигрыш в скорости в десятки раз или сделать программу в десятки раз медленнее, при серьезных ошибках в реализации. Многие элементы, из которых складывается оптимизация, уже упоминались - хранение переменных, с которыми выполняется активная работа, в регистрах, использование таблиц переходов вместо длинных последовательностей проверок и условных переходов и т.п. Тем не менее даже плохо реализованные операции не вносят заметных замедлений в программу, если они не повторяются в цикле. Практически можно говорить, что все проблемы оптимизации на среднем уровне так или иначе связаны с циклами, и именно поэтому мы рассмотрим основные правила, которые стоит иметь в виду при реализации любого алгоритма, содержащего циклы.
Оптимизация
Наиболее популярным применением ассемблера обычно считается именно оптимизация программ, то есть уменьшение времени выполнения программ по сравнению с языками высокого уровня. Но если просто переписать текст, например с языка С на ассемблер, переводя каждую команду наиболее очевидным способом, часто оказывается, что С-процедура выполнялась быстрее. Вообще говоря, ассемблер, как и любой другой язык, сам по себе не является панацеей от неэффективного программирования - чтобы действительно оптимизировать программу, требуется не только знание команд процессора, но и знание алгоритмов, навык оптимальных способов их реализации и подробная информация об архитектуре процессора.
Проблему оптимизации принято делить на три основных уровня:
Выбор наиболее оптимального алгоритма - "высокоуровневая оптимизация".
Наиболее оптимальная реализация алгоритма - "оптимизация среднего уровня".
Подсчет тактов, тратящихся на выполнение каждой команды, и оптимизация их порядка для конкретного процессора - "низкоуровневая оптимизация".
Основные рекомендации
Используйте регистр ЕАХ всюду, где возможно. Команды с непосредственным операндом, с операндом - абсолютным адресом переменной и команды XCHG с регистрами занимают на один байт меньше, если другой операнд - регистр ЕАХ.
Используйте регистр DS всюду, где возможно. Префиксы переопределения сегмента увеличивают размер программы на 1 байт и время на 1 такт.
Если к переменной в памяти, адресуемой со смещением, выполняется несколько обращений - загрузите ее в регистр.
Не используйте сложные команды - ENTER, LEAVE, LOOP, строковые команды, если аналогичное действие можно выполнить небольшой последовательностью простых команд.
Не используйте команду MOVZX для чтения байта - это требует 4 тактов для выполнения. Заменой может служить такая пара команд:
xor еах,еах mov al,source
Используйте TEST для сравнения с нулем или для других проверок равенства:
test eax,eax jz if_zero ; переход, если ЕАХ = 0 test eax,source jz if_zero ; переход, если ЕАХ = source
Исцользуйте команду XOR, чтобы обнулять регистр (конечно, если текущее состояние флагов больше не потребуется), эта команда официально поддерживается Intel как команда обнуления регистра:
xor еах,еах ; ЕАХ = 0
Не используйте умножение или деление на константу - его можно заменить другими командами, например:
; ЕАХ = ЕАХ * 10 shl eax,1 ; умножение на 2 lea eax,[eax+eax*4] ; умножение на 5 ; ЕАХ = ЕАХ * 7 mov ebx,eax shl еах,3 ; умножение на 8 sub eax,ebx ; и вычитание сохраненного ЕАХ ; АХ = АХ/10 mov dx,6554 ; DX = 65 536/10 mul dx ; DX = AX/10 (умножение ; выполняется быстрее деления) ; ЕАХ = ЕАХ mod 64 (остаток от деления на степень двойки) and eax,3Fh
Используйте короткую форму команды jmp, где возможно (jmp short метка).
Как можно реже загружайте сегментные регистры.
Как можно меньше переключайте задачи - это очень медленная процедура. Часто, если надо сохранять небольшое состояние процесса, например для реализации нитей, переключение быстрее организовать программно.
Особенности архитектуры процессоров PentiumPro и Pentium II
Процессоры Pentium Pro и Pentium II включают в себя целый набор средств для ускорения выполнения программ. В них применяется выполнение команд не по порядку, предсказание команд, аппаратное переименование регистров и предсказание переходов.
Перенос проверки условия в конец цикла
Циклы типа WHILE или FOR, которые так часто применяются в языках высокого уровня, оказываются менее эффективными по сравнению с циклами типа UNTIL из-за того, что в них требуется лишняя команда перехода:
; цикл типа WHILE mov si,counter ; число повторов mov dx,start_i ; начальное значение loop_start: cmp dx,si ; пока dx < si - выполнять jbn exit_loop
; [тело цикла]
inc dx jmp loop_start
; почти такой же цикл типа UNTIL mov si,counter mov dx,start_i loop_start: ; выполнять
; [тело цикла]
inc dx cmp dx,si ; пока dx < si jb loop_start
Естественно, цикл типа UNTIL, в отличие от цикла типа WHILE, выполнится по крайней мере один раз, так что, если это нежелательно, придется добавить одну проверку перед телом цикла, но в любом случае даже небольшое уменьшение тела цикла всегда оказывается необходимой операцией.
Предсказание переходов
Процессор поддерживает 512-байтный буфер выполненных переходов и их целей. Система предсказания может обнаруживать последовательность до четырех повторяющихся переходов, то есть четыре вложенных цикла будут иметь процент предсказания близкий к 100%. Кроме того, дополнительный буфер адресов возврата позволяет правильно предсказывать циклы, из которых происходят вызовы подпрограмм.
На неправильно предсказанный переход затрачивается как минимум девять тактов (в среднем - от 10 до 15). На правильно предсказанный невыполняющийся переход не затрачивается никаких дополнительных тактов вообще. На правильно предсказанный выполняющийся переход затрачивается один дополнительный такт. Именно поэтому минимальное время выполнения цикла на Pentium Pro или Pentium II - два такта, и, если цикл может выполняться быстрее, он должен быть развернут.
Если команда перехода не находится в буфере, система предсказания делает следующие предположения:
безусловный переход предсказывается как происходящий, и на его выполнение затрачивается 5 – 6 тактов;
условный переход назад предсказывается как происходящий, и на его выполнение также затрачивается 5 – 6 тактов;
условный переход вперед предсказывается как непроисходящий. При этом команды, следующие за ним, предварительно загружаются, и начинают исполняться, поэтому не размещайте данные сразу после команды перехода.
Префиксы
Префиксы LOCK, переопределения сегмента и изменения адреса операнда увеличивают время выполнения команды на 1 такт.
Разворачивание циклов
Для небольших циклов время выполнения проверки условия и перехода на начало цикла может оказаться значительным по сравнению с временем выполнения самого тела цикла. Более того, на Pentium Pro/Pentium II цикл не в состоянии выполняться меньше, чем за два такта процессора, хотя его тело может выполняться даже меньше, чем за такт. С этим легко справиться, вообще не создавая цикл, а просто повторив его тело нужное число раз (разумеется, только в случае, если нам заранее известно это число!). Для очень коротких циклов можно, например, удваивать или утраивать тело цикла, если, конечно, число повторений кратно двум или трем. Кроме того, бывает удобно часть работы сделать в цикле, а часть развернуть, например продолжая цепочку циклов из предыдущего примера:
; цикл от 10 до -1 mov dx,10 loop_start:
; [тело цикла]
dec dx ; уменьшить DX, jns loop_start ; если DX не отрицательный - ; продолжить цикл ; [тело цикла]
Совершенно естественно, что эти простые методики не перечисляют все возможности оптимизации среднего уровня, более того, они не описывают и десятой доли всех ее возможностей. Умение оптимизировать программы нельзя сформулировать в виде набора простых алгоритмов - слишком много существует различных ситуаций, в которых всякий алгоритм оказывается неоптимальным. При решении любой задачи оптимизации приходится пробовать десятки различных небольших изменений, далеко не все из которых оказываются полезными. Именно потому, что оптимизация всегда занимает очень много времени, рекомендуется приступать к ней только после того, как программа окончательно написана. Как и во многих других ситуациях, с оптимизацией нельзя торопиться, но и нельзя совсем забывать о ней на любой стадии создания программы.
Вычисление констант вне цикла
Самым очевидным и самым важным правилом при создании цикла на любом языке программирования является вынос всех переменных, которые не изменяются на протяжении цикла, за его пределы. В случае ассемблера имеет смысл также по возможности разместить все переменные, которые будут использоваться внутри цикла, в регистры, а старые значения нужных после цикла регистров сохранить в стеке.
Выполнение цикла задом наперед
Циклы, в которых значение счетчика растет от двойки, единицы или нуля до некоторой константы, можно реализовать вообще без операции сравнения, выполняя цикл в обратном направлении (и мы пользовались этим приемом неоднократно в наших примерах). Дело в том, что команда DECcounter устанавливает флаги точно так же, как и команда СМР counter,1, то есть следующая команда условного перехода будет обрабатывать результат сравнения счетчика с единицей:
; цикл от 10 до 2 mov dx,10 loop_start:
; [тело цикла]
dec dx ; уменьшить DX, ja loop_start ; если DX больше 1 - продолжить цикл
; цикл от 10 до 1 mov dx,10 loop_start:
; [тело цикла]
dec dx ; уменьшить DX, jae loop_start ; если DX больше или равно 1 - продолжить цикл
; цикл от 10 до 0 mov dx,10 loop_start:
; [тело цикла]
dec dx ; уменьшить DX, jns loop_start ; если DX не отрицательный - продолжить цикл
Конечно, не все циклы можно заставить выполняться в обратном направлении сразу. Например, иногда приходится изменять формат хранения массива данных также на обратный, иногда приходится вносить другие изменения, но в целом, если это возможно, всегда следует стремиться к циклам, выполняющимся задом наперед. Кроме того, если цикл построен в этой манере, выполняется до значения счетчика, равного нулю, и регистр СХ можно освободить для выполнения роли счетчика, есть вариант воспользоваться командой LOOP, хотя в некоторых случаях в низкоуровневой оптимизации команды DEC/JNZ оказываются более эффективными.
Выполнение команд
Процессор Pentium содержит два конвейера исполнения целочисленных команд (U и V) и один конвейер для команд FPU. Он может выполнять две целочисленные команды одновременно и поддерживает механизм предсказания переходов, значительно сокращающий частоту сброса очереди предвыборки из-за передачи управления по другому адресу.
Процессор перед выполнением команды анализирует сразу две следующие команды, находящиеся в очереди и, если возможно, выполняет одну из них в U-конвейере, а другую в V. Если это невозможно, первая команда загружается в U-конвейер, а V-конвейер пустует.
V-конвейер имеет определенные ограничения на виды команд, которые могут в нем исполняться. Приложение 2 содержит для каждой команды информацию о том, может ли она выполняться одновременно с другими командами и в каком конвейере. Кроме того, две команды не будут запущены одновременно, если:
команды подвержены одной из следующих регистровых зависимостей:
Первая команда пишет в регистр, а вторая читает из него.
Обе команды пишут в один и тот же регистр (кроме записи в EFLAGS).
Исключения из этих правил- пары PUSH/PUSH, PUSH/POP и PUSH/CALL, выполняющие запись в регистр ESP;
одна из команд не находится в кэше команд (кроме случая, если первая команда - однобайтная);
одна из команд длиннее семи байт (для Pentium);
одна команда длиннее восьми байт, а другая - семи (для Pentium ММХ).
Помните, что простыми перестановками команд можно выиграть до 200% скорости в критических ситуациях.
Каждый такт процессора до трех команд может быть прочитан и декодирован на микрооперации из очереди предвыборки. В этот момент работают три декодера, первый из которых может декодировать команды, содержащие до четырех микроопераций, а другие два- только команды из одной микрооперации. Если в ассемблерной программе команды упорядочены в соответствии с этим правилом (4–1–1), то все время на каждый такт будет происходить декодирование трех команд, например, если в последовательности команд
add eax,[ebx] ; 2m - в декодер 0 на первом такте mov есх,[еах] ; 2m - пауза 1 такт, пока декодер 0 ; не освободится add edx,8 ; 1m - декодер 1 на втором такте
переставить вторую и третью команды, команда add edx,8 будет декодирована в тот же такт, что и первая команда.
Число микроопераций для каждой команды приведено в приложении 2, но можно сказать, что команды, работающие только с регистрами, как правило, выполняются за одну микрооперацию, команды чтения из памяти - тоже за одну, команды записи в память - за две, а команды, выполняющие чтение-изменение-запись, - за четыре. Сложные команды содержат больше четырех микроопераций и требуют несколько тактов для декодирования. Кроме того, команды длиннее семи байт не могут быть декодированы за один такт. В среднем время ожидания в этом буфере составляет около трех тактов.
Затем микрооперации поступают в буфер накопления, где они ждут, пока все необходимые им данные не будут доступны. Затем они посылаются в ядро системы неупорядоченного исполнения, состоящей из пяти конвейеров, каждый из которых обслуживает несколько блоков исполнения. Если все данные для микрооперации готовы и в ядре есть свободный элемент, исполняющий данную микрооперацию, в буфере накопления не будет потрачено ни одного лишнего такта. После выполнения микрооперации скапливаются в буфере завершения, где результаты их записываются, операции записи в память упорядочиваются и микрооперации завершаются (три за один такт).
Время выполнения команд в пяти конвейерах исполнения приведено в таблице 21.
Таблица 21. Конвейеры процессора Pentium Pro/Pentium II
| |
Время выполнения |
Скорость |
| Конвейер 0 |
| Блок целочисленной арифметики |
1 |
1 |
| Блок команд LEA |
1 |
1 |
| Блок команд сдвига |
1 |
1 |
| Блок целочисленного умножения |
4 |
1 |
| Блок команд FADD |
3 |
1 |
| Блок команд FMUL |
5 |
2 |
| Блок команд FDIV |
17 для 32-битных 36 для 64-битных 56 для 80-битных |
17 36 56 |
| Блок MMX-арифметики |
1 |
1 |
| Блок MMX-умножений |
3 |
1 |
| Конвейер 1 |
| Блок целочисленной арифметики |
1 |
1 |
| Блок MMX-арифметики |
1 |
1 |
| Блок MMX-сдвигов |
1 |
1 |
| Конвейер 2 |
| Блок чтения |
3 при кэш-попадании |
1 |
| Конвейер 3 |
| Блок записи адреса |
не меньше 3 |
1 |
| Конвейер 4 |
| Блок записи данных |
не меньше 1 |
1 |
Указанное в таблице время - это время, требующееся для выполнения микрооперации, а скорость - с какой частотой элемент может принимать микрооперации в собственный конвейер (1 - каждый такт, 2 - каждый второй такт). То есть, например, одиночная команда FADD выполняется за три такта, но три последовательные команды FADD выполнятся также за 3 такта.
Микрооперации чтения и записи, обращающиеся к одному и тому же адресу в памяти, выполняются за один такт.
Существует особая группа синхронизирующих команд, любая из которых начинает выполняться только после того, как завершатся все микрооперации, находящиеся в состоянии выполнения. К таким командам относятся привилегированные команды WRMSR, INVD, INVLPG, WBINVD, LGDT, LLDT, LIDT, LTR, RSM и MOV в управляющие и отладочные регистры, а также две непривилегированные команды - IRET и CPUID. В тех случаях, когда, например, измеряют скорость выполнения процедуры при помощи команды RDTSC (глава 10.2), полезно выполнить одну из синхронизирующих команд, чтобы убедиться, что все измеряемые команды полностью завершились.
Выравнивание
Восьмибайтные данные должны быть выравнены по восьмибайтным границам (то есть три младших бита адреса должны быть равны нулю).
Четырехбайтные данные должны быть выравнены по границе двойного слова (то есть два младших бита адреса должны быть равны нулю).
Двухбайтные данные должны полностью содержаться в выравненном двойном слове (то есть два младших бита адреса не должны быть равны единице).
80-битные данные должны быть выравнены по 16-байтным границам.
Когда нарушается выравнивание при доступе к данным, находящимся в кэше, теряются 3 такта на каждое невыравненное обращение на Pentium и 9 – 12 тактов - на Pentium Pro/Pentium II.
Так как линейка кэша кода составляет 32 байта, метки для переходов, особенно метки, отмечающие начало цикла, должны быть выравнены по 16-байтным границам, а массивы данных, равные или большие 32 байт, должны начинаться с адреса, кратного 32.
Высокоуровневая оптимизация
Выбор оптимального алгоритма для решения задачи всегда приводит к лучшим результатам, чем любой другой вид оптимизации. Действительно, при замене пузырьковой сортировки, время выполнения которой пропорционально N2, на быструю сортировку, выполняющуюся как N * log(N), всегда найдется такое число сортируемых элементов N, что вторая программа будет выполняться быстрее, как бы она ни была реализована. Поиск лучшего алгоритма - универсальная стадия, и она относится не только к ассемблеру, но и к любому языку программирования, поэтому будем считать, что оптимальный алгоритм уже выбран.
Процессоры Intel в защищенном режиме
Дескрипторы
Дескриптор - это 64-битная (восьмибайтная) структура данных, которая может встречаться в таблицах GDT и LDT. Дескриптор способен описывать сегмент кода, сегмент данных, сегмент состояния задачи, быть шлюзом вызова, ловушки, прерывания или задачи. В GDT также может находиться дескриптор LDT.
Дескриптор сегмента данных или кода (подробно рассмотрен в главе 6.1)
байт 7: биты 31 – 24 базы сегмента
байт 6:
бит 7: бит гранулярности (0 - лимит в байтах, 1 - лимит в 4-килобайтных единицах)
бит 6: бит разрядности (0 - 16-битный, 1 - 32-битный сегмент)
бит 5: 0
бит 4: зарезервировано для операционной системы
биты 3 – 0: биты 19 – 16 лимита
байт 5: (байт доступа)
бит 7: бит присутствия сегмента
биты 6 – 5: уровень привилегий дескриптора (DPL)
бит 4: 1 (тип дескриптора - не системный)
бит 3: тип сегмента (0 - данных, 1 - кода)
бит 2: бит подчиненности для кода, бит расширения вниз для данных
бит 1: бит разрешения чтения для кода, бит разрешения записи для данных
бит 0: бит доступа (1 - к сегменту было обращение)
байт 4: биты 23 – 16 базы сегмента
байты 3 – 2: биты 15 – 0 базы
байты 1 – 0: биты 15 – 0 лимита
Если в дескрипторе бит 4 байта доступа равен 0, дескриптор называется системным. В этом случае биты 0 – 3 байта доступа определяют один из шестнадцати возможных типов дескриптора (табл. 22).
Таблица 22. Типы системных дескрипторов
| 0 |
Зарезервированный тип |
8 |
Зарезервированный тип |
| 1 |
Свободный 16-битный TSS |
9 |
Свободный 32-битный TSS |
| 2 |
Дескриптор таблицы LDT |
A |
Зарезервированный тип |
| 3 |
Занятый 16-битный TSS |
B |
Занятый 16-битный TSS |
| 4 |
16-битный шлюз вызова |
C |
32-битный шлюз вызова |
| 5 |
Шлюз задачи |
D |
Зарезервированный тип |
| 6 |
16-битный шлюз прерывания |
E |
32-битный шлюз прерывания |
| 7 |
16-битный шлюз ловушки |
F |
32-битный шлюз ловушки |
<
/p>
Дескрипторы шлюзов
Дальние CALL или JMP на адрес с любым смещением и с селектором, указывающим на дескриптор шлюза вызова, приводят к передаче управления по адресу, указанному в дескрипторе. Обычно такие дескрипторы используются для передачи управления между сегментами с различными уровнями привилегий (см. главу 10.7).
CALL или JMP на адрес с селектором, указывающим на шлюз задачи, приводят к переключению задач (см. главу 10.8).
Шлюзы прерываний и ловушек используются для вызова обработчиков соответственно прерываний и исключений типа ловушки (см. главу 10.5).
байты 7 – 6: биты 31 – 16 смещения (0 для 16-битных шлюзов и шлюза задачи)
байт 5: (байт доступа)
бит 7: бит присутствия сегмента
биты 6 – 5: DPL - уровень привилегий дескриптора
бит 4: 0
биты 3 – 0: тип шлюза (3, 4, 5, 6, 7, В, С, Е, 7)
байт 4:
биты 7 – 5: 000
биты 4 – 0: 00000 или (для шлюза вызова) число двойных слов, которые будут скопированы из стека вызывающей задачи в стек вызываемой
байты 3 – 2: селектор сегмента
байты 1 – 0: биты 15 – 0 смещения (0 для шлюза задачи)
Дескрипторы TSS и LDT
Эти два типа дескрипторов применяются в многозадачном режиме, о котором рассказано далее. TSS - сегмент состояния задачи, используемый для хранения всей необходимой информации о каждой задаче в многозадачном режиме. LDT - таблица локальных дескрипторов, своя для каждой задачи.
Форматы этих дескрипторов совпадают с форматом дескриптора для сегмента кода или данных, за исключением того, что бит разрядности всегда равен нулю и, естественно, системный бит равен нулю, и биты 3 – 0 байта доступа содержат номер типа сегмента (1, 2, 3, 9, В). Команды JMP и CALL на адрес с селектором, соответствующим TSS незанятой задачи, приводят к переключению задач.
Машинно-специфичные регистры
Это большая группа регистров (более ста), назначение которых отличается в разных моделях процессоров Intel и даже иногда в процессорах одной модели, но разных версий. Например, регистры Pentium Pro MTRR (30 регистров) описывают, какой механизм страничной адресации используют различные области памяти - не кэшируются, защищены от записи, кэшируются прозрачно и т.д. Регистры Pentium Pro MCG/MCI (23 регистра) используются для автоматического обнаружения и обработки аппаратных ошибок, регистры Pentium TR (12 регистров) используются для тестирования кэша и т.п. Мы рассмотрим при описании соответствующих команд только регистр Pentium TSC - счетчик тактов процессора и группу из четырех регистров Pentium Pro, использующуюся для подсчета различных событий (число обращений к кэшу, умножений, команд ММХ и тому подобное), так как эти регистры оказались настолько полезными, что для работы с ними появились дополнительные команды - RDTSC и RDPMC.
Механизм защиты
Теперь рассмотрим механизм, который дал название режиму процессора, - механизм защиты. Защита может действовать как на уровне сегментов, так и на уровне страниц, ограничивая доступ в зависимости от уровня привилегий (4 уровня привилегий для сегментов и два для страниц). Защита предотвращает возможность для программ вносить изменения в области памяти, занятые операционной системой или более привилегированной программой. Процессор проверяет привилегии непосредственно перед каждым обращением к памяти и, если происходит нарушение защиты, вызывает исключение #GP.
Если процессор находится в защищенном режиме, проверки привилегий выполняются всегда и их нельзя отключить, но можно использовать во всех дескрипторах и селекторах один и тот же максимальный уровень привилегий - нулевой, и создастся видимость отсутствия защиты. Именно так мы и поступали во всех примерах до сих пор - все поля DPL и RPL инициализировались нулями. Чтобы сделать незаметной проверку прав на уровне страничной адресации, надо установить биты U и W во всех элементах таблиц страниц, что мы также делали в программе pm3.asm.
За механизм защиты отвечают следующие биты и поля:
в дескрипторах сегментов:
бит S (системный сегмент)
поле типа (тип сегмента, включая запреты на чтение/запись)
поле лимита сегмента
поле DPL, определяющее привилегии сегмента или шлюза, указывает, по крайней мере, какой уровень привилегий должна иметь программа, чтобы обратиться к этому сегменту или шлюзу
в селекторах сегментов:
поле RPL, определяющее запрашиваемые привилегии, позволяет программам, выполняющимся на высоких уровнях привилегий, обращаться к сегментам, как будто их уровень привилегий ниже
поле RPL селектора, загруженного в CS, называется CPL и является текущим уровнем привилегий программы
в элементах таблиц страниц:
бит U (определяет уровень привилегий страницы)
бит W (разрешает/запрещает запись)
Уровни привилегий в процессорах Intel определены как:
0 - максимальный (для операционной системы);
1 и 2 - промежуточные (для вспомогательных программ);
3 - минимальный (для пользовательских приложений).
Перед обращением к памяти процессор выполняет несколько типов проверок, использующих все указанные флаги и поля. Рассмотрим их по порядку.
Модель памяти в защищенном режиме
Мы уже неоднократно описывали сегментную адресацию - рассказывая о назначении сегментных регистров в реальном режиме или о программировании для расширителей DOS в защищенном режиме, но каждый раз требовалась для немедленных нужд только часть всей этой сложной модели. Теперь самое время рассмотреть ее полностью.
Для любого обращения к памяти в процессорах Intel используется логический адрес, состоящий из 16-битного селектора, определяющего сегмент, и 32- или 16-битного смещения - адреса внутри сегмента. Отдельный сегмент памяти - это независимое защищенное адресное пространство, для которого определены размер, разрешенные способы доступа (чтение/запись/исполнение кода) и уровень привилегий (см. главу 10.7). Если доступ к памяти удовлетворяет всем условиям защиты, процессор преобразует логический адрес в 32- или 36-битный (на Р6) линейный. Линейный адрес - это адрес в несегментированном непрерывном адресном пространстве, который совпадает с физическим адресом в памяти, если отключен режим страничной адресации (см. главу 10.6). Чтобы получить линейный адрес из логического, процессор добавляет к смещению линейный адрес начала сегмента, который хранится в поле базы в сегментном дескрипторе. Сегментный дескриптор - это восьмибайтная структура данных, расположенная в таблице GDT или LDT; адрес таблицы находится в регистре GDTR или LDTR, а номер дескриптора в таблице определяется из значения селектора.
Дескриптор для селектора, находящегося в сегментном регистре, не считывается из памяти при каждом обращении, а хранится в скрытой части сегментного регистра и загружается только при выполнении команд MOV в сегментный регистр, POP в сегментный регистр, LDS, LES, LSS, LGS, LFS и дальных команд перехода.
Нереальный режим
Как мы уже знаем, при изменении режима скрытые части сегментных регистров сохраняют содержимое своих дескрипторов и ими можно пользоваться. Мы осуществили эту возможность в нашем первом примере, когда значения, занесенные в сегментные регистры в реальном режиме, использовались в защищенном. Возникает вопрос - а если сделать наоборот? В защищенном режиме загрузить сегментные регистры дескрипторами 4-гигабайтных сегментов с базой 0 и перейти в реальный режим? Оказывается, что это прекрасно срабатывает, и мы попадем в особый режим, который был обнаружен одновременно разными программистами и называется нереальным режимом (unreal mode), большим реальным режимом (BRM) или реальным flat-режимом (RFM). Чтобы перейти в нереальный режим, надо загрузить в CS перед переходом в реальный режим дескриптор 16-битного сегмента кода с базой 0 и лимитом 4 Гб и в остальные сегментные регистры - точно такие же дескрипторы сегментов данных.
Теперь весь дальнейший код программы, написанный для реального режима, больше не ограничен рамками 64-килобайтных сегментов и способен работать с любыми массивами. Можно подумать, что первый же обработчик прерывания от таймера загрузит в CS нормальное значение и все станет как обычно, но нет. Оказывается, что при создании дескриптора в скрытой части сегментного регистра в реальном режиме процессор не трогает поле лимита, а только изменяет базу: что бы мы ни записали в сегментный регистр, сегмент будет иметь размер 4 Гб. Если попробовать вернуться в DOS - DOS будет по-прежнему работать. Можно запускать программы такого рода:
.model tiny .code org 100h start: xor ax,ax mov ds,ax ; DS = 0 ; вывести символ в видеопамять: mov word ptr ds:[0B8000h],8403h ret end start
и они тоже будут работать. Единственное, что отключает этот режим, - программы, переключающиеся в защищенный режим и обратно, устанавливающие границы сегментов в 64 Кб, например любые программы, использующие расширители DOS.
Нереальный режим - идеальный вариант для программ, которые хотят пользоваться 32-битной адресацией и свободно обращаться ко всем прерываниям BIOS и DOS (традиционный способ состоял бы в работе в защищенном режиме с переключением в V86 для вызова BIOS или DOS, как это делается в случае DPMI).
Для переключения в этот режим можно воспользоваться, например, такой процедурой:
; область данных: GDT label byte db 8 dup(0) ; нулевой дескриптор ; 16-битный 4 Гб сегмент: db 0FFh,0FFh,0,0,0,1001001b,11001111b,0 gdtr dw 16 ; размер GDI gdt_base dd ? ; линейный адрес GDT
; код программы ; определить линейный адрес GDT xor еах,еах mov ax,cs shl eax,4 add ax,offset GDT ; загрузить GDT из одного дескриптора (не считая нулевого) mov gdt_base,eax lgdt fword ptr gdtr ; перейти в защищенный режим cli mov eax,cr0 or al,1 mov cr0,eax jmp start_PM ; сбросить очередь предвыборки ; Intel рекомендует start_PM: ; делать jmp после каждой смены режима ; загрузить все сегментные регистры дескриптором с лимитом 4 Гб mov ax,8 ; 8 - селектор нашего дескриптора mov ds,ax mov es,ax mov fs,ax mov gs,ax ; перейти в реальный режим mov eax,cr0 and al,0FEh mov cr0,eax jmp exit_PM exit_PM: ; записать что-нибудь в каждый сегментный регистр хог ах,ах mov ds,ax mov es,ax mov fs,ax mov gs,ax sti mov ax,cs mov ds,ax ; и все - теперь процессор находится в реальном режиме ; с неограниченными сегментами
Обработка прерываний и исключений
До сих пор все наши программы работали в защищенном режиме с полностью отключенными прерываниями - ими нельзя было управлять с клавиатуры, они не могли работать с дисками и вообще не делали ничего, кроме чтения или записи в те или иные области памяти. Разумеется, ни одна программа не может сделать ничего серьезного в таком режиме - нам рано или поздно придется обрабатывать прерывания.
В реальном режиме адрес обработчика прерывания считывался процессором из таблицы, находящейся по адресу 0 в памяти. В защищенном режиме эта таблица, называемая IDT - таблицей дескрипторов прерываний, может находиться где угодно. Достаточно того, чтобы ее адрес и размер были загружены в регистр IDTR. Содержимое этой таблицы - не просто адреса обработчиков, как это было в реальном режиме, а дескрипторы трех типов: шлюз прерывания, шлюз ловушки и шлюз задачи (форматы этих дескрипторов рассматривались в предыдущей главе).
Шлюзы прерываний и ловушек указывают точку входа обработчика, а также его разрядность и уровень привилегий. При передаче управления обработчику процессор помещает в стек флаги и адрес возврата, так же как и в реальном режиме, но для некоторых исключений после этого в стек помещается дополнительный код ошибки, так что не все обработчики можно завершать простой командой IRETD (или IRET для 16-битного варианта). Единственное различие между шлюзом прерывания и ловушки состоит в том, что при передаче управления через шлюз прерывания автоматически запрещаются дальнейшие прерывания, пока обработчик не выполнит IRETD. Этот механизм считается предпочтительным для обработчиков аппаратных прерываний, в то время как шлюз ловушки, который не запрещает прерывания на время исполнения обработчика, предпочтителен для обработки программных прерываний (которые фактически и являются исключениями типа ловушки). Кроме того, в защищенном режиме при вызове обработчика прерывания сбрасывается флаг трассировки ТF.
Сначала рассмотрим пример программы, обрабатывающей только аппаратное прерывание клавиатуры при помощи шлюза прерываний. Для этого надо составить IDT, загрузить ее адрес командой LIDT и не забыть загрузить то, что содержится в регистре IDTR в реальном режиме, - адрес 0 и размер 4 * 256, соответствующие таблице векторов прерываний реального режима.
; pm2.asm ; Программа, демонстрирующая обработку аппаратных прерываний в защищенном ; режиме, переключается в 32-битный защищенный режим и позволяет набирать ; текст при помощи клавиш от 1 до +. Нажатие Backspace стирает предыдущий ; символ, нажатие Esc - выход из программы. ; ; Компиляция TASM: ; tasm /m /D_TASM_ pm2.asm ; (или, для версий 3.x, достаточно tasm /m pm2.asm) ; tlink /x /3 pm2.obj ; Компиляция WASM: ; wasm /D pm2.asm ; wlink file pm2.obj form DOS ; ; Варианты того, как разные ассемблеры записывают смещение из 32-битного ; сегмента в 16-битную переменную: ifdef _TASM_ so equ small offset ; TASM 4.x else so equ offset ; WASM endif ; для MASM, по-видимому, придется добавлять лишний код, который преобразует ; смещения, используемые в IDT
. 386р RM_seg segment para public "CODE" use16 assume cs:RM_seg,ds:PM_seg,ss:stack_seg start: ; очистить экран mov ax,3 int 10h ; подготовить сегментные регистры push PM_seg pop ds ; проверить, не находимся ли мы уже в РМ mov еах,cr0 test al,1 jz no_V86 ; сообщить и выйти mov dx,so v86_msg err_exit: mov ah,9 int 21h mov ah,4Ch int 21h ; может быть, это Windows 95 делает вид, что РЕ = О? no_V86: mov ax,1600h int 2Fh test al,al jz no_windows ; сообщить и выйти mov dx,so win_msg jmp short err_exit ; итак, мы точно находимся в реальном режиме no_windows: ; вычислить базы для всех используемых дескрипторов сегментов xor еах,еах mov ax,RM_seg shl eax,4 mov word ptr GDT_16bitCS+2,ax ; базой 16bitCS будет RM_seg shr eax,16 mov byte ptr GDT_16bitCS+4,al mov ax,PM_seg shl eax,4 mov word ptr GDT_32bitCS+2,ax ; базой всех 32bit* будет mov word ptr GDT_32bitSS+2,ax ; PM_seg mov word ptr GDT_32bitDS+2,ax shr eax,16 mov byte ptr GDT_32bitCS+4,al mov byte ptr GDT_32bitSS+4,al mov byte ptr GDT_32bitDS+4,al ; вычислить линейный адрес GDT xor еах,еах mov ax,PM_seg shl eax,4 push eax add eax,offset GDT mov dword ptr gdtr+2,eax ; загрузить GDT lgdt fword ptr gdtr ; вычислить линейный адрес IDT pop eax add eax,offset IDT mov dword ptr idtr+2,eax ; загрузить IDT lidt fword ptr idtr ; если мы собираемся работать с 32-битной памятью, стоит открыть А20 in al,92h or al,2 out 92h,al ; отключить прерывания, cli ; включая NMI, in al,70h or al,80h out 70h,al ; перейти в РМ mov еах,cr0 or al,1 mov cr0,eax ; загрузить SEL_32bitCS в CS db 66h db 0EAh dd offset PM_entry dw SEL_32bitCS RM_return: ; перейти в RM mov eax,cr0 and al,0FEh mov cr0,eax ; сбросить очередь и загрузить CS реальным числом db 0EAh dw $+4 dw RM_seg ; установить регистры для работы в реальном режиме mov ax,PM_seg mov ds,ax mov es,ax mov ax,stack_seg mov bx,stack_l mov ss,ax mov sp,bx ; загрузить IDTR для реального режима mov ax,PM_seg mov ds,ax lidt fword ptr idtr_real ; разрешить NMI in al,70h and al,07FH out 70h,al ; разрешить прерывания sti ; и выйти mov ah,4Ch int 21h RM_seg ends
; 32- битный сегмент PM_seg segment para public "CODE" use32 assume cs:PM_seg ; таблицы GDI и IDT должны быть выравнены, так что будем их размещать ; в начале сегмента GDT label byte db 8 dup(0) ; 32-битный 4-гигабайтный сегмент с базой = 0 GDT_flatDS db 0FFh,0FFh,0,0,0,10010010b,11001111b,0 ; 16-битный 64-килобайтный сегмент кода с базой RM_seg GDT_16bitCS db 0FFh,0FFh,0,0,0,10011010b,0,0 ; 32-битный 4-гигабайтный сегмент кода с базой PM_seg GDT_32bitCS db 0FFh,0FFh,0,0,0,10011010b,11001111b,0 ; 32-битный 4-гигабайтный сегмент данных с базой PM_seg GDT_32bitDS db 0FFh,0FFh,0,0,0,10010010b,11001111b,0 ; 32-битный 4-гигабайтный сегмент данных с базой stack_seg GDT_32bitSS db 0FFh,0FFh,0,0,0,10010010b,11001111b,0 gdt_size = $ - GDT gdtr dw gdt_size-1 ; лимит GDT dd ? ; линейный адрес GDT ; имена для селекторов SEL_flatDS equ 001000b SEL_16bitCS equ 010000b SEL_32bitCS equ 011000b SEL_32bitDS equ 100000b SEL_32bitSS equ 101000b
; таблица дескрипторов прерываний IDT IDT label byte ; все эти дескрипторы имеют тип 0Eh - 32-битный шлюз прерывания ; INT 00 - 07 dw 8 dup(so int_handler,SEL_32bitCS,8E00h,0) ; INT 08 (irq0) dw so irq0_7_handler,SEL_32bitCS,8E00h,0 ; INT 09 (irq1) dw so irq1_handler,SEL_32bitCS,8E00h,0 ; INT 0Ah - 0Fh (IRQ2 - IRQ8) dw 6 dup(so irq0_7_handler,SEL_32bitCS,8E00h,0) ; INT 10h - 6Fh dw 97 dup(so int_handler,SEL_32bitCS,8E00h,0) ; INT 70h - 78h (IRQ8 - IRQ15) dw 8 dup(so irq8_15_handler,SEL_32bitCS,8E00h,0) ; INT 79h - FFh dw 135 dup(so int_handler,SEL_32bitCS,8E00h,0) idt_size = $ - IDT ; размер IDT idtr dw idt_size-1 ; лимит IDT dd ? ; линейный адрес начала IDT ; содержимое регистра IDTR в реальном режиме idtr_real dw 3FFh,0,0
; сообщения об ошибках при старте v86_msg db "Процессор в режиме V86 - нельзя переключиться в РМ$" win_msg db "Программа запущена под Windows - нельзя перейти в кольцо 0$"
; таблица для перевода 0Е скан-кодов в ASCII scan2ascii db 0,1Bh,'1','2','3','4','5','6','7','8','9','0','-','=',8 screen_addr dd 0 ; текущая позиция на экране
; точка входа в 32-битный защищенный режим PM_entry: ; установить 32-битный стек и другие регистры mov ax,SEL_flatDS mov ds,ax mov es,ax mov ax,SEL_32bitSS mov ebx,stack_l mov ss,ax mov esp,ebx ; разрешить прерывания sti ; и войти в вечный цикл jmp short $
; обработчик обычного прерывания int_handler: iretd ; обработчик аппаратного прерывания IRQ0 - IRQ7 irq0_7_handler: push eax mov al,20h out 20h,al pop eax iretd ; обработчик аппаратного прерывания IRQ8 - IRQ15 irq8_15_handler: push eax mov al,20h out 0A1h,al pop eax iretd ; обработчик IRQ1 - прерывания от клавиатуры irq1_handler: push eax ; это аппаратное прерывание - сохранить регистры push ebx push es push ds in al,60h ; прочитать скан-код нажатой клавиши, cmp al,0Eh ; если он больше, чем максимальный ja skip_translate ; обслуживаемый нами, - не обрабатывать, cmp al,1 ; если это Esc, je esc_pressed ; выйти в реальный режим, mov bx,SEL_32bitDS ; иначе: mov ds,bx ; DS:EBX - таблица для перевода скан-кода mov ebx,offset scan2ascii ; в ASCII xlatb ; преобразовать mov bx,SEL_flatDS mov es,bx ; ES:EBX - адрес текущей mov ebx,screen_addr ; позиции на экране, cmp al,8 ; если не была нажата Backspace, je bs_pressed mov es:[ebx+0B8000h],al ; послать символ на экран, add dword ptr screen_addr,2 ; увеличить адрес позиции на 2, jmp short skip_translate bs_pressed: ; иначе: mov al,' ' ; нарисовать пробел sub ebx,2 ; в позиции предыдущего символа mov es:[ebx+0B8000h],al mov screen_addr,ebx ; и сохранить адрес предыдущего символа skip_translate: ; как текущий ; разрешить работу клавиатуры in al,61h or al,80h out 61h,al ; послать EOI контроллеру прерываний mov al,20h out 20h,al ; восстановить регистры и выйти pop ds pop es pop ebx pop eax iretd ; сюда передается управление из обработчика IRQ1, если нажата Esc esc_pressed: ; разрешить работу клавиатуры, послать EOI и восстановить регистры in al,61h or al,80h out 61h,al mov al,20h out 20h,al pop ds pop es pop ebx pop eax ; вернуться в реальный режим cli db 0EAh dd offset RM_return dw SEL_16bitCS PM_seg ends
; Сегмент стека. Используется как 16-битный в 16-битной части программы и как ; 32-битный (через селектор SEL_32bitSS) в 32- битной части stack_seg segment para stack "STACK" stack_start db 100h dup(?) stack_l = $ - stack_start ; длина стека для инициализации ESP stack_seg ends end start
В этом примере обрабатываются только 13 скан-кодов клавиш для сокращения размеров программы - полную информацию для преобразования скан-кодов в ASCII можно получить, воспользовавшись таблицами, приведенными в приложении 1 (рис. 18, табл. 25 и 26). Кроме того, в этом примере курсор все время остается в нижнем левом углу экрана - для его перемещения можно воспользоваться регистрами 0Eh и 0Fh контроллера CRT (см. главу 5.10.4).
Как уже упоминалось в главе 5.8, кроме прерываний от внешних устройств процессор может вызывать исключения при различных внутренних ситуациях, механизм обслуживания которых похож на механизм обслуживания аппаратных прерываний. Номера прерываний, на которые отображаются аппаратные прерывания, вызываемые первым контроллером по умолчанию, совпадают с номерами некоторых исключений. Конечно, можно из обработчика опрашивать контроллер прерываний, чтобы определить, выполняется ли обработка аппаратного прерывания или это исключение, но Intel рекомендует перенастраивать контроллер прерываний (мы это делали в главе 5.10.10) так, чтобы никакие аппаратные прерывания не попадали на область от 0 до 1Fh. В нашем примере исключения не обрабатывались, но, если программа планирует запускать другие программы или задачи, без обработки исключений обойтись нельзя.
Часть исключений (исключения типа ошибки) передает в качестве адреса возврата команду, вызвавшую исключение, а часть - адрес следующей команды. Кроме того, некоторые исключения помещают в стек код ошибки, который нужно считать, прежде чем выполнять IRETD. Поэтому пустой обработчик из одной команды IRETD в нашем примере не был корректным и многие исключения привели бы к немедленному зависанию системы.
Рассмотрим исключения в том виде, как они определены для защищенного режима.
Формат кода ошибки:
биты 15 – 3: биты 15 – 3 селектора, вызвавшего исключение
бит 2: TI - установлен, если причина исключения - дескриптор, находящийся в LDT, и сброшен, если в GDT
бит 1: IDT - установлен, если причина исключения - дескриптор, находящийся в IDT
бит 0: ЕХТ - установлен, если причина исключения - аппаратное прерывание
INT 00 - ошибка #DE "Деление на ноль"
Вызывается командами DIV или IDIV, если делитель - ноль или если происходит переполнение.
INT 01 - исключение #DB "Отладочное прерывание"
Вызывается как ловушка при пошаговой трассировке (флаг TF = 1), при переключении на задачу с установленным отладочным флагом и при срабатывании точки останова во время доступа к данным, определенной в отладочных регистрах.
Вызывается как ошибка при срабатывании точки останова по выполнению команды по адресу, определенному в отладочных регистрах.
INT 02 - прерывание NMI
Немаскируемое прерывание.
INT 03 - ловушка #ВР "Точка останова"
Вызывается однобайтной командой INT3.
INT 04 - ловушка #OF "Переполнение"
Вызывается командой INT0, если флаг OF = 1.
INT 05 - ошибка #ВС "Переполнение при BOUND"
Вызывается командой BOUND при выходе операнда за допустимые границы.
INT 06 - ошибка #UD "Недопустимая операция"
Вызывается, когда процессор пытается исполнить недопустимую команду или команду с недопустимыми операндами.
INT 07 - ошибка #NM "Сопроцессор отсутствует"
Вызывается любой командой FPU, кроме WAIT, если бит ЕМ регистра CR0 установлен в 1, и командой WAIT, если МР и TS установлены в 1.
INT 08 - ошибка #DF "Двойная ошибка"
Вызывается, если одновременно произошли два исключения, которые не могут быть обслужены последовательно. К таким исключениям относятся #DE, #TS, #NP, #SS, #GP и #РЕ
Обработчик этого исключения получает код ошибки, который всегда равен нулю.
Если при вызове обработчика #DF происходит еще одно исключение, процессор отключается и может быть выведен из этого состояния только сигналом NMI или перезагрузкой.
INT 09 - зарезервировано
Эта ошибка вызывалась сопроцессором 80387, если происходило исключение #PF или #GP при передаче операнда команды FPU.
INT 0Ah - ошибка #TS "Ошибочный TSS"
Вызывается при попытке переключения на задачу с ошибочным TSS.
Обработчик этого исключения должен вызываться через шлюз задачи.
Обработчик этого исключения получает код ошибки.
Бит ЕХТ кода ошибки установлен, если переключение пыталось выполнить аппаратное прерывание, использующее шлюз задачи, индекс ошибки равен селектору TSS, если TSS меньше 67h байт, селектору LDT, если LDT отсутствует или ошибочен, селектору сегмента стека, кода или данных, если ими нельзя пользоваться (из-за нарушений защиты или ошибок в селекторе).
INT 0Bh - ошибка #NP "Сегмент недоступен"
Вызывается при попытке загрузить в регистр CS, DS, ES, FS или GS селектор сегмента, в дескрипторе которого сброшен бит присутствия сегмента (загрузка в SS вызывает исключение #SS), а также при попытке использования шлюза, помеченного как отсутствующий, или при загрузке такой таблицы локальных дескрипторов командой LLDT (загрузка при переключении задач приводит к исключению #TS).
Если операционная система реализует виртуальную память на уровне сегментов, обработчик этого исключения может загрузить отсутствующий сегмент в память, установить бит присутствия и вернуть управление.
Обработчик этого исключения получает код ошибки.
Бит ЕХТ кода ошибки устанавливается, если причина ошибки - внешнее прерывание, бит IDT устанавливается, если причина ошибки - шлюз из IDT, помеченный как отсутствующий. Индекс ошибки равен селектору отсутствующего сегмента.
INT 0Ch - ошибка #SS "Ошибка стека"
Это исключение вызывается при попытке выхода за пределы сегмента стека при выполнении любой команды, работающей со стеком, - как явно (POP, PUSH, ENTER, LEAVE), так и неявно (MOV AX,[BP + 6]), а также при попытке загрузить в регистр SS селектор сегмента, помеченного как отсутствующий (не только при выполнении команд MOV, POP и LSS, но и при переключении задач, вызове и возврате из процедуры на другом уровне привилегий).
Обработчик этого исключения получает код ошибки.
Код ошибки равен селектору сегмента, вызвавшего ошибку, если она произошла из-за отсутствия сегмента или при переполнении нового стека в межуровневой команде CALL. Во всех остальных случаях код ошибки - ноль.
INT 0Dh - исключение #GP "Общая ошибка защиты"
Все ошибки и ловушки, не приводящие к другим исключениям, вызывают #GP - в основном нарушения привилегий.
Обработчик этого исключения получает код ошибки.
Если ошибка произошла при загрузке селектора в сегментный регистр, код ошибки равен этому селектору, во всех остальных случаях код ошибки - ноль.
INT 0Eh - ошибка #PF "Ошибка страничной адресации"
Вызывается, если в режиме страничной адресации программа пытается обратиться к странице, которая помечена как отсутствующая или привилегированная.
Обработчик этого исключения получает код ошибки.
Код ошибки использует формат, отличный от кода ошибки для других исключений:
| бит 0: |
1, если причина ошибки - нарушение привилегий; 0, если было обращение к отсутствующей странице |
| бит 1: |
1, если выполнялась операция записи, 0, если чтения |
| бит 2: |
1, если операция выполнялась из CPL = 3, 0, если CPL < 3 |
| бит 3: |
0, если ошибку вызвала попытка установить зарезервированный бит в каталоге страниц |
| |
остальные биты зарезервированы |
Кроме кода ошибки обработчик этого исключения может прочитать из регистра CR2 линейный адрес, преобразование которого в физический вызвало исключение.
Исключение #PF - основное исключение для создания виртуальной памяти с использованием механизма страничной адресации.
INT 0Fh - зарезервировано
INT 10h - ошибка #MF "Ошибка сопроцессора"
Вызывается, только если бит NE в регистре CR0 установлен в 1 при выполнении любой команды FPU, кроме управляющих команд и WAIT/FWAIT, если в FPU произошло одно из исключений FPU (см. главу 2.4.3).
INT 11h - ошибка #АС "Ошибка выравнивания"
Вызывается, только если бит AM в регистре CR0 и флаг АС из EFLAGS установлены в 1, если CPL = 3 и произошло невыравненное обращение к памяти. (Выравнивание должно быть по границе слова при обращении к слову, к границе двойного слова, к двойному слову и т.д.)
Обработчик этого исключения получает код ошибки, равный нулю.
INT 12h - останов #МС "Машинно-зависимая ошибка"
Вызывается (начиная с Pentium) при обнаружении некоторых аппаратных ошибок с помощью специальных машинно-зависимых регистров MCG_*. Наличие кода ошибки, так же как и способ вызова этого исключения, зависит от модели процессора.
INT 13h – 1Fh - зарезервировано Intel для будущих исключений
INT 20h – FFh - выделены для использования программами
Обычно для отладочных целей многие программы, работающие с защищенным режимом, устанавливают обработчики всех исключений, выдающие список регистров процессора и их содержимое, а также иногда участок кода, вызвавший исключение. В качестве примера обработчика исключения типа ошибки можно рассматривать пример программы, обрабатывающей #ВС (глава 5.8.1).
Отладочные регистры
Эти восемь 32-битных регистров (DR0 – DR7) позволяют программам, выполняющимся на уровне привилегий 0, определять точки останова, не модифицируя код программ, например для отладки ПЗУ или программ, применяющих сложные схемы защиты от трассировки. Пример отладчика, использующего эти регистры, - SoftICE.
DR7 (DCR) - регистр управления отладкой
биты 31 – 30: поле LEN для точки останова 3 (размер точки останова)
00 - 1 байт
01 - 2 байта
10 - не определен (например, для останова при выполнении)
11 - 4 байта
биты 29 – 28: поле R/W для точки останова 3 (тип точки останова)
00 - при выполнении команды
01 - при записи
10 - при обращении к порту (если бит DE в регистре CR4 = 1)
11 - при чтении или записи
биты 27 – 26: иоле LEN для точки останова 2
биты 25 – 24: поле R/W для точки останова 2
биты 23 – 22: поле LEN для точки останова 1
биты 21 – 20: поле R/W для точки останова 1
биты 19 – 18: поле LEN для точки останова 0
биты 17 – 16: поле R/W для точки останова 0
биты 15 – 14: 00
бит 13: бит GD - включает режим, в котором любое обращение к любому отладочному регистру, даже из кольца защиты 0, вызывает исключение #DB (этот бит автоматически сбрасывается внутри обработчика этого исключения)
биты 12 – 10: 001
бит 9: бит GE - если этот бит 0, точка останова по обращению к данным может не сработать или сработать на несколько команд позже, так что лучше всегда сохранять его равным 1
бит 7: бит G3 - точка останова 3 включена
бит 5: бит G2 - точка останова 2 включена
бит 3: бит G1 - точка останова 1 включена
бит 2: бит G0 - точка останова 0 включена
биты 8, 6, 4, 2, 0: биты LE, L3, L2, L1, L0 - действуют так же, как GE – G0, но обнуляются при переключении задачи (локальные точки останова)
DR6 (DSR) - регистр состояния отладки - содержит информацию о причине отладочного останова для обработчика исключения #DB
биты 31 – 16: единицы
бит 15: ВТ - причина прерывания - отладочный бит в TSS задачи, в которую только что произошло переключение
бит 14: BS - причина прерывания - флаг трассировки ТF из регистра FLAGS
бит 13: BD - причина прерывания - следующая команда собирается писать или читать отладочный регистр, и бит GD в DR7 установлен в 1
бит 12: 0
биты 11 – 4: единицы
бит 3: B3 - выполнился останов в точке 3
бит 2: B2 - выполнился останов в точке 2
бит 1: B1 - выполнился останов в точке 1
бит 0: B0 - выполнился останов в точке 0
Процессор не очищает биты причин прерывания в этом регистре, так что обработчику исключения #BD следует делать это самостоятельно. Кроме того, одновременно может произойти прерывание из-за нескольких причин, тогда несколько бит будут установлены.
DR4 – DR5 зарезервированы. На процессорах до Pentium, или если бит DE регистра CR4 равен нулю, обращение к этим регистрам приводит к обращению к DR6 и DR7 соответственно. Если бит DE = 1, происходит исключение #UD
DR0 – DR3 содержат 32-битные линейные адреса четырех возможных точек останова по доступу к памяти
Если условия для отладочного останова выполняются, процессор вызывает исключение #DB.
Переключение задач
Переключение задач осуществляется, если:
текущая задача выполняет дальний JMP или CALL на шлюз задачи или прямо на TSS;
текущая задача выполняет IRET, если флаг NT равен 1;
происходит прерывание или исключение, в качестве обработчика которого в IDT записан шлюз задачи.
При переключении процессор выполняет следующие действия:
Для команд CALL и JMP проверяет привилегии (CPL текущей задачи и RPL селектора новой задачи не могут быть больше, чем DPL шлюза или TSS, на который передается управление).
Проверяется дескриптор TSS (его бит присутствия и лимит).
Проверяется, что новый TSS, старый TSS и все дескрипторы сегментов находятся в страницах, отмеченных как присутствующие.
Сохраняется состояние задачи.
Загружается регистр TR. Если на следующих шагах происходит исключение, его обработчику придется доделывать переключение задач, вместо того чтобы повторять ошибочную команду.
Тип новой задачи в дескрипторе изменяется на занятый и устанавливается флаг TS в CR0.
Загружается состояние задачи из нового TSS: LDTR, CR3, EFLAGS, EIP, регистры общего назначения и сегментные регистры.
Если переключение задачи вызывается командами JUMP, CALL, прерыванием или исключением, селектор TSS предыдущей задачи записывается в поле связи новой задачи и устанавливается флаг NT. Если флаг NT установлен, команда IRET выполняет обратное переключение задач.
При любом запуске задачи ее тип изменяется в дескрипторе на занятый. Попытка вызвать такую задачу приводит к #GP, сделать задачу снова свободной можно, только завершив ее командой IRET или переключившись на другую задачу командой JMP.
Покажем, как создавать задачи и переключаться между ними на следующем примере.
; pm4.asm ; Пример программы, выполняющей переключение задач. ; Запускает две задачи, передающие управление друг другу 80 раз, задачи выводят ; на экран символы ASCII с небольшой задержкой ; ; Компиляция: ; TASM: ; tasm /m pm4.asm ; tlink /x /3 pm4.obj ; WASM: ; wasm pm4.asm ; wlink file pm4.obj form DOS ; MASM: ; ml /c pm4.asm ; link pm4.obj,,NUL,,,
. 386p RM_seg segment para public "CODE" use16 assume cs:RM_seg,ds:PM_seg,ss:stack_seg start: ; подготовить сегментные регистры push PM_seg pop ds ; проверить, не находимся ли мы уже в РМ mov eax,cr0 test al,1 jz no_V86 ; сообщить и выйти mov dx,offset v86_msg err_exit: push cs pop ds mov ah,9 int 21h mov ah,4Ch int 21h
; убедиться, что мы не под Windows no_V86: mov ax,1600h int 2Fh test al,al jz no_windows ; сообщить и выйти mov dx,offset win_msg jmp short err_exit
; сообщения об ошибках при старте v86_msg db "Процессор в режиме V86 - нельзя переключиться в РМ$" win_msg db "Программа запущена под Windows - нельзя перейти в кольцо 0$"
; итак, мы точно находимся в реальном режиме no_windows: ; очистить экран mov ax,3 int 10h ; вычислить базы для всех дескрипторов сегментов данных xor еах,еах mov ax,RM_seg shl eax,4 mov word ptr GDT_16bitCS+2,ax shr eax,16 mov byte ptr GDT_16bitCS+4,al mov ax,PM_seg shl eax,4 mov word ptr GDT_32bitCS+2,ax mov word ptr GDT_32bitSS+2,ax shr eax,16 mov byte ptr GDT_32bitCS+4,al mov byte ptr GDT_32bitSS+4,al ; вычислить линейный адрес GDT xor eax,eax mov ax,PM_seg shl eax,4 push eax add eax,offset GDT mov dword ptr gdtr+2,eax ; загрузить GDT lgdt fword ptr gdtr ; вычислить линейные адреса сегментов TSS наших двух задач pop eax push eax add eax,offset TSS_0 mov word ptr GDT_TSS0+2,ax shr eax,16 mov byte ptr GDT_TSS0+4,al pop eax add eax,offset TSS_1 mov word ptr GDT_TSS1+2,ax shr eax,16 mov byte ptr GDT_TSS1+4,al ; открыть А20 mov al,2 out 92h,al ; запретить прерывания cli ; запретить NMI in al,70h or al,80h out 70h,al ; переключиться в РМ mov eax,cr0 or al,1 mov cr0,eax ; загрузить CS db 66h db 0EAh dd offset PM_entry dw SEL_32bitCS
RM_return: ; переключиться в реальный режим RM mov eax,cr0 and al,0FEh mov cr0,eax ; сбросить очередь предвыборки и загрузить CS db 0EAh dw $+4 dw RM_seg ; настроить сегментные регистры для реального режима mov ax,PM_seg mov ds,ax mov es,ax mov ax,stack_seg mov bx,stack_l mov ss,ax mov sp,bx ; разрешить NMI in al,70h and al,07FH out 70h,al ; разрешить прерывания sti ; завершить программу mov ah,4Ch int 21h RM_seg ends
PM_seg segment para public "CODE" use32 assume cs:PM_seg
; таблица глобальных дескрипторов GDT label byte db 8 dup(0) GDT_flatDS db 0FFh,0FFh,0,0,0,10010010b,11001111b,0 GDT_16bitCS db 0FFh,0FFh,0,0,0,10011010b,0,0 GDT_32bitCS db 0FFh,0FFh,0,0,0,10011010b,11001111b,0 GDT_32bitSS db 0FFh,0FFh,0,0,0,10010010b,11001111b,0 ; сегмент TSS задачи 0 (32-битный свободный TSS) GDT_TSS0 db 067h,0,0,0,0,10001001b,01000000b,0 ; сегмент TSS задачи 1 (32-битный свободный TSS) GDT_TSS1 db 067h,0,0,0,0,10001001b,01000000b,0 gdt_size = $ - GDT gdtr dw gdt_size-1 ; размер GDT dd ? ; адрес GDT ; используемые селекторы SEL_flatDS equ 001000b SEL_16bitCS equ 010000b SEL_32bitCS equ 011000b SEL_32bitSS equ 100000b SEL_TSS0 equ 101000b SEL_TSS1 equ 110000b
; сегмент TSS_0 будет инициализирован, как только мы выполним переключение ; из нашей основной задачи. Конечно, если бы мы собирались использовать ; несколько уровней привилегий, то нужно было бы инициализировать стеки TSS_0 db 68h dup(0) ; сегмент TSS_1. В него будет выполняться переключение, так что надо ; инициализировать все, что может потребоваться: TSS_1 dd 0,0,0,0,0,0,0,0 ; связь, стеки, CR3 dd offset task_1 ; EIP ; регистры общего назначения dd 0,0,0,0,0,stack_l2,0,0,0B8140h ; (ESP и EDI) ; сегментные регистры dd SEL_flatDS,SEL_32bitCS,SEL_32bitSS,SEL_flatDS,0,0 dd 0 ; LDTR dd 0 ; адрес таблицы ввода-вывода
; точка входа в 32-битный защищенный режим PM_entry: ; подготовить регистры xor еах,еах mov ax,SEL_flatDS mov ds,ax mov es,ax mov ax,SEL_32bitSS mov ebx,stack_l mov ss,ax mov esp,ebx ; загрузить TSS задачи 0 в регистр TR mov ax,SEL_TSS0 ltr ax ; только теперь наша программа выполнила все требования к переходу ; в защищенный режим xor еах,еах mov edi,0B8000h ; DS:EDI - адрес начала экрана task_0: mov byte ptr ds:[edi],al ; вывести символ AL на экран ; дальний переход на TSS задачи 1 db 0EAh dd 0 dw SEL_TSS1 add edi,2 ; DS:EDI - адрес следующего символа inc al ; AL - код следующего символа, cmp al,80 ; если это 80, jb task_0 ; выйти из цикла ; дальний переход на процедуру выхода в реальный режим db 0EAh dd offset RM_return dw SEL_16bitCS
; задача 1 task_1: mov byte ptr ds:[edi],al ; вывести символ на экран inc al ; увеличить код символа add edi,2 ; увеличить адрес символа ; переключиться на задачу 0 db 0EAh dd 0 dw SEL_TSS0 ; сюда будет приходить управление, когда задача 0 начнет выполнять переход ; на задачу 1 во всех случаях, кроме первого mov ecx,02000000h ; небольшая пауза, зависящая от скорости loop $ ; процессора jmp task_1
PM_seg ends
stack_seg segment para stack "STACK" stack_start db 100h dup(?) ; стек задачи 0 stack_l = $ - stack_start stack_task2 db 100h dup(?) ; стек задачи 1 stack_l2 = $ - stack_start stack_seg ends
end start
Чтобы реализовать многозадачность в реальном времени в нашем примере, достаточно создать обработчик прерывания системного таймера IRQ0 в виде отдельной (третьей) задачи и поместить в IDT шлюз этой задачи. Текст обработчика для нашего примера мог быть крайне простым:
task_3: ; это отдельная задача - не нужно сохранять регистры! mov al,20h out 20h,al jmp task_0 mov al,20h out 20h,al jmp task_1 jmp task_3
Но при вызове обработчика прерывания старая задача помечается как занятая в GDT и повторный JMP на нее приведет к ошибке. Вызов задачи обработчика прерывания, так же как и вызов задачи командой CALL, подразумевает, что она завершится командой IRET. Именно команду IRET оказывается проще всего вызвать для передачи управления из такого обработчика - достаточно только подменить селектор вызвавшей нас задачи в поле связи и выполнить IRET.
task_3: ; при инициализации DS должен быть установлен на PM_seg mov al,20h out 20h,al mov word ptr TSS_3,SEL_TSS0 iret mov al,20h out 20h,al mov word ptr TSS_3,SEL_TSS1 iret jmp task_3
Единственное дополнительное изменение, которое нужно внести, - инициализировать дескриптор TSS задачи task_1 уже как занятый, так как управление на него будет передаваться командой IRET, что, впрочем, не составляет никаких проблем.
Помните, что во вложенных задачах команда IRET не означает конца программы - следующий вызов задачи всегда передает управление на следующую после IRET команду.
Если происходит прерывание или исключение
Если происходит прерывание или исключение в режиме V86, процессор анализирует биты IOPL регистра флагов, бит VME регистра CR4 (Pentium и выше) и соответствующий бит из карты перенаправления прерываний данной задачи (только если VME = 1).
Эта карта - 32-байтное поле, находящееся в регистре TSS данной задачи, на первый байт за концом которой указывает смещение в TSS по адресу +66h. Каждый из 256 бит этого поля соответствует одному номеру прерывания. Если он установлен в 1, прерывание должно подготавливаться обработчиком из IDT в защищенном режиме, если он 0 - то 16-битным обработчиком из реального режима.
Если VME = 0, прерывание обрабатывается (обработчиком из IDT), только если IOPL = 3, иначе вызывается исключение #GP.
Если бит VME = 1 и IOPL = 3, обработка прерывания определяется битом из битовой карты перенаправления прерываний.
Если VME = 1, IOPL < 3 и бит в битовой карте равен единице, вызывается обработчик из IDT.
Если VME = 1, IOPL < 3 и бит в битовой карте равен нулю, происходит следующее:
если VIF = 0 или если VIF = 1, но произошло исключение или NMI - вызывается обработчик из реального режима;
если VIF = 1 и произошло аппаратное прерывание - вызывается обработчик #GP из защищенного режима, который должен обработать прерывание, установить флаг VIP в копии EFLAGS в стеке и вернуться в V86;
если VIP = 1 и VIF = 0 из-за выполненной в V86 команды CLI, вызывается обработчик #GP из реального режима, который должен обнулить VIF и VIP в копии EFLAGS в стеке.
Бит VIF - это флаг, появившийся в Pentium для облегчения поддержки команд CLI и STI в V86-задачах. Если в регистре CR4 установлен бит VME, команды CLI/STI изменяют значение именно этого флага, оставляя IF нетронутым для того, чтобы операционная система могла обрабатывать прерывания и управлять другими задачами.
При вызове обработчика, располагающегося в защищенном режиме, из реального в стек нулевого уровня привилегий помещаются GS, FS, DS, ES, SS, EFLAGS, CS, EIP и код ошибки для некоторых исключений в этом порядке, и обнуляются флаги VM, TF и IF, если вызывается шлюз прерывания.
Пример программы
Мы будем пользоваться различными дескрипторами по мере надобности, а для начала выполним переключение в 32-битную модель памяти flat, где все сегменты имеют базу 0 и лимит 4 Гб. Нам потребуются два дескриптора - один для кода и один для данных. Кроме того, нужны два 16-битных дескриптора с лимитами 64 Кб, чтобы загрузить их в CS и DS перед возвратом в реальный режим.
В комментариях к примеру pm0.asm мы заметили, что его можно выполнять в DOS-окне Windows 95, хотя программа и запускается уже в защищенном режиме. Это происходит потому, что Windows 95 перехватывает обращения к контрольным регистрам и позволяет программе перейти в защищенный режим, но только с минимальным уровнем привилегий. Все следующие наши примеры в этом разделе будут рассчитаны на работу с максимальными привилегиями, поэтому добавим в программу проверку на запуск из-под Windows (функция 1600h прерывания мультиплексора INT 2Fh).
Еще одно дополнительное действие, которое будем теперь выполнять при переключении в защищенный режим, - управление линией А20. После запуска компьютера для совместимости с 8086 используются 20-разрядные адреса (работают адресные линии А0 – А19), так что попытка записать что-то по линейному адресу 100000h приведет к записи по адресу 0000h. Этот режим отменяется установкой бита 2 в порту 92h и снова включается сбрасыванием этого бита в 0. (Существуют и другие способы, зависящие от набора микросхем, используемых на материнской плате, но они бывают необходимы, только если требуется максимально возможная скорость переключения.)
; pm1.asm ; Программа, демонстрирующая работу с сегментами в защищенном режиме, ; переключается в модель flat, выполняет вывод на экран и возвращается в DOS ; ; Компиляция: TASM: ; tasm /m pm1.asm ; tlink /x /3 pm1.obj ; MASM: ; ml /c pm1.asm ; link pm1.obj,,NUL,,, ; WASM: ; wasm pm1.asm ; wlink file pm1.obj form DOS
.386p ; 32-битный защищенный режим появился в 80386
; 16-битный сегмент, в котором находится код для входа ; и выхода из защищенного режима RM_seg segment para public "code" use16 assume CS:RM_seg,SS:RM_stack start: ; подготовить сегментные регистры push cs pop ds ; проверить, не находимся ли мы уже в РМ mov еах,cr0 test al,1 jz no_V86 ; сообщить и выйти mov dx,offset v86_msg err_exit: mov ah,9 int 21h mov ah,4Ch int 21h, v86_msg db "Процессор в режиме V86 - нельзя переключиться в РМ$" win_msg db "Программа запущена под Windows - нельзя перейти в кольцо 0$"
; может быть, это Windows 95 делает вид, что РЕ = 0? no_V86: mov ax,1600h ; Функция 1600h int 2Fh ; прерывания мультиплексора, test al,al ; если AL = 0, jz no_windows ; Windows не запущена ; сообщить и выйти, если мы под Windows mov dx,offset win_msg jmp short err_exit
; итак, мы точно находимся в реальном режиме no_windows: ; если мы собираемся работать с 32-битной памятью, стоит открыть А20 in al,92h or al,2 out 92h,al ; вычислить линейный адрес метки PM_entry xor еах,еах mov ax,PM_seg ; АХ - сегментный адрес PM_seg shl eax,4 ; ЕАХ - линейный адрес PM_seg add eax,offset PM_entry ; EAX - линейный адрес PM_entry mov dword ptr pm_entry_off,eax ; сохранить его ; вычислить базу для GDT_16bitCS и GDT_16bitDS xor eax,eax mov ax,cs ; AX - сегментный адрес RM_seg shl eax,4 ; ЕАХ - линейный адрес RM_seg push eax mov word ptr GDT_16bitCS+2,ax ; биты 15 - 0 mov word ptr GDT_16bitDS+2,ax shr eax,16 mov byte ptr GDT_16bitCS+4,al ; и биты 23 - 16 mov byte ptr GDT_16bitDS+4,al ; вычислить абсолютный адрес метки GDT pop eax ; EAX - линейный адрес RM_seg add ax,offset GDI ; EAX - линейный адрес GDT mov dword ptr gdtr+2,eax ; записать его для GDTR ; загрузить таблицу глобальных дескрипторов lgdt fword ptr gdtr ; запретить прерывания cli ; запретить немаскируемое прерывание in al,70h or al,80h out 70h,al ; переключиться в защищенный режим mov eax,cr0 or al,1 mov cr0,eax ; загрузить новый селектор в регистр CS db 66h ; префикс изменения разрядности операнда db 0EAh ; код команды дальнего jmp pm_entry_off dd ? ; 32-битное смещение dw SEL_flatCS ; селектор RM_return: ; сюда передается управление при выходе из защищенного режима ; переключиться в реальный режим mov еах,cr0 and al,0FEh mov cr0,eax ; сбросить очередь предвыборки и загрузить CS реальным сегментным адресом db 0EAh ; код дальнего jmp dw $+4 ; адрес следующей команды dw RM_seg ; сегментный адрес RM_seg ; разрешить NMI in al,70h and al,07Fh out 70h,al ; разрешить другие прерывания sti ; подождать нажатия любой клавиши mov ah,0 int 16h ; выйти из программы mov ah,4Ch int 21h ; текст сообщения с атрибутами, который мы будем выводить на экран message db 'Н',7,'е',7,'l',7,'l',7,'о',7,' ',7,'и',7,'з',7,' ',7 db '3',7,'2',7,'-',7,'б',7,'и',7,'т',7,'н',7,'о',7,'г',7 db 'о',7,' ',7,'Р',7,'М' message_l = $ - message ; длина в байтах rest_scr = (80*25*2-message_l)/4 ; длина оставшейся части экрана ; в двойных словах ; таблица глобальных дескрипторов GDT label byte ; нулевой дескриптор (обязательно должен быть на первом месте) db 8 dup(0) ; 4-гигабайтный код, DPL = 00: GDT_flatCS db 0FFh,0FFh,0,0,0,10011010b,11001111b,0 ; 4-гигабайтные данные, DPL = 00: GDT_flatDS db 0FFh,0FFh,0,0,0,10010010b,11001111b,0 ; 64-килобайтный код, DPL = 00: GDT_16bitCS db 0FFh,0FFh,0,0,0,10011010b,0,0 ; 64-килобайтные данные, DPL = 00: GDT_16bitDS db 0FFh,0FFh,0,0,0,10010010b,0,0 GDT_l = $ - GDT ; размер GDT
Процессоры Intel в защищенном режиме
Мы уже неоднократно сталкивались с защищенным режимом и даже программировали приложения, которые работали в нем (главы 6 и 7). но мы пользовались только средствами, которые предоставляла операционная система, и до сих пор не рассматривали то, как процессор переходит и функционирует в защищенном режиме, то есть как работают современные операционные системы. Дело в том, что управление защищенным режимом в современных процессорах Intel - это самый сложный раздел программирования и самая сложная глава в этой книге. Но материал можно легко освоить, если рассматривать этот раздел шаг за шагом - отдельные механизмы работы процессора достаточно мало перекрываются друг с другом. Прежде чем рассматривать собственно программирование, познакомимся с регистрами и командами процессора, которые пока были от нас скрыты.
Проверка лимитов
Поле лимита в дескрипторе сегмента запрещает доступ к памяти за пределами сегмента. Если бит G дескриптора равен нулю, значения лимита могут быть от 0 до FFFFFh (1 Мб). Если бит G установлен - от FFFh (4 Кб) до FFFFFFFFh (4 Гб). Для сегментов, растущих вниз, лимит принимает значения от указанного плюс 1 до FFFFh для 16-битных сегментов данных и до FFFFFFFFh - для 32-битных. Эти проверки отлавливают такие ошибки, как неправильные вычисления адресов.
Перед проверкой лимита в дескрипторе процессор выясняет лимит самой таблицы дескрипторов на тот случай, если указано слишком большое значение селектора.
Во всех случаях вызывается исключение #GP с кодом ошибки, равным индексу селектора, вызвавшего нарушение защиты.
Проверка привилегий
Все неравенства здесь арифметические, то есть А > В означает, что уровень привилегий А меньше, чем В:
при загрузке регистра DS, ES, FS или GS должно выполняться условие: DPL >= max(RPL,CPL);
при загрузке регистров SS должно выполняться условие: DPL = CPL = RPL;
при дальних JMP, CALL, RET на неподчиненный сегмент кода должно выполняться условие: DPL = CPL (RPL игнорируется);
при дальних JMP, CALL, RET на подчиненный сегмент кода должно выполняться условие: CPL >= DPL. При этом CPL не изменяется;
при дальнем CALL на шлюз вызова должны выполняться условия: CPL =< DPL шлюза, RPL =< DPL шлюза, CPL >= DPL сегмента;
при дальнем JMP на шлюз вызова должны выполняться условия: CPL =< DPL шлюза, RPL =< DPL шлюза, CPL >= DPL сегмента, если он подчиненный, CPL = DPL сегмента, если он неподчиненный.
При вызове процедуры через шлюз на неподчиненный сегмент кода с другим уровнем привилегий процессор выполняет переключение стека. В сегменте TSS текущей задачи всегда хранятся значения SS:ESP для стеков уровней привилегий 0, 1 и 2 (стек для уровня привилегий 3, потому что нельзя выполнять передачу управления на уровень 3, кроме как при помощи команд RET/IRET). При переключении стека в новый стек помещаются, до обратного адреса, параметров (их число указано в дескрипторе шлюза вызова), флагов или кода ошибки (в случае INT), старые значения SS:ESP, которые команда RET/IRET использует для обратного переключения. То, что надо выполнить возврат из процедуры, RET определяет так: RPL селектора, оставленного в стеке, больше (менее привилегированный), чем CPL.
Даже если операционная система не поддерживает многозадачность, она должна оформить сегмент TSS с действительными SS:ESP для стеков всех уровней, если она собирается использовать уровни привилегий.
Проверка типа сегмента
Загрузка селектора (и дескриптора) в регистр:
в CS можно загрузить только сегмент кода;
в DS, ES, FS, GS можно загрузить только селектор сегмента данных, сегмента кода, доступного для чтения, или нулевой селектор;
в SS можно загрузить только сегмент данных, доступный для записи;
в LDTR можно загрузить только сегмент LDT;
в TR можно загрузить только сегмент TSS.
Обращение к памяти:
никакая команда не может писать в сегмент кода;
никакая команда не может писать в сегмент данных, защищенный от записи;
никакая команда не может читать из сегмента кода, защищенного от чтения;
нельзя обращаться к памяти, если селектор в сегментном регистре нулевой.
Исполнение команды, использующей селектор в качестве операнда:
дальние CALL и JMP могут выполняться только в сегмент кода, шлюз вызова, шлюз задачи или сегмент TSS;
команда LLDT может обращаться только к сегменту LDT;
команда LTR может обращаться только к сегменту TSS;
команда LAR может обращаться только к сегментам кода и данных, шлюзам вызова и задачи, LDT и TSS;
команда LSL может обращаться только к сегментам кода, данных, LDT и TSS;
элементами IDT могут быть только шлюзы прерываний, ловушек и задач.
Некоторые внутренние операции:
при переключении задач целевой дескриптор может быть только TSS или шлюзом задачи;
при передаче управления через шлюз сегмент, на который шлюз указывает, должен быть сегментом кода (или TSS для шлюза задачи);
при возвращении из вложенной задачи селектор в поле связи TSS должен быть селектором сегмента TSS.
Регистры управления памятью
Эти четыре регистра используются для указания положения структур данных, ответственных за сегментацию в защищенном режиме.
GDTR: 6-байтный регистр, в котором содержатся 32-битный линейный адрес начала таблицы глобальных дескрипторов (GDT) и ее 16-битный размер (минус 1). Каждый раз, когда происходит обращение к памяти, по селектору, находящемуся в сегментном регистре, определяется дескриптор из таблицы GDT или LDT, в котором записан адрес начала сегмента и другая информация (см. главу 6.1).
IDTR: 6-байтный регистр, в котором содержатся 32-битный линейный адрес начала таблицы глобальных дескрипторов обработчиков прерываний (IDT) и ее 16-битный размер (минус 1). Каждый раз, когда происходит прерывание или исключение, процессор передает управление на обработчик, описываемый дескриптором из IDT с соответствующим номером.
LDTR: 10-байтный регистр, в котором содержатся 16-битный селектор для GDT и весь 8-байтный дескриптор из GDT, описывающий текущую таблицу локальных дескрипторов (LDT).
TR: 10-байтный регистр, в котором содержатся 16-битный селектор для GDT и весь 8-байтный дескриптор из GDT, описывающий TSS текущей задачи.
Регистры управления процессором
Пять 32-битных регистров CR0 – CR4 управляют функционированием процессора и работой отдельных его внутренних блоков.
CR0: флаги управления системой
бит 31: бит PG - включает и выключает режим страничной адресации
бит 30: бит CD - запрещает заполнение кэша. При этом чтение из кэша все равно будет происходить
бит 29: бит NW - запрещает сквозную запись во внутренний кэш - данные, записываемые в кэш, не появляются на внешних выводах процессора
бит 18: бит AM - разрешает флагу АС включать режим, в котором невыровненные обращения к памяти на уровне привилегий 3 вызывают исключение #АС
бит 16: бит WP - запрещает запись в страницы, помеченные как только для чтения на всех уровнях привилегий (если WP = 0, защита распространяется только на уровень 3). Этот бит предназначен для реализации метода создания копии процесса, популярного в UNIX, в котором вся память нового процесса сначала полностью совпадает со старым, а затем, при попытке записи со стороны нового процесса, создается копия страницы, в которую произошла запись
бит 5: бит NE - включает режим, в котором ошибки FPU вызывают исключение #MF, а не IRQ13
бит 4: бит ЕТ - использовался только на 80386DX и указывал, что FPU присутствует
бит 3: бит TS - устанавливается процессором после переключения задачи. Если затем выполнить любую команду FPU, произойдет исключение #NM, обработчик которого может сохранить/восстановить состояние FPU, очистить этот бит командой CLTS и продолжить программу
бит 2: бит ЕМ - эмуляция сопроцессора. Каждая команда FPU вызывает исключение #NM
бит 1: бит МР - управляет тем, как исполняется команда WAIT. Должен быть установлен для совместимости с программами, написанными для 80286 и 80386 и использующими эту команду
бит 0: бит РЕ - если он равен 1, процессор находится в защищенном режиме
(остальные биты зарезервированы, и программы не должны изменять их значения)
CR1: зарезервирован
CR2: регистр адреса ошибки страницы
Когда происходит исключение #PF, из этого регистра можно прочитать линейный адрес, обращение к которому вызвало исключение.
CR3 (PDBR): регистр основной таблицы страниц
биты 31 – 11: 20 старших бит физического адреса начала каталога страниц, если бит РАЕ в CR4 равен нулю, или
биты 31 – 5: 27 старших бит физического адреса таблицы указателей на каталоги страниц, если бит РАЕ = 1
бит 4 (80486+): бит PCD (запрещение кэширования страниц) - этот бит запрещает загрузку текущей страницы в кэш-память (например, если произошло прерывание и система не хочет, чтобы обработчик прерывания вытеснил основную программу из кэша)
бит 3 (80486+): бит PWT (бит сквозной записи страниц) - управляет методом записи страниц во внешний кэш
CR4: этот регистр (появился только в процессорах Pentium) управляет новыми возможностями процессоров. Все эти возможности необязательно присутствуют, и их надо сначала проверять при помощи команды CPUID
бит 9: бит FSR - разрешает команды быстрого сохранения/восстановления состояния FPU/MMX FXSAVE и FXRSTOR (Pentium II)
бит 8: бит РМС - разрешает выполнение команды RDPMC для программ на всех уровнях привилегий (его PMC = 0, но только на уровне 0) (Pentium Pro и выше)
бит 7: бит PGE - разрешает глобальные страницы (бит 8 атрибута страницы), которые не удаляются из TLB при переключении задач и записи в CR3 (Pentium Pro и выше)
бит 6: бит МСЕ - разрешает исключение #МС
бит 5: бит РАЕ - включает 36-битное физическое адресное пространство (Pentium Pro и выше)
бит 4: бит PSE - включает режим адресации с 4-мегабайтными страницами
бит 3: бит DE - запрещает отладочные прерывания по обращению к портам
бит 2: бит TSD - запрещает выполнение команды RDTSC для всех программ, кроме программ, выполняющихся на уровне привилегий 0
бит 1: бит PVI - разрешает работу флага VIF в защищенном режиме, что может позволить некоторым программам, написанным для уровня привилегий 0, работать на более низких уровнях
бит 0: бит VME - включает расширения режима V86 - разрешает работу флага VIF для V86-приложений
Регистры
Рассматривая регистры процессора в главе 2.1, мы специально оставили в стороне несколько регистров, не использующихся в обычном программировании, в основном именно потому, что они управляют защищенным режимом.
в которой флаг VM регистра
Режим V86 - это задача, исполняющаяся в защищенном режиме, в которой флаг VM регистра EFLAGS равен единице. Внутри этой задачи процессор ведет себя так, как если бы он находился в реальном режиме, за исключением того, что прерывания и исключения передаются обработчикам защищенного режима вне этой задачи (кроме случая, когда используется карта перенаправления прерываний).
Программы не могут изменить флаг VM. Его можно установить, только записав образ EFLAGS с установленным VM при создании TSS новой задачи и затем переключившись на нее. Кроме этой задачи для нормальной реализации V86 требуется монитор режима (VMM) - модуль, который выполняется с CPL = 0 и обрабатывает прерывания, исключения и обращения к портам ввода-вывода из задачи V86, выполняя фактически эмуляцию всего компьютера.
Чтобы выполнять в системе сразу несколько V86-задач, применяется страничная адресация. Каждая V86-задача использует ровно один мегабайт линейного адресного пространства, который можно отобразить на любую область физического.
Процессор переключается в V86 в трех ситуациях:
при переключении в задачу, в TSS которой установлен флаг VM;
при выполнении команды IRET, если NT = 0 и установлен VM в копии EFLAGS в стеке;
при выполнении команды IRET, если NT = 1 и установлен VM в копии EFLAGS в TSS.
Сегмент состояния задачи
Сегмент состояния задачи (TSS) - это структура данных, в которой сохраняется вся информация о задаче, если ее выполнение временно прерывается.
TSS имеет следующую структуру:
+00h: 4 байта - селектор предыдущей задачи (старшее слово содержит нули - здесь и для всех остальных селекторов)
+04h: 4 байта - ESP для CPL = 0
+08h: 4 байта - SS для CPL = 0
+0Ch: 4 байта - ESP для CPL = 1
+10h: 4 байта - SS для CPL = 1
+14h: 4 байта - ESP для CPL = 2
+18h: 4 байта - SS для CPL = 2
+1Сh: 4 байта - CR3
+20h: 4 байта - EIP
+24h: 4 байта - EFLAGS
+28h: 4 байта - ЕАХ
+2Ch: 4 байта - ЕСХ
+30h: 4 байта - EDX
+34h: 4 байта - ЕВХ
+38h: 4 байта - ESP
+3Ch: 4 байта - ЕВР
+40h: 4 байта - ESI
+44h: 4 байта - EDI
+48h: 4 байта - ES
+4Ch: 4 байта - CS
+50h: 4 байта - SS
+54h: 4 байта - DS
+58Н: 4 байта - FS
+5Ch: 4 байта - GS
+60h: 4 байта - LDTR
+64h: 2 байта - слово флагов задачи. Бит 0 - флаг Т: вызывает #DB при переключении на задачу остальные биты не определены и равны нулю
+66h: 2 байта - адрес битовой карты ввода-вывода. Это 16-битное смещение от начала TSS, по которому начинается битовая карта разрешения ввода-вывода (см. главы 10.7.4 и 10.9.2) и заканчивается битовая карта перенаправления прерываний (см. главу 10.9.1) данной задачи.
TSS является полноценным сегментом и описывается сегментным дескриптором, формат которого мы приводили раньше (в главе 10.4.3). Кроме того, лимит TSS не может быть меньше 67h - обращение к такому дескриптору приводит к исключению #TS. Размер TSS может быть больше, если в него входят битовые карты ввода-вывода и перенаправления прерываний и если операционная система хранит в нем дополнительную информацию. Дескриптор TSS способен находиться только в GDT - попытка загрузить его из LDT вызывает исключение #GP. Для передачи управления задачам удобнее использовать дескрипторы шлюза задачи, которые можно помещать как в GDT, так и в LDT или IDT.
Селектор
Селектор - это 16-битное число следующего формата:
биты 16 – 3: номер дескриптора в таблице (от 0 до 8191)
бит 2: 1 - использовать LDT, 0 - использовать GDT
биты 1 – 0: запрашиваемый уровень привилегий при обращении к сегменту и текущий уровень привилегий для селектора, загруженного в CS
Селектор, содержащий нулевые биты 16 – 3, называется нулевым и используется для загрузки в неиспользуемые сегментные регистры. Любое обращение в сегмент, адресуемый нулевым селектором, приводит к исключению #GP(0), в то время как даже загрузка в сегментный регистр ошибочного селектора вызывает исключение #GР(селектор). Попытка загрузки нулевого селектора в SS или CS также вызывает #GP(0), так как эти селекторы используются всегда.
Системные флаги
Регистр флагов EFLAGS - это 32-битный регистр, в то время как в главе 2.1.4 рассмотрена только часть из младших 16 бит. Теперь мы можем обсудить все:
биты 31 – 22: нули
бит 21: флаг идентификации (ID)
бит 20: флаг ожидания виртуального прерывания (VIP)
бит 19: флаг виртуального прерывания (VIF)
бит 18: флаг контроля за выравниванием (АС)
бит 17: флаг режима V86 (VM)
бит 16: флаг продолжения задачи (RF)
бит 15: 0
бит 14: флаг вложенной задачи (NT)
биты 13 – 12: уровень привилегий ввода-вывода (IOPL)
бит 11: флаг переполнения (OF)
бит 10: флаг направления (DF)
бит 9: флаг разрешения прерываний (IF)
бит 8: флаг трассировки (TF)
биты 7 – 0: флаги состояния (SF, ZF, AF, PF, CF) были рассмотрены подробно раньше
| Флаг TF: |
если он равен 1, перед выполнением каждой команды генерируется исключение #DB (INT 1). |
| Флаг IF: |
если он равен 0, процессор не реагирует ни на какие маскируемые аппаратные прерывания. |
| Флаг DP: |
если он равен 1, регистры EDI/ESI при выполнении команд строковой обработки уменьшаются, иначе - увеличиваются. |
| Поле IOPL: |
уровень привилегий ввода-вывода, с которым выполняется текущая программа или задача. Чтобы программа могла обратиться к порту ввода-вывода, ее текущий уровень привилегий (CPL) должен быть меньше или равен IOPL. Это поле можно модифицировать, только имея нулевой уровень привилегий. |
| Флаг NT: |
равен 1, если текущая задача является вложенной по отношению к какой-то другой - в обработчиках прерываний и исключений и вызванных командой call задачах. Флаг влияет на работу команды IRET. |
| Флаг RF: |
когда этот флаг равен 1, отладочные исключения временно запрещены. Он устанавливается командой IRETD из обработчика отладочного прерывания, чтобы #DB не произошло перед выполнением команды, которая его вызвала, еще раз. На флаг не влияют команды POPF, PUSHF и IRET. |
| Флаг VM: |
установка этого флага переводит процессор в режим V86 (виртуальный 8086). |
| Флаг АС: |
если установить этот флаг и флаг AM в регистре CR0, каждое обращение к памяти из программ, выполняющихся с CPL = 3, не выровненное на границу слова для слов и на границу двойного слова для двойных слов, будет вызывать исключение #АС. |
| Флаг VIF: |
это виртуальный образ флага IF (только для Pentium и выше). |
| Флаг VIP: |
этот флаг указывает процессору, что произошло аппаратное прерывание. Флаги VIF и VIP используются в многозадачных средах для того, чтобы каждая задача имела собственный виртуальный образ флага IF (только для Pentium и выше - см. главу 5.9.1). |
| Флаг ID: |
если программа может изменить значение этого флага - процессор поддерживает команду CPUID (только для Pentium и выше). |
Системные и привилегированные команды
| Команда: |
LGDT источник |
| Назначение: |
Загрузить регистр GDTR |
| Процессор: |
80286 |
Команда загружает значение источника (6-байтная переменная в памяти) в регистр GDTR. Если текущая разрядность операндов 32 бита, в качестве размера таблицы глобальных дескрипторов используются младшие два байта операнда, а в качестве ее линейного адреса - следующие 4. Если текущая разрядность операндов - 16 бит, для линейного адреса используются только байты 3, 4, 5 из операнда, а в самый старший байт адреса записываются нули.
Команда выполняется только в реальном режиме или при CPL = 0.
| Команда: |
SGDT приемник |
| Назначение: |
Прочитать регистр GDTR |
| Процессор: |
80286 |
Помещает содержимое регистра GDTR в приемник (6-байтная переменная в памяти). Если текущая разрядность операндов - 16 бит, самый старший байт этой переменной заполняется нулями (начиная с 80386, а 286 заполнял его единицами).
| Команда: |
LLDT источник |
| Назначение: |
Загрузить регистр LDTR |
| Процессор: |
80286 |
Загружает регистр LDTR, основываясь на селекторе, находящемся в источнике (16-битном регистре или переменной). Если источник - 0, все команды, кроме LAR, LSL, VERR и VERW, обращающиеся к дескрипторам из LDT, будут вызывать исключение #GP.
Команда выполняется только в защищенном режиме с CPL = 0.
| Команда: |
SLDT приемник |
| Назначение: |
Прочитать регистр LDTR |
| Процессор: |
80286 |
Помещает селектор, находящийся в регистре LDTR, в приемник (16- или 32-битный регистр или переменная). Этот селектор указывает на дескриптор в GDT текущей LDT. Если приемник 32-битный, старшие 16 бит обнуляются на Pentium Pro и не определены на предыдущих процессорах.
Команда выполняется только в защищенном режиме.
| Команда: |
LTR источник |
| Назначение: |
Загрузить регистр TR |
| Процессор: |
80286 |
Загружает регистр задачи TR, основываясь на селекторе, находящемся в источнике (16-битном регистре или переменной), указывающем на сегмент состояния задачи (TSS). Эта команда обычно используется при инициализации системы для загрузки первой задачи в многозадачной системе.
Команда выполняется только в защищенном режиме с CPL = 0.
| Команда: |
STR приемник |
| Назначение: |
Прочитать регистр TR |
| Процессор: |
80286 |
Помещает селектор, находящийся в регистре TR, в приемник (16- или 32-битный регистр или переменная). Этот селектор указывает на дескриптор в GDT, описывающий TSS текущей задачи. Если приемник 32-битный, старшие 16 бит обнуляются на Pentium Pro и не определены на предыдущих процессорах.
Команда выполняется только в защищенном режиме.
| Команда: |
LIDT источник |
| Назначение: |
Загрузить регистр IDTR |
| Процессор: |
80286 |
Загружает значение источника (6-байтная переменная в памяти) в регистр IDTR. Если текущая разрядность операндов - 32 бита, в качестве размера таблицы глобальных дескрипторов используются младшие два байта операнда, а в качестве ее линейного адреса - следующие 4. Если текущая разрядность операндов - 16 бит, для линейного адреса используются только байты 3, 4, 5 из операнда, а самый старший байт адреса устанавливается нулевым.
Команда выполняется только в реальном режиме или при CPL = 0.
| Команда: |
SIDT приемник |
| Назначение: |
Прочитать регистр IDTR |
| Процессор: |
80286 |
Помещает содержимое регистра GDTR в приемник (6-байтная переменная в памяти). Если текущая разрядность операндов - 16 бит, самый старший байт этой переменной заполняется нулями (начиная с 80386, а 286 заполнял его единицами).
| Команда: |
MOV приемник, источник |
| Назначение: |
Пересылка данных в/из управляющих и отладочных регистров |
| Процессор: |
80386 |
Приемником или источником команды MOV могут быть регистры CR0 – CR4 и DR0 – DR7. В этом случае другой операнд команды обязательно должен быть 32-битным регистром общего назначения. При записи в регистр CR3 сбрасываются все записи в TLB, кроме глобальных страниц в Pentium Pro. При модификации бит РЕ или PG в CR0 и PGE, PSE или РАЕ в CR4 сбрасываются все записи в TLB без исключения.
Команды выполняются только в реальном режиме или с CPL = 0.
| Команда: |
LMSW источник |
| Назначение: |
Загрузить слово состояния процессора |
| Процессор: |
80286 |
Копирует младшие четыре бита источника (16-битный регистр или переменная) в регистр CR0, изменяя биты РЕ, МР, ЕМ и TS. Кроме того, если бит РЕ = 1, этой командой его нельзя обнулить, то есть нельзя выйти из защищенного режима. Команда LMSW существует только для совместимости с процессором 80286, и вместо нее всегда удобнее использовать mov cr0,еах.
Команда выполняется только в реальном режиме или с CPL = 0.
| Команда: |
SMSW приемник |
| Назначение: |
Прочитать слово состояния процессора |
| Процессор: |
80286 |
Копирует младшие 16 бит регистра CR0 в приемник (16- или 32-битный регистр или 16-битная переменная). Если приемник 32-битный, значения его старших бит не определены. Команда SMSW существует только для совместимости с процессором 80286, и вместо нее удобнее использовать mov еах,cr0.
| Команда: |
CLTS |
| Назначение: |
Сбросить флаг TS в CR0 |
| Процессор: |
80286 |
Команда сбрасывает в 0 бит TS регистра CR0, который устанавливается процессором в 1 после каждого переключения задач. CLTS предназначена для синхронизации сохранения/восстановления состояния FPU в многозадачных операционных системах: первая же команда FPU в новой задаче при TS = 1 вызовет исключение #NM, обработчик которого сохранит состояние FPU для старой задачи и восстановит сохраненное ранее для новой, после чего выполнит команду CLTS и вернет управление.
Команда выполняется только в реальном режиме или с CPL = 0.
| Команда: |
ARPL приемник,источник |
| Назначение: |
Коррекция поля RPL селектора |
| Процессор: |
80286 |
Команда выполняет сравнение полей RPL двух сегментных селекторов. Приемник (16-битный регистр или переменная) содержит первый, а источник (16-битный регистр) содержит второй. Если RPL приемника меньше, чем RPL источника, устанавливается флаг ZF, и RPL приемника становится равным RPL источника. В противном случае ZF = 0 и никаких изменений не происходит. Обычно эта команда используется операционной системой, чтобы увеличить RPL селектора, переданного ей приложением, с целью удостовериться, что он соответствует уровню привилегий приложения (который система может взять из RPL сегмента кода приложения, находящегося в стеке).
Команда выполняется только в защищенном режиме (с любым CPL).
| Команда: |
LAR приемник,источник |
| Назначение: |
Прочитать права доступа сегмента |
| Процессор: |
80286 |
Копирует байты, отвечающие за права доступа из дескриптора, описываемого селектором, находящимся в источнике (регистр или переменная), в источник (регистр) и устанавливает флаг ZF Если используются 16-битные операнды, копируется только байт 5 дескриптора в байт 1 (биты 8 – 15) приемника. Для 32-битных операндов дополнительно копируются старшие 4 бита (для сегментов кода и данных) или весь шестой байт дескриптора (для системных сегментов) в байт 2 приемника. Остальные биты приемника обнуляются. Если CPL > DPL или RPL > DPL - для неподчиненных сегментов кода, если селектор или дескриптор ошибочны или в других ситуациях, в которых программа не сможет пользоваться этим селектором, команда LAR возвращает ZF = 0.
Команда выполняется только в защищенном режиме.
| Команда: |
LSL приемник,источник |
| Назначение: |
Прочитать лимит сегмента |
| Процессор: |
80286 |
Копирует лимит сегмента (размер минус 1) из дескриптора, селектор для которого находится в источнике (регистр или переменная), в приемник (регистр) и устанавливает флаг ZF в 1. Если бит гранулярности в дескрипторе установлен и лимит хранится в единицах по 4096 байт, команда LSL переведет его значение в байты. Если используются 16-битные операнды и лимит не умещается в приемнике, его старшие биты теряются. Так же, как и в случае LAR, эта команда проверяет доступность сегмента из текущей программы, и, если сегмент недоступен, в приемник ничего не загружается и флаг ZF сбрасывается в 0.
Команда выполняется только в защищенном режиме.
| Команда: |
VERR источник |
| Назначение: |
Проверить права на чтение |
| Команда: |
VERW источник |
| Назначение: |
Проверить права на запись |
| Процессор: |
80286 |
Команды проверяют, доступен ли сегмент кода или данных, селектор которого находится в источнике (16-битный регистр или переменная) для чтения (VERR) или записи (VERW), с текущего уровня привилегий. Если сегмент доступен, эти команды возвращают ZF = 1, иначе - ZF = 0.
Команды выполняются только в защищенном режиме.
| Команда: |
INVD |
| Назначение: |
Сбросить кэш-память |
| Команда: |
WBINVD |
| Назначение: |
Записать и сбросить кэш-память |
| Процессор: |
80486 |
Эти команды объявляют все содержимое внутренней кэш-памяти процессора недействительным и подают сигнал для сброса внешнего кэша, так что после этого все обращения к памяти приводят к заполнению кэша заново. Команда WBINVD предварительно сохраняет содержимое кэша в память, команда INVD приводит к потере всей информации, которая попала в кэш, но еще не была перенесена в память.
Команды выполняются только в реальном режиме или с CPL = 0.
| Команда: |
INVLPG источник |
| Назначение: |
Аннулировать страницу |
| Процессор: |
80486 |
Аннулирует (объявляет недействительным) элемент буфера TLB, описывающий страницу памяти, содержащую источник (адрес в памяти). Команда выполняется только в реальном режиме или с CPL = 0.
| Команда: |
HLT |
| Назначение: |
Остановить процессор |
| Процессор: |
8086 |
Переводит процессор в состояние останова, из которого его может вывести только аппаратное прерывание или перезагрузка. Если его выводит прерывание, то адрес возврата, помещаемый в стек для обработчика прерывания, указывает на следующую после HLT команду.
Команда выполняется только в реальном режиме или с CPL = 0.
| Команда: |
RSM |
| Назначение: |
Выйти из режима SMM |
| Процессор: |
Р5 |
Применяется для вывода процессора из режима SMM, использующегося для сохранения состояния системы в критических ситуациях (например, при выключении электроэнергии). При входе в SMM (исполняется при поступлении соответствующего сигнала на процессор от материнской платы) все регистры, включая системные, и другая информация сохраняются в специальном блоке памяти - SMRAM, а при выходе (который и осуществляется командой RSM) все восстанавливается.
Команда выполняется только в режиме SMM.
| Команда: |
RDMSR |
| Назначение: |
Чтение из MSR-регистра |
| Команда: |
WRMSR |
| Назначение: |
Запись в MSR-регистр |
| Процессор: |
Р5 |
<
/p>
Помещает содержимое машинно-специфичного регистра с номером, указанным в ЕСХ, в пару регистров EDX:EAX (старшие 32 бита в EDX и младшие в ЕАХ) (RDMSR) или содержимое регистров EDX:EAX - в машинно-специфичный регистр с номером в ЕСХ. Попытка чтения/записи зарезервированного или отсутствующего в данной модели MSR приводит к исключению #GP(0).
Команда выполняется только в реальном режиме или с CPL = 0.
| Команда: |
RDTSC |
| Назначение: |
Чтение из счетчика тактов процессора |
| Процессор: |
Р5 |
Помещает в регистровую пару EDX:EAX текущее значение счетчика тактов - 64-битного машинно-специфичного регистра TSC, значение которого увеличивается на 1 каждый такт процессора с момента его последней перезагрузки. Этот машинно-специфичный регистр доступен для чтения и записи при помощи команд RDMSR/WRMSR как регистр номер 10h, причем на Pentium Pro при записи в него старшие 32 бита всегда обнуляются. Так как машинно-специфичные регистры могут отсутствовать на отдельных моделях процессоров, их наличие всегда следует определять при помощи команды CPUID (бит 4 в EDX - наличие TSC).
Команда выполняется на любом уровне привилегий, если бит TSD в регистре CR0 равен нулю, и только в реальном режиме или с CPL = 0, если бит TSD = 1.
| Команда: |
RDPMC |
| Назначение: |
Чтение из счетчика событий |
| Процессор: |
Р6 |
Помещает значение одного из двух программируемых счетчиков событий (40-битные машинно-специфичные регистры C1h и C2h для Pentium Pro и Pentium II) в регистровую пару EDX:EAX. Выбор читаемого регистра определяется числом 0 или 1 в ЕСХ. Аналогичные регистры есть и на Pentium (и Cyrix 6х86МХ), но они имеют номера 11h и 12h, и к ним можно обращаться только при помощи команд RDMSR/WRMSR.
Способ выбора типа подсчитываемых событий тоже различается между Pentium и Pentium Pro - для Pentium надо выполнить запись в 64-битный регистр MSR 011h, разные двойные слова которого управляют выбором режима каждого из счетчиков и типа посчитываемых событий, а для Pentium Pro/Pentium II надо выполнить запись в регистр 187h для счетчика 0 и 188h - для счетчика 1. Соответственно и наборы событий между этими процессорами сильно различаются: 38 событий на Pentium, 83 - на Pentium Pro и 96 - на Pentium II.
| Команда: |
SYSENTER |
| Назначение: |
Быстрый системный вызов |
| Команда: |
SYSEXIT |
| Назначение: |
Быстрый возврат из системного вызова |
| Процессор: |
РII |
Команда SYSENTER загружает в регистр CS число из регистра MSR #174h, в регистр EIP - число из регистра MSR #176h, в регистр SS - число, равное CS + 8 (селектор на следующий дескриптор), и в регистр ESP - число из MSR #175h. Эта команда предназначена для передачи управления операционной системе - ее можно вызывать с любым CPL, а вызываемый код должен находиться в бессегментной памяти с CPL = 0. На самом деле SYSENTER модифицирует дескрипторы используемых сегментов - сегмент кода будет иметь DPL = 0, базу 0, лимит 4 Гб, станет доступным для чтения и 32-битным, а сегмент стека также получит базу 0, лимит 4 Гб, DPL = 0, 32-битный режим, доступ для чтения/записи и установленный бит доступа. Кроме того, селекторы CS и SS получают RPL = 0.
Команда SYSEXIT загружает в регистр CS число, равное содержимому регистра MSR #174h плюс 16, в EIP - число из EDX, в SS - число, равное содержимому регистра MSR #174h плюс 24, и в ESP - число из ЕСХ. Эта команда предназначена для передачи управления в бессегментную модель памяти с CPL = 3 и она тоже модифицирует дескрипторы. Сегмент кода получает DPL = 3, базу 0, лимит 4 Гб, доступ для чтения, перестает быть подчиненным и становится 32-битным. Сегмент стека также получает базу 0, лимит 4 Гб, доступ для чтения/записи и 32-битную разрядность. Поля RPL в CS и SS устанавливаются в 3.
Поддержку команд SYSENTER/SYSEXIT всегда следует проверять при помощи команды CPUID (бит 11). Кроме того, надо убедиться, что номер модели процессора не меньше трех, так как Pentium Pro (тип процессора 6, модель 1) не имеет команд SYSENTER/SYSEXIT, но бит в CPUID возвращается равным 1.
SYSENTER выполняется только в защищенном режиме, SYSEXIT выполняется только с CPL = 0.
Страничная адресация
Линейный адрес, который формируется процессором из логического адреса, соответствует адресу из линейного непрерывного пространства памяти. В обычном режиме в это пространство могут попадать области памяти, в которые нежелательно разрешать запись, - системные таблицы и процедуры, ПЗУ BIOS и т.д. Чтобы этого избежать, система может разрешать программам создавать только небольшие сегменты, но тогда теряется такая привлекательная идея flat-памяти. Сегментация - не единственный вариант организации памяти, который поддерживают процессоры Intel. Существует второй, совершенно независимый механизм - страничная адресация (pagination).
При страничной адресации непрерывное пространство линейных адресов памяти разбивается на страницы фиксированного размера (обычно 4 Кб (4096 или 1000h байт), но Pentium Pro может поддерживать и страницы по 4 Мб). При обращении к памяти процессор физически обращается не по линейному адресу, а по тому физическому адресу, с которого начинается данная страница. Описание каждой страницы из линейного адресного пространства, включающее в себя ее физический адрес и дополнительные атрибуты, хранится в одной из специальных системных таблиц, как и в случае сегментации, но в отличие от сегментации страничная адресация абсолютно невидима для программы.
Страничная адресация включается при установке бита PG регистра CR0, если бит РЕ установлен в 1 (попытка установить PG, оставаясь в реальном режиме, приводит к исключению #GP(0)). Кроме того, предварительно надо поместить в регистр CR3 физический адрес начала каталога страниц - главной из таблиц, описывающих страничную адресацию. Каталог страниц имеет размер 4096 байт (ровно одна страница) и содержит 1024 4-байтных указателя на таблицы страниц. Каждая таблица страниц тоже имеет размер 4096 байт и содержит указатели до 1024 4-килобайтных страниц. Если одна страница описывает 4 килобайта, то полностью заполненная таблица страниц описывает 4 мегабайта, а полный каталог полностью заполненных таблиц - 4 гигабайта, то есть все 32-битное линейное адресное пространство. Когда процессор выполняет обращение к линейному адресу, он сначала использует его биты 31 – 22 как номер таблицы страниц в каталоге, затем биты 21 – 12 как номер страницы в выбранной таблице, а затем биты 11 – 0 как смещение от физического адреса начала страницы в памяти. Так как этот процесс занимает достаточно много времени, в процессоре предусмотрен специальный кэш страниц - TLB (буфер с ассоциативной выборкой), так что, если к странице обращались не очень давно, процессор определит ее физический адрес сразу.
Элементы каталога страниц и таблиц страниц имеют общий формат:
биты 31 – 12: биты 31 – 12 физического адреса (таблицы страниц или самой страницы)
биты 11 – 9: доступны для использования операционной системой
бит 8: G - "глобальная страница" - страница не удаляется из буфера TLB при переключении задач или перезагрузке регистра CR3 (только на Pentium Pro, если установлен бит PGE регистра CR4)
бит 7: PS - размер страницы. 1 - для страницы размером 2 или 4 мегабайта, иначе - 0
бит 6: D - "грязная страница" - устанавливается в 1 при записи в страницу; всегда равен нулю для элементов каталога страниц
бит 5: А - бит доступа (устанавливается в 1 при любом обращении к таблице страниц или отдельной странице)
бит 4: PCD - бит запрещения кэширования
бит 3: PWT - бит разрешения сквозной записи
бит 2: U - страница/таблица доступна для программ с CPL = 3
бит 1: W - страница/таблица доступна для записи
бит 0: Р - страница/таблица присутствует. Если этот бит - 0, остальные биты элемента система может использовать по своему усмотрению, например, чтобы хранить информацию о том, где физически находится отсутствующая страница
Процессоры Pentium Pro (и старше) могут поддерживать расширения страничной адресации. Если установлен бит РАЕ, физический адрес оказывается не 32-битным (до 4 Гб), а 36-битным (до 64 Гб). Если установлен бит PSE регистра CR4, включается поддержка расширенных страниц размером 4 Мб для РАЕ = 0 и 2 Мб для РАЕ = 1. Такие страницы описываются не в таблицах страниц, а прямо в основном каталоге. Intel рекомендует помещать ядро операционной системы и все, что ему необходимо для работы, на одну 4-мегабайтную страницу, а для приложений пользоваться 4-килобайтными страницами. Расширенные страницы кэшируются в отдельном TLB, так что, если определена всего одна расширенная страница, она будет оставаться в TLB все время.
Для расширенных страниц формат элемента каталога совпадает с форматом для обычной страницы (кроме того, что бит PS = 1), но в качестве адреса используются только биты 31 – 22 - они соответствуют битам 31 – 22 физического адреса начала страницы (остальные биты адреса - нули).
Для расширенного физического адреса (РАЕ = 1) изменяется формат регистра CR3 (см. главу 10.1.3), размеры всех элементов таблиц становятся равными 8 байтам (причем используются только биты 0 – 3 байта 4), так что их число сокращается до 512 элементов в таблице и вводится новая таблица - таблица указателей на каталоги страниц. Она состоит из четырех 8-байтных элементов, каждый из которых может указывать на отдельный каталог страниц. В этом случае биты 31 – 30 линейного адреса выбирают используемый каталог страниц, биты 29 – 21 - таблицу, биты 20 – 12 - страницу, а биты 11 – 0 - смещение от начала страницы в физическом пространстве (следовательно, если биты 29 – 21 выбрали расширенную страницу, биты 20 – 0 соответствуют смещению в ней).
Основная цель страничной адресации - организация виртуальной памяти в операционных системах. Система может использовать внешние устройства - обычно диск - для расширения виртуального размера памяти. При этом, если к какой-то странице долгое время нет обращений, система копирует ее на диск и помещает отсутствующей в таблице страниц. Затем, когда программа обращается по адресу в отсутствующей странице, вызывается исключение #РЕ Обработчик исключения читает адрес, вызвавший ошибку из CR2, определяет, какой странице он соответствует, загружает ее с диска, устанавливает бит присутствия, удаляет копию старой страницы из TLB командой INVLPG и возвращает управление (не забыв снять со стека код ошибки). Команда, вызывавшая исключение типа ошибки, выполняется повторно.
Кроме того, система может периодически сбрасывать бит доступа и, если он не установится за достаточно долгое время, копировать страницу на диск и объявлять ее отсутствующей. Если при этом бит D равен нулю, в страницу не выполнялось никаких записей (с того момента, как этот бит последний раз обнулили) и ее вообще можно не сохранять.
Второе не менее важное применение страничной адресации - безопасная реализация flat-модели памяти. Операционная система может разрешить программам обращаться к любому линейному адресу, но отображение линейного пространства на физическое не будет взаимно однозначным. Скажем, если система использует первые 4 Кб памяти, физическим адресом нулевой страницы будет не ноль, а 4096 и пользовательская программа даже не узнает, что обращается не к нулевому адресу. В этом случае, правда, и сама система не сможет обращаться к первой физической странице без изменения таблицы страниц, но эта проблема решается при применении механизма многозадачности, о котором рассказано далее.
В следующем примере мы построим каталог и таблицу страниц (для первых четырех мегабайт), отображающие линейное пространство в физическое один в один, затем изменим физический адрес страницы с линейным адресом 0A1000h и попытаемся выполнить обычный цикл закраски экрана в режиме 320x200x256, заполнив видеопамять байтом с номером цвета, но у нас останется незакрашенным участок, соответствующий перенесенной странице.
; pm3.asm ; Программа, демонстрирующая страничную адресацию. ; Переносит одну из страниц, составляющих видеопамять, и пытается закрасить ; экран ; ; Компиляция: ; TASM: ; tasm /m pm3.asm ; tlink /x /3 pm3.obj ; MASM: ; ml /с pm3.asm ; link pm3.obj,,NUL,,, ; WASM: ; wasm pm3.asm ; wlink file pm3.obj form DOS
.386р RM_seg segment para public "CODE" use16 assume cs:RM_seg,ds:PM_seg,ss:stack_seg start: ; подготовить сегментные регистры push PM_seg pop ds ; проверить, не находимся ли мы уже в РМ mov еах,cr0 test al,1 jz no_V86 ; сообщить и выйти mov dx,offset v86_msg err_exit: push cs pop ds mov ah,9 int 21h mov ah,4Ch int 21h
; убедиться, что мы не под Windows no_V86: mov ax,1600h int 2Fh test al,al jz no_windows ; сообщить и выйти mov dx,offset win_msg jmp short err_exit
; сообщения об ошибках при старте v86_msg db "Процессор в режиме V86 - нельзя переключиться в РМ$" win_msg db "Программа запущена под Windows - нельзя перейти в кольцо 0$"
; итак, мы точно находимся в реальном режиме no_windows: ; очистить экран и переключиться в нужный видеорежим mov ax,13h int 10h ; вычислить базы для всех дескрипторов xor еах,еах mov ax,RM_seg shl eax,4 mov word ptr GDT_16bitCS+2,ax shr eax,16 mov byte ptr GDT_16bitCS+4,al mov ax,PM_seg shl eax,4 mov word ptr GDT_32bitCS+2,ax shr eax,16 mov byte ptr GDT_32bitCS+4,al ; вычислить линейный адрес GDT xor eax,eax mov ax,PM_seg shl eax,4 push eax add eax,offset GDT mov dword ptr gdtr+2,eax ; загрузить GDT lgdt fword ptr gdtr ; открыть А20 - в этом примере мы будем пользоваться памятью выше 1 Мб mov al,2 out 92h,al ; отключить прерывания cli ; и NMI in al,70h or al,80h out 70h,al ; перейти в защищенный режим (пока без страничной адресации) mov еах,cr0 or al,1 mov cr0,eax ; загрузить CS db 66h db 0EAh dd offset PM_entry dw SEL_32bitCS RM_return: ; переключиться в реальный режим с отключением страничной адресации mov eax,cr0 and eax,7FFFFFFEh mov cr0,eax ; сбросить очередь и загрузить CS db 0EAh dw $+4 dw RM_seg ; загрузить остальные регистры mov ax,PM_seg mov ds,ax mov es,ax ; разрешить NMI in al,70h and al,07FH out 70h,al ; разрешить другие прерывания sti ; подождать нажатия клавиши mov ah,1 int 21h ; переключиться в текстовый режим mov ax,3 int 10h ; и завершить программу mov ah,4Ch int 21h RM_seg ends
PM_seg segment para public "CODE" use32 assume cs:PM_seg ; таблица глобальных дескрипторов GDT label byte db 8 dup(0) GDT_flatDS db 0FFh,0FFh,0,0,0,10010010b,11001111b,0 GDT_16bitCS db 0FFh,0FFh,0,0,0,10011010b,0,0 GDT_32bitCS db 0FFh,0FFh,0,0,0,10011010b,11001111b,0 gdt_size = $ - GDT gdtr dw gdt_size-1 ; ее лимит dd ? ; и адрес SEL_flatDS equ 001000b ; селектор 4-гигабайтного сегмента данных SEL_16bitCS equ 010000b ; селектор сегмента кода RM_seg SEL_32bitCS equ 011000b ; селектор сегмента кода PM_seg
; точка входа в 32-битный защищенный режим PM_entry: ; загрузить сегментные регистры, включая стек xor еах,еах mov ax,SEL_flatDS mov ds,ax mov es,ax ; создать каталог страниц mov edi,00100000h ; его физический адрес - 1 Мб mov eax,00101007h ; адрес таблицы 0 = 1 Мб + 4 Кб stosd ; записать первый элемент каталога mov ecx,1023 ; остальные элементы каталога - xor еах,еах ; нули rep stosd ; заполнить таблицу страниц 0 mov eax,00000007h ; 0 - адрес страницы 0 mov ecx,1024 ; число страниц в таблице page_table: stosd ; записать элемент таблицы add еах,00001000h ; добавить к адресу 4096 байт loop page_table ; и повторить для всех элементов ; поместить адрес каталога страниц в CR3 mov еах,00100000h ; базовый адрес = 1 Мб mov cr3,еах ; включить страничную адресацию mov eax,cr0 or eax,80000000h mov cr0,eax ; а теперь изменить физический адрес страницы A1000h на А2000h mov eax,000A2007h mov es:00101000h+0A1h*4,eax ; если закомментировать предыдущие две команды, следующие четыре команды ; закрасят весь экран синим цветом, но из-за того, что мы переместили одну ; страницу, остается черный участок mov ecx,(320*200)/4 ; размер экрана в двойных словах mov edi,0A0000h ; линейный адрес начала видеопамяти mov eax,01010101h ; код синего цвета в VGA - 1 rep stosd ; вернуться в реальный режим db 0EAh dd offset RM_return dw SEL_16bitCS PM_seg ends
; Сегмент стека - используется как 16-битный stack_seg segment para stack "STACK" stack_start db 100h dup(?) stack_seg ends end start
Управление задачами
Следующий очень важный механизм, действующий только в защищенном режиме, - многозадачность. Задача - это элемент работы, который процессор может исполнять, запустить или отложить. Задачи используют для выполнения программ, процессов, обработчиков прерываний и исключений, ядра операционной системы и пр. Любая программа, выполняющаяся в защищенном режиме, должна осуществляться как задача (хотя мы пока игнорировали это требование). Процессор предоставляет средства для сохранения состояния задачи, запуска задачи и передачи управления из одной задачи в другую.
Задача состоит из сегмента состояния задачи (TSS), сегмента кода, одного или нескольких (для разных уровней привилегий) сегментов стека и одного или нескольких сегментов данных.
Задача определяется селектором своего сегмента TSS. Когда задача выполняется, ее селектор TSS (вместе с дескриптором в скрытой части) загружен в регистр TR процессора.
Запуск задачи осуществляется при помощи команды CALL или JMP на сегмент TSS или на шлюз задачи, а также при запуске обработчика прерывания или исключения, который описан как шлюз задачи. При этом автоматически осуществляется переключение задач. Состояние текущей задачи записывается в ее TSS, состояние вызываемой задачи считывается из ее TSS, и управление передается на новые CS:EIP. Если задача не была запущена командой JMP, селектор сегмента TSS старой задачи сохраняется в TSS новой и устанавливается флаг NT, так что следующая команда IRET выполнит обратное переключение задач.
Задачи не могут вызываться рекурсивно. В дескрипторе TSS-задачи, которая была запущена, но не была завершена, тип изменяется на "занятый TSS" и переход на такой TSS невозможен.
Задача может иметь собственную таблицу дескрипторов (LDT) и полный комплект собственных таблиц страниц, так как регистры LDTR и CR3 входят в состояние задачи.
Вход и выход из защищенного режима
Итак, чтобы перейти в защищенный режим, достаточно установить бит РЕ - нулевой бит в управляющем регистре CR0, и процессор немедленно окажется в защищенном режиме. Единственное дополнительное требование, которое предъявляет Intel, - чтобы в этот момент все прерывания, включая немаскируемое, были отключены.
; pm0.asm ; Программа, выполняющая переход в защищенный режим и немедленный возврат. ; Работает в DOS в реальном режиме и в DOS-окне Windows 95 (Windows ; перехватывает исключения, возникающие при попытке перехода в защищенный ; режим из V86, и позволяет нам работать, но только на минимальном уровне ; привилегий) ; ; Компиляция: ; TASM: ; tasm /m pm0.asm ; tlink /x /t pm0.obj ; MASM: ; ml /c pm0.asm ; link pm0.obj,,NUL,,, ; exe2bin pm0.exe pm0.com ; WASM: ; wasm pm0.asm ; wlink file pm0.obj form DOS COM
.model tiny .code .386p ; все наши примеры рассчитаны на 80386 org 100h ; это СОМ-программа start: ; подготовить сегментные регистры push cs pop ds ; DS - сегмент данных (и кода) нашей программы push 0B800h pop es ; ES - сегмент видеопамяти ; проверить, находимся ли мы уже в защищенном режиме mov еах,cr0 ; прочитать регистр CR0 test al,1 ; проверить бит РЕ, jz no_V86 ; если он ноль - мы можем продолжать, ; иначе - сообщить об ошибке и выйти mov ah,9 ; функция DOS 09h mov dx,offset v86_msg ; DS:DX - адрес строки int 21h ; вывод на экран ret ; конец СОМ-программы ; (раз это защищенный режим, в котором работает наша DOS-программа, это должен ; быть режим V86) v86_msg db "Процессор в режиме V86 - нельзя переключиться в РМ$"
; сюда передается управление, если мы запущены в реальном режиме no_V86: ; запретить прерывания cli ; запретить немаскируемое прерывание in al,70h ; индексный порт CMOS or al,80h ; установка бита 7 в нем запрещает NMI out 70h,аl ; перейти в защищенный режим mov еах,cr0 ; прочитать регистр CRO or al,1 ; установить бит РЕ, mov cr0,eax ; с этого момента мы в защищенном режиме ; вывод на экран xor di,di ; ES:DI - начало видеопамяти mov si,offset message ; DS:SI - выводимый текст mov cx,message_l rep movsb ; вывод текста mov ax,0720h ; пробел с атрибутом 07h mov cx,rest_scr ; заполнить этим символом остаток экрана rep stosw ; переключиться в реальный режим mov еах,cr0 ; прочитать CR0 and al,0FEh ; сбросить бит РЕ mov cr0,eax ; с этого момента процессор работает в ; реальном режиме ; разрешить немаскируемое прерывание in al,70h ; индексный порт CMOS and al,07Fh ; сброс бита 7 отменяет блокирование NMI out 70h,al ; разрешить прерывания sti ; подождать нажатия любой клавиши mov ah,0 int 16h ; выйти из СОМ-программы ret ; текст сообщения с атрибутом после каждого символа для прямого вывода на экран message db 'Н',7,'е',7,'l',7,'l',7,'о',7,' ',7,'и',7,'з',7 db ' ',7,'Р',7,'М',7 ; его длина в байтах message_l = $ - message ; длина оставшейся части экрана в словах rest_scr = (80*25)-(2*message_l) end start
В главе 6.1 при рассмотрении адресации в защищенном режиме говорилось о том, что процессор, обращаясь к памяти, должен определить адрес начала сегмента из дескриптора в таблице дескрипторов, находящейся в памяти, используя селектор, находящийся в сегментном регистре, в качестве индекса. В то же время в этом примере мы обращаемся к памяти из защищенного режима, вообще не описав никаких дескрипторов, и в сегментных регистрах у нас находятся те же числа, что и в реальном режиме.
Дело в том, что, начиная с процессора 80286, размер каждого сегментного регистра - CS, SS, DS, ES, FS и GS - не два байта, а десять, восемь из которых недоступны для программ, точно так же, как описанные выше регистры LDTR и TR. В защищенном режиме при записи селектора в сегментный регистр процессор копирует весь определяемый этим селектором дескриптор в скрытую часть сегментного регистра и больше не пользуется этим селектором вообще. Таблицу дескрипторов можно уничтожить, а обращения к памяти все равно будут выполняться, как и раньше. В реальном режиме при записи числа в сегментный регистр процессор сам создает соответствующий дескриптор в его скрытой части. Этот дескриптор описывает 16-битный сегмент, начинающийся по указанному сегментному адресу с границей 64 Кб. Когда мы переключились в защищенный режим в программе pm0.asm, эти дескрипторы остались на месте и мы могли обращаться к памяти, не принимая во внимание то, что у нас написано в сегментном регистре. Разумеется, в этой ситуации любая попытка записать в сегментный регистр число привела бы к немедленной ошибке (исключение #GP с кодом ошибки, равным загружаемому значению).
В режиме V86 текущий уровень
В режиме V86 текущий уровень привилений, CPL, всегда равен трем. В соответствии с правилами защиты выполнение команд CLI, STI, PUSHF, POPF, INT и IRET приводит к исключению #GP, если IOPL < 3. Однако команды IN, OUT, INS, OUTS, чувствительные к IOPL в защищенном режиме, в V86, управляются битовой картой ввода-вывода, расположенной в TSS задачи. Если бит, соответствующий порту, установлен в 1, обращение к нему из V86-задачи приводит к исключению #GP, если бит сброшен - команды работы с портами ввода-вывода выполняются.
Мы не будем рассматривать пример программы, реализующей режим V86, из-за его размеров. Практически все из того, что необходимо для его создания (защищенный режим, обработка прерываний, страничная адресация и переключение задач), мы уже обсудили.
Выполнение привилегированных команд
Команды LGDT, LLDT, LTR, LIDT, MOV CRn, LMSW, CLTS, MOV DRn, INVD, WBINVD, INVLPG, HLT, RDMSR, WRMSR, RDPMC, RDTSC, SYSEXIT могут выполняться, только если CPL = 0 (хотя биты РСЕ и TSD регистра CR4 разрешают использование команд RDPMC и RDTSC с любого уровня).
Команды LLDT, SLDT, LTR, STR, LSL, LAR, VERR, VERW и ARPL можно выполнять только в защищенном режиме - в реальном и V86 возникает исключение #UD.
Команды CLI и STI выполняются, только если CPL =< IOPL (IOPL - это двухбитная область в регистре флагов). Если установлен бит PVI в регистре CR4, эти команды выполняются с любым CPL, но управляют флагом VIF, а не IF.
Команды IN, OUT, INSB, INSW, INSD, OUTSB, OUTSW, OUTSD выполняются, только если CPL =< IOPL и если бит в битовой карте ввода-вывода, соответствующий данному порту, равен нулю. (Эта карта - битовое поле в сегменте TSS, каждый бит которого отвечает за один порт ввода-вывода. Признаком ее конца служит слово, в котором все 16 бит установлены в 1.)
Защита на уровне страниц
Обращение к странице памяти с битом U в атрибуте страницы или таблицы страниц, равным нулю, приводит к исключению #PF, если CPL = 3.
Попытка записи в страницу с битом W в атрибуте страницы или таблицы страниц, равным нулю, с CPL = 3 приводит к исключению #РF.
Попытка записи в страницу с битом W в атрибуте страницы или таблицы страниц, равным нулю, если бит WP в регистре CR0 равен 1, приводит к исключению #РF.
Программирование на ассемблере в среде UNIX
Адресация
Регистровый операнд всегда начинается с символа "%":
// xor edx,edx xorl %eax,%eax
Непосредственный операнд всегда начинается с символа "$":
// mov edx,offset variable movl $variable,%edx
Косвенная адресация использует немодифицированное имя переменной:
// push dword ptr variable pushl variable
Более сложные способы адресации удобнее рассматривать как варианты максимально сложного способа - по базе с индексированием, и сдвигом:
// mov eax,base_addr[ebx+edi*4] (наиболее общий случай) movl base_addr(%ebx,%edi,4),%еах // lea eax,[eax+eax*4] leal (%еах,%еах,4),%еах // mov ax,word ptr [bp-2] movw -2(%ebp),%ax // mov edx,dword ptr [edi*2] movl (,%edi,2),%edx
Блоки повторения
Повторить блок программы указанное число раз:
.rept число повторов .endr
Повторить блок программы для всех указанных значений символа:
.irp симол, значение... .endr
Повторить блок программы столько раз, сколько байт в строке, устанавливая символ равным каждому байту по очереди:
.irpc символ, строка .endr
Внутри блока повторения на символ можно ссылаться, начиная его с обратной косой черты, то есть как \символ, например такой блок:
.irp param,1,2,3 movl %st(0),%st(\param) . endr
как и такой:
.irpc param,123 movl %st(0),%st(\param) .endr
ассемблируется в:
movl %st(0),%st(1) movl %st(0),%st(2) movl %st(0),%st(3)
Директивы ассемблера
Все директивы ассемблера в UNIX всегда начинаются с символа "." (точка). Из-за большого числа отличающихся операционных систем и ассемблеров для них возникло много часто встречающихся директив. Рассмотрим только наиболее полезные.
Директивы определения данных
Эти директивы эквивалентны директивам db, dw, dd, df и т.п., применяющимся в ассемблерах для DOS/Windows. Основное отличие здесь состоит в том, чтобы дать имя переменной, значение которой определяется такой директивой; в ассемблерах для UNIX обязательно надо ставить полноценную метку, заканчивающуюся двоеточием.
Байты:
.byte выражение...
Слова:
.word выражение... или .hword выражение... или .short выражение...
Двойные слова:
.int выражение... или .long выражение...
Учетверенные слова (8-байтные переменные):
.quad выражение...
16-байтные переменные (окта-слова):
. octa выражение...
32-битные числа с плавающей запятой:
.float число... или .single число...
64-битные числа с плавающей запятой:
.double число...
80-битные числа с плавающей запятой:
.tfloat число...
Строки байтов:
.ascii строка...
Строки байтов с автоматически добавляемым нулевым символом в конце:
.asciz строка... или .string строка
Блоки повторяющихся данных:
.skip размер,значение или .space размер,значение
Заполняет области памяти указанного размера байтами с заданным значением
.fill повтор, размер, значение
Заполняет область памяти значениями заданного размера (0 – 8 байт) указанное число раз. По умолчанию размер принимается равным 1, а значение - 0.
Неинициализированные переменные:
.lcomm символ, длина, выравнивание
Зарезервировать указанное число байт для локального символа в секции .bss.
Директивы определения секций
Текст программы делится на секции - кода, данных, неинициализированных данных, отладочных символов и т.д. Секции также могут делиться далее на подсекции, располагающиеся непосредственно друг за другом, но это редко используется.
.data подсекция
Следующие команды будут ассемблироваться в секцию данных. Если подсекция не указана, данные ассемблируются в нулевую подсекцию.
.text подсекция
Следующие команды будут ассемблироваться в секцию кода.
.section имя, флаги, @тип или .section "имя", флаги
Общее определение новой секции:
флаги (для ELF):
w или #write - разрешена запись;
х или #execinstr - разрешено исполнение;
а или #alloc - разрешено динамическое выделение памяти (.bss);
тип (для ELF):
©progbits - содержит данные;
@nobits - не содержит данные (только занимает место).
Директивы управления ассемблированием
Включить текст другого файла в программу:
.include файл
Ассемблировать блок, если выполняется условие или определен или не определен символ:
.if выражение .ifdef символ .ifndef символ или .ifnotdef символ .else .endif
Выдать сообщение об ошибке:
.err
Немедленно прекратить ассемблирование:
.abort
Директивы управления листингом
Запретить листинг:
.nolist
Разрешить листинг:
.list
Конец страницы:
.eject
Размер страницы (60 строк, 200 столбцов по умолчанию):
.psize строки, столбцы
Заголовок листинга:
.title текст
Подзаголовок:
.sbttl текст
Директивы управления программным указателем
.align выражение, выражение, выражение
Выполняет выравнивание программного указателя до границы, указанной первым операндом. Второе выражение указывает, какими байтами заполнять пропускаемый участок (по умолчанию - ноль для секций данных и 90h для секций кода). Третье выражение задает максимальное число байт, которые может пропустить эта директива.
В некоторых системах первое выражение - не число, кратным которому должен стать указатель, а число бит в указателе, которые должны стать нулевыми (в нашем примере это было бы 4).
.org новое значение, заполнение
Увеличивает программный указатель до нового значения в пределах текущей секции. Пропускаемые байты заполняются указанными значениями (по умолчанию - нулями).
Директивы управления разрядностью
.code16
Следующие команды будут ассемблироваться как 16-битные.
.code32
Отменяет действие .code 16.
Директивы управления символами
Присвоение значений символам:
.equ символ, выражение
Присваивает символу значение выражения.
.equiv символ, выражение
То же, что и .equ, но выдает сообщение об ошибке, если символ определен.
.set символ, выражение
То же, что и .equ, но можно делать несколько раз. Обычно, впрочем, бывает удобнее написать просто "символ = выражение".
Управление внешними символами:
.globl символ или .global символ
Делает символ видимым для компоновщика, а значит, и для других модулей программы.
.extern символ
Директива .extern обычно игнорируется - все неопределенные символы считаются внешними.
.comm символ, длина, выравнивание
Директива эквивалентна .lcomm, но, если символ с таким именем определен при помощи .lcomm в другом модуле, будет использоваться внешний символ.
Описание отладочных символов:
.def символ .endef
Блок описания отладочного символа.
Мы не коснемся описания отладочных символов, так как их форматы сильно различаются между разнообразными операционными системами и разными форматами объектных файлов.
Инфиксные, или бинарные операторы
Высшего приоритета:
* - умножение
/ - целочисленное деление
% - остаток
< или << - сдвиг влево
> или >> - сдвиг вправо
Среднего приоритета:
| - побитовое "ИЛИ"
& - побитовое "И"
^ - побитовое "исключающее ИЛИ"
! - побитовое "ИЛИ-НЕ" (логическая импликация)
Низшего приоритета:
+ - сложение
– - вычитание
Макроопределения
Начало макроопределения:
.macro имя, аргументы
Конец макроопределения:
.endm
Преждевременный выход из макроопределения:
.exitm
Внутри макроопределения обращение к параметру выполняется аналогично блокам повторения, начиная его с обратной косой черты.
Хотя стандартные директивы и включают в себя такие вещи, как блоки повторений и макроопределения, их реализация достаточно упрощена, и при программировании для UNIX на ассемблере часто используют дополнительные препроцессоры. Долгое время было принято использовать С-препроцессор или М4, и многие ассемблеры даже могут вызывать их автоматически, но в рамках проекта GNU был создан специальный препроцессор для ассемблера - gasp. Gasp включает различные расширения вариантов условного ассемблирования, построения циклов, макроопределений, листингов, директив определения данных и так далее. Мы не будем заниматься реализацией таких сложных программ, которым может потребоваться gasp, мы даже не воспользуемся и половиной перечисленных директив, но существование этого препроцессора следует иметь в виду.
Операторы ассемблера
Как и в ассемблерах для DOS, ассемблеры для UNIX могут вычислять значения выражений в момент компиляции, например:
// поместить в ЕАХ число 320 * 200 movl $320*$200, %еах
В этих выражениях встречаются следующие операторы.
Основные правила
Итак, в ассемблере AT&T в качестве допустимых символов в тексте программы рассматриваются только латинские буквы, цифры и символы "%" (процент) "$" (доллар), "*" (звездочка) , "." (точка), "," (запятая) и "_" (подчеркивание). Помимо них существуют символы начала комментария, различные для разных ассемблеров и различные для комментария размером в целую строку или правую часть строки. Любые другие символы, кроме кавычек, двоеточия, пробела и табуляции, если они не часть комментария или не заключены в кавычки, считаются ошибочными.
Если последовательность допустимых символов, с которой начинается строка, не начинается со специального символа или цифры и не заканчивается двоеточием - это команда процессора:
// остановить процессор hlt
Если последовательность допустимых символов начинается с символа "%" - это название регистра процессора:
// поместить в стек содержимое регистра ЕАХ pushl %eax
Если последовательность начинается с символа "$" - это непосредственный операнд:
// поместить в стек 0, число 10h и адрес переменной variable pushl $0 pushl $0x10 pushl $variable
Если последовательность символов начинается с точки - это директива ассемблера:
.align 2
Если последовательность символов, с которой начинается строка, заканчивается двоеточием - это метка (внутренняя переменная ассемблера, значение которой соответствует адресу в указанной точке):
eternal_loop: jmp eternal_loop variable: .byte 7
Метки, состоящие из одной цифры от 0: до 9:, используются как локальные - обращение к метке 1f соответствует обращению к ближайшей из меток 1: вперед по тексту программы, обращение к метке 4b соответствует обращению к ближайшей из меток 4: назад по тексту программы.
Одни и те же метки могут использоваться без ограничений и как цель для команды перехода, и как переменные.
Специальная метка "." (точка) всегда равна текущему адресу (в точности как "$" в ассемблерах для DOS/Windows).
Если последовательность символов начинается с символа "*" - это абсолютный адрес (для команд jmp и call), иначе - относительный.
Префиксные, или унарные операторы
– (минус) - отрицательное число
~ (тильда) - "логическое НЕ"
Программирование без использования libc
Может оказаться, что программа вынуждена многократно вызывать те или иные стандартные функции из libc в критическом участке, тормозящем выполнение всей программы. В этом случае стоит обратить внимание на то, что многие функции libc на самом деле всего лишь более удобный для языка С интерфейс к системным вызовам, предоставляемым самим ядром операционной системы. Такие операции, как ввод/вывод, вся работа с файловой системой, с процессами, с TCP/IP и т.п., могут выполняться путем передачи управления ядру операционной системы напрямую.
Чтобы осуществить системный вызов, надо передать его номер и параметры на точку входа ядра аналогично функции libc syscall(2). Номера системных вызовов (находятся в файле /usr/include/sys/syscall.h) и способ обращения к точке входа (дальний call по адресу 0007:00000000) стандартизированы SysV/386 ABI, но, например в Linux, используется другой механизм - прерывание 80h, так что получается, что обращение к ядру операционной системы напрямую делает программу привязанной к этой конкретной системе. Часть этих ограничений можно убрать, используя соответствующие #define, но в общем случае этот выигрыш в скорости оборачивается еще большей потерей переносимости, чем само использование ассемблера в UNIX.
Посмотрим, как реализуются системные вызовы в рассматриваемых нами примерах:
// hellolnx.s // Программа, выводящая сообщение "Hello world" на Linux // без использования libc // // Компиляция: // as -о hellolnx.o hellolnx.s // ld -s -o hellolnx hellolnx.o // .text .globl _start _start: // системный вызов #4 "write", параметры в Linux помещают слева направо, // в регистры %еах, %ebx, %ecx, %edx, %esi, %edi movl $4,%eax xorl %ebx,%ebx incl %ebx // %ebx = 1 (идентификатор stdout) movl $message,%ecx movl $message_l,%edx // передача управления в ядро системы - прерывание с номером 80h int $0x80
// системный вызов #1 "exit" (%еах = 1, %ebx = 0) xorl %eax,%eax incl %eax xorl %ebx,%ebx int $0x80 hlt
.data message: .string "Hello world\012" message_l = . - message
Linux - это довольно уникальный случай в отношении системных вызовов. В более традиционных UNIX-системах - FreeBSD и Solaris - системные вызовы реализованы согласно общему стандарту SysV/386, и различие в программах заключается только в том, что ассемблер, поставляемый с FreeBSD, не поддерживает некоторые команды и директивы.
// hellobsd.s // Программа, выводящая сообщение "Hello world" на FreeBSD // без использования libc // // Компиляция: // as -о hellobsd.o hellobsd.s // ld -s -o hellobsd hellobsd.o // .text .globl _start _start: // системная функция 4 "write" // в FreeBSD номер вызова помещают в %еах, а параметры - в стек // справа налево плюс одно двойное слово pushl $message_l // параметр 4 - длина буфера pushl $message // параметр 3 - адрес буфера pushl $1 // параметр 2 - идентификатор устройства movl $4,%еах // параметр 1 - номер функции в еах pushl %eax // в стек надо поместить любое двойное слово, но мы поместим номер вызова // для совместимости с Solaris и другими строгими операционными системами // lcall $7,$0 - ассемблер для FreeBSD не поддерживает эту команду .byte 0x9a .long 0 .word 7 // восстановить стек addl $12,%esp // системный вызов 1 "exit" xorl %eax,%eax pushl %eax incl %eax pushl %eax // lcall $7,$0 .byte 0x9A .long 0 .word 7 hlt
.data message: .ascii "Hello world\012" message_l = . - message
И теперь то же самое в Solaris:
// hellosol.s // Программа, выводящая сообщение "Hello world" на Solaris/x86 // без использования libc // // Компиляция: // as -о hellosol.o hellosol.s // ld -s -o hellosol hellosol.o .text .globl _start _start: // комментарии - см. hellobsd.s pushl $message_l pushl $message movl $4,%eax pushl %eax lcall $7,$0 addl $16,%esp
xorl %eax,%eax pushl %eax incl %eax pushl %eax lcall $7,$0 hit
.data message: .string "Hello world\012" message_l = . - message
Конечно, создавая эти программы, мы нарушили спецификацию SysV/386 ABI несколько раз, но из-за того, что мы не обращались ни к каким разделяемым библиотекам, это прошло незамеченным. Требования к полноценной программе сильно разнятся в различных операционных системах, и все они выполнены с максимально возможной тщательностью в файлах crt*.o, которые мы подключали в примере с использованием библиотечных функций. Поэтому, если вы не ставите себе цель сделать программу абсолютно минимального размера, гораздо удобнее назвать свою процедуру main (или _main) и добавлять crt*.o и -lс при компоновке.
Программирование на ассемблере в среде UNIX
Операционная система MS-DOS, получившая дальнейшее развитие в виде Windows, долгое время была практически единственной операционной системой для персональных компьютеров на базе процессоров Intel. Но с течением времени мощность процессоров выросла настолько, что для них стало возможным работать под управлением операционных систем класса UNIX, использовавшихся обычно на более мощных компьютерах других компаний. В настоящее время существует более двадцати операционных систем для Intel, представляющих те или иные диалекты UNIX. Мы рассмотрим наиболее популярные из них.
Linux - бесплатно распространяемая операционная система, соединяющая в себе особенности двух основных типов UNIX-систем, System V и BSD приблизительно в равной мере. В ней достаточно много отличий и отступлений от любых стандартов, принятых для UNIX, но они более эффективны.
FreeBSD - бесплатно распространяемая операционная система, представляющая вариант BSD UNIX. Считается наиболее стабильной из UNIX-систем для Intel.
Solaris/x86 - коммерческая операционная система компании Sun Microsystems, представляющая вариант System V UNIX, изначально созданная для компьютеров Sun, существует в версии для Intel 80x86. Распространяется бесплатно для образовательных целей.
Несмотря на то что при программировании для UNIX обычно употребляется исключительно язык С, пользоваться ассемблером в этих системах можно, и даже крайне просто. Программы в UNIX выполняются в защищенном режиме с моделью памяти flat и могут вызывать любые функции из библиотеки libc или других библиотек точно так же, как это делают программы на С. Конечно, круг задач, для которых имеет смысл использовать ассемблер в UNIX, ограничен. Если вы не занимаетесь разработкой ядра операционной системы или, например, эмулятора DOS, практически все можно сделать и на С, но иногда встречаются ситуации, когда требуется что-то особенное. Написать процедуру, выполняющую что-то как можно быстрее (например, воспроизведение звука из файла в формате МР3), или программу, использующую память более эффективно (хотя это часто можно повторить на С), или программу, использующую возможности нового процессора, поддержка которого еще не добавлена в компилятор (если вы знаете ассемблер для UNIX), достаточно просто.
Программирование с использованием libc
Все программы для UNIX, написанные на С, постоянно обращаются к различным функциям, находящимся в libc.so или других стандартных или нестандартных библиотеках. Программы и процедуры на ассемблере, естественно, могут делать то же самое. Вызов библиотечной функции выполняется обычной командой call, а передача параметров осуществляется в соответствии с С-конвенцией: параметры помещают в стек справа налево и очищают стек после вызова функции. Единственная сложность здесь состоит в том, что к имени вызываемой функции в некоторых системах, например FreeBSD, приписывается в начале символ подчеркивания, в то время как в других (Linux и Solaris) имя не изменяется. Если имена в системе модифицируются, имена процедур, включая main(), написанных на ассемблере, также должны быть изменены заранее.
Посмотрим на примере программы, выводящей традиционное сообщение "Hello world", как это делается:
// helloelf.s // Минимальная программа, выводящая сообщение "Hello world" // Для компиляции в формат ELF // // Компиляция: // as -о helloelf.o helloelf.s // Компоновка: // (пути к файлу crt1.o могут отличаться на других системах) // Solaris с SunPro С // ld -s -о helloelf.sol helloelf.o /opt/SUNWspro/SC4.2/lib/crt1.о -lс // Solaris с GNU С // ld -s -o helloelf.gso helloelf.o // /opt/gnu/lib/gcc-lib/i586-cubbi-solaris2.5.1/2.7.2.3.f.1/crt1.о -lс // Linux // ld -s -m elf_i386 -o helloelf.lnx /usr/lib/crt1.o /usr/lib/crti.o // -L/usr/lib/gcc-lib/i586-cubbi-linuxlibc1/2.7.2 helloelf.o -lc -lgcc // /usr/lib/crtn.o // .text // код, находящийся в файлах crt*.o, передаст управление на процедуру main // после настройки всех параметров .globl main main: // поместить параметр (адрес строки message) в стек pushl $message // вызвать функцию puts (message) call puts // очистить стек от параметров popl %ebx // завершить программу ret
.data message: .string "Hello world\0"
В случае FreeBSD придется внести всего два изменения - добавить символ подчеркивания в начало имен функций puts и main и заменить директиву .string на .ascii, так как версия ассемблера, обычно распространяемого с FreeBSD, .string не понимает.
// hellocof.s // Минимальная программа, выводящая сообщение "Hello world" // Для компиляции в вариант формата COFF, используемый во FreeBSD // Компиляция для FreeBSD: // as -о hellocof.o hellocof.s // ld -s -о hellocof.bsd /usr/lib/crt0.o hellocof.o -lc
.text .globl _main _main: pushl $message call _puts popl %ebx ret
.data message: .ascii "Hello world\0"
Пользуясь этой техникой, можно создавать программы точно так же, как и на С, но выигрыш за счет того, что на ассемблере можно соптимизировать программу на несколько процентов лучше, чем это сделает компилятор с С (с максимальной оптимизацией), оказывается незначительным по сравнению с потерей переносимости. Кроме того, при написании любой сколько-нибудь значительной программы целиком на ассемблере мы столкнемся с тем, что, как и в случае с Win32, нам придется создавать собственные включаемые файлы с определениями констант и структур, взятых из включаемых файлов для С. А так как эти ассемблеры не умеют работать со структурами данных, необходимо описывать их средствами используемого препроцессора - срр или m4.
Лучшее применение ассемблера для UNIX (кроме собственно разработки ядра системы) все-таки остается за небольшими процедурами, требующими большой вычислительной мощности, - кодированием, архивированием, преобразованиями типа Фурье, которые не очень сложны и при необходимости могут быть легко переписаны заново на ассемблере для другого процессора или на С.
Синтаксис AT&T
Проблема в том, что ассемблер для UNIX кардинально отличается от того, что рассматривалось в этой книге до сих пор. В то время как основные ассемблеры для MS-DOS и Windows используют синтаксис, предложенный компанией Intel, изобилующий неоднозначностями, часть которых решается за счет использования поясняющих операторов типа byte ptr, word ptr или dword ptr, а часть не решается вообще (все те случаи, когда приходится указывать код команды вручную), в UNIX с самого начала используется вариант универсального синтаксиса AT&T, синтаксис SysV/386, который специально создавался с целью устранения неоднозначностей в толковании команд. Вообще говоря, существует и ассемблер для DOS/Windows, использующий АТ&Т-синтаксис, - это gas, входящий в набор средств разработки DJGPP, а также есть ассемблер, использующий Intel-синтаксис и способный создавать объектные файлы в формате ELF, применяемом в большинстве UNIX-систем, - это бесплатно распространяемый в сети Inetrnet ассемблер NASM. Мы будем рассматривать только ассемблеры, непосредственно входящие в состав операционных систем, то есть ассемблеры, которые вызываются стандартной командой as.
Запись команд
Названия команд, не принимающих операндов, совпадают с названиями, принятыми в синтаксисе Intel:
nop
К названиям команд, имеющих операнды, добавляются суффиксы, отражающие размер операндов:
b - байт;
w - слово;
l - двойное слово;
q - учетверенное слово;
s - 32-битное число с плавающей запятой;
l - 64-битное число с плавающей запятой;
t - 80-битное число с плавающей запятой.
// mov byte ptr variable,0 movb $0,variable // fild qword ptr variable fildq variable
Команды, принимающие операнды разных размеров, требуют указания двух суффиксов, сначала суффикса источника, а затем приемника:
// movsx edx,al movsbl %al,%edx
Команды преобразования типов имеют в AT&T названия из четырех букв - С, размер источника, Т и размер приемника:
// cbw cbtw // cwde cwtl // cwd cwtl // cdq cltd
Но многие ассемблеры понимают и принятые в Intel формы для этих четырех команд.
Дальние команды передачи управления (jmp, call, ret) отличаются от ближних префиксом l:
// call far 0007:00000000 lcall $7,$0 // retf 10 lret $10
Если команда имеет несколько операндов, операнд-источник всегда записывается первым, а приемник - последним, то есть в точности наоборот по сравнению с Intel-синтаксисом:
// mov ax,bx movw %bx,%ax // imul eax.ecx,16 imull $16,%ecx,%eax
Все префиксы имеют имена, которыми они задаются как обычные команды, - перед командой, для которой данный префикс предназначен. Имена префиксов замены сегмента - segcs, segds, segss, segfs, seggs, имена префиксов изменения разрядности адреса и операнда- addr16 и data 16:
segfs movl variable,%eax rep stosd
Кроме того, префикс замены сегмента будет включен автоматически, если используется оператор ":" в контексте операнда:
movl %fs:variable, %eax
Символы ASCII
Глоссарий
Активационная запись (activation record) - область стека, заполняемая при вызове процедуры
Ассемблер (assembly language) - язык программирования низкого уровня
Ассемблер (assembler) - компилятор с языка ассемблера
Байт (byte) - тип данных, имеющий размер 8 бит, минимальная адресуемая единица памяти
Бит (bit) - минимальная единица измерения информации
"Всплывающая" программа (popup program) - резидентная программа, активирующаяся по нажатию определенной "горячей" клавиши
"Горячая" клавиша (hotkey) - клавиша или комбинация клавиш, используемая не для ввода символов, а для вызова программ и подобных необычных действий
Двойное слово (double word) - тип данных, имеющий размер 32 бита
Дескриптор (descriptor) - восьмибайтная структура, хранящаяся в одной из таблиц GDT, LDT или IDT и описывающая сегмент или шлюз
Директива (directive) - команда ассемблеру, которая не соответствует командам процессора
Драйвер (driver) - служебная программа, выполняющая функции посредника между операционной системой и внешним устройством
Защищенный режим (protected mode) - режим процессора, в котором действуют механизмы защиты, сегментная адресация с дескрипторами и селекторами и страничная адресация
Задача (task) - программа, модуль или другой участок кода программы, который можно запустить, выполнять, отложить и завершить
Идентификатор (handle или identifier) - число (если handle) или переменная другого типа, используемая для идентификации того или иного ресурса
Исключение (exception) - событие, при котором выполнение программы прекращается и управление передается обработчику исключения
Код (code) - исполнимая часть программы (обычная программа состоит из кода, данных и стека)
Компилятор (compiler) - программа, преобразующая текст, написанный на понятном человеку языке программирования, в исполнимый файл
Конвейер (pipe) - последовательность блоков процессора, которая задействуется при выполнении команды
Конвенция (convention) - договоренность о передаче параметров между процедурами
Конечный автомат (finite state machine) - программа, которая может переключаться между различными состояниями и выполнять в разных состояниях разные действия
Кэш (cache) - быстрая память, использующаяся для буферизации обращений к основной памяти
Лимит (limit) - поле дескриптора (равно размеру сегмента минус 1)
Линейный адрес (linear address) - адрес, получаемый сложением смещения и базы сегмента
Ловушка (trap) - исключение, происходящее после вызвавшей его команды
Метка (label) - идентификатор, связанный с адресом в программе
Нить (thread) - процесс, данные и код которого совпадают с данными и кодом других процессов
Нереальный режим (unreal mode) - реальный режим с границами сегментов по 4 Гб
Операнд (operand) - параметр, передаваемый команде процессора
Описатель носителя (media descriptor) - байт, используемый DOS для идентификации типа носителя (обычно не используется)
Останов (abort) - исключение, происходящее асинхронно
Отложенное вычисление (lazy evaluation) - вычисление, которое выполняется, только если реально требуется его результат
Очередь предвыборки (prefetch queue) - буфер, из которого команды передаются на расшифровку и выполнение
Ошибка (fault) - исключение, происходящее перед вызвавшей его командой
Пиксель (pixel) - минимальный элемент растрового изображения
Повторная входимость (reentrancy) - возможность запуска процедуры из обработчика прерывания, прервавшего выполнение этой же процедуры
Подчиненный сегмент (conforming segment) - сегмент, на который можно передавать управление программам с более низким уровнем привилегий
Прерывание (interrupt) - сигнал от внешнего устройства, приводящий к прерыванию выполнения текущей программы и передаче управления специальной программе-обработчику (см. ловушка)
Разворачивание циклов (loop unrolling) - превращение циклов, выполняющихся известное число раз, в линейный участок кода
Реальный режим (real mode) - режим, в котором процессор ведет себя идентично 8086 - адресация не выше одного мегабайта памяти, размер всех сегментов ограничен и равен 64 Кб, только 16-битный режим
Резидентная программа (resident program) - программа, остающаяся в памяти после возврата управления в DOS
Сегмент (segment) - элемент сегментной адресации в памяти или участок программы для DOS/Windows
Селектор (selector) - число, хранящееся в сегментном регистре
Секция (section) - участок программы для UNIX
Скан-код (scan-code) - любой код, посылаемый клавиатурой
Слово (word) - тип данных, имеющий размер 16 бит
Смещение (offset) - относительный адрес, отсчитываемый от начала сегмента
Стековый кадр (stack frame) - область стека, занимаемая параметрами процедуры, активационной записью и локальными переменными или только локальными переменными
Страничная адресация (pagination) - механизм адресации, в котором линейное адресное пространство разделяется на страницы, которые могут располагаться в разных областях памяти или вообще отсутствовать
Таблица переходов (jumptable) - массив адресов процедур для косвенного перехода на процедуру с известным номером
Шлюз (gate) - структура данных, позволяющая осуществлять передачу управления между разными уровнями привилегий в защищенном режиме
Кодировки второй половины ASCII

Рис. 19. Кодировка IBM
Кодировка по умолчанию для первых компьютеров - этот набор символов хранится в постоянной памяти и используется BIOS (рис. 19).

Рис. 20. Кодировка cp866
Кодировка cp866 используется DOS-приложениями как основная кодировка и компьютерной сетью Fidonet как транспортная кодировка (рис. 20).

Рис. 21. Кодировка KOI8-R (RFC 1489)
Кодировка KOI8-R используется как транспортная кодировка в Internet и как основная кодировка в большинстве бесплатно распространяемых операционных систем (рис. 21).

Рис. 22. Кодировка ISO 8859-5
Кодировка ISO 8859-5 используется как основная в большинстве коммерческих UNIX-совместимых операционных систем (рис. 22).

Рис. 23. Кодировка cp1251
Кодировка cp1251 используется как основная в графических приложениях для Microsoft Windows (рис. 23).
Коды символов расширенного ASCII
Таблица 24. Расширенные ASCII-коды
| Клавиша |
Код |
Клавиша |
Код |
Клавиша |
Код |
Клавиша |
Код |
Клавиша |
Код |
| F1 |
3Bh |
Alt-R |
13h |
Shift-F11 |
87h |
Alt-Tab |
A5h |
Alt-I |
17h |
| F2 |
3Ch |
Alt-S |
1Fh |
Shift-F12 |
88h |
Ctrl-Tab |
94h |
Alt-J |
24h |
| F3 |
3Dh |
Alt-T |
14h |
Alt-0 |
81h |
Alt-Del |
A3h |
Alt-K |
25h |
| F4 |
3Eh |
Alt-U |
16h |
Alt-1 |
82h |
Alt-End |
9Fh |
Alt-L |
26h |
| F5 |
3Fh |
Alt-V |
2Fh |
Alt-2 |
83h |
Alt-Home |
97h |
Ctrl-Right |
74h |
| F6 |
40h |
Alt-W |
11h |
Alt-3 |
84h |
Alt-Ins |
A2h |
Ctrl-End |
75h |
| F7 |
41h |
Alt-X |
2Dh |
Alt-4 |
85h |
Alt-PgUp |
99h |
Ctrl-Home |
77h |
| F8 |
42h |
Alt-Y |
15h |
Alt-5 |
86h |
Alt-PgDn |
A1h |
Ctrl-PgDn |
76h |
| F9 |
43h |
Alt-Z |
2Ch |
Alt-6 |
87h |
Alt-Enter |
1Ch |
Ctrl-PgUp |
84h |
| F10 |
44h |
Alt-\ |
2Bh |
Alt-7 |
88h |
Ctrl-F1 |
5Eh |
Alt-Up |
98h |
| F11 |
85h |
Alt-, |
33h |
Alt-8 |
89h |
Ctrl-F2 |
5Fh |
Alt-Down |
A0h |
| F12 |
86h |
Alt-. |
34h |
Alt-9 |
8Ah |
Ctrl-F3 |
60h |
Alt-Left |
9Bh |
| Alt-F1 |
68h |
Alt-/ |
35h |
AltC |
8Bh |
Ctrl-F4 |
61h |
Alt-Right |
9Dh |
| Alt-F2 |
69h |
Alt-BS |
0Eh |
Alt-= |
8Ch |
Ctrl-F5 |
62h |
Alt-K/ |
A4h |
| Alt-F3 |
6Ah |
Alt-[ |
1Ah |
NUL |
03h |
Ctrl-F6 |
63h |
Ctrl-K* |
37h |
| Alt-F4 |
6Bh |
Alt-] |
1Bh |
Shift-Tab |
0Fh |
Ctrl-F7 |
64h |
Alt-K- |
4Ah |
| Alt-F5 |
6Ah |
Alt-; |
27h |
Ins |
52h |
Ctrl-F8 |
65h |
Alt-K+ |
4Eh |
| Alt-F6 |
6Dh |
Alt-' |
28h |
Del |
53h |
Ctrl-F9 |
66h |
Alt-KEnter |
A6h |
| Alt-F7 |
6Eh |
Alt-` |
29h |
SysRq |
72h |
Ctrl-F10 |
67h |
Ctrl-K/ |
95h |
| Alt-F8 |
6Fh |
Shift-F1 |
54h |
Down |
50h |
Ctrl-F11 |
89h |
Ctrl-K* |
96h |
| Alt-F9 |
70h |
Shift-F2 |
55h |
Left |
4Bh |
Ctrl-F12 |
8Ah |
Ctrl-K- |
8Eh |
| Alt-F10 |
71h |
Shift-F3 |
56h |
Right |
4Dh |
Alt-A |
1Eh |
Ctrl-K+ |
90h |
| Alt-F11 |
8Bh |
Shift-F4 |
57h |
Up |
48h |
Alt-B |
30h |
Ctrl-K8 |
8Dh |
| Alt-F12 |
8Ch |
Shift-F5 |
58h |
Enter |
4Fh |
Alt-C |
2Eh |
Ctrl-K5 |
8Fh |
| Alt-M |
32h |
Shift-F6 |
59h |
Home |
47h |
Alt-D |
20h |
Ctrl-K2 |
91h |
| Alt-N |
31h |
Shift-F7 |
5Ah |
PgDn |
51h |
Alt-E |
12h |
Ctrl-K0 |
92h |
| Alt-O |
18h |
Shift-F8 |
5Bh |
PgUp |
49h |
Alt-F |
21h |
Ctrl-K. |
93h |
| Alt-P |
19h |
Shift-F9 |
5Ch |
Ctrl-Left |
73h |
Alt-G |
22h |
|
|
| Alt-Q |
10h |
Shift-F10 |
5Dh |
Alt-Esc |
01h |
Alt-H |
23h |
|
|
Префикс "K" соответствует клавишам на цифровой клавиатуре.
Команды процессоров Intel 8088– Pentium II
Таблица 27. Команды
| Команда |
Код |
8088 8087 |
80186 |
80286 80287 |
80386 80387 |
80486 |
P 5 |
P 6 |
| AAA |
37 |
8 |
8 |
3 |
4 |
3 |
3 NP |
1m |
| AAD i8 |
D5 ib |
60 |
15 |
14 |
19 |
14 |
10 NP |
3m |
| AAM i8 |
D4 ib |
83 |
19 |
16 |
17 |
15 |
18 NP |
4m |
| AAS |
3F |
8 |
7 |
3 |
4 |
3 |
3 NP |
1m |
| ADC ac,im |
14w im |
4 |
4 |
3 |
2 |
1 |
1 PU |
2m |
| ADC r,im |
80sw /2im |
4 |
4 |
3 |
2 |
1 |
1 PU |
2m |
| ADC m,im |
80sw /2im |
23+ea |
16 |
7 |
7 |
3 |
3 PU |
4m |
| ADC r,r |
10dw /r |
3 |
3 |
2 |
2 |
1 |
1 PU |
2m |
| ADC m,r |
10dw /r |
24+ea |
10 |
7 |
7 |
3 |
3 PU |
4m |
| ADC r,m |
10dw /r |
13+ea |
10 |
7 |
6 |
2 |
2 PU |
3m |
| ADD ac,im |
04w im |
4 |
4 |
3 |
2 |
1 |
1 UV |
1m |
| ADD r,im |
80sw /0 im |
4 |
4 |
3 |
2 |
1 |
1 UV |
1m |
| ADD m,im |
80sw /0 im |
23+ea |
16 |
7 |
7 |
3 |
3 UV |
4m |
| ADD r,r |
00dw /r |
3 |
3 |
2 |
2 |
1 |
1 UV |
1m |
| ADD m,r |
00dw /r |
24+ea |
10 |
7 |
7 |
3 |
3 UV |
4m |
| ADD r,m |
00dw /r |
13+ea |
10 |
7 |
6 |
2 |
2 UV |
2m |
| AND ac,im |
24w im |
4 |
4 |
3 |
2 |
1 |
1 UV |
1m |
| AND r,im |
80sw /4 im |
4 |
4 |
3 |
2 |
1 |
1 UV |
1m |
| AND m,im |
80sw /4 im |
23+ea |
16 |
7 |
7 |
3 |
3 UV |
4m |
| AND r,r |
20dw /r |
3 |
3 |
2 |
2 |
1 |
1 UV |
1m |
| AND m,r |
20dw /r |
24+ea |
10 |
7 |
7 |
3 |
3 UV |
4m |
| AND r,m |
20dw /r |
13+ea |
10 |
7 |
6 |
2 |
2 UV |
2m |
| ARPL r,r |
63 /r |
|
|
10 |
20 |
9 |
7 NP |
C |
| ARPL m,r |
63 /r |
|
|
11 |
21 |
9 |
7 NP |
C |
| BOUND r,m |
62 /r |
|
35 |
13 |
10 |
7 |
8 NP |
C |
| BSF r16,r16 |
0F BC /r |
|
|
|
10+3n |
6..42 |
6..34 NP |
2m |
| BSF r32,r32 |
0F BC /r |
|
|
|
10+3n |
6..42 |
6..42 NP |
2m |
| BSF r16,m16 |
0F BC /r |
|
|
|
10+3n |
6..43 |
6..35 NP |
3m |
| BSF r32,m32 |
0F BC /r |
|
|
|
10+3n |
6..43 |
6..43 NP |
3m |
Общая информация о скоростях выполнения
Скорости выполнения команд для процессоров 8086 – Р5 даны в тактах (когда говорят, что тактовая частота процессора 100 MHz, это означает, что за секунду проходит 100 миллионов тактов).
Для процессоров Р5 (Pentium, Pentium MMX) помимо скорости указано, может ли команда выполняться одновременно с другими, и если да, то в каком конвейере (см. главу 9.3.2):
UV - может выполняться одновременно, в любом конвейере;
PU - может выполняться одновременно, в U-конвейере;
PV - может выполняться одновременно, в V-конвейере;
FX - может выполняться одновременно с командой FXCH;
NP - не может выполняться одновременно (для ММХ - не может выполняться одновременно с командой того же типа, который указан после буквы "n").
Для процессоров Р6 (Pentium Pro, Pentium II) указано количество микроопераций, на которые декодируется команда. Буквой "С" отмечены команды со сложным строением (см. главу 9.3.3).
Во всех случаях даны минимально возможные скорости - если шина данных не заблокирована, операнды выровнены по границам двойных слов, операнды находятся в кэше данных, команды по адресу для перехода находятся в кэше кода, переходы угаданы процессором правильно, в момент выполнения команды не происходит заполнения кэша, страницы находятся в TLB (иначе для Р5 следует прибавить 13 – 28 тактов), не происходит исключений в момент выполнения команды, не происходят AGI и т.д.
Операнды обозначаются следующим образом:
im - непосредственный операнд;
i8, i16, i32 - непосредственный операнд указанного размера;
ас - ЕАХ, АХ, AL;
r - любой регистр общего назначения;
r8 - АН, AL, BH, BL, DH, DL, CH, CL;
r16 - АХ, ВХ, СХ, DX, ВР, SP, SI, DI;
r32 - ЕАХ, ЕВХ, ЕСХ, EDX, EBP, ESP, ESI, EDI;
sr - сегментный регистр;
m - операнд в памяти;
mm - регистр ММХ;
s0 - регистр ST(0);
si - регистр ST(i).
Для команд условных переходов приводятся два значения скорости выполнения - если переход произошел и если нет.
Другие используемые сокращения:
РМ - защищенный режим;
RM - реальный режим;
VM - режим V86;
TS - переключение задачи;
CG - шлюз вызова;
TG - шлюз задачи;
/in - увеличение привилегий;
/out - уменьшение привилегий;
/NT - переход во вложенную задачу.
Время переключения задачи:
Pentium: TS = 85 при переключении в 32-битный и 16-битный TSS и 71 при переключении в V86 TSS;
80486: TS = 199 при переключении в 32-битный и 16-битный TSS и 177 при переключении в V86 TSS;
80386: TS = 307 – 314 при переключении в 32-битный и 16-битный TSS и 224 – 231 при переключении в V86;
80286: JS = 280.
Общий формат команды процессора Intel
Команда может содержать до шести полей:
Префиксы- от нуля до четырех однобайтных префиксов.
Код - один или два байта, определяющие команду.
ModR/M - 1 байт (если он требуется), описывающий операнды:
биты 7 – 6: поле MOD - режим адресации;
биты 5 – 3: поле R/O - либо указывает регистр, либо является продолжением кода команды;
биты 2 – 0: поле R/M - либо указывает регистр, либо совместно с MOD - режим адресации.
SIB - 1 байт, если он требуется (расширение ModR/M для 32-битной адресации):
биты 7 – 6: S - коэффициент масштабирования;
биты 5 – 3: I - индексный регистр;
биты 2 – 0: В - регистр базы.
Смещение - 0, 1, 2 или 4 байта.
Непосредственный операнд - 0, 1, 2 или 4 байта - будем использовать /ib и /iw для указания этих операндов.
Префиксы
Все префиксы выполняются за 1 такт и имеют размер 1 байт:
F0h: LOCK
F2h: REPNE/REPNZ
F3h: REP/REPE/REPZ
2Eh: CS:
36h: SS:
3Eh: DS:
26h: ES:
64h: FS:
65h: GS:
66h: OS
67h: AS
В этом приложении приведены скорости
В этом приложении приведены скорости выполнения всех команд процессоров Intel от 8086 до Pentium II и машинные коды, которые им соответствуют.
Символы ASCII
Номера строк соответствуют первой цифре в шестнадцатеричном коде символа, номера столбцов - второй, так что, например, код большой латинской буквы A - 41h (см. рис. 18).

Рис. 18. Таблица символов ASCII
Скан-коды клавиатуры
Таблица 25. Скан-коды
| Клавиша |
Код |
Клавиша |
Код |
Клавиша |
Код |
Клавиша |
Код |
| Esc |
01h |
Enter |
1Ch |
K* |
37h |
Ins |
52h |
| 1 ! |
02h |
Ctrl |
1Dh |
Alt |
38h |
Del |
53h |
| 2 @ |
03h |
A |
1Eh |
SP |
39h |
SysRq |
54h |
| 3 # |
04h |
S |
1Fh |
Caps |
3Ah |
Macro |
56h |
| 4 $ |
05h |
D |
20h |
F1 |
3Bh |
F11 |
57h |
| 5 % |
06h |
F |
21h |
F2 |
3Ch |
F12 |
58h |
| 6 ^ |
07h |
G |
22h |
F3 |
3Dh |
PA1 |
5Ah |
| 7 & |
08h |
H |
23h |
F4 |
3Eh |
F13/LWin |
5Bh |
| 8 * |
09h |
J |
24h |
F5 |
3Fh |
F14/RWin |
5Ch |
| 9 ( |
0Ah |
K |
25h |
F6 |
40h |
F15/Menu |
5Dh |
| 0 ) |
0Bh |
L |
26h |
F7 |
41h |
F16 |
63h |
| - _ |
0Ch |
; : |
27h |
F8 |
42h |
F17 |
64h |
| = + |
0Dh |
' " |
28h |
F9 |
43h |
F18 |
65h |
| BS |
0Eh |
` ~ |
29h |
F10 |
44h |
F19 |
66h |
| Tab |
0Fh |
LShift |
2Ah |
Num |
45h |
F20 |
67h |
| Q |
10h |
\ | |
2Bh |
Scroll |
46h |
F21 |
68h |
| W |
11h |
Z |
2Ch |
Home |
47h |
F22 |
69h |
| E |
12h |
X |
2Dh |
- |
48h |
F23 |
6Ah |
| R |
13h |
C |
3Eh |
PgUp |
49h |
F24 |
6Bh |
| T |
14h |
V |
2Fh |
K- |
4Ah |
EraseEOF |
6Dh |
| Y |
15h |
B |
30h |
|
4Bh |
Copy/Play |
6Fh |
| U |
16h |
N |
31h |
K5 |
4Ch |
CrSel |
72h |
| I |
17h |
M |
32h |
® |
4Dh |
Delta |
73h |
| O |
18h |
, < |
33h |
K+ |
4Eh |
ExSel |
74h |
| P |
19h |
. > |
34h |
End |
4Fh |
Clear |
76h |
| [ { |
1Ah |
/ ? |
35h |
I |
50h |
|
|
| ] } |
1Bh |
RShift |
36h |
PgDn |
51h |
|
|
Префикс "K" соответствует клавишам на цифровой клавиатуре.
Таблица 26. Служебные скан-коды
| Код |
Функция |
Код |
Функция |
| 00h |
Буфер клавиатуры переполнен |
FAh |
ACK |
| AAh |
Самотестирование закончено |
FCh |
Ошибка самотестирования |
| E0h |
Префикс для серых клавиш |
FDh |
Ошибка самотестирования |
| E1h |
Префикс для клавиш без кода отпускания |
FEh |
RESEND |
| F0h |
Префикс отпускания клавиши |
FFh |
Ошибка клавиатуры |
| EEh |
Эхо |
|
|
Список используемых сокращений
ABI - Application Binary Interface - интерфейс для приложений на низком уровне
AGI - Address Generation Interlock - задержка для генерации адреса
AMIS - Alternative Multiplex Interrupt Specification - спецификация альтернативного мультиплексорного прерывания
API - Application Program Interface - интерфейс между приложением и программой
ASCII - American Standard Code for Information Interchange - американский стандартный код для обмена информацией
AT&T - American Telephone and Telegraph - американский телефон и телеграф (компания, которой принадлежала торговая марка UNIX)
BCD - Binary Coded Decimal - двоично-десятичный формат
BIOS - Basic Input/Output System - основная система ввода-вывода
BIT - BInary digiT - двоичная цифра
BPB - BIOS Parameter Block - блок параметров BIOS (для блочных устройств)
BRM - Big Real Mode - большой реальный режим (то же, что и нереальный режим)
BSD - Berkeley System Distribution - один из основных видов UNIX-систем
BSS - Block, Started by Symbol - участок программы, содержащий неинициализированные данные
CMOS - Complementary Metal Oxide Semiconductor - комплементарные металлооксидные пары
COFF - Common Object File Format - общий формат объектных файлов
CPL - Current Privilege Level - текущий уровень привилегий
CRT - Cathode Ray Tube - электронно-лучевая трубка
DAC - Digital to Analog Converter - цифро-аналоговый преобразователь
DDK - Drivers Development Kit - набор для создания драйверов
DLL - Dynamically Linked Library - динамическая библиотека
DMA - Direct Memory Access - прямой доступ к памяти
DOS - Disk Operating System - дисковая операционная система
DPL - Descriptor Privilege Level - уровень привилегий дескриптора
DPMI - DOS Protected Mode Interface - интерфейс для защищенного режима в DOS
DSP - Digital Signal Processor - процессор для оцифрованного звука в звуковых картах
DTA - Disk Transfer Area - область передачи дисковых данных (в DOS)
ELF - Executable and Linking Format - формат исполнимых и компонуемых файлов
EMS - Expanded Memory Specification - спецификация доступа к дополнительной памяти
ЕРВ - Execution Program Block - блок информации об исполняемой программе
FAT - File Allocation Table - таблица распределения файлов
FCR - FIFO control register - регистр управления FIFO
FIFO - First In First Out - первый вошел - первый вышел (очередь)
FM - Frequency Modulation - частотный синтез
FPU - Floating Point Unit - блок для работы с числами с плавающей запятой
GDT - Global Descriptor Table - глобальная таблица дескрипторов
HCI - Human Computer Interface - интерфейс между пользователем и программой
HMA - High Memory Area - верхняя область памяти (64 Кб после первого мегабайта)
IBM - International Business Machines - название компании
ICW - Initialization Control Word - управляющее слово инициализации
IDE - Integrated Drive Electronics - один из интерфейсов для жестких дисков
IDT - Interrupt Descriptor Table - таблица дескрипторов обработчиков прерываний
IER - Interrupt Enable Register - регистр разрешения прерываний
IOCTL - Input/Output Contorl - управление вводом-выводом
IOPL - Input/Output Privilege Level - уровень привилегий ввода-вывода
IRQ - Interrupt Request - запрос на прерывание (от внешнего устройства)
ISP - Interrupt Sharing Protocol - протокол разделения прерываний
LCR - Line Control Register - регистр управления линией
LDT - Local Descritor Table - таблица локальных дескрипторов
LE - Linear Executable - линейный исполнимый формат
LFN - Long File Name - длинное имя файла
LSR - Line Status Register - регистр состояния линии
MASM - Macro Assembler - макроассемблер (ассемблер компании Microsoft)
MCR - Modem Control Register - регистр управления модемом
ММХ - Multimedia Extention - расширение для работы с мультимедийными приложениями
MSR - Modem State Register - регистр состояния модема
MSR - Machine Specific Register - машинно-специфичный регистр
NE - New Executable - новый исполнимый формат
NPX - Numerical Processor Extention - расширение для работы с числами с плавающей запятой
OCW - Operation Control Word - управляющее слово (для контроллера прерываний)
РЕ - Portable Executable - переносимый исполнимый формат
POST - Power On Self Test - самотестирование при включении
PSP - Program Segment Prefix - префикс программного сегмента
RBR - Reciever Buffer Register - регистр буфера приемника
RFC - Request For Comments - запрос для комментария (форма публикации документов в Internet, включая стандарты)
RFM - Real Flat Mode - реальный flat-режим (то же, что и нереальный режим)
RTC - Real Time Clock - часы реального времени
RPL - Requestor Privilege Level - запрашиваемый уровень привилегий
RPN - Reverse Polish Notation - обратная польская запись (для арифметических выражений)
SCSI - Small Computer System Interface - один из интерфейсов для жестких дисков
SUN - Stanford University Networks - название компании
SVGA - Super VGA - любой видеоадаптер, способный на режимы, большие 13h
TASM - Turbo Assembler - турбоассемблер (ассемблер компании Borland)
THR - Transmitter Holding Register - регистр хранения передатчика
TSR - Terminate and Stay Resident - завершиться и остаться резидентным
TSS - Task State Segment - сегмент состояния задачи
UMB - Upper Memory Block - блок верхней памяти (между границами 640 Кб и 1 Мб)
VBE - VESA BIOS Extention - спецификация VESA для расширения BIOS
VCPI - Virtual Control Program Interface - один из интерфейсов к защищенному режиму для DOS
VESA - Video Electronics Standard Association - Ассоциация по стандартизации видео в электронике
VGA - Video Graphics Array - основной тип видеоадаптеров
VxD - Virtual X Device - виртуальное устройство Х (общее название виртуальных драйверов в Windows 95)
WASM - Watcom Assembler - ассемблер компании Watcom
XMS - Extended Memory Specification - спецификация доступа к расширенной памяти
Управляющие символы ASCII
Таблица 23. Управляющие символы ASCII
| Код |
Имя |
Ctrl-код |
Назначение |
| 00 |
NUL |
^@ |
Пусто (конец строки) |
| 01 |
SOH |
^A |
Начало заголовка |
| 02 |
STX |
^B |
Начало текста |
| 03 |
EOT |
^C |
Конец текста |
| 04 |
ENQ |
^D |
Конец передачи |
| 06 |
ACK |
^F |
Подтверждение |
| 07 |
BEL |
^G |
Звонок |
| 08 |
BS |
^H |
Шаг назад |
| 09 |
HT |
^I |
Горизонтальная табуляция |
| 0A |
LF |
^J |
Перевод строки |
| 0B |
VT |
^K |
Вертикальная табуляция |
| 0C |
FF |
^L |
Перевод страницы |
| 0D |
CR |
^M |
Возврат каретки |
| 0E |
SO |
^N |
Выдвинуть |
| 0F |
SI |
^O |
Сдвинуть |
| 10 |
DLE |
^P |
Оставить канал данных |
| 11 |
DC1/XON |
^Q |
Управление устройством 1 |
| 12 |
DC2 |
^R |
Управление устройством 2 |
| 13 |
DC3/XOFF |
^S |
Управление устройством 3 |
| 14 |
DC4 |
^T |
Управление устройством 4 |
| 15 |
NAK |
^U |
Отрицательное подтверждение |
| 16 |
SYN |
^V |
Синхронизация |
| 17 |
ETB |
^W |
Конец блока передачи |
| 18 |
CAN |
^X |
Отмена |
| 19 |
EM |
^Y |
Конец носителя |
| 1A |
SUB |
^Z |
Замена |
| 1B |
ESC |
^[ |
Escape |
| 1C |
FS |
^\ |
Разделитель файлов |
| 1D |
GS |
^] |
Разделитель групп |
| 1E |
RS |
^^ |
Разделитель записей |
| 1F |
US |
^_ |
Разделитель полей |
| 20 |
SP |
|
Пробел |
| 7F |
DEL |
^? |
Удаление |
Значения полей кода команды
В кодах некоторых команд мы будем встречать специальные биты и группы бит, которые обозначим w, s, d, reg, sreg и cond:
w = 0, если команда работает с байтами;
w = 1, если команда работает со словами или двойными словами;
s = 0, если непосредственный операнд указан полностью;
s = 1, если непосредственный операнд - младший байт большего операнда и должен рассматриваться как число со знаком;
d = 0, если код источника находится в поле R/O, а приемника - в R/M;
d = 1, если код источника находится в поле R/M, а приемника - в R/O.
Запись 10dw будет означать, что код команды - 000100dw.
Поле reg определяет используемый регистр и имеет длину 3 бита:
000 - AL/AX/EAX/ST(0)/MM0
001 - CL/CX/ECX/ST(1)/MM1
010 - DL/DX/EDX/ST(2)/MM2
011 - BL/BX/EBX/ST(3)/MM3
100 - AH/SP/ESP/ST(4)/MM4
101 - CH/BP/EBP/ST(5)/MM5
110 - DH/SI/ESI/ST(6)/MM6
111 - BH/DI/EDI/ST(7)/MM7
Запись C8r будет означать 11001reg.
Поле sreg определяет используемый сегментный регистр:
000 - ES
001 - CS
010 - SS
011 - DS
100 - FS
110 - GS
Поле cond определяет условие для команд Jcc, CMOVcc, SETcc, FCMOVcc.
Его значения для разных команд:
0000 - О
0001 - NO
0010 - C/B/NAE
0011 - NC/NB/AE
0100 - E/Z
0101 - NE/NZ
0110 - BE/NA
0111 - NBE/A
1000 - S
1001 - NS
1010 - Р/РЕ
1011 - NP/PO
1100 - L/NGE
1101 - NL/GE
1110 - LE/NG
1111 - LNE/G
Запись типа 4сс будет означать 0100cond.
Значения поля ModRM
Поле R/O (биты 5– 3) содержит либо дополнительные три бита кода команды, либо код операнда, который может быть только регистром. Будем обозначать второй случай reg, а в первом записывать используемые биты.
Поля MOD (биты 7 – 6) и R/M (биты 3 – 0) определяют операнд, который может быть как регистром, так и переменной в памяти:
MOD = 11, если используется регистровая адресация и R/M содержит код регистра reg;
MOD = 00, если используется адресация без смещения ([ВХ + SI] или [EDX]);
MOD = 01, если используется адресация с 8-битным смещением (variable[BX + SI]);
MOD = 10, если используется адресация с 16- или 32-битным смещением.
Значение поля R/M различно в 16- и 32-битных режимах.
R/M в 16-битном режиме:
000 - [ВХ + SI]
001 - [ВХ + DI]
010 - [BP + SI]
011 - [ВР + DI]
100 - [SI]
101 - [DI]
110 - [ВР] (кроме MOD = 00 - в этом случае после ModR/M располагается 16-битное смещение, то есть используется прямая адресация)
111 - [ВХ]
R/M в 32-битном режиме:
000 - [ЕАХ]
001 - [ЕСХ]
010 - [EDX]
011 - [ЕВХ]
100 - используется SIB
101 - [ЕВР] (кроме MOD = 00 - в этом случае используется SIB, после которого располагается 32-битное смещение)
110 - [ESI]
111 - [EDI]
Значения поля SIB
Значение поля S:
00 - не используется;
01 - умножение на 2;
10 - умножение на 4;
11 - умножение на 8;
Значения полей I и В:
(I - регистр, используемый в качестве индекса, то есть умножающийся на S, В - регистр базы, который не умножается)
000 - ЕАХ
001 - ЕСХ
010 - EDX
011 - ЕВХ
100 - для I - индекса нет
для В - ESP
101 - для I - ЕВР
для В - ЕВР, только если MOD = 01 или 10, если MOD = 00 - базы нет
110 - ESI
111 - EDI
Поля ModR/M и SIB будут записываться как /r, если поле R/O содержит код регистра, или /0 – /7, если поле R/O содержит дополнительные три бита кода команды. В других случаях поля ModR/M и SIB отсутствуют только у команд без операндов, так что они не будут обозначаться дополнительно.
Программирование: Языки - Технологии - Разработка