ДОС-вариант
Процессоры Intel в реальном режиме
Процессор Intel x86 после включения питания оказывается в так называемом режиме реальной адресации памяти, или просто реальном режиме. Большинство операционных систем сразу же переводят его в защищенный режим, позволяющий им обеспечивать многозадачность, распределение памяти и другие функции. Пользовательские программы в таких операционных системах часто работают еще в одном режиме, режиме V86, из которого им доступно все то же, что и из реального, кроме команд, относящихся к управлению защищенным режимом. Таким образом, эта глава описывает не только реальный режим, но и V86, то есть все то, что доступно программисту, если он не проектирует операционную систему или DPMI-сервер, в подавляющем большинстве случаев.
Регистры процессора
Начиная с 80386, процессоры Intel предоставляют 16 основных регистров для пользовательских программ плюс еще 11 регистров для работы с числами с плавающей запятой (FPU/NPX) и мультимедийными приложениями (MMX). Все команды так или иначе изменяют значения регистров, и всегда быстрее и удобнее обращаться к регистру, чем к памяти.
Помимо основных регистров из реального (но не из виртуального) режима доступны также регистры управления памятью (GDTR, IDTR, TR, LDTR) регистры управления (CR0, CR1 – CR4), отладочные регистры (DR0 – DR7) и машинно-специфичные регистры, но они не применяются для повседневных задач и будут рассматриваться в соответствующих главах позже.
Регистры общего назначения
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).
Другие четыре регистра общего назначения — ESI (индекс источника), EDI (индекс приемника), EBP (указатель базы), ESP (указатель стека) — имеют более конкретное назначение и могут применяться для хранения всевозможных временных переменных, только когда они не используются по назначению. Регистры ESI и EDI используются в строковых операциях, EBP и ESP используются при работе со стеком (см. параграф 2.1.3). Так же, как и с регистрами EAX – EDX, младшие половины этих четырех регистров называются SI, DI, BP и SP соответственно, и в процессорах до 80386 только они и присутствовали.
Сегментные регистры
При использовании каждой из сегментированных моделей памяти для формирования любого адреса применяются два числа — адрес начала сегмента и смещение искомого байта относительно этого начала (в бессегментной модели памяти flat адреса начал всех сегментов равны). Операционные системы (кроме DOS) могут размещать сегменты, с которыми работает программа пользователя, в разных местах в памяти, и даже могут временно записывать их на диск, если памяти не хватает. Так как сегменты могут оказаться где угодно, программа обращается к ним, используя вместо настоящего адреса начала сегмента 16-битное число, называемое селектором. В процессорах Intel предусмотрено шесть шестнадцатибитных регистров — CS, DS, ES, FS, GS, SS, используемых для хранения селекторов. (Регистры FS и GS отсутствовали в 8086, но появились уже в 80286.) Это не значит, что программа не может одновременно работать с большим количеством сегментов памяти, — в любой момент времени можно изменить значения, записанные в этих регистрах.
В отличие от регистров DS, ES, GS, FS, которые называются регистрами сегментов данных, регистры CS и SS отвечают за сегменты двух особенных типов — сегмент кода и сегмент стека. Сегмент кода содержит программу, исполняющуюся в данный момент, так что запись нового селектора в этот регистр приводит к тому, что далее будет исполнена не следующая по тексту программы команда, а команда из кода, находящегося в другом сегменте, с тем же смещением. Смещение следующей выполняемой команды всегда хранится в специальном регистре — EIP (указатель инструкции, шестнадцатибитная форма IP), запись в который также приведет к тому, что следующей будет исполнена какая-нибудь другая команда. На самом деле все команды передачи управления — перехода, условного перехода, цикла, вызова подпрограммы и т.п. — и осуществляют эту самую запись в CS и EIP.
Стек
Стек — это специальным образом организованный участок памяти, используемый для временного хранения переменных, для передачи параметров вызываемым подпрограммам и для сохранения адреса возврата при вызове процедур и прерываний. Легче всего представить стек в виде стопки листов бумаги (это одно из значений слова «stack» в английском языке) — вы можете класть и забирать листы бумаги только с вершины стопки. Таким образом, если записать в стек числа 1, 2, 3, то при чтении они будут получаться в обратном порядке — 3, 2, 1. Стек располагается в сегменте памяти, описываемом регистром SS, а текущее смещение вершины стека записано в регистре ESP, причем при записи в стек значение этого смещения уменьшается, то есть стек растет вниз от максимально возможного адреса (рис. 4). Такое расположение стека «вверх ногами» может быть необходимо, например в бессегментной модели памяти, когда все сегменты, включая сегмент стека и сегмент кода, занимают одну и ту же область — всю память. Тогда программа исполняется в нижней области памяти, в области малых адресов, и EIP растет, а стек располагается в верхней области памяти, и ESP уменьшается.
При вызове подпрограммы параметры в большинстве случаев помещают в стек, а в EBP записывают текущее значение ESP. Тогда, если подпрограмма использует стек для хранения локальных переменных, ESP изменится, но EBP можно будет использовать для того, чтобы считывать значения параметров напрямую из стека (их смещения будут записываться как EBP + номер параметра). Более подробно вызовы подпрограмм и все возможные способы передачи параметров рассмотрены в главе
Регистр флагов
Еще один важный регистр, использующийся при выполнении большинства команд, — регистр флагов EFLAGS. Как и раньше, его младшие 16 бит, представлявшие из себя весь этот регистр до 80386, называются FLAGS. В этом регистре каждый бит является флагом, то есть устанавливается в 1 при определенных условиях или установка его в 1 изменяет поведение процессора. Все флаги, расположенные в старшем слове регистра EFLAGS, имеют отношение к управлению защищенным режимом, поэтому здесь рассмотрен только регистр FLAGS (рис. 5).
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 (вложенная задача) применяются
в защищенном режиме.
Прямая адресация
Если известен адрес операнда, располагающегося в памяти, можно использовать этот адрес. Если операнд — слово, находящееся в сегменте, на который указывает 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 и в других ситуациях, когда программа будет запускаться в защищенном режиме, смещение не может превышать границ двойного слова.
Косвенная адресация
По аналогии с регистровыми и непосредственными операндами адрес операнда в памяти также можно не указывать непосредственно, а хранить в любом регистре. До 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-битного слова.
Адресация по базе со сдвигом
Теперь скомбинируем два предыдущих метода адресации: следующая команда
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, так же как и для обычной косвенной адресации. С помощью этого метода можно организовывать доступ к одномерным массивам байт: смещение соответствует адресу начала массива, а число в регистре — индексу элемента массива, который надо считать. Очевидно, что, если массив состоит не из байт, а из слов, придется умножать базовый регистр на два, а если из двойных слов — на четыре. Для этого предусмотрен следующий специальный метод адресации.
Косвенная адресация с масштабированием
Этот метод адресации полностью идентичен предыдущему, за исключением того, что с его помощью можно прочитать элемент массива слов, двойных слов или учетверенных слов, просто поместив номер элемента в регистр:
mov ax,[esi*2]+2
Множитель, который может быть равен 1, 2, 4 или 8, соответствует размеру элемента массива — байту, слову, двойному слову, учетверенному слову соответственно. Из регистров в этом варианте адресации можно использовать только EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, но не SI, DI, BP или SP, которые можно было использовать в предыдущих вариантах.
Адресация по базе с индексированием
В этом методе адресации смещение операнда в памяти вычисляется как сумма чисел, содержащихся в двух регистрах, и смещения, если оно указано. Все следующие команды — это разные формы записи одного и того же действия:
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 приемник, источник
Назначение:
Пересылка данных
Процессор:
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 относятся к сравнению чисел без знака, слова «больше» и «меньше» учитывают знак.
Команда:
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
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
Копирует содержимое источника (регистр или переменная размером в байт или слово) в приемник (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-битная, то вычисленное смещение дополняется нулями.
Двоичная арифметика
Все команды из этого раздела, кроме команд деления и умножения, изменяют флаги OF, SF, ZF, AF, CF, PF в соответствии с назначением каждого из этих флагов (см. главу 2.1.4).
Команда:
ADD приемник, источник
Назначение:
Сложение
Процессор:
8086
Команда выполняет арифметическое сложение приемника и источника, помещает сумму в приемник, не изменяя содержимое источника. Приемник может быть регистром или переменной, источник может быть числом, регистром или переменной, но нельзя использовать переменную одновременно и для источника, и для приемника. Команда ADD никак не различает числа со знаком и без знака, но, употребляя значения флагов CF (перенос при сложении чисел без знака), OF (перенос при сложении чисел со знаком) и SF (знак результата), можно использовать ее и для тех, и для других.
Команда:
ADC приемник, источник
Назначение:
Сложение с переносом
Эта команда во всем аналогична ADD, кроме того, что она выполняет арифметическое сложение приемника, источника и флага СF. Пара команд ADD/ADC используется для сложения чисел повышенной точности. Сложим, например, два 64-битных целых числа: пусть одно из них находится в паре регистров EDX:EAX (младшее двойное слово (биты 0 – 31) — в ЕАХ и старшее (биты 32 – 63) — в EDX), а другое — в паре регистров ЕВХ:ЕСХ:
add eax,ecx
adc edx,ebx
Если при сложении младших двойных слов произошел перенос из старшего разряда (флаг CF = 1), то он будет учтен следующей командой ADC.
Команда:
XADD приемник, источник
Назначение:
Обменять между собой и сложить
Выполняет сложение, помещает содержимое приемника в источник, — сумму операндов — в приемник. Источник всегда регистр, приемник может быть регистром и переменной.
Команда:
SUB приемник, источник
Назначение:
Вычитание
Процессор:
8086
Вычитает источник из приемника и помещает разность в приемник. Приемник может быть регистром или переменной, источник может быть числом, регистром или переменной, но нельзя использовать переменную одновременно и для источника, и для приемника. Точно так же, как и команда ADD, SUB не делает различий между числами со знаком и без знака, но флаги позволяют использовать ее как для тех, так и для других.
Команда:
SBB приемник, источник
Назначение:
Вычитание с займом
Процессор:
8086
Эта команда во всем аналогична 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 источник
Назначение:
Умножение чисел без знака
Выполняет умножение содержимого источника (регистр или переменная) и регистра AL, АХ, ЕАХ (в зависимости от размера источника) и помещает результат в АХ, DX:AX, EDX:EAX соответственно. Если старшая половина результата (АН, DX, EDX) содержит только нули (результат целиком поместился в младшую половину), флаги CF и OF устанавливаются в 0, иначе — в 1. Значение остальных флагов (SF, ZF, AF и PF) не определено.
Команда:
IDIV источник
Назначение:
Целочисленное деление со знаком
Процессор:
8086
Выполняет целочисленное деление со знаком AL, АХ или ЕАХ (в зависимости от размера источника) на источник (регистр или переменная) и помещает результат в AL, АХ или ЕАХ, а остаток — в АН, DX или EDX соответственно. Результат всегда округляется в сторону нуля, знак остатка всегда совпадает со знаком делимого, абсолютное значение остатка всегда меньше абсолютного значения делителя. Значения флагов CF, OF, SF, ZF, AF и PF после этой команды не определены, а переполнение или деление на ноль вызывает исключение #DE (ошибка при делении) в защищенном режиме и прерывание 0 — в реальном.
Команда:
DIV источник
Назначение:
Целочисленное деление без знака
Выполняет целочисленное деление без знака AL, АХ или ЕАХ (в зависимости от размера источника) на источник (регистр или переменная) и помещает результат в AL, АХ или ЕАХ, а остаток — в АН, DX или EDX соответственно. Результат всегда округляется в сторону нуля, абсолютное значение остатка всегда меньше абсолютного значения делителя. Значения флагов CF, OF, SF, ZF, AF и PF после этой команды не определены, а переполнение или деление на ноль вызывает исключение #DE (ошибка при делении) в защищенном режиме и прерывание 0 — в реальном.
Команда:
INC приемник
Назначение:
Инкремент
Процессор:
8086
Увеличивает приемник (регистр или переменная) на 1. Единственное отличие этой команды от ADD приемник,1 состоит в том, что флаг CF не затрагивается. Остальные арифметические флаги (OF, SF, ZF, AF, PF) устанавливаются в соответствии с результатом сложения.
Команда:
DEC приемник
Назначение:
Декремент
Уменьшает приемник (регистр или переменная) на 1. Единственное отличие этой команды от SUB приемник,1 состоит в том, что флаг CF не затрагивается. Остальные арифметические флаги (OF, SF, ZF, AF, PF) устанавливаются в соответствии с результатом вычитания.
Команда:
NEG приемник
Назначение:
Изменение знака
Выполняет над числом, содержащимся в приемнике (регистр или переменная), операцию дополнения до двух. Эта операция эквивалентна обращению знака операнда, если рассматривать его как число со знаком. Если приемник равен нулю, флаг CF устанавливается в 0, иначе — в 1. Остальные флаги (OF, SF, ZF, AF, PF) устанавливаются в соответствии с результатом операции.
Красивый пример применения команды NEG — получение абсолютного значения числа, используя всего две команды — изменение знака и переход на первую команду еще раз, если знак отрицательный:
label0: neg eax
js label0
Команда:
CMP приемник, источник
Назначение:
Сравнение
Сравнивает приемник и источник и устанавливает флаги. Сравнение осуществляется путем вычитания источника (число, регистр или переменная) из приемника (регистр или переменная; приемник и источник не могут быть переменными одновременно), причем результат вычитания никуда не записывается, единственным результатом работы этой команды оказывается изменение флагов CF, OF, SF, ZF, AF и PF. Обычно команду СМР используют вместе с командами условного перехода (Jcc), условной пересылки данных (CMOVcc) или условной установки байт (SETcc), которые позволяют использовать результат сравнения, не обращая внимания на детальное значение каждого флага. Так, команды CMOVE, JE и SETE выполнят соответствующие действия, если значения операндов предшествующей команды СМР были равны.
Несмотря на то что условные команды почти всегда применяются сразу после СМР, не надо забывать, что точно так же их можно использовать после любой команды, модифицирующей флаги, например: проверить равенство АХ нулю можно более короткой командой
test ax,ax
а равенство единице — однобайтной командой
dec ax
Команда:
CMPXCHG приемник, источник
Назначение:
Сравнить и обменять между собой
Сравнивает значение, содержащееся в AL, АХ, ЕАХ (в зависимости от размера операндов), с приемником (регистром). Если они равны, содержимое источника копируется в приемник и флаг ZF устанавливается в 1. Если они не равны, содержимое приемника копируется в AL, АХ, ЕАХ и флаг ZF устанавливается в 0. Остальные флаги устанавливаются по результату операции сравнения, как после СМР. Источник всегда регистр, приемник может быть регистром и переменной.
Команда:
CMPXCHG8B приемник
Назначение:
Сравнить и обменять восемь байт
Процессор:
Р5
Выполняет сравнение содержимого регистров EDX:EAX как 64-битного числа (младшее двойное слово в ЕАХ, старшее — в EDX) с приемником (восьмибайтная переменная в памяти). Если они равны, содержимое регистров ЕСХ:ЕВХ как 64-битное число (младшее двойное слово в ЕВХ, старшее — в ЕСХ) копируется в приемник. Иначе содержимое приемника копируется в EDX:EAX.
Команду LEA часто используют для быстрых арифметических вычислений, например умножения:
lea bx,[ebx+ebx*4] ; ВХ=ЕВХ*5
или сложения:
lea ebx,[eax+12] ; ЕВХ=ЕАХ+12
(эти команды меньше, чем соответствующие MOV и ADD, и не изменяют флаги)
Десятичная арифметика
Процессоры Intel поддерживают операции с двумя форматами десятичных чисел: неупакованное двоично-десятичное число — байт, принимающий значения от 00 до 09, и упакованное двоично-десятичное число — байт, принимающий значения от 00 до 99h. Все обычные арифметическиe операции над такими числами приводят к неправильным результатам. Например, если увеличить 19h на 1, то получится число 1Ah, а не 20h. Для коррекции результатов арифметических действий над двоично-десятичными числами используются следующие команды.
Команда:
DAA
Назначение:
BCD-коррекция после сложения
Если эта команда выполняется сразу после 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-коррекция после вычитания
Если эта команда выполняется сразу после 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-коррекция после сложения
Корректирует сумму двух неупакованных двоично-десятичных чисел в AL. Если коррекция приводит к десятичному переносу, АН увеличивается на 1. Эта команда имеет смысл сразу после команды сложения двух таких чисел. Например, если при сложении 05 и 06 в АХ окажется число 000Bh, то команда ААА скорректирует его в 0101h (неупакованное десятичное 11). Флаги CF и OF устанавливаются в 1, если произошел перенос из AL в АН, иначе они равны нулю. Значения флагов OF, SF, ZF и PF не определены.
Команда:
AAS
Назначение:
ASCII-коррекция после вычитания
Корректирует разность двух неупакованных двоично-десятичных чисел в AL сразу после команды SUB или SBB. Если коррекция приводит к займу, АН уменьшается на 1. Флаги CF и OF устанавливаются в 1, если произошел заем из AL в АН, и в ноль — в противном случае. Значения флагов OF, SF, ZF и PF не определены.
Команда:
AAM
Назначение:
ASCII-коррекция после умножения
Корректирует результат умножения неупакованных двоично-десятичных чисел, находящийся в АХ после выполнения команды 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-коррекция перед делением
Выполняет коррекцию неупакованного двоично-десятичного числа, находящегося в регистре АХ, так, чтобы последующее деление привело к корректному десятичному результату. Например, разделим десятичное 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 можно использовать для быстрого умножения на любое число.
Логические операции
Команда:
AND приемник, источник
Назначение:
Логическое И
Команда выполняет побитовое «логическое И» над приемником (регистр или переменная) и источником (число, регистр или переменная; источник и приемник не могут быть переменными одновременно) и помещает результат в приемник. Любой бит результата равен 1, только если соответствующие биты обоих операндов были равны 1, и равен 0 в остальных случаях. Наиболее часто AND применяют для выборочного обнуления отдельных бит, например, команда
and al,00001111b
обнулит старшие четыре бита регистра AL, сохранив неизменными четыре младших.
Флаги OF и CF обнуляются, SF, ZF и PF устанавливаются в соответствии с результатом, AF не определен.
Команда:
OR приемник, источник
Назначение:
Логическое ИЛИ
Выполняет побитовое «логическое ИЛИ» над приемником (регистр или переменная) и источником (число, регистр или переменная; источник и приемник не могут быть переменными одновременно) и помещает результат в приемник. Любой бит результата равен 0, только если соответствующие биты обоих операндов были равны 0, и равен 1 в остальных случаях. Команду OR чаще всего используют для выборочной установки отдельных бит, например, команда
or al,00001111b
приведет к тому, что младшие четыре бита регистра AL будут установлены в 1.
При выполнении команды OR флаги OF и CF обнуляются, SF, ZF и PF устанавливаются в соответствии с результатом, AF не определен.
Команда:
XOR приемник, источник
Назначение:
Логическое исключающее ИЛИ
Выполняет побитовое «логическое исключающее ИЛИ» над приемником (регистр или переменная) и источником (число, регистр или переменная; источник и приемник не могут быть переменными одновременно) и помещает результат в приемник. Любой бит результата равен 1, если соответствующие биты операндов различны, и нулю, если одинаковы. XOR используется для самых разных операций, например:
xor ах,ах ; обнуление регистра АХ
или
xor ах,bх
xor bх,ах
xor ах,bх ; меняет местами содержимое АХ и ВХ
Оба этих примера могут выполняться быстрее,
чем соответствующие очевидные команды
mov ax,0
или
xchg ax,bx
Команда:
NOT приемник
Назначение:
Инверсия
Каждый бит приемника (регистр или переменная), равный нулю, устанавливается в 1, и каждый бит, равный 1, сбрасывается в 0. Флаги не затрагиваются.
Команда:
TEST приемник, источник
Назначение:
Логическое сравнение
Вычисляет результат действия побитового «логического И» над приемником (регистр или переменная) и источником (число, регистр или переменная; источник и приемник не могут быть переменными одновременно) и устанавливает флаги SF, ZF и PF в соответствии с полученным результатом, не сохраняя результат (флаги OF и CF обнуляются, значение AF не определено). TEST, так же как и СМР, используется в основном в сочетании с командами условного перехода (Jcc), условной пересылки данных (CMOVcc) и условной установки байт (SETcc).
Сдвиговые операции
Команда:
SAR приемник, счетчик
Назначение:
Арифметический сдвиг вправо
Команда:
SAL приемник, счетчик
Назначение:
Арифметический сдвиг влево
Команда:
SHR приемник, счетчик
Назначение:
Логический сдвиг вправо
Команда:
SHL приемник, счетчик
Назначение:
Логический сдвиг влево
Эти четыре команды выполняют двоичный сдвиг приемника (регистр или переменная) вправо (в сторону старшего бита) или влево (в сторону младшего бита) на значение счетчика (число или регистр 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.
Сдвиги на 1 изменяют значение флага OF: SAL/SHL устанавливают его в 1, если после сдвига старший бит изменился (то есть старшие два бита исходного числа не были одинаковыми), и в 0, если старший бит остался тем же. SAR устанавливает OF в 0, и SHR устанавливает OF в значение старшего бита исходного числа. Для сдвигов на несколько бит значение OF не определено. Флаги SF, ZF, PF устанавливаются всеми сдвигами в соответствии с результатом, значение AF не определено (кроме случая, если счетчик сдвига равен нулю, в котором ничего не происходит и флаги не изменяются).
В процессорах 8086 непосредственно можно было задавать в качестве второго операнда только число 1 и при использовании CL учитывать все биты, а не только младшие 5, но уже начиная с 80186 эти команды приняли свой окончательный вид.
Команда:
SHRD приемник, источник, счетчик
Назначение:
Сдвиг повышенной точности вправо
Команда:
SHLD приемник, источник, счетчик
Назначение:
Сдвиг повышенной точности влево
Приемник (регистр или переменная) сдвигается влево (в случае SHLD) или вправо (в случае SHRD) на число бит, указанное в счетчике (число или регистр CL, откуда используются только младшие 5 бит, которые могут принимать значения от 0 до 31). Старший (для SHLD) или младший (в случае SHRD) бит не обнуляется, а считывается из источника (регистр), значение которого не изменяется. Например, если приемник содержал 00101001b, источник 1010b, счетчик равен 3, SHRD даст в результате 01000101b, a SHLD — 01001101b (см. рис. 8).
Флаг OF устанавливается при сдвигах на 1 бит, если изменился знак приемника, и сбрасывается, если знак не изменился; при сдвигах на несколько бит флаг OF не определен. Во всех случаях SF, ZF и PF устанавливаются в соответствии с результатом и AF не определен, кроме случая со сдвигом на 0 бит, в котором значения флагов не изменяются. Если счетчик больше, чем разрядность приемника, — результат и все флаги не определены.
Команда:
ROR приемник, счетчик
Назначение:
Циклический сдвиг вправо
Команда:
ROL приемник, счетчик
Назначение:
Циклический сдвиг влево
Команда:
RCR приемник, счетчик
Назначение:
Циклический сдвиг вправо через флаг переноса
Команда:
RCL приемник, счетчик
Назначение:
Циклический сдвиг влево через флаг переноса
Эти команды осуществляют циклический сдвиг приемника (регистр или переменная) на число бит, указанное в счетчике (число или регистр CL, из которого учитываются только младшие пять бит, принимающие значения от 0 до 31). При выполнении циклического сдвига на 1 команды ROR (ROL) сдвигают каждый бит приемника вправо (влево) на одну позицию, за исключением самого младшего (старшего), который записывается в позицию самого старшего (младшего) бита. Команды RCR и RCL выполняют аналогичное действие, но включают флаг CF в цикл, как если бы он был дополнительным битом в приемнике (рис. 9).
Операции над битами и байтами
Команда:
BT база, смещение
Назначение:
Проверка бита
Команда ВТ считывает во флаг 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 база, смещение
Назначение:
Проверка и инверсия бита
Эти три команды соответственно устанавливают в 1 (BTS), сбрасывают в 0 (ВТR) и инвертируют (ВТС) значение бита, который находится в битовой строке с началом, указанным в базе (регистр или переменная), и смещением, указанным во втором операнде (число от 0 до 31 или регистр). Если битовая база — регистр, то смещение не может превышать 15 или 31 в зависимости от разрядности этого регистра. Если битовая база — переменная в памяти, то смещение может принимать значения от -231 до 231–1 (если оно указано в регистре).
После выполнения команд BTS, BTR и ВТС флаг CF равен значению считанного бита до его изменения в результате действия команды, флаги OF, SF, ZF, AF и PF не определены.
Команда:
BSF приемник, источник
Назначение:
Прямой поиск бита
Команда:
BSR база, смещение
Назначение:
Обратный поиск бита
BSF сканирует источник (регистр или переменная), начиная с самого младшего бита, и записывает в приемник (регистр) номер первого встретившегося бита, равного 1. Команда BSR сканирует источник, начиная с самого старшего бита, и возвращает номер первого встретившегося ненулевого бита, считая от нуля, то есть, если источник равен 0100 0000 0000 0010b, то BSF возвратит 1 a BSR — 14.
Если весь источник равен нулю, значение приемника не определено и флаг ZF устанавливается в 1, иначе ZF всегда сбрасывается. Флаги CF, OF, SF, AF и PF не определены.
Команда:
SETcc приемник
Назначение:
Установка байта по условию
Это набор команд, которые устанавливают приемник (восьмибитный регистр или переменная размером в один байт) в 1 или 0, если удовлетворяется или не удовлетворяется определенное условие. Условием в каждом случае реально является состояние тех или иных флагов, но, если команда из набора SETcc используется сразу после СМР, условия приобретают формулировки, соответствующие отношениям между операндами СМР (см. табл. 6). Скажем, если операнды СМР были неравны, то команда SETNE, выполненная сразу после этого СМР, установит значение своего операнда в 1.
Команды передачи управления
Команда:
JMP операнд
Назначение:
Безусловный переход
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 метка
Назначение:
Условный переход
Это набор команд, каждая из которых выполняет переход (типа short или near), если удовлетворяется соответствующее условие. Условием в каждом случае реально является состояние тех или иных флагов, но, если команда из набора Jcc используется сразу после СМР, условия приобретают формулировки, соответствующие отношениям между операндами СМР (см. табл. 7). Например, если операнды СМР были равны, то команда JE, выполненная сразу после этого СМР, осуществит переход. Операнд для всех команд из набора Jcc — 8-битное или 32-битное смешение относительно текущей команды.
Команды Jcc не поддерживают дальних переходов, так что, если требуется выполнить условный переход на дальнюю метку, необходимо использовать команду из набора Jcc с обратным условием и дальний JMP, как, например:
cmp ах,0
jne local_1
jmp far_label ; переход, если АХ = 0
lосаl_1:
Команда:
JCXZ метка
Назначение:
Переход, если СХ = 0
Команда:
JECXZ метка
Назначение:
Переход, если EСХ = 0
Выполняет ближний переход на указанную метку, если регистр CX или ECX (для JCXZ и JECXZ соответственно) равен нулю. Так же как и команды из серии Jcc, JCXZ и JECXZ не могут выполнять дальних переходов. Проверка равенства СХ нулю, например, может потребоваться в начале цикла, организованного командой LOOPNE, — если в него войти с СХ = 0, то он будет выполнен 65 535 раз.
Команда:
LOOP метка
Назначение:
Цикл
Уменьшает регистр ЕСХ на 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 метка
Назначение:
Цикл, пока не ноль
Все эти команды уменьшают регистр ЕСХ на один, после чего выполняют переход типа 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 операнд
Назначение:
Вызов процедуры
Сохраняет текущий адрес в стеке и передает управление по адресу, указанному в операнде. Операндом может быть непосредственное значение адреса (метка в ассемблерных программах), регистр или переменная, содержащие адрес перехода. Если в качестве адреса перехода указано только смещение, считается, что адрес расположен в том же сегменте, что и команда CALL. При этом, так же как и в случае с JMP, выполняется ближний вызов процедуры. Процессор помещает значение регистра EIP (IP при 16-битной адресации), соответствующее следующей за CALL команде, в стек и загружает в EIP новое значение, осуществляя тем самым передачу управления. Если операнд CALL — регистр или переменная, то его значение рассматривается как абсолютное смещение, если операнд — метка в программе, то ассемблер указывает ее относительное смещение. Чтобы осуществить дальний CALL в реальном режиме, режиме V86 или в защищенном режиме при переходе в сегмент с теми же привилегиями, процессор помещает в стек значения регистров CS и EIP (IP при 16-битной адресации) и выполняет дальний переход аналогично команде JMP.
Команда:
RET число
RETN число
RETF число
Назначение:
Возврат из процедуры
RETN считывает из стека слово (или двойное слово, в зависимости от режима адресации) и загружает его в IP (или EIP), выполняя тем самым действия, обратные ближнему вызову процедуры командой, CALL. RETF соответственно загружает из стека IP (EIP) и CS, возвращаясь из дальней процедуры. Если в ассемблерной программе указана команда RET, ассемблер заменит ее на RETN или RETF в зависимости от того, как была описана процедура, которую эта команда завершает. Операнд для RET необязателен, но, если он присутствует, после считывания адреса возврата из стека будет удалено указанное количество байт — это бывает нужно, если при вызове процедуры ей передавались параметры через стек.
Команда:
INT число
Назначение:
Вызов прерывания
INT помещает в стек содержимое регистров EFLAGS, CS и EIP, после чего передает управление программе, называемой «обработчик прерывания» с указанным в качестве операнда номером (число от 0 до 0FFh), аналогично команде CALL. В реальном режиме адреса обработчиков прерываний считываются из таблицы, начинающейся в памяти по адресу 0000h:0000h. Адрес каждого обработчика занимает 4 байта, так что, например, адрес обработчика прерывания 10h находится в памяти по адресу 0000h:0040h. В защищенном режиме адреса обработчиков прерываний находятся в таблице IDT и обычно недоступны для прямого чтения или записи, так что для установки собственного обработчика программа должна обращаться к операционной системе. В DOS вызовы прерываний используются для выполнения большинства системных функций — работы с файлами, вводом/выводом и т.д. Например, следующий фрагмент кода завершает выполнение программы и возвращает управление DOS:
mov ax,4C01h
int 21h
Команда:
IRET
IRETD
Назначение:
Возврат из обработчика прерывания
Возврат управления из обработчика прерывания или исключения. IRЕТ загружает из стека значения IP, CS и FLAGS, a IRETD — EIP, CS и EFLAGS соответственно. Единственное отличие IRET от RETF состоит в том, что восстанавливается значение регистра флагов, из-за чего многим обработчикам прерываний приходится изменять значение EFLAGS, находящегося в стеке, чтобы, например, вернуть флаг CF установленным в случае ошибки.
Команда:
INT3
Назначение:
Вызов прерывания 3
Размер этой команды — один байт (код 0CCh), что делает ее удобной для отладки программ отладчиками, работающими в реальном режиме. Такие отладчики записывают этот байт вместо первого байта команды, перед которой требуется точка останова, и переопределяют адрес обработчика прерывания 3 на соответствующую процедуру внутри отладчика.
Команда:
INTO
Назначение:
Вызов прерывания 4 при переполнении
INTO — еще одна специальная форма команды INT. Эта команда вызывает обработчик прерывания 4, если флаг OF установлен в 1.
Команда:
BOUND индекс, границы
Назначение:
Проверка выхода за границы массива
BOUND проверяет, не выходит ли значение первого операнда (регистр), взятое как число со знаком, за границы, указанные во втором операнде (переменная). Границы — два слова или двойных слова (в зависимости от разрядности операндов), рассматриваемые как целые со знаком, расположенные в памяти подряд. Первая граница считается нижней, вторая — верхней. Если индекс меньше нижней границы или больше верхней, вызывается прерывание 5 (или исключение #BR), причем адрес возврата указывает не на следующую команду, а на BOUND, так что обработчик должен исправить значение индекса или границ, прежде чем выполнять команду IRET.
Команда:
ENTER размер, уровень
Назначение:
Вход в процедуру
Команда ENTER создает стековый кадр заданного размера и уровня вложенности (оба операнда — числа; уровень вложенности может принимать значения только от 0 до 31) для вызова процедуры, использующей динамическое распределение памяти в стеке для своих локальных переменных. Так, команда
enter 2048,3
помещает в стек указатели на стековый кадр текущей процедуры и той, из которой вызывалась текущая, создает стековый кадр размером 2 килобайта для вызываемой процедуры и помещает в ЕВР адрес начала кадра. Пусть процедура MAIN имеет уровень вложенности 0, процедура PROCA запускается из MAIN и имеет уровень вложенности 1, и PROCB запускается из PROCA с уровнем вложенности 2.
Строковые операции
Все команды для работы со строками считают, что строка-источник находится по адресу DS:SI (или DS:ESI), то есть в сегменте памяти, указанном в DS со смещением в SI, а строка-приемник — соответственно в ES:DI (или ES:EDI). Кроме того, все строковые команды работают только с одним элементом строки (байтом, словом или двойным словом) за один раз. Для того чтобы команда выполнялась над всей строкой, необходим один из префиксов повторения операций.
Префикс:
REP
Назначение:
Повторять
Префикс:
REPE
Назначение:
Повторять, пока равно
Префикс:
REPNE
Назначение:
Повторять, пока не равно
Префикс:
REPZ
Назначение:
Повторять, пока ноль
Префикс:
REPNZ
Назначение:
Повторять, пока не ноль
Все эти команды — префиксы для операций над строками. Любой из префиксов выполняет следующую за ним команду строковой обработки столько раз, сколько указано в регистре ЕСХ (или СХ, в зависимости от разрядности адреса), уменьшая его при каждом выполнении команды на 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
Назначение:
Копирование строки двойных слов
Копирует один байт (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
Назначение:
Сравнение строк двойных слов
Сравнивает один байт (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
Назначение:
Сканирование строки двойных слов
Сравнивает содержимое регистра 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
Назначение:
Чтение двойного слова из строки
Копирует один байт (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
Назначение:
Запись двойного слова в строку
Копирует регистр 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
Назначение:
Чтение строки двойных слов из порта
Считывает из порта ввода-вывода, номер которого указан в регистре 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
Назначение:
Запись строки двойных слов в порт
Записывает в порт ввода-вывода, номер которого указан в регистре 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, так что, если порт не успевал обрабатывать информацию с той скоростью, с которой ее поставляла эта команда, часть данных терялась.
Управление флагами
Команда:
STC
Назначение:
Установить флаг переноса
Устанавливает флаг CF в 1.
Команда:
CLC
Назначение:
Сбросить флаг переноса
Процессор:
8086
Сбрасывает флаг CF в 0.
Команда:
CMC
Назначение:
Инвертировать флаг переноса
Процессор:
8086
Инвертирует флаг СF.
Команда:
STD
Назначение:
Установить флаг направления
Устанавливает флаг DF в 1, так что при последующих строковых операциях регистры DI и SI будут уменьшаться.
Команда:
CLD
Назначение:
Сбросить флаг направления
Сбрасывает флаг DF в 0, так что при последующих строковых операциях регистры DI и SI будут увеличиваться.
Команда:
LAHF
Назначение:
Загрузить флаги состояния в АН
Копирует младший байт регистра FLAGS в АН, включая флаги SF (бит 7), ZF (бит 6), AF (бит 4), PF (бит 2) и CF (бит 0). Бит 1 устанавливается в 1, биты 3 и 5 — в 0.
Команда:
SAHF
Назначение:
Загрузить флаги состояния из АН
Загружает флаги SF, ZF, AF, PF и CF из регистра АН значениями бит 7, 6, 4, 2 и 0 соответственно. Зарезервированные биты 1, 3 и 5 регистра флагов не изменяются.
Команда:
PUSHF
Назначение:
Поместить FLAGS в стек
Команда:
PUSHFD
Назначение:
Поместить ЕFLAGS в стек
Эти команды копируют содержание регистра FLAGS или EFLAGS в стек (уменьшая SP или ESP на 2 или 4 соответственно). При копировании регистра EFLAGS флаги VM и RF (биты 16 и 17) не копируются, соответствующие биты в двойном слове, помещенном в стек, обнуляются.
Команда:
POPF
Назначение:
Загрузить FLAGS из стека
Процессор:
8086
Команда:
POPFD
Назначение:
Загрузить EFLAGS из стека
Считывает из вершины стека слово (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
Назначение:
Запретить прерывания
Сбрасывает флаг IF в 0. После выполнения этой команды процессор игнорирует все прерывания от внешних устройств (кроме NMI). В защищенном режиме эта команда, так же как и все другие команды, модифицирующие флаг IF (POPF или IRET), выполняется, только если программе даны соответствующие привилегии (CPL < IOPL).
Команда:
STI
Назначение:
Разрешить прерывания
Устанавливает флаг IF в 1, отменяя тем самым действие команды CLI.
Команда:
SALC
Назначение:
Установить AL в соответствии с CF
Устанавливает AL в 0FFh, если флаг CF = 1, и сбрасывает в 00h, если CF = 0. Это недокументированная команда с кодом 0D6h, присутствующая во всех процессорах Intel и совместимых с ними (начиная с 8086). В документации на Pentium Pro эта команда упоминается в общем списке команд, но ее действие не описывается. Действие SALC аналогично SBB AL,AL, но SALC не изменяет значений флагов.
Загрузка сегментных регистров
Команда:
LDS приемник, источник
Назначение:
Загрузить адрес, используя DS
Команда:
LES приемник, источник
Назначение:
Загрузить адрес, используя ES
Команда:
LFS приемник, источник
Назначение:
Загрузить адрес, используя FS
Команда:
LGS приемник, источник
Назначение:
Загрузить адрес, используя GS
Команда:
LSS приемник, источник
Назначение:
Загрузить адрес, используя SS
Второй операнд (источник) для всех этих команд — переменная в памяти размером в 32 или 48 бит (в зависимости от разрядности операндов). Первые 16 бит из этой переменной загружаются в соответствующий сегментный регистр (DS для LDS, ES для LES и т.д.), а следующие 16 или 32 — в регистр общего назначения, указанный в качестве первого операнда. В защищенном режиме значение, загружаемое в сегментный регистр, всегда должно быть правильным селектором сегмента (в реальном режиме любое число может использоваться как селектор).
Другие команды
Команда:
NOP
Назначение:
Отсутствие операции
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
Назначение:
Префикс блокировки шины данных
На все время выполнения команды, снабженной таким префиксом, будет заблокирована шина данных, и если в системе присутствует другой процессор, он не сможет обращаться к памяти, пока не закончится выполнение команды с префиксом LOCK. Команда XCHG автоматически всегда выполняется с блокировкой доступа к памяти, даже если префикс LOCK не указан. Этот префикс можно использовать только с командами ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD и XCHG.
Команда:
UD2
Назначение:
Неопределенная операция
Эта команда всегда вызывает ошибку «неопределенная операция» (исключение #UD). Впервые она описана как таковая для Pentium Pro, но во всех предыдущих процессорах эта команда (код 0Fh 0Bh) не была определена и, естественно, приводила к такой же ошибке. UD2 предназначена для тестирования программного обеспечения, в частности операционных систем, которые должны уметь корректно обрабатывать такую ошибку. Название команды происходит от команды UD (код 0Fh 0FFh), которая была определена AMD для процессоров AMD K5.
Команда:
CPUID
Назначение:
Идентификация процессора
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-байтную строку — идентификатор производителя
Псевдокоманды определения переменных
Псевдокоманда — это директива ассемблера, которая приводит к включению данных или кода в программу, хотя сама она никакой команде процессора не соответствует. Псевдокоманды определения переменных указывают ассемблеру, что в соответствующем месте программы располагается переменная, определяют тип переменной (байт, слово, вещественное число и т.д.), задают ее начальное значение и ставят в соответствие переменной метку, которая будет использоваться для обращения к этим данным. Псевдокоманды определения данных записываются в общем виде следующим образом:
имя_переменной 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.
Структуры
Директива 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.
Сегменты
Каждая программа, написанная на любом языке программирования, состоит из одного или нескольких сегментов. Обычно область памяти, в которой находятся команды, называют сегментом кода, область памяти с данными — сегментом данных и область памяти, отведенную под стек, — сегментом стека. Разумеется, ассемблер позволяет изменять устройство программы как угодно — помещать данные в сегмент кода, разносить код на множество сегментов, помещать стек в один сегмент с данными или вообще использовать один сегмент для всего.
Сегмент программы описывается директивами 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 для данного регистра. Эта директива не изменяет значений сегментных регистров, а только позволяет ассемблеру проверять допустимость ссылок и самостоятельно вставлять при необходимости префиксы переопределения сегментов, если они необходимы.
Перечисленные директивы удобны для создания больших программ на ассемблере, состоящих из разнообразных модулей и содержащих множество сегментов. В повседневном программировании обычно используется ограниченный набор простых вариантов организации программы, известных как модели памяти.
Директивы управления программным счетчиком
Программный счетчик — внутренняя переменная ассемблера, равная смещению текущей команды или данных относительно начала сегмента. Для преобразования меток в адреса используется именно значение этого счетчика. Значением счетчика можно управлять с помощью следующих директив.
org выражение
Устанавливает значение программного счетчика. Директива ORG с операндом 100h обязательно используется при написании файлов типа COM, которые загружаются в память после блока параметров размером 100h.
even
Директива EVEN делает текущее значение счетчика кратным двум, вставляя команду NOP, если оно было нечетным. Это увеличивает скорость работы программы, так как для доступа к слову, начинающемуся с нечетного адреса, процессор должен считать два слова из памяти. Если при описании сегмента не использовалось выравнивание типа BYTE, счетчик в начале сегмента всегда четный.
align значение
Округляет значение программного счетчика до кратного указанному значению. Оно может быть любым четным числом. Если счетчик некратен указанному числу, эта директива вставляет необходимое количество команд NOP.
Структуры 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:
Структуры 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 окажется еще и значительно меньше.
Конечные автоматы
Конечный автомат — процедура, которая помнит свое состояние и при обращении к ней выполняет различные действия для разных состояний. Например, рассмотрим процедуру, которая складывает регистры АХ и ВХ при первом вызове, вычитает при втором, умножает при третьем, делит при четвертом, снова складывает при пятом и т.д. Очевидная реализация, опять же, состоит в последовательности условных переходов:
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, использование косвенного перехода приводит к тому, что не требуется никаких проверок и время выполнения управляющей структуры остается одним и тем же для четырех или четырех тысяч состояний.
Передача параметров
Процедуры могут получать или не получать параметры из вызывающей процедуры и могут возвращать или не возвращать результаты (процедуры, которые что-либо возвращают, называются функциями в языке Pascal, но ассемблер не делает каких-либо различий между ними).
Параметры можно передавать с помощью одного из шести механизмов:
по значению;
по ссылке;
по возвращаемому значению;
по результату;
по имени;
отложенным вычислением.
Параметры можно передавать в одном из пяти мест:
в регистрах;
в глобальных переменных;
в стеке;
в потоке кода;
в блоке параметров.
Так что всего в ассемблере возможно 30 различных способов передачи параметров для процедур. Рассмотрим их по порядку.
Передача параметров по значению
Процедуре передается собственно значение параметра. При этом фактически значение параметра копируется, и процедура использует его копию, так что модификация исходного параметра оказывается невозможной. Этот механизм применяется для передачи небольших параметров, таких как байты или слова.
Например, если параметры передаются в регистрах:
mov ax,word ptr 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
Передача параметров по результату
Этот механизм отличается от предыдущего только тем, что при вызове процедуры предыдущее значение параметра никак не определяется, а переданный адрес используется только для записи в него результата.
Передача параметров по имени
Это механизм, который используют макроопределения, директива EQU, а также, например, препроцессор С при обработке команды #define. При реализации этого механизма в компилирующем языке программирования (к которому относится и ассемблер) приходится заменять передачу параметра по имени другими механизмами при помощи, в частности, макроопределений.
Если определено макроопределение
pass_by_name macro parameter1
mov ax,parameter1
endm
то теперь в программе можно передавать параметр так:
pass_by_name value
call procedure
Примерно так же поступают языки программирования высокого уровня, поддерживающие этот механизм: процедура получает адрес специальной функции-заглушки, которая вычисляет адрес передаваемого по имени параметра.
Передача параметров отложенным вычислением
Как и в предыдущем случае, здесь процедура получает адрес функции, вычисляющей значение параметра. Такой механизм удобен, если вычисление значения параметра требует много ресурсов или времени, например, если функция должна выбрать один из нескольких ходов при игре в шахматы, вычисление каждого параметра может занимать несколько минут. При передаче параметров отложенным вычислением функция получает адрес заглушки, которая при первом обращении к ней вычисляет значение параметра и сохраняет его во внутренней локальной переменной, а при дальнейших вызовах возвращает ранее вычисленное значение. Если процедуре вообще не потребуются значения части параметров (например, если первый же ход приводит к мату), то использование отложенных вычислений способствует значительному выигрышу. Этот механизм чаще всего применяется в системах искусственного интеллекта и операционных системах.
Рассказав об основных механизмах того, как передавать параметры процедуре, рассмотрим применяемые в ассемблере варианты, где их передавать.
Передача параметров в регистрах
Если процедура получает небольшое число параметров, идеальным местом для их передачи оказываются регистры. Примерами использования этого метода могут служить практически все вызовы прерываний 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], определяет число остальных параметров). Но подробнее о тонкостях передачи параметров в стеке рассказано далее, а здесь приведен обзор методов.
Передача параметров в потоке кода
В этом необычном методе передаваемые процедуре данные размещаются прямо в коде программы, сразу после команды 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, например поиск файла, использующий блок параметров DTA, или загрузка (и исполнение) программы, использующая блок параметров ЕРВ.
Локальные переменные
Часто процедурам требуются локальные переменные, которые не будут нужны после того, как процедура закончится. По аналогии с методами передачи параметров можно говорить о локальных переменных в регистрах — каждый регистр, который сохраняют при входе в процедуру и восстанавливают при выходе, фактически играет роль локальной переменной. Единственный недостаток регистров в роли локальных переменных — их слишком мало. Следующий вариант — хранение локальных данных в переменной в сегменте данных — удобен и быстр для большинства несложных ассемблерных программ, но процедуру, использующую этот метод, нельзя вызывать рекурсивно: такая переменная на самом деле является глобальной и находится в одном и том же месте в памяти для каждого вызова процедуры. Третий и наиболее распространенный способ хранения локальных переменных в процедуре — стек. Принято располагать локальные переменные в стеке сразу после сохраненного значения регистра ВР, так что на них можно ссылаться изнутри процедуры, как [ВР-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).
Последовательности команд, используемые в начале и в конце таких процедур, оказались настолько часто применяемыми, что в процессоре 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
Область в стеке, отводимая для локальных переменных вместе с активационной записью, называется стековым кадром.
Сортировки
Еще одна часто встречающаяся задача при программировании — сортировка данных. Все существующие алгоритмы сортировки можно разделить на сортировки перестановкой, в которых на каждом шаге алгоритма меняется местами пара чисел; сортировки выбором, в которых на каждом шаге выбирается наименьший элемент и дописывается в отсортированный массив; и сортировки вставлением, в которых элементы массива рассматривают последовательно и каждый вставляют на подходящее место в отсортированном массиве. Самая простая сортировка перестановкой — пузырьковая, в которой более легкие элементы «всплывают» к началу массива. Сначала второй элемент сравнивается с первым и, если нужно, меняется с ним местами. Затем третий элемент сравнивается со вторым и только в том случае, когда они переставляются, сравнивается с первым, и т.д. Этот алгоритм также является и самой медленной сортировкой — в худшем случае для сортировки массива 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
Обработчики прерываний
Когда в реальном режиме выполняется команда 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 отсутствует.
Обработчики прерываний
Когда в реальном режиме выполняется команда 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 отсутствует.
Прерывания от внешних устройств
Прерывания от внешних устройств, или аппаратные прерывания — это то, что понимается под термином «прерывание». Внешние устройства (клавиатура, дисковод, таймер, звуковая карта и т.д.) подают сигнал, по которому процессор прерывает выполнение программы и передает управление на обработчик прерывания. Всего на персональных компьютерах используется 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 секунды, то есть половину времени между прерываниями, вся система будет работать в два раза медленнее. А если еще одна программа с таким же долгим обработчиком перехватит это прерывание, система остановится совсем. Именно поэтому обработчики прерываний принято писать исключительно на ассемблере.
Последовательный порт
Каждый из последовательных портов обменивается данными с процессором через набор портов ввода-вывода: СОМ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, это — старший байт делителя частоты порта. Значение скорости порта определяется по значению делителя частоты
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
Таймер
Все, что нам было известно до сих пор о системном таймере, — это устройство, вызывающее прерывание 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 представляет собой идеальный вариант такого начального числа для большинства приложений.
Адресация в защищенном режиме
Прежде чем познакомиться с программированием в защищенном режиме, рассмотрим механизм адресации, применяющийся в нем. Так же как и в реальном режиме, адрес складывается из адреса начала сегмента и относительного смещения, но если в реальном режиме адрес начала сегмента просто лежал в соответствующем сегментном регистре, деленый на 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 страничного механизма виртуализации памяти.
|
Ольга | Отличная статья...спасибо...много полезного нашла))) 2008-06-05 19:13:21 | Doom666 | Да....Хорошая статья.. 2010-10-14 02:59:48 | |
|