Дизассемблирование

Построив логическое дерево, получаем следующую картину (см. рис. 2). При ее изучении бросается в глаза, во-первых, условие "a >2", которого не было в исходной программе, а во-вторых, изменение порядка обработки case. В то же время, вызовы функций printf следуют один за другим строго согласно их объявлению.

Пример трансляция оператора switch компилятором Microsoft Visual C

Рисунок 2 Пример трансляция оператора switch компилятором Microsoft Visual C

Назначение гнезда (a > 2) объясняется очень просто – последовательная обработка всех операторов case крайне непроизводительная. Хорошо, если их всего четыре-пять штук, а если программист напишет в switch сотню - другую case? Вот компилятор и "утрамбовывает" дерево, уменьшая его высоту. Вместо одной ветви, изображенной на рис. 2, транслятор построил две, поместив в левую только числа не большие двух, а в правую – все остальные. Благодаря этому, ветвь "666h" из конца дерева была перенесена в его начало. Данный метод оптимизации поиска значений называют "методом вилки".

Изменение порядка сравнений – право компилятора. Стандарт ничего об этот не говорит и каждая реализация вольна поступать так, как ей это заблагорассудится. Другое дело – case-обработчики (т.е. тот код, которому case передает управление в случае истинности отношения). Они обязаны располагаться так, как были объявлены в программе, т.к. при отсутствии закрывающего оператора break они должны выполняться строго в порядке, замышленном программистом, хотя эта возможность языка Си используется крайне редко.

Таким образом, идентификация оператора switch не сильно усложняется: если после уничтожения узлового гнезда и прививки правой ветки к левой (или наоборот) получается эквивалентное дерево, и это дерево образует характерную "косичку" – здесь имеет дело оператор множественного выбора или его аналог.

Весь вопрос в том: правомерно ли удалять гнездо, и не нарушит ли эта операция структуры дерева? Смотрим – на левой ветке узлового гнезда расположены гнезда (a == 2), (a == 0) и (a == 1), а на левом – (a==0x666) Очевидно, если a == 0x666, то a != 0 и a != 1! Следовательно, прививка правой ветки к левой вполне безопасна и после такого преобразования дерево принимает вид типичный для конструкции switch (см. рис. 3)

Усечение логического дерева

Рисунок 3 Усечение логического дерева

Увы, такой простой прием идентификации срабатывает не всегда! Иные компиляторы могут сделать еще хуже! Если откомпилировать пример компилятором Borland C++ 5.0, то код будет выглядеть так:

; int cdecl main(int argc,const char **argv,const char *envp)_main proc near ; DATA XREF: DATA:00407044 o push ebpmov ebp, esp; Открываем кадр стека; Компилятор помещает нашу переменную a в регистр EAX; Поскольку она не была инициализирована, то заметить этот факт; не так-то легко! sub eax, 1; Уменьшает EAX на единицу! ; Никакого вычитания в программе не было! jb short loc_401092; Если EAX < 1, то переход на вызов printf("a == 0"); (CMP та же команда SUB, только не изменяющая операндов?); Этот код сгенерирован в результате трансляции; ветки CASE 0: printf("a == 0");; Внимание, какие значения может принимать EAX, чтобы; удовлетворять условию этого отношения? На первый взгляд, EAX < 1,; в частости, 0, -1, -2,… СТОП! Ведь jb - это беззнаковая инструкция; сравнения! А -0x1 в беззнаковом виде выглядит как 0xFFFFFFFF; 0xFFFFFFFF много больше единицы, следовательно, единственным подходящим; значением будет ноль; Таким образом, данная конструкция - просто завуалированная проверка EAX на; равенство нулю! jz short loc_40109F; Переход, если установлен флаг нуля; Он будет установлен в том случае, если EAX == 1; И действительно переход идет на вызов printf("a == 1") dec eax; Уменьшаем EAX на единицу jz short loc_4010AC; Переход если установлен флаг нуля, а он будет установлен, когда после; вычитания единицы командой SUB, в EAX останется ровно единица,; т.е. исходное значение EAX должно быть равно двум; И верно - управление передается ветке вызова printf("a == 2")! sub eax, 664h; Отнимаем от EAX число 0x664 jz short loc_4010B9; Переход, если установлен флаг нуля, т.е. после двукратного уменьшения EAX; равен 0x664, следовательно, исходное значение - 0x666 jmp short loc_4010C6; прыгаем на вызов printf("Default"). Значит, это - конец switch loc_401092: ; CODE XREF: _main+6 j; // printf("a==0"); push offset aA0 ; "a == 0"call _printfpop ecxjmp short loc_4010D1 loc_40109F: ; CODE XREF: _main+8 j; // printf("a==1");push offset aA1 ; "a == 1"call _printfpop ecxjmp short loc_4010D1 loc_4010AC: ; CODE XREF: _main+B j; // printf("a==2");push offset aA2 ; "a == 2"call _printfpop ecxjmp short loc_4010D1 loc_4010B9: ; CODE XREF: _main+12 j; // printf("a==666");push offset aA666h ; "a == 666h"call _printfpop ecxjmp short loc_4010D1 loc_4010C6: ; CODE XREF: _main+14 j; // printf("Default");push offset aDefault ; "Default"call _printfpop ecx loc_4010D1: ; CODE XREF: _main+21 j _main+2E j .xor eax, eaxpop ebpretn_main endp

Код, сгенерированный компилятором, модифицирует сравниваемую переменную в процессе сравнения! Оптимизатор посчитал, что DEC EAX короче, чем сравнение с константой, да и работает быстрее. Прямая ретрансляция кода дает конструкцию вроде: "if (a-- == 0) printf("a == 0"); else if (a==0) printf("a == 1"); else if (--a == 0) printf("a == 2"); else if ((a-=0x664)==0) printf("a == 666h); else printf("Default")", - в которой совсем не угадывается оператор switch! Впрочем, угадать его возможно. Где есть длинная цепочка "IF-THEN-ELSE-IF-THEN-ELSE…". Узнать оператор множественного выбора будет еще легче, если изобразить его в виде дерева (см. рис. 4) , характерная "косичка"!

Построение логического дерева с гнездами, модифицирующими саму сравниваемую переменную

Рисунок 4 Построение логического дерева с гнездами, модифицирующими саму сравниваемую переменную

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


Страница: