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

4.3.1. Команда LOOP

Команда LOOP позволяет организовать цикл с известным числом повторений:

mov ecx, n

L: ...

...

loop L

Команда LOOP требует, чтобы в качестве счётчика цикла использовался регистр ECX. Собственно, команда LOOP вычитает единицу именно из этого регистра, сравнивает полученное значение с нулём и осуществляет переход на указанную метку, если значение в регистре ECX больше 0. Метка определяет смещение перехода, которое не может превышать 128 байт.

При использовании команды LOOP следует также учитывать, что с её помощью реализуется цикл с постусловием, следовательно, тело цикла выполняется хотя бы один раз. Хуже того, если до начала цикла записать в регистр ECX значение 0, то при вычитании единицы, которое выполняется до сравнения с нулём, в регистре ECX окажется ненулевое значение, и цикл будет выполняться 232 раз.

Команда LOOP не относится к самым быстрым командам. В большинстве случаев её можно заменить последовательностью других команд.

4.3.2. Команды LOOPE / LOOPZ и LOOPNE / LOOPNZ

Эти команды похожи на команду LOOP, но позволяют также организовать и досрочный выход из цикла.

LOOPE <метка>; Команды являются синонимами

LOOPZ <метка>

Действие этой команды можно описать следующим образом:ECX = ECX - 1; if (ECX!= 0 && ZF == 1) goto <метка>;

До начала цикла в регистр ECX необходимо записать число повторений цикла. Команда LOOPE / LOOPZ, как и команда LOOP ставится в конце цикла, а перед ней помещается команда, которая меняет флаг ZF (обычно это команда сравнения CMP). Команда LOOPE / LOOPZ заставляет цикл повторяться ECX раз, но только если предыдущая команда фиксирует равенство сравниваемых величин (вырабатывает нулевой результат, т.е.ZF = 1).

По какой именно причине произошёл выход из цикла надо проверять после цикла. Причём надо проверять флаг ZF, а не регистр ECX, т.к. условие ZF = 0 может появиться как раз на последнем шаге цикла, когда и регистр ECX стал нулевым.

Команда LOOPNE / LOOPNZ аналогична команде LOOPE / LOOPZ, но досрочный выход из цикла осуществляется, если ZF = 1.

Рассмотрим пример: пусть в регистре ESI находится адрес начала некоторого массива двойных слов, а в переменной n – количество элементов массива, требуется проверить наличие в массиве элементов, кратных заданному числу x, и занести в переменную f значение 1, если такие элементы есть, и 0 в противном случае.

mov ebx, x

mov ecx, n

mov f, 1

L1: mov eax, [esi]

add esi, 4

cdq

idiv ebx

cmp edx, 0

loopne L1

je L2

mov f, 0

L2:

Массивы

Модификация адресов

Как уже было сказано, массивы в языке ассемблера описываются по директивам определения данных с использованием конструкции повторения (см. раздел 2.6). Для того чтобы обратиться к элементу массива, необходимо так или иначе указать адрес начала массива и смещение элемента в массиве. Смещение первого элемента массива всегда равно 0. Смещения остальных элементов массива зависят от размера элементов.

Пусть X – некий массив. Тогда адрес элемента массива можно вычислить по следующей формуле:

адрес(X[i]) = X + (type X) * i, где i – номер элемента массива, начинающийся с 0

Напомним, что имя переменной эквивалентно её адресу (для массива – адресу начала массива), а операция type определяет размер переменной (для массива определяется размер элемента массива в соответствии с использованной директивой).

Для удобства в языке ассемблера введена операция модификации адреса, которая схожа с индексным выражением в языках высокого уровня – к имени массива надо приписать целочисленное выражение или имя регистра в квадратных скобках:

x[4]

x[ebx]

Однако принципиальное отличие состоит в том, в программе на языке высокого уровня мы указываем индекс элемента массива, а компилятор умножает его на размер элемента массива, получая смещение элемента массива. В программе на языке ассемблера указывается именно смещение, т.е. программист должен сам учитывать размер элемента массива. Компилятор же языка ассемблера просто прибавляет смещение к указанному адресу. Приведённые выше команды можно записать по-другому:

x + 4

[x + 4]

[x] + [4]

[x][4]

[x + ebx]

[x] + [ebx]

[x][ebx]

Обратите внимание, что при использовании регистра для модификации адреса наличие квадратных скобок обязательно. В противном случае компилятор зафиксирует ошибку.

Адрес может вычисляться и по более сложной схеме:

<база> + <множитель> * <индекс> + <смещение>

База – это регистр или имя переменной. Индекс должен быть записан в некотором регистре. Множитель – это константа 1 (можно опустить), 2, 4 или 8. Смещение – целое положительное или отрицательное число.

mov eax, [ebx + 4 * ecx - 32]

mov eax, [x + 2 * ecx]

5.2. Команда LEA

Команда LEA осуществляет загрузку в регистр так называемого эффективного адреса:

LEA <регистр>, <ячейка памяти>

Команда не меняет флаги. В простейшем случае с помощью команды LEA можно загрузить в регистр адрес переменной или начала массива:

x dd 100 dup (0)

lea ebx, x

Однако поскольку адрес может быть вычислен с использованием операций сложения и умножения, команда LEA имеет также ряд других применений (см. раздел 8.3.2).

