Идентификация IF - THEN - ELSE

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

Идентификация IF - THEN - ELSE — Архив WASM.RU

  Значение каждого элемента текста определяется
контекстом его употребления. Текст не описывает мир,
а вступает в сложные взаимоотношения с миром.

Тезис аналитической философии

  Существует два вида алгоритмов - безусловные и условные. Порядок действий безусловного алгоритма всегда постоянен и не зависит от входных данных. Например: "a=b+c". Порядок действий условных алгоритмов, напротив, зависит от входных данных. Например: "если c не равно нулю, то: a=b/c; иначе: вывести сообщение об ошибке".

  Обратите внимание на выделенные жирным шрифтом ключевые слова "если", "то" и "иначе", называемые операторами условия или условными операторами. Без них не обходится ни одна программа (вырожденные примеры наподобие "Hello, World!" - не в счет). Условные операторы - сердце любого языка программирования. Поэтому, чрезвычайно важно уметь их правильно идентифицировать.

  В общем виде (не углубляясь в синтаксические подробности отдельных языков программирования) оператор условия схематично изображается так:

IF (условие) THEN { оператор_1; оператор_2;} ELSE { оператор_a; оператор_b;}

  Задача компилятора - преобразовать эту конструкцию в последовательность машинных команд, выполняющих оператор_1, оператор_2, если условие истинно и, соответственно - оператор_a, оператор_b; если оно ложно. Однако микропроцессоры серии 80x86 поддерживают весьма скромный набор условных команд, ограниченный фактически одними условными переходами. Программистам, знакомым лишь с IBM PC, такое ограничение не покажется чем-то неестественным, между тем, существует масса процессоров, поддерживающих префикс условного выполнения инструкции. Т.е. вместо того, чтобы писать:

Код (Text):
  1.  
  2. TEST ECX,ECX
  3. JNZ xxx
  4. MOV EAX,0x666
  5.  

там поступают так:

Код (Text):
  1.  
  2. TEST ECX,ECX
  3. IFZ MOV EAX,0x666
  4.  

  "IFZ" - и есть префикс условного выполнения, разрешающий выполнение следующей команды только в том случае, если установлен флаг нуля.

  В этом смысле микропроцессоры 80x86 можно сравнить с ранними диалектами языка Бейсика, не разрешающими использовать в условных выражениях никакой другой оператор кроме "GOTO". Сравните:

Код (Text):
  1.  
  2. IF A=B THEN PRINT "A=B"     10 IF A=B THEN GOTO 30
  3. 20 GOTO 40
  4. 30 PRINT "A=B"
  5. 40 ... // прочий код программы
  6.  
  7. Старый диалект "Бейсика"        Новый диалект "Бейсика"  
  8.  
  9. <b>Листинг 139  </b>
  10.  

  Если вы когда-нибудь программировали на старых диалектах Бейсика, то, вероятно, помните, что гораздо выгоднее выполнять GOTO если условие ложно, а в противном случае продолжать нормальное выполнение программы. (Как видите, вопреки расхожему мнению, навыки программирования на Бейсике отнюдь не бесполезны, особенно - в дизассемблировании программ).

  Большинство компиляторов (даже не оптимизирующих) инвертируют истинность условия, транслируя конструкцию

Код (Text):
  1.  
  2. IF (условие) THEN {оператор_1; оператор_2}
  3.  

в следующий псевдокод:

Код (Text):
  1.  
  2. IF (NOT условие) THEN continue
  3. оператор_1;
  4. оператор_2;
  5. continue:
  6. ...
  7.  
  8. <b>Листинг 140</b>
  9.  

  Следовательно, для восстановления исходного текста программы, нам придется вновь инвертировать условие и "подцепить" блок операторов {оператор_1; оператор_2} к ключевому слову THEN. Т.е. если откомпилированный код выглядит так:

Код (Text):
  1.  
  2. 10 IF A<>B THEN 30
  3. 20 PRINT "A=B"
  4. 30 ...// прочий код программы
  5.  
  6. <b>Листинг 141</b>
  7.  

  Можно с уверенностью утверждать, что в исходном тексте присутствовали следующие строки: "IF A=B THEN PRINT "A=B"". А если, программист, наоборот, проверял переменные A и B на неравенство, т.е. "IF A<>B THEN PRINT "A<>B""? Все равно компилятор инвертирует истинность условия и сгенерирует следующий код:

Код (Text):
  1.  
  2. 10 IF A=B THEN 30
  3. 20 PRINT "A<>B"
  4. 30 ...// прочий код программы
  5.  
  6. <b>Листинг 142</b>
  7.  

  Конечно, встречаются и дебильные компиляторы, страдающие многословием. Их легко распознать по безусловному переходу, следующему сразу же после условного оператора:

Код (Text):
  1.  
  2. IF (условие) THEN do
  3. GOTO continue
  4. do:
  5. оператор1;
  6. оператор2;
  7. continue:
  8. ...
  9.  
  10. <b>Листинг 143</b>
  11.  

  В таком случае инвертировать условие не нужно. Впрочем, если это сделать, ничего страшного не произойдет, разве что код программы станет менее понятным, да и то не всегда.

  Рассмотрим теперь как транслируется полная конструкция

Код (Text):
  1.  
  2. IF (условие) THEN {оператор_1; оператор_2;} ELSE {оператор_a; оператор_b;}
  3.  

Одни компиляторы поступают так:

Код (Text):
  1.  
  2. IF (условие) THEN do_it
  3. // Ветка ELSE
  4. оператор_a;
  5. оператор_b
  6. GOTO continue
  7.  
  8. do_it:
  9. //Ветка IF
  10. оператор_1;
  11. оператор_2;
  12. continue:
  13.  

  А другие так:

Код (Text):
  1.  
  2. IF (NOT условие) THEN else
  3. //Ветка IF
  4. оператор_1;
  5. оператор_2;
  6. GOTO continue
  7.  
  8. else:
  9. // Ветка ELSE
  10. оператор_a;
  11. оператор_b
  12. continue:
  13.  
  14. <b>Листинг 144</b>
  15.  

  Разница межу ними в том, что вторые инвертируют истинность условия, а первые - нет. Поэтому, не зная "нрава" компилятора, определить: как выглядел подлинный исходный текст программы - невозможно! Однако это не создает проблем, ибо условие всегда можно записать так, как это удобно. Допустим, не нравится вам конструкция "IF (c<>0) THEN a=b/c ELSE PRINT "Ошибка!"" пишите ее так: "IF (c==0) THEN PRINT "Ошибка!" ELSE a=b/c" и - ни каких гвоздей!

  Типы условий: Условия делятся на простые (элементарные) и сложные (составные). Пример первых - "if (a==b)...", вторых "if ((a==b) && (a!=0))...". Очевидно, что любое сложное условие можно разложить на ряд простых условий. Вот с простых условий мы и начнем.

  Существуют два основных типа элементарных условий: условия отношений ("меньше", "равно", "больше", "меньше или равно", "не равно", "больше или равно", соответственно обозначаемые как: "<", "==", ">", "<=", "!=", ">=") и логические условия ("И", "ИЛИ", "НЕ", "И исключающее ИЛИ", в Си-нотации соответственно обозначаемые так: "&", "|", "!", "^"). Известный хакерский авторитет Мэтт Питрек приплетает сюда и проверку битов, однако несколько некорректно смешивать в одну кучу людей и коней, даже если они чем-то и взаимосвязаны. Поэтому, о битовых операциях мы поговорим отдельно.

  Если условие истинно, оно возвращает булево значение TRUE, соответственно, если ложно - FALSE. Внутренне (физическое) представление булевых переменных зависит от конкретной реализации и может быть любым. По общепринятому соглашению, FALSE равно нулю, а TRUE не равно нулю. Часто (но не всегда) TRUE равно единице, но на это нельзя полагаться! Так, код "IF ((a>b)!=0)Е" абсолютно корректен, а: "IF ((a>b)==1)Е" привязан к конкретной реализации и потому нежелателен.

  Обратите внимание: "IF ((a>b)!=0)Е" проверяет на неравенство нулю отнюдь не значения самих переменных a и b, а именно - результата их сравнения. Рассмотрим следующий пример: "IF ((666==777)==0) printf("Woozl!")" - как вы думаете, что отобразится на экране, если его запустить? Правильно - "Woozl"! Почему? Ведь ни 666, ни 777 не равно нулю! Да, но ведь 666 != 777, следовательно, условие (666==777) - ложно, следовательно равно нулю. Кстати, если записать "IF ((a=b)==0)Е" получится совсем иной результат - значение переменной b будет присвоено переменной a, и потом проверено на равенство нулю.

  Логические условия чаще всего используются для связывания двух или более элементарных условий отношения в составное. Например, "IF ((a==b) && (a!=0))Е". При трансляции программы компилятор всегда выполняют развертку составных условий в простые. В данном случае это происходит так: "IF a==b THEN IF a=0 THENЕ" На втором этапе выполняется замена условных операторов на оператор GOTO:

Код (Text):
  1.  
  2. IF a!=b THEN continue
  3. IF a==0 THEN continue
  4. ...// код условия
  5. :continue
  6. ...// прочий код
  7.  
  8. <b>Листинг 145</b>
  9.  

  Порядок вычисления элементарных условий в сложном выражении зависит от прихотей компилятора, гарантируется лишь, что условия, "связанные" операцией логического "И" проверяются слева направо в порядке их объявления в программе. Причем, если первое условие ложно, то следующее за ним вычислено не будет! Это дает возможность писать код наподобие следующего: "if ((filename) & (f=fopen(&filename[0],"rw")))Е" - если указатель filename указывает на невыделенную область памяти (т.е. попросту говоря содержит нуль - логическое FALSE), функция fopen не вызывается и ее краха не происходит. Такой способ вычислений получил название "быстрых булевых операций" (теперь-то вы знаете, что подразумевается под "быстротой").

  Перейдем теперь к вопросу идентификации логических условий и анализу сложных выражений. Вернемся к уже облюбованному нами выражению "if ((a==b) && (a!=0))Е" и вглядимся в результат его трансляции:

Код (Text):
  1.  
  2. IF a!=b THEN continue  -------!
  3. IF a==0 THEN continue ---!    !
  4. ...// код условия        !    !
  5. :continue             <--! <---
  6. ...// прочий код
  7.  
  8. <b>Листинг 146</b>
  9.  

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

  Идентификация логической операции "ИЛИ" намного сложнее в силу неоднозначности ее трансляции. Рассмотрим это на примере выражения "if ((a==b) || (a!=0))...". Его можно разбить на элементарные операции и так:

Код (Text):
  1.  
  2. IF a==b THEN do_it -------------!
  3. IF a!=0 THEN do_it -----!       !
  4. goto continue -----!    !       !
  5. :do_it             ! <--! <-----!
  6. ...// код условия  !
  7. :continue     <-!
  8. ...// прочий код
  9.  
  10. <b>Листинг 147</b>
  11.  

  и так:

Код (Text):
  1.  
  2. IF a==b THEN do_it  -----------!
  3. IF a==0 THEN continue--!       !
  4. :do_it                 ! <-----!
  5. ...// код условия      !
  6. :continue  <-----------!
  7. ...// прочий код
  8.  
  9. <b>Листинг 148</b>
  10.  

  Первый вариант обладает весьма запоминающийся внешностью - серия проверок (без инверсии условия) на одну и ту же метку, расположенную перед кодом условия, а в конце этой серии - безусловный переход на метку, расположенную позади кода условия.

  Однако оптимизирующие компиляторы выкидывают безусловный переход, инвертируя проверку последнего условия в цепочке и, соответственно меняя адрес перехода. По неопытности эту конструкцию часто принимают за смесь OR и AND. Кстати, о смещенных операциях - рассмотрим результат трансляции следующего выражения: "if ((a==b) || (a==c) && a(!=0))...":

Код (Text):
  1.  
  2. IF a==b THEN check_null
  3. IF a!=c THEN continue
  4. check_null:
  5. IF a==0 THEN continue
  6. ...// код условия
  7. continue:
  8. ...// прочий код
  9.  
  10. <b>Листинг 149</b>
  11.  

  Как из непроходимого леса элементарных условий получить одно удобочитаемое составное условие? Начинаем плясать от печки, т.е. от первой операции сравнения. Смотрите, если условие a==b окажется истинно, оно "выводит из игры" проверку условия a!=c. Такая конструкция характерна для операции OR - т.е. достаточно выполнения хотя бы одного условия из двух для "срабатывания" кода. Пишем в уме или карандашом: "if ((a==b) || ...)", далее - если условие (a!=c) истинно, все дальнейшие проверки прекращаются, и происходит передача управления на метку, расположенную позади условного кода. Логично предположить, что мы имеем дело в последней операцией OR в цепочке сравнений - это ее "почерк". Значит, мы инвертируем условие выражения и продолжаем писать: "if ((a==b) || (a==c)Е)". Последний бастион - проверка условия "a==0". Выполнить условный код, миновав его не удаться, - следовательно, это не OR, а AND! А AND всегда инвертирует условие срабатывания, и поэтому, оригинальный код должен был выглядеть так: "if ((a==b) || (a==c) && (a!=0))". Ура! У нас получилось!

  Впрочем, как любил поговаривать Дмитрий Николаевич, не обольщайтесь - то, что мы рассмотрели - это простейший пример. В реальной жизни оптимизирующие компиляторы такого понаворочают...

  ___Впрочем, для ломания головы вполне хватит и не оптимизирующих, но прежде, чем перейти к изучению конкретных реализаций, рассмотрим на последок две "редкоземельные" операции NOT и XOR.

  __NOT - одноместная операция, поэтому, она не может использоваться для связывания, однако,

  Наглядное представление сложных условий в виде дерева. Конструкцию, состоящую из трех - четырех элементарных условий, можно проанализировать и в уме (да и то, если есть соответствующие навыки), но хитросплетения пяти и более условий образуют самый настоящий лабиринт - его с лету не возьмешь. Неоднозначность трансляции сложных условий порождает неоднозначность интерпретации, что приводит к многовариантному анализу, причем с каждым шагом в голове приходится держать все больше и больше информации. Так недолго и крышей поехать или окончательно запутаться и получить неверных результат.

  Выход - в использовании двухуровневой системы ретрансляции. На первом этапе элементарные условия преобразуются к некоторой промежуточной форме записи, наглядно и непротиворечиво отображающей взаимосвязь элементарных операций. Затем осуществляется окончательная трансляция в любую подходящую нотацию (например, Си, Бейсик или Pascal).

  Единственная проблема - выбрать удачную промежуточную форму. Существует множество решений, но в книге по соображениям экономии бумажного пространства, мы рассмотрим только одно - деревья.

  Изобразим каждое элементарное условие в виде узла, с двумя ветвями, соответствующим состояниям: условие истинно и условие ложно. Для наглядности обозначим "ложь" равнобедренным треугольником, а "истину" - квадратом и условимся всегда располагать ложь на левой, а истину на правой ветке. Получившуюся конструкцию назовем "гнездом" (nest).

Рисунок 22. 0х015 Схематическое представление гнезда (nest)

  Гнезда могут объединяться в деревья, соединясь узлами с ветками другого узла. Причем, каждый узел может соединяться только с одним гнездом, но всякое гнездо может соединяться с несколькими узлами. Непонятно? Не волнуйтесь, сейчас со всем этим мы самым внимательным образом разберемся.

  Рассмотрим объединение двух элементарных условий логической операцией "AND" на примере выражения "((a==b) && (a!=0))". Извлекаем первое слева условие (a==b), "усаживаем" его в гнездо с двумя ветвями: левая соответствует случаю, когда a!=b (т.е. условие a==b - ложно), а правая, соответственно, - наоборот. Затем, то же самое делаем и со вторым условием (a!=0). У нас получаются два очень симпатичных гнездышка, - остается лишь связать их меж собой операцией логического "AND". Как известно, "AND" выполняет второе условие только в том случае, если истинно первое. Значит, гнездо (a!=0) следует прицепить к правой ветке гнезда (a==b). Тогда - правая ветка гнезда (a!=0) будет соответствовать истинности выражения "((a==b) && (a!=0))", а обе левые ветки - его ложности. Обозначим первую ситуацию меткой "do_it", а вторую - "continue". В результате дерево должно принять вид, изображенный на рис. 23.

  Для наглядности отметим маршрут из вершины дерева к метке "do_it" жирной стрелкой. Как видите, в пункт "do_it" можно попасть только одним путем. Вот так графически выглядит операция "AND".

Рисунок 23. 0х016 Графическое представление операции AND в виде двоичного дерева. Обратите внимание - в пункт do_it можно попасть только одним путем!

  Перейдем теперь к операции логического "OR". Рассмотрим конструкцию "((a==b) || (a!=0))". Если условие "(a==b)" истинно, то и все выражение считается истинным. Следовательно, правая ветка гнезда "(a==b)" связана с меткой "do_it". Если же условие же "(a==b)" ложно, то выполняется проверка следующего условия. Значит, левая ветка гнезда "(a==b)" связана с гнездом "(a!=b)". Очевидно, если условие "(a!=b)" истинно, то истинно и все выражение "((a==b) || (a!=0))", напротив, если условие "(a!=b)" ложно, то ложно и все выражение, т.к. проверка условия "(a!=b)" выполняется только в том случае, если условие "(a==b)" ложно. Отсюда мы заключаем, что левая ветка гнезда "(a!=b)" связана с меткой "continue", а правая - с "do_it". (см. рис. 24). Обратите внимание - в пункт "do_it" можно попасть двумя различными путями! Вот так графически выглядит операция "OR".

Рисунок 24. 0х017 Графическое представление операции OR в виде двоичного дерева. Обратите внимание - в пункт do_it можно попасть двумя различными путями!

  До сих пор мы отображали логические операции на деревья, но ведь деревья создавались как раз для противоположной цели - преобразованию последовательности элементарных условий к интуитивно понятному представлению. Займемся этим? Пусть в тексте программы встретится следующий код:

Код (Text):
  1.  
  2. IF a==b THEN check_null
  3. IF a!=c THEN continue
  4. check_null:
  5. IF a==0 THEN continue
  6. ...// код условия
  7. continue:
  8. ...// прочий код
  9.  
  10. <b>Листинг 150</b>
  11.  

  Извлекаем условие (a==b) и сажаем его в "гнездо", - смотрим: если оно ложно, то выполняется проверка (a!=c), значит, гнездо (a!=c) связано с левой веткой гнезда (a==b). Если же условие (a==b) истинно, то управление передается метке check_null, проверяющей истинность условия (a==0), следовательно, гнездо (a==0) связано с правой веткой гнезда (a==b). В свою очередь, если условие (a!=с) истинно, управление получает метка "continue", в противном случае - "check_null". Значит, гнездо (a!=0) связано одновременно и с правой веткой гнезда (a==b) и с левой веткой гнезда (a!=c).

  Конечно, это проще рисовать, чем описывать! Если вы все правильно зарисовали, у вас должно получится дерево очень похожее на изображенное на рисунке 25.

  Смотрите: к гнезду "(a==0)" можно попасть двумя путями - либо через гнездо (a==b), либо через цепочку двух гнезд (a==b) а (a!=c). Следовательно, эти гнезда связаны операцией OR. Записываем: "if ( (a==b) || !(a!=c)Е.)". Откуда взялся NOT? Так ведь гнездо (a==0) связано с левой веткой гнезда (a!=с), т.е. проверяется ложность его истинности! (Кстати, "ложность истинности" - очень хорошо звучит). Избавляемся от NOT, инвертируя условие: "if ( (a==b) || (a==c)Е.)Е". Далее - из гнезда (a==0) до пункта do_it можно добраться только одним путем, значит, оно связано операцией AND. Записываем: "if (((a==b) || (a==c)) && !(a==0))Е". Теперь избавляемся от лишних скобок и операции NOT. В результате получается: "if ((a==b) || (a==c) && (a!=0)) {// Код условия}"

  Не правда ли все просто? Причем вовсе необязательно строить деревья вручную, - при желании можно написать программу, берущую эту работу на себя.

Рисунок 25. 0х018 Графическое представление сложного выражения

  Исследование конкретных реализаций. Прежде чем приступать к отображению конструкции "IF (сложное условие) THEN оператор_1:оператор_2 ELSE оператор_а:оператор_b" на машинный язык, вспомним, что, во-первых, агрегат "IF - THEN - ELSE" можно выразить через "IF - THEN", во-вторых, "THEN оператор_1:оперратор_2" можно выразить через "THEN GOTO do_it", в-третьих, любое сложное условие можно свести к последовательности элементарных условий отношения. Таким образом, на низком уровне мы будем иметь дело лишь с конструкциями "IF (простое условие отношения) THEN GOTO do_it", а уже из них, как из кирпичиков, можно сложить что угодно.

  Итак, условия отношения, или другими словами, результат операции сравнения двух чисел. В микропроцессорах Intel 80x86 сравнение целочисленных значений осуществляется командой CMP, а вещественных - одной из следующих инструкций сопроцессора: FCOM, FCOMP, FCOMPP, FCOMI, FCOMIP, FUCOMI, FUCOMIP. Предполагается, что читатель уже знаком с языком ассемблера, поэтому не будем подробно останавливаться на этих инструкциях и рассмотрим их лишь вкратце.

  ::CMP. Команда CMP эквивалентна операции целочисленного вычитания SUB, за одним исключением - в отличие от SUB, CMP не изменяет операндов, а воздействует лишь на флаги основного процессора: флаг нуля, флаг переноса, флаг знака и флаг переполнения.

  Флаг нуля устанавливается в единицу, если результат вычитания равен нулю, т.е. операнды равны друг другу.

  Флаг переноса устанавливается в единицу, если в процессе вычитания произошел заем из старшего бита уменьшаемого операнда, т.е. уменьшаемое меньше вычитаемого.

  Флаг знака равен старшему - знаковому - биту результата вычислений, т.е. результат вычислений - отрицательное число.

  Флаг переполнения устанавливается в единицу, если в результате вычислений "залез" в старший бит, приводя к потере знака числа.

  Для проверки состояния флагов существует множество команд условных переходов, выполняющихся в случае, если определенный флаг (набор флагов) установлен (сброшен). Инструкции, использующиеся для анализа результата сравнения целых чисел, перечислены в таблице 16.

  В общем случае конструкция "IF (элементарное условие отношения) THEN do_it" транслируется в следующие команды процессора:

Код (Text):
  1.  
  2. CMP A,B
  3. Jxx do_it
  4. continue:
  5.  

  Между инструкциями "CMP" и "Jxx" могут находиться и другие команды, не изменяющие флагов процессора, например "MOV", "LEA".

условие состояние флагов инструкция
Zero flag Carry Flag Sing Flag
a == b 1 ? ? JZ JE  
a != b 0 ? ? JNZ JNE  
a < b беззнаковое ? 1 ? JC JB JNAE
знаковое ? ? !=OF JL JNGE  
a > b беззнаковое 0 0 ? JA JNBE  
знаковое 0 ? ==OF JG JNLE  
a >=b беззнаковое ? 0 ? JAE JNB JNC
знаковое ? ? ==OF JGE JNL  
a <= b беззнаковое (ZF == 1) || (CF == 1) ? JBE JNA  
знаковое 1 ?   JLE JNG  

Таблица 16. Соответствие операций отношения командам процессора

  ::сравнение вещественных чисел. Команды сравнения вещественных чисел FCOMxx (см. таблицу 18) в отличие от команд целочисленного сравнения воздействуют на регистры сопроцессора, а не основного процессора. На первый взгляд - логично, но весь камень преткновения в том, что инструкций условного перехода, управляемых флагами сопроцессора, не существует! К тому же, флаги сопроцессора непосредственно недоступны, - чтобы прочитать их статус необходимо выгрузить регистр состояния сопроцессора SW в память или регистр общего назначения основного процессора.

  Хуже всего - анализировать флаги вручную! Если при сравнении целых чисел можно и не задумываться: какими именно флагами управляется условный переход, достаточно написать, скажем: "CMP A,B; JGE do_it". ("Jump [if] Great [or] Equal" - прыжок, если A больше или равно B), то теперь этот номер не пройдет! Правда, можно схитрить и скопировать флаги сопроцессора в регистр флагов основного процессора, а затем использовать "родные" инструкции условного перехода из серии Jxx.

  Конечно, непосредственно скопировать флаги из сопроцессора в основной процессор нельзя и эту операцию приходится осуществлять в два этапа. Сначала флаги FPU выгружать в память или регистр общего назначения, а уже оттуда заталкивать в регистр флагов CPU. Непосредственно модифицировать регистр флагов CPU умеет только одна команда - POPF. Остается только выяснить - каким флагам сопроцессора, какие флаги процессора соответствуют. И вот что удивительно - флаги 8й, 10й и 14й сопроцессора совпадают с 0ым, 2ым и 6ым флагами процессора - CF, PF и ZF соответственно (см. таблицу 17). То есть - старшей байт регистра флагов сопроцессора можно безо всяких преобразований затолкать в младший байт регистра флагов процессора и это будет работать, ноЕ при этом исказятся 1й, 3й и 5й биты флагов CPU, никак не используемые в текущих версиях процессора, но зарезервированные на будущее. Менять значение зарезервированных битов нельзя! Кто знает, вдруг завтра один из них будут отвечать за самоуничтожение процессора? Шутка, конечно, но в ней есть своя доля истины.

  К счастью, никаких сложных манипуляций нам проделывать не придется - разработчики процессора предусмотрели специальную команду - SAHF, копирующую 8й, 10й, 12й, 14й и 15й бит регистра AX в 0й, 2й, 4й, 6й и 7й бит регистра флагов CPU соответственно. Сверяясь по таблице 17 мы видим, что 7й бит регистра флагов CPU содержит флаг знака, а соответствующий ему флаг FPU - признак занятости сопроцессора!

  Отсюда следует, что для анализа результата сравнения вещественных чисел использовать знаковые условные переходы (JL, JG, JLE, JNL, JNLE, JGE, JNGE) нельзя! Они работают с флагами знака и переполнения, - естественно, если вместо флага знака им подсовывают флаг занятости сопроцессора, а флаг переполнения оставляют в "подвешенном" состоянии, условный переход будет срабатывать не так, как вам бы этого хотелось! Применяйте лишь беззнаковые инструкции перехода - JE, JB, JA и др. (см. таблицу 16)

  Разумеется, это не означает, что сравнивать знаковые вещественные значения нельзя, - можно, еще как! Но для анализа результатов сравнения обязательно всегда использовать только беззнаковые условные переходы!

CPU 7 6 5 4 3 2 1 0
SF ZF -- AF -- PC -- CF
FPU 15 14 13 12 11 10 9 8
Busy! C3(ZF) TOP C2(PF) C1 C0(CF)

Таблица 17. Соответствие флагов CPU и FPU

  Таким образом, вещественная конструкция "IF (элементарное условие отношения) THEN do_it" транслируется в одну из двух следующих последовательностей инструкций процессора:

Код (Text):
  1.  
  2. fld    [a]        fld    [a]
  3. fcomp  [b]        fcomp  [b]
  4. fnstsw ax         fnstsw ax
  5. sahf              test   ah, bit_mask
  6. jxx    do_it      jnz    do_it
  7.  
  8. <b>Листинг 151</b>
  9.  

  Первый вариант более нагляден, зато второй работает быстрее. Однако, такой код (из всех известных мне компиляторов) умеет генерировать один лишь Microsoft Visual C++. Borland C++ и хваленый WATCOM C испытывают неопределимую тягу к инструкции SAHF, чем вызывают небольшие тормоза, но чрезвычайно упрощают анализ кода, - ибо, встретив команду наподобие JNA, мы и спросонок скажем, что переход выполняется когда a <= b, а вот проверка битвой маски "TEST AH, 0x41/JNZ do_it" заставит нас крепко задуматься или машинально потянуться к справочнику за разъяснениями (см. таблицу 16)

  Команды семейства FUCOMIxx в этом смысле гораздо удобнее в обращении, т.к. возвращают результат сравнения непосредственно в регистры основного процессора, но - увы - их "понимает" только Pentium Pro, а в более ранних микропроцессорах они отсутствуют. Поэтому, вряд ли читателю доведется встретиться с ними в реальных программах, так что не имеет никакого смысла останавливаться на этом вопросе. Во всяком случае, всегда можно обратится к странице 3-112 руководства "Instruction Set Reference", где эти команды подробно описаны.

