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

Сегодня мы продолжим знакомство с основными командами ассемблера. Программирование под Windows — это, конечно, замечательно, но чтобы программировать на ассемблере, необходимо знать команды ассемблера. Те, кому не по душе теория, могут не волноваться: она скоро закончится, и мы снова вернемся к практике. Помню, как мне не нравилась вся эта скучная теория, особенно когда самые непонятные моменты приходилось читать на английском. Но искусство требует жертв. Если вы хотите научиться искусству программирования на ассемблере — вам придется пожертвовать своей нетерпеливостью и, если не убить ее в себе, то хотя бы покалечить. Покажите ей, кто из вас хозяин: она, жаждущая тотчас же получать свой кусок "бесплатного сыра", или вы, знающий истинную цену жемчужин знания, и желающий на этот раз насобирать их целый мешок? Надеюсь, что хозяином своего ума оказались вы же, поэтому продолжим искать жемчуг на дне океана теории ассемблера.

Логические команды AND, OR, XOR, NOT были рассмотрены нами в 8-й части цикла. Добавлю лишь, что NOT не оказывает влияния на флаги, а AND, OR и XOR изменяют флаги SF, ZF, PF в соответствии с результатом.

BT источник,индекс
Команды BT, BTS, BTR, BTC оперируют с отдельным битом в памяти или регистре общего назначения (РОН). Эти команды сначала передают значение указанного бита флагу CF, чтобы далее можно было организовать условный переход посредством команд JC (перейти если CF=1) или JNC (перейти если CF=0). BT больше ничего и не делает, BTS устанавливает заданный бит в единицу, BTR сбрасывает бит в ноль, BTC изменяет значение бита на противоположное. Операндом-источником этих команд может быть слово или двойное слово. Индекс может быть РОН или непосредственным значением. Биты отсчитываются от младшего к старшему, то есть справа налево начиная с нулевого. Примеры:
bt ax,15 ;проверить старший бит в регистре
bts word[bx],15 ;проверить и установить бит в единицу
btr ax,cx ;проверить бит в регистре и сбросить его в ноль
btc word[bx],cx ;проверить бит в памяти и переключить его

BSF приемник,источник
BSF и BSR сканируют источник (слово или двойное слово) в поисках бита, установленного в единицу. Индекс первого найденного бита заносится в операнд-приемник, который должен быть РОН. Сканируемое битовое поле определяется операндом-источником и может быть РОН или памятью. Если все биты источника оказались нулевыми, устанавливается флаг ZF, иначе ZF сбрасывается в ноль. BSF сканирует биты от младшего к старшему, а BSR производит поиск в обратном порядке.

SHL источник,индекс
SHL сдвигает содержимое источника влево на количество бит, указанное в индексе. Источник может иметь размер в байт, слово, двойное слово и являться РОН или памятью. Индекс может быть непосредственным значением или регистром CL. С правой стороны вдвигаются нулевые биты, а последний выдвигаемый из источника бит становится значением флага CF. Команда SAL является синонимом SHL. Команды SHL, SAL являются удобным средством для быстрого умножения числа на степень двойки:
mov ax,17
shl ax,3 ;умножить 17 на 8 (2 в степени 3)
SHR и SAR сдвигают содержимое источника вправо на количество бит, указанное в индексе. Команды выполняют действие, аналогичное SHL/SAL, только при использовании SHR, с левой стороны вдвигаются нули, а SAR вдвигает слева знаковый старший бит операнда: ноль для положительного значения либо единицу для отрицательного. Поэтому команда SHR широко используется для деления целочисленных операндов на степень двойки без учета знака, а SAL — с учетом знака:
mov cl,2
shr ax,cl ;разделить содержимое ax на 4 (2 в степени 2)

SHLD приемник,источник,индекс
SHLD сдвигает влево биты операнда приемника, вдвигая справа биты операнда источника. Содержимое источника при этом не изменяется. Приемник может быть РОН или памятью размером в слово или двойное слово, источник должен быть РОН. Индекс, в котором указывается количество битов для сдвига, может быть непосредственным значением или регистром CL.

SHRD сдвигает биты приемника вправо, вдвигая в него слева младшие биты из источника. Правила те же, что и для команды SHLD.

ROL и RCL осуществляют циклический сдвиг битов операнда влево. Синтаксис и правила для операндов такие же, как у SHL. Отличия в том, что при каждом сдвиге выдвигаемый слева бит операнда вдвигается в него же справа (команда ROL). При использовании команды RCL выдвигаемый бит прежде, чем быть вдвинутым с другой стороны, попадает в CF и вдвигается обратно в операнд лишь на следующем шаге цикла.

ROR и RCR производят циклический сдвиг байта, слова или двойного слова вправо. Команды аналогичны командам ROL и RCL за исключением направления сдвига.
BSWAP изменяет порядок следования байтов в операнде, который должен быть 32-битным РОН. Биты 0-7 меняются местами с битами 24-31, а биты 8- 15 — с битами 16-23:
mov edx,1A2B3C4Dh
bswap edx ;edx=4D3C2B1Ah

Команды передачи управления

Хотя я и касался в прошлых выпусках нескольких команд условного и безусловного перехода, теперь необходимо эту информацию закрепить в уме окончательно и бесповоротно.
JMP передает управление в указанную точку программы безусловно. Целевой адрес может быть определен в команде непосредственно либо косвенно через регистр или память. На самом деле процессор узнает о том, какая команда должна выполняться следующей, по содержимому пары регистров CS:(E)IP, где CS — это адрес текущего сегмента кода, а EIP/IP — смещение относительно текущего сегмента. Для 16-битного режима используется 16- битный регистр IP, а для 32-битного — 32-битный EIP. Обычно при выполнении процессором какой-либо команды содержимое EIP/IP автоматически увеличивается на число байтов, занимаемое командой. Таким образом, после выполнения одной команды регистр EIP/IP будет содержать адрес следующей за ней команды. Команды передачи управления просто изменяют содержимое EIP/IP, а также содержимое CS, если переход выполняется в другой сегмент кода.

Команды передачи управления могут быть разного размера в зависимости от дальности перехода. Обычно компилятор самостоятельно выбирает тип перехода near (близкий внутрисегментный) или far (дальний межсегментный), но можно и принудительно определить его выбор, вставив между командой и операндом префикс near либо far. Следует помнить, что для 32-битного режима размер адреса для близкого перехода равен двойному слову (32 бита), так как для внутрисегментного перехода используется лишь EIP. Для дальнего перехода размер адреса удваивается, так как необходимо указывать не только смещение, но и базовый адрес сегмента кода. В 16-битном режиме размеры адресов для ближнего и дальнего переходов — соответственно 16 и 32 бита. Также может быть использован короткий тип перехода (short). Целевой адрес короткого перехода может находиться в пределах -128 — +127 байт относительно следующей за JMP команды. Зато команда короткого перехода занимает всего 2 байта: 1-й байт — сама команда, 2-й — значение смещения перехода. Поэтому компилятор старается выбирать короткий вариант перехода везде, где это возможно. Все это сразу может показаться очень сложным и непонятным, но для использования команд перехода вам вовсе не обязательно высчитывать адрес самостоятельно. Достаточно вставить метку в текст программы и при использовании команды перехода указать в качестве операнда имя этой метки. Каждая метка может быть объявлена лишь один раз в программе. Простейший способ объявления метки — поставить двоеточие сразу после ее имени — например, "metka1:". За таким объявлением метки может даже следовать очередная команда в той же строке, хотя для удобства чтения текста программы лучше объявлять метки в отдельных строках кода. Метка, имя которой начинается с точки, считается локальной меткой — например, ".local1:". Ее имя автоматически присоединяется к имени последней глобальной метки (без точки в начале имени), поэтому вы можете использовать короткое имя такой метки лишь до объявления следующей глобальной метки. Для доступа к локальной метке из других мест придется использовать полное имя: "jmp metka1.local1". Метка, имя которой начинается с двух точек, является исключением из правил — такая метка имеет свойства глобальной метки, но следующие за ней локальные метки к ней не привязываются. Существуют также и безымянные метки. Их в тексте программы может быть сколько угодно. Безымянная метка обозначается двумя собаками: "@@:". Переход к безымянной метке может быть осуществлен при помощи специальных символов. @B или @R указывают на ближайшую предшествующую безымянную метку. @F является указателем на ближайшую нижеследующую безымянную метку.

Команда CALL схожа с командой JMP, но позволяет впоследствии осуществить возврат из вызываемой подпрограммы в точку вызова. CALL передает управление процедуре (подпрограмме), сохраняя в стек адрес следующей за CALL команды. Позже командой RET можно вернуть управление на команду, следующую за командой CALL.
RET, RETN и RETF прекращают исполнение процедуры и возвращают управление в точку, откуда была вызвана процедура. RETN и RETF являются соответственно командами ближнего (внутрисегментного) и дальнего (межсегментного) возврата.

Условные переходы

Команды условного перехода осуществляют переход, если соблюдено условие:
— отношения между операндами со знаком (больше или меньше);
— отношения операндов без знака (выше или ниже);
— состояния флагов ZF, SF, CF, OF, PF.
Синтаксис команд условного перехода всегда одинаковый:

Jcc адрес_перехода
cc означает условие перехода — например, JZ передает управление на указанный адрес, если установлен флаг нуля ZF. В связи с тем, что процессор не различает числа с учетом знака и без его учета, условия "больше" и "меньше" относятся к сравнению чисел с учетом знакового бита, а условия "выше" и "ниже" являются аналогичными условиями для сравнения чисел без учета знака. Если условие команды соблюдается, то производится переход в указанную операндом точку программы, иначе выполняется следующая команда. Существующие команды условного перехода описаны в таблице.


КомандаФлагиУсловие перехода
JOOF=1Переполнение
JNOOF=0Нет переполнения
JB, JC, JNAECF=1Ниже, перенос
JAE, JNB, JNCCF=0Выше или равно, нет переноса
JE, JZZF=1Равно, ноль
JNE, JNZZF=0Не равно, не ноль
JBE, JNACF=1 или ZF=1Ниже или равно
JA, JNBECF=0 и ZF=0Выше
JSSF=1Отрицательное
JNSSF=0Положительное
JP, JPEPF=1Четное количество единичных битов
JNP, JPOPF=0Нечетное количество единичных битов
JL, JNGESF<>OFМеньше
JGE, JNLSF=OFБольше или равно
JLE, JNGZF=1 или SF<>OFМеньше или равно
JG, JNLEZF=0 и SF=OFБольше
JCXZ/JECXZCX/ECX = 0


Команды условного перехода могут выполнять короткий или близкий переход внутри сегмента, но не могут быть использованы для дальнего межсегментного перехода. При необходимости условного перехода в другой сегмент команду условного перехода комбинируют с командой JMP. Команды JCXZ/JECXZ, в отличие от других приведенных команд, выполняют переход в зависимости от содержимого регистра CX/ECX. Переход осуществляется только в случае, когда содержимое равно нолю. Еще одно отличие этих команд в том, что они могут выполнять только короткий переход. Для увеличения дальности перехода команды JCXZ/JECXZ комбинируют с командой JMP.

Раз уж мы рассмотрели команды условного перехода, скажу пару слов о команде условной установки байта SETcc. Здесь "cc" — то же самое, что и в командах Jcc, — условие. Если выполняется условие, то в операнд помещается единица, иначе — ноль. Условия могут быть такими же, что и в командах Jcc: SETO, SETNO, SETB и т.д., кроме CXZ/ECXZ. Операнд имеет размер в байт и может быть любым 8-битным РОН (AL, AH, BL, BH, CL, CH, DL, DH) или памятью. Например, команда SETZ AL поместит в AL единицу, если ZF=1, либо обнулит AL, если ZF=0.

Команда LOOP тоже является в какой-то мере командой условного перехода. Хотя изначально эта команда предназначена для организации циклов. LOOP уменьшает на единицу содержимое CX/ECX (в зависимости от разрядности режима 16/32 бит) и производит переход, если содержимое не равно нулю. Иначе, если значение в CX/ECX равно нулю, выполняется команда, следующая за LOOP. Таким образом, для организации цикла необходимо обозначить начало цикла меткой, а в конце цикла выполнить команду LOOP метка. Естественно, поместить число повторений цикла в CX/ECX необходимо до его начала. Команду LOOP можно заставить работать с 16-битным регистром CX даже в 32-битном режиме, если использовать мнемонику LOOPW. Для принудительного использования ECX независимо от режима существует мнемоника LOOPD. LOOPE и ее синоним LOOPZ отличаются от обычной LOOP лишь тем, что прекращают цикл не только при нулевом значении CX/ECX, но и при ZF=0. LOOPEW и LOOPZW служат для принудительного использования CX, а LOOPED и LOOPZD — для ECX. LOOPNE и LOOPNZ завершают цикл при установленном флаге ZF. Соответственно, LOOPNEW и LOOPNZW форсируют использование 16- битного CX, а LOOPNED и LOOPNZD — 32-битного ECX. Все вышеописанные подвиды команды LOOP могут осуществлять только короткий переход не более 128 байт назад либо 127 байт вперед от адреса команды, следующей за командой LOOP. Ввиду особой сложности восприятия принципов работы команды 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:

mov esi,-1
mov ecx,dlina_stroki

cycl:
inc esi
cmp [stroka+esi],' '
loopne cycl

jecxz net_probelov
mov eax,esi
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

В начале программы мы помещаем в ECX длину проверяемой строки. А в ESI необходимо поместить смещение первого символа относительно начала строки. Смещение первого символа — 0, но ввиду того, что в начале цикла перед сравнением мы будем каждый раз увеличивать это смещение на единицу для обработки очередного символа, необходимо заранее позаботиться о том, чтобы изначальное смещение было на единицу меньше, поэтому mov esi,-1. Далее следует собственно цикл проверки. Увеличиваем ESI, сравниваем содержимое ячейки памяти по адресу stroke+esi с непосредственным значением символа "пробел". Если сравнение выявит равенство, то будет установлен флаг ZF, иначе ZF=0. Поэтому мы используем команду LOOPNE, чтобы повторять команды на метке cycl до тех пор, пока результат сравнения NE (Not Equal — Не Равно) и, разумеется, до тех пор, пока ECX не будет равно нулю. Конечно, кажется более разумным запись команды inc esi после команды сравнения. Но команда inc влияет на флаг ZF, а нам нельзя допускать изменения флага ZF после команды сравнения до тех пор, пока результат сравнения не будет обработан командой условного перехода. Цикл поиска пробела может быть прекращен в двух случаях:
1 — найден пробел, и ESI содержит его смещение относительно начала строки;
2 — пробел не найден, ECX=0, ESI содержит смещение последнего символа — нуль-терминатора в нашем примере.

Поэтому по завершению цикла мы совершаем условный переход командой JECXZ в случае равенства ECX нулю. Иначе — выполняем преобразование значения смещения в символьный вид, копируем его в два заранее приготовленных байта на метке msg_spc_2 (третий байт — нуль-терминатор для строки вывода) и выводим сообщение. Команда AAM, если вы помните, преобразует двоичное содержимое AX в формат неупакованного BCD-числа. Если у нас пробел, к примеру, после 12-го символа, то неупакованный BCD-эквивалент результата в AX будет 0102h. Для того, чтобы получить символьный формат (3132h), мы используем команду OR AX,3030h. Теперь остается только обменять местами AL и AH, потому что записанное в AX 3132h будет читаться из регистра с младшего байта — справа налево, а записываться в память — слева направо. Иначе в сообщении вместо 12 мы увидим 21. Оператор word после команды mov необходим потому, что метка msg_spc_2 у нас объявляет данные в байтах (db). Если мы не заставим компилятор сгенерировать инструкцию для копирования слова (word), он будет пытаться сгенерировать инструкцию копирования байта (byte) и сообщать нам об ошибке несоответствия размерности операндов, потому что AX явно не является однобайтовым регистром. Компилятор FASM всегда старается генерировать инструкции минимального размера, поэтому иногда необходимо указывать после команды требуемый размер операндов операторами word или dword. Возможно, самые забывчивые из вас зададутся вопросом, почему пробел является 13-м символом, а в ESI получилось число 12? Напоминаю: люди нумеруют элементы начиная с единицы, а компьютеры отсчитывают смещение от нулевого. Поэтому первый "человеческий" символ с компьютерной точки зрения всегда является нулевым, второй — первым… тринадцатый — двенадцатым. Таков закон процессоров: ноль — положительное число, и именно с него начинается отсчет. Поэтому мы можем после цикла добавить к ESI единичку и написать, что пробел является символом №_, или, ничего не прибавляя, написать, что пробел следует за символом №_, как это сделано в моем примере. Но никогда не забывайте про эту разницу в единицу между человеческим представлением и компьютерным. Многие начинающие программисты долго не могут обнаружить ошибку в своих программах, если забудут, что процессор "загибает свои пальцы", начиная с нулевого.

Стоит признать, что это не самый лучший вариант программы поиска пробела. А дело в том, что мы еще не знакомы с командами обработки строк. Эти команды также обычно называют цепочечными командами, но познакомимся мы с ними в следующий раз. Пока что впитывайте этот материал. Помните: "Тяжело в учении — легко в бою!" Не надейтесь, что после беглого просмотра материала вам все сразу станет ясно. Ассемблер — король всех языков программирования. Чтобы его изучить, надо приложить усилие.

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


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

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