АССЕМБЛЕРНЫЕ ФИЧИ

Здесь собраны некоторые относящиеся к ассемблеру фичи, не публикуемые в большинстве обычных туториалов. Не следует этот текст как-либо комментировать, и если содержание Вам покажется слишком простым - тогда считайте, что это дока для начинающих.
Всю положительную энергию, полученную от прочтения данного текста надлежит использовать во славу деструктивного вирмэйкинга.


СОДЕРЖАНИЕ



 

НЕМНОГО ВСЕМ ИЗВЕСТНОЙ АРИФМЕТИКИ

TOP

CDQ (convert double-word to quad-word)

Преобразование двойного слова (в EAX) в учетверенное слово (в EDX:EAX)
Более понятный эквивалент:

cdq
mov     edx, eax
sar     edx, 31

NOT

Инверсия всех битов операнда.

not     eax
xor     eax, 0FFFFFFFFh

NEG

Изменение знака операнда.

neg     eax
xor     eax, 0FFFFFFFFh
inc     eax
eax' = ~eax+1 = -eax

TEST

То же что и AND, но без изменения содержимого операндов. (Изменяются только флаги)

test    eax, 123
push    eax
and     eax, 123
pop     eax

CMP

Понятно, все слышали про CMP. ;-) Вот альтернативная точка зрения.
То же, что и SUB, но без изменения содержимого операндов. (Изменяются только флаги).

cmp     eax, 123
push    eax
sub     eax, 123
pop     eax

SBB (subtract with borrow)

Вычитание с заемом. Кроме второго операнда, из первого вычитается еще и CF (Carry Flag, флаг переноса).
Наиболее интересна форма команды с одинаковыми операндами. В этом случае в каждый бит регистра загружается значение CF, то есть регистр=0 если CF=0 и регистр=0xFFFFFFFF если CF=1.

sbb     eax, eax
if (cf == 0)
  eax = 0;
else
  eax = -1;


 

УСЛОВНОЕ ВЫПОЛНЕНИЕ БЕЗ ПЕРЕДАЧИ УПРАВЛЕНИЯ

TOP

А вот несколько приводящих в ужас примеров с использованием вышеперечисленных команд. Ассемблерный код примеров взят из какой-то доки по оптимизации для пня.

Пример 1

cmp     eax, ebx
sbb     ecx, ecx
if (eax >= ebx)
  ecx = 0;
else
  ecx = -1;

Пример 2
(нахождение абсолютного значения)

  eax >= 0 eax < 0  
mov     edx, eax
sar     edx, 31
xor     eax, edx
sub     eax, edx
edx = eax
edx = 0
eax ^= 0
eax -= 0
edx = eax
edx = 0xFFFFFFFF=-1
eax' = ~eax
eax'' = eax'-edx =
      = ~eax-(-1) =
      = ~eax+1 =
      = -eax
if (a < 0) a = -a;
//a = abs(a)

Пример 3
(нахождение минимального значения)

  ebx >= ebx ebx < eax  
sub     ebx, eax
sbb     ecx, ecx
and     ecx, ebx
add     eax, ecx
ebx'= ebx-eax
ecx = 0
ecx' = 0
eax' += 0
ebx' = ebx-eax
ecx = 0xFFFFFFFF
ecx' = ebx' = ebx - eax
eax' = eax + ecx' =
     = eax + (ebx - eax) =
     = ebx
if (a > b) a = b;
//a = min(a,b);

Пример 4

  eax = 0 eax != 0  
cmp     eax, 1
sbb     eax, eax
and     ecx, eax
xor     eax, -1
and     eax, ebx
or      eax, ecx
cf = 1
eax = 0FFFFFFFFh
ecx' = ecx
eax' = 0
eax'' = 0
eax''' = ecx
cf = 0
eax = 0
ecx' = 0
eax' = 0FFFFFFFFh
eax'' = ebx
eax''' = eax'' | ecx' =
    = ebx | 0 =
    = ebx
if (a != 0)
  a = b;
else
  a = c;


 

ВСЕМИ ЛЮБИМЫЕ BCD-ОПЕРАЦИИ

TOP

Замечено, что многие программисты боятся таких команд, как AAA, AAS, ASS ;-) и некоторых других. Причина этого (по Фрэйду) кроется глубоко в детстве и связана с сексуальными переживаниями, не поддающимися психоанализу. ;-))

Пара слов о BCD

BCD -- (binary coded decimal) -- двоично-десятичные числа. Выдумавшие их разработчики были лентяи и мазерфакеры, поэтому BCD-числа числа бывают двух видов -- упакованые и неупакованные.

Понятно, что нормальные арифметические (ADD/SUB/MUL/DIV) операции с такими числами невозможны, поэтому вместо того чтобы их выкинуть на помойку люди занимаются с ними мазохизмом в виде команд AAA, AAS, AAM, AAD, DAA и DAS.

Команды DAA и DAS предназначены для работы с Packed BCD. Команды AAA, AAS, AAM и AAD предназначены для Unpacked BCD. Команда AAD -- это коррекция ПЕРЕД делением, все остальные -- коррекции ПОСЛЕ соответствующих операций.

Действие BCD-команд

AAA AAS
Ascii Adjust after Addition Ascii Adjust after Substraction
ASCII-коррекция после сложения ASCII-коррекция после вычитания
if ((al & 0x0F) > 9) || (AF == 1)
{
  al = (al + 6) & 0x0F
  ah = ah + 1
  AF = 1
  CF = 1
}
else
{
  CF = 0
  AF = 0
}
if ((al & 0x0F) > 9) || (AF == 1)
{
  al = al - 6
  al = al & 0x0F
  ah = ah - 1
  AF = 1
  CF = 1
}
else
{
  CF = 0
  AF = 0
}
AAM AAD
Ascii Adjust after Multiplication Ascii Adjust before Division
ASCII-коррекция после умножения ASCII-коррекция перед делением
операнд по умолчанию = 10
AAM xx          al' = al mod xx
                ah' = al div xx

AAM 0           div 0

AAM 1           mov ah, al
                mov al, 0
AAD xx          add al, ah * xx
                mov ah, 0

AAD 0           mov ah, 00

AAD 1           add al, ah
                mov ah, 0
DAA DAS
Decimal Adjust after Addition Decimal Adjust after Substraction
десятичная коррекция после сложения десятичная коррекция после вычитания
if ((al & 0x0F) > 9) || (AF == 1)
{
  al = al + 6
  AF = 1
}
else
  AF = 0
if (al > 0x9F) || (CF == 1)
{
  al = al + 0x60
  CF = 1
}
else
  CF = 0
if ((al & 0x0F) > 9) || (AF == 1)
{
  al = al - 6
  AF = 1
}
else
  AF = 0
if (al > 0x9F) || (CF == 1)
{
  al = al - 0x60
  CF = 1
}
else
  CF = 0

О применимости BCD-чисел

Это может показаться странным, но BCD иногда используются в реальной жизни.

Например самый типичный вирмэйкерский объект -- таймер компьютера, то бишь время и дата, хранящиеся в CMOS, находятся в Packed BCD формате.

Флаг AF (Auxilary Flag) -- флаг вспомогательного переноса -- существует как раз для BCD арифметики. Работает так же, как и CF, но для бита 3 а не 7.

И даже в наборе команд сопроцессора присутствуют команды для работы с BCD: FBLD -- загрузить и FBSTP -- сохранить число в BCD формате.

Перевод чисел в BCD и обратно

BCD --> Binary Binary --> BCD
aam   16
aad   10
aam   10
aad   16

Пример использования BCD-арифметики

Когда-то один знакомый (написавший как-то весьма крутой вирус) задал мне интересную задачку -- написать подпрограмму выдачи шестнадцатиричного числа на экран (DOS, число в AX) -- наименьшей возможной длины.

Вот несколько вариантов решения:

20 байт 21 байт 21 байт 21 байт
hexword:
mov     cx, 4
cycle:
rol     ax, 4
push    ax

and     al,15
daa
add     al,-10h
adc     al,'0'+10h

int     29h
pop     ax
loop    cycle
ret
hexword:
mov     cx, 4
cycle:
rol     ax, 4
push    ax

and     al,15
sub     al,'A'-'0'
aam     '9'-'A'+1
add     al,'A'

int     29h
pop     ax
loop    cycle
ret
hexword:
mov     cx, 4
cycle:
rol     ax, 4
push    ax

and     al, 15
aam     10
aad     'A'-'0'
add     al, '0'

int     29h
pop     ax
loop    cycle
ret
hexword:
mov     cx, 4
cycle:
rol     ax, 4
push    ax

and     al, 15
add     al, 90h
daa
adc     al, 'A'-1
daa

int     29h
pop     ax
loop    cycle
ret


 

НЕСКОЛЬКО СЛОВ О КОМАНДАХ MUL И DIV

TOP

Умножение на константу

