Ассемблер под Windows для чайников. Часть 11

Продолжим изучение наидревнейшего и бессменного языка электронных машин. Сегодня пришла пора познакомиться поближе с командами обработки строк. Эти команды также обычно называют цепочечными командами, потому что с их помощью можно обрабатывать не только строки символов, но и цепочки любых данных. Цепочечные команды незаменимы при работе с массивами. Несмотря на их кажущуюся ограниченность, большинство высокоуровневых функций обработки массивов в конечном итоге преобразуются в машинные команды обработки строк. Если они преобразуются высокоуровневым компилятором автоматически, вы рискуете получить не самый лучший с точки зрения производительности код. Если же вы оформляете обработку массивов вручную, то имеете все шансы добиться максимального быстродействия обработки данных. Именно поэтому в наш век сверхскоростных процессоров и высокоуровневых языков движки самых крутых современных игр до сих пор частично или целиком пишутся на ассемблере.

Цепочечные команды работают с элементами строк размером в байт, слово или двойное слово. Адреса строковых элементов для таких команд всегда указываются косвенно. Адрес источника (от англ. Source Index) предварительно помещается в SI/ESI (16/32-битный режим). Адрес приемника (Destination Index) должен находиться в DI/EDI. Такая косвенная адресация разработана для того, чтобы строковые команды могли автоматически изменять адрес текущего элемента (после его обработки) на адрес следующего элемента, увеличивая адрес или уменьшая его на 1, 2 или 4 в зависимости от размера элемента строки (байт, слово или двойное слово). Будет ли адрес увеличен или уменьшен, зависит от состояния флага направления DF. Состоянием флага DF можно управлять с помощью команд CLD (сброс DF в ноль) и STD (установка DF в единицу). Если DF=0, то цепочечные команды будут обрабатывать строки слева направо, автоматически увеличивая индексные регистры SI/ESI и DI/EDI. Если DF=1, то обработка будет происходить в обратном порядке — содержимое индексных регистров будет каждый раз уменьшаться. Всего существует 7 основных команд обработки строк: MOVS, CMPS, SCAS, LODS, STOS, INS и OUTS. Каждая цепочечная команда имеет краткую форму, которая не нуждается в операндах и использует SI и/или DI в 16-битном режиме, а в 32-битном — ESI и/или EDI. При этом адрес сегмента, на который ссылаются SI или ESI (источник), по умолчанию считывается из DS, а DI или EDI (приемник) всегда ссылаются на данные сегмента, адрес которого хранится в ES. Обычно после загрузки приложения Windows регистры DS и ES и так указывают на сегмент данных, поэтому на практике зачастую достаточно поместить лишь эффективный адрес строки- источника в SI/ESI, а строки-приемника — в DI/EDI. Краткая форма цепочечной команды образуется добавлением к мнемонике команды буквы, определяющей размер элемента строки: B — байт, W — слово, D — двойное слово. При использовании полной формы команд необходимо указать размер элемента строки (byte/word/dword), а также операнды приемник и источник: ES:(E)DI, DS:(E)SI. Перед SI/ESI может быть указан любой сегментный префикс, но перед DI/EDI — исключительно ES.

Команда MOVS копирует элемент строки, адрес которого задан в SI/ESI в ячейку памяти, указанную в DI/EDI. Размер элемента может быть задан как byte, word или dword:
MOVS BYTE [DI],[SI]
MOVS WORD [ES:DI],[SS:SI]
MOVSD

Команда CMPS сравнивает элементы строк приемника и источника путем вычитания элемента строки приемника из элемента строки источника и устанавливает флаги AF, SF, ZF, PF, CF, OF в соответствии с результатом. Сами элементы при этом не изменяются. Если сравниваемые элементы равны, то устанавливается флаг ZF, иначе ZF сбрасывается в ноль. Заметьте, что первым операндом команды CMPS должен быть источник (SI/ESI), а вторым — приемник (DI/EDI):
CMPSB
CMPS WORD [DS:SI],[ES,DI]
CMPS DWORD [FS:ESI],[EDI]

