Идентификация циклов — Архив WASM.RU

Все статьи

Идентификация циклов — Архив 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 слева приведен цикл с условием завершения, а справа - с условием продолжения. Как видно, цикл с условием завершения на одну команду короче! Поэтому, практически все компиляторы (даже не оптимизирующие) всегда генерируют левый вариант. (А некоторые, особо одаренные, даже умеют превращать циклы с предусловием в еще более эффективные циклы с пост-условием - см. "Циклы с условием в конце").

    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)" в общем случае компилируется в следующий код (обратите внимание: в переходе использовалась та же самая операция отношения, что и в исходном цикле, - красота и никаких ошибок при декомпиляции):

repeat: <---------! INC A ! CMP A, 10 ! JB repeat---! end
  Листинг 181

Вернувшись страницей назад, сравним код цикла с пост-условием с кодом цикла с предусловием. Не правда ли, цикл с условием в конце компактнее и быстрее? Некоторые компиляторы (например, 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 языка высокого уровня. Смотрите:

MOV A, xxx          ; Инициализация переменной "счетчика"
JMP conditional     ; Переход к проверке условия продолжения цикла
repeat:             ; Начало цикла
...                 ; // ТЕЛО 
...                 ; //      ЦИКЛА
ADD A, xxx [SUB A, xxx]; Модификация счетчика
conditional:        ; Проверка условия продолжения цикла
CMP A, xxx          ; ^
Jxx repeat          ; Переход в начало цикла, если условие истинно
  Листинг 182

  Непосредственный прыжок вниз может быть результат компиляции и цикла for, и оператора GOTO, но GOTO сейчас не в моде и используется крайне редко, а без него оператор условного перехода "IF - THEN" не может прыгнуть непосредственно в середину цикла while! Выходит, изо всех "кандидатов" остается только цикл for.
  Некоторые, особо продвинутые компиляторы (Microsoft Visual C++, Borland C++, но не WATCOM C), поступают хитрее: анализируя код они еще на стадии компиляции пытаются определить: выполняется ли данный цикл хотя бы один раз и, если видят, что он действительно выполняется, превращают for в типичный цикл с постусловием:

MOV A, xxx      ; Инициализация переменной "счетчика"
repeat:         ; Начало цикла
...             ; // ТЕЛО
...             ; //      ЦИКЛА
ADD A, xxx [SUB A, xxx]; Модификация счетчика
CMP A, xxx      ; Проверка условия продолжения цикла
Jxx repeat      ; Переход в начало цикла, если условие истинно
  Листинг 183

  Наконец, самые крутые компиляторы (из которых автор на вскидку может назвать один лишь Microsoft Visual C++ 6.0) могут даже заменять циклы с приращением на циклы с убыванием при условии, что параметр цикла не используется операторами цикла, а лишь прокручивает цикл определенное число раз. Зачем это компилятору? Оказывается, циклы с убыванием гораздо короче - однобайтовая инструкция DEC не только уменьшает операнд, но и выставляет Zero-флаг при достижении нуля. В результате, в команде CMP A, xxx отпадает всякая необходимость.

MOV A, xxx      ; Инициализация переменной "счетчика"
repeat:         ; Начало цикла
...             ; // ТЕЛО
...             ; //      ЦИКЛА
DEC A           ; Декремент счетчика
JNZ repeat      ; Повтор, пока A != 0
  Листинг 184
  Таким образом, в зависимости от настроек и характера компилятора, циклы for могут транслироваться и в циклы с предусловием, и в циклы с постусловием, начинающими свое выполнение с проверки условия продолжения цикла. Причем, условие продолжения может инвертироваться в условие завершения, а возрастающий цикл может "волшебным" образом превращаться в убывающий.
  Такая неоднозначность затрудняет идентификацию циклов for, - надежно отождествляются лишь циклы, начинающиеся с проверки постусловия (т.к. они не могут быть отображены на do без использования GOTO). Во всех остальных случаях никаких строгих рекомендаций по распознаванию for дать невозможно.
  Скажем так: если логика исследуемого цикла синтаксически удобнее выражается через оператор for, то и выражайте ее через for! В противном случае используйте while или do (repeat\until) для циклов с пред- и пост- условием соответственно.
  И в заключение пара слов о "кастрированных" циклах - язык Си позволяет опустить инициализацию переменной цикла, условие выхода из цикла, оператор приращения переменной или все это вместе. При этом for вырождается во while, и становится практически неотличимым от него.

  ::Циклы с условием в середине. Популярные языки высокого уровня непосредственно не поддерживают циклы с условием в середине, хотя необходимость в них возникает достаточно часто. Поэтому, программисты их реализуют на основе уже имеющихся циклов while (while\do) и оператора выхода из цикла break. Например:

while(1)               repeat:
{                              ...
    ...                        CMP xxx
    if (условие) break;        Jxx end
    ...                        ...
}                       JMP repeat
                        end:
  Листинг 185

  Компилятор (если он не совсем Осел - И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;..." компилируется приблизительно так:

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  ; Переход в начало цикла
  Листинг 186

  ::Сложные условия. До сих пор, говоря об условиях завершения и продолжения цикла, мы рассматривали лишь элементарные условия отношения, в то время как практически все языки высокого уровня допускают использование составных условий. Однако составные условия можно схематично изобразить в виде абстрактного "черного ящика" с входом/выходом и логическим двоичными деревом внутри. Построение и реконструкция логических деревьев подробно рассматриваются в главе "Идентификация IF - THEN - ELSE" здесь же нас интересует не сами условия, а организация циклов.

  ::Вложенные циклы. Циклы - понятное дело - могут быть и вложенными. Казалось бы, какие проблемы? Начало каждого цикла надежно определяется по перекрестной ссылке, направленной вниз. Конец цикла - условный или безусловный переход на его начало. У каждого цикла только одно начло и только один конец (хотя условий выхода может быть сколько угодно, но это - другое дело). Причем, циклы не могут пересекаться - если между началом и концом одного цикла встречается начало другого цикла, то этот цикл - вложенный.
  Но не все так просто: тут есть два подводных камня. Первый: оператор continue в циклах с предусловием, второй - сложные условия продолжения цикла с постусловием. Рассмотрим их подробнее.
  Поскольку, continue в циклах с предусловием, транслируется в безусловный переход, направленный "вверх", он становится практически неотличим от конца цикла. Смотрите:

while(условие1)
{
    ...
    if (условие2) continue;
    ...
}

транслируется в:

NOT условие1 выхода из цикла---------!  <-!  <-----!
...                                  !    !        !
если НЕ условие2 GOTO continue ---!  !    !        !
безусловный переход в начало ------)--)---!        !
continue:                   <-----!  !             !
...                                  !             !
безусловный переход в начало ---------)------------!
конец всего <------------------------!

  Два конца и два начала вполне напоминают два цикла, из которых один вложен в другой. Правда, начала обоих циклов совмещены, но ведь может же такое быть, если в цикл с пост условием вложен цикл с предусловием? На первый взгляд да, но если подумать, то... ай-ай-ай! А ведь условие1 выхода из цикла прыгает аж за второй конец! Если это предусловие вложенного цикла, то оно прыгало бы за первый конец. А если условие1 - это предусловие материнского цикла, то конец вложенного цикла не смог бы передать на него управление. Выходит, это не два цикла, а один. А первый "конец" - результат трансляции оператора continue.

  С разбором сложных условий продолжения цикла с постусловием дела обстоят еще лучше. Рассмотрим такой пример:

do
{
...
} while(условие1 || условие2);

Результат его трансляции в общем случае будет выглядеть так:

...                 <---! <-!
условие продолжения1 ---!   !
условие прололжения2 -------!

Ну, чем не:

do
{
    do
    {
        ...
    }while(условие1)

}while(условие2)

  Строго говоря, предложенный вариант является логически верным, но синтаксически некрасивым. Материнский цикл крутит в своем теле один лишь вложенный цикл и не содержит никаких других операторов. Так зачем он тогда, спрашивается, нужен? Объединить его с вложенным циклом в один!

  Дизассемблерные листинги примеров. Давайте для закрепления сказанного рассмотрим несколько живых примеров.
  Начнем с самого простого - с циклов while\do:

#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 с настройками по умолчанию должен выглядеть так:

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 необходимо инвертировать условие выхода
    ; из цикла на условие продолжения цикла (Т.е. заменить >= на <)
    ; Сделав это, мы получаем:
    ; while (var_a++ < 0xA)... 
    ; 

    // Начало тела цикла
    push   offset aOperatorCiklaW ; "Оператор цикла while\n"
    call   _printf
    add    esp, 4
    ; printf("Оператор цикла while\n")

    jmp    short loc_40100B
    ; Безусловный переход, направленный назад, на метку loc_40100B
    ; Между loc_40100B и jmp short loc_40100B есть только одно условие
    ; выхода из цикла - jge short loc_40102B, значит, исходный цикл
    ; выглядел так:
    ; while (var_a++ < 0xA) printf("Оператор цикла while\n")

loc_40102B:                ; CODE XREF: main_401000+1A.j
                           ; main_401000+45.j
                           ; ^^^^^^^^^^^^^^^^
    ; // Это начало цикла с пост-условием
    ; // Однако на данном этапе мы этого еще не знаем, хотя и можем догадываться
    ; // благодаря наличию перекрестной ссылки, направленной вниз

    ; Ага, никакого условия в начале цикла не присутствует, значит, это цикл
    ; с условием в конце или середине
    push   offset aOperatorCiklaD ; "Оператор цикла do\n"
    call   _printf
    add    esp, 4
    ; printf("Оператор цикла do\n")
    ; // Тело цикла

    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:
    ; do printf("Оператор цикла do\n"); while (--a > 0)
    ;
    ; // Для повышения читабельности дизассемблерного текста рекомендуется
    ; // заменить префиксы loc_ в начале цикла на while и do (repeat) в циклах
    ; // с пред- и пост- условием соответственно

    mov    esp, ebp
    pop   ebp
    ; Закрываем кадр стека
    retn
main    endp

  Листинг 188

  Совсем другой результат получится если включить оптимизацию. Откомпилируем тот же самый пример с ключом "/Ox" (максимальная оптимизация) и посмотрим на результат, выданный компилятором:

main        proc near        ; CODE XREF: start+AF.p
    push   esi
    push   edi
    ; Сохраняем регистры в стеке

    mov    esi, 1
    ; Присваиваем ESI значение 0х1
    ; Внимание - взгляните на исходный код - ни одна из переменных не имела
    ; такого значения!

    mov    edi, 0Ah
    ; Присваиваем EDI значение 0xA. Ага, это константа для проверки условия
    ; выхода из цикла

loc_40100C:                ; CODE XREF: main+1D.j
                           ; ^^^^^^^^^^^^^^^^^^^^
    ; Судя по перекрестной ссылке, направленной вниз, этот - цикл!

    push   offset aOperatorCiklaW ; "Оператор цикла while\n"
    call   _printf
    add    esp, 4
    ; printf("Оператор цикла while\n")
    ; ...тело цикла 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) ==
    ; do ... while ((int a=1) < 10)
    ; 
    ; Причем, что интересно, он не сравнивал переменную цикла с константой,
    ; а поместил константу в регистр и уменьшал его до тех пор, пока тот не стал
    ; равен нулю! Зачем? А затем, что так короче, да и работает быстрее
    ; Что ж, это все хорошо, но как нам декомпилировать этот цикл?
    ; Непосредственное отображение на язык Си дает следующую конструцию:
    ; var_ ESI = 1; var _EDI = 0xA;
    ; do {
    ;;printf("Оператор цикла while\n"); var_EDI--; var_ESI++;
    ; } while(var_EDI > 0)
    ; 
    ; Правда, коряво и запутано? Что-ж, тогда попытаемся избавится от одной
    ; из двух переменных. Это действительно возможно, т.к. они модифицируются
    ; синхронно, и var_EDI = 0xB - var_ESI
    ; ОК, выполняем подстановку:
    ; var_ ESI = 1; var _EDI = 0xB - var_ESI ; (== 0xA;)
    ; do {
    ;;printf("Оператор цикла while\n"); var_EDI--; var_ESI++;
    ;                                   ^^^^^^^^^^
    ; Это мы вообще сокращаем, т.к. var_EDI уже выражена через var_ESI
    ; } while((0xB - var_ESI) > 0); (== var_ESI > 0xB)
    ; 
    ; Что, ж уже получается нечто осмысленное:
    ; 
    ; var_ ESI = 1; var _EDI == 0xA;
    ; do {
    ;;    printf("Оператор цикла while\n"); var_ESI++;
    ; } while(var_ESI > 0xB)
    ; На этом можно и остановится, а можно и пойти дальше, преобразовав цикл
    ; с пост-условием в более наглядный цикл с предусловием
    ;
    ; var_ ESI = 1; var _EDI == 0xA; <-- var_EDI не используется, 
    ;можно сократить
    ; while (var_ESI < = 0xA) {
    ;;    printf("Оператор цикла while\n"); var_ESI++;
    ; }
    ; Но и это не предел выразительности: во-первых var_ESI < = 0xA эквивалентно
    ; var_EDI < 0xB, а во-вторых, поскольку, переменная var_ESI используется лишь
    ; как счетчик, ее начальное значение можно безбоязненно привести к нулевому
    ; значению, а операцию инкремента внести в сам цикл:

    ; var_ ESI = 0;
    ; while (var_ESI++ < 0xA) <-- вычитаем единицу из левой 
    ; и правой половины
    ; printf("Оператор цикла while\n");
    ; 
    ; Ну, разве не красота?! Сравните этот вариант с первоначальным - 
    ; насколько он стал яснее и понятнее

