Циклические конструкции позволяют выполнить некоторый набор инструкций определенное количество раз. На ассемблере можно реализовать различные типы циклов различным образом.
Но, как правило, они вовлекают инструкцию сравнения результатов CMP
и переход к определенной метке.
В ряде языков программирования есть тип циклов - цикл while, который выполняет некоторые действия, пока истинно некоторое условие:
// бейсикоподобный синтаксис WHILE условие действия цикла END WHILE // сиподобный синтаксис while(условие){ действия цикла }
На ассемблере общий вид цикла while будет выглядеть следующим образом:
В ассемблере подобное можно реализовать следующим образом:
while: CMP Xn, Operand2 // сравниваем значения B.NE end_while // если условие неверно, выходим из цикла инструкции цикла B while // заново выполняем действия цикла end_while: // завершение цикла // остальные инструкции программы
После метки while
идут действия цикла, где сначала сравниваем значение некоторого регистра Xn:
CMP Xn, Operand2
Далее, если выполняется некоторое условие, то переходим на метку завершения цикла. В данном случае переход идет на метку end_while
, если значение регистра Xn больше или равно
значению Operand2. Если же это условие не выполняются то выполняются последующие действия вплоть до инструкции B while
, которая производит переход обратно на метку while
Если условие выполняется, то производится переход на метку end_while
, которая знаменует завершение цикла и за которой идут остальные действия программы.
Посмотрим на полном примере:
// METANIT.COM. Пример программы с циклом типа while .global _start _start: mov x0, #0 // помещаем в регистр X0 число 0 while: cmp x0, #5 // сравнивание значение регистра X0 с числом 5 b.ge end_while // если X0 равно или больше 5, то выходим из цикла - переход к метке end_while add x0, x0, #1 // X0 = X0 + 1 b while // переход обратно к метке while end_while: // завершение цикла mov x8, #93 // номер функции Linux для выхода из программы - 93 svc 0 // вызываем функцию и выходим из программы
В данном случае помещаем в регистр X0 число 0. В условном цикле сравниваем значение регистра с числом 5
CMP X0, #5
Если значения не равны, то добавляем в регистр X0 единицу
ADD X0, X0, #1
И переходим обратно к метке while
, тем самым повторяя действия.
Если же значение из регистра X0 равно 5, то инструкция B.EQ end_while
выполняет переход к метке end_while
, после которой идет завершение программы.
А на консоль будет выведен код возврата из программы - число 5 из регистра X0.
Еще одним типом циклов является цикл do..while
- он сначала выполняет некоторые действия, а потом проверяет условие. Если условие верно, повторяет действия. Если условие НЕ верно,
завершает работу:
do{ выполняемые действия } while(условие);
С точки зрения реализации это самый легкий тип цикла:
// METANIT.COM. Пример программы с циклом типа do..while .global _start _start: mov x0, #0 // помещаем в регистр X0 число 0 do_while: add x0, x0, #1 // X0 = X0 + 1 - выполняемые действия цикла cmp x0, #5 // сравнивание значение регистра X0 с числом 5 b.lt do_while // если X0 меньше числа 5, то переходим обратно к метке while // завершение программы mov x8, #93 // номер функции Linux для выхода из программы - 93 svc 0 // вызываем функцию и выходим из программы
Здесь помещаем в регистр X0 число 0. Затем после метки do_while
производим действия цикла - увеличиваем значение регистра X0 на единицу.
Затем сравниваем значение регистра X0 с числом 5. И если X0 меньше числа 5, то переходим обратно к метке do_while
и повторяем действия цикла. Когда
X0 станет равным 6, то произодейт выход из цикла.
В ряде языков высокого уровня, например, есть циклическая конструкция for
:
// бейсикоподобный синтаксис FOR i = M TO N выполняемые действия NEXT i // сиподобный синтаксис for(var i = M; i < N; i++) { выполняемые действия }
то есть есть некоторый счетчик i, и пока этот счетчик не достигнет значения N, будут выполняться некоторые действия цикла.
В ассемблере для создания циклов аля-for применяется следующая обобщенная конструкция:
// METANIT.COM. Пример программы с циклом типа for .global _start _start: mov x0, #0 // регистр X0 - условный счетчик for_start: // метка, на которую проецируется цикл cmp x0, #5 // сравниваем с некоторым пределом b.ge for_end // условие - если счетчик больше или равен пределу, выход из цикла // выполняемые действия add x0, x0, 1 // действия цикла - увеличение счетчика b for_start // повторяем цикл for_end: // завершение программы mov x8, #93 // номер функции Linux для выхода из программы - 93 svc 0 // вызываем функцию и выходим из программы
В начале помещаем в некоторый регистр, который будет выполнять роль счетчика, некоторое начальное значение. Далее идет метка (в примере выше метка for_start
),
после которой помещаются действия цикла. В самом цикле могут быть различные инструкции, но как минимум идет изменение значение счетчика (в примере выше - увеличение на единицу):
ADD Xn, Xn, #1
Затем проверяем некоторое условие, например, сравниваем счетчик с некоторым предельным значением (в коде выше с числом 5):
CMP Xn, #5
Далее проверяем флаги и в зависимости от результата сравнения выполняем опять переход к метке for_start
. Таким образом действия повторяются, пока программа
программа будет соответствовать выбранному условию. Так, в примере выше, если значение регистра Xn равно 5 (по сути, если Z-флаг установлен), переходим к метке
for_end
и таким образом выходим из цикла.
Если мы посмотрим на ассемблерную реализацию циклов while и for, то увидим, что они в принципе похожи:
// цикл while mov x0, #0 while: cmp x0, #5 b.ge end_while add x0, x0, #1 b while end_while: // цикл for mov x0, #0 for_start: cmp x0, #5 b.ge for_end add x0, x0, 1 b for_start for_end:
Такая реализация очень проста и понятна. Однако реализация цикла do_while гораздо эффективнее
mov x0, #0 do_while: add x0, x0, #1 cmp x0, #5 b.lt do_while
В отличие от while/for здесь отсутствует инструкция перехода к концу цикла, и сама конструкция будет теоретически выполняться быстрее. Но в циклах while/for нам важно выполнять определенные действия только при соблюдении некоторого условия. Соответственно это условие вначале надо проверить. Однако мы могли бы вынести действия цикла вперед:
// METANIT.COM. Пример программы с циклом типа do..while .global _start _start: mov x0, #0 // помещаем в регистр X0 число 0 b compare // переходим к проверке условия while: // собственно действия цикла while add x0, x0, #1 // X0 = X0 + 1 - выполняемые действия цикла compare: // проверка условия cmp x0, #5 // сравнивание значение регистра X0 с числом 5 b.lt while // если X0 меньше числа 5, то переходим обратно к метке while // завершение программы mov x8, #93 // номер функции Linux для выхода из программы - 93 svc 0 // вызываем функцию и выходим из программы
Здесь цикл начинается с перехода к метке compare, где проверяется условие - X0 должен быть меньше 5. Если это условие верно, переходим к метке while, где выполняем действия цикла. Затем опять проверяем условия и выполняем действия цикла, пока не дойдем до ситуации, когда X0 = 5.
Таким образом, безусловного перехода инстркция b
выполняется только один раз, а в остальном цикл for/while будет соответствовать циклу do-while.
Но и цикл do-while мы тоже можем до некоторой степени оптимизировать. Перепишем пример следующим образом:
.global _start _start: mov x0, #5 do_while: // собственно действия цикла while subs x0, x0, #1 // X0 = X0 - 1 - выполняемые действия цикла b.ne do_while // проверка условия - если X0 != 0, то переходим обратно к метке do_while // завершение программы mov x8, #93 // номер функции Linux для выхода из программы - 93 svc 0 // вызываем функцию и выходим из программы
Здесь идет обратный отсчет - от 5 до 1. И теперь значение в регистре X0 уменьшается на 1. Но цикл срабатывает столько же раз, сколько и в предыдущей реализации. Поскольку когда
результат операции subs
окажется равен 0 (то есть X0 = 0), то будет установлен флаг нуля. Соответственно мы можем проверить этот флаг, и если он еще не установлен, продолжить цикл.
Таким образом, мы отбрасываем инструкцию cmp
, поскольку нам не надо вручную сравнивать значения.
Для проверки на 0 с последующим переходом к определенной метке при равенстве или неравенстве нулю можно также использовать соответственно инструкции CBZ и CBNZ:
// METANIT.COM. Пример программы с циклом типа do..while .global _start _start: mov x0, #5 do_while: // собственно действия цикла while sub x0, x0, #1 // X0 = X0 - 1 - выполняемые действия цикла cbnz x0, do_while // проверка условия - если X0 != 0, то переходим к метке do_while mov x8, #93 // номер функции Linux для выхода из программы - 93 svc 0 // вызываем функцию и выходим из программы
Если нам надо, чтобы X1 допускал значение 0 (от 5 до 0 - в этом случае флаг нуля мы так проверить не сможем), то мы можем проверять флаг знака, который устанавливается, если результат отрицательный:
.global _start _start: mov x0, #5 do_while: // собственно действия цикла while subs x0, x0, #1 // X0 = X0 - 1 - выполняемые действия цикла b.pl do_while // проверка условия - если X0 > -1, то переходим обратно к метке do_while mov x8, #93 // номер функции Linux для выхода из программы - 93 svc 0 // вызываем функцию и выходим из программы