Команда SCAS производит сканирование строки с целью поиска заданного значения. Искомое значение необходимо предварительно поместить в AL/AX/EAX в зависимости от размера искомого элемента. SCAS вычитает элемент строки приемника из AL/AX/EAX и устанавливает флаги AF, SF, ZF, PF, CF, OF в соответствии с результатом. Если сравниваемые элементы равны, то устанавливается флаг ZF, иначе он сбрасывается в ноль. Единственный операнд — элемент сканируемой строки, указанный при помощи DI/EDI:
SCAS BYTE [ES:DI]
SCASW
SCAS DWORD [ES:EDI]

Команда LODS загружает элемент строки источника в регистр AL/AX/EAX. Операнд, содержащий адрес загружаемого элемента, может быть регистром SI/ESI с любым сегментным префиксом:
LODS BYTE [DS:SI]
LODS WORD [CS:SI]
LODSD

Команда STOS сохраняет содержимое AL/AX/EAX в элемент строки приемника. Правила для операнда такие же, как и в команде SCAS.
Команда INS вводит элемент строки из порта. Номер порта должен содержаться в регистре DX. Операнд-приемник указывается в DI/EDI:
INSB
INS WORD [ES:DI],DX
INS DWORD [EDI],DX

Команда OUTS передает содержимое элемента строки источника в порт вывода, указанный в DX. Операнд-приемник — DX, операнд-источник — SI/ESI: OUTS DX,BYTE [SI]
OUTSW
OUTS DX,DWORD [GS:ESI]

Для автоматической обработки строк всеми вышеперечисленными командами используются префиксы повторения: REP, REPE/REPZ и REPNE/REPNZ. Перед использованием команды с префиксом повторения необходимо поместить в CX/ECX число повторений команды. Префикс повторения автоматически уменьшает регистр CX/ECX и повторяет выполняемую команду до тех пор, пока CX/ECX не будет равен нулю. REPE/REPZ и REPNE/REPNZ прекращают повторение команды не только при CX/ECX=0, но и в зависимости от состояния флага ZF: REPE/REPZ может повторяться лишь до тех пор, пока ZF установлен, а REPNE/REPNZ — пока ZF=0. Повторяемая команда обработки строк будет обрабатывать каждый раз следующий элемент строки.

Что ж, посмотрим как можно использовать некоторые цепочечные команды на практике. В прошлой части мы искали пробел в строке, циклически выполняя команду cmp при помощи команды loop. Сейчас мы можем пойти более правильным путем:

include 'win32ax.inc'

.data
stroka db 'Ищем пробел в этой строке',0
dlina_stroki = $-stroka
msg_no_spc db 'Нет пробелов',0
msg_spc db 'Первый пробел — символ № '
msg_spc_2 db 0,0,0

.code
start:

cld
mov ecx,dlina_stroki
lea edi,[stroka]
mov al,' '
repne scasb

jecxz net_probelov
mov eax,edi
sub eax,stroka
aam
or ax,3030h
xchg al,ah
mov word [msg_spc_2],ax
invoke MessageBox,0,msg_spc,0,0
jmp exit

net_probelov:
invoke MessageBox,0,msg_no_spc,0,0
exit:
invoke ExitProcess,0

.end start

Командой CLD обнуляем флаг DF, чтобы обрабатывать элементы строки слева направо. Задвигаем в ECX длину строки. Загружаем в EDI эффективный адрес строки. В AL помещаем значение символа "пробел". Префикс REPNE заставит команду SCASB повторяться, пока результатом сравнения элемента строки с содержимым AL будет Not Equal (Не Равно). Или пока автоматически уменьшаемое содержимое ECX не сравняется с нулем после проверки последнего символа строки. Если пробел будет найден, то в EDI будет содержаться смещение следующего за ним символа. Однако это будет смещение относительно начала сегмента, а не относительно начала строки. В нашем случае начало строки и начало сегмента совпадают, потому что наша строка самая первая в секции данных. Но мы должны предусматривать все варианты, и потому после копирования в EAX значения из EDI необходимо отнять от этого значения относительный адрес начала строки: sub eax,stroka. Тонкости приведения результата к символьному виду были описаны в прошлый раз. Теперь посмотрим, что мы сможем сделать интересного с этими командами. Как вам идея бегущей строки в заголовке окна? Раз строка — значит, обрабатывать ее надо командой обработки строк. Как проще всего представить реализацию бегущей строки? Правильно: это повторяющийся цикл, на каждом повторении которого происходит перенос одного крайнего символа в другой конец строки. Как будто вы сложили текст из детских кубиков и по одному переставляете кубики из одного конца строки в другой. Памяти под содержимое строки у нас выделяется ровно столько, сколько символов в строке. Поэтому представим, что кубики лежат в узкой длинной коробке, длина которой совпадает с длиной строки кубиков. В таком случае для циклического сдвига строки влево последовательность наших действий будет следующая: взять крайний кубик, сдвинуть кубики влево, положить взятый кубик на освободившееся место справа. Вот и весь алгоритм. Берем кубик — тьфу, символ — в регистр командой mov, сдвигаем строку влево командой movsb с префиксом rep, кладем символ обратно с другого конца строки:


stroka db 'Бегущая строка !!! ',0
strlen = $-stroka-1

cld
mov ecx,strlen-1
lea esi,[stroka+1]
lea edi,[stroka]
mov byte al,[edi]
rep movsb
mov byte [edi],al


Этот кусок кода сдвигает строку влево на один символ, перемещая первый символ строки на место последнего. Чтобы не сдвигать завершающий строку нуль-терминатор, при определении strlen добавляем минус один. А в ECX помещаем еще на единицу меньше, так как один символ будет обработан отдельно через регистр AL. В приемник загружаем адрес начала строки, а в источник — адрес второго символа (stroka+1). Перед запуском копирования цепочки байтов сохраняем первый символ в AL. После копирования извлекаем его по адресу, содержащемуся в EDI, — это будет уже адрес последнего символа строки. Для реализации бегущей строки в заголовке окна нам недостает организации цикличного повторения кода сдвига строки, установки измененной строки новым текстом заголовка окна и, наконец, кода самого окна. С последним проще: в качестве основы программы возьмите TEMPLATE.ASM из папки ..\FASM\EXAMPLES\TEMPLATE\. Желательно работать с копией файла, потому что оригинал еще может пригодиться. Вставьте в секцию данных первые две строчки вышеприведенного кода. А вот со вставкой остальной части в секцию кода пока повремените. Еще раз вспомним наши условия. Требуется, чтобы код, производящий циклический сдвиг строки, повторялся через определенный промежуток времени, а после каждого сдвига строка устанавливалась новым текстом заголовка. Для выполнения каких-либо действий в оконном приложении Windows через определенные промежутки времени в операционной системе предусмотрена достаточно полезная API-функция SetTimer. Параметры у нее следующие:

1. Дескриптор окна, которое будет получать сообщения таймера. Окно должно принадлежать вызывающему функцию процессу.
2. Отличный от нуля идентификатор таймера. Если первый параметр не указан, то и этот параметр игнорируется.
3. Время задержки в миллисекундах, через которое будут приходить сообщения.
4. Указатель на процедуру обработки сообщений таймера в приложении. Если указать ноль, то сообщения WM_TIMER будут приходить в общую процедуру обработки сообщений.

Короче, чтобы кинуть таймер на нашу прогу, дописываем сразу после создания основного окна строку запуска таймера. После успешного создания окна его дескриптор должен быть в EAX, поэтому сие будет выглядеть примерно так:

invoke CreateWindowEx,0,_class,_title,WS_VISIBLE+WS_DLGFRAME+WS_SYSMENU,
128,128,256,192,NULL,NULL,[wc.hInstance],NULL
test eax,eax
jz error
invoke SetTimer,eax,1,100,0


Добавляем обработку сообщений от таймера в процедуру обработки сообщений, посланных окну, и обработчик:

cmp [wmsg],WM_TIMER
je .wmtimer

.wmtimer:
;сюда вставляем код, который будет выполняться по получению сообщения от таймера
jmp .finish


Теперь остается вставить код сдвига строки в обработчик сообщений от таймера, добавив после сдвига строку:
invoke SetWindowText,[hwnd],stroka
для установки измененной строки новым текстом заголовка окна. Ах, да, чуть не забыл: в самом начале шаблона программы замените include 'win32w.inc' на include 'win32a.inc'. W — это юникод, A — это ANSI. Мы же считали, что каждый символ будет занимать один байт, а не два, как в юникоде. Хотя при желании можно внести некоторые изменения и корректно обрабатывать строку в юникоде:

strlen = ($-stroka)/2-1

cld
mov ecx,strlen-1
lea esi,[stroka+2]
lea edi,[stroka]
mov word ax,[edi]
rep movsw
mov word [edi],ax


В этом случае не забудьте подключить кодировку 1251:
include 'encoding\WIN1251.INC'

Чтобы получить длину строки в символах, а не в байтах, делим длину в байтах на два. В ECX помещается количество символов для обработки, поэтому ничего не меняем. В приемник, как и в примере с ANSI, загружается тот же адрес. А вот в источник грузим stroke+2, потому что каждый символ занимает 2 байта. Первый символ, естественно, помещаем в двухбайтный AX. Сдвиг производим командой MOVSW.

Ну что, тяжело в учении? Согласен, тяжело. А кому сейчас легко? Перечитайте материал еще раз, и двигаемся дальше. Сделаем нашу строку не только бегущей, но и прыгающей. Добавьте в ANSI-версию программы бегущей строки перед вызовом функции SetWindowText следущие строки:

mov ecx,strlen
lea edi,[stroka]
.cycl:
.if byte [edi]>='A' & byte [edi]<='Z'
add byte [edi],('a'-'A')
.elseif byte [edi]>='a' & byte [edi]<='z'
add byte [edi],('A'-'a')
.elseif byte [edi]>='А' & byte [edi]<='Я'
add byte [edi],('а'-'А')
.elseif byte [edi]>='а' & byte [edi]<='я'
add byte [edi],('А'-'а')
.endif
add edi,1
loop .cycl


Не забудьте подключить описания макросов IF:
include 'macro/if.inc'

Теперь объясню, что делает этот код. Макрос .if (если) генерирует код для проверки простых условий и выполнения следующих за макроинструкцией команд в зависимости от результата. Блок условно выполняющихся команд обязательно должен заканчиваться инструкцией .endif (конец блока если). Перед ней могут быть использованы инструкции .elseif (иначе если) для обозначения кода, выполняемого при истинности условия, если ранее уже не была встречена истина. Также последний блок условно выполняемого кода перед .endif может быть обозначен инструкцией .else. Этот блок будет исполнен, если ни одно из условий не выполнено. Итак, для того, чтобы строка "прыгала", необходимо менять буквы верхнего регистра на нижний, а нижнего — на верхний. Числовые значения, обозначающие буквы в ANSI-кодировке ASCII идут по порядку от латинского 'A'(41h) до 'Z'(5Ah), от латинского 'a'(61h) до 'z'(7Ah). Подобным образом дело обстоит и с русскими буквами. Поэтому, если значение символа больше или равно латинскому 'A' и при этом меньше или равно 'Z', то мы можем утверждать, что это заглавная буква латинского алфавита. По аналогии мы определяем прописные латинские буквы и кириллические символы. Числовое значение символа 'a' больше значения 'A' ровно настолько же, насколько значение 'b' больше 'B', и т.д. Так что для изменения регистра латинской буквы с верхнего на нижний надо прибавить к значению эту разность. Для изменения регистра с нижнего на верхний — отнять или прибавить отрицательную разность. Теперь, думаю, вам стало ясно, каким образом можно изменить регистр символа. Заметьте, что, если символ не является буквой, то он и не подвергнется изменению. Ну и, естественно, проверка и изменение регистра производится столько раз, сколько мы указали через ECX. Чтобы сделать только прыгающую строку (без бегущей составляющей), вам надо удалить из программы код сдвига строки. Для получения более привлекательного эффекта напишите строку буквами с чередующимся регистром — например: "ПрЫгАюЩаЯ СтРоКа!!!". Можно оформить заголовок эффектом "бегущая буква". Для этого нам придется ввести переменную, которая будет хранить смещение "бегущей буквы" относительно начала строки. Чтобы обозначить 32-битную переменную и инициализировать ее значение нулем, допишите в секцию данных:
k dd 0

Теперь полностью замените обработчик сообщения таймера:

.wmtimer:
lea edi,[stroka]
add edi,[k]
mov byte bl,[edi]
.if byte [edi]>='A' & byte [edi]<='Z'
add byte [edi],('a'-'A')
.elseif byte [edi]>='a' & byte [edi]<='z'
add byte [edi],('A'-'a')
.elseif byte [edi]>='А' & byte [edi]<='Я'
add byte [edi],('а'-'А')
.elseif byte [edi]>='а' & byte [edi]<='я'
add byte [edi],('А'-'а')
.endif
invoke SetWindowText,[hwnd],stroka
mov byte [edi],bl
.if [k] < strlen-1
inc [k]
.else
mov [k],0
.endif
jmp .finish …

У нас получился достаточно простой алгоритм. При каждом сообщении таймера в EDI загружается адрес начала строки и увеличивается на содержимое K. Теперь, когда EDI содержит адрес обрабатываемого символа, мы временно копируем значение этого символа в BL. Далее — знакомая нам проверка и смена регистра буквы. Вывод измененной строки в заголовок. Возвращаем символ из BL, чтобы привести строку в памяти к изначальному виду. Увеличиваем K на единицу при условии, что K<strlen-1. Иначе — обнуление K. Почему сравниваем K со strlen-1? Потому, что K — это смещение, а не порядковый номер символа. Для первого символа K=0, для второго — K=1, для энного символа K будет равняться n-1. Здесь есть еще один важный момент, на котором я хотел бы заострить ваше внимание. После сохранения символа в регистр BL, но перед его восстановлением происходит вызов API- функции SetWindowText. Часто в подобных ситуациях начинающий программист забывает, что вызываемая API-функция обычно использует содержимое некоторых регистров под собственные нужды и изменяет их содержимое. Например, если бы для временного хранения значения символа мы использовали AL или CL вместо BL, результат был бы далек от требуемого. Следует помнить, что после использования API-функции windows сохраняются лишь регистры EBX, EBP, ESP, ESI, EDI. Остальные регистры придется сохранять вручную (команда PUSH) и восстанавливать (команда POP) после использования API-функции, если, конечно, их содержимое вас интересует в дальнейшем. Теперь можно немного упростить предыдущий эффект и получить эффект ручного ввода символов. Задача: отображать сначала один символ строки, потом — два, потом — три и т.д. Решение:

lea edi,[stroka]
add edi,[k]
mov byte bl,[edi]
mov byte [edi],0
invoke SetWindowText,[hwnd],stroka
mov byte [edi],bl
.if [k] < strlen
inc [k]
.else
mov [k],0
.endif
jmp .finish


Этот вариант похож на предыдущий, только вместо замены регистра символа мы просто временно записываем ноль на его место. Как известно, строка в windows завершается нулем. Строка, у которой первый символ имеет нулевое значение, считается и отображается пустой. Если мы поставим ноль, к примеру, вместо четвертого символа, то отображаемая строка будет состоять из трех символов. Поэтому на этот раз мы ставим условие [k] < strlen: последним обрабатываемым символом в этом алгоритме должен являться ноль-терминатор. Хотя он и так содержит ноль, но только так мы сможем в конце цикла отобразить строку целиком. Для большего эффекта можно добавить звуковое оформление:

.if [k] < strlen
inc [k]
invoke Beep,37,10
.else
mov [k],0
invoke Beep,370,50
.endif


Функция Beep генерирует звук на системном динамике. Первый параметр — частота воспроизводимого сигнала в герцах — может находиться в пределах от 37 до 32767. Второй пареметр — время звучания в миллисекундах. Будьте осторожны со вторым параметром, потому что функция не возвращает управление программе до завершения воспроизводимого звука. Если вы установите слишком большое время звучания сигнала, то не сможете закрыть программу до прекращения звука.

Ну, думаю, на сегодня с вас хватит. Надо же оставить вам немного свободной памяти под личные нужды: как зовут, где живешь, зачем учишь ассемблер? До новых встреч с новыми знаниями!
Все приводимые примеры были протестированы на правильность работы под Windows XP и, скорее всего, будут работать под другими версиями Windows, однако я не даю никаких гарантий их правильной работы на вашем компьютере. Исходные тексты программ вы можете найти на форуме: сайт .

BarMentaLisk, SASecurity gr., q@sa-sec.org


Компьютерная газета. Статья была опубликована в номере 33 за 2008 год в рубрике программирование

©1997-2024 Компьютерная газета