Обработка массивов

Пусть есть массив x и переменная n, хранящая количество элементов этого массива.

x dd 100 dup(?)

n dd?

Для обработки массива можно использовать несколько способов.

1. В регистре можно хранить смещение элемента массива.

mov eax, 0

mov ecx, n

mov ebx, 0

L: add eax, x[ebx]

add ebx, type x

dec ecx

cmp ecx, 0

jne L

2. В регистре можно хранить номер элемента массива и умножать его на размер элемента.

mov eax, 0

mov ecx, n

L: dec ecx

add eax, x[ecx * type x]

cmp ecx, 0

jne L

3. В регистре можно хранить адрес элемента массива. Адрес начала массива можно записать в регистр с помощью команды LEA.

mov eax, 0

mov ecx, n

lea ebx, x

L: add eax, [ebx]

add ebx, type x

dec ecx

cmp ecx, 0

jne L

4. При необходимости можно в один регистр записать адрес начала массива, а в другой – номер или смещение элемента массива.

mov eax, 0

mov ecx, n

lea ebx, x

L: dec ecx

add eax, [ebx + ecx * type x]

cmp ecx, 0

jne L

Модификацию адреса можно производить также по двум регистрам: x[ebx][esi]. Это может быть удобно при работе со структурами данных, которые рассматриваются как матрицы. Рассмотрим для примера подсчёт количества строк матриц с положительной суммой элементов.

mov esi, 0; Начальное смещение строки

mov ebx, 0; EBX будет содержать количество строк, удовлетворяющих условию

mov ecx, m; Загружаем в ECX количество строк

L1: mov edi, 0; Начальное смещение элемента в строке

mov eax, 0; EAX будет содержать сумму элементов строки

mov edx, n; Загружаем в EDX количество элементов в строке

L2: add eax, y[esi][edi]; Прибавляем к EAX элемент массива

add edi, type y; Прибавляем к смещению элемента в строке размер элемента

dec edx; Уменьшаем на 1 счётчик внутреннего цикла

cmp edx, 0; Сравниваем EDX с нулём

jne L2; Если EDX не равно 0, то переходим к началу цикла

cmp eax, 0; После цикла сравниваем сумму элементов строки с нулём

jle L3; Если сумма меньше или равна 0, то обходим увеличение EBX

inc ebx; Если же сумму больше 0, то увеличиваем EBX

L3: mov eax, n; Загружаем в EAX количество элементов в строке

imul eax, type y; Умножаем количество элементов в строке на размер элемента

add esi, eax; Прибавляем к смещению полученный размер строки

dec ecx; Уменьшаем на 1 счётчик внешнего цикла

cmp ecx, 0; Сравниваем ECX с нулём

jne L1; Если ECX не равно 0, то переходим к началу цикла

Поразрядные операции

Поразрядные операции реализуют одну и ту же логическую операцию над всеми битами переменной. К поразрядным операциям относят также операции сдвига.

Логические команды

Операция отрицания меняет значение всех битов переменной на противоположное. Операция имеет один операнд, который может быть регистром или ячейкой памяти. Операция не меняет флаги.

NOT <операнд>

Операция поразрядное «и» выполняет логическое умножение всех пар бит операндов.

AND <операнд1>, <операнд2>

Операция поразрядное «или» выполняет логическое сложение всех пар бит операндов.

OR <операнд1>, <операнд2>

Операция поразрядное исключающее «или» выполняет сложение по модулю 2 всех пар бит операндов.

XOR <операнд1>, <операнд2>

Операции AND, OR и XOR имеют по два операнда. Первый может быть регистром или ячейкой памяти, а второй – регистром, ячейкой памяти или непосредственным операндом. Операнды должны иметь одинаковый размер. Результат помещается на место первого операнда. Операции меняют флаги CF, OF, PF, SF и ZF.

Операция XOR имеет интересную особенность – если значения операндов совпадают, то результатом будет значение 0. Поэтому операцию XOR используют для обнуления регистров – она выполняется быстрее, чем запись нуля с помощью команды MOV.

xor eax, eax; При любом значении EAX результат будет равен 0

Операцию XOR можно также использовать для обмена значений двух переменных.

xor eax, ebx

xor eax, ebx

xor eax, ebx

Команды сдвига

Операции сдвига вправо и сдвига влево сдвигают биты в переменной на заданное количество позиций. Каждая команда сдвига имеет две разновидности:

<мнемокод> <операнд>, <непосредственный операнд>

<мнемокод> <операнд>, CL

Первый операнд должен быть регистром или ячейкой памяти. Именно в нём осуществляется сдвиг. Второй операнд определяет количество позиций для сдвига, которое задаётся непосредственным операндом или хранится в регистре CL (и только CL).

Команды сдвига меняют флаги CF, OF, PF, SF и ZF.

Существует несколько разновидностей сдвигов, которые отличаются тем, как заполняются «освобождающиеся» биты.

Логические сдвиги

При логическом сдвиге «освобождающиеся» биты заполняются нулями. Последний ушедший бит сохраняется во флаге CF.

SHL <операнд>, <количество>; Логический сдвиг влево

SHR <операнд>, <количество>; Логический сдвиг вправо


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: