Идентификация циклов

Дата публикации 16 июн 2002

Идентификация циклов — Архив 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):
  1.  
  2.     while:           while:
  3.     CMP A, 10        CMP A, 10
  4.     JAE end          JB continue
  5.     INC A            JMP end
  6.     JMP while        continue:
  7.     end:             INC A
  8.                      JMP while
  9.                      end:
  10.  

  Листинг 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 языка высокого уровня. Смотрите:

Код (Text):
  1.  
  2. MOV A, xxx          ; Инициализация переменной "счетчика"
  3. <b>JMP conditional</b>     ; Переход к проверке условия продолжения цикла
  4. repeat:             ; Начало цикла
  5. ...                 ; // ТЕЛО
  6. ...                 ; //      ЦИКЛА
  7. ADD A, xxx [SUB A, xxx]; Модификация счетчика
  8. conditional:        ; Проверка условия продолжения цикла
  9. CMP A, xxx          ; ^
  10. Jxx repeat          ; Переход в начало цикла, если условие истинно
  11.  
  Листинг 182

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

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

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

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

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

Код (Text):
  1.  
  2. while(1)               repeat:
  3. {                              ...
  4.     ...                        <b>CMP xxx</b>
  5.     if (условие) break;        <b>Jxx end</b>
  6.     ...                        ...
  7. }                       <b>JMP repeat</b>
  8.                         end:
  9.  
  Листинг 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;..." компилируется приблизительно так:

Код (Text):
  1.  
  2. repeat:     ; Начало цикла while
  3. INC A       ; a++
  4. CMP A, 10   ; Проверка условия завершения цикла
  5. JAE end     ; Конец, если a >= 10
  6. CMP A,2     ; if (a == 2) ...
  7. JNZ woo     ; Переход к варианту "иначе", если a != 2
  8. JMP repeat  ; <== continue
  9. woo:        ; // ТЕЛО
  10. ...         ; //       ЦИКЛА
  11. JMP repeat  ; Переход в начало цикла
  12.  
  Листинг 186

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

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

Код (Text):
  1.  
  2. while(условие1)
  3. {
  4.     ...
  5.     if (условие2) continue;
  6.     ...
  7. }
  8.  
  9. транслируется в:
  10.  
  11. NOT условие1 выхода из цикла---------!  <-!  <-----!
  12. ...                                  !    !        !
  13. если НЕ условие2 GOTO continue ---!  !    !        !
  14. безусловный переход в начало ------)--)---!        !
  15. continue:                   <-----!  !             !
  16. ...                                  !             !
  17. безусловный переход в начало ---------)------------!
  18. конец всего <------------------------!
  19.  

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

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

Код (Text):
  1.  
  2. do
  3. {
  4. ...
  5. } while(условие1 || условие2);
  6.  
  7. Результат его трансляции в общем случае будет выглядеть так:
  8.  
  9. ...                 <---! <-!
  10. условие продолжения1 ---!   !
  11. условие прололжения2 -------!
  12.  
  13. Ну, чем не:
  14.  
  15. do
  16. {
  17.     do
  18.     {
  19.         ...
  20.     }while(условие1)
  21.  
  22. }while(условие2)
  23.  

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

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

Код (Text):
  1.  
  2. #include <stdio.h>
  3.  
  4. main()
  5. {
  6.     int a=0;
  7.     while(a++<10) printf("Оператор цикла while\n");
  8.  
  9.     do {
  10.     printf("Оператор цикла do\n");
  11.     } while(--a >0);
  12. }
  13.  

  Листинг 187 Демонстрация идентификации циклов while\do

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

Код (Text):
  1.  
  2. main    proc near       ; CODE XREF: start+AF.p
  3.  
  4. var_a        = dword    ptr -4
  5.  
  6.     push   ebp
  7.     mov    ebp, esp
  8.     ; Открываем кадр стека
  9.  
  10.     push   ecx
  11.     ; Резервируем память для одной локальной переменной
  12.  
  13.     mov    [ebp+var_a], 0
  14.     ; Заносим в переменную var_a значение 0x0
  15.  
  16. loc_40100B:             ; CODE XREF: main_401000+29.j
  17. ;                       ^^^^^^^^^^^^^^
  18.     ; Перекрестная ссылка, направленная вниз, говорит о том, что это начло цикла
  19.     ; Естественно: раз перекрестная ссылка направлена вниз, то переход,
  20.     ; ссылающийся на этот адрес, будет направлен вверх!
  21.  
  22.     mov    eax, [ebp+var_a]
  23.     ; Загружаем в EAX значение переменной var_a
  24.  
  25.     mov    ecx, [ebp+var_a]
  26.     ; Загружаем в EСX значение переменной var_a
  27.     ; (недальновидность компилятора - можно было бы поступить и короче MOV ECX,EAX)
  28.  
  29.     add    ecx, 1
  30.     ; Увеличиваем ECX на единицу
  31.  
  32.     mov    [ebp+var_a], ecx
  33.     ; Обновляем var_a
  34.  
  35.     cmp    eax, 0Ah
  36.     ; Сравниваем старое (до обновления) значение переменной var_a с числом 0xA
  37.  
  38.     jge    short loc_40102B
  39.     ; Если var_a >= 0xA - прыжок "вперед", непосредственно за инструкцию
  40.     ; безусловного перехода, направленного "назад"
  41.     ; Раз "назад", значит, - это цикл, а, поскольку, условие выхода из цикла
  42.     ; проверяется в его начале, то это цикл с предусловием
  43.     ; Для его отображения на цикл while необходимо инвертировать условие выхода
  44.     ; из цикла на условие продолжения цикла (Т.е. заменить >= на <)
  45.     ; Сделав это, мы получаем:
  46.     ; <b>while (var_a++ < 0xA)...</b>
  47.     ;
  48.  
  49.     // Начало тела цикла
  50.     push   offset aOperatorCiklaW ; "Оператор цикла while\n"
  51.     call   _printf
  52.     add    esp, 4
  53.     <b>; printf("Оператор цикла while\n")</b>
  54.  
  55.     jmp    short loc_40100B
  56.     ; Безусловный переход, направленный назад, на метку loc_40100B
  57.     ; Между loc_40100B и jmp short loc_40100B есть только одно условие
  58.     ; выхода из цикла - jge short loc_40102B, значит, исходный цикл
  59.     ; выглядел так:
  60.     <b>; while (var_a++ < 0xA) printf("Оператор цикла while\n")</b>
  61.  
  62. loc_40102B:                ; CODE XREF: main_401000+1A.j
  63.                            ; main_401000+45.j
  64.                            ; ^^^^^^^^^^^^^^^^
  65.     ; // Это начало цикла с пост-условием
  66.     ; // Однако на данном этапе мы этого еще не знаем, хотя и можем догадываться
  67.     ; // благодаря наличию перекрестной ссылки, направленной вниз
  68.  
  69.     ; Ага, никакого условия в начале цикла не присутствует, значит, это цикл
  70.     ; с условием в конце или середине
  71.     push   offset aOperatorCiklaD ; "Оператор цикла do\n"
  72.     call   _printf
  73.     add    esp, 4
  74.     <b>; printf("Оператор цикла do\n")</b>
  75.     ; // Тело цикла
  76.  
  77.     mov    edx, [ebp+var_a]
  78.     ; Загружаем в EDX значение переменной var_a
  79.  
  80.     sub    edx, 1
  81.     ; Уменьшаем EDX на единицу
  82.  
  83.     mov    [ebp+var_a], edx
  84.     ; Обновляем переменную var_a
  85.  
  86.     cmp    [ebp+var_a], 0
  87.     ; Сравниваем переменную var_a с нулем
  88.  
  89.     jg     short loc_40102B
  90.     ; Если var_a > 0, то переход в начало цикла
  91.     ; Поскольку, условие расположено в конце тела цикла, этот цикл - do:
  92.     ; <b>do printf("Оператор цикла do\n"); while (--a > 0)</b>
  93.     ;
  94.     ; // Для повышения читабельности дизассемблерного текста рекомендуется
  95.     ; // заменить префиксы loc_ в начале цикла на while и do (repeat) в циклах
  96.     ; // с пред- и пост- условием соответственно
  97.  
  98.     mov    esp, ebp
  99.     pop   ebp
  100.     ; Закрываем кадр стека
  101.     retn
  102. main    endp
  103.  

  Листинг 188

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

Код (Text):
  1.  
  2. main        proc near        ; CODE XREF: start+AF.p
  3.     push   esi
  4.     push   edi
  5.     ; Сохраняем регистры в стеке
  6.  
  7.     mov    esi, 1
  8.     ; Присваиваем ESI значение 0х1
  9.     ; <i><b>Внимание</b> - взгляните на исходный код - ни одна из переменных не имела
  10.     ; такого значения!</i>
  11.  
  12.     mov    edi, 0Ah
  13.     ; Присваиваем EDI значение 0xA. Ага, это константа для проверки условия
  14.     ; выхода из цикла
  15.  
  16. loc_40100C:                ; CODE XREF: main+1D.j
  17.                            ; ^^^^^^^^^^^^^^^^^^^^
  18.     ; Судя по перекрестной ссылке, направленной вниз, этот - цикл!
  19.  
  20.     push   offset aOperatorCiklaW ; "Оператор цикла while\n"
  21.     call   _printf
  22.     add    esp, 4
  23.     <b>; printf("Оператор цикла while\n")</b>
  24.     ; ...тело цикла while? (растерянно так)
  25.     ; Постой, постой! А где же предусловие?!
  26.  
  27.     dec    edi
  28.     ; Уменьшаем EDI на один
  29.  
  30.     inc    esi
  31.     ; Увеличиваем ESI на один
  32.  
  33.     test    edi, edi
  34.     ; Проверяем EDI на равенство нулю
  35.  
  36.     ja     short loc_40100C
  37.     ; Переход в начало цикла, пока EDI != 0
  38.     ; Так... (задумчиво) Компилятор в порыве оптимизации превратил неэффективный
  39.     ; цикл с предусловием в более компактный и быстрый цикл с пост-условием
  40.     ; Имел ли он на это право? А почему нет?! Проанализировав код, компилятор понял
  41.     ; что данный цикл выполняется, по крайней мере, один раз, следовательно,
  42.     ; скорректировав условие продолжения, его проверку можно вынести в конец цикла
  43.     ; Поэтому-то начальное значение переменной цикла равно единице, а не нулю!
  44.     ; Т.е. while ((int a = 0) < 10) компилятор заменил на
  45.     ; do ... while (((int a = 0)+1) &lt 10) ==
  46.     <b>; do ... while ((int a=1) < 10)</b>
  47.     ;
  48.     ; Причем, что интересно, он не сравнивал переменную цикла с константой,
  49.     ; а поместил константу в регистр и уменьшал его до тех пор, пока тот не стал
  50.     ; равен нулю! Зачем? А затем, что так короче, да и работает быстрее
  51.     ; Что ж, это все хорошо, но как нам декомпилировать этот цикл?
  52.     ; Непосредственное отображение на язык Си дает следующую конструцию:
  53.     <b>; var_ ESI = 1; var _EDI = 0xA;
  54.     ; do {
  55.     ;;printf("Оператор цикла while\n"); var_EDI--; var_ESI++;
  56.     ; } while(var_EDI > 0)</b>
  57.     ;
  58.     ; Правда, коряво и запутано? Что-ж, тогда попытаемся избавится от одной
  59.     ; из двух переменных. Это действительно возможно, т.к. они модифицируются
  60.     ; синхронно, и <b>var_EDI = 0xB - var_ESI</b>
  61.     ; ОК, выполняем подстановку:
  62.     ; <b>var_ ESI = 1; var _EDI = 0xB - var_ESI ; (== 0xA;)
  63.     ; do {
  64.     ;;printf("Оператор цикла while\n"); var_EDI--; var_ESI++;</b>
  65.     ;                                   ^^^^^^^^^^
  66.     ; Это мы вообще сокращаем, т.к. var_EDI уже выражена через var_ESI
  67.     <b>; } while((0xB - var_ESI) > 0); (== var_ESI > 0xB)</b>
  68.     ;
  69.     ; Что, ж уже получается нечто осмысленное:
  70.     ;
  71.     <b>; var_ ESI = 1; var _EDI == 0xA;
  72.     ; do {
  73.     ;;    printf("Оператор цикла while\n"); var_ESI++;
  74.     ; } while(var_ESI > 0xB)</b>
  75.     ; На этом можно и остановится, а можно и пойти дальше, преобразовав цикл
  76.     ; с пост-условием в более наглядный цикл с предусловием
  77.     ;
  78.     <b>; var_ ESI = 1; var _EDI == 0xA; <-- var_EDI не используется,
  79.     ;можно сократить
  80.     ; while (var_ESI &lt = 0xA) {
  81.     ;;    printf("Оператор цикла while\n"); var_ESI++;
  82.     ; }</b>
  83.     ; Но и это не предел выразительности: во-первых var_ESI &lt = 0xA эквивалентно
  84.     ; var_EDI &lt 0xB, а во-вторых, поскольку, переменная var_ESI используется лишь
  85.     ; как счетчик, ее начальное значение можно безбоязненно привести к нулевому
  86.     ; значению, а операцию инкремента внести в сам цикл:
  87.  
  88.     <b>; var_ ESI = 0;
  89.     ; while (var_ESI++ < 0xA) <-- вычитаем единицу из левой
  90.     ; и правой половины
  91.     ; printf("Оператор цикла while\n");</b>
  92.     ;
  93.     ; Ну, разве не красота?! Сравните этот вариант с первоначальным -
  94.     ; насколько он стал яснее и понятнее
  95.  
  96. loc_40101F:                ; CODE XREF: main+2F.j
  97. ;                           ^^^^^^^^^^^^^^^^^^^^
  98. ; Перекрестная ссылка, направленная вниз, говорит о том, что это - начало цилка
  99.  
  100.     ; // Предусловия нет - значит, это цикл do
  101.  
  102.     push   offset aOperatorCiklaD ; "Оператор цикла do\n"
  103.     call   _printf
  104.     add    esp, 4
  105.     <b>; printf("Оператор цикла do\n");</b>
  106.  
  107.     dec    esi
  108.     ; Уменьшаем var_ESI
  109.  
  110.     test    esi, esi
  111.     ; Проверка ESI на равенство нулю
  112.  
  113.     jg     short loc_40101F
  114.     ; Продолжать цикл, пока var_ESI > 0
  115.     ;
  116.     ; ОК. Этот цикл легко и непринужденно отображается на язык Си:
  117.     <b>; do printf("Оператор цикла do\n"); while (--var_ESI > 0 )</b>
  118.  
  119.     pop    edi
  120.     pop    esi
  121.     ; Восстанавливаем сохраненные регистры
  122.  
  123.     retn
  124. main        endp
  125.  

  Листинг 189

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

Код (Text):
  1.  
  2. _main        proc near        ; DATA XREF: DATA:00407044.o
  3.  
  4.     push   ebp
  5.     mov    ebp, esp
  6.     ; Открываем кадр стека
  7.  
  8.     push    ebx
  9.     ; Сохраняем EBP в стеке
  10.  
  11.     xor    ebx, ebx
  12.     ; Присваиваем регистровой переменной EBX значение ноль
  13.     ; Как легко догадаться - EBX и есть "a"
  14.  
  15.     jmp    short loc_40108F
  16.     ; Безусловный прыжок вниз. Очень похоже на цикл for...
  17.  
  18. loc_401084:                ; CODE XREF: _main+19.j
  19. ;                           ^^^^^^^^^^^^^^^^^^^^^
  20. ; Перекрестная ссылка, направленная вниз - значит, это начало какого-то цикла
  21.  
  22.     push   offset aOperatorCiklaW ; "Оператор цикла while\n"
  23.     call   _printf
  24.     pop    ecx
  25.     <b>; printf("Оператор цикла while\n")</b>
  26.  
  27. loc_40108F:                ; CODE XREF: _main+6.j
  28.     ; А вот сюда был направлен самый первый jump
  29.     ; Посмотрим: что же это такое?
  30.  
  31.     mov    eax, ebx
  32.     ; Копирование EBX в EAX
  33.  
  34.     inc    ebx
  35.     ; Увеличение EBX
  36.  
  37.     cmp    eax, 0Ah
  38.     ; Сравнение EAX со значением 0xA
  39.  
  40.     jl     short loc_401084
  41.     ; Переход в начало цикла, если EAX &lt 0xA
  42.     ; Вот так-то Borland оптимизировал код! Он расположил условие в конце цикла,
  43.     ; но, чтобы не транслировать цикл с предусловием в цикл с постусловием,
  44.     ; просто начал выполнение цикла с этого самого условия!
  45.     ;
  46.     ; Отображение этого цикла на язык Си дает:
  47.  
  48.     <b>; for (int a=0; a < 10; a++) printf("Оператор цикла while\n")</b>
  49.     ;
  50.     ; и, хотя подлинный цикл выглядел совсем не так, наш вариант нечем не хуже!
  51.     ; (а может даже и лучше - нагляднее)
  52.  
  53. loc_401097:                ; CODE XREF: _main+29.j
  54. ;                           ^^^^^^^^^^^^^^^^^^^^^
  55.     ; Начало цикла!
  56.  
  57.     ; Условия нет - значит, это цикл с постусловием
  58.  
  59.     push   offset aOperatorCiklaD ; "Оператор цикла do\n"
  60.     call   _printf
  61.     pop    ecx
  62.     <b>; printf("Оператор цикла do\n")</b>
  63.  
  64.     dec    ebx
  65.     ; --var_EBX
  66.  
  67.     test    ebx, ebx
  68.     jg     short loc_401097
  69.     ; Продолжать цикл, пока var_EBX > 0
  70.     <b>; do printf("Оператор цикла do\n"); while (--var_EBX &gt 0)</b>
  71.  
  72.      xor    eax, eax
  73.     <b>; return 0</b>
  74.  
  75.     pop    ebx
  76.     pop    ebp
  77.     ; Восстанавливаем сохраненные регистры
  78.  
  79.     retn
  80. _main        endp
  81.  

  Листинг 190

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

Код (Text):
  1.  
  2.     mov    [ebp+var_a], 0
  3.     ; Присвоение переменной a значения 0
  4.  
  5.     mov    esi, esi
  6.     ; Э... на редкость умный код! При его виде трудно не упасть со стула!
  7.  
  8. loc_401250:                ; CODE XREF: sub_40123C+34.j
  9. ;                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  10. ; Начало цикла
  11.  
  12.     mov    eax, [ebp+var_a]
  13.     ; Загрузка в EAX значения переменной var_a
  14.  
  15.     inc    [ebp+var_a]
  16.     ; Увеличение var_a на единицу
  17.  
  18.     cmp    eax, 9
  19.     ; Сравнение EAX со значением 0x9
  20.  
  21.     jle    short loc_401260
  22.     ; Переход, если EAX <= 0x9 (EAX < 0xA)
  23.  
  24.     jmp    short loc_401272
  25.     ; Безусловный переход в конец цикла
  26.     ; Стало быть, предыдущий условный переход - переход на его продолжение
  27.     ; Какой неоптимальный код! Зато нет инверсии условия продолжения цикла,
  28.     ; что упрощает дизассемблирование
  29.  
  30.     align 4
  31.     ; Выравнивание перехода по адресам, кратным четырем, ускорят код, но заметно
  32.     ; увеличивает его размер (особенно, если переходов очень много)
  33.  
  34. loc_401260:                ; CODE XREF: sub_40123C+1D.j
  35.     add    esp, 0FFFFFFF4h
  36.     ; Вычитание из ESP значения 12 (0xC)
  37.  
  38.     push   offset aOperatorCiklaW ; "Оператор цикла while\n"
  39.     call    printf
  40.     add    esp, 10h
  41.     ; Восстанавливаем стек (0xC + 0x4 ) == 0x10
  42.  
  43.     jmp    short loc_401250
  44.     ; Переход в начало цикла
  45.  
  46. loc_401272:
  47.     ; Конец цикла
  48.  

  Листинг 191

  Разобравшись с while\do, перейдем к циклам for. Рассмотрим следующий пример:

Код (Text):
  1.  
  2. #include <stdio.h>
  3.  
  4. main()
  5. {
  6.     int a;
  7.     for (a=0;a<10;a++)    printf("Оператор цикла for\n");
  8. }
  9.  

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

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

Код (Text):
  1.  
  2. main        proc near        ; CODE XREF: start+AF.p
  3.  
  4. var_a        = dword    ptr -4
  5.  
  6.     push   ebp
  7.     mov    ebp, esp
  8.     ; Открываем кадр стека
  9.  
  10.     push   cx
  11.     ; Резервируем память для локальной переменной
  12.  
  13.     mov    [ebp+var_a], 0
  14.     ; Присваиваем локальной переменной var_a значение 0
  15.  
  16.     jmp    short loc_401016
  17.     ; Непосредственный переход на код проверки условия продолжения цикла -
  18.     ; характерный признак for
  19.  
  20. loc_40100D:                ; CODE XREF: main+29.j
  21. ;                            ^^^^^^^^^^^^^^^^^^^^
  22. ; Перекрестная ссылка, направленная вниз говорит о том, что это начало цикла
  23.  
  24.     mov    eax, [ebp+var_a]
  25.     ; Загрузка в EAX значения переменной var_a
  26.  
  27.     add    eax, 1
  28.     ; Увеличение EAX на единицу
  29.  
  30.     mov    [ebp+var_a], eax
  31.     ; Обновление EAX
  32.     ; Следовательно, исходный код выглядел так:
  33.     ; ++a
  34.  
  35. loc_401016:                ; CODE XREF: main+B.j
  36.     cmp    [ebp+var_a], 0Ah
  37.     ; Сравниваем var_a со значением 0xA
  38.  
  39.     jge    short loc_40102B
  40.     ; Выход из цикла, если var_a >= 0xA
  41.  
  42.     push    offset aOperatorCiklaF ; "Оператор цикла for\n"
  43.     call    _printf
  44.     add     esp, 4
  45.     <b>; printf("Оператор цикла for\n")</b>
  46.  
  47.     jmp    short loc_40100D
  48.     ; Безусловный переход в начало цикла
  49.     ;
  50.     ; Итак, что мы имеем?
  51.     <i><b>; инициализация переменной var_a
  52.     ; переход на проверку условия выхода из цикла ----------!
  53.     ; инкремент переменной var_a <---------------------!    !
  54.     ; проверка условия относительно var_a <----------- ! ---!
  55.     ; прыжок на выход из цикла, если условие истинно---!----!
  56.     ; вызов printf                                     !    !
  57.     ; переход в начало цикла --------------------------!    !
  58.     ; конец цикла <-----------------------------------------!</b></i>
  59.     ;
  60.     ; Проверка на завершения, расположенная в начале цикла, говорит о том, что
  61.     ; это цикл с предусловием, но непосредственно выразить его через while
  62.     ; не удается - мешает безусловный переход в середину цикла, минуя код
  63.     ; инкремента переменной var_a
  64.     ; Однако этот цикл с легкостью отображается на оператор for, смотрите:
  65.     ; for (a = 0; a < 0xA; a++) printf("Оператор цикла for\n")
  66.     ;
  67.     ; Действительно, цикл for сначала инициирует переменную - счетчик,
  68.     ; затем проверяет условие продолжение цикла
  69.     ; (оптимизируемое компилятором в условие завершение), далее выполняет
  70.     ; оператор цикла, модифицирует счетчик, вновь проверяет условие и т.д.
  71.     ;
  72.  
  73. loc_40102B:                ; CODE XREF: main+1A.j
  74.     mov    esp, ebp
  75.     pop    ebp
  76.     ; Закрываем кадр стека
  77.  
  78.     retn
  79. main        endp
  80.  

  Листинг 193

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

Код (Text):
  1.  
  2. main        proc near        ; CODE XREF: start+AF.p
  3.     push   esi
  4.     mov    esi, 0Ah
  5.     ; Инициализируем переменную - счетчик
  6.     ;<i><b> Внимание!</b> В исходном коде начальное значение счетчика равнялось нулю!</i>
  7.  
  8. loc_401006:                ; CODE XREF: main+14.j
  9.     push   offset aOperatorCiklaF ; "Оператор цикла for\n"
  10.     call   _printf
  11.     add    esp, 4
  12.     ;<b> printf("Оператор цикла for\n")</b>
  13.     ; Выполняем оператор цикла! Причем безо всяких проверок!
  14.     ; Хитрый компилятор проанализировал код и понял, что цикл выполняется
  15.     ; по крайней мере один раз!
  16.  
  17.     dec    esi
  18.     ; Уменьшаем счетчик, хотя в исходном коде программы мы его увеличивали!
  19.     ; Ну, правильно - dec \ jnz намного короче INC\ CMP reg, const\ jnz xxx
  20.     ; Ой и мудрит компилятор! Кто же ему давал право так изменять цикл?!
  21.     ; А очень просто - он понял, что параметр цикла в самом цикле используется
  22.     ; только как счетчик, и нет никакой разницы - увеличивается он
  23.     ; с каждой итерацией или уменьшается!
  24.  
  25.     jnz    short loc_401006
  26.     ; Переход в начало цикла если ESI > 0
  27.     ;
  28.     ; М да, по внешнему виду это типичный
  29.     ;<b> a = 0xa; do printf("Оператор цикла for\n"); while (--a)</b>
  30.     ;
  31.     ; Если вас устраивает читабельность такой формы записи - оставляйте ее, а нет:
  32.     ;<b> for (a = 0; a < 10; a++) Оператор цикла for\n")</b>
  33.     ;
  34.     ; Постой, постой! На каком основании автор выполнил такое преобразование?!
  35.     ; А на том самом - что и компилятор: раз параметр цикла используется только
  36.     ; как счетчик, законна любая запись, выполняющая цикл ровно десять раз -
  37.     ; остается выбрать ту, которая удобнее (с эстетической точки зрения)
  38.     ; Никто же не будет утверждать, что
  39.     ; for (a = 10; a > 0; a--) более привычно чем for (a = 0; a < 10; a++)?
  40.  
  41.     pop    esi
  42.     retn
  43. main        endp
  44.  

  Листинг 194

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

Код (Text):
  1.  
  2. _main        proc near        ; DATA XREF: DATA:00407044.o
  3.  
  4.     push   ebp
  5.     mov    ebp, esp
  6.     ; Открываем кадр стека
  7.  
  8.     push   ebx
  9.     ; Сохраняем EBX в стеке
  10.  
  11.     xor    ebx, ebx
  12.     ; Присваиваем регистровой переменной EBX значение 0
  13.  
  14. loc_401082:                ; CODE XREF: _main+15.j
  15. ;                           ^^^^^^^^^^^^^^^^^^^^^^
  16. ; Начало цикла
  17.  
  18.     push   offset aOperatorCiklaF ; format
  19.     call   _printf
  20.     pop    ecx
  21.     ; Начинаем цикл с выполнения его тела
  22.     ; OK, Borland понял, что цикл выполняется по крайней мере раз
  23.  
  24.     inc    ebx
  25.     ; Увеличиваем параметр цикла
  26.  
  27.     cmp    ebx, 0Ah
  28.     ; Сравниваем EBX со значением 0xA
  29.  
  30.     jl     short loc_401082
  31.     ; Переход в начало цикла, пока EBX < 0xA
  32.  
  33.     xor    eax, eax
  34.     pop    ebx
  35.     pop    ebp
  36.     retn
  37. _main        endp
  38.  

  Листинг 195

  Видно, что Borland C++ 5.0 не дотягивает до Microsoft Visual C++ 6.0 - понять, что цикл выполняется один раз он понял, а вот реверс счетчика ума уже не хватило. Аналогичным образом поступает и большинство других компиляторов, в частности WATCOM C.

  Теперь настала очередь циклов с условием в середине или циклов, завершаемых вручную оператором break. Рассмотрим следующий пример:

Код (Text):
  1.  
  2. #include <stdio.h>
  3.  
  4. main()
  5. {
  6.     int a=0;
  7.     while(1)
  8.     {
  9.         printf("1й оператор\n");
  10.         if (++a>10) break;
  11.         printf("2й оператор\n");
  12.     }
  13.  
  14.     do
  15.     {
  16.     printf("1й оператор\n");
  17.     if (--a<0) break;
  18.     printf("2й оператор\n");
  19.     }while(1);
  20. }
  21.  

  Листинг 196 Демонстрация идентификации break

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

Код (Text):
  1.  
  2. main        proc near        ; CODE XREF: start+AF.p
  3.  
  4. var_a        = dword    ptr -4
  5.  
  6.     push   ebp
  7.     mov    ebp, esp
  8.     ; Открываем кадр стека
  9.  
  10.     push   ecx
  11.     ; Резервируем место для локальной переменной
  12.  
  13.     mov    [ebp+var_a], 0
  14.     ; Присваиваем переменной var_a значение 0х0
  15.  
  16. loc_40100B:                ; CODE XREF: main+3F.j
  17. ;                           ^^^^^^^^^^^^^^^^^^^^^
  18. ; Перекрестная ссылка, направленная вниз - цикл
  19.  
  20.     mov    eax, 1
  21.     test   eax, eax
  22.     jz     short loc_401041
  23.     ; Смотрите! Когда optimize disabled, - компилятор транслирует безусловный
  24.     ; цикл "слишком буквально", т.к. присваивает EAX значение 1 (TRUE)
  25.     ; и затем педантично проверяет ее на равенство нулю
  26.     ; Если в кои веки TRUE будет равно FALSE - произойдет выход из цикла
  27.     ; Словом, все эти три инструкции - глупый и бесполезный код цикла
  28.     ;<b> while (1)</b>
  29.  
  30.     push   offset a1iOperator ; "1й оператор\n"
  31.     call   _printf
  32.     add    esp, 4
  33.     ;<b> printf("1й оператор\n")</b>
  34.  
  35.     mov    ecx, [ebp+var_a]
  36.     ; Загружаем в ECX значение переменной var_a
  37.  
  38.     add    ecx, 1
  39.     ; Увеличивем ECX на единицу
  40.  
  41.     mov    [ebp+var_a], ecx
  42.     ; Обновляем var_a
  43.  
  44.     cmp    [ebp+var_a], 0Ah
  45.     ; Сравниваем var_a со значением 0xA
  46.  
  47.     jle    short loc_401032
  48.     ; Переход, если var_a <= 0xA
  49.     ; Но <b><i>куда</i></b> этот переход? Во-первых, переход направлен вниз, т.е. это уже
  50.     ; не переход к началу цикла, следовательно и условие - не условие цикла, а
  51.     ; результат компиляции конструкции IF - THEN
  52.     ; Второе - переход прыгает на первую команду, следующую за безусловным
  53.     ; jump loc_401041, передающим управление инструкции, следующей
  54.     ; за командной jmp short loc_401075 - безусловного перехода, направленного
  55.     ; вверх - в начало цикла
  56.     ; Следовательно, jmp    short loc_401041 осуществляет выход из цикла, а
  57.     ; jle short loc_401032 - продолжает его выполнение
  58.  
  59.     jmp    short loc_401041
  60.     ; ОК, - это переход на завершение цикла. А кто у нас завершает цикл?
  61.     ; Ну, конечно же,<b> break!</b> Следовательно, окончательная декомпиляции выглядит так
  62.     ;<b> if (++var_a > 0xA) break</b>
  63.     ; Мы инвертировали "<=" в ">", т.к. JLE передает управление на код продолжения
  64.     ; цикла, а ветка THEN в нашем случае - на break
  65.  
  66. loc_401032:                ; CODE XREF: main+2E.j
  67. ;                           ^^^^^^^^^^^^^^^^^^^^^
  68. ; Перекрестная ссылка направлена вверх - следовательно, это не начало цикла
  69.  
  70.     push   offset a2iOperator ; "2й оператор\n"
  71.     call   _printf
  72.     add    esp, 4
  73.     ;<b> printf("2й оператор\n")</b>
  74.  
  75.     jmp    short loc_40100B
  76.     ; Прыжок в начало цикла. Вот мы и добрались до конца цикла
  77.     ; Восстанавливаем исходный код:
  78.     ; while(1)
  79.     ; {
  80.     ;     printf("1й оператор\n");
  81.     ;     if (++var_a > 0xA) break;
  82.     ;     printf("2й оператор\n");
  83.     ; }
  84.     ;
  85.  
  86. loc_401041:                ; CODE XREF: main+12.j main+30.j ...
  87. ;                                                ^^^^^^^^^^
  88. ; Перекрестная ссылка, направленная вниз, говорит, что это начало цикла
  89.  
  90.     push   offset a1iOperator_0 ; "1й оператор\n"
  91.     call   _printf
  92.     add    esp, 4
  93.     ;<b> printf("1й оператор\n")</b>
  94.  
  95.     mov    edx, [ebp+var_a]
  96.     sub    edx, 1
  97.     mov   [ebp+var_a], edx
  98.     ;<b> --var_a</b>
  99.  
  100.     cmp    [ebp+var_a], 0
  101.     ; Сравниваем var_a со значением 0x0
  102.  
  103.     jge    short loc_40105F
  104.     ; Переход вниз, если var_a >= 0
  105.     ; Смотрите: оператор break цикла do ничем не отличается от break цикла while!
  106.     ; Поэтому, не будем разглагольствовать, а сразу его декомпилируем!
  107.     ;<b> if (var_a < 0) ...</b>
  108.  
  109.     jmp    short loc_401075
  110.     ;<b> ...break</b>
  111.  
  112. loc_40105F:                ; CODE XREF: main+5B.j
  113.     push   offset a2iOperator_0 ; "2й оператор\n"
  114.     call   _printf
  115.     add    esp, 4
  116.     <b>; printf("2й оператор\n")</b>
  117.  
  118.     mov    eax, 1
  119.     test   eax, eax
  120.     jnz    short loc_401041
  121.     ; А это - проверка продолжения цикла
  122.  
  123. loc_401075:                ; CODE XREF: main+5D.j
  124.     mov    esp, ebp
  125.     pop    ebp
  126.     ; Закрываем кадр стека
  127.  
  128.      retn
  129. main        endp
  130.  

  Листинг 197

  Что ж, оператор break в обоих циклах выглядит одинаково и элементарно распознается (правда, не с первого взгляда, но отслеживанием нескольких переходов - да). А вот с бесконечными циклами не оптимизирующий компилятор подкачал, транслировав их в код, проверяющий условие, истинность (не истинность) которого очевидна. А как поведет себя оптимизирующий компилятор?
  Давайте откомпилируем тот же самый пример компилятором Microsoft Visual C++ 6.0 с ключом "/Ox" и посмотрим:

Код (Text):
  1.  
  2. main        proc near        ; CODE XREF: start+AF.p
  3.     push    esi
  4.     ; Сохраняем ESI в стеке
  5.  
  6.     xor    esi, esi
  7.     ; Присваиваем ESI значение 0
  8.     ;<b> var_ESI = 0;</b>
  9.  
  10. loc_401003:               ; CODE XREF: main+23.j
  11. ;                          ^^^^^^^^^^^^^^^^^^^^^
  12. ; Перекрестная ссылка, направленная вперед
  13. ; Это - начало цикла
  14.  
  15.     push   offset a1iOperator ; "1й оператор\n"
  16.     call   _printf
  17.     add    esp, 4
  18.     ;<b> printf("1й оператор\n")</b>
  19.     ;
  20.     ; Ага! Проверки на дорогах нет, значит, это цикл с постусловием
  21.     ; (или условием в середине)
  22.  
  23.     inc    esi
  24.     ;<b> ++var_ESI</b>
  25.  
  26.     cmp    esi, 0Ah
  27.     ; Сравниваем var_ESI со значением 0xA
  28.  
  29.     jg     short loc_401025
  30.     ; Выход из цикла, если var_ESI > 0xA
  31.     ; Поскольку, данная команда - не последняя в теле цикла,
  32.     ; это цикл с условием в середине
  33.     ;<b> if (var_ESI > 0xA) break</b>
  34.  
  35.     push   offset a2iOperator ; "2й оператор\n"
  36.     call   _printf
  37.     add    esp, 4
  38.     ;<b> printf("2й оператор\n")</b>
  39.  
  40.     jmp    short loc_401003
  41.     ; Безусловный переход в начало цикла
  42.     ; Как видно, оптимизирующий компилятор выкинул никому ненужную проверку
  43.     ; условия, упростив код и облегчив его понимание:
  44.     ; Итак:
  45.     ;<b> var_ESI = 0
  46.     ; for (;;)</b>     <-- вырожденный for представляет собой бесконечный цикл
  47.     ;<b> {
  48.     ;     printf("1й оператор\n");
  49.     ;     ++var_ESI;
  50.     ;     if (var_ESI > 0xA) break;
  51.     ;     printf("2й оператор\n");
  52.     ; }</b>
  53.  
  54. loc_401025:                ; CODE XREF: main+14.j
  55. ;                           ^^^^^^^^^^^^^^^^^^^^^
  56. ; Это не начало цикла!
  57.  
  58.     push   offset a1iOperator_0 ; "1й оператор\n"
  59.     call    _printf
  60.     add    esp, 4
  61.     ;<b> printf("1й оператор\n")</b>
  62.     ; Хм, как же это не начало цикла?! Очень похоже!
  63.  
  64.     dec    esi
  65.     ;<b> --var_ESI</b>
  66.  
  67.     js     short loc_401050
  68.     ; Выход из цикла, если var_ESI < 0
  69.  
  70.     inc    esi
  71.     ; Увеличиваем var_ESI на единицу
  72.     ; М-м-м... (задумчиво)...
  73.  
  74. loc_401036:                ; CODE XREF: main+4E.j
  75. ;                          ^^^^^^^^^^^^^^^^^^^^^^
  76. ; А вот это начало цикла!
  77.  
  78.     push   offset a2iOperator_0 ; "2й оператор\n"
  79.     call   _printf
  80.     ;<b> printf("2й оператор\n")</b>
  81.     ; Только странно, что начало цикла начинается с его, с позволения сказать,
  82.     ; середины...
  83.  
  84.     push   offset a1iOperator_0 ; "1й оператор\n"
  85.     call   _printf
  86.     add    esp, 8
  87.     ;<b> printf("1й оператор\n")</b>
  88.     ;
  89.     ; ???!!! Что за чудеса творятся? Во-первых, вызов первого оператора второго
  90.     ; цикла уже встречался ранее, во-вторых, не может же следом за серединой цикла
  91.     ; следовать его начало?!
  92.  
  93.     dec    esi
  94.     ;<b> --var_ESI</b>
  95.  
  96.     jnz    short loc_401036
  97.     ; Продолжение цикла, пока var_ESI != 0
  98.  
  99. loc_401050:                ; CODE XREF: main+33.j
  100.     ; Конец цикла
  101.     ; Да... тут есть над чем подумать!
  102.     ; Компилятор нормально "перевалил" первую строку цикла
  103.     ;<i> printf("1й оператор\n")</i>
  104.     ; а затем "напоролся" на ветвление:
  105.     ; if (--a<0) break
  106.     ; Хитрые парни из Microsoft знают, что для супер - конвейерных процессоров
  107.     ; (коими и являются чипы Pentium) ветвления все равно, что чертополох для
  108.     ; Тиггеров. Кстати, Си-компиляторы под процессоры серии CONVEX вообще
  109.     ; отказываются компилировать циклы с ветвлениями, истощенно понося
  110.     ; умственные способности программистов. А вы еще IBM PC ругаете ;-)
  111.     ; Вот и приходится компилятору исправлять ляпы программиста, что он делать
  112.     ; в принципе не обязан, но за что ему большое человеческое спасибо!
  113.     ; Компилятор как бы "прокручивает" цикл, "слепляя" вызовы функций printf
  114.     ; и вынося ветвления в конец
  115.     ; Образно исполняемый код можно представить трассой, а процессор - гонщиком
  116.     ; Чем длиннее участок дороги без поворотов, тем быстрее его проскочит гонщик!
  117.     ; Выносить условие из середины цикла в его конец компилятор вполне правомерен,
  118.     ; ведь переменная, относительно которой выполняется ветвление,
  119.     ; не модифицируется ни функцией printf, ни какой другой
  120.     ; Поэтому, не все ли равно где ее проверять? Конечно же не все равно!!!
  121.     ; К моменту когда условие (--a < 10) становится истинно, успевает выполниться
  122.     ; первый printf, а вот второй - уже не получает управления
  123.     ; Вот для этого-то компилятор и поместил код проверки условия следом за
  124.     ; первым вызовом первой функции printf, а затем изменил порядок вызова
  125.     ; printf в теле цикла. Это привело к тому, что на момент выхода из цикла
  126.     ; по условию первый printf выполняется на один раз больше, чем второй
  127.     ; (т.к. он встречается дважды)
  128.     ; Остается разобраться с увеличением var_ESI - что бы это значило?
  129.     ; Давайте рассуждать от противного: что произойдет, если выкинуть
  130.     ; команду INC ESI? Поскольку, счетчик цикла при первой итерации цикла
  131.     ; декрементируется дважды, возникнет недостача и цикл выполниться на раз
  132.     ; короче. Что бы этого не произошло, var_ESI искусственно увеличивается
  133.     ; на единицу
  134.     ; Ой, и не просто во всей этой головоломке разобраться, а представьте:
  135.     ; насколько сложно реализовать компилятор, умеющий проделывать такие фокусы!
  136.     ; А еще кто-то ругает автоматическую оптимизацию. Да уж! Конечно, руками-то
  137.     ; можно и круче оптимизировать(особенно понимания смысл кода), но ведь эдак
  138.     ; и мозги вывихнуть будет можно! А компилятор, даже будучи стиснут со всех
  139.     ; сторон кривым кодом программиста, за доли секунды успевает его довольно
  140.     ; прилично окультурить
  141.  
  142.     pop    esi
  143.     retn
  144. main        endp
  145.  

  Листинг 198

  Компиляторы Borland C++ и WATCOM при трансляции бесконечных циклов заменяют код проверки условия продолжения цикла на безусловный переход, но вот, увы, оптимизировать ветвления, вынося их в конец цикла так, как это делает Microsoft Visual C++ 6.0 они не умеют...
  Теперь, после break, рассмотрим: как компиляторы транслирует его "астральный антипод", - оператор continue. Возьмем следующий пример:

