Автор: Пользователь скрыл имя, 19 Октября 2011 в 15:00, реферат
Дизассемблирование (От англ. disassemble - разбирать, демонтировать) – это процесс или способ получения исходного текста программы на ассемблере из программы в машинных кодах. Полезен при определении степени оптимальности транслятора и при генерации кодов собственной программы. Позволяет понять алгоритм или метод построения программ, у которых отсутствуют исходные тексты. Существуют специальные программы дизассемблеры, которые выполняют этот процесс.
Введение.
Структура природы человека.
Биологическое и социальное в человеке.
Роль биологических и географических факторов в формировании социальной жизни.
Социальная жизнь.
push offset asc_406030 ; "%x\n"
call _printf
add esp, 8
; printf("%x\n",
var_c)
mov edx, [ebp+var_c]
; Загружаем
в EDX значение переменной var_c
sub edx, 0Ah
; Вычитаем
из var_c значение 0xA, записывая результат
в EDX
mov [ebp+var_c], edx
; Обновляем var_c
; var_c = var_c
- 0xA
mov eax, [ebp+var_c]
push eax
push offset asc_406034 ; "%x\n"
call _printf
add esp, 8
; printf("%x\n",var_c)
mov esp, ebp
pop ebp
; Закрываем кадр стека
retn
main endp
Теперь рассмотрим оптимизированный
вариант того же примера:
main proc near ; CODE XREF: start+AF p
push ecx
; Резервируем
место для локальной переменной var_a
mov eax, [esp+var_a]
; Загружаем
в EAX значение локальной переменной
var_a
push esi
; Резервируем
место для локальной переменной
var_b
mov esi, [esp+var_b]
; Загружаем
в ESI значение переменной var_b
sub esi, eax
; Вычитаем
из var_a значение var_b, записывая результат
в ESI
push esi
push offset asc_406030 ; "%x\n"
call _printf
; printf("%x\n",
var_a - var_b)
add esi, 0FFFFFFF6h
; Добавляем к ESI (разности var_a и var_b) значение 0хFFFFFFF6
; Поскольку, 0xFFFFFFF6 == -0xA, данная строка кода выглядит так:
; ESI = (var_a
- var_b) + (- 0xA) = (var_a - var_b) - 0xA
push esi
push offset asc_406034 ; "%x\n"
call _printf
add esp, 10h
; printf("%x\n",
var_a - var_b - 0xA)
pop esi
pop ecx
; Закрываем
кадр стека
retn
main endp
Компиляторы
(Borland, WATCOM) генерируют практически идентичный
код.
§1.3
Идентификация оператора "/"
В
общем случае оператор "/" транслируется
либо в машинную инструкцию "DIV"
(беззнаковое целочисленное
Несколько сложнее происходит быстрое деление знаковых чисел. Совершенно недостаточно выполнить арифметический сдвиг вправо (команда арифметического сдвига вправо SAR заполняет старшие биты с учетом знака числа), ведь если модуль делимого меньше модуля делителя, то арифметический сдвиг вправо сбросит все значащие биты в "битовую корзину", в результате чего получиться 0xFFFFFFFF, т.е. -1, в то время как правильный ответ - ноль. Однако деление знаковых чисел арифметическим сдвигом вправо дает округление в большую сторону. Для округления знаковых чисел в меньшую сторону необходимо перед выполнением сдвига добавить к делимому число 2^N- 1, где N - количество битов, на которые сдвигается число при делении. Легко видеть, что это приводит к увеличению всех сдвигаемых битов на единицу и переносу в старший разряд, если хотя бы один из них не равен нулю.
Следует
отметить: деление очень медленная
операция, гораздо более медленная
чем умножение (выполнение DIV может
занять свыше 40 тактов, в то время
как MUL обычно укладываться в 4), поэтому,
продвинутые оптимизирующие компиляторы
заменяют деление умножением. Существует
множество формул подобных преобразований,
одной из самых популярных является: a/b
= 2^N/b * a/2^N', где N' - разрядность числа. Следовательно,
грань между умножением и делением очень
тонка, а их идентификация является довольной
сложной процедурой. Рассмотрим следующий
пример:
main()
{
int a;
printf("%x
%x\n",a / 32, a / 10);
}
Идентификация
оператора "/"
Результат
компиляции компилятором Microsoft Visual C++ с
настройками по умолчанию должен выглядеть
так:
main proc near ;
CODE XREF: start+AF p
var_a = dword ptr
-4
push ebp
mov ebp, esp
; Открываем
кадр стека
push ecx
; Резервируем
память для локальной
mov eax, [ebp+var_a]
; Копируем
в EAX значение переменной var_a
cdq
; Расширяем
EAX до четверного слова EDX:EAX
mov ecx, 0Ah
; Заносим
в ECX значение 0xA
idiv ecx
; Делим (учитывая знак) EDX:EAX на 0xA, занося частное в EAX
; EAX = var_a
/ 0xA
push eax
; Передаем
результат вычислений функции
printf
mov eax, [ebp+var_a]
; Загружаем
в EAX значение var_a
cdq
; Расширяем
EAX до четверного слова EDX:EAX
and edx, 1Fh
; Выделяем
пять младших бит EDX
add eax, edx
; Складываем
знак числа для выполнения
округления отрицательных
; в меньшую
сторону
sar eax, 5
; Арифметический сдвиг вправо на 5 позиций
; эквивалентен делению числа на 2^5 = 32
; Таким
образом, последние четыре
; EAX = var_a / 32
; Обратите внимание: даже при выключенном режиме оптимизации компилятор
; оптимизировал
деление
push eax
push offset aXX ; "%x %x\n"
call _printf
add esp, 0Ch
; printf("%x
%x\n", var_a / 0xA, var_a / 32)
mov esp, ebp
pop ebp
; Закрываем
кадр стека
retn
main endp
Теперь,
рассмотрим оптимизированный вариант
того же примера:
main proc near ; CODE XREF: start+AF p
push ecx
; Резервируем
память для локальной
mov ecx, [esp+var_a]
; Загружаем
в ECX значение переменной var_a
mov eax, 66666667h
; В исходном
коде ничего подобного не было!
imul ecx
; Умножаем это число на переменную var_a
; Обратите
внимание: именно умножаем, а не
делим.
sar edx, 2
; Выполняем
арифметический сдвиг всех
; в первом приближении эквивалентно его делению на 4
; Однако ведь в EDX находятся старшее двойное слово результата умножения!
; Поэтому,
три предыдущих команды
; EDX = (66666667h * var_a) >> (32 + 2) = (66666667h * var_a) / 0x400000000
;
; Теперь немного упростим код:
; (66666667h * var_a) / 0x400000000 = var_a * 66666667h / 0x400000000 =
; = var_a *
0,
; Заменяя
по всем правилам математики
умножение на деление и
; выполняя округление до меньшего целого получаем:
; var_a * 0,1000000000 = var_a * (1/0,1000000000) = var_a/10
;
; От такого преобразования код стал намного понятнее!
; Тогда возникает вопрос можно ли распознать такую ситуацию в чужой
; программе, исходный текст которой
; неизвестен? Можно - если встречается умножение, а следом за ним
; сдвиг
вправо, обозначающий деление, сократив
код, по методике показанной выше!
mov eax, edx
; Копируем
полученное частное в EAX
shr eax, 1Fh
; Сдвигаем
на 31 позицию вправо
add edx, eax
; Складываем: EDX = EDX + (EDX >> 31)
; Нетрудно понять, что после сдвига EDX на 31 бит вправо
; в нем останется лишь знаковый бит числа
; Тогда - если число отрицательно, добавляем к результату деления один,
; округляя его в меньшую сторону. Таким образом, весь этот хитрый код
; обозначает ни что иное как тривиальную операцию знакового деления:
; EDX = var_a / 10
; Конечно, программа становится очень громоздкой,
; зато весь этот код выполняется всего лишь за 9 тактов,
; в то время как в не оптимизированном варианте за 28!
; /* Измерения проводились на процессоре CLERION с ядром P6, на других
; процессорах
количество тактов может
; Т.е.
оптимизация дала более чем
трехкратный выигрыш!
mov eax, ecx
; Теперь нужно вспомнить: что находится в ECX.
; В ECX
последний раз разгружалось значение
переменной var_a
push edx
; Передаем
функции printf результат деления var_a на 10
cdq
; Расширяем
EAX (var_a) до четверного слова EDX:EAX
and edx, 1Fh
; Выбираем
младшие 5 бит регистра EDX, содержащие
знак var_a
add eax, edx
; Округляем
до меньшего
sar eax, 5
; Арифметический
сдвиг на 5 эквивалентен делению var_a
на 32
push eax
push offset aXX ; "%x %x\n"
call _printf