При умножении чисел на постоянный множитель (для увеличения скорости) вместо MUL можно использовать серии из SHL и ADD(OR).
Пример: Для многих не секрет, что в демках требуется писать код, выполняющийся с наибольшей скоростью. Рассмотрим типичный пример -- умножение на 320.

imul    eax, 320
shl     eax, 6  ; *64
mov     ebx, eax
shl     ebx, 2  ; *256
add     eax, ebx

Использование MUL вместо DIV

Рассмотрим интересный случай. Генератор случайных чисел, алгоритм - почти как у ТурбоПаскаля 7.0, период 2^32.

randseed        dd      ?

; void randomize() { randseed = GetTickCount(); }

randomize:      call    GetTickCount
                mov     randseed, eax
                ret

; DWORD random() { return randseed = randseed * 0x8088405 + 1; }

random:         mov     eax, randseed
                imul    eax, 8088405h
                inc     eax
                mov     randseed, eax
                ret

; DWORD rnd(DWORD range) { return random() % range; }

rnd:            xchg    ebx, eax        ; now EBX=range
                call    random
                xor     edx, edx
                mul     ebx             ; (*)
                xchg    edx, eax
                ret

Обратим внимание на строчку с (*), то бишь на MUL.
Неочевидно, но факт - здесь его действие почти такое же как и у DIV. Более того - в этой ситуации MUL работает примерно в 1.5 раза быстрее, а в случае нулевого параметра -- rnd(0) -- в результате возвращается 0, тогда как при DIV произошла бы ошибка деления.

Несложно догадаться, что возникает такая ситуация только в конкретном случае, когда операнд для DIV(MUL) -- случайное 16- или 32-битное число.

 

Сравнение ASCIIZ-строк с использованием хэш-функций

TOP

Предположим, что нам нужно сравнить две ASCIIZ-строки (обычные заканчивающиеся нулем текстовые строки). Рассматриваемое действие - посчитать от строчек некоторую хэш-функцию (контрольную сумму, etc.), и сравнивать уже эти функции. При известном наборе строчек и/или ограничениях на них, ошибки исключены. Преимущество такого сравнения в случаях:

Вот кусок кода, который позволяет вытаскивать из экспорта адреса функций по хэшам их имен.

calchash        macro   procname
                hash = 0
                irpc    c, <procname>
                ; rol 7
                hash = ((hash shl 7) and 0FFFFFFFFh) or (hash shr (32-7))
                hash = hash xor "&c"
                endm
                endm

mov_h           macro   reg, procname
                calchash procname
                mov     reg, hash
                endm

j_GetProcAddress        dd      ?

                mov_h   edi, GetProcAddress
                call    findfunc
                jz      exit
                mov     j_GetProcAddress, eax

; find function's address in export table
;
; input:  EBX=imagebase va, ECX=export table va, EDI=name csum
; modify: EDX, ESI
; output: ZF=1, EAX=0 (function not found)
;         ZF=0, EAX=function va

findfunc:       xor     esi, esi        ; current index
search_cycle:   lea     edx, [esi*4+ebx]
                add     edx, [ecx].ex_namepointersrva
                mov     edx, [edx]      ; name va
                add     edx, ebx        ; +imagebase

                xor     eax, eax        ; calculate hash
calc_hash:      rol     eax, 7
                xor     al, [edx]
                inc     edx
                cmp     byte ptr [edx], 0
                jne     calc_hash

                cmp     eax, edi        ; compare hashs
                je      name_found

                inc     esi             ; index++
                cmp     esi, [ecx].ex_numofnamepointers
                jb      search_cycle
                xor     eax, eax        ; return 0
                ret

name_found:     mov     edx, [ecx].ex_ordinaltablerva
                add     edx, ebx        ; +imagebase
                movzx   edx, word ptr [edx+esi*2]; edx=current ordinal
;               sub     edx, [ecx].ex_ordinalbase  ; -ordinal base
                mov     eax, [ecx].ex_addresstablerva
                add     eax, ebx        ; +imagebase
                mov     eax, [eax+edx*4]; eax=current address
                add     eax, ebx        ; +imagebase
                ret                     ; return address


 

Вывод десятичных чисел на экран (itoa)

TOP

Достаточно интересная задача, к тому же процедура вывода числа весьма универсальна.

itoa:           xor     edx, edx
                mov     ebx, 10  ; основание системы счисления
                div     ebx
                push    edx      ; запомнить остаток от деления
                or      eax, eax ; частное == 0 ?
                jz      @@1
                call    itoa     ; рекурсивный вызов (если есть что делить)
@@1:            pop     eax
                add     al, '0'
                ...              ; вывод символа (stosb, INT 29h, etc.)
                ret