Assembler / P22
.pdf1
22. Строковые команды
Это последняя большая группа команд, которую нам предстоит рассмотреть. Эти команды позволяют с помощью одной-двух инструкций обработать целую строку. Сразу отметим, что принципиально новых возможностей (как, например, битовые команды) строковые команды не дают. Для написания программ, которые нам предстоит рассмотреть, можно было обойтись и уже изученными средствами. Но применение строковых команд увеличивает качество программ: уменьшает размер кода и резко увеличивает быстродействие. Кроме того, компиляторы языков высокого уровня, как правило, не генерируют строковые команды. Их использование возможно только средствами языка Ассемблера. Особенно эти команды полезны для компьютерной графики.
22.1. Команда пересылки строк.
Детально рассмотрим команду пересылки (копирования) строк. На ее примере мы легко поймем остальные строковые команды.
Пример. Скопируем содержимое области памяти mem1 в область памяти mem2 (области не перекрываются).
Сначала напишем программу с использованием уже известных нам средств.
.DATA
mem1 DB 4,5,6,1,2,3 mem1len = $ - mem1 mem2 DB memlen DUP(?)
.CODE start:mov ax, @data
mov ds,ax
mov si, OFFSET mem1 mov di, OFFSET mem2 mov cx, mem1len
L:mov al,[si] mov [di],al inc si
inc di loop L
...
Итак, мы успешно справились с задачей, используя изученные ранее средства. А теперь изменим программу, воспользовавшись строковой командой movs (ее еще называют строковым примитивом, так как на ее основе можно по-
строить более сложную команду).
.CODE
start:mov ax, @data mov ds, ax
mov es, ax
mov si, OFFSET mem1 mov di, OFFSET mem2 mov cx, mem1len
cld ; Сбросить флаг направления
L:movsb loop L
...
2
Команда movsb заменяет четыре команды первой программы. Что еще нового появилось в программе?
Во-первых, команда загрузки ES — регистра дополнительного сегмента данных (до сих пор мы его практически не использовали). В строковых командах приемник всегда находится в дополнительном сегменте.
Во-вторых, команда cld. Эта команда относится к классу команд изменения флагов. В регистре Flags есть три управляющих флага. Один из них DF (direction flag — флаг направления). Этот флаг устанавливается или сбрасывается специальными командами.
Сбросить DF |
cld |
DF 0 |
(CLear DF — очистка DF) |
|
|
|
|
|
Установить DF |
std |
DF 1 |
(SeT DF) |
|
|
Команда movs может пересылать слова или байты. размеру операнда соответствует мнемоника: movsb — для байтов, movsw — для слов (начиная с 386го процессора, появилась команда movsd — для двойных слов). Регистры SI и DI должны быть заранее настроены на нужные адреса (например так, как это сделано в нашей программе). Дадим формальное описание команды (для процессора 8086)
Переслать строку |
movs dst,src |
(DI) (SI) |
|
(movsb, movsw) |
модифицировать SI и DI |
(MOVe String) |
|
флаги не изменяются |
Расшифруем, что означает модификация содержимого SI и DI после выполнения операции копирования. Это перемещение указателей SI и DI на следующие элементы цепочек. Выполняется модификация в соответствии с таблицей.
|
|
DF = 0 (автоинкремент) |
|
DF = 1 (автодекремент) |
|
цепочка байтов |
si si+1, di di+1 |
|
si si–1, di di–1 |
|
|
цепочка слов |
si si+2, di di+2 |
|
si si–2, di di–2 |
|
|
Для процессора 80386 описание команды выглядит иначе. |
|
||||
Переслать строку |
|
movs dst,src |
(EDI) (ESI) |
|
|
|
(movsb, movsw, movsd) |
модифицировать ESI и EDI |
|
||
(MOVe String) |
|
флаги не изменяются |
|
||
|
|
|
|
|
|
|
|
DF = 0 (автоинкремент) |
|
DF = 1 (автодекремент) |
|
цепочка байтов |
esi esi+1, edi edi+1 |
|
esi esi–1, edi edi–1 |
|
|
цепочка слов |
esi esi+2, edi edi+2 |
|
esi esi–2, edi edi–2 |
|
|
цепочка двойных слов |
esi esi+4, edi edi+4 |
|
esi esi–4, edi edi–4 |
|
Итак, если DF сброшен, то цепочка проходится в направлении увеличения адреса, а если DF установлен — в направлении уменьшения адреса. Приращение указателя равно размеру элемента данных.
Осталось пояснить вариант movs dst,src. Ассемблер по описанию операндов в секции данных распознает какие цепочки пересылаются — байтовые или состоящие из слов (или из двойных слов для процессора 386), и в зависимо-
3
сти от этого генерирует команду movsb или movsw (или movsd). Проведем эксперимент: заменим в нашей программе movsb на movs mem2, mem1. Трансляция закончится неудачей:
Can't override ES segment (нельзя переопределять сегмент ES).
Точнее, нельзя указывать сегментный регистр для приемника, отличный от ES. Дело в том, что Ассемблеру мало того, что мы загрузили сегментные регистры DS и ES. Ему еще нужно директивное указание, с какими логическими сегментами связаны DS и ES. Для этого предназначена директива ASSUME (предполагать):
ASSUME es:@data
Теперь ES ассоциирован с DGROUP и Ассемблер убежден, что операндприемник в команде movs расположен правильно — в дополнительном сегменте. (Возникает вопрос: а почему мы не включили аналогичные директивы для других сегментных регистров? За нас это сделала директива определения модели памяти .MODEL small и упрощенные сегментные директивы.)
Есть еще одно решение для преодоления ошибки трансляции — явно указать в команде префикс замены сегмента: movs es:mem2,mem1. Тогда можно обойтись без директивы ASSUME.
Но лучше использовать строковые команды с явным указанием размера операндов: movsb, movsw, movsd.
22.2. Префикс повторения.
Программу можно еще улучшить. Для повышения эффективности использования строковых команд введен префикс повторения rep.
Повторять примитив |
|
rep примитив |
CX CX–1 пока CX 0 |
(REPetition — повторение) |
|
|
Проведем последнее изменение нашей программы. Вместо команд
L:movsb loop L
поместим одну команду rep movsb
Время выполнения резко сокращается. Приведем расчеты для процессора 8086. В первом варианте тратится 18 + 17 = 35 на итерацию. Во втором: 9 + 17 тактов для первой итерации и 17 тактов на остальные.
Если CX = 0, то строковая команда с префиксом повторения не выполняется ни разу. (Здесь отличие от команды loop. Если перед выполнением этой команды CX = 0, то цикл выполняется 0FFFFh раз).
Выполнение команды с префиксом повторения имеет свои особенности при работе с Turbo Debugger. Если вы нажимаете клавишу F8 (Step), то rep movsb будет выполнена как одна команда, а если нажимаете F7, то команда movsb будет последовательно выполнена CX раз. Попробуйте!
22.3. Использование флага направления.
На первый взгляд флаг направления не нужен. В программах обработки массивов, которые мы писали до сих пор, мы всегда перемещали указатели в сторону возрастания адресов. Но вот пример, где такой подход не приведет к успеху.
4
Пример. Дана область памяти m DB 1,2,3,4,0,0,0,0.Скопировать подстроку из первых четырех элементов на две позиции вправо (т.е. в итоге нужно получить m DB 1,2,1,2,3,4,0,0)
Если мы начнем перемещать по элементу от начала к концу, то получим следующее
исходное состояние |
1, 2, 3, 4, 0, 0, 0, 0 |
1-й шаг |
1, 2, 1, 4, 0, 0, 0, 0 |
2-й шаг |
1, 2, 1, 2, 0, 0, 0, 0 |
3-й шаг |
1, 2, 1, 2, 1, 0, 0, 0 |
4-й шаг |
1, 2, 1, 2, 1, 2, 0, 0 |
Понятно, что произошло? На первом шаге третий элемент исходной цепочки необратимо изменился, и дальнейшие действия привели к неправильному результату. Поэтому нужно было сначала скопировать последний элемент затем предпоследний и т.д., то есть перемещаться в направлении уменьшения адресов. Решение дается следующей программой
mov si, OFFSET m + 4 mov di, OFFSET m + 6 mov cx, 5
std
rep movsb
Пример. Напишем подпрограмму, которая производит перемещение данных, предварительно выясняя направление. Пусть ES = DS. Напишем подпрограмму и макрос для ее вызова.
copstr PROC
cld |
; прямое направление |
|
cmp |
si,di |
; сравнение адресов цепочек |
je r |
|
; Если совпадают — на r |
ja move |
; Источник выше |
|
std |
|
; Обратное направление |
add |
si,cx |
; |
dec |
si |
; SI указывает на последний элемент |
add |
di,cx |
|
dec |
di |
; DI указывает на последний элемент |
move:rep |
movsb |
|
r: ret |
|
|
copstr ENDP
Заметим, что сравнение SI и DI беззнаковое (сравниваем адреса). Макрос для вызова copstr:
copy MACRO src, dst, len mov si, OFFSET src mov di, OFFSET dst mov cx, len
call copstr
ENDM
Для нашего первого примера, с которого мы начали изучать строковые команды, вызов макроса имеет вид copy mem1, mem2, mem1len.
22.4. Команды пересылки.
5
Помимо movs для пересылки цепочек используются еще две команды. С их помощью очередной элемент цепочки загружается в аккумулятор (AL — для байтов, AX — для слов, EAX — для двойных слов) или читается из аккумулятора. Это дает возможность преобразования элемента, пока он находится в аккумуляторе.
Загрузить элемент строки |
lods |
src |
ac (SI) |
|
(lodsb, |
lodsw) |
модифицировать SI |
(LOaD String) |
|
|
флаги не изменяются |
|
|
|
|
Заполнить элемент строки |
stos |
dst |
(DI) ac |
|
(stosb, |
stosw) |
модифицировать DI |
(STOre String) |
|
|
флаги не изменяются |
По аналогии с командой movs должно быть ясно: SI ассоциирован с DS, а DI с ES. В командах lodsb и stosb индексный регистр меняется на 1 (на +1, если DF = 0, и на –1, если DF = 1), а в командах lodsw и stosw — на 2. Соответственно, ac — это AL или AX. (Для процессора 386 добавляются команды lodsd и stosd).
Мы видим, что одна команда movs dst, src эквивалентна по действию двум командам: lods src и stos dst. Но зато между этими командами можно поместить команды изменения содержимого аккумулятора.
Перед этими командами можно использовать префикс повторения, хотя для команды lods он бесполезен. А вот с помощью команды rep stos удобно заполнять область памяти.
Пример. Обнулить двести байт, начиная с адреса mem. EVENDATA ; директива выравнивания по четному адресу mem DB 200 DUP(?)
...
mov ax, @data mov es, ax
mov di, OFFSET mem mov cx, 100
xor ax, ax rep stosw
Этот фрагмент выполняется вдвое быстрее, чем если бы мы повторили 200 раз команду stosb.
22.5. Неожиданное применение строковых команд — определение типа процессора.
Бывают случаи, когда программа должна определить тип процессора, на котором она работает (ну, например, программы диагностики оборудования). Тип процессора можно определить только по косвенным признакам. Серия программ, различающих процессоры (от 8086 до Pentium) приведена в книге Правиков "Ключевые дискеты". Эти программы, как правило, анализируют новые флаги, появляющиеся в регистре флагов с увеличением номера процессора. В Pentium воявилась команда CPUID, с возможностями которой мы ознакомимся позже, и необходимость в специальных программах отпала.
Рассмотрим задачу различения процессоров 8086 и 8088 (хотя сейчас это имеет лишь исторический интерес). Система команд у процессоров одна и та
6
же. Одинаков формат регистра Flags (на различиях в этом регистре основаны программы диагностики для старших моделей процессоров). Измерять быстродействие обращения к внешней шине (у 8086 она 16-разрядная, у 8088 — 8- разрядная) — дело безнадежное. Но можно воспользоваться косвенным признаком — длиной очереди команд. Очередь команд в шинном интерфейсе занимает у 8086 — 6 байтов, а у 8088 — 4 байта. Идея алгоритма: программа изменяет свой код одной командой rep stosb. При этом код команд, находящихся в
очереди, изменить невозможно.
...
.DATA
len DB ?, '$' ; строка для вывода длины конвейера inc_bx DB 43h ; код команды inc bx,
; получен с помощью debug
.CODE |
|
|
|
|
|
start: mov ax, @data |
|
|
|
||
|
mov ds,ax |
|
|
|
|
|
xor bx,bx |
; В BX - длина очереди |
|
|
|
|
cld |
|
|
|
|
|
mov di, OFFSET cs:_nop |
|
|
|
|
|
mov ax, cs |
|
|
|
|
|
mov es, ax ; Настроим ES на кодовый сегмент |
|
|||
|
mov al, inc_bx |
|
|
|
|
|
mov cx,6 ; попытаемся изменить 6 байтов |
|
|||
|
cli ; запрет внешних прерываний |
|
|
||
|
rep stosb |
|
|
|
|
_nop: ; Здесь поместим 6 команд nop |
|
|
|||
REPT 6 |
|
|
|
|
|
|
nop |
|
|
|
|
ENDM |
|
|
|
|
|
|
sti ; разрешить прерывания |
|
|
||
|
mov ax,6 |
|
|
|
|
|
sub ax, bx ; Получить длину очереди |
|
|||
|
add al, '0' ; Сформировать код цифры |
|
|||
|
mov len, al |
|
|
|
|
|
message len |
|
|
|
|
... |
|
|
|
|
|
При запуске программы на 8088 на экран будет выведено число 4 — длина |
|||||
очереди, т.к. команды после метки _nop приняли вид |
|
|
|||
nop |
nop |
nop |
nop |
inc bx |
inc bx |
очередь Команды, находящиеся в очереди, не претерпели изменений, а команды,
оставшиеся в ОЗУ, — изменились. Если выполнить эту программу на 8086 (и выше), то будет выведена длина очереди 6 (хотя на 80386 длина очереди составляет 15 байт).
Любопытно, что если запустить эту программу под управлением отладчика, то программа выведет на экран длину очереди 0! Дело в том, что в очереди теперь не команды программы, а команды отладчика.
Это наводит на мысль, что можно создавать программы, защищенные от трассировки отладчиком.
7
Пример. Фроловы БСП т.1 кн. 3
.DATA
s DB "Программа работает под управлением отладчика!",
CRLFT
...
cli
call Test sti
...
Test PROC near
mov byte ptr next, 90h ; код команды nop next: ret
message s ret
Test ENDP
22.6. Команды сравнения Эти две команды осуществляют сравнение элементов цепочек, не меняя
операндов. Удобство их применения — в автоматическом переходе к следующему элементу цепочки
Сравнить элементы строк |
cmps src,dst |
(SI) – (DI) |
|
|
|
(cmpsb, cmpsw) |
модифицировать SI и DI |
|
|
(CoMPare Strings) |
|
флаги сост. изменяются |
|
|
|
|
|
|
|
Сканировать элемент строки |
scas dst |
|
ac – (DI) |
|
|
(scasb, scasw) |
|
модифицировать DI |
|
(SCAn String) |
|
|
флаги сост. изменяются |
|
По результатам этих операций выставляются флаги, которые анализируются последующими командами. (Для процессора 386 добавляются команды cmpsd и scasd).
Пример. Заменить каждый элемент байтового массива его абсолютной величиной (в предположении, что в массиве нет элемента 80h).
.DATA
m DB 1,-2,3,2,1,-3 len_m = $ - m
.CODE
start:
mov ax, @data mov ds, ax mov es, ax cld
mov al,0
mov cx, len_m mov di, OFFSET m
p:scasb ; из 0 вычитаем элемент массива
jl n ; если результат отрицательный
; то исходный элемент положительный
neg byte ptr [di-1] ; меняем знак у отрицательного
;с учетом того, что указатель
;уже перемещен на следующий
8
; элемент
n:loop p
…
Применение префикса повторения rep для таких команд бессмысленно, так как можно будет воспользоваться результатом только самого последнего сравнения. Но есть еще два префикса повторения, специально предназначенные для этих команд.
Повторять примитив пока |
|
repe/repz примитив |
CX CX-1 |
пока |
равно (нуль) |
|
|
(CX 0 и ZF = 1) |
|
(REPetition if Equal/Zero) |
|
|
|
|
|
|
|
|
|
Повторять примитив пока |
|
repne/repnz примитив |
CX CX-1 |
пока |
не равно (не нуль) |
|
|
(CX 0 и ZF = 0) |
|
(REPetition if Not Equl or Zero) |
|
|
|
Пример. Определение длины строки. string DB "abc", 0
…
mov ax, @data mov es, ax cld
mov al,0 mov cx, -1
mov di, OFFSET string repne scasb
not cx dec cx
Проследим выполнение этого фрагмента, начиная с команды repne scasb.
a |
b |
c |
0 |
|
CX = FFFFh |
CX = FFFEh |
CX = FFFDh |
CX = FFFCh |
CX = FFFBh |
DI в начале |
|
|
|
DI в конце |
На схеме показано начальное и конечное положение указателя DI (в конечном положении DI содержит адрес байта, следующего за нулевым). Показано также текущее значение счетчика CX. Команда not cx инвертирует биты CX. В результате CX = 4. Команда dec cx корректирует это значение. В результате в CX длина строки (без учета терминатора строки — нулевого байта).
Пример. В двух строках одинаковой длины сосчитать количество несовпадающих элементов в позициях с одинаковыми номерами.
.MODEL small
.STACK 100h
.DATA
s1 DB "abcdesss" len_s1 = $ - s1 s2 DB "abcdfsss"
.CODE
start:
mov ax,@data mov ds,ax
9
mov es,ax cld
mov cx, len_s1
xor ax,ax ; Счетчик несовпадающих элементов mov si, OFFSET s1
mov di, OFFSET s2
comp: repe cmpsb ; Повторять, пока совпадение je fin ; Исчерпали строки — закончить
inc ax ; Нашли совпадение — увеличить счетчик or cx,cx; Строки исчерпаны?
jne comp ; Нет — продолжим сравнение fin: mov ax, 4C00h
int 21h
END start
Для строк, указанных в программе, количество несовпадений равно 1. Пример. В строке отыскать адрес последнего вхождения буквы 'z'.
string db "asdzbxzar",0
...
mov ax, @data mov es, ax cld
mov al,0
mov di, OFFSET string mov cx, -1
repne scasb ; Найти конец строки dec di ; Переместить указатель на 0
std ; Поиск в направлении уменьшения адресов mov al, 'z'
mov cx, -1 repne scasb
inc di ; Вернуть указатель на найденный символ
; В DI — адрес символа
...
Эта программа некорректна. Она правильно работает, если в строке заведомо имеется символ 'z'.
Упражнение. Переработать этот фрагмент так, чтобы программа корректно работала и при отсутствии символа 'z'.
Пример. В строке превратить прописные буквы в строчные.
.MODEL small
.STACK 100h
.DATA string DB "aSd1-E" lenstr = $ - string
.CODE start:mov ax,@data
mov ds,ax mov es,ax
mov si, OFFSET string mov di,si
mov cx,lenstr
10
cld
L:lodsb
cmp al,'A' jnae m
cmp al,'Z' jnbe m
add al, 'a'-'A'
m:stosb loop L
mov ax,4C00h int 21h
END start
Часть цикла можно было переписать эффективнее: add al, 'a'-'A'
stosb loop L
jmp short fin
m:inc di loop L
fin: mov ax,4C00h
|
Тогда не происходит лишнего обращения к памяти, а только перемещается |
|||||
указатель. |
|
|
|
|
|
|
|
А теперь пример на новые возможности 386-го процессора. |
|||||
|
Пример. Заполнить массив двойных слов числом 12345678h. Сразу посмот- |
|||||
рим листинг. |
|
|
|
|
|
|
1 |
0000 |
|
|
MODEL |
small |
|
2 |
|
|
|
.386 |
|
|
3 |
0000 |
|
|
STACK 100h |
||
4 |
0000 |
|
|
.DATA |
|
|
5 |
0000 |
0C*(00000000) |
arr |
DD |
12 DUP(0) |
|
6 |
0030 |
|
|
.CODE |
|
|
7 |
0000 |
B8 |
0000s |
start:mov ax,@data |
||
8 |
0003 |
8E |
D8 |
|
mov ds,ax |
|
9 |
0005 |
8E |
C0 |
|
mov es,ax |
|
10 |
0007 |
66| B8 12345678 |
|
mov eax,12345678h |
||
11 |
000D |
66| B9 0000000C |
|
mov ecx,12 |
||
12 |
0013 |
66| BF 00000000r |
mov edi,OFFSET arr |
|||
13 |
0019 |
FC |
|
|
cld |
|
14 |
001A |
F3> 66| AB |
|
rep stosd |
||
15 |
001D |
B8 |
4C00 |
|
mov ax,4C00h |
|
16 |
0020 |
CD 21 |
|
int 21h |
||
17 |
|
|
|
|
END start |
Отметим , что в листинге для команды rep stosd имеется два префикса: 66 — префикс размера операнда, и F3 — префикс повторения.
И еще замечание, не имеющее отношения к строковым операциям. Вместо команды mov ecx,12 можно написать mov cx,12, а вместо mov edi,OFFSET arr — mov di,OFFSET arr. Убедимся, например, во втором утверждении. Заменим команду mov edi,OFFSET arr двумя командами:
mov edi, 0FFFF0000h mov di,OFFSET arr