Автор: Пользователь скрыл имя, 19 Октября 2011 в 15:00, реферат
Дизассемблирование (От англ. disassemble - разбирать, демонтировать) – это процесс или способ получения исходного текста программы на ассемблере из программы в машинных кодах. Полезен при определении степени оптимальности транслятора и при генерации кодов собственной программы. Позволяет понять алгоритм или метод построения программ, у которых отсутствуют исходные тексты. Существуют специальные программы дизассемблеры, которые выполняют этот процесс.
Введение.
Структура природы человека.
Биологическое и социальное в человеке.
Роль биологических и географических факторов в формировании социальной жизни.
Социальная жизнь.
; printf("%x
%x\n", var_a / 10, var_a / 32)
retn
main endp
Однако
такие компиляторы как Borland и WATCOM
не умеют заменять деление более быстрым
умножением для чисел отличных от степени
двойки. В подтверждении тому рассмотрим
результат компиляции того же примера
компилятором Borland C++:
_main proc near ;
DATA XREF: DATA:00407044 o
push ebp
mov ebp, esp
; Открываем
кадр стека
push ebx
; Сохраняем
EBX
mov eax, ecx
; Копируем
в EAX содержимое
mov ebx, 0Ah
; Заносим
в EBX значение 0xA
cdq
; Расширяем
EAX до четверного слова EDX:EAX
idiv ebx
; Делим
ECX на 0xA (примерно 20 тактов)
push eax
; Передаем
полученное значение функции printf
test ecx, ecx
jns short loc_401092
; Если
делимое не отрицательно, то переход
на loc_401092
add ecx, 1Fh
; Если
делимое положительно, то добавляем
к нему 0x1F для округления
loc_401092: ; CODE XREF: _main+11 j
sar ecx, 5
; Сдвигом
на пять позиций вправо делим
число на 32
push ecx
push offset aXX ; "%x %x\n"
call _printf
add esp, 0Ch
; printf("%x
%x\n", var_a / 10, var_a / 32)
xor eax, eax
; Возвращаем
ноль
pop ebx
pop ebp
; Закрываем
кадр стека
retn
_main endp
§1.4
Идентификация оператора "%"
Специальной инструкции для вычисления остатка в наборе команд микропроцессоров серии 80x86 нет, - вместо этого остаток вместе с частным возвращается инструкциями деления DIV, IDIV и FDIVx. Если делитель представляет собой степень двойки (2^N = b), а делимое беззнаковое число, то остаток будет равен N младшим битам делимого числа. Если же делимое - знаковое, необходимо установить все биты, кроме первых N равными знаковому биту для сохранения знака числа. Причем, если N первых битов равно нулю, все биты результата должны быть сброшены независимо от значения знакового бита.
Таким образом, если делимое - беззнаковое число, то выражение a % 2^N транслируется в конструкцию: "AND a, N", в противном случае трансляция становится неоднозначна - компилятор может вставлять явную проверку на равенство нулю с ветвлением, а может использовать хитрые математические алгоритмы, самый популярный из которых выглядит так: DEC x\ OR x, -N\ INC x. Весь фокус в том, что если первые N бит числа x равны нулю, то все биты результата кроме старшего, знакового бита, будут гарантированно равны одному, а OR x, -N принудительно установит в единицу и старший бит, т.е. получится значение, равное, -1. А INC -1 даст ноль! Напротив, если хотя бы один из N младших битов равен одному, заема из старших битов не происходит и INC x возвращает значению первоначальный результат.
Продвинутые оптимизирующие компиляторы могут путем сложных преобразований заменять деление на ряд других, более быстродействующих операций. К сожалению, алгоритмов для быстрого вычисления остатка для всех делителей не существует и делитель должен быть кратен k * 2^t, где k и t - некоторые целые числа. Тогда остаток можно вычислить по следующей формуле: a % b = a % k*2^t = a -((2^N/k * a/2^N) & -2^t)*k
Эта формула очень сложна и идентификация оптимизированного оператора "%" может быть весьма и весьма непростой, особенно учитывая, что оптимизаторы изменяют порядок команд.
Рассмотрим
следующий пример:
main()
{
int a;
printf("%x
%x\n",a % 16, a % 10);
}
Идентификация
оператора "%"
Результат
его компиляции компилятором Microsoft
Visual C++ с настройками по умолчанию
должен выглядеть так:
main proc near ;
CODE XREF: start+AF p
var_4 = 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 (var_a) на ECX (0xA)
push edx
; Передаем
остаток от деления var_a на 0xA функции
printf
mov edx, [ebp+var_a]
; Заносим
в EDX значение переменной var_a
and edx, 8000000Fh
; "Вырезаем" знаковый бит и четыре младших бита числа
; в четырех
младших битах содержится
jns short loc_401020
; Если
число не отрицательно, то прыгаем
на loc_401020
dec edx
or edx, 0FFFFFFF0h
inc edx
; Эта последовательность, как говорилось выше, характера для быстрого
; расчета остатка знакового числа
; Следовательно, последние шесть инструкций расшифровываются как:
; EDX = var_a
% 16
loc_401020: ; CODE XREF: main+19 j
push edx
push offset aXX ; "%x %x\n"
call _printf
add esp, 0Ch
; printf("%x
%x\n",var_a % 0xA, var_a % 16)
mov esp, ebp
pop ebp
; Закрываем кадр стека
retn
main endp
Любопытно, что оптимизация не влияет на алгоритм вычисления остатка. Увы, ни Microsoft Visual C++, ни остальные известные компиляторы не умеют вычислять остаток умножением.
§1.5
Идентификация оператора "*"
В общем случае оператор "*" транслируется либо в машинную инструкцию "MUL" (беззнаковое целочисленное умножение), либо в "IMUL" (целочисленное умножение со знаком), либо в "FMULx" (вещественное умножение). Если один из множителей кратен степени двойки, то "MUL" ("IMUL") обычно заменяется командой битового сдвига влево "SHL" или инструкцией "LEA", способной умножать содержимое регистров на 2, 4 и 8. Обе последних команды выполняются за один такт, в то время как MUL требует в зависимости от модели процессора от двух до девяти тактов. К тому же LEA за тот же такт успевает сложить результат умножение с содержимым регистра общего назначения и/или константой. Это позволяет умножать на 3, 5 и 9 просто добавляя к умножаемому регистру его значение. Правда, у операции LEA есть один недочет - она может вызывать остановку AGI, в конечном счете весь выигрыш в быстродействии сводится на нет.
Рассмотрим
следующий пример:
main()
{
int a;
printf("%x
%x %x\n",a * 16, a * 4 + 5, a * 13);
}
Идентификация
оператора "*"
Результат
его компиляции компилятором Microsoft
Visual C++ с настройками по умолчанию должен
выглядеть так:
main proc near ;
CODE XREF: start+AF p
var_a = dword ptr
-4
push ebp
mov ebp, esp
; Открываем
кадр стека
push ecx
; Резервируем
место для локальной переменной
var_a
mov eax, [ebp+var_a]
; Загружаем
в EAX значение переменной var_a
imul eax, 0Dh
; Умножаем
var_a на 0xD, записывая результат в
EAX
push eax
; Передаем
функции printf произведение var_a * 0xD
mov ecx, [ebp+var_a]
; Загружаем
в ECX значение var_a
lea edx, ds:5[ecx*4]
; Умножаем ECX на 4 и добавляем к полученному результату 5, записывая его в EDX
; И все
это выполняется за один такт!
push edx
; Передаем
функции printf результат var_a * 4 + 5
mov eax, [ebp+var_a]
; Загружаем
в EAX значение переменной var_a
shl eax, 4
; Умножаем
var_a на 16
push eax
; Передаем
функции printf произведение var_a * 16
push offset aXXX ; "%x %x %x\n"
call _printf
add esp, 10h
; printf("%x
%x %x\n", var_a * 16, var_a * 4 + 5, var_a * 0xD)
mov esp, ebp
pop ebp
; Закрываем кадр стека
retn
main endp
За
вычетом вызова функции printf и загрузки
переменной var_a из памяти уходит всего
лишь три такта процессора. Если скомпилировать
этот пример с ключом "/Ox", то получится
вот что:
main proc near ; CODE XREF: start+AF p
push ecx
; Выделяем
память для локальной
mov eax, [esp+var_a]
; Загружаем
в EAX значение переменной var_a
lea ecx, [eax+eax*2]
; ECX = var_a
* 2 + var_a = var_a * 3
lea edx, [eax+ecx*4]
; EDX = (var_a * 3)* 4 + var_a = var_a * 13!
; Так компилятор ухитрился умножить var_a на 13,
; причем всего за один (!) такт. Также следует отметить, что обе инструкции LEA
; прекрасно
спариваются на Pentium MMX и Pentium Pro!
lea ecx, ds:5[eax*4]
; ECX = EAX*4
+ 5
push edx
push ecx
; Передаем
функции printf var_a * 13 и var_a * 4 +5