Язык ассемблера - Структуры циклов

Циклы — одна из самых распространённых управляющих конструкций в программах. В языке ассемблера можно использовать инструкцию LOOP или условные переходы для реализации различной логики циклов.


Инструкция LOOP

LOOPЭто инструкция x86, специально предназначенная для циклов, использующая ECX в качестве счётчика:

При каждом выполнении LOOP ECX автоматически уменьшается на 1; если ECX не равен 0, происходит переход к целевой метке.

Пример

; Путь к файлу: loop_basic.asm
; Базовое использование инструкции 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

ИнструкцияУсловие переходаОписание
LOOPECX != 0Стандартный цикл: сначала уменьшить ECX, затем проверить
LOOPE / LOOPZECX != 0 и ZF = 1Продолжать цикл, если равно
LOOPNE / LOOPNZECX != 0 и ZF = 0Продолжать цикл, если не равно

Пример

; Пример LOOPE: найти первое ненулевое значение в массиве

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_loop.asm
; Реализует: 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

Сначала выполните тело цикла, затем проверьте условие:

Пример

; Цикл 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

Вложенные циклы

Вложенные циклы требуют внимания к сохранению счётчика внешнего цикла

Пример

; Путь к файлу: nested_loop.asm
; Пример вложенного цикла: вывод таблицы умножения (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

Пример

; Путь к файлу: sum_1_to_100.asm
; Вычислить 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. Полный алгоритм преобразования числа в строку будет подробно объяснён в главе «Обработка чисел».

Другие расширения