Циклические конструкции позволяют выполнить некоторый набор инструкций определенное количество раз. На ассемблере можно реализовать различные типы циклов различным образом. Но, как правило, они вовлекают инструкцию сравнения результатов CMP и переход к определенной метке.
В ряде языков программирования есть тип циклов - цикл while, который выполняет некоторые действия, пока истинно некоторое условие:
// бейсикоподобный синтаксис WHILE условие действия цикла END WHILE // сиподобный синтаксис while(условие){ действия цикла }
Пример цикла на сиподобных языках:
i = 0; while(i < 100) { i++; }
Здесь в цикле увеличиваем переменную i на 1, пока она меньше 100. Когда переменная i окажется равна 100, выходим из цикла.
Реализация подобного цикла на ассемблере:
.data i dword 0 .code main proc mov i, 0 while_start: cmp i, 10 jnl while_end ; если i не меньше 10, переход к метке while_end ; выполняемые действия inc i jmp while_start while_end: mov eax, i ; EAX = 10 ret main endp end
Здесь метка while_start
знаменует начало цикла, в котором сначала сравниваем значение i с числом 10. Если значение i НЕ меньше 10 (jnl
),
переходим к концу цикла - метке while_end. Иначе инкрементируем значение переменной и переходим к началу цикла - метке while_start.
Еще одним типом циклов является цикл do..while - он сначала выполняет некоторые действия, а потом проверяет условие. Если условие верно, повторяет действия. Если условие НЕ верно, завершает работу:
// сиподобный синтаксис do{ выполняемые действия } while(условие);
С точки зрения реализации на ассемблере это самый легкий тип цикла:
.code main proc mov eax, 1 ; помещаем в регистр EAX число 1 do_while: ; выполняемые действия add eax, 1 ; действия цикла - EAX = EAX + 1 cmp eax, 10 ; сравнивание значение регистра EAX с числом 10 jl do_while ; если EAX меньше 10, переход к метке do_while ret main endp end
Здесь помещаем в регистр EAX число 1. Затем после метки do_while
производим действия цикла - увеличиваем значение регистра X0 на единицу.
Далее сравниваем значение регистра EAX с числом 10. И если EAX меньше числа 10, то переходим обратно к метке do_while и повторяем действия цикла.
Когда EAX станет равным 10, то произодейт выход из цикла.
В ряде языков высокого уровня, например, есть циклическая конструкция for:
// бейсикоподобный синтаксис FOR i = M TO N выполняемые действия NEXT i // сиподобный синтаксис for(var i = M; i < N; i++) { выполняемые действия }
то есть есть некоторый счетчик i, и пока этот счетчик не достигнет значения N, будут выполняться некоторые действия цикла.
Реализация цикла for в ассемблере:
.code main proc mov eax, 0 ; в EAX помещаем число - условный счетчик for_start: ; метка, на которую проецируется цикл cmp eax, 10 ; сравниваем с некоторым пределом jnl for_end ; условие - если счетчик больше или равен пределу, выход из цикла ; выполняемые действия add eax, 1 ; действия цикла - увеличение счетчика jmp for_start ; повторяем цикл for_end: ret main endp end
В данном случае в EAX помещаем некоторое значение, которое будет выполнять роль счетчика цикла. Собственно цикл начинается с метки for_start
.
Сначала мы проверяем условие. Допустим, нам надо, чтобы цикл повторялся, пока в регистре EAX не окажется число 10. Если в EAX чило 10 или больше, то выходим из цикла - переходим к метке
for_end
. Если в EAX все еще меньше 10, то выполняем действия цикла - увеличиваем EAX на 1 и затем переходим обратно к метке for_start для повторения цикла.
Если мы посмотрим на ассемблерную реализацию циклов while и for, то увидим, что они в принципе похожи:
;цикл while mov eax, 0 while_start: cmp eax, 10 jnl while_end add eax, 1 jmp while_start while_end: ; цикл for mov eax, 0 for_start: cmp eax, 10 jnl for_end add eax, 1 jmp for_start for_end:
Такая реализация очень проста и понятна. Однако реализация цикла do_while гораздо эффективнее
mov eax, 1 do_while: add eax, 1 cmp eax, 10 jl do_while
В отличие от while/for здесь отсутствует инструкция jmp
, и сама конструкция будет теоретически выполняться быстрее. Но в циклах while/for нам важно выполнять определенные действия только
при соблюдении некоторого условия. Соответственно это условие вначале надо проверить. Однако мы могли бы вынести действия цикла вперед:
.code main proc mov eax, 0 jmp for_condition ; переходим к проверке условия for_start: add eax, 1 for_condition: cmp eax, 10 jl for_start ; если условие соблюдено переходим к метке for_start для выполнения действий ret main endp end
Здесь цикл начинается с перехода к метке for_condition, где проверяется условие - EAX должен быть меньше 10. Если это условие верно, переходим к метке for_start, где выполняем действия цикла. Затем опять проверяем условия и выполняем действия цикла, пока не дойдем до ситуации, когда EAX = 10.
Таким образом, инстркция jmp
выполняется только один раз, а в остальном цикл for/while будет соответствовать циклу do-while.
Но и цикл do-while мы тоже можем до некоторой степени оптимизировать. Перепишем пример следующим образом:
.code main proc mov eax, 9 ; помещаем в регистр EAX число 9 do_while: ; выполняемые действия sub eax, 1 ; действия цикла - EAX = EAX - 1 jnz do_while ; если флаг нуля установлен, то есть EAX=0, переход к метке do_while ret main endp end
Здесь идет обратный отсчет - от 9 до 1. И теперь значение в регистре EAX уменьшается на 1. Но цикл срабатывает столько же раз, сколько и в предыдущей реализации. Поскольку когда результат
операции sub
окажется равен 0 (то есть EAX = 0), то будет установлен флаг нуля. Соответственно мы можем проверить этот флаг, и если он еще не установлен, продолжить цикл.
Таким образом, мы отбрасываем инструкцию cmp
, поскольку нам не надо вручную сравнивать значения.
Если нам надо, чтобы EAX допускал значение 0 (от 9 до 0 - в этом случае флаг нуля мы так проверить не сможем), то мы можем проверять флаг знака, который устанавливается, если результат отрицательный:
.code main proc mov eax, 9 ; помещаем в регистр EAX число 9 do_while: ; выполняемые действия sub eax, 1 ; действия цикла - EAX = EAX - 1 jns do_while ; если флаг знака SF =1, переход к метке do_while ret main endp end