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. В первом случае изменяется только младший байт слова, во втором — всё слово!
Из методов адресации нам осталось изучить только один метод — косвенный. Но это мы сделаем позже.
5.2. Описание системы команд.
Нам предстоит изучить несколько десятков команд центрального процессора. Поэтому нужно сразу договориться о единообразной форме изложения.
Каждую новую команду мы будем описывать в виде таблицы такого примерно вида:
Название команды мнемоника действие
английское название (и его перевод) изменение флагов
Сочетание операндов
Поясним термин "мнемоника". Он происходит от греческого — искусство запоминания. Мнемоника команды, т.е. ее сокращенное название, призвана напомнить программисту действие команды. Именно поэтому мы приводим и английское название команды, выделяя заглавными те буквы, которые образуют мнемонику. Специальная программа — Ассемблер ("сборщик") преобразует эту мнемонику в код, состоящий из нулей и единиц. При выполнении программы процессор дешифрует этот код и исполнит предписываемую последовательность действий.
Вслед за мнемоникой команды следуют сокращенные обозначения операндов. Условно команды (для процессора 8086) можно разбить на двухоперандные, однооперандные и не имеющие операндов. В двухоперандных командах, как правило, можно выделить один операнд, который в результате выполнения команды претерпевает изменения — его мы будем называть приемником (destination, сокращенно 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 с.].
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.
Упражнение. Выполнить программу по шагам, на каждом шаге записывать содержимое 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
Имеется аналогичная команда для вычитания
Вычесть с заемом 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. http://www.df.lth.se/~john_e/]). Посредством одной команды заполнить все биты регистра 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 флаги не изменяются
операнд подразумевается
Преобразовать слово в двойное слово 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
twos complement NEGation меняет все флаги состояния
neg r/m8; r/m16
Эта команда вычитает знаковое целое из нуля.
Задача. Пусть dst — байт. При каком значении байта команда neg установит флаг OF?
Замечание*. Как выполнить операцию neg над длинным целым, расположенным в DX:AX? Имеется остроумное решение [The Assembly Gems Pages. http://www.df.lth.se/~john_e/]. В нем используется команда not dst. Она относится к группе команд для битовых операций, которую мы будем изучать позже. Эта команда инвертирует все биты операнда. Например, если AL = 56h, то после выполнения not al содержимое AL равно C9h. (Распишите двоичные представления этих чисел.). В частности, вместо команды neg dst можно было использовать команды not dst / add dst,1. Разница в том, что neg dst всегда устанавливает флаг CF, если операнд отличен от нуля. А теперь обещанный программный фрагмент:
not dx
neg ax
sbb dx,-1
Самостоятельно проанализируйте, почему он приводит к правильному результату.
Its basic approach is to compute the 2s complement by first computing the 1s 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 относится к битовым операциям, которые мы будем изучать существенно позже. Она выполняет операцию «исключающее ИЛИ» над битами приемника и источника: . По таблице 0.2 мы видим, что при любом значении 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
Благодаря удвоенному размеру результата переполнение никогда не возникает! Но в ряде случаев важно знать, можно ли разместить произведение в ячейке такого же размера, что и сомножители. Это можно выяснить, анализируя флаги. Если результат остается в младшем байте (слове), то CF = OF = 0. В противном случае CF = OF = 1. При этом надо иметь в виду, что AH (DX) все равно изменяется, если результат отрицательный. Например, при перемножении небольших отрицательных слов в результате получится DX = 0FFFFh, но процессор "понимает", что это — расширение знакового разряда, а не выход за пределы младшего слова.
Замечание. Может вызвать удивление тот факт, что потребовались две команды умножения — для знаковых и беззнаковых чисел. Ведь вычеты по модулю n образуют кольцо, а его знаковое и беззнаковое представление изоморфны. Поэтому нам нужна была только одна команда сложения и, стало быть, нужна только одна команда умножения. Объяснение этому противоречию следующее: результат умножения оказывается в кольце вычетов по модулю . Команды mul и imul осуществляют отображение , где . Выполните в 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
остаток частное 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! (в русифицированной версии программы), но пользователь может исправить содержимое ячеек с исходными данными — программа не прекратила работу.
Детально механизм прерывания мы рассмотрим в дальнейшем.
Здесь полезно уяснить, почему переполнение при делении приводит к прерыванию, а при сложении и вычитании нет. Дело в том, что в последнем случае результат можно интерпретировать по-разному, в зависимости от типа операндов: знаковые они или беззнаковые. Процессор выставляет флаги, а программист сам решает, как ему интерпретировать результат. При этом, как мы видели, при сложении знаковых целых может возникать переполнение, а при сложении беззнаковых — нет, и наоборот.
Теперь разберем вопрос, как поделить байт на байт. Команды деления предусматривают, что при делении на байт делимое находится в слове 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 — слова. Вычислить
Проверить на двух тестовых наборах:
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. Конечно, лучше пристыковать данные к хвосту программы, но тогда при необходимости вставить в текст программы команду, пропущенную по ошибке, придется дополнительно переделывать все команды, содержащие адреса ячеек памяти (в дальнейшем с такой работой прекрасно справится Ассемблер).
Теперь напишем программу. В комментариях в скобках будем указывать результат выполнения команды для первого теста. Это поможет нам при отладке программы.
; Вычисление числителя
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. Но с удивлением видим примерно следующее:
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) Сохранение кода программы в файле.
Выполнение еще не отлаженной программы может привести к зависанию компьютера и тогда программу придется вводить вручную заново. Запись кода программы в файл детально разобрана в предыдущей главе.
Дадим файлу имя a2v0.com. Размер файла 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).
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 a2v0.com
Составим таблицу из четырех колонок. В первой колонке будем записывать вычисляемые элементы формулы. Во второй колонке укажем размер (байт, слово, двойное слово) промежуточного результата. В оставшихся двух колонках выпишем промежуточные результаты выполнения обоих тестов.
Выражение Размер
результата Результаты для
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
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. При делении возникнет переполнение.
Рассмотрим другой пример. Вычислить выражение , где x, y, z — слова. Ограничимся вычислением числителя. Проблема в том, что после перемножения x – 2 и y – 1 результат окажется в DX:AX, и непонятно, как организовать вычитание этого длинного слова из непосредственного операнда. Рассмотрим два решения: неверное и верное. Пусть x размещено по адресу 200, y — по адресу 202. Там записаны числа: [0200] = 8002h, [0202] = 8001h.
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 произошло знаковое переполнение!
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
На этот раз результат получился правильный. Подчеркнем, что первый вариант годится для подавляющего большинства наборов исходных данных. Но для второго варианта диапазон допустимых значений все-таки шире, и именно поэтому ему следует отдать предпочтение.
Задача. Вычислить , где x, v — слова, y — байт. x = 1, y = –2. (Тогда 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
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
включение в стек
размер
вершина стека последний элемент текущее 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: содержимое слова в вершине стека помещается в приемник, после чего указатель перемещается вверх на следующее слово.
Именно благодаря такому порядку действий команды push и pop можно использовать совместно для занесения и извлечения данных (разберите, что произойдет, если в обеих командах push и pop изменение указателя будет происходить в качестве первого действия).
В 286-м процессоре можно использовать в команде push непосредственный операнд. Итак, если в 8086 для размещения в стеке константы 5 нужно было использовать две команды: mov ax,5 и push ax, то в 286-м процессоре достаточно одной команды push 5. (Атрибутный оператор не нужен, т.к. в стеке сохраняются только слова.)
В 286-м процессоре появились две новые команды для работы со стеком.
Включить в стек все HL- и PI-регистры pusha ax cx dx bx sp bp si di
PUSH All флаги не изменяются
операнд подразумевается
В стек помещается то содержимое SP, которое было в нем до выполнения команды pusha.
Извлечь из стека все HL- и PI-регистры popa di si bp sp bx dx cx ax
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.
5. Методы адресации. Арифметические команды. Стек
Лекции по предмету «Информатика»