Циклы

Последнее обновление: 18.07.2023

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

while

В ряде языков программирования есть тип циклов - цикл 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 - он сначала выполняет некоторые действия, а потом проверяет условие. Если условие верно, повторяет действия. Если условие НЕ верно, завершает работу:

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:

// бейсикоподобный синтаксис
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               // вызываем функцию и выходим из программы

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850