Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
 iakovlev.org 
 Languages
 С
 GNU С Library 
 Qt 
 STL 
 Threads 
 C++ 
 Samples 
 stanford.edu 
 ANSI C
 Libs
 LD
 Socket
 Pusher
 Pipes
 Encryption
 Plugin
 Inter-Process
 Errors
 Deep C Secrets
 C + UNIX
 Linked Lists / Trees
 Asm
 Perl
 Python
 Shell
 Erlang
 Go
 Rust
 Алгоритмы
NEWS
Последние статьи :
  Тренажёр 16.01   
  Эльбрус 05.12   
  Алгоритмы 12.04   
  Rust 07.11   
  Go 25.12   
  EXT4 10.11   
  FS benchmark 15.09   
  Сетунь 23.07   
  Trees 25.06   
  Apache 03.02   
 
TOP 20
 Secure Programming for Li...6507 
 Linux Kernel 2.6...5279 
 Trees...1118 
 Максвелл 3...1050 
 William Gropp...987 
 Go Web ...961 
 Ethreal 3...929 
 Ethreal 4...915 
 Gary V.Vaughan-> Libtool...913 
 Ext4 FS...901 
 Clickhouse...900 
 Rodriguez 6...899 
 Ethreal 1...897 
 Steve Pate 1...884 
 C++ Patterns 3...860 
 Assembler...854 
 Ulrich Drepper...844 
 DevFS...786 
 MySQL & PosgreSQL...771 
 Стивенс 9...757 
 
  01.01.2024 : 3621733 посещений 

iakovlev.org
ДОС-вариант

Процессоры 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