loc_40101F:                ; CODE XREF: main+2F.j
;                           ^^^^^^^^^^^^^^^^^^^^
; Перекрестная ссылка, направленная вниз, говорит о том, что это - начало цилка

    ; // Предусловия нет - значит, это цикл do

    push   offset aOperatorCiklaD ; "Оператор цикла do\n"
    call   _printf
    add    esp, 4
    ; printf("Оператор цикла do\n");

    dec    esi
    ; Уменьшаем var_ESI

    test    esi, esi
    ; Проверка ESI на равенство нулю

    jg     short loc_40101F
    ; Продолжать цикл, пока var_ESI > 0
    ; 
    ; ОК. Этот цикл легко и непринужденно отображается на язык Си:
    ; do printf("Оператор цикла do\n"); while (--var_ESI > 0 )

    pop    edi
    pop    esi
    ; Восстанавливаем сохраненные регистры

    retn
main        endp

  Листинг 189

  Несколько иначе оптимизирует циклы компилятор Borland C++ 5.x. Смотрите:

_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
    ; printf("Оператор цикла while\n")

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 оптимизировал код! Он расположил условие в конце цикла,
    ; но, чтобы не транслировать цикл с предусловием в цикл с постусловием,
    ; просто начал выполнение цикла с этого самого условия!
    ; 
    ; Отображение этого цикла на язык Си дает:

    ; for (int a=0; a < 10; a++) printf("Оператор цикла while\n")
    ; 
    ; и, хотя подлинный цикл выглядел совсем не так, наш вариант нечем не хуже!
    ; (а может даже и лучше - нагляднее)

loc_401097:                ; CODE XREF: _main+29.j
;                           ^^^^^^^^^^^^^^^^^^^^^
    ; Начало цикла!

    ; Условия нет - значит, это цикл с постусловием

    push   offset aOperatorCiklaD ; "Оператор цикла do\n"
    call   _printf
    pop    ecx
    ; printf("Оператор цикла do\n")

    dec    ebx
    ; --var_EBX

    test    ebx, ebx
    jg     short loc_401097
    ; Продолжать цикл, пока var_EBX > 0
    ; do printf("Оператор цикла do\n"); while (--var_EBX > 0)

     xor    eax, eax
    ; return 0

    pop    ebx
    pop    ebp
    ; Восстанавливаем сохраненные регистры

    retn
_main        endp

  Листинг 190

  Остальные компиляторы генерируют аналогичный или даже еще более примитивный и очевидный код, поэтому не будем подробно их разбирать, а лишь кратно опишем используемые ими схемы трансляции.
  Компилятор Free Pascal 1.x ведет себя аналогично компилятору Borland C++ 5.0, всегда помещая условие в конец цикла и начиная с него выполнение while-циклов.
  Компилятор WATCOM C не умеет преобразовывать циклы с предусловием в циклы с постусловием, вследствие чего располагает условие выхода из цикла в начале while-циклов, а в их конец вставляет безусловный jump. (Классика!)
  Компилятор GCC вообще не оптимизирует циклы с предусловием, генерируя самый неоптимальный код. Смотрите:

    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. Рассмотрим следующий пример:

#include <stdio.h>

