OLE Variant 2 ANSI — Архив WASM.RU

Все статьи

OLE Variant 2 ANSI — Архив WASM.RU

Исходники к статье

Для кого эта статья?

Этот материал полезен для начинающих воинов дзена и может поведать о том как:

  • конвертировать целые числа и float`ы единым алгоритмом
  • узнать количество десятичных цифр в целом числе через логарифм
  • избавиться от условных переходов при помощи команд cmovXX, bt и модификации кода
Здесь также будет рассмотрен исходник автономной процедуры для преобразования типа variant в ANSI строку.

OLE Variant

Этот тип данных используется для передачи информации через COM. Переменная такого типа может содержать до 64 бит полезной информации (это могут быть числа, указатели или флаги). Изнутри эта структура выглядит так:

struct VARIANT
  vt		rw 1 ;тип данных
  wReserved1	rw 1 ;зарезервировано
  wReserved2	rw 1 ;зарезервировано
  wReserved3	rw 1 ;зарезервировано
  value 	rq 1 ;данные
ends

Единственное поле которое заслуживает подробного описания это vt - поле типа данных. Оно может содержать следующие значения:

нотация MicrosoftнотацияDelphihex значениеописание
vt_emptyvarEmpty00hпустая структура
vt_nullvarNull01hпустая структура
vt_i2varSmallint02hцелое со знаком 2 байта
vt_i4varInteger03hцелое со знаком 4 байта
vt_r4varSingle04hчисло с плавающей точкой 4 байта
vt_r8varDouble05hчисло с плавающей точкой 8 байт
vt_cyvarCurrency06hх.з.
vt_datevarDate07hчисло с плавающей точкой 8 байт (01.01.1900=2.0)
vt_bstrvarOleStr08hуказатель на unicode строку
vt_dispatchvarDispatch09hуказатель на интерфейс IDispatch
vt_errorvarError0Ahкод ошибки 4 байта
vt_boolvarBoolean0Bh1 бит
vt_variantvarVariant0Chуказатель на переменную типа variant
vt_unknownvarUnknown0Dhх.з.
vt_i1varShortInt10hцелое со знаком 1 байт
vt_ui1varByte11hцелое без знака 1 байт
vt_ui2varWord12hцелое без знака 2 байта
vt_ui4varLongWord13hцелое без знака 4 байта
vt_i8varInt6414hцелое со знаком 8 байт
vt_clsidvarStrArg48hуказатель на CLSID

Единый алгоритм преобразования чисел
Математический сопроцессор аппаратно поддерживает упакованые двоично-десятичные числа, следовательно можно переложить всю работу на инструкцию fbstp. для особо непродвинутых приведу формат упакованого BCD числа.

9876543210
sxxxxxxx17,1615,1413,1211,109,87,65,43,21,0
Как видно из таблицы BCD - число содержит 18 тетрад при этом значение тетрады лежит в диапазоне от 0 до 9 (в общем это перевёрнутое текстовое отображение числа).
S - это знаковый бит (1 - число отрицательное)

Теперь перейдём к числам в формате с плавающей точкой. Для начала нужно преобразовать число с плавающей точкой в число с фиксированной точкой. Допустим у нас есть число:

87954.6465
В идеале его нужно привести к 18-значному виду вот так:
879546465000000000
Напрашивается формула Y=X*10(18-<длина целой части>). Вопрос: как найти длину целой части? Самый быстрый и маленький способ решить эту задачу - использовать логарифмы.

Итак определение:
Логарифм данного числа X при основании Y - это показатель степени, в которую нужно возвести число Y, чтобы получить X.

Можно проще:
X=Y(logYX)

То есть если взять логарифм по онованию 2 мы получим количество значащих битов в целом числе. Если же взять логарифм по основанию 10 - это будет количество десятичных символов в числе. Фактически сопроцессор способен рассчитывать только логарифмы по основанию 2 и для того чтобы найти логарифм с произвольным основанием применяется формула:
logYX=(log2X)/(log2Y)
Заботливые инженеры intel предусмотрели ряд часто используемых констант, их можно получить при помощи инструкций fldXXX (Нас интересует fldl2t - загрузить log210).

В результате всего вышесказанного вырисовывается следующий алгоритм:

  1. Загрузить число в FPU
  2. Подсчитать длину целой части
  3. Умножить число на 10(18-<длина целой части>) (при этом лучше всего воспользоваться таблицей значений)
  4. сохранить BCD
  5. Если знаковый бит установлен вывести минус
  6. Вывести целую часть
  7. Вывести точку
  8. Вывести оставшиеся цифры

Однако для умножения на 10x необходимо использовать только точные целые значения (иначе будет жуткая погрешность). Сопроцессор оперирует числами с мантиссой в 64 бита и порядком в 15 бит, а этого не достаточно чтобы точно представить число 1018. Умножение на 64 битовое целое число невозможно по той же причине. Поэтому придётся использовать 32 разрядные целые числа с диапазоном от 0 до 109.

Следовательно число:

0.00000123456789

Будет урезано до:

1234

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

Оптимизация
Это несомненно самая дзенская часть статьи. Итак рассмотрим техники оптимизации, которые я применил от простого к сложному.

  • Инструкции cmovXX или инструкции условной пересылки данных выглядят так:
    cmovXX reg16,reg16
    cmovXX reg32,reg32
    Это позволяет копировать данные из одного регистра в другой при исполнении некоторого условия. Например чтобы поместить в eax наибольшее число из eax и edx нужно написать
    cmp   eax,edx
    cmovl eax,edx
  • Маски позволяют обнулить регистр или память при некотором условии. Маска принимает 2 значения: -1 и 0.

    Вот стандартный способ применения маски из флага cf:

    cmp ax,dx ;если ax>=dx то обнулить ax
    sbb dx,dx
    and ax,dx

    Хотя можно получить маску из любого флага используя setXX:

    xor   ax,ax ;если cx=dx, то ax=-1, иначе ax=0
    cmp   сx,dx
    setne al
    dec   ax
  • Битовые множества могут существенно сократить количество сравнений и условных переходов.
    ensemble db 01000110b ;множество чисел 1,2,6 
    -//-
    movzx eax,al
    bt    [ensemble],eax
    jc    quit            ;если (al=1) или (al=2) или (al=6) то выход
  • Модификация кода. Самомодифицирующаяся программа похожа на поезд, который сам прокладывает себе рельсы
    mov  byte[dirrection],0FDh ;загружаем Маш. код инструкции std
    rcr  al,1                  ;если al нечётный то
    sbb  byte[dirrection],0    ;std превращается в cld
    dirrection:
    rb   1
    Можно даже например создать процедуру в стеке, которая сама выбросит себя от туда при завершении:
    push 0004C2ACh	;маш-коды инструкций 0ACh-lodsb  0004C2h-ret 4
    call esp	;вызываем процедуру из стека

Процедура Variant2Str

Процедура понимает следующие типы:

  • varSmallint
  • varInteger
  • varSingle
  • varDouble
  • varOleStr
  • varBoolean
  • varByte
  • varWord
  • varLongWord
  • varInt64

иначе выводит NaN

ограничения:

  • длина числа не более 18 десятичных цифр
  • требуется наличие WideCharToMultiByte в секции импорта
  • логический тип представляется как да/нет

Параметры:
value - указатель на переменную типа variant
lpsz - указатель на выходную строку

procedure Variant2Str(value,lpsz: pointer);assembler;
const
power10:array[0..9] of longword=(1000000000,          //таблица степеней десяти
                                 100000000,
                                 10000000,
                                 1000000,
                                 100000,
                                 10000,
                                 1000,
                                 100,
                                 10,
                                 1);
opcodes:array[0..3] of byte=($df {fild word[ecx]},    //опкоды для загрузки данных
                             $db {fild dword[ecx]},
                             $d9 {fld  dword[ecx]},
                             $dd {fld  qword[ecx]});
NaNSet: integer=229436;                               //1111000000000111100b множество
                                                      //поддерживаемых типов
asm
finit
pusha
mov   edi,lpsz                  
mov   ecx,Value
movzx eax,word[ecx]              //в eax тип переменной

cmp    al,$0B                    //если это vt_bool
jne    @notbool                  //то выводим да или нет
mov    edx,'аД'                  //и выходим
mov    eax,'теН'              
test   [ecx+8],al
cmovne eax,edx
mov    [edi],eax
popa
ret
@notbool:

cmp    al,8                       //если это vt_bstr
jne    @notstring                 //то воспользуемся 
xor    eax,eax                    //функцией WideCharToMultiByte
push   eax                        //и выходим
push   eax
push   65536
push   edi
push   -1
push   [ecx+8]
push   eax
push   eax
call   WideCharToMultiByte
popa
ret
@notstring:

push  ax
fstcw [esp]                       //устанавливаем максимальную точность FPU
or    word[esp],0000011100000000b //и режим округления в меньшую сторону
fldcw [esp]

add   ecx,8                       //теперь ecx указывает на поле данных
cmp   ax,20                       //если тип не поддерживается
ja    @NaN                        //выводим NaN и выходим
bt    [NaNSet],eax
jnc   @NaN

mov   edx,$00C30100               //подготавливаем опкоды
mov   dl,byte[opcodes+eax-2]      //для загрузки данных в зависимости от типа.
mov   bx,$29bf                    //в bx опкод для fild qword[ecx]  
cmp   al,16
cmova dx,bx

push  edx
call  esp                         //загружаем данные
pop   edx

fld1                              //вычисляем длину целой части числа
fld   st(1)
fabs
fyl2x
fldl2t
fdivp
fistp dword[esp-4]

mov   eax,dword[esp-4]
add   eax,1                       //если log(X)=-1,
adc   eax,0                       //то X<1=>
dec   eax                         //=>длина числа=1 (eax=0)
cmp   al,17                       //если число слишком длинное то
ja    @NaN                        //выводим NaN и выходим
mov   edx,eax
sub   al,8
cmc
sbb   cl,cl                       //умножаем на 10(18-<длина числа>)
and   al,cl                       //при этом если (18-<длина числа>)>9
fimul dword[power10+eax*4]        //то умножаем на 109
fbstp [edi]
fldcw [esp]                       //восстанавливаем настройки FPU
pop   cx

neg     eax
add     eax,edx
lea     esi,[esp-26+eax+9]        //Адрес первого символа в буфере

push esi                          
push edi
mov     esi,edi
lea     edi,[esp-18]
mov     ecx,9
@unpack:xor  ax,ax                //распаковываем BCD число в буфер
        lodsb                     
        shl  ax,4
        shr  al,4
        add  ax,$3030             //и преобразуем его в ASCII
        stosw
loop @unpack
pop edi
pop esi

shl     byte[edi+9],1             //если число отрицательное
mov     byte[edi],'-'             //ставим минус
adc     edi,0

lea     eax,[esp-25]              //dx -  длина целой части
mov     ecx,esi                   //ecx - общая длина
sub     ecx,eax
std
@copy: lodsb                      //переворачиваем строку из буфера
       mov [edi],al               //и отделяем целую часть
       inc edi                    //от дробной точкой
       mov byte[edi],'.'
       sub dl,1
       adc edi,0
loop @copy
mov    [edi],cl                   //завершающий ноль
scasb
mov    al,'0'                     //удаляем лишние нули
mov    cl,18                      //в конце строки
repe   scasb
mov    byte[edi+2],ch
cmp    byte[edi+1],47             //если последний символ - это точка
cmc                               //то удаляем её
sbb    al,al
and    byte[edi+1],al
cld
popa

ret
@NaN:
mov    dword[edi],'NaN'
pop    ax
popa
end;

2002-2013 (c) wasm.ru