Код (Text):
  1.  
  2. #include <stdio.h>
  3.  
  4. main()
  5. {
  6.     int a=0;
  7.     while (a++<10)
  8.     {
  9.         if (a == 2) continue;
  10.         printf("%x\n",a);
  11.     }
  12.  
  13.     do
  14.     {
  15.         if (a == 2) continue;
  16.         printf("%x\n",a);
  17.     } while (--a>0);
  18. }
  19.  

  Листинг 199 Демонстрация идентификации continue

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

Код (Text):
  1.  
  2. main        proc near        ; CODE XREF: start+AF.p
  3.  
  4. var_a        = dword    ptr -4
  5.  
  6.     push   ebp
  7.     mov    ebp, esp
  8.     ; Открываем кадр стека
  9.  
  10.     push   ecx
  11.     ; Резервируем место для локальной переменной
  12.  
  13.     mov    [ebp+var_a], 0
  14.     ; Присваиваем локальной переменной var_a значение 0
  15.  
  16. loc_40100B:                ; CODE XREF: main+22.j main+35.j
  17. ;                                       ^^^^^^^^^^^^^^^^^^^
  18. ; Две перекрестные ссылки, направленные вперед, говорят о том, что это либо
  19. ; начало двух циклов (один из которых - вложенный), либо переход в начало
  20. ; цикла оператором continue
  21.  
  22.     mov    eax, [ebp+var_a]
  23.     ; Загружаем в EAX значение var_a
  24.  
  25.     mov    ecx, [ebp+var_a]
  26.     ; Загружаем в ECX значение var_a
  27.  
  28.     add    ecx, 1
  29.     ; Увеличиваем ECX на единицу
  30.  
  31.     mov    [ebp+var_a], ecx
  32.     ; Обновляем переменную var_a
  33.  
  34.     cmp    eax, 0Ah
  35.     ; Сравниваем значение переменной var_a до увеличения с числом 0xA
  36.  
  37.     jge    short loc_401037
  38.     ; Выход из цикла (переход на команду, следующую за инструкцией, направленной
  39.     ; вверх - в начало цикла) если var_a >= 0xA
  40.  
  41.     cmp   [ebp+var_a], 2
  42.     ; Сравниваем var_a со значением 0x2
  43.  
  44.     jnz    short loc_401024
  45.     ; Если var_a != 2, то прыжок на команду, следующую за инструкцией
  46.     ; безусловного перехода, направленной вверх - в начало цикла
  47.     ; Очень похоже на условие выхода из цикла, но не будет спешить с выводами!
  48.     ; Вспомним - в начале цикла нам встретились две перекрестные ссылки
  49.     ; Безусловный переход "jmp short loc_40100B" как раз образует одну из них
  50.     ; А кто "отвечает" за другую?
  51.     ; Чтобы ответить на этот вопрос необходимо проанализировать остальной код цикла
  52.  
  53.     jmp    short loc_40100B
  54.     ; Безусловный переход, направленный в начало цикла - это либо конец цикла,
  55.     ; либо continue
  56.     ; Предположим, что это конец цикла. Тогда что же представляет собой
  57.     ; "jge short loc_401037"? Предусловие выхода из цикла? Не похоже - в таком
  58.     ; случае они прыгало бы гораздо "ближе" - на метку loc_401024
  59.     ; А может, "jge short loc_401037" предусловие одного цикла, а
  60.     ; "jnz short loc_401024" - постусловие другого, вложенного в него?
  61.     ; Вполне возможно, но маловероятно - в этом случае постусловие представляло бы
  62.     ; собой условие продолжения, а не завершения цилкла
  63.     ; Поэтому, с некоторой долей неуверенности, мы можем принять конструкцию
  64.     ; CMP var_a, 2 \ JNZ loc_401024 \ JMP loc_40100B за <b>if (a==2) continue</b>
  65.  
  66. loc_401024:                ; CODE XREF: main+20.j
  67.     mov    edx, [ebp+var_a]
  68.     push   edx
  69.     push   offset asc_406030 ; "%x\n"
  70.     call   _printf
  71.     add    esp, 8
  72.     ;<b> printf("%x\n",var_a)</b>
  73.  
  74.     jmp    short loc_40100B
  75.     ; А вот это - явно конец цикла, т.к. jmp short loc_40100B - самая
  76.     ; последняя ссылка на начало цикла
  77.     ; Итак, подытожим, что мы имеем:
  78.     ; Условие, расположенное в начале цикла, крутит этот цикл до тех пор, пока
  79.     ; var_a < 0xA, причем инкремент параметра цикла происходит до его сравнения
  80.     ; Затем следует еще одно условие, возвращающее управление в начало цикла, если
  81.     ; var_a == 2. Строй замыкает оператор цикла printf и безусловный переход в его
  82.     ; начало. Т.е.
  83.     ;
  84.     ; Начало цикла:             <-----------! <--!
  85.     ; Инкремент переменной var_a            !    !
  86.     ; условие "далекого" выхода -------!    !    !
  87.     ; условие "ближнего" продолжения --)----!    !
  88.     ; тело цикла                       !         !
  89.     ; безусловный переход в начало ----)---------!
  90.     ; конец цикла                 <----!
  91.     ;
  92.     ; Условие "ближнего" продолжение не может быть концом цикла, т.к. тогда условию
  93.     ; "далекого" выхода пришлось выйти аж из надлежащего цикла, на что ни break,
  94.     ; ни другие операторы не способны. Таким образом, условие ближнего продолжения
  95.     ; может быть только оператором continue и на языке Си всю эту конструкция
  96.     ; будет выглядеть так:
  97.     ;<b> while(a++<10)</b>                // <-- инкремент var_a и условие далекого выхода
  98.     ;<b> {
  99.     ;     if (a == 2) continue;</b>    // <-- условие ближнего продолжения
  100.     ;<b>     printf(%x\n",var_a);</b>     // <-- тело цикла
  101.     ;<b> } </b>                           // <-- безусловный переход на начало цикла
  102.  
  103. loc_401037:                ; CODE XREF: main+1A.j main+5D.j
  104. ;                                                 ^^^^^^^^^
  105. ; Начало цикла
  106.  
  107.     cmp    [ebp+var_a], 2
  108.     ; Сравниваем переменную var_a со значением 0x2
  109.  
  110.     jnz    short loc_40103F
  111.     ; Если var_a != 2, то продолжение цикла
  112.  
  113.     jmp    short loc_401050
  114.     ; Переход к коду проверки условия продолжения цикла
  115.     ; Это бесспорно "continue" и вся конструкция выглядит так:
  116.     ;<b> if (a==2) continue</b>
  117.  
  118. loc_40103F:                ; CODE XREF: main+3B.j
  119.     mov    eax, [ebp+var_a]
  120.     push   eax
  121.     push   offset asc_406034 ; "%x\n"
  122.     call   _printf
  123.     add    esp, 8
  124.     ;<b> printf("%x\n", var_a)</b>
  125.  
  126. loc_401050:                    ; CODE XREF: main+3D.j
  127.     mov    ecx, [ebp+var_a]
  128.     sub    ecx, 1
  129.     mov    [ebp+var_a], ecx
  130.     ;<b> --var_a</b>
  131.  
  132.     cmp    [ebp+var_a], 0
  133.     ; Сравнение var_a с нулем
  134.  
  135.     jg    short loc_401037
  136.     ; Пока var_a > 0 продолжать цикл. Похоже на постусловие верно? Тогда:
  137.     ; do
  138.     ; {
  139.     ;     if (a==2) continue;
  140.     ;     printf("%x\n", var_a);
  141.     ; } while (--var_a > 0);
  142.     ;
  143.     mov    esp, ebp
  144.     pop    ebp
  145.     retn
  146. main        endp
  147.  

  Листинг 200

  А теперь посмотрим, как повлияла оптимизация ("/Ox") на вид циклов:

Код (Text):
  1.  
  2. main        proc near        ; CODE XREF: start+AF.p
  3.     push   esi
  4.     mov    esi, 1
  5.  
  6. loc_401006:                    ; CODE XREF: main+1F.j
  7. ;                                ^^^^^^^^^^^^^^^^^^^^
  8. ; Начало цикла
  9.  
  10.     cmp    esi, 2
  11.     jz     short loc_401019
  12.     ; Переход на loc_401019, если ESI == 2
  13.  
  14.     push   esi
  15.     push   offset asc_406030 ; "%x\n"
  16.     call   _printf
  17.     add     esp, 8
  18.     ;<b> printf("%x\n", ESI)</b>
  19.     ; Прим: эта ветка выполняется только если ESI !=2
  20.     ; Следовательно, ее можно изобразить так:
  21.     ;<b> if (ESI != 2) printf("%x\n", ESI)</b>
  22.  
  23. loc_401019:                ; CODE XREF: main+9.j
  24.     mov    eax, esi
  25.     inc    esi
  26.     ; ESI++;
  27.  
  28.     cmp    eax, 0Ah
  29.     jl     short loc_401006
  30.     ; Продолжение цикла пока (ESI++ < 0xA)
  31.     ; Итого:
  32.     ;<b> do
  33.     ; {
  34.     ;     if (ESI != 2) printf("%x\n", ESI);
  35.     ; } while (ESI++ < 0xA)
  36.     ;</b>
  37.     ; А что, выглядит вполне читабельно, не правда ли? Ни чуть не хуже, чем
  38.     ;<b> if (ESI == 2) continue</b>
  39.     ;
  40.  
  41. loc_401021:                ; CODE XREF: main+37.j
  42. ;                                        ^^^^^^^^
  43. ; Начало цикла
  44.  
  45.     cmp    esi, 2
  46.     jz     short loc_401034
  47.     ; Переход на loc_401034, если ESI == 2
  48.  
  49.     push   esi
  50.     push   offset asc_406034 ; "%x\n"
  51.     call   _printf
  52.     add    esp, 8
  53.     ;<b> printf("%x\n",ESI);</b>
  54.     ; Прим. эта ветка выполняется лишь когда ESI != 2
  55.  
  56. loc_401034:                ; CODE XREF: main+24.j
  57.     dec    esi
  58.     ; --ESI
  59.  
  60.     test   esi, esi
  61.     jg     short loc_401021
  62.     ; Условие продолжение цикла - крутить кака ESI > 0
  63.     ; Итого:
  64.     ;<b> do
  65.     ; {
  66.     ;     if (ESI != 2)
  67.     ;    {
  68.     ;        printf("%x\n", ESI);
  69.     ;     }
  70.  
  71.     ; } while (--ESI > 0)</b>
  72.     ;
  73.  
  74.     pop    esi
  75.     retn
  76. main        endp
  77.  

  Листинг 201

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

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

Код (Text):
  1.  
  2. main()
  3. {
  4.     int a; int b;
  5.     for (a = 1, b = 10; a < 10, b > 1; a++, b --)
  6.     printf("%x %x\n", a, b);
  7. }
  8.  

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

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

Код (Text):
  1.  
  2. main        proc near        ; CODE XREF: start+AF.p
  3.  
  4. var_b        = dword    ptr -8
  5. var_a        = dword    ptr -4
  6.  
  7.     push   ebp
  8.     mov    ebp, esp
  9.     ; Открываем кадр стека
  10.  
  11.     sub    esp, 8
  12.     ; Резервируем память для двух локальных переменных
  13.  
  14.     mov    [ebp+var_a], 1
  15.     ; Присваиваем переменной var_a значение 0x1
  16.  
  17.     mov    [ebp+var_b], 0Ah
  18.     ; Присваиваем переменной var_b значение 0xA
  19.  
  20.     jmp    short loc_401028
  21.     ; Прыжок на код проверки условия выхода из цикла
  22.     ; Это характерная черта не оптимизированных циклов for
  23.  
  24. loc_401016:                   ; CODE XREF: main+43.j
  25. ;                                          ^^^^^^^^^
  26. ; Перекрестная ссылка, направленная вниз, говорит о том, что это - начало цикла
  27. ; А выше мы уже выяснили, что тип цикла - for
  28.  
  29.     mov    eax, [ebp+var_a]
  30.     add    eax, 1
  31.     mov    [ebp+var_a], eax
  32.     ;<b> var_a++</b>
  33.  
  34.     mov    ecx, [ebp+var_b]
  35.     sub    ecx, 1
  36.     mov    [ebp+var_b], ecx
  37.     ;<b> var_b--</b>
  38.  
  39. loc_401028:                   ; CODE XREF: main+14.j
  40.     cmp    [ebp+var_b], 1
  41.     jle    short loc_401045
  42.     ; Выход из цикла, если var_b <= 0x1
  43.     ; Обратите внимание: выполняется проверка лишь одного (второго слева) счетчика!
  44.     ; Выражение (a1,a2,a3,...an) компилятор считает бессмысленным и берет лишь an
  45.     ; молчаливо отбрасывая все остальное
  46.     ; (из известных мне компиляторов на это ругается один WATCOM)
  47.     ; В данном случае проверяется лишь условие (b > 1), а (a < 10) игнорируется!!!
  48.  
  49.     mov    edx, [ebp+var_b]
  50.     push   edx
  51.     mov    eax, [ebp+var_a]
  52.     push   eax
  53.     push   offset aXX    ; "%x %x\n"
  54.     call   _printf
  55.     add    esp, 0Ch
  56.     ;<b> printf("%x %x\n", var_a, var_b)</b>
  57.  
  58.     jmp    short loc_401016
  59.     ; Конец цикла
  60.     ; Итак, данный цикл можно представить как:
  61.     ;<b> while(1)
  62.     ; {
  63.     ;     var_a++;
  64.     ;     var_b--;
  65.     ;     if (var_b <= 0x1) break;
  66.     ;    printf("%x %x\n", var_a, var_b)
  67.     ; }</b>
  68.     ;
  69.     ; Но по соображениям удобочитаемости имеет смысл скомпоновать это код в for
  70.     ;for (var_a=1,var_b=0xA;var_b>1;var_a++,var_b--) printf("%x %x\n",var_a,var_b)
  71.     ;
  72.  
  73. loc_401045:                    ; CODE XREF: main+2C.j
  74.     mov    esp, ebp
  75.     pop    ebp
  76.     ; Закрываем кадр стека
  77.  
  78.     retn
  79. main    endp
  80.  

  Листинг 203

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


0 1.841
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532