Идентификация циклов
"связь между элементами системы носит трансуровневый характер и проявляет себя в виде повторяющихся единиц разных уровней (мотивов)"
тезис классического постструктурализма в его отечественном изводе
Циклы
– единственная (за исключением неприличного "GOTO") конструкция языков высокого уровня, имеющая ссылку "назад", т.е. в область более младших адресов. Все остальные виды ветвлений – будь то IF – THEN – ELSE или оператор множественного выбора switch всегда направлены "вниз" – в область старших адресов. Вследствие этого, логическое дерево, изображающее цикл, настолько характерно, что легко опознается с первого взгляда.
Существуют три основных типа цикла: циклы с условием вначале (см. рис. 36 слева), циклы с условием в конце (см. рис. 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+AFp
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+29j
; ^^^^^^^^^^^^^^
; Перекрестная ссылка, направленная вниз, говорит о том, что это начло цикла
; Естественно: раз перекрестная ссылка направлена вниз, то переход,
; ссылающийся на этот адрес, будет направлен вверх!
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+1Aj
; main_401000+45j
; ^^^^^^^^^^^^^^^^
; // Это начало цикла с пост-условием
; // Однако на данном этапе мы этого еще не знаем, хотя и можем догадываться
; // благодаря наличию перекрестной ссылки, направленной вниз
; Ага, никакого условия в начале цикла не присутствует, значит, это цикл
; с условием в конце или середине
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+AFp
push esi
push edi
; Сохраняем регистры в стеке
mov esi, 1
; Присваиваем ESI значение 0х1
; Внимание – взгляните на исходный код – ни одна из переменных не имела
; такого значения!
mov edi, 0Ah
; Присваиваем EDI значение 0xA. Ага, это константа для проверки условия
; выхода из цикла
loc_40100C: ; CODE XREF: main+1Dj
; ^^^^^^^^^^^^^^^^^^^^
; Судя по перекрестной ссылке, направленной вниз, этот – цикл!
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+2Fj
; ^^^^^^^^^^^^^^^^^^^^
; Перекрестная ссылка, направленная вниз, говорит о том, что это – начало цилка
; // Предусловия нет – значит, это цикл 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:00407044o
push ebp
mov ebp, esp
; Открываем кадр стека
push ebx
; Сохраняем EBP в стеке
xor ebx, ebx
; Присваиваем регистровой переменной EBX
значение ноль
; Как легко догадаться – EBX и есть "a"
jmp short loc_40108F
; Безусловный прыжок вниз. Очень похоже на цикл for…
loc_401084: ; CODE XREF: _main+19j
; ^^^^^^^^^^^^^^^^^^^^^
; Перекрестная ссылка, направленная вниз – значит, это начало какого-то цикла
push offset aOperatorCiklaW ; "Оператор
цикла while\n"
call _printf
pop ecx
; printf("Оператор цикла
while\n")
loc_40108F: ; CODE XREF: _main+6j
; А вот сюда был направлен самый первый 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+29j
; ^^^^^^^^^^^^^^^^^^^^^
; Начало цикла!
; Условия нет – значит, это цикл с постусловием
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+34j
; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
; Начало
цикла
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+1Dj
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+AFp
var_a = dword ptr -4
push ebp
mov ebp, esp
; Открываем кадр стека
push ecx
; Резервируем память для локальной переменной
mov [ebp+var_a], 0
; Присваиваем локальной переменной var_a значение 0
jmp short loc_401016
; Непосредственный переход на код проверки условия продолжения цикла -
; характерный признак for
loc_40100D: ; CODE XREF: main+29j
; ^^^^^^^^^^^^^^^^^^^^
; Перекрестная ссылка, направленная вниз говорит о том, что это начало цикла
mov eax, [ebp+var_a]
; Загрузка в EAX значения переменной var_a
add eax, 1
; Увеличение EAX на единицу
mov [ebp+var_a], eax
; Обновление EAX
; Следовательно, исходный код выглядел так:
; ++a
loc_401016: ; CODE XREF: main+Bj
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+1Aj
mov esp, ebp
pop ebp
; Закрываем кадр стека
retn
main endp
Листинг 193
А теперь задействуем оптимизацию и посмотрим, как видоизмениться наш цикл:
main proc near ; CODE XREF: start+AFp
push esi
mov esi, 0Ah
; Инициализируем переменную – счетчик
; Внимание! В исходном коде начальное значение счетчика равнялось нулю!
loc_401006: ; CODE XREF: main+14j
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:00407044o
push ebp
mov ebp, esp
; Открываем кадр стека
push ebx
; Сохраняем EBX в стеке
xor ebx, ebx
; Присваиваем регистровой переменной EBX
значение 0
loc_401082: ; CODE XREF: _main+15j
; ^^^^^^^^^^^^^^^^^^^^^^
; Начало
цикла
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+AFp
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+3Fj
; ^^^^^^^^^^^^^^^^^^^^^
; Перекрестная ссылка, направленная вниз – цикл
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+2Ej
; ^^^^^^^^^^^^^^^^^^^^^
; Перекрестная ссылка направлена вверх – следовательно, это не начало цикла
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+12j main+30j ...
; ^^^^^^^^^^
; Перекрестная ссылка, направленная вниз, говорит, что это начало цикла
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+5Bj
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+5Dj
mov esp, ebp
pop ebp
; Закрываем кадр стека
retn
main endp
Листинг 197
Что ж, оператор break в обоих циклах выглядит одинаково и элементарно распознается (правда, не с первого взгляда, но отслеживанием нескольких переходов – да). А вот с бесконечными циклами не оптимизирующий компилятор подкачал, транслировав их в код, проверяющий условие, истинность (не истинность) которого очевидна. А как поведет себя оптимизирующий компилятор?
Давайте откомпилируем тот же самый пример компилятором Microsoft Visual C++ 6.0 с ключом "/Ox" и посмотрим:
main proc near ; CODE XREF: start+AFp
push esi
; Сохраняем ESI в стеке
xor esi, esi
; Присваиваем ESI значение 0
; var_ESI = 0;
loc_401003: ; CODE XREF: main+23j
; ^^^^^^^^^^^^^^^^^^^^^
; Перекрестная ссылка, направленная вперед
; Это – начало цикла
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+14j
; ^^^^^^^^^^^^^^^^^^^^^
; Это не начало цикла!
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+4Ej
; ^^^^^^^^^^^^^^^^^^^^^^
; А вот это начало цикла!
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+33j
; Конец цикла
; Да… тут есть над чем подумать!
; Компилятор нормально "перевалил" первую строку цикла
; 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+AFp
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+22j main+35j
; ^^^^^^^^^^^^^^^^^^^
; Две перекрестные ссылки, направленные вперед, говорят о том, что это либо
; начало двух циклов (один из которых – вложенный), либо переход в начало
; цикла
оператором
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+20j
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+1Aj main+5Dj
; ^^^^^^^^^
; Начало цикла
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+3Bj
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+3Dj
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+AFp
push esi
mov esi, 1
loc_401006: ; CODE XREF: main+1Fj
; ^^^^^^^^^^^^^^^^^^^^
; Начало цикла
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+9j
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+37j
; ^^^^^^^^
; Начало цикла
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+24j
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+AFp
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+43j
; ^^^^^^^^^
; Перекрестная ссылка, направленная вниз, говорит о том, что это – начало цикла
; А выше мы уже выяснили, что тип цикла - 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+14j
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+2Cj
mov esp, ebp
pop ebp
; Закрываем кадр стека
retn
main endp
Листинг 203
Оптимизированный вариант программы рассматривать не будем, т.к. это не покажет нам ничего нового. Какой бы компилятор мы не выбрали – выражения инициализации и модификации счетчиков будут обрабатываться вполне корректно в порядке их объявления в тексте программы, а вот множественные выражения продолжения цикла не умеет правильно обрабатывать ни один компилятор!