Loop в ассемблере
Инструкция LOOP
![]() | Что такое JavaScript |
Если вы интересуетесь программированием вообще, и сайтостроением в частности, то вы наверняка слышали слово JavaScript. И, если вы до сих пор не узнали толком, что же это такое, то пришло время сделать это. Подробнее.
Инструкция LOOP в Ассемблере уменьшает значение в регистре СХ в реальном режиме или ECX в защищённом. Если после этого значение в СХ не равно нулю, то команда LOOP выполняет переход на МЕТКУ. Синтаксис:
Состояние флагов не изменяется.
МЕТКА — это допустимый в Ассемблере идентификатор. О метках в Ассемблере я рассказывал здесь.
Алгоритм работы команды LOOP:
- CX = CX — 1
- Если CX не равен 0, то выполнить переход
- Иначе не выполнять переход, продолжить цикл
То есть команда LOOP выполняется в два этапа. Сначала из регистра СХ вычитается единица и его значение сравнивается с нулём. Если регистр не равен нулю, то выполняется переход к указанной МЕТКЕ. Иначе переход не выполняется и управление передаётся команде, которая следует сразу после команды LOOP.
Как выполнить цикл в Ассемблере
Выполнение цикла в Ассемблере можно организовать с помощью нескольких команд. Одна из таких команд — это команда LOOP. Команда цикла в Ассемблере всегда уменьшает значение счётчика на единицу. Это значение находится в регистре СХ (или ECX). Отличия между командами цикла заключаются только в условиях, при которых выполняется переход к метке или цикл завершается.
Команда LOOP выполняет переход к метке во всех случаях, когда значение в регистре СХ не равно нулю. Чтобы организовать цикл с помощью этой команды, нам надо сначала в регистр СХ записать число итераций цикла (то есть сколько раз цикл должен быть выполнен), затем вставить в код метку, а затем написать команды, которые должны быть выполнены в цикле. А уже в конце списка этих команд записать команду LOOP.
Более понятно это будет в примере программы (см. ниже).
Возможные ошибки
Начинающие довольно часто совершают одни и те же ошибки при организации циклов в Ассемблере. А именно — неправильно задают или обнуляют значение счётчика перед выполнение цикла.
При обнулении счётчика перед циклом при первой итерации цикла значение в регистре CX будет равно FFFFh (потому что команда LOOP отнимет от СХ единицу, а в СХ у нас был 0), и цикл в программе будет выполняться, соответственно 65536 раз.
Ещё один момент: диапазон адресов для передачи управления в команде LOOP ограничен в пределах -128…+127 байтов относительно адреса следующей команды. Если учесть, что в реальном режиме процессора средняя длина машинной команды равна 3 байта, то получается, что блок команд, выполняющихся в цикле, может состоять примерно из 42 команд. Если же в вашем цикле будет больше команд, то, например, MASM, выдаст сообщение об ошибке, которое будет выглядеть примерно так:
error A2075: jump destination too far : by 10 byte(s)
Здесь говорится, что местоположение перехода слишком далеко (примерно на 10 байт больше допустимого).
Ещё одна ошибка — это изменение значения регистра CX в теле цикла. В итоге команда LOOP будет работать неправильно. К тому же при этом можно попасть в бесконечный цикл. Пример:
Здесь в теле цикла увеличивается значение регистра СХ, поэтому он никогда не будет равен нулю, и, следовательно, цикл никогда не завершится.
А теперь о происхождении мнемоники LOOP. В общем то это не мнемоника, а слово. В переводе с английского оно означает “петля”, “виток”, “цикл”.
Алгоритмическая структура «Цикл» в языке Ассемблер
К изучению языка Ассемблер учащиеся подходят, как правило, имея начальные знания в области программирования. Поэтому им проще будет понять, как реализуются основные алгоритмические структуры в Ассемблере, если при изложении нового материала преподаватель будет проводить аналогию с изученным ими ранее языком программирования (например, Turbo Pascal).
Алгоритмическая структура “цикл”, как известно, обеспечивает выполнение некоторой последовательности действий, которая называется телом цикла.
Выделяется три типа циклов: цикл “ДЛЯ”, цикл “ПОКА”, цикл “ДО”. Друг от друга различные типы циклов отличаются в основном лишь способом проверки окончания цикла.
В языке программирования Паскаль для реализации каждого типа цикла имеются специальные операторы, но любой из этих трех типов можно организовать при помощи условного оператора и оператора безусловного перехода.
Система команд языка Ассемблер тоже позволяет организовать циклическое выполнение некоторого фрагмента программы, к примеру, используя команды условной передачи управления или команду безусловного перехода JMP.
Как и в языке Паскаль, в Ассемблере существует специальная команда, которая позволяет сокращать листинг циклической программы.
Это команда LOOP .
Данная команда выполняет следующие функции:
- Автоматически уменьшает значение счетчика.
- Выполняет проверку на выход из цикла.
- Выполняет переход на начало тела цикла.
Команда LOOP может быть использована лишь в случае цикла с известным числом повторений, т.е. цикла “ДЛЯ”. Количество повторений цикла должно быть присвоено регистру СХ до начала цикла.
Таким образом, команда LOOP заменила тройку команд:
Рассмотрим использование этой команды на практике.
Пример: Составим программу, которая выводит на экран 1000 нулей.
(1) prg segment para public ‘code’
(2) assume cs:prg,ss:prg,es:prg,ds:prg
(3) org 100h
(4) start: jmp go
(5) go:
(6) mov ax, 0600h
(7) mov bh,07
(8) mov cx, 0000
(9) mov dx,184fh
(10) mov cx,1000
(11) Zero:
(12) mov ah,02
(13) mov dl,30h
(14) int 21h
(15) loop Zero
(16) ret
(17) prg ends
(18) end start
Строки с (1) по (10) и с (16) по (18) вы уже знаете.
Строка (11) – это метка (начало цикла). Строка (15) – конец цикла. Все, что находится в пределах строк (11) – (15), является циклом. Сам цикл будет повторяться 1000 раз, для чего мы и заносим в СХ число 1000 (строка (10)).
В строке (12) заносим в регистр ah число 02 (запрос функции вывода одного символа).
В строке (13) в регистр dl заносим код выводимого символа (код символа “0” – 30h).
В строке (14) вызываем прерывание int 21h.
Теперь на экране появится первый ноль. Остается уменьшить счетчик (СХ) на 1 и повторить. Что мы и делаем в строке (15).
Задача 1 для практики: Составить фрагмент программы на языке Ассемблер, подсчитывающий сумму первых 10 натуральных чисел (результат записать в АХ).
Задача 2 для практики: Составить фрагмент программы на языке Ассемблер, вычисляющий значение выражения: (результат записать в АХ).
Задача 3 для практики: Составить фрагмент программы на языке Ассемблер, вычисляющий факториал заданного числа К (К – от 0 до 8).
Команды ассемблера
Команда lea для арифметики
Для выполнения некоторых арифметических операций можно использовать команду lea 2 Intel® 64 and IA-32 Architectures Optimization Reference Manual, 3.5.1.3 Using LEA . Она вычисляет адрес своего операнда-источника и помещает этот адрес в операнд-назначение. Ведь она не производит чтение памяти по этому адресу, верно? А значит, всё равно, что она будет вычислять: адрес или какие-то другие числа.
Вспомним, как формируется адрес операнда:
Вычисленный адрес будет равен база + индекс ? множитель + смещение.
Чем это нам удобно? Так мы можем получить команду с двумя операндами-источниками и одним результатом:
Вспомните, что при сложении командой add результат записывается на место одного из слагаемых. Теперь, наверно, стало ясно главное преимущество lea в тех случаях, где её можно применить: она не перезаписывает операнды-источники. Как вы это сможете использовать, зависит только от вашей фантазии: прибавить константу к регистру и записать в другой регистр, сложить два регистра и записать в третий… Также lea можно применять для умножения регистра на 3, 5 и 9, как показано выше.
Команда loop
- уменьшить значение регистра %ecx на 1;
- если %ecx = 0, передать управление следующей за loop команде;
- если %ecx
, передать управление на метку.
Напишем программу для вычисления суммы чисел от 1 до 10 (конечно же, воспользовавшись формулой суммы арифметической прогрессии, можно переписать этот код и без цикла — но ведь это только пример).
На Си это выглядело бы так:
Команды сравнения и условные переходы. Безусловный переход
Команда loop неявно сравнивает регистр %ecx с нулём. Это довольно удобно для организации циклов, но часто циклы бывают намного сложнее, чем те, что можно записать при помощи loop . К тому же нужен эквивалент конструкции if()<> . Вот команды, позволяющие выполнять произвольные сравнения операндов:
Команда cmp выполняет вычитание операнд_1 — операнд_2 и устанавливает флаги. Результат вычитания нигде не запоминается.
Сравнили, установили флаги, — и что дальше? А у нас есть целое семейство jump -команд, которые передают управление другим командам. Эти команды называются командами условного перехода. Каждой из них поставлено в соответствие условие, которое она проверяет. Синтаксис:
Команды jcc не существует, вместо cc нужно подставить мнемоническое обозначение условия.
Мнемоника | Английское слово | Смысл | Тип операндов |
---|---|---|---|
E | equal | равенство | любые |
N | not | инверсия условия | любые |
G | greater | больше | со знаком |
L | less | меньше | со знаком |
A | above | больше | без знака |
B | below | меньше | без знака |
Таким образом, je проверяет равенство операндов команды сравнения, jl проверяет условие операнд_1 и так далее. У каждой команды есть противоположная: просто добавляем букву n :
- je — jne : равно — не равно;
- jg — jng : больше — не больше.
Теперь пример использования этих команд:
Сравните с кодом на Си:
Кроме команд условного перехода, область применения которых ясна сразу, также существует команда безусловного перехода. Эта команда чем-то похожа на оператор goto языка Си. Синтаксис:
Эта команда передаёт управление на адрес, не проверяя никаких условий. Заметьте, что адрес может быть задан в виде непосредственного значения (метки), регистра или обращения к памяти.
Произвольные циклы
Все инструкции для написания произвольных циклов мы уже рассмотрели, осталось лишь собрать всё воедино. Лучше сначала посмотрите код программы, а потом объяснение к ней. Прочитайте её код и комментарии и попытайтесь разобраться, что она делает. Если сразу что-то непонятно — не страшно, сразу после исходного кода находится более подробное объяснение.
Программа: поиск наибольшего элемента в массиве
Сначала мы заносим в регистр %eax число array[0]. После этого мы сравниваем каждый элемент массива, начиная со следующего (нам незачем сранивать нулевой элемент с самим собой), с текущим наибольшим значением из %eax , и, если этот элемент больше, он становится текущим наибольшим. После просмотра всего массива в %eax находится наибольший элемент. Отметим, что если массив состоит из 1 элемента, то следующий после нулевого элемента будет находиться за границей массива, поэтому перед циклом стоит безусловный переход на проверку границы.
Этот код соответствует приблизительно следующему на Си:
Возможно, такой способ обхода массива не очень привычен для вас. В Си принято использовать переменную с номером текущего элемента, а не указатель на него. Никто не запрещает пойти этим же путём и на ассемблере:
Рассматривая код этой программы, вы, наверно, уже поняли, как создавать произвольные циклы с постусловием на ассемблере, наподобие do<> while(); в Си. Ещё раз повторю эту конструкцию, выкинув весь код, не относящийся к циклу:
В Си есть ещё один вид цикла, с проверкой условия перед входом в тело цикла (цикл с предусловием): while()<> . Немного изменив предыдущий код, получаем следующее: