Фундаментальные основы хакерства

Идентификация математических операторов


"…если вы обессилены, то не удивительно, что вся ваша жизнь -- не развлечение. У вас… так много вычислений, расчетов, которые необходимо сделать в вашей жизни, что она просто не может быть развлечением."

Ошо "Пустая Лодка"

Беседы по высказываниям Чжуан Цзы

Идентификация оператора "+". В общем случае оператор "+" транслируется либо в машинную инструкцию ADD, "перемалывающую" целочисленные операнды, либо в инструкцию FADDx, обрабатывающую вещественные значения. Оптимизирующие компиляторы могут заменять "ADD xxx, 1" более компактной командой "INC xxx", а конструкцию "c = a + b + const" транслировать в машинную инструкцию "LEA c, [a + b + const]". Такой трюк позволяет одним махом складывать несколько переменных, возвратив полученную сумму в любом регистре общего назначения, – не обязательно в левом слагаемом как это требует мнемоника команды ADD. Однако, "LEA" не может быть непосредственно декомпилирована в оператор "+", поскольку она используется не только для оптимизированного сложения (что, в общем-то, побочный продукт ее деятельности), но и по своему непосредственному назначению – вычислению эффективного смещения. (подробнее об этом см. "Идентификация констант и смещений", "Идентификация типов"). Рассмотрим следующий пример:

main()

{

int a, b,c;

c = a + b;

printf("%x\n",c);

c=c+1;

printf("%x\n",c);

}

Листинг 204 Демонстрация оператора "+"



Результат его компиляции компилятором Microsoft Visual C++ 6.0 с настройками по умолчанию должен выглядеть так:

main         proc near           ; CODE XREF: start+AFp

var_c        = dword      ptr -0Ch

var_b        = dword      ptr -8

var_a        = dword      ptr -4

push   ebp

mov    ebp, esp

; Открываем кадр стека

sub    esp, 0Ch

; Резервируем память для локальных переменных

mov    eax, [ebp+var_a]

; Загружаем в EAX значение переменной var_a


add    eax, [ebp+var_b]

; Складываем EAX со значением переменной var_b

и записываем результат в EAX

mov    [ebp+var_c], eax

; Копируем сумму var_a

и var_b в переменную var_c, следовательно:

; var_c = var_a + var_b

mov    ecx, [ebp+var_c]

push   ecx

push   offset asc_406030 ; "%x\n"

call   _printf

add    esp, 8

; printf("%x\n", var_c)

mov    edx, [ebp+var_c]

; Загружаем в EDX значение переменной var_c

add    edx, 1

; Складываем EDX со значением 0х1, записывая результат в EDX

mov    [ebp+var_c], edx

; Обновляем var_c

; var_c = var_c +1

mov    eax, [ebp+var_c]

push   eax

push   offset asc_406034 ; "%x\n"

call   _printf

add    esp, 8

; printf("%\n",var_c)

mov    esp, ebp

pop    ebp

; Закрываем кадр стека

retn

main         endp

Листинг 205

А теперь посмотрим, как будет выглядеть тот же самый пример, скомпилированный с ключом "/Ox" (максимальная оптимизация):

main         proc near           ; CODE XREF: start+AFp

push   ecx

; Резервируем место для одной локальной переменной

; (компилятор посчитал, что три переменные можно ужать в одну и это дейст. так)

mov    eax, [esp+0]

; Загружаем в EAX значение переменной var_a

mov    ecx, [esp+0]

; Загружаем в EAX значение переменной var_b

; (т.к .переменная не инициализирована загружать можно откуда угодно)

push   esi

; Сохраняем регистр ESI в стеке

lea    esi, [ecx+eax]

; Используем LEA для быстрого сложения ECX и EAX с последующей записью суммы

; в регистр ESI

; "Быстрое сложение" следует понимать не в смысле, что команда LEA выполняется

; быстрее чем ADD, - количество тактов той и другой одинаково, но LEA

; позволяет избавиться от создания временной переменной для сохранения

; промежуточного результата сложения, сразу направляя результат в ESI

; Таким образом, эта команда декомпилируется как

; reg_ESI = var_a + var_b



push   esi

push   offset asc_406030 ; "%x\n"

call   _printf

; printf("%x\n", reg_ESI)

inc    esi

; Увеличиваем ESI на единицу

; reg_ESI = reg_ESI + 1

push   esi

push   offset asc_406034 ; "%x\n"

call   _printf

add    esp, 10h

; printf("%x\n", reg_ESI)

pop    esi

pop    ecx

retn

main         endp

Листинг 206

Остальные компиляторы (Borland C++, WATCOM C) генерируют приблизительно идентичный код, поэтому, приводить результаты бессмысленно – никаких новых "изюминок" они в себе не несут.

Идентификация оператора "–". В общем случае оператор "– " транслируется либо в машинную инструкцию 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);

}

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

Не оптимизированный вариант будет выглядеть приблизительно так:

main         proc near           ; CODE XREF: start+AFp

var_c        = dword      ptr -0Ch

var_b        = dword      ptr -8

var_a        = dword      ptr -4

push   ebp

mov    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   ecx

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

Листинг 208

А теперь рассмотрим оптимизированный вариант того же примера:

main         proc near           ; CODE XREF: start+AFp

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

Листинг 209

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

Идентификация оператора "/". В общем случае оператор "/" транслируется либо в машинную инструкцию "DIV" (беззнаковое целочисленное деление), либо в "IDIV" (целочисленное деление со знаком), либо в "FDIVx" (вещественное деление).


Если делитель кратен степени двойки, то "DIV" заменяется на более быстродействующую инструкцию битового сдвига вправо "SHR a, N", где a – делимое, а N – показатель степени с основанием два.

Несколько сложнее происходит быстрое деление знаковых чисел. Совершенно недостаточно выполнить арифметический сдвиг вправо (команда арифметического сдвига вправо SAR

заполняет старшие биты с учетом знака числа), ведь если модуль делимого меньше модуля делителя, то арифметический сдвиг вправо сбросит все значащие биты в "битовую корзину", в результате чего получиться 0xFFFFFFFF, т.е. –1, в то время как правильный ответ – ноль. Вообще же, деление знаковых чисел арифметическим сдвигом вправо дает округление в большую сторону, что совсем не входит в наши планы. Для округления знаковых чисел в меньшую сторону необходимо перед выполнением сдвига добавить к делимому число , где N

– количество битов, на которые сдвигается число при делении. Легко видеть, что это приводит к увеличению всех сдвигаемых битов на единицу и переносу в старший разряд, если хотя бы один из них не равен нулю.

Следует отметить: деление очень медленная операция, гораздо более медленная чем умножение (выполнение DIV

может занять свыше 40 тактов, в то время как MUL обычно укладываться в 4), поэтому, продвинутые оптимизирующие компиляторы заменяют деление умножением. Существует множество формул подобных преобразований, вот, например, она (самая популярная из них):

, где N – разрядность числа. Выходит, грань между умножением и делением очень тока, а их идентификация довольно сложна. Рассмотрим следующий пример:

main()

{

int a;

printf("%x %x\n",a / 32, a / 10);

}

Листинг 210 Идентификация оператора "/"

Результат его компиляции компилятором Microsoft Visual C++ с настройками по умолчанию должен выглядеть так:

main         proc near           ; CODE XREF: start+AFp

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 позиций

; эквивалентен делению числа на 25 = 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

Листинг 211

А теперь, засучив рукава и глотнув пустырника (или валерьянки) рассмотрим оптимизированный вариант того же примера:

main         proc near           ; CODE XREF: start+AFp

push   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, на других

; процессорах количество тактов может отличается */



; Т.е. оптимизация дала более чем трехкратный выигрыш, браво Microsoft!

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

add    esp, 10h

; printf("%x %x\n", var_a / 10, var_a / 32)

retn

main         endp

Листинг 212

Ну, а другие компиляторы, насколько они продвинуты в плане оптимизации? Увы, ни Borland, ни WATCOM не умеют заменять деление более быстрым умножением для чисел отличных от степени двойки. В подтверждении тому рассмотрим результат компиляции того же примера компилятором Borland C++:

_main        proc near           ; DATA XREF: DATA:00407044o

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+11j

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

Листинг 213

Идентификация оператора "%". Специальной инструкции для вычисления остатка в наборе команд микропроцессоров серии 80x86 нет, - вместо этого остаток вместе с частным возвращается инструкциями деления DIV, IDIV

и FDIVx

(см. идентификация оператора "/").

Если делитель представляет собой степень двойки (2N = b), а делимое беззнаковое число, то остаток будет равен N младшим битам делимого числа. Если же делимое – знаковое, необходимо установить все биты, кроме первых N равными знаковому биту для сохранения знака числа. Причем, если N первых битов равно нулю, все биты результата должны быть сброшены независимо от значения знакового бита.

Таким образом, если делимое – беззнаковое число, то выражение a % 2N

транслируется в конструкцию: "AND a, N", в противном случае трансляция становится неоднозначна – компилятор может вставлять явную проверку на равенство нулю с ветвлением, а может использовать хитрые математические алгоритмы, самый популярный из которых выглядит так: DEC x\ OR x, -N\ INC x. Весь фокус в том, что если первые N бит числа x равны нулю, то все биты результата кроме старшего, знакового бита, будут гарантированно равны одному, а OR x, -N

принудительно установит в единицу и старший бит, т.е. получится значение, равное, –1. А INC –1 даст ноль! Напротив, если хотя бы один из N младших битов равен одному, заема из старших битов не происходит и INC x

возвращает значению первоначальный результат.

Продвинутые оптимизирующие компиляторы могут путем сложных преобразований заменять деление на ряд других, более быстродействующих операций. К сожалению, алгоритмов для быстрого вычисления остатка для всех делителей не существует и делитель должен быть кратен , где k и t – некоторые целые числа.


Тогда остаток можно вычислить по следующей формуле:

Да, эта формула очень сложна и идентификация оптимизированного оператора "%" может быть весьма и весьма непростой, особенно учитывая патологическую любовь оптимизаторов к изменению порядка команд.

Рассмотрим следующий пример:

main()

{

int a;

printf("%x %x\n",a % 16, a % 10);

}

Листинг 214 Идентификация оператора "%"

Результат его компиляции компилятором Microsoft Visual C++ с настройками по умолчанию должен выглядеть так:

main         proc near           ; CODE XREF: start+AFp

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+19j

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

Листинг 215

Любопытно, что оптимизация не влияет на алгоритм вычисления остатка.


Увы, ни Microsoft Visual C++, ни остальные известные мне компиляторы не умеют вычислять остаток умножением.

Идентификация оператора "*".

В общем случае оператор "*" транслируется либо в машинную инструкцию "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);

}

Листинг 216 Идентификация оператора "*"

Результат его компиляции компилятором Microsoft Visual C++ с настройками по умолчанию должен выглядеть так:

main         proc near           ; CODE XREF: start+AFp

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

Листинг 217

За вычетом вызова функции printf и загрузки переменной var_a из памяти на все про все требуется лишь три

такта процессора. А что будет, если скомпилировать этот пример с ключиком "/Ox"? А будет вот что:

main         proc near           ; CODE XREF: start+AFp

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

shl    eax, 4

; Умножаем var_a на

16

push   eax

push   offset aXXX  ; "%x %x %x\n"

call   _printf

add    esp, 14h

; printf("%x %x %x\n", var_a * 16, var_a * 4 + 5, var_a * 13)

retn

main         endp

Листинг 218

Этот код, правда, все же не быстрее предыдущего, не оптимизированного, и укладывается в те же три такта, но в других случаях выигрыш может оказаться вполне ощутимым.

Другие компиляторы так же используют LEA для быстрого умножения чисел. Вот, к примеру, Borland поступает так:

_main        proc near           ; DATA XREF: DATA:00407044o



lea    edx, [eax+eax*2]

; EDX = var_a*3

mov    ecx, eax

; Загружаем в ECX неинициализированную регистровую переменную var_a

shl    ecx, 2

; ECX = var_a * 4

push   ebp

; Сохраняем EBP

add    ecx, 5

; Добавляем к var_a

* 4 значение 5

; Borland

не использует LEA

для сложения. А жаль…

lea    edx, [eax+edx*4]

; EDX = var_a + (var_a *3) *4 = var_a * 13

; А вот в этом Borland и MS единодушны :-)

mov    ebp, esp

; Открываем кадр стека

; Да, да… вот так посреди функции и открываем…

; Выше, кстати, "потерянная" команда push EBP

push   edx

; Передаем printf произведение var_a

* 13

shl    eax, 4

; Умножаем ((var_a

*4) + 5) на 16

; Что такое?! Да, это глюк компилятора, посчитавшего: раз переменная var_a

; неинициализирована, то ее можно и не загружать…

push   ecx

push   eax

push   offset aXXX  ; "%x %x %x\n"

call   printf

add    esp, 10h

xor    eax, eax

pop    ebp

retn

_main        endp

Листинг 219

Хотя "визуально" Borland генерирует более "тупой" код, его выполнение укладывается в те же три такта процессора. Другое дело WATCOM, показывающий удручающе отсталый результат на фоне двух предыдущих компиляторов:

main         proc near

push   ebx

; Сохраняем EBX в стеке

mov    eax, ebx

; Загружаем в EAX значение неинициализированной регистровой переменной var_a

shl    eax, 2

; EAX = var_a * 4

sub    eax, ebx

; EAX = var_a * 4 – var_a = var_a * 3

; Вот каков WATCOM! Сначала умножает "с запасом", а потом лишнее отнимает!

shl    eax, 2

; EAX = var_a * 3 * 4 = var_a * 12

add    eax, ebx

; EAX = var_a * 12 + var_a = var_a * 13

; Вот так, да? Четыре инструкции, в то время как "ненавистный" многим

; Microsoft Visual C++ вполне обходится и двумя!

push   eax

; Передаем printf значение var_a

* 13

mov    eax, ebx

; Загружаем в EAX значение неинициализированной регистровой переменной var_a



shl    eax, 2

; EAX = var_a * 4

add    eax, 5

; EAX = var_a * 4 + 5

; Ага! Пользоваться LEA WATCOM то же не умеет!

push   eax

; Передаем printf значение var_a * 4 + 5

shl    ebx, 4

; EBX = var_a * 16

push   ebx

; Передаем 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*13)

pop    ebx

retn

main_        endp

Листинг 220

В результате, код, сгенерированный компилятором WATCOM требует шести тактов, т.е. вдвое больше, чем у конкурентов.

::Комплексные операторы. Язык Си\Си++ выгодно отличается от большинства своих конкурентов поддержкой комплексных операторов: x= (где x – любой элементарный оператор), ++ и – –.

Комплексные операторы семейства "a x= b" транслируются в "a = a x b" и они идентифицируются так же, как и элементарные операторы (см. "элементарные операторы").

Операторы "++" и "––": в префиксной форме они выражаются в тривиальные конструкции "a = a +1" и "a = a – 1" не представляющие для нас никакого интереса, но вот постфиксная форма – дело другое.

__обращение к разным частям одной переменной


Содержание раздела