main()
{
    int a;
    for (a=0;a<10;a++)    printf("Оператор цикла for\n");
}

  Листинг 192 Демонстрация идентификации циклов for

  Результат компиляции Microsoft Visual C++ 6.0 с настройками по умолчанию будет выглядеть так:

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
    ; printf("Оператор цикла for\n")

    jmp    short loc_40100D
    ; Безусловный переход в начало цикла
    ; 
    ; Итак, что мы имеем?
    ; инициализация переменной var_a
    ; переход на проверку условия выхода из цикла ----------!
    ; инкремент переменной var_a <---------------------!    !
    ; проверка условия относительно var_a <----------- ! ---!
    ; прыжок на выход из цикла, если условие истинно---!----!
    ; вызов printf                                     !    !
    ; переход в начало цикла --------------------------!    !
    ; конец цикла <-----------------------------------------!
    ; 
    ; Проверка на завершения, расположенная в начале цикла, говорит о том, что
    ; это цикл с предусловием, но непосредственно выразить его через 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

  А теперь задействуем оптимизацию и посмотрим, как видоизмениться наш цикл:

main        proc near        ; CODE XREF: start+AF.p
    push   esi
    mov    esi, 0Ah
    ; Инициализируем переменную - счетчик
    ; Внимание! В исходном коде начальное значение счетчика равнялось нулю!

loc_401006:                ; CODE XREF: main+14.j
    push   offset aOperatorCiklaF ; "Оператор цикла for\n"
    call   _printf
    add    esp, 4
    ; printf("Оператор цикла for\n")
    ; Выполняем оператор цикла! Причем безо всяких проверок!
    ; Хитрый компилятор проанализировал код и понял, что цикл выполняется
    ; по крайней мере один раз!

    dec    esi
    ; Уменьшаем счетчик, хотя в исходном коде программы мы его увеличивали!
    ; Ну, правильно - dec \ jnz намного короче INC\ CMP reg, const\ jnz xxx
    ; Ой и мудрит компилятор! Кто же ему давал право так изменять цикл?!
    ; А очень просто - он понял, что параметр цикла в самом цикле используется
    ; только как счетчик, и нет никакой разницы - увеличивается он
    ; с каждой итерацией или уменьшается!

    jnz    short loc_401006
    ; Переход в начало цикла если ESI > 0
    ;
    ; М да, по внешнему виду это типичный
    ; a = 0xa; do printf("Оператор цикла for\n"); while (--a)
    ; 
    ; Если вас устраивает читабельность такой формы записи - оставляйте ее, а нет:
    ; for (a = 0; a < 10; a++) Оператор цикла for\n")
    ; 
    ; Постой, постой! На каком основании автор выполнил такое преобразование?!
    ; А на том самом - что и компилятор: раз параметр цикла используется только
    ; как счетчик, законна любая запись, выполняющая цикл ровно десять раз -
    ; остается выбрать ту, которая удобнее (с эстетической точки зрения)
    ; Никто же не будет утверждать, что
    ; for (a = 10; a > 0; a--) более привычно чем for (a = 0; a < 10; a++)?

    pop    esi
    retn
main        endp

  Листинг 194

  А что скажет нам товарищ Borland C++ 5.0? Компилируем и смотрим:

_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. Рассмотрим следующий пример:

#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 с настройками по умолчанию должен выглядеть так:

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 - произойдет выход из цикла
    ; Словом, все эти три инструкции - глупый и бесполезный код цикла
    ; while (1)

    push   offset a1iOperator ; "1й оператор\n"
    call   _printf
    add    esp, 4
    ; printf("1й оператор\n")

    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
    ; Но куда этот переход? Во-первых, переход направлен вниз, т.е. это уже
    ; не переход к началу цикла, следовательно и условие - не условие цикла, а
    ; результат компиляции конструкции IF - THEN
    ; Второе - переход прыгает на первую команду, следующую за безусловным
    ; jump loc_401041, передающим управление инструкции, следующей
    ; за командной jmp short loc_401075 - безусловного перехода, направленного
    ; вверх - в начало цикла
    ; Следовательно, jmp    short loc_401041 осуществляет выход из цикла, а
    ; jle short loc_401032 - продолжает его выполнение

    jmp    short loc_401041
    ; ОК, - это переход на завершение цикла. А кто у нас завершает цикл?
    ; Ну, конечно же, break! Следовательно, окончательная декомпиляции выглядит так
    ; if (++var_a > 0xA) break
    ; Мы инвертировали "<=" в ">", т.к. JLE передает управление на код продолжения
    ; цикла, а ветка THEN в нашем случае - на break

