Идентификация циклов — Архив WASM.RU
"Связь между элементами системы носит
трансуровневый характер и проявляет себя в виде
повторяющихся единиц разных уровней (мотивов)"
Тезис классического постструктурализма
в его отечественном изводе  Циклы - единственная (за исключением неприличного "GOTO") конструкция языков высокого уровня, имеющая ссылку "назад", т.е. в область более младших адресов. Все остальные виды ветвлений - будь то IF - THEN - ELSE или оператор множественного выбора switch всегда направлены "вниз" - в область старших адресов. Вследствие этого, логическое дерево, изображающее цикл, настолько характерно, что легко опознается с первого взгляда.
  Существуют три основных типа цикла: циклы с условием вначале , циклы с условием в конце (см. рис. 36 в центре) и циклы с условием в середине (см. рис. 36 справа). Комбинированные циклы имеют несколько условий в разных местах, например, в начале и в конце одновременно.
  Рисунок 36.0х024 Логическое дерево цикла с условием вначале (слева) и условием в конец (справа).
  В свою очередь условия бывают двух типов: условия завершения цикла и условия продолжения цикла . В первом случае:если условие завершения истинно происходит переход в конец цикла, иначе - его продолжение.Во втором: если условие продолжения цикла ложно происходит переход в конец цикла, в противном случае - его продолжения. Легко показать, что условия продолжения цикла представляют собой инвертированные условия завершения. Таким образом, со стороны транслятора вполне достаточно поддержки условий одного типа. И действительно, операторы циклов while,do и for языка Си работают исключительно с условиями продолжения цикла. Оператор while языка Pascal так же работает с условием продолжения цикла, и исключение составляет один лишь repeat-until ожидающий условие завершения цикла.
  ::Циклы с условиями в начале (так же называемые циклами с преусловием). В языках Си и Pascal поддержка циклов с преусловием обеспечивается оператором "while (условие)", где "условие" - условие продолжения цикла. Т.е. цикл "while (a <10) a++;" выполняется до тех пор, пока условие (a > 10) остается истинным. Однако транслятор при желании может инвертировать условие продолжение цикла на условие завершения цикла. На платформе Intel 80x86 такой трюк экономит от одной до двух машинных команд. Смотрите: на листинге 180 слева приведен цикл с условием завершения, а справа - с условием продолжения. Как видно, цикл с условием завершения на одну команду короче! Поэтому, практически все компиляторы (даже не оптимизирующие) всегда генерируют левый вариант. (А некоторые, особо одаренные, даже умеют превращать циклы с предусловием в еще более эффективные циклы с пост-условием - см. "Циклы с условием в конце").
Код (Text):
while: while: CMP A, 10 CMP A, 10 JAE end JB continue INC A JMP end JMP while continue: end: INC A JMP while end:  Листинг 180.Слева показан цикл с условием завершения цикла, а справа - тот же цикл, но с условием продолжения цикла. Как видно, цикл с условием завершения на одну команду короче.
  Цикл с условием завершения не может быть непосредственно отображен на оператор while. Кстати, об этом часто забывают начинающие, допуская ошибку "что вижу, то пишу": "while (a >= 10) a++". С таким условием данный цикл вообще не выполниться ни разу! Но как выполнить инверсию условия и при этом гарантированно не ошибиться? Казалось бы, что может быть проще, - а вот попросите знакомого хакера назвать операцию, обратную "больше". Очень может быть (даже наверняка!) ответом будет... "меньше". А вот и нет, - правильный ответ "меньше или равно". Полный перечень обратных операций отношений можно найти в таблице 25, приведенной ниже
 
Логическая операция Обратная логическая операция == != != == > <= < >= <= > > <= Таблица 25 Обратные операции отношения   ::Циклы с условием в конце (так же называемые циклами с пост-условием). В языке Си поддержка циклов с пост-условием обеспечивается парой операторов do - while, а в языке Pascal - repeat\until. Циклы с пост-условием без каких либо проблем непосредственно отображаются с языка высокого уровня на машинный код и, соответственно, наоборот. Т.е. в отличие от циклов с предусловием, инверсии условия не происходит. Например: "do a++; while (a<10)" в общем случае компилируется в следующий код (обратите внимание: в переходе использовалась та же самая операция отношения, что и в исходном цикле, - красота и никаких ошибок при декомпиляции):
  Листинг 181repeat: <---------! INC A ! CMP A, 10 ! JB repeat---! end
Вернувшись страницей назад, сравним код цикла с пост-условием с кодом цикла с предусловием. Не правда ли, цикл с условием в конце компактнее и быстрее? Некоторые компиляторы (например, Microsoft Visual C++) умеют транслировать циклы с предусловием в циклы с пост-условием. На первый взгляд - это вопиющая самодеятельность компилятора, - если программист хочет проверять условие в начале, то какое право имеет транслятор ставить его в конце?! На самом же деле, разница между "до" или "после" не столь велика и значительна. Если компилятор уверен, что цикл выполняется хотя бы один раз, то он вправе выполнять проверку когда угодно. Разумеется, при этом необходимо несколько скорректировать условие проверки: "while (a<b)" не эквивалентно "do ... while (a<b)", т.к. в первом случае при (a == b) уже происходит выход из цикла, а во втором цикл выполняется еще одну итерацию. Однако этой беде легко помочь: увеличим а на единицу ("do ... while ((a+1)<b)") или вычтем эту единицу из b ("do ... while (a<(b-1))") и... теперь все будет работать! Спрашивается: и на кой все эти извращения, значительно раздувающие код? Дело в том, что блок статического предсказания направления ветвлений Pentium-процессоров оптимизирован именно под переходы, направленные назад, т.е. в область младших адресов. Поэтому, циклы с постусловием должны выполняться несколько быстрее аналогичных им циклов с предусловием.
  ::Циклы со счетчиком. Циклы со счетчиком (for) не являются самостоятельным типом циклов, а представляют собой всего лишь синтаксическую разновидность циклов с предусловием. В самом деле, "for (a = 0; a < 10; a++)" в первом приближении это то же самое, что и: "a = 0; while (a < 10) {...;a++;}". Однако, результаты компиляции двух этих конструкций не обязательно должны быть идентичны друг другу! Оптимизирующие компиляторы (да и значительная часть не оптимизирующих) поступают хитрее, передавая после инициализации переменной-счетчика управление на команду проверки условия выхода из цикла. Образовавшаяся конструкция, во-первых, характерна и при анализе программы сразу бросается в глаза, а, во-вторых, не может быть непосредственно отображена на циклы while языка высокого уровня. Смотрите:
  Листинг 182Код (Text):
MOV A, xxx ; Инициализация переменной "счетчика" <b>JMP conditional</b> ; Переход к проверке условия продолжения цикла repeat: ; Начало цикла ... ; // ТЕЛО ... ; // ЦИКЛА ADD A, xxx [SUB A, xxx]; Модификация счетчика conditional: ; Проверка условия продолжения цикла CMP A, xxx ; ^ Jxx repeat ; Переход в начало цикла, если условие истинно
  Непосредственный прыжок вниз может быть результат компиляции и цикла for, и оператора GOTO, но GOTO сейчас не в моде и используется крайне редко, а без него оператор условного перехода "IF - THEN" не может прыгнуть непосредственно в середину цикла while! Выходит, изо всех "кандидатов" остается только цикл for.
  Некоторые, особо продвинутые компиляторы (Microsoft Visual C++, Borland C++, но не WATCOM C), поступают хитрее: анализируя код они еще на стадии компиляции пытаются определить: выполняется ли данный цикл хотя бы один раз и, если видят, что он действительно выполняется, превращают for в типичный цикл с постусловием:  Листинг 183Код (Text):
MOV A, xxx ; Инициализация переменной "счетчика" repeat: ; Начало цикла ... ; // ТЕЛО ... ; // ЦИКЛА ADD A, xxx [SUB A, xxx]; Модификация счетчика CMP A, xxx ; Проверка условия продолжения цикла Jxx repeat ; Переход в начало цикла, если условие истинно
  Наконец, самые крутые компиляторы (из которых автор на вскидку может назвать один лишь Microsoft Visual C++ 6.0) могут даже заменять циклы с приращением на циклы с убыванием при условии, что параметр цикла не используется операторами цикла, а лишь прокручивает цикл определенное число раз. Зачем это компилятору? Оказывается, циклы с убыванием гораздо короче - однобайтовая инструкция DEC не только уменьшает операнд, но и выставляет Zero-флаг при достижении нуля. В результате, в команде CMP A, xxx отпадает всякая необходимость.
  Листинг 184Код (Text):
MOV A, xxx ; Инициализация переменной "счетчика" repeat: ; Начало цикла ... ; // ТЕЛО ... ; // ЦИКЛА DEC A ; Декремент счетчика JNZ repeat ; Повтор, пока A != 0
  Таким образом, в зависимости от настроек и характера компилятора, циклы for могут транслироваться и в циклы с предусловием, и в циклы с постусловием, начинающими свое выполнение с проверки условия продолжения цикла. Причем, условие продолжения может инвертироваться в условие завершения, а возрастающий цикл может "волшебным" образом превращаться в убывающий.
  Такая неоднозначность затрудняет идентификацию циклов for, - надежно отождествляются лишь циклы, начинающиеся с проверки постусловия (т.к. они не могут быть отображены на do без использования GOTO). Во всех остальных случаях никаких строгих рекомендаций по распознаванию for дать невозможно.
  Скажем так: если логика исследуемого цикла синтаксически удобнее выражается через оператор for, то и выражайте ее через for! В противном случае используйте while или do (repeat\until) для циклов с пред- и пост- условием соответственно.
  И в заключение пара слов о "кастрированных" циклах - язык Си позволяет опустить инициализацию переменной цикла, условие выхода из цикла, оператор приращения переменной или все это вместе. При этом for вырождается во while, и становится практически неотличимым от него.
  ::Циклы с условием в середине. Популярные языки высокого уровня непосредственно не поддерживают циклы с условием в середине, хотя необходимость в них возникает достаточно часто. Поэтому, программисты их реализуют на основе уже имеющихся циклов while (while\do) и оператора выхода из цикла break. Например:
  Листинг 185Код (Text):
while(1) repeat: { ... ... <b>CMP xxx</b> if (условие) break; <b>Jxx end</b> ... ... } <b>JMP repeat</b> end:
  Компилятор (если он не совсем Осел - Иi в смысле) разворачивает бесконечный цикл в безусловный переход JMP, направленный, естественно назад (ослы генерируют код like - "MOV EAX, 1\CMP EAX,1\JZ repeat"). Безусловный переход, направленный назад, весьма характерен - за исключением бесконечного цикла его может порождать один лишь оператор GOTO, но GOTO уже давно не в моде. А раз у нас есть бесконечный цикл, то условие его завершения может находиться лишь в середине этого цикла (сложные случаи многопоточных защит, модифицирующих из соседнего потока безусловный переход в NOP, мы пока не рассматриваем). Остается прочесать тело цикла и найти это самое условие.
  Сделать это будет нетрудно - оператор break транслируется в переход на первую команду, следующую на JMP repeat, а сам break получает управление от ветки IF (условие) - THEN - [ELSE]. Условие ее срабатывания и будет искомым условием завершения цикла. Вот, собственно, и все.  ::Циклы с множественными условиями выхода. Оператор break позволяет организовать выход из цикла в любом удобном для программиста месте, поэтому, любой цикл может иметь множество условий выхода беспорядочно разбросанных по его телу. Это ощутимо усложняет анализ дизассемблируемой программы, т.к. возникает риск "прозевать" одно из условий завершения цикла, что приведет к неправильному пониманию логики программы.
  Идентифицировать же условия выхода из цикла очень просто - они всегда направлены "вниз" т.е. в область старших адресов и указывают на команду, непосредственно следующую за инструкций условного (безусловного) перехода, направленного "вверх" - в область младших адресов. (см. так же "Циклы с условием в середине").  ::Циклы с несколькими счетчиками. Оператор "запятая" языка Си позволяет осуществлять множественную инициализацию и модификацию счетчиков цикла for. Например: "for (a=0, b=10; a != b; a++, b--)". А как насчет нескольких условий завершения? И "ветхий" и "новый " заветы (первое и второе издание K&R соответственно), и стандарт ANSI C, и руководства по С, прилагаемые к компиляторам Microsoft Visual C, Borland C, WATCOM C на этот счет хранят "партизанское" гробовое молчание.
  Если попробовать скомпилировать следующий код "for (a=0, b=10; a >0, b <10 ; a++, b--)" он будет благополучно "проглочен" практически всеми компиляторами без малейших ругательств с их стороны, но ни один их них не откомпилирует данный пример правильно. Логическое условие (a1,a2,a3,...an) лишено смысла и компиляторы без малейших колебаний и зазрений совести отбросяст все, кроме самого правого выражения an. Оно-то и будет единолично пределять условие продолжение цикла. Один лишь WATCOM вяло ворчит по этому поводу: "Warning! W111: Meaningless use of an expression: the line contains an expression that does nothing useful. In the example "i = (1,5);", the expression "1," is meaningless. This message is also generated for a comparison that is useless"
  Если условие продолжения цикла зависит от нескольких переменных, то их сравнения следует объединить в одно выражение посредством логических операций OR, AND и др. Например: "for (a=0, b=10; (a >0 && b <10) ; a++, b--)" - цикл прерывается сразу же, как только одно из двух условий станет ложно; "for (a=0, b=10; (a >0 || b <10); a++, b--)" - цикл продолжается до тех пор, пока истинно хотя бы одно условие из двух.   В остальном же циклы с несколькими счетчиками транслируются аналогично циклам с одним счетчиком, за исключением того, что инициализируется и модифицируется не одна, а сразу несколько переменных.  ::Идентификация continue. Оператор continue приводит к непосредственной передаче управления на код проверки условия продолжения (завершения) цикла. В общем случае он транслируется в безусловный jump, в циклах с предусловием направленный вверх, а в циклах в постусловием - вниз. Код, следующий за continue, уже не получает управления, поэтому continue практически всегда используется в условных конструкциях.
  Например: "while (a++ < 10) if (a == 2) continue;..." компилируется приблизительно так:  Листинг 186Код (Text):
repeat: ; Начало цикла while INC A ; a++ CMP A, 10 ; Проверка условия завершения цикла JAE end ; Конец, если a >= 10 CMP A,2 ; if (a == 2) ... JNZ woo ; Переход к варианту "иначе", если a != 2 JMP repeat ; <== continue woo: ; // ТЕЛО ... ; // ЦИКЛА JMP repeat ; Переход в начало цикла  ::Сложные условия. До сих пор, говоря об условиях завершения и продолжения цикла, мы рассматривали лишь элементарные условия отношения, в то время как практически все языки высокого уровня допускают использование составных условий. Однако составные условия можно схематично изобразить в виде абстрактного "черного ящика" с входом/выходом и логическим двоичными деревом внутри. Построение и реконструкция логических деревьев подробно рассматриваются в главе "Идентификация IF - THEN - ELSE" здесь же нас интересует не сами условия, а организация циклов.
  ::Вложенные циклы. Циклы - понятное дело - могут быть и вложенными. Казалось бы, какие проблемы? Начало каждого цикла надежно определяется по перекрестной ссылке, направленной вниз. Конец цикла - условный или безусловный переход на его начало. У каждого цикла только одно начло и только один конец (хотя условий выхода может быть сколько угодно, но это - другое дело). Причем, циклы не могут пересекаться - если между началом и концом одного цикла встречается начало другого цикла, то этот цикл - вложенный.
  Но не все так просто: тут есть два подводных камня. Первый: оператор continue в циклах с предусловием, второй - сложные условия продолжения цикла с постусловием. Рассмотрим их подробнее.
  Поскольку, continue в циклах с предусловием, транслируется в безусловный переход, направленный "вверх", он становится практически неотличим от конца цикла. Смотрите:Код (Text):
while(условие1) { ... if (условие2) continue; ... } транслируется в: NOT условие1 выхода из цикла---------! <-! <-----! ... ! ! ! если НЕ условие2 GOTO continue ---! ! ! ! безусловный переход в начало ------)--)---! ! continue: <-----! ! ! ... ! ! безусловный переход в начало ---------)------------! конец всего <------------------------!  Два конца и два начала вполне напоминают два цикла, из которых один вложен в другой. Правда, начала обоих циклов совмещены, но ведь может же такое быть, если в цикл с пост условием вложен цикл с предусловием? На первый взгляд да, но если подумать, то... ай-ай-ай! А ведь условие1 выхода из цикла прыгает аж за второй конец! Если это предусловие вложенного цикла, то оно прыгало бы за первый конец. А если условие1 - это предусловие материнского цикла, то конец вложенного цикла не смог бы передать на него управление. Выходит, это не два цикла, а один. А первый "конец" - результат трансляции оператора continue.
  С разбором сложных условий продолжения цикла с постусловием дела обстоят еще лучше. Рассмотрим такой пример:
Код (Text):
do { ... } while(условие1 || условие2); Результат его трансляции в общем случае будет выглядеть так: ... <---! <-! условие продолжения1 ---! ! условие прололжения2 -------! Ну, чем не: do { do { ... }while(условие1) }while(условие2)  Строго говоря, предложенный вариант является логически верным, но синтаксически некрасивым. Материнский цикл крутит в своем теле один лишь вложенный цикл и не содержит никаких других операторов. Так зачем он тогда, спрашивается, нужен? Объединить его с вложенным циклом в один!
  Дизассемблерные листинги примеров. Давайте для закрепления сказанного рассмотрим несколько живых примеров.
  Начнем с самого простого - с циклов while\do:Код (Text):
#include <stdio.h> main() { int a=0; while(a++<10) printf("Оператор цикла while\n"); do { printf("Оператор цикла do\n"); } while(--a >0); }  Листинг 187 Демонстрация идентификации циклов while\do
  Результат компиляции этого примера компилятором Microsoft Visual C++ 6.0 с настройками по умолчанию должен выглядеть так:
Код (Text):
main proc near ; CODE XREF: start+AF.p var_a = dword ptr -4 push ebp mov ebp, esp ; Открываем кадр стека push ecx ; Резервируем память для одной локальной переменной mov [ebp+var_a], 0 ; Заносим в переменную var_a значение 0x0 loc_40100B: ; CODE XREF: main_401000+29.j ; ^^^^^^^^^^^^^^ ; Перекрестная ссылка, направленная вниз, говорит о том, что это начло цикла ; Естественно: раз перекрестная ссылка направлена вниз, то переход, ; ссылающийся на этот адрес, будет направлен вверх! mov eax, [ebp+var_a] ; Загружаем в EAX значение переменной var_a mov ecx, [ebp+var_a] ; Загружаем в EСX значение переменной var_a ; (недальновидность компилятора - можно было бы поступить и короче MOV ECX,EAX) add ecx, 1 ; Увеличиваем ECX на единицу mov [ebp+var_a], ecx ; Обновляем var_a cmp eax, 0Ah ; Сравниваем старое (до обновления) значение переменной var_a с числом 0xA jge short loc_40102B ; Если var_a >= 0xA - прыжок "вперед", непосредственно за инструкцию ; безусловного перехода, направленного "назад" ; Раз "назад", значит, - это цикл, а, поскольку, условие выхода из цикла ; проверяется в его начале, то это цикл с предусловием ; Для его отображения на цикл while необходимо инвертировать условие выхода ; из цикла на условие продолжения цикла (Т.е. заменить >= на <) ; Сделав это, мы получаем: ; <b>while (var_a++ < 0xA)...</b> ; // Начало тела цикла push offset aOperatorCiklaW ; "Оператор цикла while\n" call _printf add esp, 4 <b>; printf("Оператор цикла while\n")</b> jmp short loc_40100B ; Безусловный переход, направленный назад, на метку loc_40100B ; Между loc_40100B и jmp short loc_40100B есть только одно условие ; выхода из цикла - jge short loc_40102B, значит, исходный цикл ; выглядел так: <b>; while (var_a++ < 0xA) printf("Оператор цикла while\n")</b> loc_40102B: ; CODE XREF: main_401000+1A.j ; main_401000+45.j ; ^^^^^^^^^^^^^^^^ ; // Это начало цикла с пост-условием ; // Однако на данном этапе мы этого еще не знаем, хотя и можем догадываться ; // благодаря наличию перекрестной ссылки, направленной вниз ; Ага, никакого условия в начале цикла не присутствует, значит, это цикл ; с условием в конце или середине push offset aOperatorCiklaD ; "Оператор цикла do\n" call _printf add esp, 4 <b>; printf("Оператор цикла do\n")</b> ; // Тело цикла mov edx, [ebp+var_a] ; Загружаем в EDX значение переменной var_a sub edx, 1 ; Уменьшаем EDX на единицу mov [ebp+var_a], edx ; Обновляем переменную var_a cmp [ebp+var_a], 0 ; Сравниваем переменную var_a с нулем jg short loc_40102B ; Если var_a > 0, то переход в начало цикла ; Поскольку, условие расположено в конце тела цикла, этот цикл - do: ; <b>do printf("Оператор цикла do\n"); while (--a > 0)</b> ; ; // Для повышения читабельности дизассемблерного текста рекомендуется ; // заменить префиксы loc_ в начале цикла на while и do (repeat) в циклах ; // с пред- и пост- условием соответственно mov esp, ebp pop ebp ; Закрываем кадр стека retn main endp  Листинг 188
  Совсем другой результат получится если включить оптимизацию. Откомпилируем тот же самый пример с ключом "/Ox" (максимальная оптимизация) и посмотрим на результат, выданный компилятором:
Код (Text):
main proc near ; CODE XREF: start+AF.p push esi push edi ; Сохраняем регистры в стеке mov esi, 1 ; Присваиваем ESI значение 0х1 ; <i><b>Внимание</b> - взгляните на исходный код - ни одна из переменных не имела ; такого значения!</i> mov edi, 0Ah ; Присваиваем EDI значение 0xA. Ага, это константа для проверки условия ; выхода из цикла loc_40100C: ; CODE XREF: main+1D.j ; ^^^^^^^^^^^^^^^^^^^^ ; Судя по перекрестной ссылке, направленной вниз, этот - цикл! push offset aOperatorCiklaW ; "Оператор цикла while\n" call _printf add esp, 4 <b>; printf("Оператор цикла while\n")</b> ; ...тело цикла while? (растерянно так) ; Постой, постой! А где же предусловие?! dec edi ; Уменьшаем EDI на один inc esi ; Увеличиваем ESI на один test edi, edi ; Проверяем EDI на равенство нулю ja short loc_40100C ; Переход в начало цикла, пока EDI != 0 ; Так... (задумчиво) Компилятор в порыве оптимизации превратил неэффективный ; цикл с предусловием в более компактный и быстрый цикл с пост-условием ; Имел ли он на это право? А почему нет?! Проанализировав код, компилятор понял ; что данный цикл выполняется, по крайней мере, один раз, следовательно, ; скорректировав условие продолжения, его проверку можно вынести в конец цикла ; Поэтому-то начальное значение переменной цикла равно единице, а не нулю! ; Т.е. while ((int a = 0) < 10) компилятор заменил на ; do ... while (((int a = 0)+1) < 10) == <b>; do ... while ((int a=1) < 10)</b> ; ; Причем, что интересно, он не сравнивал переменную цикла с константой, ; а поместил константу в регистр и уменьшал его до тех пор, пока тот не стал ; равен нулю! Зачем? А затем, что так короче, да и работает быстрее ; Что ж, это все хорошо, но как нам декомпилировать этот цикл? ; Непосредственное отображение на язык Си дает следующую конструцию: <b>; var_ ESI = 1; var _EDI = 0xA; ; do { ;;printf("Оператор цикла while\n"); var_EDI--; var_ESI++; ; } while(var_EDI > 0)</b> ; ; Правда, коряво и запутано? Что-ж, тогда попытаемся избавится от одной ; из двух переменных. Это действительно возможно, т.к. они модифицируются ; синхронно, и <b>var_EDI = 0xB - var_ESI</b> ; ОК, выполняем подстановку: ; <b>var_ ESI = 1; var _EDI = 0xB - var_ESI ; (== 0xA;) ; do { ;;printf("Оператор цикла while\n"); var_EDI--; var_ESI++;</b> ; ^^^^^^^^^^ ; Это мы вообще сокращаем, т.к. var_EDI уже выражена через var_ESI <b>; } while((0xB - var_ESI) > 0); (== var_ESI > 0xB)</b> ; ; Что, ж уже получается нечто осмысленное: ; <b>; var_ ESI = 1; var _EDI == 0xA; ; do { ;; printf("Оператор цикла while\n"); var_ESI++; ; } while(var_ESI > 0xB)</b> ; На этом можно и остановится, а можно и пойти дальше, преобразовав цикл ; с пост-условием в более наглядный цикл с предусловием ; <b>; var_ ESI = 1; var _EDI == 0xA; <-- var_EDI не используется, ;можно сократить ; while (var_ESI < = 0xA) { ;; printf("Оператор цикла while\n"); var_ESI++; ; }</b> ; Но и это не предел выразительности: во-первых var_ESI < = 0xA эквивалентно ; var_EDI < 0xB, а во-вторых, поскольку, переменная var_ESI используется лишь ; как счетчик, ее начальное значение можно безбоязненно привести к нулевому ; значению, а операцию инкремента внести в сам цикл: <b>; var_ ESI = 0; ; while (var_ESI++ < 0xA) <-- вычитаем единицу из левой ; и правой половины ; printf("Оператор цикла while\n");</b> ; ; Ну, разве не красота?! Сравните этот вариант с первоначальным - ; насколько он стал яснее и понятнее loc_40101F: ; CODE XREF: main+2F.j ; ^^^^^^^^^^^^^^^^^^^^ ; Перекрестная ссылка, направленная вниз, говорит о том, что это - начало цилка ; // Предусловия нет - значит, это цикл do push offset aOperatorCiklaD ; "Оператор цикла do\n" call _printf add esp, 4 <b>; printf("Оператор цикла do\n");</b> dec esi ; Уменьшаем var_ESI test esi, esi ; Проверка ESI на равенство нулю jg short loc_40101F ; Продолжать цикл, пока var_ESI > 0 ; ; ОК. Этот цикл легко и непринужденно отображается на язык Си: <b>; do printf("Оператор цикла do\n"); while (--var_ESI > 0 )</b> pop edi pop esi ; Восстанавливаем сохраненные регистры retn main endp  Листинг 189
  Несколько иначе оптимизирует циклы компилятор Borland C++ 5.x. Смотрите:
Код (Text):
_main proc near ; DATA XREF: DATA:00407044.o push ebp mov ebp, esp ; Открываем кадр стека push ebx ; Сохраняем EBP в стеке xor ebx, ebx ; Присваиваем регистровой переменной EBX значение ноль ; Как легко догадаться - EBX и есть "a" jmp short loc_40108F ; Безусловный прыжок вниз. Очень похоже на цикл for... loc_401084: ; CODE XREF: _main+19.j ; ^^^^^^^^^^^^^^^^^^^^^ ; Перекрестная ссылка, направленная вниз - значит, это начало какого-то цикла push offset aOperatorCiklaW ; "Оператор цикла while\n" call _printf pop ecx <b>; printf("Оператор цикла while\n")</b> loc_40108F: ; CODE XREF: _main+6.j ; А вот сюда был направлен самый первый jump ; Посмотрим: что же это такое? mov eax, ebx ; Копирование EBX в EAX inc ebx ; Увеличение EBX cmp eax, 0Ah ; Сравнение EAX со значением 0xA jl short loc_401084 ; Переход в начало цикла, если EAX < 0xA ; Вот так-то Borland оптимизировал код! Он расположил условие в конце цикла, ; но, чтобы не транслировать цикл с предусловием в цикл с постусловием, ; просто начал выполнение цикла с этого самого условия! ; ; Отображение этого цикла на язык Си дает: <b>; for (int a=0; a < 10; a++) printf("Оператор цикла while\n")</b> ; ; и, хотя подлинный цикл выглядел совсем не так, наш вариант нечем не хуже! ; (а может даже и лучше - нагляднее) loc_401097: ; CODE XREF: _main+29.j ; ^^^^^^^^^^^^^^^^^^^^^ ; Начало цикла! ; Условия нет - значит, это цикл с постусловием push offset aOperatorCiklaD ; "Оператор цикла do\n" call _printf pop ecx <b>; printf("Оператор цикла do\n")</b> dec ebx ; --var_EBX test ebx, ebx jg short loc_401097 ; Продолжать цикл, пока var_EBX > 0 <b>; do printf("Оператор цикла do\n"); while (--var_EBX > 0)</b> xor eax, eax <b>; return 0</b> pop ebx pop ebp ; Восстанавливаем сохраненные регистры retn _main endp  Листинг 190
  Остальные компиляторы генерируют аналогичный или даже еще более примитивный и очевидный код, поэтому не будем подробно их разбирать, а лишь кратно опишем используемые ими схемы трансляции.
  Компилятор Free Pascal 1.x ведет себя аналогично компилятору Borland C++ 5.0, всегда помещая условие в конец цикла и начиная с него выполнение while-циклов.
  Компилятор WATCOM C не умеет преобразовывать циклы с предусловием в циклы с постусловием, вследствие чего располагает условие выхода из цикла в начале while-циклов, а в их конец вставляет безусловный jump. (Классика!)
  Компилятор GCC вообще не оптимизирует циклы с предусловием, генерируя самый неоптимальный код. Смотрите:Код (Text):
mov [ebp+var_a], 0 ; Присвоение переменной a значения 0 mov esi, esi ; Э... на редкость умный код! При его виде трудно не упасть со стула! loc_401250: ; CODE XREF: sub_40123C+34.j ; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ; Начало цикла mov eax, [ebp+var_a] ; Загрузка в EAX значения переменной var_a inc [ebp+var_a] ; Увеличение var_a на единицу cmp eax, 9 ; Сравнение EAX со значением 0x9 jle short loc_401260 ; Переход, если EAX <= 0x9 (EAX < 0xA) jmp short loc_401272 ; Безусловный переход в конец цикла ; Стало быть, предыдущий условный переход - переход на его продолжение ; Какой неоптимальный код! Зато нет инверсии условия продолжения цикла, ; что упрощает дизассемблирование align 4 ; Выравнивание перехода по адресам, кратным четырем, ускорят код, но заметно ; увеличивает его размер (особенно, если переходов очень много) loc_401260: ; CODE XREF: sub_40123C+1D.j add esp, 0FFFFFFF4h ; Вычитание из ESP значения 12 (0xC) push offset aOperatorCiklaW ; "Оператор цикла while\n" call printf add esp, 10h ; Восстанавливаем стек (0xC + 0x4 ) == 0x10 jmp short loc_401250 ; Переход в начало цикла loc_401272: ; Конец цикла  Листинг 191
  Разобравшись с while\do, перейдем к циклам for. Рассмотрим следующий пример:
Код (Text):
#include <stdio.h> main() { int a; for (a=0;a<10;a++) printf("Оператор цикла for\n"); }  Листинг 192 Демонстрация идентификации циклов for
  Результат компиляции Microsoft Visual C++ 6.0 с настройками по умолчанию будет выглядеть так:
Код (Text):
main proc near ; CODE XREF: start+AF.p var_a = dword ptr -4 push ebp mov ebp, esp ; Открываем кадр стека push cx ; Резервируем память для локальной переменной mov [ebp+var_a], 0 ; Присваиваем локальной переменной var_a значение 0 jmp short loc_401016 ; Непосредственный переход на код проверки условия продолжения цикла - ; характерный признак for loc_40100D: ; CODE XREF: main+29.j ; ^^^^^^^^^^^^^^^^^^^^ ; Перекрестная ссылка, направленная вниз говорит о том, что это начало цикла mov eax, [ebp+var_a] ; Загрузка в EAX значения переменной var_a add eax, 1 ; Увеличение EAX на единицу mov [ebp+var_a], eax ; Обновление EAX ; Следовательно, исходный код выглядел так: ; ++a loc_401016: ; CODE XREF: main+B.j cmp [ebp+var_a], 0Ah ; Сравниваем var_a со значением 0xA jge short loc_40102B ; Выход из цикла, если var_a >= 0xA push offset aOperatorCiklaF ; "Оператор цикла for\n" call _printf add esp, 4 <b>; printf("Оператор цикла for\n")</b> jmp short loc_40100D ; Безусловный переход в начало цикла ; ; Итак, что мы имеем? <i><b>; инициализация переменной var_a ; переход на проверку условия выхода из цикла ----------! ; инкремент переменной var_a <---------------------! ! ; проверка условия относительно var_a <----------- ! ---! ; прыжок на выход из цикла, если условие истинно---!----! ; вызов printf ! ! ; переход в начало цикла --------------------------! ! ; конец цикла <-----------------------------------------!</b></i> ; ; Проверка на завершения, расположенная в начале цикла, говорит о том, что ; это цикл с предусловием, но непосредственно выразить его через while ; не удается - мешает безусловный переход в середину цикла, минуя код ; инкремента переменной var_a ; Однако этот цикл с легкостью отображается на оператор for, смотрите: ; for (a = 0; a < 0xA; a++) printf("Оператор цикла for\n") ; ; Действительно, цикл for сначала инициирует переменную - счетчик, ; затем проверяет условие продолжение цикла ; (оптимизируемое компилятором в условие завершение), далее выполняет ; оператор цикла, модифицирует счетчик, вновь проверяет условие и т.д. ; loc_40102B: ; CODE XREF: main+1A.j mov esp, ebp pop ebp ; Закрываем кадр стека retn main endp  Листинг 193
  А теперь задействуем оптимизацию и посмотрим, как видоизмениться наш цикл:
Код (Text):
main proc near ; CODE XREF: start+AF.p push esi mov esi, 0Ah ; Инициализируем переменную - счетчик ;<i><b> Внимание!</b> В исходном коде начальное значение счетчика равнялось нулю!</i> loc_401006: ; CODE XREF: main+14.j push offset aOperatorCiklaF ; "Оператор цикла for\n" call _printf add esp, 4 ;<b> printf("Оператор цикла for\n")</b> ; Выполняем оператор цикла! Причем безо всяких проверок! ; Хитрый компилятор проанализировал код и понял, что цикл выполняется ; по крайней мере один раз! dec esi ; Уменьшаем счетчик, хотя в исходном коде программы мы его увеличивали! ; Ну, правильно - dec \ jnz намного короче INC\ CMP reg, const\ jnz xxx ; Ой и мудрит компилятор! Кто же ему давал право так изменять цикл?! ; А очень просто - он понял, что параметр цикла в самом цикле используется ; только как счетчик, и нет никакой разницы - увеличивается он ; с каждой итерацией или уменьшается! jnz short loc_401006 ; Переход в начало цикла если ESI > 0 ; ; М да, по внешнему виду это типичный ;<b> a = 0xa; do printf("Оператор цикла for\n"); while (--a)</b> ; ; Если вас устраивает читабельность такой формы записи - оставляйте ее, а нет: ;<b> for (a = 0; a < 10; a++) Оператор цикла for\n")</b> ; ; Постой, постой! На каком основании автор выполнил такое преобразование?! ; А на том самом - что и компилятор: раз параметр цикла используется только ; как счетчик, законна любая запись, выполняющая цикл ровно десять раз - ; остается выбрать ту, которая удобнее (с эстетической точки зрения) ; Никто же не будет утверждать, что ; for (a = 10; a > 0; a--) более привычно чем for (a = 0; a < 10; a++)? pop esi retn main endp  Листинг 194
  А что скажет нам товарищ Borland C++ 5.0? Компилируем и смотрим:
Код (Text):
_main proc near ; DATA XREF: DATA:00407044.o push ebp mov ebp, esp ; Открываем кадр стека push ebx ; Сохраняем EBX в стеке xor ebx, ebx ; Присваиваем регистровой переменной EBX значение 0 loc_401082: ; CODE XREF: _main+15.j ; ^^^^^^^^^^^^^^^^^^^^^^ ; Начало цикла push offset aOperatorCiklaF ; format call _printf pop ecx ; Начинаем цикл с выполнения его тела ; OK, Borland понял, что цикл выполняется по крайней мере раз inc ebx ; Увеличиваем параметр цикла cmp ebx, 0Ah ; Сравниваем EBX со значением 0xA jl short loc_401082 ; Переход в начало цикла, пока EBX < 0xA xor eax, eax pop ebx pop ebp retn _main endp  Листинг 195
  Видно, что Borland C++ 5.0 не дотягивает до Microsoft Visual C++ 6.0 - понять, что цикл выполняется один раз он понял, а вот реверс счетчика ума уже не хватило. Аналогичным образом поступает и большинство других компиляторов, в частности WATCOM C.
  Теперь настала очередь циклов с условием в середине или циклов, завершаемых вручную оператором break. Рассмотрим следующий пример:
Код (Text):
#include <stdio.h> main() { int a=0; while(1) { printf("1й оператор\n"); if (++a>10) break; printf("2й оператор\n"); } do { printf("1й оператор\n"); if (--a<0) break; printf("2й оператор\n"); }while(1); }  Листинг 196 Демонстрация идентификации break
  Результат компиляции Microsoft Visual C++ 6.0 с настройками по умолчанию должен выглядеть так:
Код (Text):
main proc near ; CODE XREF: start+AF.p var_a = dword ptr -4 push ebp mov ebp, esp ; Открываем кадр стека push ecx ; Резервируем место для локальной переменной mov [ebp+var_a], 0 ; Присваиваем переменной var_a значение 0х0 loc_40100B: ; CODE XREF: main+3F.j ; ^^^^^^^^^^^^^^^^^^^^^ ; Перекрестная ссылка, направленная вниз - цикл mov eax, 1 test eax, eax jz short loc_401041 ; Смотрите! Когда optimize disabled, - компилятор транслирует безусловный ; цикл "слишком буквально", т.к. присваивает EAX значение 1 (TRUE) ; и затем педантично проверяет ее на равенство нулю ; Если в кои веки TRUE будет равно FALSE - произойдет выход из цикла ; Словом, все эти три инструкции - глупый и бесполезный код цикла ;<b> while (1)</b> push offset a1iOperator ; "1й оператор\n" call _printf add esp, 4 ;<b> printf("1й оператор\n")</b> mov ecx, [ebp+var_a] ; Загружаем в ECX значение переменной var_a add ecx, 1 ; Увеличивем ECX на единицу mov [ebp+var_a], ecx ; Обновляем var_a cmp [ebp+var_a], 0Ah ; Сравниваем var_a со значением 0xA jle short loc_401032 ; Переход, если var_a <= 0xA ; Но <b><i>куда</i></b> этот переход? Во-первых, переход направлен вниз, т.е. это уже ; не переход к началу цикла, следовательно и условие - не условие цикла, а ; результат компиляции конструкции IF - THEN ; Второе - переход прыгает на первую команду, следующую за безусловным ; jump loc_401041, передающим управление инструкции, следующей ; за командной jmp short loc_401075 - безусловного перехода, направленного ; вверх - в начало цикла ; Следовательно, jmp short loc_401041 осуществляет выход из цикла, а ; jle short loc_401032 - продолжает его выполнение jmp short loc_401041 ; ОК, - это переход на завершение цикла. А кто у нас завершает цикл? ; Ну, конечно же,<b> break!</b> Следовательно, окончательная декомпиляции выглядит так ;<b> if (++var_a > 0xA) break</b> ; Мы инвертировали "<=" в ">", т.к. JLE передает управление на код продолжения ; цикла, а ветка THEN в нашем случае - на break loc_401032: ; CODE XREF: main+2E.j ; ^^^^^^^^^^^^^^^^^^^^^ ; Перекрестная ссылка направлена вверх - следовательно, это не начало цикла push offset a2iOperator ; "2й оператор\n" call _printf add esp, 4 ;<b> printf("2й оператор\n")</b> jmp short loc_40100B ; Прыжок в начало цикла. Вот мы и добрались до конца цикла ; Восстанавливаем исходный код: ; while(1) ; { ; printf("1й оператор\n"); ; if (++var_a > 0xA) break; ; printf("2й оператор\n"); ; } ; loc_401041: ; CODE XREF: main+12.j main+30.j ... ; ^^^^^^^^^^ ; Перекрестная ссылка, направленная вниз, говорит, что это начало цикла push offset a1iOperator_0 ; "1й оператор\n" call _printf add esp, 4 ;<b> printf("1й оператор\n")</b> mov edx, [ebp+var_a] sub edx, 1 mov [ebp+var_a], edx ;<b> --var_a</b> cmp [ebp+var_a], 0 ; Сравниваем var_a со значением 0x0 jge short loc_40105F ; Переход вниз, если var_a >= 0 ; Смотрите: оператор break цикла do ничем не отличается от break цикла while! ; Поэтому, не будем разглагольствовать, а сразу его декомпилируем! ;<b> if (var_a < 0) ...</b> jmp short loc_401075 ;<b> ...break</b> loc_40105F: ; CODE XREF: main+5B.j push offset a2iOperator_0 ; "2й оператор\n" call _printf add esp, 4 <b>; printf("2й оператор\n")</b> mov eax, 1 test eax, eax jnz short loc_401041 ; А это - проверка продолжения цикла loc_401075: ; CODE XREF: main+5D.j mov esp, ebp pop ebp ; Закрываем кадр стека retn main endp  Листинг 197
  Что ж, оператор break в обоих циклах выглядит одинаково и элементарно распознается (правда, не с первого взгляда, но отслеживанием нескольких переходов - да). А вот с бесконечными циклами не оптимизирующий компилятор подкачал, транслировав их в код, проверяющий условие, истинность (не истинность) которого очевидна. А как поведет себя оптимизирующий компилятор?
  Давайте откомпилируем тот же самый пример компилятором Microsoft Visual C++ 6.0 с ключом "/Ox" и посмотрим:Код (Text):
main proc near ; CODE XREF: start+AF.p push esi ; Сохраняем ESI в стеке xor esi, esi ; Присваиваем ESI значение 0 ;<b> var_ESI = 0;</b> loc_401003: ; CODE XREF: main+23.j ; ^^^^^^^^^^^^^^^^^^^^^ ; Перекрестная ссылка, направленная вперед ; Это - начало цикла push offset a1iOperator ; "1й оператор\n" call _printf add esp, 4 ;<b> printf("1й оператор\n")</b> ; ; Ага! Проверки на дорогах нет, значит, это цикл с постусловием ; (или условием в середине) inc esi ;<b> ++var_ESI</b> cmp esi, 0Ah ; Сравниваем var_ESI со значением 0xA jg short loc_401025 ; Выход из цикла, если var_ESI > 0xA ; Поскольку, данная команда - не последняя в теле цикла, ; это цикл с условием в середине ;<b> if (var_ESI > 0xA) break</b> push offset a2iOperator ; "2й оператор\n" call _printf add esp, 4 ;<b> printf("2й оператор\n")</b> jmp short loc_401003 ; Безусловный переход в начало цикла ; Как видно, оптимизирующий компилятор выкинул никому ненужную проверку ; условия, упростив код и облегчив его понимание: ; Итак: ;<b> var_ESI = 0 ; for (;;)</b> <-- вырожденный for представляет собой бесконечный цикл ;<b> { ; printf("1й оператор\n"); ; ++var_ESI; ; if (var_ESI > 0xA) break; ; printf("2й оператор\n"); ; }</b> loc_401025: ; CODE XREF: main+14.j ; ^^^^^^^^^^^^^^^^^^^^^ ; Это не начало цикла! push offset a1iOperator_0 ; "1й оператор\n" call _printf add esp, 4 ;<b> printf("1й оператор\n")</b> ; Хм, как же это не начало цикла?! Очень похоже! dec esi ;<b> --var_ESI</b> js short loc_401050 ; Выход из цикла, если var_ESI < 0 inc esi ; Увеличиваем var_ESI на единицу ; М-м-м... (задумчиво)... loc_401036: ; CODE XREF: main+4E.j ; ^^^^^^^^^^^^^^^^^^^^^^ ; А вот это начало цикла! push offset a2iOperator_0 ; "2й оператор\n" call _printf ;<b> printf("2й оператор\n")</b> ; Только странно, что начало цикла начинается с его, с позволения сказать, ; середины... push offset a1iOperator_0 ; "1й оператор\n" call _printf add esp, 8 ;<b> printf("1й оператор\n")</b> ; ; ???!!! Что за чудеса творятся? Во-первых, вызов первого оператора второго ; цикла уже встречался ранее, во-вторых, не может же следом за серединой цикла ; следовать его начало?! dec esi ;<b> --var_ESI</b> jnz short loc_401036 ; Продолжение цикла, пока var_ESI != 0 loc_401050: ; CODE XREF: main+33.j ; Конец цикла ; Да... тут есть над чем подумать! ; Компилятор нормально "перевалил" первую строку цикла ;<i> printf("1й оператор\n")</i> ; а затем "напоролся" на ветвление: ; if (--a<0) break ; Хитрые парни из Microsoft знают, что для супер - конвейерных процессоров ; (коими и являются чипы Pentium) ветвления все равно, что чертополох для ; Тиггеров. Кстати, Си-компиляторы под процессоры серии CONVEX вообще ; отказываются компилировать циклы с ветвлениями, истощенно понося ; умственные способности программистов. А вы еще IBM PC ругаете ;-) ; Вот и приходится компилятору исправлять ляпы программиста, что он делать ; в принципе не обязан, но за что ему большое человеческое спасибо! ; Компилятор как бы "прокручивает" цикл, "слепляя" вызовы функций printf ; и вынося ветвления в конец ; Образно исполняемый код можно представить трассой, а процессор - гонщиком ; Чем длиннее участок дороги без поворотов, тем быстрее его проскочит гонщик! ; Выносить условие из середины цикла в его конец компилятор вполне правомерен, ; ведь переменная, относительно которой выполняется ветвление, ; не модифицируется ни функцией printf, ни какой другой ; Поэтому, не все ли равно где ее проверять? Конечно же не все равно!!! ; К моменту когда условие (--a < 10) становится истинно, успевает выполниться ; первый printf, а вот второй - уже не получает управления ; Вот для этого-то компилятор и поместил код проверки условия следом за ; первым вызовом первой функции printf, а затем изменил порядок вызова ; printf в теле цикла. Это привело к тому, что на момент выхода из цикла ; по условию первый printf выполняется на один раз больше, чем второй ; (т.к. он встречается дважды) ; Остается разобраться с увеличением var_ESI - что бы это значило? ; Давайте рассуждать от противного: что произойдет, если выкинуть ; команду INC ESI? Поскольку, счетчик цикла при первой итерации цикла ; декрементируется дважды, возникнет недостача и цикл выполниться на раз ; короче. Что бы этого не произошло, var_ESI искусственно увеличивается ; на единицу ; Ой, и не просто во всей этой головоломке разобраться, а представьте: ; насколько сложно реализовать компилятор, умеющий проделывать такие фокусы! ; А еще кто-то ругает автоматическую оптимизацию. Да уж! Конечно, руками-то ; можно и круче оптимизировать(особенно понимания смысл кода), но ведь эдак ; и мозги вывихнуть будет можно! А компилятор, даже будучи стиснут со всех ; сторон кривым кодом программиста, за доли секунды успевает его довольно ; прилично окультурить pop esi retn main endp  Листинг 198
  Компиляторы Borland C++ и WATCOM при трансляции бесконечных циклов заменяют код проверки условия продолжения цикла на безусловный переход, но вот, увы, оптимизировать ветвления, вынося их в конец цикла так, как это делает Microsoft Visual C++ 6.0 они не умеют...
  Теперь, после break, рассмотрим: как компиляторы транслирует его "астральный антипод", - оператор continue. Возьмем следующий пример:Код (Text):
#include <stdio.h> main() { int a=0; while (a++<10) { if (a == 2) continue; printf("%x\n",a); } do { if (a == 2) continue; printf("%x\n",a); } while (--a>0); }  Листинг 199 Демонстрация идентификации continue
  Результат его компиляции компилятором Microsoft Visual C++ 6.0 с настройками по умолчанию будет выглядеть так:
Код (Text):
main proc near ; CODE XREF: start+AF.p var_a = dword ptr -4 push ebp mov ebp, esp ; Открываем кадр стека push ecx ; Резервируем место для локальной переменной mov [ebp+var_a], 0 ; Присваиваем локальной переменной var_a значение 0 loc_40100B: ; CODE XREF: main+22.j main+35.j ; ^^^^^^^^^^^^^^^^^^^ ; Две перекрестные ссылки, направленные вперед, говорят о том, что это либо ; начало двух циклов (один из которых - вложенный), либо переход в начало ; цикла оператором continue mov eax, [ebp+var_a] ; Загружаем в EAX значение var_a mov ecx, [ebp+var_a] ; Загружаем в ECX значение var_a add ecx, 1 ; Увеличиваем ECX на единицу mov [ebp+var_a], ecx ; Обновляем переменную var_a cmp eax, 0Ah ; Сравниваем значение переменной var_a до увеличения с числом 0xA jge short loc_401037 ; Выход из цикла (переход на команду, следующую за инструкцией, направленной ; вверх - в начало цикла) если var_a >= 0xA cmp [ebp+var_a], 2 ; Сравниваем var_a со значением 0x2 jnz short loc_401024 ; Если var_a != 2, то прыжок на команду, следующую за инструкцией ; безусловного перехода, направленной вверх - в начало цикла ; Очень похоже на условие выхода из цикла, но не будет спешить с выводами! ; Вспомним - в начале цикла нам встретились две перекрестные ссылки ; Безусловный переход "jmp short loc_40100B" как раз образует одну из них ; А кто "отвечает" за другую? ; Чтобы ответить на этот вопрос необходимо проанализировать остальной код цикла jmp short loc_40100B ; Безусловный переход, направленный в начало цикла - это либо конец цикла, ; либо continue ; Предположим, что это конец цикла. Тогда что же представляет собой ; "jge short loc_401037"? Предусловие выхода из цикла? Не похоже - в таком ; случае они прыгало бы гораздо "ближе" - на метку loc_401024 ; А может, "jge short loc_401037" предусловие одного цикла, а ; "jnz short loc_401024" - постусловие другого, вложенного в него? ; Вполне возможно, но маловероятно - в этом случае постусловие представляло бы ; собой условие продолжения, а не завершения цилкла ; Поэтому, с некоторой долей неуверенности, мы можем принять конструкцию ; CMP var_a, 2 \ JNZ loc_401024 \ JMP loc_40100B за <b>if (a==2) continue</b> loc_401024: ; CODE XREF: main+20.j mov edx, [ebp+var_a] push edx push offset asc_406030 ; "%x\n" call _printf add esp, 8 ;<b> printf("%x\n",var_a)</b> jmp short loc_40100B ; А вот это - явно конец цикла, т.к. jmp short loc_40100B - самая ; последняя ссылка на начало цикла ; Итак, подытожим, что мы имеем: ; Условие, расположенное в начале цикла, крутит этот цикл до тех пор, пока ; var_a < 0xA, причем инкремент параметра цикла происходит до его сравнения ; Затем следует еще одно условие, возвращающее управление в начало цикла, если ; var_a == 2. Строй замыкает оператор цикла printf и безусловный переход в его ; начало. Т.е. ; ; Начало цикла: <-----------! <--! ; Инкремент переменной var_a ! ! ; условие "далекого" выхода -------! ! ! ; условие "ближнего" продолжения --)----! ! ; тело цикла ! ! ; безусловный переход в начало ----)---------! ; конец цикла <----! ; ; Условие "ближнего" продолжение не может быть концом цикла, т.к. тогда условию ; "далекого" выхода пришлось выйти аж из надлежащего цикла, на что ни break, ; ни другие операторы не способны. Таким образом, условие ближнего продолжения ; может быть только оператором continue и на языке Си всю эту конструкция ; будет выглядеть так: ;<b> while(a++<10)</b> // <-- инкремент var_a и условие далекого выхода ;<b> { ; if (a == 2) continue;</b> // <-- условие ближнего продолжения ;<b> printf(%x\n",var_a);</b> // <-- тело цикла ;<b> } </b> // <-- безусловный переход на начало цикла loc_401037: ; CODE XREF: main+1A.j main+5D.j ; ^^^^^^^^^ ; Начало цикла cmp [ebp+var_a], 2 ; Сравниваем переменную var_a со значением 0x2 jnz short loc_40103F ; Если var_a != 2, то продолжение цикла jmp short loc_401050 ; Переход к коду проверки условия продолжения цикла ; Это бесспорно "continue" и вся конструкция выглядит так: ;<b> if (a==2) continue</b> loc_40103F: ; CODE XREF: main+3B.j mov eax, [ebp+var_a] push eax push offset asc_406034 ; "%x\n" call _printf add esp, 8 ;<b> printf("%x\n", var_a)</b> loc_401050: ; CODE XREF: main+3D.j mov ecx, [ebp+var_a] sub ecx, 1 mov [ebp+var_a], ecx ;<b> --var_a</b> cmp [ebp+var_a], 0 ; Сравнение var_a с нулем jg short loc_401037 ; Пока var_a > 0 продолжать цикл. Похоже на постусловие верно? Тогда: ; do ; { ; if (a==2) continue; ; printf("%x\n", var_a); ; } while (--var_a > 0); ; mov esp, ebp pop ebp retn main endp  Листинг 200
  А теперь посмотрим, как повлияла оптимизация ("/Ox") на вид циклов:
Код (Text):
main proc near ; CODE XREF: start+AF.p push esi mov esi, 1 loc_401006: ; CODE XREF: main+1F.j ; ^^^^^^^^^^^^^^^^^^^^ ; Начало цикла cmp esi, 2 jz short loc_401019 ; Переход на loc_401019, если ESI == 2 push esi push offset asc_406030 ; "%x\n" call _printf add esp, 8 ;<b> printf("%x\n", ESI)</b> ; Прим: эта ветка выполняется только если ESI !=2 ; Следовательно, ее можно изобразить так: ;<b> if (ESI != 2) printf("%x\n", ESI)</b> loc_401019: ; CODE XREF: main+9.j mov eax, esi inc esi ; ESI++; cmp eax, 0Ah jl short loc_401006 ; Продолжение цикла пока (ESI++ < 0xA) ; Итого: ;<b> do ; { ; if (ESI != 2) printf("%x\n", ESI); ; } while (ESI++ < 0xA) ;</b> ; А что, выглядит вполне читабельно, не правда ли? Ни чуть не хуже, чем ;<b> if (ESI == 2) continue</b> ; loc_401021: ; CODE XREF: main+37.j ; ^^^^^^^^ ; Начало цикла cmp esi, 2 jz short loc_401034 ; Переход на loc_401034, если ESI == 2 push esi push offset asc_406034 ; "%x\n" call _printf add esp, 8 ;<b> printf("%x\n",ESI);</b> ; Прим. эта ветка выполняется лишь когда ESI != 2 loc_401034: ; CODE XREF: main+24.j dec esi ; --ESI test esi, esi jg short loc_401021 ; Условие продолжение цикла - крутить кака ESI > 0 ; Итого: ;<b> do ; { ; if (ESI != 2) ; { ; printf("%x\n", ESI); ; } ; } while (--ESI > 0)</b> ; pop esi retn main endp  Листинг 201
  Остальные компиляторы сгенерируют приблизительно такой же код. Общим для всех случаев будет то, что на циклах с предусловием оператор continue практически неотличим от вложенного цикла, а на циклах с постусловием continue эквивалентен элементарному ветвлению.
  Наконец, настала очередь циклов for, вращающих несколько счетчиков одновременно. Рассмотрим следующий пример:
Код (Text):
main() { int a; int b; for (a = 1, b = 10; a < 10, b > 1; a++, b --) printf("%x %x\n", a, b); }  Листинг 202 Демонстрация идентификации циклов for с несколькими счетчиками
  Результат его компиляции компилятором Microsoft Visual C++ 6.0 должен выглядеть так:
Код (Text):
main proc near ; CODE XREF: start+AF.p var_b = dword ptr -8 var_a = dword ptr -4 push ebp mov ebp, esp ; Открываем кадр стека sub esp, 8 ; Резервируем память для двух локальных переменных mov [ebp+var_a], 1 ; Присваиваем переменной var_a значение 0x1 mov [ebp+var_b], 0Ah ; Присваиваем переменной var_b значение 0xA jmp short loc_401028 ; Прыжок на код проверки условия выхода из цикла ; Это характерная черта не оптимизированных циклов for loc_401016: ; CODE XREF: main+43.j ; ^^^^^^^^^ ; Перекрестная ссылка, направленная вниз, говорит о том, что это - начало цикла ; А выше мы уже выяснили, что тип цикла - for mov eax, [ebp+var_a] add eax, 1 mov [ebp+var_a], eax ;<b> var_a++</b> mov ecx, [ebp+var_b] sub ecx, 1 mov [ebp+var_b], ecx ;<b> var_b--</b> loc_401028: ; CODE XREF: main+14.j cmp [ebp+var_b], 1 jle short loc_401045 ; Выход из цикла, если var_b <= 0x1 ; Обратите внимание: выполняется проверка лишь одного (второго слева) счетчика! ; Выражение (a1,a2,a3,...an) компилятор считает бессмысленным и берет лишь an ; молчаливо отбрасывая все остальное ; (из известных мне компиляторов на это ругается один WATCOM) ; В данном случае проверяется лишь условие (b > 1), а (a < 10) игнорируется!!! mov edx, [ebp+var_b] push edx mov eax, [ebp+var_a] push eax push offset aXX ; "%x %x\n" call _printf add esp, 0Ch ;<b> printf("%x %x\n", var_a, var_b)</b> jmp short loc_401016 ; Конец цикла ; Итак, данный цикл можно представить как: ;<b> while(1) ; { ; var_a++; ; var_b--; ; if (var_b <= 0x1) break; ; printf("%x %x\n", var_a, var_b) ; }</b> ; ; Но по соображениям удобочитаемости имеет смысл скомпоновать это код в for ;for (var_a=1,var_b=0xA;var_b>1;var_a++,var_b--) printf("%x %x\n",var_a,var_b) ; loc_401045: ; CODE XREF: main+2C.j mov esp, ebp pop ebp ; Закрываем кадр стека retn main endp  Листинг 203
  Оптимизированный вариант программы рассматривать не будем, т.к. это не покажет нам ничего нового. Какой бы компилятор мы не выбрали - выражения инициализации и модификации счетчиков будут обрабатываться вполне корректно в порядке их объявления в тексте программы, а вот множественные выражения продолжения цикла не умеет правильно обрабатывать ни один компилятор! © Крис Касперски
Идентификация циклов
Дата публикации 16 июн 2002