инструкция назначение результат
FCOM Сравнивает вещественное значение, находящееся на вершине стека сопроцессора, с операндом, находящимся в памяти или стеке FPU флаги FPU
FCOMP То же самое, что и FCOM, но с выталкиванием вещественного значения с вершины стека
FCOMPP Сравнивает два вещественных значения, лежащих на вершине стека сопроцессора, затем выталкивает их из стека
FCOMI Сравнивает вещественное значение, находящееся на вершине стека сопроцессора с другим вещественным значением, находящимся в стеке FPU флаги CPU
FCOMIP Сравнивает вещественное значение, находящееся на вершине стека сопроцессора с другим вещественным значением, находящимся в стеке FPU, затем выталкивает верхнее значение из стека
FUCOMI Неупорядоченно сравнивает вещественное значение, находящееся на вершине стека сопроцессора с другим вещественным значением, находящимся в стеке FPU
FUCOMIP Неупорядоченно сравнивает вещественное значение, находящееся на вершине стека сопроцессора с другим вещественным значением, находящимся в стеке FPU, затем выталкивает верхнее значение из стека

Таблица 18. Команды сравнения вещественных значений

флаги FPU назначение битовая маска
OE Флаг переполнения Overfull Flag #0x0008
C0 Флаг переноса Carry Flag #0x0100
C1 ---   #0x0200
C2 Флаг четности Partite Flag #0x0400
C3 Флаг нуля Zero Flag #0x4000

Таблица 19. Назначение и битовые маски флагов сопроцессора

отношение состояние флагов FPU SAHF битовая маска
a C0 == 1 JB #0x0100 == 1
a>b C0 == 0 C3 == 0 JNBE #0x4100 == 0
a==b C3 == 1 JZ #0x4000 == 1
a!=b C3 == 0 JNZ #0x4000 == 0
a>=b C0 == 0 JNB #0x0100 == 0
a<=b C0 == 1 C3 === 1 JNA #0x4100 == 1

Таблица 20. Состояние регистров флагов для различных операций отношения. 'a' - левый, а 'b' правый операнд команды сравнения вещественных значений

компилятор алгоритм анализа флагов FPU
Borland C++ копирует флаги сопроцессора в регистр флагов основного процессора
Microsoft Visual C++ тест битовой маски
WATCOM C копирует флаги сопроцессора в регистр флагов основного процессора
Free Pascal копирует флаги сопроцессора в регистр флагов основного процессора

Таблица 21. "Характер" некоторых компиляторов

  Условные команды булевой установки. Начиная с 80386 чипа, язык микропроцессоров Intel обогатился командой условной установки байта - SETxx, устанавливающей свой единственный операнд в единицу (булево TRUE), если условие "xx" равно и, соответственно, сбрасывающую его в нуль (булево FALSE), если условие "xx" - ложно.

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

команда отношение условие
SETA SETNBE   a>b беззнаковое CF == 0 && ZF == 0
SETG SETNLE   знаковое ZF == 0 && SF == OF
SETAE SETNC SETNB a>=b беззнаковое CF == 0
SETGE SETNL   знаковое SF == OF
SETB SETC SETNAE a беззнаковое CF == 1
SETL SETNGE   знаковое SF != OF
SETBE SETNA   a<=b беззнаковое CF == 1 || ZF == 1
SETLE SETNG   знаковое ZF == 1 || SF != OF
SETE SETZ   a==b --- ZF == 1
SETNE SETNZ   a!=0 --- ZF == 0

Таблица 22. Условные команды булевой установки

  Прочие условные команды. Микропроцессоры серии 80x86 поддерживают множество условных команд, в общем случае не отображающихся на операции отношения, а потому и редко использующиеся компиляторами (можно даже сказать - вообще не использующиеся), но зато часто встречающиеся в ассемблерных вставках. Словом, они заслуживают хотя бы беглого упоминания.

  ::Команды условного перехода. Помимо описанных в таблице 16, существует еще восемь других условных переходов - JCXZ, JECXZ, JO, JNO, JP (он же JPE), JNP (он же JPO), JS и JNS. Из них только JCXZ и JECXZ имеют непосредственное отношение к операциям сравнения. Оптимизирующие компиляторы могут заменять конструкцию "CMP [...]CX, 0\JZ do_it" на более короткий эквивалент "J[...]CX do_it", однако, чаще всего они (в силу ограниченности интеллекта и лени своих разработчиков) этого не делают.

  Условные переходы JO и JNS используются в основном в математических библиотеках для обработки чисел большой разрядности (например, 1024 битых целых).

  Условные переходы JS и JNS помимо основного своего предназначения часто используются для быстрой проверки значения старшего бита.

  Условные переходы JP и JNP вообще практически не используются, ну разве что в экзотичных ассемблерных вставках.

команда переход, если… флаги
JCXZ регистр CX равен нулю CX == 0
JECXZ регистр ECX равен нулю ECX == 0
JO переполнение OF == 1
JNO нет переполнения OF == 0
JP JPE число бит младшего байта результата четно PF == 1
JNP JPO число бит младшего байта результата нечетно PF == 0
JS знаковый бит установлен SF == 1
JNS знаковый бит сброшен SF == 0

Таблица 23. Вспомогательные условные переходы

  ::Команды условной пересылки. Старшие процессоры семейства Pentium (Pentium Pro, Pentium II, CLERION) поддерживают команду условной пересылки CMOVxx, пересылающей значение из источника в приемник, если условие xx - истинно. Это позволяет писать намного более эффективный код, не содержащий ветвлений и укладывающийся в меньшее число инструкций.

  Рассмотрим конструкцию "IF a<b THEN a=b". Сравните: как она транслируется с использованием условных переходов (1) и команды условной пересылки (2).

Код (Text):
  1.  
  2. CMP A,B             CMP A, B
  3. JAE continue:           CMOVB A, B
  4. MOV A,B
  5. continue:
  6. 1)              2)
  7.  
  8. <b>Листинг 152</b>
  9.  

  К сожалению, ни один из известных мне компиляторов на момент написания этих строк, никогда не использовал CMOVxx при генерации кода, однако, выигрыш от нее настолько очевиден, что появления усовершенствованных оптимизирующих компиляторов следует ожидать в самом ближайшем будущем. Вот почему эта команда включена в настоящий обзор. В таблице 24 дана ее краткое, но вполне достаточное для дизассемблирования программ, описание. За более подробными разъяснениями обращайтесь к странице 3-59 справочного руководства "Instruction Set Reference" от Intel.

команда отношение условие
CMOVA CMOVNBE   a>b беззнаковое CF == 0 && ZF == 0
CMOVG CMOVNLE   знаковое ZF == 0 && SF == OF
CMOVAE CMOVNC CMOVNB a>=b беззнаковое CF == 0
CMOVGE CMOVNL   знаковое SF == OF
CMOVB CMOVC CMOVNAE a беззнаковое CF == 1
CMOVL CMOVNGE   знаковое SF != OF
CMOVBE CMOVNA   a<=b беззнаковое CF == 1 || ZF == 1
CMOVLE CMOVNG   знаковое ZF == 1 || SF != OF
CMOVE CMOVZ   a==b --- ZF == 1
CMOVNE CMOVNZ   a!=0 --- ZF == 0

Таблица 24. Основные команды условной пересылки

  Булевы сравнения. Логической лжи (FALSE) соответствует значение ноль, а логической истине (TRUE) - любое ненулевое значение. Таким образом, булевы отношения сводятся к операции сравнения значения переменной с нулем. Конструкция "IF (a) THEN do_it" транслируется в "IF (a!=0) THEN do_it".

  Практически все компиляторы заменяют инструкцию "CMP A, 0" более короткой командой "TEST A,A" или "OR A,A". Во всех случаях, если A==0, устанавливается флаг нуля и, соответственно, наоборот.

  Поэтому, встретив к дизассемблером тексте конструкцию a la "TEST EAX, EAX\ JZ do_it" можно с уверенностью утверждать, что мы имеем дело с булевым сравнением.

  Идентификация условного оператора "(условие)?do_it:continue" Конструкция "a=(условие)?do_it:continue" языка Си в общем случае транслируется так: "IF (условие) THEN a=do_it ELSE a=continue", однако результат компиляции обоих конструкций вопреки распространенному мнению, не всегда идентичен.

  В силу ряда обстоятельств оператор "?" значительно легче поддается оптимизации, чем ветвление "IF - THEN - ELSE". Покажем это на следующем примере:

Код (Text):
  1.  
  2. main()
  3. {
  4. int a;      // Переременная специально не иницилизирована
  5. int b;      // чтобы компилятор не заменил ее константой
  6.  
  7. a=(a>0)?1:-1;   // Условный оператор
  8.  
  9. if (b>0)        // Ветвление
  10. b=1;
  11. else
  12. b=-1;
  13.  
  14. return a+b;
  15. }
  16.  
  17. <b>Листинг 153</b>
  18.  

  Если пропустить эту программу сквозь компилятор Microsoft Visual C++, на выходе мы получим такой код:

Код (Text):
  1.  
  2. push    ebp
  3. mov ebp, esp
  4. ; Открываем кадр стека
  5.  
  6. sub esp, 8
  7. ; Резервируем место для локальных переменных
  8.  
  9. ; // Условный оператор ?
  10. ; Начало условного оператора ?
  11. xor eax, eax
  12. ; Обнуляем EAX
  13.  
  14. cmp [ebp+var_a], 0
  15. ; Сравниваем переменную a с нулем
  16.  
  17. setle   al
  18. ; Поместить в al значение 0x1, если var_a <= 0
  19. ; Соответственно, поместить в al значение 0, если var_a>0
  20.  
  21. dec eax
  22. ; Уменьшить EAX на единицу
  23. ; Теперь,     если var_a > 0, то EAX := -1
  24. ;       если var_a <=0, то EAX := 0
  25.  
  26. and eax, 2
  27. ; Сбросить все биты, кроме второго слева, считая от одного
  28. ; Теперь,     если var_a > 0, то EAX := 2
  29. ;       если var_a <=0, то EAX := 0
  30.  
  31. add eax, 0FFFFFFFFh
  32. ; Отнять от EAX 0x1
  33. ; Теперь,     если var_a > 0, то EAX := 1
  34. ;       если var_a <=0, то EAX := -1
  35. mov [ebp+var_a], eax
  36. ; Записать результат в переменную var_a
  37. ; Конец оператора ?
  38. ; Обратите внимание: для трансляции условного оператора не потребовалось ни
  39. ; одного условного перехода, - компилятор сумел обойтись без ветвлений!
  40.  
  41.  
  42. ; // Ветвление
  43. ; Начало ветвления IF - THEN - ELSE
  44. cmp [ebp+var_b], 0
  45. ; Сравнение переменной var_b с нулем
  46.  
  47. jle short else
  48. ; Переход, если var_b <= 0
  49.  
  50. ; Ветка "var_b > 0"
  51. mov [ebp+var_b], 1
  52. ; Записываем в переменную var_b значение 1
  53.  
  54. jmp short continue
  55. ; Переход к метке continue
  56.  
  57. ; Ветка "var_b > 0"
  58. else:                   ; CODE XREF: _main+1D j
  59. mov [ebp+var_b], 0FFFFFFFFh
  60. ; Записываем в переменную var_b значение -1
  61.  
  62. continue:                   ; CODE XREF: _main+26 j
  63. ; Конец ветвления IF-THEN-ELSE
  64. ; Обратите внимание - представление ветвления "IF-THEN-ELSE" намного компактнее
  65. ; условного оператора "?", однако, содержит в себе условные переходы, ощутимо
  66. ; снижающие быстродействие программы
  67.  
  68. mov eax, [ebp+var_a]
  69. ; Загружаем в EAX значение переменной var_a
  70.  
  71. add eax, [ebp+var_b]
  72. ; Складываем значение переменной var_a со значением переменной var_b
  73. ; и помещаем результат в EAX
  74.  
  75. mov esp, ebp
  76. pop ebp
  77. ; Закрываем кадр стека
  78. retn
  79.  
  80. <b>Листинг 154</b>
  81.  

  Таким образом, мы видим, что нельзя апории утверждать, будто бы результат трансляции условного оператора "?" всегда эквивалентен результату трансляции конструкции "IF-THEN-ELSE". Однако тот же Microsoft Visual C++ в режиме агрессивной оптимизации в обоих случаях генерирует идентичный код. Смотрите:

Код (Text):
  1.  
  2. _main       proc near
  3. push    ecx
  4. ; Резервируем место для локальных переменных a и b
  5. ; Поскольку, они никогда не используются вместе, а только поочередно,
  6. ; компилятор помещает их в одну ячейку памяти
  7.  
  8. mov edx, [esp+0]                    ; команда N1 оператора ?
  9. ; Загрузка в EDX значения переменной a
  10.  
  11. xor eax, eax                    ; команда N2 оператора ?
  12. ; Обнуляем EAX
  13. ; Поскольку, команда setle al изменяет содержимое одного лишь al, и не трогает
  14. ; остальную часть регистра, нам приходится очищать его самостоятельно
  15.  
  16. test    edx, edx                    ; команда N3 оператора ?
  17. ; Проверка переменной a на равенство нулю
  18.  
  19. mov edx, [esp+0]                    ; команда N1 ветвления IF
  20. ; Загрузка в EDX значения переменной b
  21.  
  22. setle   al                      ; команда N4 оператора ?
  23. ; Поместить в al значение 0x1, если a <= 0
  24. ; Соответственно, поместить в al значение 0, если a>0
  25.  
  26. dec eax                     ; команда N5 оператора ?
  27. ; Уменьшить EAX на единицу
  28. ; Теперь,     если a > 0, то EAX := -1
  29. ;       если a <=0, то EAX := 0
  30.  
  31. xor ecx, ecx                    ; команда N2 ветвления IF
  32. ; Обнулить ECX
  33.  
  34. and eax, 2                      ; команда N6 оператора ?
  35. ; Сбросить все биты, кроме второго слева, считая от одного
  36. ; Теперь,     если a > 0, то EAX := 2
  37. ;       если a <=0, то EAX := 0
  38.  
  39. dec eax                     ; команда N7 оператора ?
  40. ; Уменьшить EAX на единицу
  41. ; Теперь,     если a > 0, то EAX := 1
  42. ;       если a <=0, то EAX := -1
  43.  
  44.  
  45. test    edx, edx                    ; команда N3 ветвления IF
  46. ; Проверка переменной b на равенство нулю
  47.  
  48. setle   cl                      ; команда N4 ветвления IF
  49. ; Поместить в сl значение 0x1, если b <= 0
  50. ; Соответственно, поместить в cl значение 0, если b>0
  51.  
  52. dec ecx                     ; команда N5 ветвления IF
  53. ; Уменьшить ECX на единицу
  54. ; Теперь,     если b > 0, то ECX := -1
  55. ;       если b <=0, то ECX := 0
  56.  
  57. and ecx, 2                      ; команда N6 ветвления IF
  58. ; Сбросить все биты, кроме второго слева, считая от одного
  59. ; Теперь,     если b > 0, то ECX := 2
  60. ;       если b <=0, то ECX := 0
  61.  
  62. dec ecx                     ; команда N7 ветвления IF
  63. ; Уменьшить ECX на единицу
  64. ; Теперь,     если b > 0, то ECX := -1
  65. ;       если b <=0, то ECX := 0
  66.  
  67. add eax, ecx
  68. ; Сложить переменную a с переменной b
  69.  
  70. pop ecx
  71. ; Закрыть кадр стека
  72. retn
  73. _main       endp
  74.  
  75. <b>Листинг 155</b>
  76.  

  Компилятор некоторым образом перемешал команды, относящиеся к условному оператору "?", с командами ветвления "IF-THEN-ELSE" (это было сделано для лучшего спаривания инструкций), однако, если их сравнить, то выяснится - реализации обеих конструкций абсолютно идентичны друг другу!

  Однако с точки зрения языка условный оператор "?" выгодно отличается от ветвления тем, что может непосредственно использоваться в выражениях, например:

Код (Text):
  1.  
  2. main()
  3. {
  4. int a;
  5. printf("Hello, %s\n", (a>0)?"Sailor":"World!");
  6. }
  7.  
  8. <b>Листинг 156</b>
  9.  

  Попробуйте так же компактно реализовать это с помощью ветвлений! Но на самом деле, это удобство лишь внешнее, а компилятор транслирует приведенный пример так:

Код (Text):
  1.  
  2. main()
  3. {
  4. int a;
  5. char *p;
  6. static char s0[]="Sailor";
  7. static char s1[]="World";
  8. if (a>0) p=s0; else p=s1;
  9.  
  10. printf("Hello, %s\n", p);
  11. }
  12.  
  13. <b>Листинг 157</b>
  14.  

  Откомпилируйте оба листинга и дизассемблируйте полученные файлы, - они должны быть идентичны. Таким образом, при декомпиляции Си/Си++ программ в общем случае невозможно сказать использовалось ли в них ветвление или условный оператор, однако, все же есть некоторые зацепки, помогающие восстановить истинный вид исходного текста в некоторых частных случаях.

  Например, маловероятно, чтобы программист строил свой листинг, как показано в последнем примере. Зачем вводить статические переменные и сложным образом манипулировать с указателем, когда проще поступить использовать условный оператор вместо ветвления?

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

  Идентификация типов. Условные команды - ключ к идентификации типов. Поскольку, анализ результата сравнения знаковых и беззнаковых переменных осуществляется различными группами инструкций, можно уверенно и однозначно отличить signed int от unsigned int.

  Вообще же, идентификация типов - тема отдельного разговора, поэтому не будет отклоняться в сторону, а рассмотрим ее чуточку позже в одноименной главе.

  16 разрядный режим. Одна из неприятных особенностей 16-разрядного режима - ограниченная "дальнобойность" команд условного перехода. Разработчики микропроцессора в стремлении добиться высокой компактности кода, отвели на целевой адрес всего один байт, ограничив тем самым длину прыжка интервалом в 255 байт. Это, так называемый, короткий (short) переход, адресуемый относительным знаковым смещением, отсчитываемым от начала следующий за инструкцией перехода командой (см. рис 26). Такая схема адресации ограничивает длину прыжка "вперед" (т.е. "вниз") всего 128 байтами, а "назад" (т.е. "вверх") и того меньше - 127! (Прыжок вперед короче потому, что ему требуется "пересечь" и саму команду перехода). Этих ограничений лишен ближний (near) безусловный переход, адресуемый двумя байтами и действующий в пределах всего сегмента.

Рисунок 26. 0х019 Внутреннее представление короткого (short) перехода

  Короткие переходы усложняют трансляцию ветвлений - ведь не всякий целевой адрес находится в пределах 128 байт! Существует множество путей обойти это ограничение. Наиболее популярен следующий примем: если транслятор видит, что целевой адрес выходит за пределы досягаемости условного перехода, он инвертирует условие срабатывания и совершает короткий (short) переход на метку continue, а на do_it передает управление ближним (near) переходом, действующим в пределах одного сегмента (см. рис. 27)

Рисунок 27. 0х01A Трансляция коротких переходов

  Аналогичным образом можно выкрутиться и в тех ситуациях, когда целевой адрес расположен совсем в другом сегменте - достаточно лишь заменить ближний безусловный переход на дальний. Вот, собственно, и все.

  К великому счастью разработчиков компиляторов и не меньшей радости хакеров, дизассемблирующих программы, в 32-разрядном режиме условный переход "бьет" в пределах всего четырех гигабайтного адресного пространства и все эти проблемы исчезают, как бородавки после сеанса Кашпировского.

  Листинги примеров. А теперь для лучшего уяснения материала, рассмотренного в этой главе, давайте рассмотрим несколько "живых" примеров, откомпилированных различными компиляторами. Начнем с исследования элементарных целочисленных отношений:

Код (Text):
  1.  
  2. #include <stdio.h>
  3.  
  4. main()
  5. {
  6. int a; int b;
  7. if (a<b) printf("a<b");
  8. if (a>b) printf("a>b");
  9. if (a==b) printf("a==b");
  10. if (a!=b) printf("a!=b");
  11. if (a>=b) printf("a>=b");
  12. if (a<=b) printf("a<=b");
  13. }
  14.  
  15. <b>Листинг 158</b>
  16.  

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

Код (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. ; Резервируем память для локальных переменных var_a и var_b
  13.  
  14. mov eax, [ebp+var_a]
  15. ; Загружаем в EAX значение переменной var_a
  16.  
  17. cmp eax, [ebp+var_b]
  18. ; Сравниваем значение переменной var_a со значением переменной var_b
  19.  
  20. jge short loc_40101B
  21. ; Если var_a >= var_b то переход на continue иначе - печать строки
  22. ; Обратите внимание, что оригинальный код выглядел так:
  23. ; if (a<b) printf("a<b");
  24. ; Т.е. условие отношения было инвентировано компилятором!
  25. ; Знаковая операция JGE говорит о том, что и сравнивыемые переменные
  26. ; var_a и var_b - так же знаковые
  27.  
  28. ; // ВЕТКА DO_IT
  29. push    offset aAB_4    ; "a<b"
  30. call    _printf
  31. add esp, 4
  32. ; Печать строки "a<b"
  33.  
  34. ; // ВЕТКА CONTINUE
  35. loc_40101B:             ; CODE XREF: main+C j
  36. mov ecx, [ebp+var_a]
  37. ; Загружаем в ECX значение переменной var_a
  38.  
  39. cmp ecx, [ebp+var_b]
  40. ; Сравниваем значение переменной var_a с переменной var_b
  41.  
  42. jle short loc_401030
  43. ; Переход, если var_a <= var_b, иначе - печать строки
  44. ; Следовательно, строка печатается, когда !(var_a <= var_b), или
  45. ; var_a > var_b. Тогда исходный код программы должен выглядеть так:
  46. ; if (a>b) printf("a>b");
  47.  
  48. push    offset aAB_3    ; "a>b"
  49. call    _printf
  50. add esp, 4
  51. ;
  52.  
  53. loc_401030:             ; CODE XREF: main+21 j
  54. mov edx, [ebp+var_a]
  55. ; Загружаем в EDX значение переменной var_a
  56.  
  57. cmp edx, [ebp+var_b]
  58. ; Сравниваем значение переменной var_a с переменной var_b
  59.  
  60. jnz short loc_401045
  61. ; Переход если var_a!=var_b, иначе печать строки
  62. ; Следовательно, оригинальный код программы выглядел так:
  63. ; if (a==b) printf("a==b");
  64.  
  65. push    offset aAB  ; "a==b"
  66. call    _printf
  67. add esp, 4
  68.  
  69. loc_401045:             ; CODE XREF: main+36 j
  70. mov eax, [ebp+var_a]
  71. ; Загружаем в EAX значение переменной var_a
  72.  
  73. cmp eax, [ebp+var_b]
  74. ; Сравниваем значение переменной var_a со значением переменной var_b
  75.  
  76. jz  short loc_40105A
  77. ; Переход, если var_a==var_b, иначе - печать строки.
  78. ; Следовательно, оригинальный код программы выглядел так:
  79. ; if (a!==b) printf("a!=b");
  80.  
  81. push    offset aAB_0    ; "a!=b"
  82. call    _printf
  83. add esp, 4
  84.  
  85. loc_40105A:             ; CODE XREF: main+4B j
  86. mov ecx, [ebp+var_a]
  87. ; Загружаем в ECX значение переменной var_a
  88.  
  89. cmp ecx, [ebp+var_b]
  90. ; Сравниваем значение переменной var_a с переменной var_b
  91.  
  92. jl  short loc_40106F
  93. ; Переход, если var_a < var_b, иначе - печать строки
  94. ; Следовательно, оригинальный код программы выглядел так:
  95. ; if (a>=b) printf("a>=b");
  96.  
  97. push    offset aAB_1    ; "a>=b"
  98. call    _printf
  99. add esp, 4
  100.  
  101. loc_40106F:             ; CODE XREF: main+60 j
  102. mov edx, [ebp+var_a]
  103. ; Загружаем в EDX значение переменной var_a
  104.  
  105. cmp edx, [ebp+var_b]
  106. ; Сравниваем значение переменной var_a с переменной var_b
  107.  
  108. jg  short loc_401084
  109. ; Переход если var_a>var_b, иначе печать строки
  110. ; Следовательно, оригинальный код программы выглядел так:
  111. ; if (a<=b) printf("a<=b");
  112.  
  113. push    offset aAB_2    ; "a<=b"
  114. call    _printf
  115. add esp, 4
  116.  
  117. loc_401084:             ; CODE XREF: main+75 j
  118. mov esp, ebp
  119. pop ebp
  120. ; Закрываем кадр стека
  121.  
  122. retn
  123. main        endp
  124.  
  125. <b>Листинг 159</b>
  126.  

  А теперь сравним этот, 32-разрядный код, с 16-разрядным кодом, сгенерированном компилятором Microsoft C++ 7.0 (ниже, для экономии места приведен лишь фрагмент):

Код (Text):
  1.  
  2. mov ax, [bp+var_a]
  3. ; Загрузить в AX значение переменной var_a
  4.  
  5. cmp [bp+var_b], ax
  6. ; Сравнить значение переменной var_a со значением переменной var_b
  7.  
  8. jl  loc_10046
  9. ; Переход на код печати строки, если var_a < var_b
  10.  
  11. jmp loc_10050
  12. ; Безусловный переход на continue
  13. ; Смотрите! Компилятор, не будучи уверен, что "дальнобойности" короткого
  14. ; условного перехода хватит для достижения метки continue, вместо этого
  15. ; прыгнул на метку do_it, расположенную неподалеку - в гарантированной
  16. ; досягаемости, а передачу управления на continue взял на себя
  17. ; безусловный переход
  18. ; Таким образом, инверсия истинности условия сравнения имело место дважды
  19. ; первый раз при трансляции условия отношения, второй раз - при генерации
  20. ; машинного кода. А NOT на NOT можно сократить!
  21. ; Следовательно, оригинальный код выглядел так:
  22. ; if (a<b) printf("a<b");
  23.  
  24. loc_10046:              ; CODE XREF: _main+11 j
  25. mov ax, offset aAB  ; "a<b"
  26. push    ax
  27. call    _printf
  28. add sp, 2
  29.  
  30. loc_10050:              ; CODE XREF: _main+13 j
  31. ; // прочий код
  32.  
  33. <b>Листинг 160</b>
  34.  

  А теперь заменим тип сравниваемых переменных с int на float и посмотрим, как это повлияет на сгенерированный код. Результат компиляции Microsoft Visual C++ должен выглядеть так (ниже приведен лишь фрагмент):

Код (Text):
  1.  
  2. fld [ebp+var_a]
  3. ; Загрузка значения вещественной переменной var_a на вершину стека сопроцессора
  4.  
  5. fcomp   [ebp+var_b]
  6. ; Сравнение значение переменной var_a с переменной var_b
  7. ; с сохранением результата сравнения во флагах сопроцессора
  8.  
  9. fnstsw  ax
  10. ; Скопировать регистр флагов сопроцессора в регистр AX
  11.  
  12. test    ah, 1
  13. ; Нулевой бит регистра AH установлен?
  14. ; Соответственно: восьмой бит регистра флагов сопроцессора установлен?
  15. ; А что у нас храниться в восьмом бите?
  16. ; Ага, восьмой бит содержит флаг переноса.
  17.  
  18. jz  short loc_20
  19. ; Переход, если флаг переноса сброшен, т.е. это равносильно конструкции jnc
  20. ; при сравнении целочисленных значений. Смотрим по таблице 16 - синоним jnc
  21. ; команда jnb.
  22. ; Следовательно, оригинальный код выглядел так:
  23. ; if (a<b) printf("a<b");
  24.  
  25. push    offset $SG339   ; "a<b"
  26. call    _printf
  27. add esp, 4
  28. loc_20:                 ; CODE XREF: _main+11 j
  29.  
  30. <b>Листинг 161</b>
  31.  

  Гораздо нагляднее код, сгенерированный компилятором Borland C++ или WATCOM C. Смотрите:

Код (Text):
  1.  
  2. fld [ebp+var_a]
  3. ; Загрузка значения вещественной переменной var_a на вершину стека сопроцессора
  4.  
  5. fcomp   [ebp+var_b]
  6. ; Сравнение значение переменной var_a с переменной var_b
  7. ; с сохранением результата сравнения во флагах сопроцессора
  8.  
  9. fnstsw  ax
  10. ; Скопировать регистр флагов сопроцессора в регистр AX
  11.  
  12. sahf
  13. ; Скопировать соответствующие биты регистра AH во флаги основного процессора
  14.  
  15. jnb short loc_1003C
  16. ; Переход, если !(a<b), иначе печать строки printf("a<b")
  17. ; Теперь, не копаясь ни в каких справочных таблицах, можно восстановить
  18. ; оригинальный код:
  19. ; if (a<b) printf("a<b");
  20.  
  21. push    offset unk_100B0 ; format
  22. call    _printf
  23. pop ecx
  24. loc_1003C:              ; CODE XREF: _main+F j
  25.  
  26. <b>Листинг 162</b>
  27.  

  Теперь, "насобачившись" на идентификации элементарных условий, перейдем к вещам по настоящему сложным. Рассмотрим следующий пример:

Код (Text):
  1.  
  2. #include <stdio.h>
  3.  
  4. main()
  5. {
  6. unsigned int a; unsigned int b; int c; int d;
  7. if (d) printf("TRUE"); else if (((a>b) && (a!=0)) || ((a==c) && (c!=0))) printf("OK\n");
  8. if (c==d) printf("+++\n");
  9. }
  10.  
  11. <b>Листинг 163</b>
  12.  

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

Код (Text):
  1.  
  2. _main       proc near
  3.  
  4. var_d       = dword ptr -10h
  5. var_C       = dword ptr -0Ch
  6. var_b       = dword ptr -8
  7. var_a       = dword ptr -4
  8.  
  9. push    ebp
  10. mov ebp, esp
  11. ; Открытие кадра стека
  12.  
  13. sub esp, 10h
  14. ; Резервирование места для локальный переменных
  15.  
  16. cmp [ebp+var_d], 0
  17. ; Сравнение значение переменной var_d с нулем
  18.  
  19. jz  short loc_1B
  20. ; Если переменная var_d равна нулю, переход к метке loc_1B, иначе
  21. ; печать строки TRUE. Схематически это можно изобразить так:
  22. ;                       var_d == 0
  23. ;                     /            \
  24. ;                  loc_1B          printf("TRUE");
  25.  
  26. push    offset $SG341   ; "TRUE"
  27. call    _printf
  28. add esp, 4
  29. jmp short loc_44
  30. ; "Ага", говорим мы голосом Пяточка, искушающего Кенгу!
  31. ; Вносим этот условный переход в наше дерево
  32. ;
  33. ;                       var_d == 0
  34. ;                     /            \
  35. ;                  loc_1B          printf("TRUE");
  36. ;                                      |
  37. ;                                     loc_44
  38.  
  39. loc_1B:                 ; CODE XREF: _main+A j
  40. mov eax, [ebp+var_a]
  41. ; Загружаем в EAX значение переменной var_a
  42.  
  43. cmp eax, [ebp+var_b]
  44. ; Сравниваем переменную var_a с переменной var_b
  45.  
  46. jbe short loc_29
  47. ; Если var_a меньше или равна переменной var_b, то переход на loc_29
  48. ; Прививаем новое гнездо к нашему дереву, попутно обращая внимание не то, что
  49. ; var_a и var_b - беззнаковые переменные!
  50. ;
  51. ;                       var_d == 0
  52. ;                     /            \
  53. ;                  loc_1B          printf("TRUE");
  54. ;                     |                 |
  55. ;              var_a  <= var_b        loc_44
  56. ;             /              \
  57. ;         continue           loc_29
  58.  
  59. cmp [ebp+var_a], 0
  60. ; Сравниваем значение переменной var_a с нулем
  61.  
  62. jnz short loc_37
  63. ; Переход на loc_37, если var_a не равна нулю
  64. ;
  65. ;                       var_d == 0
  66. ;                     /            \
  67. ;                  loc_1B          printf("TRUE");
  68. ;                     |                 |
  69. ;              var_a  <= var_b        loc_44
  70. ;             /              \
  71. ;         var_a !=0           loc_29
  72. ;        /         \
  73. ;    continue       loc_37
  74.  
  75. loc_29:                 ; CODE XREF: _main+21 j
  76. ; Смотрите - в нашем дереве уже есть метка loc_29! Корректируем его!
  77. ;
  78. ;                       var_d == 0
  79. ;                     /            \
  80. ;                  loc_1B          printf("TRUE");
  81. ;                     |                 |
  82. ;              var_a  <= var_b        loc_44
  83. ;             /              \
  84. ;         var_a !=0           loc_29
  85. ;         /         \          |
  86. ;        |          |          |
  87. ;         \       loc_37       |
  88. ;          \                   |
  89. ;           \------------------+
  90.  
  91. mov ecx, [ebp+var_a]
  92. ; Загружаем в ECX значение переменной var_a
  93.  
  94. cmp ecx, [ebp+var_C]
  95. ; Сравниваем значение переменной var_a с переменной var_C
  96.  
  97. jnz short loc_44
  98. ; переход, если var_a != var_C
  99. ;
  100. ;                       var_d == 0
  101. ;                     /            \
  102. ;                  loc_1B          printf("TRUE");
  103. ;                     |                 |
  104. ;              var_a  <= var_b        loc_44
  105. ;             /              \
  106. ;         var_a !=0           loc_29
  107. ;         /         \          |
  108. ;        |          |          |
  109. ;         \       loc_37       |
  110. ;          \                   |
  111. ;           \------------------+
  112. ;                     |
  113. ;               var_a != var_C
  114. ;              /               \
  115. ;          continue            loc_44
  116.  
  117. cmp [ebp+var_C], 0
  118. ; Сравнение значения переменной var_C с нулем
  119.  
  120. jz  short loc_44
  121. ; Переход на loc_44 если var_C == 0
  122. ;
  123. ;                       var_d == 0
  124. ;                     /            \
  125. ;                  loc_1B          printf("TRUE");
  126. ;                     |                 |
  127. ;              var_a  <= var_b        loc_44
  128. ;             /              \          |
  129. ;         var_a !=0           loc_29    |
  130. ;         /         \          |        |
  131. ;        |          |          |        |
  132. ;         \       loc_37       |        |
  133. ;          \                   |        |
  134. ;           \------------------+        |
  135. ;                     |                 |
  136. ;               var_a != var_C          |
  137. ;              /               \       /
  138. ;         var_C == 0            |     /
  139. ;        /          \            |   /
  140. ;     continue       \-----------+--/
  141. ;                                |
  142. ;                               loc_44
  143.  
  144. loc_37:                 ; CODE XREF: _main+27 j
  145. ; Смотрим - метка loc_37 уже есть в дереве! Прививаем!
  146. ;
  147. ;                       var_d == 0
  148. ;                     /            \
  149. ;                  loc_1B          printf("TRUE");
  150. ;                     |                 |
  151. ;              var_a  <= var_b       loc_44
  152. ;             /              \       |
  153. ;         var_a !=0           loc_29 |
  154. ;         /         \          |     |
  155. ;        |           \         |     |
  156. ;         \           \-----а | Я--------!
  157. ;          \                   |     |     !
  158. ;           \------------------+    /      !
  159. ;                     |            /       !
  160. ;               var_a != var_C    /        !
  161. ;              /               \ /         !
  162. ;         var_C == 0            |          !
  163. ;        /          \            |         !
  164. ;        !           \-----------+         !
  165. ;        !                       |         !
  166. ;        !                       !         !
  167. ;        \---------------------------------!------!
  168. ;                                !                !
  169. ;                               loc_44         loc_37
  170. ;                                                 |
  171. ;                                              printf("OK");
  172. push    offset $SG346   ; "OK\n"
  173. call    _printf
  174. add esp, 4
  175.  
  176. loc_44:                 ; CODE XREF: _main+19 j _main+2F j ...
  177. ; Смотрите - ветки loc_44 и loc_37 смыкаются!
  178. ;
  179. ;                       var_d == 0
  180. ;                     /            \
  181. ;                  loc_1B          printf("TRUE");
  182. ;                     |                 |
  183. ;              var_a  <= var_b        loc_44
  184. ;             /              \        |
  185. ;         var_a !=0           loc_29  |
  186. ;         /         \          |      |
  187. ;        |           \         |      |
  188. ;         \           \-----а | Я--------!
  189. ;          \                   |      |    !
  190. ;           \------------------+     /     !
  191. ;                     |             /      !
  192. ;               var_a != var_C     /       !
  193. ;              /               \  /        !
  194. ;         var_C == 0            \|         !
  195. ;        /          \            |         !
  196. ;        !           \-----------+         !
  197. ;        !                       |         !
  198. ;        !                       !         !
  199. ;        \---------------------------------!------!
  200. ;                                !                !
  201. ;                               loc_44         loc_37
  202. ;                                 |               |
  203. ;                                 |            printf("OK");
  204. ;                                 |                |
  205. ;                                  \-------+-------/
  206. ;                                          |
  207. ;                                          |
  208.  
  209. mov edx, [ebp+var_C]
  210. ; Загружаем в EDX значение переменной var_C
  211.  
  212. cmp edx, [ebp+var_d]
  213. ; Сравниваем значение var_C со значением переменной var_D
  214.  
  215. jnz short loc_59
  216. ; Переход, если var_C != var_D
  217.  
  218. push    offset $SG348   ; "+++\n"
  219. call    _printf
  220. add esp, 4
  221. ;                       var_d == 0
  222. ;                     /            \
  223. ;                  loc_1B          printf("TRUE");
  224. ;                     |                 |
  225. ;              var_a  <= var_b        loc_44
  226. ;             /              \        |
  227. ;         var_a !=0           loc_29  |
  228. ;         /         \          |      |
  229. ;        |           \         |      |
  230. ;         \           \-----а | Я--------!
  231. ;          \                   |      |    !
  232. ;           \------------------+     /     !
  233. ;                     |             /      !
  234. ;               var_a != var_C     /       !
  235. ;              /               \  /        !
  236. ;         var_C == 0             |         !
  237. ;        /          \            |         !
  238. ;        !           \-----------+         !
  239. ;        !                       |         !
  240. ;        !                       !         !
  241. ;        \---------------------------------!------!
  242. ;                                !                !
  243. ;                               loc_44         loc_37
  244. ;                                 |               |
  245. ;                                 |            printf("OK");
  246. ;                                 |                |
  247. ;                                  \-------+-------/
  248. ;                                          |
  249. ;                                          |
  250. ;                                      var_C != var_D
  251. ;                                     /              \
  252. ;                            printf("+++")            !
  253. ;                                                   конец
  254.  
  255.  
  256. loc_59:                 ; CODE XREF: _main+4A j
  257.         mov esp, ebp
  258.         pop ebp
  259.         retn
  260. _main       endp
  261.  
  262. <b>Листинг 164</b>
  263.  

  В итоге вырастает огромное разлапистое дерево, в котором на первый взгляд просто невозможно разобраться. Но, как говориться, глаза страшатся, а руки делают. Первым делом, оптимизируем дерево: избавимся от "перекрученных" ветвей, инвертировав условие в гнезде, и выкинем все метки - теперь, когда скелет дерева построен, они уже не нужны. Если все сделать правильно, дерево должен выглядеть так:

Рисунок 28. 0х01В Логическое древо

  Сразу же бросается в глаза, что все пути проходят точку "Z", сплетающую все ветви воедино. Это значит, что мы имеем дело с двумя самостоятельными деревьями, представленными собственными конструкциями "IF". Замечательно! Такой поворот событий весьма упрощает анализ - раз деревья независимые, то и анализироваться они могут независимо! Итак, начинаем с верхнего из них...

  От гнезда "var_d !=0" отходят две ветки - правая ведет к "printf("OK")" и далее к завершению конструкции "IF - THEN [ELSE]", а левая, прежде чем выйти к точке "Z", минует целое полчище гнезд. В переводе на русский язык ситуация выглядит так: "если переменная var_d не равна нулю, то печатаем "OK" и сваливаем, иначе выполняем дополнительные проверки". Проще говоря: "IF (var_d !=0) THEN printf("OK") ELSE Е". Т.е. левая ветка гнезда (var_d != 0) есть ветка "ELSE". Изучим ее?

  От гнезда (var_a <= var_b) к узлу "printf("OK")" ведут два пути: !(var_a <= var_b) а !(var_a ==0 ) и !(var_a != var_c) а !(var_c == 0). Где есть альтернатива - там всегда есть OR. Т.е. либо первый путь, либо второй. В то же время, узлы обоих путей последовательно связаны друг с другом, - значит, они объедены операций AND. Таким, образом, эта ветка должна выглядеть так: "IF (( var_a > var_b) && (var_0 != 0)) || (var_a == var_c) && (var_c != 0)) printf("OK")", прививаем "ELSE" к первому IF и получаем. "IF (var_d !=0) THEN printf("OK") ELSE IF(( var_a > var_b) && (var_0 != 0)) || (var_a == var_c) && (var_c != 0)) printf("OK")"

  Ну, а разбор второго дерева вообще тривиален: "IF (var_c==var_d) printf("+++")". Итак, исходный текст дизассемблируемой программы выглядел так:

Код (Text):
  1.  
  2. u_int a; u_int b; ?_int c; ?_int d;
  3. if (d) printf("TRUE");
  4. else
  5. if (((a>b) && (a!=0)) || ((a==c) && (c!=0))) printf("OK\n");
  6.  
  7. if (c==d) printf("+++\n");
  8.  
  9. <b>Листинг 165</b>
  10.  

  Тип переменных a и b мы определили как unsigned int, т.к. они результат сравнения анализировался беззнаковой условной командой - jnb. А вот тип переменных c и d, увы, определить так и не удалось. Однако это не умоляет значимости того факта, что мы смогли ретранслировать сложное условие, в котором без деревьев было бы немудрено и запутаться...

  - Больше всего следует опасаться идей, которые переходят в дела.
Френк Херберт "Мессия дюны"

  Оптимизация ветвлений: Какое коварство - под флагом оптимизации сделать каждую строчку кода головоломкой. Тьфу-ты, тут ящика пива не хватит, чтобы с этим справиться (а с этим лучше справляться вообще без пива - на трезвую голову). Итак, предположим, встретился вам код следующего содержания. На всякий случай, чтобы избавить вас от копания по справочникам (хотя, покопаться в них лишний раз - только на пользу) отмечу, что команда SETGE устанавливает выходной операнд в 1, если флаги состояния SF и OF равны (т.е. SF==OF). Иначе выходной операнд устанавливается в ноль.

Код (Text):
  1.  
  2. mov eax, [var_A]
  3. xor ecx,ecx
  4. cmp eax, 0x666
  5. setge cl
  6. dec ecx
  7. and ecx, 0xFFFFFC00
  8. add ecx, 0x300
  9. mov [var_zzz],ecx
  10.  
  11. <b>Листинг 166</b>
  12.  

  На первый взгляд этот фрагмент заимствован из какого-то хитро-запутанного защитного механизма, но нет. Перед вами результат компиляции следующего тривиального выражения: if (a<0x666) zzz=0x200 else zzz=0x300, которое в не оптимизированном виде выглядит так:

Код (Text):
  1.  
  2. mov eax,[var_A]
  3. cmp eax,0x666
  4. jge Label_1
  5. mov ecx, 0x100
  6. jmp lable_2
  7. Label_1:
  8. mov ecx, 0x300
  9. Lable_2:
  10. mov [var_zzz],ecx
  11.  
  12. <b>Листинг 167</b>
  13.  

  Чем же компилятору не понравился такой вариант? Между прочим, он даже короче. Короче-то, он короче, но содержит ветвления, - т.е. внеплановые изменения нормального хода выполнения программы. А ветвления отрицательно сказываются на производительности, хотя бы уже потому, что они приводят к очистке конвейера. Конвейер же в современных процессорах очень длинный и быстро его не заполнишь... Поэтому, избавление от ветвлений путем хитроумных математических вычислений вполне оправдано и горячо приветствуется. Попутно это усложняет анализ программы, защищая ее от всех посторонних личностей типа хакеров (т.е. нас с вами).

  Впрочем, если хорошенько подумать.... Начнем пошагово исполнять программу, мысленно комментируя каждую строчку.

Код (Text):
  1.  
  2. mov eax, [var_A]
  3. ; eax == var_A
  4.  
  5. xor ecx,ecx
  6. ; ecx=0;
  7.  
  8. cmp eax, 0x666
  9. ; if eax<0x666 { SF=1; OF=0} else {SF=0; OF=0}
  10.  
  11. setge cl
  12. ; if eax<0x666 (т.е. SF==1, OF ==0) cl=0 else cl=1
  13.  
  14. dec ecx
  15. ; if eax<0x666 ecx=-1 else ecx=0
  16.  
  17. and ecx, 0xFFFFFC00
  18. ; if eax<0x666 (т.е. ecx==-1) ecx=0xFFFFFC00 (-0x400) else ecx=0;
  19.  
  20. add ecx, 0x300
  21. ; if eax<0x666 (т.е. ecx=-0x400) ecx=0x100 else ecx=0x300;
  22.  
  23. mov [esp+0x66],ecx
  24.  
  25. <b>Листинг 168</b>
  26.  

  Получилось! Мы разобрались с этим алгоритмом и успешно реверсировали его! Теперь видно, что это довольно простой пример (в жизни будут нередко попадаться и более сложные). Но основная идея ясна, - если встречаются команда SETxx - держите нос по ветру: пахнет условными переходами! В вырожденных случаях SETxx может быть заменена на SBB (вычитание с заемом). По этому поводу решим вторую задачу:

Код (Text):
  1.  
  2. SUB EBX,EAX
  3. SBB ECX,ECX
  4. AND ECX,EBX
  5. ADD EAX,ECX
  6.  
  7. <b>Листинг 169</b>
  8.  

  Что этот код делает? Какие-то сложные арифметические действия? Посмотрим...

Код (Text):
  1.  
  2. SUB EBX,EAX
  3. ; if (EBX<EAX) SF=1 else SF=0
  4.  
  5. SBB ECX,ECX
  6. ; if (EBX<EAX) ECX=-1 else ECX=0
  7.  
  8. AND ECX,EBX
  9. ; if (EBX<EAX) ECX=EBX else ECX=0
  10.  
  11. ADD EAX,ECX
  12. ; if (EBX<EAX) EAX=EAX+(EBX-EAX) else EAX=EAX
  13.  
  14. <b>Листинг 170</b>
  15.  

  Раскрывая скобки в последнем выражении (мы ведь не забыли, что от EBX отняли EAX?) получаем: if (EBX<EAX) EAX=EBX, - т.е. это классический алгоритм поиск минимума среди двух знаковых чисел. А вот еще один пример:

Код (Text):
  1.  
  2. CMP EAX,1
  3. SBB EAX,EAX
  4. AND ECX,EAX
  5. XOR EAX,-1
  6. AND EAX,EBX
  7. OR  EAX,ECX
  8.  
  9. <b>Листинг 171</b>
  10.  

  Попробуйте решить его сами и только потом загляните в ответ:

Код (Text):
  1.  
  2. CMP EAX,1
  3. ; if (EAX!=0) SF=0 else SF=1
  4.  
  5. SBB EAX,EAX
  6. ; if (EAX!=0) EAX=-1 else EAX=0
  7.  
  8. AND ECX,EAX
  9. ; if (EAX!=0) ECX=ECX else ECX=0
  10.  
  11. XOR EAX,-1
  12. ; if (EAX!=0) EAX=0 else EAX=-1
  13.  
  14. AND EAX,EBX
  15. ; if (EAX!=0) EAX=0 else EAX=EBX
  16.  
  17. OR  EAX,ECX
  18. ; if (EAX!=0) EAX=ECX else EAX=EBX
  19.  
  20. <b>Листинг 172</b>
  21.  

  Да... после таких упражнений тов. Буль будет во сне сниться! Но... таковы уж издержки цивилизации. К слову сказать, подавляющее большинство компиляторов достаточно лояльно относятся к условным переходам и не стремятся к их тотальному изгнанию. Так что... особо напрягаться при анализе оптимизированного кода не приходится (правда, к ручной оптимизации это не относится - профессиональные разработчики выкидывают переходы первую очередь). © Крис Касперски


0 2.329
archive

archive
New Member

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