1
5. Методы адресации. Арифметические команды. Стек.
5.1. Методы адресации: прямой, непосредственный, регистровый.
Мы составили и выполнили с помощью отладчика простую программу из
четырех команд:
mov ax,[200h]
mov bx,2
add ax,bx
nop
На примере этой программы введем некоторые термины. Здесь mov, add и
nop — операции: скопировать, сложить и ничего не делать (только увеличить
счетчик команд). ax, bx, 2, [200h] — операнды, то есть то, над чем вы-
полняются операции. Методы адресации — способы, с помощью которых в
команде указываются обрабатываемые данные. В программе использованы три
метода:
регистровый (указано символическое имя регистра, например BX);
непосредственный (указана константа 2);
прямой (указан адрес, точнее, смещение ячейки памяти 200h).
При непосредственной адресации константа помещается в код команды.
Команде не нужно тратить время на обращение к памяти для выборки констан-
ты. А в первой команде операнд хранится в ячейке памяти, что замедлит вы-
полнение команды.
Заметим, что непосредственный операнд нельзя изменить в процессе вы-
полнения команды. Например, недопустима команда add 24,ax.
Теперь остановимся подробнее на прямом методе адресации. Сразу возни-
кает вопрос: адрес — это сегмент:смещение, но в примере программы указано
только смещение. Откуда берется сегментная часть адреса? Ответ: подразумева-
ется, что сегментная часть адреса находится в сегментном регистре DS. А если
нужен другой сегментный адрес или сегментная часть адреса хранится, скажем,
в регистре ES? Ответ на этот вопрос мы узнаем позднее.
И еще вопрос: в нашем примере 200h может быть адресом и слова и байта;
как ассемблер узнает, с чем работает команда — со словами или с байтами? В
приведенной команде операнд-приемник — 16-разрядный регистр AX. Поэтому
операнд-источник также 16-разрядный, т.е. в нашем примере 200h — адрес сло-
ва. Размер приемника и источника одинаков. Но вот другой пример: введем ко-
манду mov [600],3. В панели кода мы увидим mov byte ptr [0600],03. Мини-
ассемблер, встроенный в td, за нас «решил», что 0600 — это адрес байта. Опе-
ранд-байт снабжается атрибутным оператором byte ptr (ptr — PoinTeR —
указатель). Если же мы хотим записать число 3 именно в слово по адресу DS:
0600, то надо указать в команде другой атрибутный оператор: mov word ptr
[0600],3.
Подчеркнем, почему это важно. Рассмотрим пример. Пусть [0600] = FDEC.
Тогда результат выполнения команды mov word ptr [0600],3 есть
[0600]=0003, а для команды mov byte ptr [600],3 результат [600]=FD03.
В первом случае изменяется только младший байт слова, во втором — всѐ сло-
во!
Из методов адресации нам осталось изучить только один метод — косвен-
ный. Но это мы сделаем позже.
2
5.2. Описание системы команд.
Нам предстоит изучить несколько десятков команд центрального процессо-
ра. Поэтому нужно сразу договориться о единообразной форме изложения.
Каждую новую команду мы будем описывать в виде таблицы такого при-
мерно вида:
Название команды мнемоника действие
английское название (и его перевод) изменение флагов
Сочетание операндов
Поясним термин "мнемоника". Он происходит от греческого μνημονικον —
искусство запоминания. Мнемоника команды, т.е. ее сокращенное название,
призвана напомнить программисту действие команды. Именно поэтому мы
приводим и английское название команды, выделяя заглавными те буквы, кото-
рые образуют мнемонику. Специальная программа — Ассемблер ("сборщик")
преобразует эту мнемонику в код, состоящий из нулей и единиц. При выполне-
нии программы процессор дешифрует этот код и исполнит предписываемую
последовательность действий.
Вслед за мнемоникой команды следуют сокращенные обозначения операн-
дов. Условно команды (для процессора 8086) можно разбить на двухоперанд-
ные, однооперандные и не имеющие операндов. В двухоперандных командах,
как правило, можно выделить один операнд, который в результате выполнения
команды претерпевает изменения — его мы будем называть приемником (desti-
nation, сокращенно dst), и операнд, остающийся неизменным — его назовем
источником (source, сокращенно src). Например, в команде add ax,bx ре-
гистр AX является приемником, а BX — источником. Иногда операнды нельзя
разграничить указанным образом (оба операнда изменяются или остаются
неизменными). Тогда используем обозначение opr.
Заметим, что операнд может и не присутствовать явно в мнемонике коман-
ды, а лишь подразумеваться.
Еще ограничения на операнды:
операндом не может быть счетчик команд IP,
в двухоперандных командах недопустимо, чтобы оба операнда выбирались
из памяти.
Имеются также очень сильные ограничения на использование в качестве
операндов сегментных регистров. С этими ограничениями мы познакомимся
позднее.
Описание действия команды, как правило, очень кратко и схематично и не-
формально раскрывается в тексте, идущем за таблицей. Особо отмечается, как
команда действует на регистр флагов.
В разделе «Сочетание операндов» перечислены возможные типы операндов.
Здесь используются обозначения: r8 — байтовый регистр (например, AL, CH),
r16 — регистр-слово общего назначения (SI, BX, …), r/m8 — регистр или ячейка
памяти размером в байт, r/m16 — регистр или ячейка памяти размером в слово,
i8 и i16 — непосредственные 8- и 16-разрядные операнды. Первые буквы этих
обозначений взяты от английских слов register, memory, immediate.
Как правило, мы даем неформальные описания команд. Полное описание
приведено, например, в справочнике [Юров В. Assembler: Специальный спра-
вочник. — СПб: "Питер", 2004 — 412 с.].
3
5.3. Команды пересылки данных.
Переместить mov dst,src dst ← src
(MOVe data — перемещение данных) флаги не изменяются
mov r/m8,r8; r/m16,r16; r8,r/m8; r16,r/m16; r/m8,i8; r/m16,i16
Команда копирует содержимое источника (или сам источник, если это
непосредственный операнд) в приемник. При этом источник не изменяется.
Примеры мы уже видели. Возможны различные комбинации операндов: ре-
гистр-регистр, регистр-память, память-регистр и т.д. Заметим, что нельзя копи-
ровать командой mov содержимое ячейки памяти в другую ячейку, т.е. недопу-
стима команда mov word ptr [200],[300]. Для этого придется воспользоваться
каким-нибудь промежуточным регистром:
mov ax,[300]
mov [200],ax
Имеются также ограничения на операции пересылки с сегментными реги-
страми. Но о них поговорим в дальнейшем отдельно.
Обменять xchg opr1,opr2 opr1 ↔ opr2
to eXCHanGe флаги не изменяются
xchg r/m8,r8; r/m16,r16; r8,r/m8; r6,r/m16
Задача. Обменять значениями BX и слово по адресу 320, 1) используя толь-
ко команду mov; 2) используя команду xchg.
Упражнение. Ассемблировать и дисассемблировать команду xchg ax,ax.
Что получиться?
Задача. Написать и отладить программу, которая меняет порядок содержи-
мого четырех байт с адресами 400, ..., 403.
5.4. Арифметические команды. Сложение и вычитание.
Сложить add dst,src dst dst + src
(integer ADD — сложение целых) изменяются все флаги состояния
add r/m8,r8; r/m16,r16; r8,r/m8; r6,r/m16; r/m8,i8; r/m16,i16
Упражнение. Выполнить в TD программу
mov ax,0
add al,40h
add al,40h
add al,40h
add al,40h
add al,40h
по шагам, на каждом шаге записывать содержимое регистра AL и регистра фла-
гов. Интерпретировать флаги.
Вычесть sub dst,src dst dst – src
(to SUBtract) изменяются все флаги состояния
sub r/m8,r8; r/m16,r16; r8,r/m8; r6,r/m16; r/m8,i8; r/m16,i16
Напомним, что если при вычитании понадобился заем за пределами разряд-
ной сетки, то устанавливается флаг CF.
4
Упражнение. Выполнить программу по шагам, на каждом шаге записывать
содержимое AX и регистра флагов. Интерпретировать флаги.
mov ax,1
mov bx,ax
sub ax,bx
sub ax,bx
sub ax,bx
sub ax,bx
Пример. Вычислить z = x + y – 5, где x, y, z — слова, расположенные по ад-
ресам 200, 202, 204 соответственно.
mov ax,[200] ; Поместить x в аккумулятор
add ax,[202] ; Прибавить y
sub ax,5 ; Вычесть 5
mov [204],ax ; Поместить содержимое аккумулятора в z
Упражнение. Исполните эту программу с помощью TD. Предварительно
занесите в x, y, z данные: –2, 1, 0. Коды отрицательных чисел проверяйте с по-
мощью встроенного калькулятора (Ctrl+F4).
Заметим, что команды add и sub выполняют действия с целыми числами
независимо от их интерпретации — знаковые они или беззнаковые. Этой ин-
терпретацией занимается программист по состоянию флагов, которые устанав-
ливаются, как говорится, на все случаи жизни. Для беззнаковых чисел програм-
мист анализирует CF, для знаковых — OF и SF. Какими средствами выполняет-
ся такой анализ — узнаем чуть позже.
5.5. Длинные целые.
Так как диапазона целых для одного слова (–32768 ... +32767) может не хва-
тить, используют представление целых чисел, занимающее несколько слов. Рас-
смотрим случай, когда для целого отводится два слова.
Как организовать в программе сложение таких чисел? Первое, что приходит
в голову: отдельно сложить младшие и старшие слова слагаемых. Но что делать,
если при сложении младших слов возник перенос единицы за пределы слова?
Этот перенос попадает в регистр флагов — в CF. Его нужно прибавить к сумме
старших слов.
старшие слова младшие слова
+ +
+
CF
Операция сложения старших слов и значения CF выполняются командой
Сложить с переносом adс dst,src dst dst + src + CF
to ADd with Carry изменяются все флаги состояния
adc r/m8,r8; r/m16,r16; r8,r/m8; r6,r/m16; r/m8,i8; r/m16,i16
5
Имеется аналогичная команда для вычитания
Вычесть с заемом sbb dst,src dst dst – src – CF
to SuBtract with Borrow изменяются все флаги состояния
sbb r/m8,r8; r/m16,r16; r8,r/m8; r6,r/m16; r/m8,i8; r/m16,i16
Пример. Вычислить z = x + y – 5, где x, y, z — двойные слова, расположен-
ные по адресам 200, 204, 208. (Как всегда, более значимое расположено по бо-
лее старшему адресу, поэтому для x, например, адрес старшего слова — 202, а
младшего — 200.)
mov ax,[200] ; Поместить младшее слово x в аккумулятор
mov dx,[202] ; Поместить старшее слово x в DX
add ax,[204] ; Прибавить младшее слово y
adc dx,[206] ; Прибавить старшее слово y с возможным
; переносом
sub ax,5 ; Из младшего слова результата вычесть 5
sbb dx,0 ; Из старшего слова результата вычесть 0
; с возможным заемом
mov [208],ax ; Поместить младшее слово в z
mov [20A],dx ; Поместить старшее слово в z
Несколько раз прогоните эту программу в отладчике по шагам с различны-
ми исходными данными, чтобы увидеть эффект сложения и вычитания содер-
жимого CF. Например, 1) x = 2FFFEh, y = 3h; 2) x = 2h, y = 30003h.
Разумеется, с использованием введенных команд можно реализовать сло-
жение и вычитание целых длиной 3 слова, 4 слова и т.д.
Задача. Напишите программу сложения целых чисел длиной три слова.
Задача*([The Assembly Gems Pages. [Link] Посред-
ством одной команды заполнить все биты регистра AX значением флага CF
(иными словами, если CF = 1, то AX = FFFF, иначе AX = 0).
Решение. sbb ax,ax
When you subtract AX from AX a zero will be the return, however if the carry flag
(CF=1) is set the instruction will use the extra bit to borrow from and this will make
the selected register contain FFFFh. Note that the Carry Flag will not be modified.
5.6. Операнды различного размера
Для выполнения операций над операндами, имеющими различную длину
(например, 16 и 8 бит), нужно уметь выравнивать размер операнда. Естественно,
нужно увеличить размер операнда, имеющего меньший размер.
Если требуется увеличить размер беззнакового операнда, то достаточно в
старший байт (старшее слово) записать нуль.
Задача. Прибавить беззнаковое число, хранящееся в BH, к длинному цело-
му, расположенному по адресу DS:30C.
Если операнд — знаковый, то для увеличения размера операнда предназна-
чены специальные команды.
Преобразовать байт в слово cbw расширяет знак AL в AH
Convert Byte to Word флаги не изменяются
операнд подразумевается
6
Преобразовать слово в двойное слово cwd расширяет знак AX в DX
Convert Word to Doubleword флаги не изменяются
операнд подразумевается
Обратите внимание, команды однооперандные, но явно операнд в синтак-
сисе команды не присутствует. Подразумевается, что он находится в аккумуля-
торе. Отсутствие явного операнда сокращает размер кода команды.
Пример. Пусть AL = B4 = 10110100. После cwb получаем AX = FFB4 =
1111111110110100. Знаковый бит размножился. Число осталось прежним (из-
менилось его представление). Пусть теперь AL = 6A = 01101010. После cwb
получаем AX = 006A.
Задача. Прибавить знаковое число, хранящееся в BH, к длинному целому по
адресу DS:30C.
5.7. Дополнительные арифметические команды.
Увеличить на 1 inc dst dst dst + 1
INCrement by 1 меняет все флаги состояния кроме CF
inc r/m8; r/m16
Уменьшить на 1 dec dst dst dst - 1
DECrement by 1 меняет все флаги состояния кроме CF
inc r/m8; r/m16
Эти команды применяют для организации счетчиков. Конечно, для этой це-
ли можно было бы воспользоваться командами add dst,1 и sub dst,1. Но,
во-первых, коды команд inc и dec короче (убедитесь в этом!), во-вторых,
странное на первый взгляд решение — не изменять флаг CF — в ряде случаев
оказывается важным. Пусть, например, мы осуществляем сложение длинных
целых переменной длины. В процессе сложения мы подсчитываем количество
сложенных слов (с помощью inc), но для корректного сложения должны сохра-
нять значение флага CF (ведь все слова, следующие за младшим, мы складыва-
ем, применяя команду adc).
Изменить знак neg dst dst ← – dst
two's complement NEGation меняет все флаги состояния
neg r/m8; r/m16
Эта команда вычитает знаковое целое из нуля.
Задача. Пусть dst — байт. При каком значении байта команда neg устано-
вит флаг OF?
Замечание*. Как выполнить операцию neg над длинным целым, располо-
женным в DX:AX? Имеется остроумное решение [The Assembly Gems Pages.
[Link] В нем используется команда not dst. Она относится
к группе команд для битовых операций, которую мы будем изучать позже. Эта
команда инвертирует все биты операнда. Например, если AL = 56h, то после
выполнения not al содержимое AL равно C9h. (Распишите двоичные представ-
ления этих чисел.). В частности, вместо команды neg dst можно было использо-
вать команды not dst / add dst,1. Разница в том, что neg dst всегда устанавливает
7
флаг CF, если операнд отличен от нуля. А теперь обещанный программный
фрагмент:
not dx
neg ax
sbb dx,-1
Самостоятельно проанализируйте, почему он приводит к правильному ре-
зультату.
It's basic approach is to compute the 2's complement by first computing the 1's
complement and than adding 1, which requires carry propagation all the way to the
most significant part. We get a slight break here in that we can use the NEG instruction
for the least significant part. Remember that NEG sets the carry if the source is not 0.
5.8. Обнуление 16-разрядного регистра.
Фирма Intel не стала вводить в состав команд специальную команду для об-
нуления приемника. Для этой цели можно использовать команду mov dst, 0. Но
если dst — это 16-разрядный регистр, то для его обнуления рекомендуется ко-
манда xor r16,r16. Поясним операцию xor dx,dx. Команда xor dst,src от-
носится к битовым операциям, которые мы будем изучать существенно позже.
Она выполняет операцию «исключающее ИЛИ» над битами приемника и источ-
ника: dsti dsti srci . По таблице 0.2 мы видим, что b b 0 при любом зна-
чении b. Код команды xor ax,ax короче, чем mov ax,0 (убедитесь в этом
самостоятельно в отладчике). В то же время коды команд mov r8,0 и xor
r8,r8 имеют одинаковую длину, поэтому для обнуления 8-разрядного регистра
лучше использовать команду mov.
5.9. Умножение и деление.
Команды сложения и вычитания работали одинаково как со знаковыми, так
и с беззнаковыми операндами. Умножение и деление знаковых и беззнаковых
операндов выполняется разными командами. Начнем с умножения.
Умножить знаковые целые imul src для байтов AX AL*src
для слов DX:AX AX*src
signed Integer MULtiply изменение флагов — см. ниже
imul r/m8; r/m16
Умножить беззнаковые целые mul src для байтов AX AL*src
для слов DX:AX AX*src
unsigned Integer MULtiply изменение флагов — см. ниже
mul r/m8; r/m16
Поясним операции картинками:
для байтов: для слов:
AL AX
src src
AH AL DX AX
8
Благодаря удвоенному размеру результата переполнение никогда не возни-
кает! Но в ряде случаев важно знать, можно ли разместить произведение в ячей-
ке такого же размера, что и сомножители. Это можно выяснить, анализируя
флаги. Если результат остается в младшем байте (слове), то CF = OF = 0. В про-
тивном случае CF = OF = 1. При этом надо иметь в виду, что AH (DX) все равно
изменяется, если результат отрицательный. Например, при перемножении не-
больших отрицательных слов в результате получится DX = 0FFFFh, но процес-
сор "понимает", что это — расширение знакового разряда, а не выход за преде-
лы младшего слова.
Замечание. Может вызвать удивление тот факт, что потребовались две ко-
манды умножения — для знаковых и беззнаковых чисел. Ведь вычеты по моду-
лю n образуют кольцо, а его знаковое и беззнаковое представление изоморфны.
Поэтому нам нужна была только одна команда сложения и, стало быть, нужна
только одна команда умножения. Объяснение этому противоречию следующее:
результат умножения оказывается в кольце вычетов по модулю n 2 . Команды
mul и imul осуществляют отображение Zn Zn Zn2 , где n 2k . Выполните в
TD программный фрагмент:
mov ax,-2 ; дизассемблируется как mov ax,FFFE
mov bx,3
imul bx
mov ax,0FFFE
mul bx
Выполним первые три команды. Получим результат FFFF FFFA = –6. При
этом CF = OF = 0 (результат остался в слове AX; в DX расширение знака). После
этого мы перемножили те же слова fffe и 3 как беззнаковые. На этот раз произ-
ведение равно 2fffa = 196602 и в слове оно не уместилось (CF = OF = 1). Но
младшее слово результата в обоих случаях одинаковое: fffa. В кольце вычетов
Zn достаточно одной операции умножения. (Это замечание пригодится нам, ко-
гда мы будем анализировать результаты работы компилятора языка Си.)
Теперь рассмотрим обратную операцию — деление. Здесь также имеется
два варианта — знаковый и беззнаковый.
Делить знаковые idiv src для байтов AL ← AX/src, AH ← остаток
целые для слов AX ← DX:AX/src, DX ← остаток
signed Integer DIVide значения флагов не определены
idiv r/m8; r/m16
Делить беззнаковые div src для байтов AL ← AX/src, AH ← остаток
целые для слов AX ← DX:AX/src, DX ← остаток
unsigned Integer DIVide значения флагов не определены
div r/m8; r/m16
Запомнить, в каких регистрах расположены делимое, частное и остаток по-
может мнемоническая схема ―деления уголком‖. Легко заметить, что AH:AL
(DX:AX) как бы сдвигается вниз и вправо.
делимое делитель AX src DX:AX src
9
остаток частное AH AL DX AX
Делитель src не может быть непосредственным операндом (т.е. команда
idiv 3 невозможна, предварительно нужно загрузить 3 в какой-нибудь ре-
гистр, а затем использовать этот регистр в качестве операнда).
Флаги после операции деления не определены.
Отметим особенность деления знаковых чисел. Какой знак получает оста-
ток при делении, например, отрицательного числа на положительное?
Пример. Если разделить –26 на +7, то получим частное –4 и остаток +2 (в
самом деле, (+7)(–4) + 2 = –26). Но можно считать также, что частное –3, а оста-
ток –5 (действительно, (+7)(–3) – 5 = –26). Какой результат принять? Если взять
абсолютные величины операндов, то 26 = 7*3+5. Поэтому принят второй вари-
ант.
На основе примера можно сформулировать правило: для знаковых чисел
остаток имеет тот же знак, что и делимое.
Задача. В нулевой главе (пункт 0.2) величина 5 mod –3 вычислена двумя
способами в зависимости от определения. Составьте программу и вычислите в
td эту величину, используя команду idiv.
Решение.
mov ax,5
mov bh,–2
idiv bh
В результате в регистре AH число 2.
В отличие от команд mul и imul, где правильный результат получается
всегда, команды деления могут быть невыполнимы. Это происходит в следую-
щих случаях:
делитель равен нулю;
результат вне допустимого диапазона.
Пример. Пусть AX = FFFE, BL = 2.
1) idiv bl
Переполнения нет, т.к. AX = –2. Результат: AH = 0, AL = –1.
2) div bl
Переполнение, т.к. результат деления 7FFF не умещается в байте.
Что должен делать процессор, если произошло переполнение? Дальнейшие
вычисления бессмысленны, так как результат выполнения команды неверен.
Нужно сообщить об этом пользователю.
Реализовано это так. Если процессор не может выполнить операцию деле-
ния, то внутри него возникает прерывание (interrupt). Активизируется програм-
ма обработки прерывания. Например, она выдает сообщение: divide overflow, и
завершает работу программы, где возникла ошибка.
Если же проводятся вычисления в электронной таблице Excel, то в ячейке с
результатом появляется #ДЕЛ/0! (в русифицированной версии программы), но
пользователь может исправить содержимое ячеек с исходными данными —
программа не прекратила работу.
Детально механизм прерывания мы рассмотрим в дальнейшем.
Здесь полезно уяснить, почему переполнение при делении приводит к пре-
рыванию, а при сложении и вычитании нет. Дело в том, что в последнем случае
10
результат можно интерпретировать по-разному, в зависимости от типа операн-
дов: знаковые они или беззнаковые. Процессор выставляет флаги, а програм-
мист сам решает, как ему интерпретировать результат. При этом, как мы видели,
при сложении знаковых целых может возникать переполнение, а при сложении
беззнаковых — нет, и наоборот.
Теперь разберем вопрос, как поделить байт на байт. Команды деления
предусматривают, что при делении на байт делимое находится в слове AX. Ре-
шение проблемы будет различным в зависимости от типа операндов.
беззнаковые: обнулить AH mov al, делимое
mov ah,0
div делитель
знаковые: расширить знак AL в AH mov al, делимое
cbw
idiv делитель
Упражнение. Разработать аналогичные последовательности команд для де-
ления слова на слово.
Вопросы умножения и деления с многократной точностью мы не рассмат-
риваем. Соответствующие программы можно найти, например, в [Злобин В.К.,
Григорьев В.Л. Программирование арифметических операций в микропроцес-
сорах. — М.: Высшая школа, 1991. — 304 с.].
5.10. Пример программы с арифметическими операциями (задание A2).
5.10.1. Формулировка задания.
Пусть x, z — байты, y, v — слова. Вычислить
y(z 1) 1
v 1
x3
Проверить на двух тестовых наборах:
1) исходные данные: x = 1, y = –3, z = 4; результат: v = –1.
2) исходные данные: x = 7Dh, y = 6DB7h, z = –6h; результат: v = –5FFh.
Отчет должен содержать:
1)текст программы с комментариями. В комментариях к каждой команде
привести результаты пошагового выполнения программы на первом тестовом
наборе.
2) таблицу результатов промежуточных вычислений дли обоих тестов.
5.10.2. Программа
Разместим данные в памяти: x по адресу 200, y по адресу 201 = 200+1 (т.к. x
занимает один байт), z по адресу 203 = 201+2 (т.к. y занимает два байта), v по
адресу 204. Конечно, лучше пристыковать данные к хвосту программы, но тогда
при необходимости вставить в текст программы команду, пропущенную по
ошибке, придется дополнительно переделывать все команды, содержащие адре-
са ячеек памяти (в дальнейшем с такой работой прекрасно справится Ассем-
блер).
Теперь напишем программу. В комментариях в скобках будем указывать ре-
зультат выполнения команды для первого теста. Это поможет нам при отладке
программы.
; Вычисление числителя
11
mov al,[203] ; Поместить z в AL (AL = 4)
cbw ; Расширить AL со знаком до слова (AX = 4)
dec ax ; Вычислить z – 1 (AX = 3)
imul word ptr [201] ; Вычислить y(z – 1) (DX:AX = –9 = FFFF FFF7)
add ax,1 ; Вычислить младшее слово числителя (AX = FFF8)
adc dx,0 ; Вычислить старшее слово числителя (DX = FFFF)
; Вычисление знаменателя
mov bx,ax ; Сохранить младшее слово числителя в BX (BX = FFF8)
mov al,[200] ; Поместить x в AL (AL = 1)
cbw ; Расширить AL со знаком до слова (AX = 1)
add ax,3 ; Вычислить знаменатель (AX = 4)
xchg ax,bx ; Поместить младшее слово числителя — в AX,
; знаменатель — в BX (AX = FFF8, BX = 4)
idiv bx ; Выполнить деление числителя на знаменатель
; (AX = –2 = FFFE —частное, DX = 0 — остаток)
inc ax ; Увеличить результат на единицу (AX = –1 = FFFF)
mov [204],ax ; Результат — в v ([204] = FFFF)
nop
Прокомментируем четвертую команду. Использован атрибутный оператор
word ptr, т.к. иначе процессор "не будет знать", с какими операндами рабо-
тать: перемножать байты AL и [201] или слова AX и [201]. Завершаем програм-
му командой nop (нет операции). Она пригодится при работе в отладчике.
5.10.3. Выполнение программы в отладчике Turbo Debugger.
1) Вызов отладчика.
C:\prog\>td
На экране окно CPU. Расширяем его во весь экран нажатием клавиши F5.
2) Ассемблирование текста программы.
Вводим текст программы (разумеется, без комментариев).
cs:0100►A00302 mov al,[0203]
cs:0103 98 cbw
cs:0104 48 dec ax
cs:0105 F72E0102 imul word ptr [0201]
cs:0109 050100 add ax,0001
cs:010C 83D200 adc dx,0000
cs:010F 8BD8 mov bx,ax
cs:0111 A00002 mov al,[0200]
cs:0114 98 cbw
cs:0115 050300 add ax,0003
cs:0118 93 xchg bx,ax
cs:0119 F7FB idiv bx
cs:011B 40 inc ax
cs:011C A30402 mov [0204],ax
cs:011F 90 nop
Отметим интересный момент, который может возникнуть при работе с TD.
Набрав команду nop, мы перемещаем курсор вверх, чтобы установить его на
команду с адресом cs:100. Но с удивлением видим примерно следующее:
12
cs:00FF 00A00302 add [bx+si+0203],ah
cs:0103 98 cbw
cs:0104 48 dec ax
Куда же исчезла первая команда нашей программы? Дело в том, что в диа-
пазоне адресов CS:0000 — CS:00FF расположена специальная область данных,
носящая название «префикс программного сегмента» — Program segment prefix
(PSP). В панели кода отладчик рассматривает данные как коды некоторых ко-
манд. В результате дисассемблирования код последней из таких «команд»
начался с адреса CS:00FF и занял четыре байта (первый байт здесь содержит
нуль, а остальные три байта, как нетрудно видеть, код команды mov
al,[0203]). Нужно не пролистывать панель кода «назад», а использовать ко-
манду локального меню Alt+F10 → Goto (Ctrl+G). В диалоговом окне вводим
адрес 100. Теперь код программы отображается правильно.
3) Сохранение кода программы в файле.
Выполнение еще не отлаженной программы может привести к зависанию
компьютера и тогда программу придется вводить вручную заново. Запись кода
программы в файл детально разобрана в предыдущей главе.
Дадим файлу имя [Link]. Размер файла 20h.
4) Ввод исходные данных.
Введем данные для первого тестового набора. В него входит отрицательное
число –3. Вызываем встроенный калькулятор (Ctrl+F4). Вводим –3. Получаем
результат: word –3 (FFFDh).
Теперь расположим исходные данные, начиная с адреса 200. Перейдем в
панель данных (Shift+Tab). Перейдем к адресу 200 (Ctrl+G). Выберем команду
локального меню Alt+F10 → Change (Ctrl+C). В диалоговом окне введем после-
довательность байтов: 1, 0fdh, 0ff, 4, 0, 0. При вводе числа FFFD нужно соблю-
дать осторожность. Числа хранятся в «перевернутом виде» (более значимое по
более старшему адресу). Ввод fd и ff ошибочен, потому что число должно начи-
наться с десятичной цифры. Ввод 0fd ошибочен, потому что цифру d отладчик
воспринимает как суффикс десятичного числа.
Проконтролировать правильность ввода исходных данных можно в панели
кода. Перемещайте курсор на команды, содержащие прямую адресацию. Тогда в
заголовке окна будет отображаться содержимое ячейки памяти, к которой об-
ращается команда. Для команды mov al,[0203]отображается ds:0203 = 04.
Для команды imul word ptr [0201] отображается ds:0201 = FFFD.
5) Трассировка программы.
Проведем трассировку программы, проверяя правильность промежуточных
результатов. Для этого нажимаем функциональную клавишу F7 (Trace).
После выполнения команды умножения imul word ptr [0201] про-
анализируем результат. Он размещается в DX:AX, но т.к. CF=OF=0, то DX =
FFFF — это расширение знака, и произведение фактически содержится только в
AX = FFF7. Выясним, что это за число. Нажимаем Ctrl+F4 и вводим 0–0FFF7.
Получаем результат: 9. Следовательно, FFF7 — это представление числа –9 в
дополнительном коде. Итак, результат умножения — отрицательное число –9.
После add ax,0001 в панели флагов видим, что CF=0. Это означает, что
при сложении младших слов не возник единичный перенос в старшее слово ре-
зультата.
Когда мы доберемся до команды nop, в ячейке 204 появятся байты FF FF,
т.е. результат правильный (равен –1).
13
6) Прогон программы.
Выполним прогон программы на втором тестовом наборе x = 7Dh, y =
6DB7h, z = –6h. На этот раз не будем проводить пошагового выполнения (трас-
сировки), а выполним программу, как единое целое.
Сначала найдем дополнительный код отрицательного числа – 6. Он равен
FFFA. Введем новые исходные данные: 7dh,0b7,6dh,0fa,0,0. (Из FFFA удаляем
старший байт — расширение знака.)
Переместимся в панели кода на команду с адресом 100: Alt+F10/Goto (вво-
дим 100). Будет выделена команда с адресом 100. А теперь сделаем так, чтобы
IP содержал адрес этой команды: Alt+F10/New cs:ip. Поставим курсор на коман-
ду nop в конце программы. Нажмем клавишу F4 (Here — здесь). Программа бу-
дет выполнена.
В слове с адресом 204 записаны байты 01 FA, т.е. число FA01. Вызовем
калькулятор (Ctrl+F4). Введем выражение 0-0fa01. Результат: word 1535 (5FFh).
Итак, получен ответ –5FF.
Прогон программы на втором тестовом наборе также дал правильный ре-
зультат.
7) Завершение работы с отладчиком.
Alt+X.
5.10.4. Таблица результатов промежуточных вычислений дли обоих тестов.
Вызовем отладчик, указав в командной строке имя файла с программой.
C:\prog>td [Link]
Составим таблицу из четырех колонок. В первой колонке будем записывать
вычисляемые элементы формулы. Во второй колонке укажем размер (байт, сло-
во, двойное слово) промежуточного результата. В оставшихся двух колонках
выпишем промежуточные результаты выполнения обоих тестов.
Выражение Размер Результаты для Результаты для
результата 1-го теста 2-го теста
z–1 слово 3 FFF9 = –7
y(z–1) дв. слово FFFF FFF7 = –9 FFFC FFFF = –30001
y(z–1)+1 дв. слово FFFF FFF8 = –8 FFFD 0000 = –30000
x+3 слово 4 0080
(y(z–1)+1)/(x+3) слово FFFE = –2 FA00 = –600
(y(z–1)+1)/(x+3)+1 слово FFFF = –1 FA01 = –5FF
Эта таблица понадобится при выполнении задания C1.
5.10.5. Обсуждение некоторых фрагментов программы
Ответим на вопрос, почему команды вычисления знаменателя дроби не
приведены в такой последовательности:
mov al,[200]
add al,3
cbw
т.е. сначала увеличение операнда на 3, а затем его расширение со знаком. Про-
следим промежуточные результаты на втором тестовом наборе данных:
Правильно Результат Неправильно Результат
mov al,[200] AL = 7Dh mov al,[200] AL = 7Dh
cbw AX = 007Dh add al,3 AL = 80h
14
add ax,3 AX = 0080h cbw AX = FF80h
Итак, во втором варианте произошло знаковое переполнение из-за того, что
размер операнда (байт) был недостаточно велик. В то же время на первом тесто-
вом наборе сработает и "неправильный" вариант программы. В нашем исходном
варианте шире диапазон исходных значений, к которым применима программа.
А теперь рассмотрим фрагмент вычисления числителя дроби:
; Вычислить y(z – 1)
add ax,1 ; Вычислить младшее слово числителя
adc dx,0 ; Вычислить старшее слово числителя
Что произойдет, если заменить add ax,1 более компактной командой
inc ax? (Нетрудно убедиться, что код второй команды занимает всего один
байт, а первой — три байта). На первом тестовом наборе вычисления будут пра-
вильными, но на втором наборе результат окажется неверным. В самом деле,
после выполнения команды imul word ptr [201] содержимое регистров
следующее: DX = FFFCh, AX = FFFFh (проверьте!). При этом CF = 0. После вы-
полнения команды inc ax содержимое AX нулевое, а CF по-прежнему сбро-
шен! И в результате команда adc dx,0 оставит содержимое DX неизменным.
В то же время команда add ax,1 не только обнулит AX, но и установит флаг
переноса: CF = 1. Тогда результат adc dx,0 будет другим: DX = FFFDh.
Предпоследняя команда программы inc ax правомерна, т.к. действие
производится над словом, а не над элементом двойного слова.
Подчеркнем, что несмотря на наши усилия увеличить диапазон допустимых
входных значений, программа не сможет вычислить v на втором тестовом набо-
ре, если x = –2. При делении возникнет переполнение.
2 ( x 2)( y 1)
Рассмотрим другой пример. Вычислить выражение , где x,
z
y, z — слова. Ограничимся вычислением числителя. Проблема в том, что после
перемножения x – 2 и y – 1 результат окажется в DX:AX, и непонятно, как орга-
низовать вычитание этого длинного слова из непосредственного операнда. Рас-
смотрим два решения: неверное и верное. Пусть x размещено по адресу 200, y —
по адресу 202. Там записаны числа: [0200] = 8002h, [0202] = 8001h.
1 вариант (неправильный). Перепишем формулу для числителя в виде
2 (( x 2))( y 1) . Теперь нужно сложить DX:AX с непосредственным операн-
дом 0000 0002.
mov ax,[200] ; AX = 8002h
mov bx,[202] ; BX = 8001h
sub ax,2 ; AX = 8000h
neg ax ; AX = 8000h, OF = 1
dec bx ; BX = 8000h
imul bx ; DX:AX = 4000 0000h
add ax,2 ; AX = 0002h
adc dx,0 ; DX = 4000h
Результат оказался положительным числом, хотя должен быть отрицатель-
ным! Нетрудно видеть, почему. При выполнении операции neg ax произошло
знаковое переполнение!
15
2 вариант (правильный). Разместим непосредственный операнд в паре реги-
стров SI:DI.
mov ax,[200] ; AX = 8002h
mov bx,[202] ; BX = 8001h
sub ax,2 ; AX = 8000h
dec bx ; BX = 8000h
imul bx ; DX:AX = 4000 0000h
mov si,0
mov di,2
sub di,ax ; DI = 0002h
sbb si,dx ; SI = C000h
mov ax,di ; AX = 0002h
mov dx,si ; DX = C000h
На этот раз результат получился правильный. Подчеркнем, что первый ва-
риант годится для подавляющего большинства наборов исходных данных. Но
для второго варианта диапазон допустимых значений все-таки шире, и именно
поэтому ему следует отдать предпочтение.
1 3xy
Задача. Вычислить v , где x, v — слова, y — байт. x = 1, y = –2. (То-
y 1
гда v = –7.)
Решение. Расположим x по адресу 200, y по адресу 202 (202 = 200 + 2), v по
адресу 203 (203 = 202 + 1). При вычислении числителя мы должны получить
двойное слово. Организуем вычисления так: сначала перемножим байт y и чис-
ло 3. Получим слово. Это слово умножим на x, Получим двойное слово. Кон-
станту 1 разместим как двойное слово в паре регистров SI:DI (в SI расположим
старшие разряды, а в DI — младшие разряды). В знаменателе сначала преобра-
зуем байт y в слово, а затем увеличим его на 1. Перед выполнением операции
деления числитель оказывается в паре SI:DI, а знаменатель в AX. Чтобы выпол-
нить деление, мы размещаем числитель в паре регистров DX:AX, а знаменатель
в DI.
; Вычисление числителя
mov al,3 ; (AL = 3)
imul byte ptr [202] ; 3*y (AX = -6 = FFFA)
imul word ptr [200] ; 3*x*y (DX:AX = FFFF:FFFA)
mov si,0 ;
mov di,1 ; (SI:DI = 0000 0001)
sub di,ax ; (DI = 0007, CF = 1)
sbb si,dx ; 1 – 3*x*y (SI = 0000)
; Вычисление знаменателя
mov al,[202] ; (AL = FE = –2)
cbw ; (AX = FFFE = –2)
inc ax ; (AX = FFFF = –1)
xchg ax,di ; (AX = 0007, DI = FFFF)
mov dx,si ; (DX = 0000)
idiv di ; (AX = FFF9 = -7 — частное,
; DX = 0000 — остаток)
mov [203],ax
nop
16
5.11. Стек. Команды работы со стеком
В рассмотренных фрагментах промежуточные результаты сохранялись в ре-
гистрах. Это — лучшее решение, т.к. обеспечивает наивысшее быстродействие
программы. Но если регистров для хранения промежуточных данных не хвата-
ет, придется записывать данные в оперативную память. Здесь уместно начать
знакомство со стеком.
Стек (stack) — важное понятие теоретического программирования, частный
случай линейного списка. Стек — это одномерная динамическая структура дан-
ных, добавление элемента в которую (или исключение элемента из которой)
производится с одного конца, называемого вершиной стека (TOS — Top of
Stack). Другими словами, стек организован в виде списка типа LIFO (Last In –
First Out — вошедший последним – выходит первым). Стек называют также
магазинной памятью. При этом имеют в виду аналогию с магазином автомати-
ческого оружия. Патрон, который последним оказался в магазине при его сна-
ряжении, первым попадает в ствольную коробку при стрельбе.
Мы пока не будем рассматривать теоретические аспекты этого понятия, а
сразу обратимся к программно-аппаратному стеку в 8086. Стек размещается в
ОЗУ, в стековом сегменте (т.е. сегментный адрес области памяти для стека хра-
нится в SS). Элементы стека — слова. Регистр SP (Stack Pointer — указатель
стека) содержит адрес (точнее, смещение) последнего включенного в стек слова,
т.е. SP указывает на вершину стека. При включении в стек новых слов вершина
перемещается в направлении уменьшения адресов (говорят: стек растет вниз).
Включение в стек (push) обозначают стрелкой, направленной вниз↓, извлечение
из стека (pop) — стрелкой, направленной вверх ↑. Перечислим команды для ра-
боты со стеком:
Включить в стек push src 1) SP ← SP – 2
2) (SP) ← src
PUSH флаги не изменяются
push r/m16; i16 (начиная с 80286)
Извлечь из стека pop dst 1) dst ← (SP)
2) SP ← SP + 2
POP флаги не изменяются
push r/m16
Включить в стек флаги pushf flags
PUSH Flags флаги не изменяются
операнд подразумевается
Извлечь из стека флаги popf flags
POP Flags флаги из стека
операнд подразумевается
Проиллюстрируем понятие стека следующей наглядной картинкой:
младшие адреса слова
начало сегмента стека SS
17
включение в стек
размер
вершина стека последний элемент текущее SP стека
извлечение из стека
Исходное SP
старшие адреса
Стек является удобным средством для сохранения и восстановления реги-
стров.
Пример. Сохраним в стеке содержимое AX, BX, CX. Пусть AX = 3, BX = 5,
CX = 4, SP = FFFE.
Состояние стека после выполнения фрагмента
push ax
push bx
push cx
FFF6
FFF8 4 текущее SP
FFFA 5
FFFC 3
FFFE исходное SP
Проследим, как происходило занесение в стек содержимого регистра AX.
Сначала указатель стека SP перемещается на следующее слово. Это сводится к
вычитанию из содержимого SP двойки: FFFE – 2 = FFFC. После этого по адресу
SS:SP заносится содержимое регистра AX. Далее эта операция повторяется еще
дважды, только уже с регистрами BX и CX. В результате SP = FFF8.
Восстановим содержимое регистров из стека (предположим, что содержи-
мое регистров AX, BX, CX со времени их сохранения претерпело изменения, а
содержимое SP не изменилось).
pop cx
pop bx
pop ax
Обратите внимание, что извлечение регистров происходит в порядке, об-
ратном их занесению. В результате SP = FFFE. Значения 3, 5, 4 пока сохраняют-
ся в памяти, но при следующих операциях со стеком они будут скорее всего
уничтожены.
Упражнение. Выполнить этот пример в Turbo Debugger. В панели регистров
занести в регистры требуемые значения; в панели кода набрать последователь-
ность из указанных шести команд; выполнить эти команды, наблюдая измене-
ния в панели стека. Обратите внимание, что вершина стека в панели стека отоб-
ражается черным треугольником.
Заметим, что действия по перемещению указателя стека (т.е. увеличение и
уменьшение на два) выполняются в командах push и pop в разном порядке:
push: сначала указатель перемещается вниз на свободное слово, после чего в
это слово записывается содержимое источника.
pop: содержимое слова в вершине стека помещается в приемник, после чего
указатель перемещается вверх на следующее слово.
18
Именно благодаря такому порядку действий команды push и pop можно ис-
пользовать совместно для занесения и извлечения данных (разберите, что про-
изойдет, если в обеих командах push и pop изменение указателя будет происхо-
дить в качестве первого действия).
В 286-м процессоре можно использовать в команде push непосредственный
операнд. Итак, если в 8086 для размещения в стеке константы 5 нужно было
использовать две команды: mov ax,5 и push ax, то в 286-м процессоре достаточ-
но одной команды push 5. (Атрибутный оператор не нужен, т.к. в стеке сохра-
няются только слова.)
В 286-м процессоре появились две новые команды для работы со стеком.
Включить в стек все HL- и pusha ax cx dx bx sp bp si di
PI-регистры
PUSH All флаги не изменяются
операнд подразумевается
В стек помещается то содержимое SP, которое было в нем до выполнения
команды pusha.
Извлечь из стека все HL- и popa di si bp sp bx dx cx ax
PI-регистры
POP All флаги не изменяются
операнд подразумевается
Эти две команды полезны, когда нужно сохранить и восстановить практи-
чески все регистры общего назначения. Если же нужно сохранить и восстано-
вить только два-три регистра, то лучше для каждого из них использовать от-
дельную команду push и pop.
5.12. Операции с сегментными регистрами
Только в трех командах возможен операнд – сегментный регистр. Это ко-
манды mov, push, pop.
Команда mov может иметь только такие форматы: mov sr,r/m16; r/m16,sr (sr
— сегментный регистр). Поэтому недопустимы, например, команды mov es,
0B800h и mov es, ds. Чтобы реализовать эти действия, приходится использовать
промежуточный регистр: mov ax,0B800h / mov es,ax и mov ax,ds / mov es, ax.
Регистр cs не может быть приемником в команде mov. (Для изменения со-
держимого cs надо использовать, например, команду дальнего безусловного
перехода, которую мы изучим позже.)
Сегментные регистры могут быть операндами в командах push и pop. Вме-
сто пары команд mov ax, ds / mov es, ax эффектнее использовать push ds / pop es.
Но первая пара команд выполняется быстрее: ведь при выполнении второй па-
ры команд происходит два обращения к памяти, сопровождающиеся изменени-
ем sp.