Базовый CS с нуля
Циклы как повторные переходы
Ты знаешь, что инструкция перехода может установить счётчик команд на любой адрес — включая адрес раньше в памяти, чем текущая инструкция. Что произойдёт, если CPU прыгнет назад к инструкции, которую он уже выполнял?
Он выполнит её снова. А если следующая инструкция снова является обратным переходом — он выполнит её в третий раз. Четвёртый. И продолжает так, пока что-то не изменится.
Это повторение и есть цикл. Не специальная аппаратная функция — у CPU нет понятия «цикл». Это просто тот же цикл выборка–декодирование–исполнение, снова и снова выполняющий те же инструкции под управлением условного перехода, который продолжает указывать счётчик команд назад, пока условие не станет ложным.
Каждый цикл while, каждый цикл for, каждое повторение в каждой программе сводится
к этому одному механизму. Как только ты поймёшь обратный переход — ты поймёшь циклы
на уровне, каким их видит машина.
После этого урока ты сможешь объяснить цикл как обратный переход под управлением условия, трассировать машинные шаги цикла while, описать, почему while и for имеют одинаковую базовую структуру, и объяснить точно, почему возникает бесконечный цикл.
Обратный переход: возврат к более ранней инструкции. Инструкция перехода может адресоваться на любой адрес — вперёд или назад в памяти. Обратный переход устанавливает счётчик команд на адрес, который меньше, чем адрес самой инструкции перехода. Следующая выборка приходит из этого более раннего адреса, и CPU заново выполняет всё между ним и переходом.
Нет ничего особенного в обратном переходе для аппаратуры CPU. Это тот же опкод JUMP, что и прямой переход. Единственное отличие — величина и направление смещения цели: отрицательное смещение = назад, положительное = вперёд. CPU просто устанавливает СК в адрес цели и возобновляет свой цикл.
Обратный переход полезен в паре с условным тестом. Если прыгать назад безусловно — сразу получается бесконечный цикл. Если прыгать назад только когда условие выполнено — получается управляемое повторение: тело выполняется снова только пока условие остаётся истинным.
Структура цикла while в машинном коде. while (условие) { тело } компилируется
в следующую машинную структуру:
[LOOP_TOP: CMP ...] ; проверить условие
[условный переход на LOOP_END] ; выйти, если условие ЛОЖНО
[инструкции тела] ; тело цикла
[безусловный JUMP на LOOP_TOP] ; обратный переход к тесту
[LOOP_END: ...] ; продолжение после циклаКлючевое наблюдение: тест стоит первым, перед телом. На каждой итерации CPU:
- Оценивает условие с помощью инструкции сравнения, обновляя флаги.
- Проверяет флаг условным переходом — если условие ложно, переход срабатывает и CPU выходит из цикла (прямой переход на LOOP_END). Если условие истинно — fall-through.
- Выполняет тело цикла.
- Доходит до безусловного обратного перехода и устанавливает СК обратно на LOOP_TOP.
- Возвращается к шагу 1.
Обратный переход в конце цикла всегда безусловный. Вся логика решения находится в условном переходе в начале.
Почему это работает
Почему условие проверяется в начале? Потому что while (условие) не должен
выполнять тело вообще, если условие уже ложно при первом достижении цикла. Проверка
в начале гарантирует, что тело выполнится ноль раз, когда условие начинается ложным.
Это отличается от цикла do { тело } while (условие), где тело выполняется хотя бы
один раз, потому что тест стоит в конце. На машинном уровне единственное отличие —
стоит ли пара CMP + условный переход до или после инструкций тела.
Структура цикла for: тот же машинный код, другой синтаксис. for (init; условие; обновление) { тело } — это синтаксический сахар, удобство на уровне источника. После
компиляции это та же структура с обратным переходом, что и цикл while:
[инструкции инициализации] ; выполняются один раз до начала цикла
[LOOP_TOP: CMP ... условие ...]
[условный переход на LOOP_END]
[инструкции тела]
[инструкции обновления] ; инкремент/декремент внизу
[безусловный JUMP на LOOP_TOP]
[LOOP_END: ...]Код инициализации выполняется один раз перед первой итерацией. Код обновления
выполняется в конце каждой итерации, непосредственно перед обратным переходом. Тест
и обратный переход стоят ровно в тех же позициях, что и в цикле while. На машинном
уровне CPU не может отличить скомпилированный while от скомпилированного for —
они дают одинаковый паттерн инструкций.
Бесконечные циклы: когда обратный переход не останавливается. Цикл работает вечно, когда условие, проверяемое перед каждым обратным переходом, никогда не становится ложным. Это может произойти по двум причинам:
-
Безусловный обратный переход: у «цикла» вообще нет условия — переход в конце всегда срабатывает, СК всегда сбрасывается на начало, и CPU никогда не выходит. Это
while (true), скомпилированный в простой JUMP (без CMP, без условия), — намеренно используется в серверных и событийных циклах. -
Условие, которое никогда не становится ложным: тест существует, но тело цикла никогда не изменяет сравниваемые значения, поэтому условие остаётся истинным всегда. Например, если цикл отсчитывает от 10, но ошибка в теле мешает счётчику убывать, CMP всегда видит то же значение, флаг не меняется, и обратный переход всегда срабатывает.
Бесконечный цикл — не аппаратная ошибка. CPU делает ровно то, что ему говорит программа: выполняет обратный переход, сбрасывает СК, выполняет тело и снова прыгает назад — бесконечно. CPU продолжит, пока его не остановит ОС или прерывание.
Трассировка простого цикла обратного отсчёта.
Программа: отсчёт от 3 до 0, остановка когда счётчик достигает 0.
Адрес Инструкция Эффект
100 LOAD R0, 200 R0 ← значение счётчика (начинается с 3)
104 CMP R0, 0 сравнить R0 с 0, обновить флаги Z и N
108 JE 120 прыжок на 120 (выход), если Z=1 (R0 == 0)
112 SUB R0, 1 R0 ← R0 − 1
116 JUMP 104 обратный переход к тесту по адресу 104
120 ... цикл завершён; продолжение здесьИтерация 1 (R0 = 3):
- CMP по адресу 104: 3 − 0 = 3. Z = 0, N = 0. СК → 108.
- JE по адресу 108: Z = 0, условие «равно» ложно. Fall-through. СК → 112.
- SUB по адресу 112: R0 = 3 − 1 = 2. СК → 116.
- JUMP по адресу 116: обратный переход. СК ← 104.
Итерация 2 (R0 = 2):
- CMP по адресу 104: 2 − 0 = 2. Z = 0. СК → 108.
- JE по адресу 108: Z = 0, fall-through. СК → 112.
- SUB по адресу 112: R0 = 2 − 1 = 1. СК → 116.
- JUMP по адресу 116: СК ← 104.
Итерация 3 (R0 = 1):
- CMP по адресу 104: 1 − 0 = 1. Z = 0. СК → 108.
- JE по адресу 108: Z = 0, fall-through. СК → 112.
- SUB по адресу 112: R0 = 1 − 1 = 0. СК → 116.
- JUMP по адресу 116: СК ← 104.
Итерация 4 (R0 = 0 — условие выхода):
- CMP по адресу 104: 0 − 0 = 0. Z = 1. СК → 108.
- JE по адресу 108: Z = 1, условие «равно» истинно. Переход. СК ← 120.
- Цикл завершён. R0 = 0.
Тело выполнилось ровно 3 раза. Обратный переход по адресу 116 сработал 3 раза. На 4-м тесте флаг Z наконец стал 1, и условный переход вышел из цикла.
Частая ошибка
Распространённое заблуждение — считать, что цикл «знает, сколько раз он выполнится». CPU этого не знает. На каждой итерации CPU проверяет условие, видит значение флага и принимает одно бинарное решение (переход или fall-through). У него нет памяти о том, сколько раз он уже прыгал назад. Количество итераций возникает из того, сколько раз условие было проверено и оказалось истинным — CPU ничего не считает; только изменение условия завершает цикл.
Обратный переход устанавливает СК на адрес, меньший чем адрес инструкции перехода. После обратного перехода с адреса 120 на адрес 100 чему равен СК?
В цикле while, скомпилированном в машинный код, где стоит инструкция CMP относительно тела цикла?
Цикл while со счётчиком начинается с 5, уменьшается на 1 при каждой итерации, останавливается, когда счётчик достигает 0. Сколько раз срабатывает обратный переход?
В машинном коде есть ли аппаратное отличие между циклом while и циклом for?
У цикла нет инструкции сравнения — только безусловный JUMP назад на начало. Сколько итераций он выполняет?
Каков машинный механизм, заставляющий цикл while повторять своё тело?
Цикл — это обратный переход — инструкция перехода, адрес цели которой находится
раньше в памяти, чем она сама. В сочетании с условным тестом обратный переход создаёт
управляемое повторение: тело цикла выполняется, условие переоценивается, и если условие
всё ещё истинно — СК сбрасывается на начало цикла; если ложно — условный переход
выходит из цикла, прыгая вперёд за тело. Цикл while ставит тест перед телом;
цикл for добавляет блок инициализации перед циклом и блок обновления перед обратным
переходом, но базовый машинный паттерн идентичен. Бесконечный цикл возникает, когда
обратный переход безусловен или когда проверяемое перед каждой итерацией условие никогда
не становится ложным — CPU не может обнаружить или предотвратить это; он продолжает
цикл обратных переходов бесконечно, пока ОС или прерывание его не остановит.