OLE Variant 2 ANSI


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

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

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

  • конвертировать целые числа и 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 нотацияDelphi hex значение описание
vt_empty varEmpty 00h пустая структура
vt_null varNull 01h пустая структура
vt_i2 varSmallint 02h целое со знаком 2 байта
vt_i4 varInteger 03h целое со знаком 4 байта
vt_r4 varSingle 04h число с плавающей точкой 4 байта
vt_r8 varDouble 05h число с плавающей точкой 8 байт
vt_cy varCurrency 06h х.з.
vt_date varDate 07h число с плавающей точкой 8 байт (01.01.1900=2.0)
vt_bstr varOleStr 08h указатель на unicode строку
vt_dispatch varDispatch 09h указатель на интерфейс IDispatch
vt_error varError 0Ah код ошибки 4 байта
vt_bool varBoolean 0Bh 1 бит
vt_variant varVariant 0Ch указатель на переменную типа variant
vt_unknown varUnknown 0Dh х.з.
vt_i1 varShortInt 10h целое со знаком 1 байт
vt_ui1 varByte 11h целое без знака 1 байт
vt_ui2 varWord 12h целое без знака 2 байта
vt_ui4 varLongWord 13h целое без знака 4 байта
vt_i8 varInt64 14h целое со знаком 8 байт
vt_clsid varStrArg 48h указатель на CLSID

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

9 8 7 6 5 4 3 2 1 0
sxxxxxxx 17,16 15,14 13,12 11,10 9,8 7,6 5,4 3,2 1,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;

[C] murder

 

Источник WASM.RU /10.04.2008/

Поделиться в соц сетях

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Есть идеи, замечания, предложения? Воспользуйтесь формой Обратная связь или отправьте сообщение по адресу replay@sciencestory.ru
© 2017 Истории науки. Информация на сайте опубликована в ознакомительных целях может иметь ограничение 18+