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).
В результате всего вышесказанного вырисовывается следующий алгоритм:
- Загрузить число в FPU
- Подсчитать длину целой части
- Умножить число на 10(18-<длина целой части>) (при этом лучше всего воспользоваться таблицей значений)
- сохранить BCD
- Если знаковый бит установлен вывести минус
- Вывести целую часть
- Вывести точку
- Вывести оставшиеся цифры
Однако для умножения на 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/