loc_401032:                ; CODE XREF: main+2E.j
;                           ^^^^^^^^^^^^^^^^^^^^^
; Перекрестная ссылка направлена вверх - следовательно, это не начало цикла

    push   offset a2iOperator ; "2й оператор\n"
    call   _printf
    add    esp, 4
    ; printf("2й оператор\n")

    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
    ; printf("1й оператор\n")

    mov    edx, [ebp+var_a]
    sub    edx, 1
    mov   [ebp+var_a], edx
    ; --var_a

    cmp    [ebp+var_a], 0
    ; Сравниваем var_a со значением 0x0

    jge    short loc_40105F
    ; Переход вниз, если var_a >= 0
    ; Смотрите: оператор break цикла do ничем не отличается от break цикла while!
    ; Поэтому, не будем разглагольствовать, а сразу его декомпилируем!
    ; if (var_a < 0) ...

    jmp    short loc_401075
    ; ...break

loc_40105F:                ; CODE XREF: main+5B.j
    push   offset a2iOperator_0 ; "2й оператор\n"
    call   _printf
    add    esp, 4
    ; printf("2й оператор\n")

    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" и посмотрим:

main        proc near        ; CODE XREF: start+AF.p
    push    esi
    ; Сохраняем ESI в стеке

    xor    esi, esi
    ; Присваиваем ESI значение 0
    ; var_ESI = 0;

loc_401003:               ; CODE XREF: main+23.j
;                          ^^^^^^^^^^^^^^^^^^^^^
; Перекрестная ссылка, направленная вперед
; Это - начало цикла

    push   offset a1iOperator ; "1й оператор\n"
    call   _printf
    add    esp, 4
    ; printf("1й оператор\n")
    ; 
    ; Ага! Проверки на дорогах нет, значит, это цикл с постусловием
    ; (или условием в середине)

    inc    esi
    ; ++var_ESI

    cmp    esi, 0Ah
    ; Сравниваем var_ESI со значением 0xA

    jg     short loc_401025
    ; Выход из цикла, если var_ESI > 0xA
    ; Поскольку, данная команда - не последняя в теле цикла,
    ; это цикл с условием в середине
    ; if (var_ESI > 0xA) break

    push   offset a2iOperator ; "2й оператор\n"
    call   _printf
    add    esp, 4
    ; printf("2й оператор\n")

    jmp    short loc_401003
    ; Безусловный переход в начало цикла
    ; Как видно, оптимизирующий компилятор выкинул никому ненужную проверку
    ; условия, упростив код и облегчив его понимание:
    ; Итак:
    ; var_ESI = 0
    ; for (;;)     <-- вырожденный for представляет собой бесконечный цикл
    ; {
    ;     printf("1й оператор\n");
    ;     ++var_ESI;
    ;     if (var_ESI > 0xA) break;
    ;     printf("2й оператор\n");
    ; }

loc_401025:                ; CODE XREF: main+14.j
;                           ^^^^^^^^^^^^^^^^^^^^^
; Это не начало цикла!

    push   offset a1iOperator_0 ; "1й оператор\n"
    call    _printf
    add    esp, 4
    ; printf("1й оператор\n")
    ; Хм, как же это не начало цикла?! Очень похоже!

    dec    esi
    ; --var_ESI

    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
    ; printf("2й оператор\n")
    ; Только странно, что начало цикла начинается с его, с позволения сказать,
    ; середины...

    push   offset a1iOperator_0 ; "1й оператор\n"
    call   _printf
    add    esp, 8
    ; printf("1й оператор\n")
    ;
    ; ???!!! Что за чудеса творятся? Во-первых, вызов первого оператора второго
    ; цикла уже встречался ранее, во-вторых, не может же следом за серединой цикла
    ; следовать его начало?!

    dec    esi
    ; --var_ESI

    jnz    short loc_401036
    ; Продолжение цикла, пока var_ESI != 0

loc_401050:                ; CODE XREF: main+33.j
    ; Конец цикла
    ; Да... тут есть над чем подумать!
    ; Компилятор нормально "перевалил" первую строку цикла
    ; printf("1й оператор\n")
    ; а затем "напоролся" на ветвление:
    ; 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. Возьмем следующий пример:

#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 с настройками по умолчанию будет выглядеть так:

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 за if (a==2) continue

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
    ; printf("%x\n",var_a)

    jmp    short loc_40100B
    ; А вот это - явно конец цикла, т.к. jmp short loc_40100B - самая
    ; последняя ссылка на начало цикла
    ; Итак, подытожим, что мы имеем:
    ; Условие, расположенное в начале цикла, крутит этот цикл до тех пор, пока
    ; var_a < 0xA, причем инкремент параметра цикла происходит до его сравнения
    ; Затем следует еще одно условие, возвращающее управление в начало цикла, если
    ; var_a == 2. Строй замыкает оператор цикла printf и безусловный переход в его
    ; начало. Т.е.
    ; 
    ; Начало цикла:             <-----------! <--!
    ; Инкремент переменной var_a            !    !
    ; условие "далекого" выхода -------!    !    !
    ; условие "ближнего" продолжения --)----!    !
    ; тело цикла                       !         !
    ; безусловный переход в начало ----)---------!
    ; конец цикла                 <----!
    ; 
    ; Условие "ближнего" продолжение не может быть концом цикла, т.к. тогда условию
    ; "далекого" выхода пришлось выйти аж из надлежащего цикла, на что ни break,
    ; ни другие операторы не способны. Таким образом, условие ближнего продолжения
    ; может быть только оператором continue и на языке Си всю эту конструкция
    ; будет выглядеть так:
    ; while(a++<10)                // <-- инкремент var_a и условие далекого выхода
    ; {
    ;     if (a == 2) continue;    // <-- условие ближнего продолжения
    ;     printf(%x\n",var_a);     // <-- тело цикла 
    ; }                            // <-- безусловный переход на начало цикла

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" и вся конструкция выглядит так:
    ; if (a==2) continue

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
    ; printf("%x\n", var_a)

loc_401050:                    ; CODE XREF: main+3D.j
    mov    ecx, [ebp+var_a]
    sub    ecx, 1
    mov    [ebp+var_a], ecx
    ; --var_a

    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") на вид циклов:

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
    ; printf("%x\n", ESI)
    ; Прим: эта ветка выполняется только если ESI !=2
    ; Следовательно, ее можно изобразить так:
    ; if (ESI != 2) printf("%x\n", ESI)

loc_401019:                ; CODE XREF: main+9.j
    mov    eax, esi
    inc    esi
    ; ESI++;

    cmp    eax, 0Ah
    jl     short loc_401006
    ; Продолжение цикла пока (ESI++ < 0xA)
    ; Итого:
    ; do
    ; {
    ;     if (ESI != 2) printf("%x\n", ESI);
    ; } while (ESI++ < 0xA)
    ;
    ; А что, выглядит вполне читабельно, не правда ли? Ни чуть не хуже, чем
    ; if (ESI == 2) continue
    ; 

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
    ; printf("%x\n",ESI);
    ; Прим. эта ветка выполняется лишь когда ESI != 2

loc_401034:                ; CODE XREF: main+24.j
    dec    esi
    ; --ESI

    test   esi, esi
    jg     short loc_401021
    ; Условие продолжение цикла - крутить кака ESI > 0
    ; Итого:
    ; do
    ; {
    ;     if (ESI != 2)
    ;    {
    ;        printf("%x\n", ESI);
    ;     }

    ; } while (--ESI > 0)
    ;

    pop    esi
    retn
main        endp

  Листинг 201

  Остальные компиляторы сгенерируют приблизительно такой же код. Общим для всех случаев будет то, что на циклах с предусловием оператор continue практически неотличим от вложенного цикла, а на циклах с постусловием continue эквивалентен элементарному ветвлению.

  Наконец, настала очередь циклов for, вращающих несколько счетчиков одновременно. Рассмотрим следующий пример:

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 должен выглядеть так:

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
    ; var_a++

    mov    ecx, [ebp+var_b]
    sub    ecx, 1
    mov    [ebp+var_b], ecx
    ; var_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
    ; printf("%x %x\n", var_a, var_b)

    jmp    short loc_401016
    ; Конец цикла
    ; Итак, данный цикл можно представить как:
    ; while(1)
    ; {
    ;     var_a++;
    ;     var_b--;
    ;     if (var_b <= 0x1) break;
    ;    printf("%x %x\n", var_a, var_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

  Оптимизированный вариант программы рассматривать не будем, т.к. это не покажет нам ничего нового. Какой бы компилятор мы не выбрали - выражения инициализации и модификации счетчиков будут обрабатываться вполне корректно в порядке их объявления в тексте программы, а вот множественные выражения продолжения цикла не умеет правильно обрабатывать ни один компилятор!

2002-2013 (c) wasm.ru