Дизассемблиры

Автор: Пользователь скрыл имя, 19 Октября 2011 в 15:00, реферат

Краткое описание

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

Оглавление

Введение.
Структура природы человека.
Биологическое и социальное в человеке.
Роль биологических и географических факторов в формировании социальной жизни.
Социальная жизнь.

Файлы: 1 файл

kyrsovaya0002.doc

— 391.00 Кб (Скачать)

add esp, 4

jmp short loc_40107A

; ^ break 

loc_40106D:    ; CODE XREF: main+24 j main+2F j

; // printf("Default")

push offset aDefault ; "Default"

call _printf

add esp, 4 

loc_40107A:    ; CODE XREF: main+3E j main+4D j ...

; // КОНЕЦ SWITCH

mov esp, ebp

pop ebp

; Закрываем  кадр стека

retn

main  endp 

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

 

Рисунок 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 ebp

mov 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 _printf

pop ecx

jmp short loc_4010D1 

loc_40109F:    ; CODE XREF: _main+8 j

; // printf("a==1");

push offset aA1 ; "a == 1"

call _printf

pop ecx

jmp short loc_4010D1 

loc_4010AC:    ; CODE XREF: _main+B j

; // printf("a==2");

push offset aA2 ; "a == 2"

call _printf

pop ecx

jmp short loc_4010D1 

loc_4010B9:    ; CODE XREF: _main+12 j

; // printf("a==666");

push offset aA666h ; "a == 666h"

call _printf

pop ecx

jmp short loc_4010D1 

loc_4010C6:    ; CODE XREF: _main+14 j

; // printf("Default");

push offset aDefault ; "Default"

call _printf

pop ecx 

loc_4010D1:    ; CODE XREF: _main+21 j _main+2E j ...

xor eax, eax

pop ebp

retn

_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-обработчикам размером в два-три экрана, включая в них помимо всего прочего и циклы, и ветвления, и даже вложенные операторы множественно выбора! В результате правая часть "косички" превращается в непроходимый таежный лес. Но даже если и так - левая часть "косички", все равно останется достаточно простой и легко распознаваемой!

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

main_  proc near  ; CODE XREF: __CMain+40 p

push 8

call __CHK

; Проверка  стека на переполнение 

cmp eax, 1

; Сравнение  регистровой переменной EAX, содержащей  в себе переменную a

; со  значением 1 

jb short loc_41002F

; Если EAX == 0, то переход к ветви с  дополнительными проверками 

jbe short loc_41003A

; Если EAX == 1 (т.е. условие bellow уже обработано  выше), то переход

; к ветке  вызова printf("a == 1"); 

cmp eax, 2

; Сравнение  EAX со значением 2 

jbe short loc_410041

; Если EAX == 2 (условие EAX <2 уже было обработано  выше),  то переход

; к ветке  вызова printf("a == 2"); 

cmp eax, 666h

; Сравнение  EAX со значением 0x666 

jz short loc_410048

; Если EAX == 0x666, то переход к ветке вызова printf("a == 666h"); 

jmp short loc_41004F

; Ни одно из условий не подошло - переход к ветке "Default" 
 

loc_41002F:    ; CODE XREF: main_+D j

; // printf("a == 0");

test eax, eax

jnz short loc_41004F

; Совершенно  непонятно - зачем здесь дополнительная  проверка?!

; Это  ляп компилятора - она ни к  чему! 

push offset aA0 ; "A == 0"

; Здесь WATCOM сумел обойтись всего одним вызовом printf!

; Обработчики  case всего лишь передают ей нужный аргумент!

; Вот  это действительно - оптимизация!

jmp short loc_410054 

loc_41003A:    ; CODE XREF: main_+F j

; // printf("a == 1");

push offset aA1 ; "A == 1"

jmp short loc_410054 

loc_410041:    ; CODE XREF: main_+14 j

; // printf("a == 2");

push offset aA2 ; "A == 2"

jmp short loc_410054 

loc_410048:    ; CODE XREF: main_+1B j

; // printf("a == 666h");

push offset aA666h ; "A == 666h"

jmp short loc_410054 

loc_41004F:    ; CODE XREF: main_+1D j main_+21 j

Информация о работе Дизассемблиры