Assembler / P05
.pdf11
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) Сохранение кода программы в файле.
Выполнение еще не отлаженной программы может привести к зависанию компьютера и тогда программу придется вводить вручную заново. Запись кода программы в файл детально разобрана в предыдущей главе.
Дадим файлу имя 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).
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 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 |
cbw |
AX = 007Dh |
Неправильно |
Результат |
mov al,[200] |
AL = 7Dh |
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 y 1 , где 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 |
|
|
|
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 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.
