Автор: Пользователь скрыл имя, 19 Октября 2011 в 15:00, реферат
Дизассемблирование (От англ. disassemble - разбирать, демонтировать) – это процесс или способ получения исходного текста программы на ассемблере из программы в машинных кодах. Полезен при определении степени оптимальности транслятора и при генерации кодов собственной программы. Позволяет понять алгоритм или метод построения программ, у которых отсутствуют исходные тексты. Существуют специальные программы дизассемблеры, которые выполняют этот процесс.
Введение.
Структура природы человека.
Биологическое и социальное в человеке.
Роль биологических и географических факторов в формировании социальной жизни.
Социальная жизнь.
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 Усечение логического
дерева
Увы,
такой простой прием
; 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
много больше единицы,
; значением будет ноль
; Таким
образом, данная конструкция -
просто завуалированная
; равенство
нулю!
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