Язык ассемблера - Структуры циклов
Циклы — одна из самых распространённых управляющих конструкций в программах. В языке ассемблера можно использовать инструкцию LOOP или условные переходы для реализации различной логики циклов.
Инструкция LOOP
LOOPЭто инструкция x86, специально предназначенная для циклов, использующая ECX в качестве счётчика:
При каждом выполнении LOOP ECX автоматически уменьшается на 1; если ECX не равен 0, происходит переход к целевой метке.
Пример
; Базовое использование инструкции LOOP: повторить вывод 5 раз
section .data
msg db 'example', 0xA
len equ $ - msg
section .text
global _start
_start:
mov ecx, 5 ; Счётчик цикла = 5 (5 итераций)
repeat:
; Сохранить ecx (системные вызовы могут его изменить)
push ecx
; Вывести msg
mov eax, 4
mov ebx, 1
mov ecx, msg
mov edx, len
int 0x80
; Восстановить ecx
pop ecx
loop repeat ; ecx--; если ecx != 0, перейти к repeat
mov eax, 1
mov ebx, 0
int 0x80
Вывод:
$ nasm -f elf32 loop_basic.asm -o loop_basic.o $ ld -m elf_i386 loop_basic.o -o loop_basic $ ./loop_basic example example example example example
loopПо умолчанию в качестве счетчика используется ECX (32-бит). В 16-битном режиме используется CX; в 64-битном режиме используется RCX. Обязательно установите ECX правильно перед циклом.
Варианты LOOP
| Инструкция | Условие перехода | Описание |
|---|---|---|
| LOOP | ECX != 0 | Стандартный цикл: сначала уменьшить ECX, затем проверить |
| LOOPE / LOOPZ | ECX != 0 и ZF = 1 | Продолжать цикл, если равно |
| LOOPNE / LOOPNZ | ECX != 0 и ZF = 0 | Продолжать цикл, если не равно |
Пример
section .data
array db 0, 0, 0, 5, 0, 0 ; Первые три равны 0, четвёртый равен 5
section .text
global _start
_start:
mov ecx, 6 ; Длина массива
mov esi, array - 1 ; Указать на одну позицию перед массивом
find_nonzero:
inc esi ; Перейти к следующему элементу
cmp byte [esi], 0 ; Текущий элемент == 0?
loope find_nonzero ; Если он равен 0 и ecx > 0, продолжить цикл
; Цикл завершён: найден первый ненулевой элемент или обход завершён
; esi теперь указывает на адрес первого ненулевого элемента
; ecx хранит количество элементов, ещё не сравнённых
mov eax, 1
mov ebx, 0
int 0x80
Условный цикл (цикл WHILE)
Используйте условные переходы для реализации цикла while: сначала проверьте условие, затем выполните тело цикла.
Пример
; Реализует: while (x < 100) x = x * 2;
section .data
x dd 1
limit dd 100
section .text
global _start
_start:
; цикл while
while_start:
mov eax, [x] ; Загрузить x
cmp eax, [limit] ; x < 100 ?
jge while_end ; Если x >= 100, выйти из цикла
shl eax, 1 ; x = x * 2
mov [x], eax ; Сохранить x обратно
jmp while_start ; Вернуться к началу цикла и снова проверить условие
while_end:
; x теперь равен 128 (1->2->4->8->16->32->64->128, останавливается, когда 128 >= 100)
mov eax, 1
mov ebx, [x] ; Вернуть x как код выхода
int 0x80
Цикл DO-WHILE
Сначала выполните тело цикла, затем проверьте условие:
Пример
section .data
counter dd 0
section .text
global _start
_start:
mov dword [counter], 0
do_loop:
; Тело цикла: counter++ (выполняется по крайней мере один раз)
inc dword [counter]
; Проверить условие
cmp dword [counter], 5
jl do_loop ; Продолжать, пока счётчик < 5
; счётчик в итоге = 5
mov eax, 1
mov ebx, 0
int 0x80
Вложенные циклы
Вложенные циклы требуют внимания к сохранению счётчика внешнего цикла
Пример
; Пример вложенного цикла: вывод таблицы умножения (3×)
section .data
space db ' '
newline db 0xA
; Используется для хранения преобразованных числовых символов (2 цифры + пробел + null)
num_str db ' ', 0
section .text
global _start
_start:
mov ecx, 3 ; Счётчик внешнего цикла (количество строк)
outer_loop:
push ecx ; ★ Сохранить счётчик внешнего цикла! (Важно)
mov ecx, 3 ; Счётчик внутреннего цикла (количество столбцов)
inner_loop:
push ecx ; Сохранить счётчик внутреннего цикла
; Вычислить произведение = номер внешней строки (4 - текущий ecx) × номер внутреннего столбца
; Здесь опущен вывод преобразования конкретного числа, демонстрируется только структура
; Фактический вывод: номер строки × номер столбца
pop ecx ; Восстановить счётчик внутреннего цикла
loop inner_loop ; Внутренний цикл
; Вывод новой строки
mov eax, 4
mov ebx, 1
mov ecx, newline
mov edx, 1
int 0x80
pop ecx ; ★ Восстановить счётчик внешнего цикла
loop outer_loop ; Внешний цикл
mov eax, 1
mov ebx, 0
int 0x80
Самая распространённая ошибка во вложенных циклах — забыть использовать стек для сохранения значения счётчика внешнего цикла. LOOP изменяет ECX; когда вы сбрасываете ECX во внутреннем цикле, счётчик внешнего цикла теряется. Решение — сохранять его перед каждым входом во внутренний цикл,
push ecxи восстанавливать его после выхода.pop ecx。
Приёмы оптимизации циклов
Некоторые способы повышения эффективности циклов:
Пример
; Метод A: Использование инструкции LOOP (медленнее, но неочевидно)
mov ecx, 1000
loop_a:
add eax, [ebx]
add ebx, 4
loop loop_a
; Метод B: Использование условного перехода (ручной подсчёт) (обычно быстрее)
mov ecx, 1000
loop_b:
add eax, [ebx]
add ebx, 4
dec ecx
jnz loop_b
; Метод C: Развёртывание цикла (самый быстрый, но большой размер кода)
; Развернуть цикл на 1000 итераций, чтобы обрабатывать 4 элемента за итерацию
mov ecx, 250 ; 1000 / 4 = 250
loop_unrolled:
add eax, [ebx]
add eax, [ebx + 4]
add eax, [ebx + 8]
add eax, [ebx + 12]
add ebx, 16
dec ecx
jnz loop_unrolled
; Метод D: Обратный отсчёт (уменьшает количество инструкций сравнения)
mov ecx, 1000
lea ebx, [array + 1000*4 - 4] ; Начать с конца массива
loop_reverse:
add eax, [ebx]
sub ebx, 4
dec ecx
jnz loop_reverse
Полный пример: Вычислить сумму от 1 до 100
Пример
; Вычислить 1+2+...+100 = 5050
section .data
msg db 'Сумма от 1 до 100 равна: '
msg_len equ $ - msg
newline db 0xA
section .bss
result_str resb 12 ; Сохранить преобразованную числовую строку
section .text
global _start
_start:
; Вычислить накопительную сумму
mov ecx, 100 ; Цикл выполняется 100 раз
mov eax, 0 ; Начальная накопительная сумма равна 0
mov ebx, 1 ; Текущее число для добавления
sum_loop:
add eax, ebx ; eax += ebx
inc ebx ; Следующее число
loop sum_loop ; Продолжить цикл
; eax = 5050
; Вывести текст приглашения
push eax ; Сохранить результат
mov eax, 4
mov ebx, 1
mov ecx, msg
mov edx, msg_len
int 0x80
pop eax ; Восстановить результат
; Преобразовать число в строку (простая версия: напрямую вывести целочисленное значение)
; Упрощённо здесь: преобразовать младший байт eax в символ
add al, '0' ; Неточно, только для демонстрации
mov [result_str], al
; Вывести результат
mov eax, 4
mov ebx, 1
mov ecx, result_str
mov edx, 12
int 0x80
mov eax, 4
mov ebx, 1
mov ecx, newline
mov edx, 1
int 0x80
mov eax, 1
mov ebx, 0
int 0x80
Другие расширенияПреобразование чисел в строки — распространённая проблема в ассемблере. Простой метод выше работает только когда число в диапазоне 0-9. Полный алгоритм преобразования числа в строку будет подробно объяснён в главе «Обработка чисел».