Дизассемблирование

1.2 Идентификация оператора "-"

В общем случае оператор "- " транслируется либо в машинную инструкцию SUB (если операнды - целочисленные значения), либо в инструкцию FSUBx (если операнды - вещественные значения). Оптимизирующие компиляторы могут заменять "SUB xxx, 1" более компактной командой "DEC xxx", а конструкцию "SUB a, const" транслировать в "ADD a, -const", которая ничуть не компактнее и ни сколь не быстрей (и та, и другая укладываться в один так). Рассмотрим это в следующем примере: main(){int a,b,c; c = a - b;printf("%x\n",c); c = c - 10;printf("%x\n",c); }

Демонстрация идентификации оператора "-"

Не оптимизированный вариант будет выглядеть приблизительно так: main proc near ; CODE XREF: start+AF p var_c = dword ptr -0Chvar_b = dword ptr -8var_a = dword ptr -4 push ebpmov ebp, esp; Открываем кадр стека sub esp, 0Ch; Резервируем память под локальные переменные mov eax, [ebp+var_a]; Загружаем в EAX значение переменной var_a sub eax, [ebp+var_b]; Вычитаем из var_a значением переменной var_b, записывая результат в EAX mov [ebp+var_c], eax; Записываем в var_c разность var_a и var_b; var_c = var_a - var_b mov ecx, [ebp+var_c]push ecxpush offset asc_406030 ; "%x\n"call _printfadd 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 eaxpush offset asc_406034 ; "%x\n"call _printfadd esp, 8; printf("%x\n",var_c) mov esp, ebppop ebp; Закрываем кадр стекаretn main endp Теперь рассмотрим оптимизированный вариант того же примера: main proc near ; CODE XREF: start+AF ppush 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 esipush 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 esipush offset asc_406034 ; "%x\n"call _printfadd esp, 10h; printf("%x\n", var_a - var_b - 0xA) pop esipop ecx; Закрываем кадр стека retn main endp

Компиляторы (Borland, WATCOM) генерируют практически идентичный код.

1.3 Идентификация оператора "/"

В общем случае оператор "/" транслируется либо в машинную инструкцию "DIV" (беззнаковое целочисленное деление), либо в "IDIV" (целочисленное деление со знаком), либо в "FDIVx" (вещественное деление). Если делитель кратен степени двойки, то "DIV" заменяется на более быстродействующую инструкцию битового сдвига вправо "SHR a, N", где a - делимое, а N - показатель степени с основанием два.

Несколько сложнее происходит быстрое деление знаковых чисел. Совершенно недостаточно выполнить арифметический сдвиг вправо (команда арифметического сдвига вправо 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 ebpmov 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 eaxpush offset aXX ; "%x %x\n"call _printfadd esp, 0Ch; printf("%x %x\n", var_a / 0xA, var_a / 32) mov esp, ebppop ebp; Закрываем кадр стека retn main endp

Теперь, рассмотрим оптимизированный вариант того же примера: main proc near ; CODE XREF: start+AF ppush ecx; Резервируем память для локальной переменной var_a mov ecx, [esp+var_a]; Загружаем в ECX значение переменной var_a mov eax, 66666667h; В исходном коде ничего подобного не было! imul ecx; Умножаем это число на переменную var_a; Обратите внимание: именно умножаем, а не делим. sar edx, 2; Выполняем арифметический сдвиг всех битов EDX на две позиции вправо, что; в первом приближении эквивалентно его делению на 4; Однако ведь в EDX находятся старшее двойное слово результата умножения!; Поэтому, три предыдущих команды фактически расшифровываются так:; EDX = (66666667h * var_a) >> (32 + 2) = (66666667h * var_a) / 0x400000000; ; Теперь немного упростим код:; (66666667h * var_a) / 0x400000000 = var_a * 66666667h / 0x400000000 =; = var_a * 0,10000000003492459654808044433594; Заменяя по всем правилам математики умножение на деление и одновременно; выполняя округление до меньшего целого получаем:; 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 eaxpush offset aXX ; "%x %x\n"call _printfadd esp, 10h; printf("%x %x\n", var_a / 10, var_a / 32) retn main endp


Страница: