
6. Массивы и циклы
6.1. Индексная адресация
До сих пор мы писали программы, в которых обращались к отдельным ячейкам памяти, т.е. в терминологии языков программирования высокого уровня работали с простыми переменными. Но из опыта работа с теми же языками мы знаем, что очень полезно иметь возможность работать с данными, объединенными в некоторые совокупности: массивы, структуры и т.д. Посмотрим, как в процессоре реализовано обращение к элементам массива.
Допустим, мы рассматриваем байтовый массив по адресу 300. Элемент с номером нуль расположен по адресу 300, первый элемент — по адресу 301 и т.д. С теми возможностями, которыми мы сейчас располагаем, мы можем записать в аккумулятор, допустим, третий элемент командой mov al,[303] Но, оказывается, то же самое можно сделать иначе:
mov si,3
mov al,[si+300]
В индексный регистр si мы поместили номер элемента, а затем выбрали его по адресу 300+si. Можно было записать эту команду иначе: mov al,300[si] с тем же эффектом. Вместо si здесь можно было использовать di и bx (и даже bp — для com-программ, которые мы создаем в debug, — для них совпадают значения сегментных регистров, а вообще-то bp предназначено для стекового сегмента).
Значение si можно вычислять в программе, и тем самым обращаться к разным элементам массива.
Пример. В массиве из 16 байт расположенном с 200-го адреса, сосчитать количество элементов, величина которых больше значения –2.
mov cx,10h; Количество элементов — в CX
mov bx,0 ; Счетчик подходящих элементов — в BX
mov si,0 ; SI — индекс массива
m: cmp byte ptr [si+200],-2 ; Очередной элемент больше -2 ?
jng n ; Нет — на n
inc bx ; Да — увеличить счетчик
n: inc si ; Перейти к следующему элементу
dec cx ; Уменьшить счетчик повторений цикла
jnz m ; Если счетчик повторений отличен от нуля — на m
nop ; В BX — количество подходящих элементов
Использованный метод адресации носит название — индексный.
6.2. Команды организации циклов
В только что разобранной программе легко выделить конструкцию для организации цикла с фиксированным числом повторений:
для k:= 1 до N выполнить <тело цикла>
Ее эквивалент на языке Ассемблера
mov cx,N
next:
<тело цикла>
dec cx
jnz next
Последние две команды можно заменить одной командой loop next.
Управление циклом |
loop opr |
CX
|
LOOP — петля |
|
Флаги не изменяются |
Обратите внимание, что счетчик повторений — обязательно регистр CX. Команда loop проверяет именно его содержимое.
Если цикл включен в программу, и количество повторений тела цикла предварительно вычисляется, то имеет смысл проверить, что CX отличен от нуля.
Задача. Пусть в программе, приведенной выше команда mov cx,10h заменена на mov cx,0. Сколько раз будет выполнено тело цикла?
Проверку на равенство содержимого CX нулю легко осуществить последовательностью двух команд:
cmp cx,0
je error
но можно для этой же цели воспользоваться одной командой jcxz.
Перейти по CX = 0 |
jcxz opr |
если
CX = 0 , то IP
|
Jump if CX equal Zero |
Флаги не изменяются |
Команду ставят перед началом цикла. Метка opr, разумеется, находится вне цикла. Эту команду причисляют к командам условного перехода, хотя она заведомо выпадает из их ряда: jcxz проверяет содержимое регистра CX, а прочие команды — состояние флагов.
Имеются еще две команды организации циклов: LOOPZ/LOOPE opr и LOOPNZ/LOOPNE opr (через дробную черту указаны альтернативные мнемоники). В них непосредственно перед уменьшением CX проводится проверка флага ZF и в зависимости от его значения выполнение цикла продолжается до исчерпания CX или досрочно прекаращается. Но эти команды редко используются. Поэтому их изложение опустим.
6.3. Косвенная адресация
Настало время перечислить все методы адресации. Нами уже изучены методы:
-
регистровый: inc bx
-
непосредственный: mov ax,6
-
прямой: dec word ptr [200].
При использовании косвенной адресации используется один или два регистра, в которых хранятся некоторые слагаемые адреса. Дадим сразу общую схему:
Здесь EA — Effective Address — эффективный (исполнительный) адрес. Фигурные скобки означают, что вычисляется один из перечисленных внутри них элементов (в том числе и никакой: на это указывает пробел). Возможны любые комбинации перечисленных элементов, за исключением двух: 1) [] , т.е. недопустимы квадратные скобки без содержимого, 2) [BP]. Если все-таки нужно использовать [BP], то для этого следует использовать адресацию [BP+D8], где D8 = 0 (в программе на языке Ассемблера можно записать обращение к операнду, как [BP], но Ассемблер автоматически переведет его в форму [BP+0]). Причину, почему использование [BP] невозможно, мы узнаем позже, когда изучим кодирование команд.
Упражнение. Введите в debug команду mov ax,[bp] и дисассемблируйте ее.
Итак, смысл приведенной выше формулы ясен: процессор складывает содержимое базового регистра, индексного регистра и смещение (displacement). Полученная величина является адресом, а точнее, смещением (offset).
В каком же сегменте вычисляется это смещение? Здесь действует правило: если в выражении встречается BP, то полный адрес вычисляется с использованием содержимого сегмента стека SS, во всех остальных случаях используется сегмент данных DS.
Отдельные комбинации регистров, входящих в EA, имеют свои названия. В литературе по поводу названий методов адресации нет единого мнения. Чаще употребляются следующие наименования.
1) [BX], [SI], [DI] (не [BP]!) — регистровая косвенная адресация.
2) [BX+20], 20[BX], [BP+8] — базовая адресация.
3) [SI–4], –4[SI], [DI+6] — индексная адресация.
-
[BX+SI+2], [BX][SI+2], 2[BX][SI] — базовая индексная адресация со смещением. Все три выражения эквивалентны. Предпочтительнее использовать первое из них.
Остается неясным, чем базовый метод адресации отличается от индексного. В 16-разрядном режиме — ничем! Эти названия, по-видимому, были даны "на вырост", с учетом перспективы. Когда мы приступим к изучению 32-разрядного режима, то увидим, чем отличаются эти два режима.
6.4. Turbo Debugger. Пример работы.
Осуществим вызов Turbo Debugger:
D:\>td
(если, конечно, к нему проложена "дорожка").
На экране появится окно (так называемое "окно CPU") . В верхней строке экрана главное меню:
File |
View |
Run |
Breakpoints |
Data |
Options |
Window |
Help |
Файл |
Вид |
Запуск |
Остановы |
Данные |
Опции |
Окно |
Справка |
Доступ к его пунктам осуществляется нажатием F10. Каждому пункту соответствует выпадающее меню.
В нижней строке экрана подсказки о назначении некоторых "горячих клавиш":
F1-Help F2-Bkpt F3-Mod F4-Here F5-Zoom F6-Next F7-Trace F8-Step F9-Run F10-Menu
Окно CPU разбито на панели (рис. 6.1.).
код
|
регистры |
флаги |
данные
|
стек |
Рис.6.1.
Активной является панель кода (в ней находится синяя полоса курсора). Переход из панели в панель по часовой стрелке осуществляется нажатием клавиши Tab, против часовой стрелки — Shift+Tab (активизировать панель также можно щелчком мыши). С каждой панелью связано свое локальное меню, оно вызывается нажатием Alt+F10 или щелчком правой кнопки мыши.
Познакомимся с локальным меню на примере панели регистров. Перейдите в панель регистров (Tab). Выделите курсором регистр DX, в котором сейчас записан 0 (отладчик заранее очищает регистры общего назанчения). Вызовите локальное меню (Alt+F10). Выделите в нем команду Decrement. В нижней строке экрана появляется подсказка: Decrement the highlighted register (уменьшение выделенного регистра). Если вы нажмете функциональную клавишу F1 (Help), то на экране появится окно с текстом Справки. Это окно закрывается нажатием клавиши Esc. Нажмите Enter. Содержимое DX изменится и станет равным FFFF. Увеличим его на 1. Вспомним, что начальные буквы пунктов локального меню были выделены цветом. Это означает, что находясь в панели можно осуществить команду локального меню нажатием комбинации клавиш Ctrl+буква. Нажмем (и не отпускаем) клавишу Ctrl. В нижней строке экрана появляется подсказка: Ctrl: I-Increment D-Decrement и т.д. Не отпуская Ctrl, нажмем клавишу I. Содержимое DX увеличится на 1 и вновь станет равным нулю. В локальном меню после пункта Change (изменить) стоит многоточие. Это означает, что после выбора этого пункта меню появится диалоговое окно. Запишите в CX число 7.
В TD имеется возможность вычислять выражения. Допустим, нам нужно ввести в байт (в панели данных) десятичное значение –120. Попытка ввода числа –120d приведет к сообщению Syntax error. Найдем 16-ричный эквивалент –120d. Для этого выберем в главном меню Data/ Evaluate/modify (или нажмем Ctrl+F4). Введем в поле Expression число –120d и нажмем Enter. В поле Result прочитаем: word –120 (FF88h). Отбрасывая расширение знакового бита, вводим число 88h.
6.5. Пример программы обработки массива (задание a4)
6.5.1. Формулировка задания
Дан массив A из 16 байтов. Скопировать его в массив B, заменяя элементы, равные 2, на нуль. Поместить в массив C адреса (смещения) измененных элементов из массива A. Сосчитать количество элементов, которые не подверглись изменению.
Отчет должен содержать: текст программы с комментариями, входные и выходные значения массивов.
6.5.2. Программа
Разместим массив A, начиная с адреса 200, массив B — с адреса 210 (адреса 16-ричные!), массив C — с адреса 220.
Приведем текст программы.
mov si,200 ; Адрес массива A — в SI
mov di,210 ; Адрес массива B — в DI
mov bx,220 ; Адрес массива C — в BX
sub dx,dx ; Счетчик неизменяемых элементов — в DX
mov cx,10 ; Количество элементов (10h = 16)
; в обрабатываемом массиве — в CX
n: mov al,[si]; Поместим очередной элемент массива A в AL
cmp al,2 ; Сравним его с 2
jne m ; Если равен 2,
mov byte ptr [di],0 ; то записать в массив B нуль,
mov [bx],si ; поместить в C смещение измененного элемента,
inc bx ; и переместить в нем указатель
inc bx ; на следующий элемент,
jmp c
m: mov [di],al ; иначе — записать в B элемент из A,
inc dx ; увеличить счетчик неизменяемых элементов,
c: inc si ; переместить указатели в массиве A
inc di ; и в массиве B
loop n ; Конец цикла
nop
Тестовые данные выберем следующие:
до выполнения программы:
массив A: 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
массив B: 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
массив C: 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
после выполнения программы:
массив A: 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 (без изменений)
массив B: 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
массив C: 200, 202, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
регистр DX = 14 = 0Eh.
Текст программы и данные составляют содержание письменного отчета.
6.5.3. Выполнение задания в отладчике Turbo Debugger
Вместо debug воспользуемся намного более мощным и удобным отладчиком Turbo Debugger. Именно в этом отладчике мы будем в дальнейшем проводить отладку программ, написанных на языке Ассемблера.
1) Запуск Turbo Debugger. Ввод программы.
D:\USER\>td.exe
На экране появится окно CPU. Сейчас окно CPU занимает часть экрана. Нажмем F5 (Zoom), и окно будет "распахнуто" до размеров экрана.
Перейдем к выполнению
задания. Находясь в панели кода, начнем
ввод команд программы. Нажмем букву m
(первая буква команды mov
si,200). Появляется окно "Enter
instruction to assemble" (введите команду, которую
нужно ассемблировать). Завершаем ввод
команды и нажимаем Enter. Встроенный
мини-ассемблер немедленно преобразует
ее в машинный код. Для ввода следующей
команды вызываем команду ассемблирования
из локального меню нажатием Ctrl+A (или
Alt+F10/Assemble...). В появляющемся диалоговом
окне мы видим ранее введенную команду
mov si,0200.
Нажимем клавишу
(ранее введенная команда становится
выделенной). Нажимаем клавишу End (выделение
снимается). Редактируем команду, чтобы
получить mov di,0210.
Аналогично набираем остальные команды.
cs:0100BE0002
mov si,0200
cs:0103 BF1002 mov di,0210
cs:0106 BB2002 mov bx,0220
cs:0109 2BD2 sub dx,dx
cs:010B B91000 mov cx,0010
cs:010E 8A04 mov al,[si]
cs:0110 3C02 cmp al,02
cs:0112 7509 jne 011D
cs:0114 C60500 mov byte ptr [di],00
cs:0117 8937 mov [bx],si
cs:0119 43 inc bx
cs:011A 43 inc bx
cs:011B EB03 jmp 0120
cs:011D 8805 mov [di],al
cs:011F 42 inc dx
cs:0120 46 inc si
cs:0121 47 inc di
cs:0122 E2EA loop 010E
cs:0124 90 nop
Команды перехода (адреса 010D и 011B) приходится набирать дважды: сначала как jne 100 и jmp 100 (так как нам неизвестно числовое значение меток m и c), и только после ввода всего текста, когда мы выясним, что m = 011D, а с = 0120, мы можем исправить эти команды, поставив там нужные числовые значения меток, т.е. адреса перехода.
2) Сохраним код программы. Для этого, нажимая Shift+Tab, перейдем в панель данных. Вызовем локальное меню: Alt+F10 (рис. 6.2)
Goto... |
Search... |
Next |
Change... |
Follow |
Previous |
Display
as |
Block |
Рис. 6.2.
Выбираем команду Block. Появляется новое меню (на это указывает стрелка, замыкающая название команды Block). Меню показано на рис. 6.3.
Clear... |
Move... |
Set... |
Read... |
Write... |
Рис. 6.3.
Выбираем команду Write.... Появляется окно с запросом имени файла: "Enter write file name". В поле ввода File Name введем имя файла a4v0.com. Имя, разумеется, может быть любым, а вот расширение — только .com, не .exe. Нажимаем Enter.
Появляется новое окно с запросом адреса блока памяти и количества байтов в блоке: Enter memory address, count. Наша программа расположена, начиная с адреса CS:0100. В панели данных подразумевается, что сегментная часть адреса блока находится в регистре DS. Но в панели регистров мы видим, что содержимое сегментных регистров CS и DS совпадает. Поэтому адрес блока можно задать как 100 — только смещение (offset), без указания сегментной части адреса. Количество записываемых байтов: 0124h – 00FFh = 25h. Итак, в поле ввода указываем через запятую два числа: 100, 25, и нажимаем Enter.
Заметим, что если бы размер программы составил бы 2Dh, то ввод 100, 2d был бы ошибкой: в файл было бы записано всего два байта, т.к. d — суффикс десятичного числа. В этом случае обязательно надо указывать суффикс шестнадцатеричного числа: 100, 2dh.
3) Введем исходные данные. Сейчас активна панель данных.
Перейдем на адрес 200: Alt+F10/Goto. Появится окно с запросом адреса, на который надо перейти: Enter address to position to. Набираем 200 и нажимаем Enter. В панели данных отображается содержимое байтов, начиная с адреса DS:0200.
Сначала обнулим массивы A,B,C: Alt+F10/Block/Clear. Появляется уже знакомый запрос: Enter memory address, count. Вводим 200,40 и нажимаем Enter.
Теперь введем единицы в массив A: Alt+F10/Block/Set. В ответ на запрос Enter address, count, byte value вводим 200,10,1 и нажимаем Enter. Убеждаемся, что 16 байтов, начиная с адреса 200, содержат единицы.
Подправим первые три элемента массива A: Alt+F10/Change. В ответ на запрос Enter new data bytes вводим 2,1,2 и нажимаем Enter.
Исходные данные подготовлены. Может быть имеет смысл сохранить файл a4v0.com еще раз, разместив в нем исходные данные. Для этого надо повторить действия предыдущего пункта, только размер программы будет иной: 240h –0FFh = 141h. При этом в файле сохранится и "мусор" из диапазона адресов 125h –1FFh.
4) Разместим на экране окна для отображения содержимого массивов A и B. Массив C будем отображать в панели данных окна CPU.
Массив A. F10/View/Dump. На экране появляется окно с содержимым, аналогичным панели данных. Alt+F10/Goto. Вводим 200. Переместим окно в левый нижний угол: Ctrl+F5 (окно приобретает тонкую зеленую рамку) и клавишами перемещения курсора перемещаем его. Затем нажимаем Shift и клавиши перемещения курсора — окно меняет размеры. Добиваемся, чтобы в окне отображалось ровно 16 байтов — это две строки. Нажатием Enter положение и размеры окна закрепляются, зеленая рамка заменяется обычной. (Операцию изменения положения и размеров окна проще осуществлять мышью. Для изменения положения окна ухватите его мышью за верхнюю рамку и перемещайте. Для изменения размера окна ухватите его мышью за правый нижний угол рамки.)
Массив B. F10/View/Another/Dump. Появляется окно. Alt+F10/Goto. Вводим 210. Переместим окно в правый нижний угол и изменим его размеры, так чтобы отображалось 16 байтов.
Массив C. Перейдем в окно CPU. Это можно сделать либо последовательно нажимая F6 (переход из окна в окно), либо нажимая Alt+номер_окна, в нашем случае Alt+1. В панели данных перейдем на адрес 220. Массив C — это массив слов. Отобразим содержимое панели как слова: Alt+F10/Display as/Word. Изменим размер окна CPU так, чтобы на экране были размещены все три окна по следующей схеме (рис. 6.4):
код |
|
регистры |
флаги |
|
|
|
|
данные — массив C |
стек |
|
|
данные — массив A |
данные — массив B |
Рис. 6.4.
5) Программу на языке Ассемблера никогда не следует сразу запускать на выполнение. Ее нужно обязательно прогонять по шагам, так как ошибки в таких программах сделать легче, чем в программах на языке высокого уровня, а последствия этих ошибок — куда тяжелее.
Мы находимся в окне CPU, в панели данных. Чтобы перейти в панель кода, нажмем Tab. В счетчике команд IP находится адрес 100, что соответствует указателю на первую команду нашей программы в панели кода. При последовательном нажатии на F7 указатель будет перемещаться по программе. Внимательно прослеживаем содержимое регистров и ячеек памяти. Если должна выполняться команда работы с памятью, то справа вверху в панели кода отображается текущее содержимое ячейки памяти. Это полезно иметь в виду, чтобы быть уверенным, что из памяти действительно выбирается нужная информация (т.е. корректно перемещаются указатели, правильно используются методы адресации и т.д.).
Если в программе замечена ошибка, исправляем ее. Для этого возможно придется вставить новые команды или удалить существующие. Чтобы заново не набирать команды, расположенные вслед за ошибочными, воспользуйтесь в панели данных командой перемещения данных (в том числе и кода!) в памяти: Alt+F10/Block/Move. Разберитесь самостоятельно, как работать с этой командой.
Если нужно вновь прогнать программу по шагам, сначала следует в счетчик команд занести 0100 — адрес первой команды. Для этого переместитесь в панели кода на команду с адресом 100: Alt+F10/Goto и введите 100. Будет выделена команда с адресом 100. А теперь сделаем так, чтобы IP содержал адрес этой команды: Alt+F10/New cs:ip. В строке подсказки при выборе этого пункта меню вы увидите: Set the cs:ip to the current location — установить CS:IP в текущее положение курсора. Эквивалентная команда — Ctrl+N. Теперь вновь можно нажимать F7.
Выполнить программу как единое целое можно так: выделим команду nop в конце программы. При этом в IP, разумеется, должен быть стартовый адрес программы. Нажмем клавишу F4 (Here — здесь). Программа будет выполнена.