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

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

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

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

Оглавление

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

Файлы: 1 файл

kyrsovaya0002.doc

— 391.00 Кб (Скачать)
">add esp, 10h

; 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 содержимое неинициализированной  регистровой переменной ECX 

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

; "Вырезаем" знаковый бит и четыре младших  бита числа

; в четырех  младших битах содержится остаток от деления EDX на 16 

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

; Выделяем  память для локальной переменной var_a 

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 

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