» Работа с дробными числами на ассемблере Assembler. . . Блог программистов


Блог программистов






200814 Ноя

Работа с дробными числами на ассемблере

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

   Сопроцессор имеет восемь 10-байтных регистров, организованных в кольцевой стек. Вершиной стека является регистр ST(0). Если в стек заносится какое-то значение, то регистры FPU «сдвигаются», по сути, меняется значение индекс, который задаёт какой сейчас регистр является вершиной стека (непонятно? Читайте далее). Так что ST(0) — всегда вершина, ST(1) — первое значение ниже вершины и т.д. до ST(7). Если речь идёт о ST(0), то цифру иногда опускают и пишут просто ST. В FASM регистры пишутся так: st0, st1, st2 и т.д. Также надо заботиться о том, чтобы этот стек не переполнился.
   Для каждого стекового регистра формируется 2-битный тег (признак), содержание которого соответствует определенному состоянию стекового регистра (00 — допустимое ненулевое число; 01 — нуль; 10 — специальные значения (не число); 11 — пустой регистр). Режим работы FPU и его состояние определяются содержанием регистров слова управления CW (Control Word) и слова состояния SW (Status Word).
   Слово состояния SW отражает общее состояние устройства FPU. Регистр слова состояния FPU, по сути, является аналогом регистра флагов центрального процессора.
• IE (бит 0) — наличие недействительной операции: деление на ноль, умножение на бесконечность, корень из отрицательного числа и т.д.;
• DE (бит 1)- фиксируется появление денормализованного операнда ;
• ZE (бит 2)- фиксируется попытка деления на ноль;
• ОЕ (бит 3)- переполнение, при котором результат операции выходит за пределы представления в формате получателя;
• UE (бит 4)- потеря значимости (антипереполнение) — результат не равен нулю, но слишком мал для представления в формате получателя;
• РЕ (бит 5)- неточный результат, результат нельзя точно представить в формате получателя или имеет место округление.
• SF (бит 6) — ошибки в работе стекового регистра: переполнение стека, извлечение из пустого стека. Переполнению стека соответствуют SF=l,Cl=t; изъятию из пустого стека — SF=1,C1=0;
• ES (бит 7) — устанавливается в 1 при возникновении любого особого случая;
• СО — СЗ (биты 8, 9, 10, 14) — являются битами кода условия, отмечают результаты команд сравнения, проверки и анализа. Основное назначение этих бит — определение условий перехода. Они похожи на арифметические флаги в регистре EFLAGS. Командой FSTSW АХ их значения можно занести в регистр АХ, а командой SAMF скопировать их из регистра АХ в биты флагов EFLAGS в следующем порядке: СО ->CF, С2 -> PF, СЗ ZF.
• ТОР (биты 11, 12, 13) — определяют номер регистра стека, который в текущий момент является вершиной стека. При сдвиге регистров сопроцессора изменяется только значение этого поля.
• В (бит 15) — отмечает занятость выполнением операции или наличие не обслуженного запроса (сохранен только для совместимости с сопроцессором 8087).
Например, если предыдущей командой была предпринята попытка деления на ноль, то будет выставлен флаг ZE.
   Слово управления CW содержит поля управления точностью и округлением, а также набор флагов (первые шесть бит), каждый из которых является маской отдельных исключений сопроцессора. Маскировка любого исключения проводится занесением в соответствующее поле маски еденицы.
• IM … РМ (биты 0 — 5) — маски исключений (совпадают с соответствующими разрядами слова состояния);
• RC — управление округлением в соответствии со стандартом IEEE -754 (00 — округление до ближайшего или четного; 01 — округление вниз к -<»; 10 — округление вверх к +°°; 11 — усечение к 0);
• PC — управление точностью согласно стандарта IEEE-754. (00 — 24 бита (одинарная); 01 — зарезервирован; 10-53 бита (двойная); 11-64 бита (расширенная)). По умолчанию вводится режим максимальной точности.
Не указанные поля не используются. Если, к примеру, флаг ZM выставлен, то даже при попытке деления на ноль, программа продолжит своё выполнение и результат будет записан в виде бесконечности и флаг ZE в регистре слова состояние будет выставлен, с помощью этого флага программа сможет узнать об ошибке.
   Так, у нас в каждом «рабочем» регистре сопроцессора по 10 байт. В итоге у нас 80 бит, объявлять этот тип в FASM надо так tbyte (или tword). Кроме 80-битных чисел с расширенной точностью сопроцессор может обрабатывать также 64-битные числа с двойной точностью и 32-битные с одинарной. Точность здесь — это характеристика формата записи вещественного числа: extended, double, single соответственно. Но сам FPU работает только с форматом расширенной точности, при загрузке чисел с двойной и одинарной точностью он автоматически конвертирует в формат расширенной точности, а выгрузке значений он конвертирует их в нужный нам формат.
   Все команды сопроцессора начинаются с буквы “f”, удобно, не правда ли? Итак, приступим к разбору команд процессора. Кстати, насчёт компилятора FASM, он поддерживает максимум команд сопроцессора, а если вы всё-таки нашли неподдерживаемую этим компилятором команду, то качните последнюю версию FASM’a.
   Команда fwait (или просто wait) – инструкция синхронизации работы CPU и FPU, заставляет процессор проверить наличие подвешенных немаскируемых прерываний и разобраться с ними до продолжения работы.
    Команды finit и fninit устанавливают контекст FPU в его значение по умолчанию. finit перед совершением операции проверяет на подвешенные немаскируемые прерывания, fninit этого не делает.
   Команды fclex и fnclex очищают флаги исключений FPU в слове статуса FPU. fclex перед совершением операции проверяет на подвешенные немаскируемые прерыванияя, fnclex этого не делает
   Команда fld задвигает значение с плавающей точкой в стек FPU. Операндом может быть 32-битное, 64-битное или 80-битное значение в памяти или регистр FPU, его значение загружается в st0 и автоматически конвертируется в формат расширенной точности. Например, fld dword [edx] загружает значение одинарной точности из памяти, или например fld tword [number]. Команда fild, операндом которой может быть 16-битное, 32-битное или 64-битное значение в памяти загружает целое число из памяти в регистр FPU с автоматическим конвертированием его в формат расширенной точности. Для загрузки дробных чисел пользуемся командой fld, а для загрузки целых чисел из памяти пользуемся командой fild.
   Часто бывает нужно загрузить ноль, еденицу или число Пи в FPU. fld1, fldz, fldl2t, fldl2e, fldpi, fldlg2 и fldln2 загружают часто используемые константы в стек регистров FPU. Эти константы: единица, ноль, двоичный логарифм 10, двоичный логарифм числа e, число «пи», десятичный логарифм двойки и натуральный логарифм двойки соответственно.
   При загрузке какого-либо значения в стек сопроцессора, то все значения стека сдвигаются, а наше значение загружается в st0. Если все регистры сопроцессора и так содержат какие-либо значения, то при загрузке нового значения стек будет переполнен, и нужное нам значение не будет загружено (загрузится ерунда).
   Команда fst копирует значение из вершины стека сопроцессора в операнд-адресат, которым может быть 32-битное или 64-битное расположение в памяти или другой регистр FPU. fstp совершает ту же операцию, но далее выталкивает значение из стека, освобождая ST(0). fstp может сохранять ещё и 80-битное значение в память. fst и fstp занимаются вещественными числами. Для целых чисел существуют аналогичные команды fist и fistp, операндами которых могут быть 32-битные и 64-битные расположения в памяти. Если в регистре содержится не целое значение, то оно будет округлено перед сохранением. Метод округления зависит от поля RC в регистре слова управления. Также может быть полезна инструкция fisttp, которая просто отбрасывает дробную часть перед сохранением (она также выталкивает содержимое st0.
   Команда fadd складывает операнд-источник и операнд-адресат и сохраняет сумму в адресате. Операндом-адресатом всегда должен быть регистр FPU, если источник — это расположение в памяти, то адресат это регистр ST(0) и нужно указать только источник. Если обоими операндами являются регистры FPU, то одним из них должен быть st0. Операндом в памяти может быть 32-битное или 64-битное значение. Например, fadd qword [edx] прибавляет значение двойной точности к ST(0), fadd st2,st0 складывает st0 и st2 и сохраняет результат в st2, а fadd st0,st2 складывает st0 и st2 и сохраняет результат в st0. Команда faddp складывает операнд-источник и операнд-адресат, сохраняет сумму в адресате и далее выталкивает значение из стека, освобождая st0. Операндом-адресатом должен быть регистр FPU, а операндом-источником – st0. Если операнды не указаны, то в качестве операнда-адресата используется st1. Например, просто написав faddp, мы прибавляем st0 к st1 и выдвигаем вершину стека, а команда faddp st2,st0 прибавляет st0 к st2 и выдвигает вершину стека.
   Команда fiadd конвертирует целочисленный операнд-источник в расширенный формат с плавающей точкой и прибавляет его к st0. Операндом должно быть 32-битное или 64-битное расположение в памяти. Например, fiadd word [number] прибавляет целочисленное слово к st0.
   Команды fsub, fsubr, fmul, fdiv и fdivr похожи на fadd, имеют такие же правила для операндов и различаются только в совершаемых вычислениях. fsub вычитает операнд-источник из операнда-адресата, fsubr вычитает операнд-адресат из операнда-источника, fmul перемножает источник и адресат, fdiv делит операнд-адресат на операнд-источник, fdivr делит операнд-источник на операнд-адресат. fsubp, fsubrp, fmulp, fdivp и fdivrp совершают те же операции и выталкивают вершину стека регистров, правила для операнда такие же, как с инструкцией faddp. fisub, fisubr, fimul, fidivr и fidivr совершают те же операции после преобразования целочисленного операнда-источника в формат с плавающей точкой, они имеют такие же правила для операндов, как и инструкция fiadd.
   Команда fxch меняет местами содержимое регистра st0 и другого регистра FPU. Операндом должен служить регистр FPU, а если он не указан, меняются местами регистры st0 и st1.
   Команды fcomi, fcomip, fucomi, fucomip сравнивают st0 с другим регистром FPU и ставят, в зависимости от результатов, флаги ZF, PF и CF. fcomip и fucomip ещё выталкивают вершину стека после завершения сравнения. Отличие команды fcomi от fucomi заключается в том что команда генерирует исключение в случае если один из операндов NaN. Аналогично для команд fcomip и fucomip.
Семейство команд fcmovxx сравнивает операнды и перемещает операнд-источник в операнд-адресат, если условие выполняется, операнд-адресат всегда регистр st0. Например, fcmovb st0,st2 переводит st2 в st0 если st0 меньше чем st2. Таблица «окончаний» для команды fcmovxx.

Мнемоника Тестируемое условие	Описание
b         CF = 1                меньше
e         ZF = 1                равно
be        CF or ZF = 1          меньше или равно
u         PF = 1                ненормализованное
nb        CF = 0                не меньше
ne        ZF = 0                не равно
nbe       CF and ZF = 0         не меньше и не равно
nu        PF = 0                нормализованное


   Команда ffree освобождает регистр, не являющийся вершиной стека. Операндом является регистр FPU.
   Команды fincstp и fdecstp вращают кольцевой стек FPU на единицу, прибавляя или отнимая единицу от поля STP слова статуса FPU. У этих инструкций нет операндов. fdecstp вращает так: st7->st0->st1->st2…, fincstp – наоборот.
   Команда fsqrt вычисляет квадратный корень из значения в регистре ST(0), fsin вычисляет синус этого значения, fcos вычисляет его косинус, fchs дополняет его знаковый бит, fabs очищает знак, чтобы создать абсолютное значение, frndint округляет до ближайшего целого значения, зависящего от текущего режима округления. f2xm1 вычисляет экспоненциальное значение 2 в степени ST(0) и вычитает из результата 1.0 (2^x-1), значение в st0 должно лежать в пределах от -1.0 до +1.0. Все вышеперецисленные инструкции сохраняют значение в ST(0) и не имеют операндов.
   Команды fstsw и fnstsw сохраняют текущее значение слова статуса FPU в указанном месте. Операндом-адресатом может быть либо 16 бит в памяти, либо регистр AX. fstsw перед сохранением слова проверяет на подвешенные немаскируемые прерывания, fnstsw этого не делает.
   Команды fstcw и fnstcw сохраняют текущее значение управляющего слова FPU в указанном месте в памяти. fstcw перед сохранением слова проверяет на подвешенные немаскируемые прерывания, fnstcw этого не делает. fldcw загружает операнд в управляющее слово FPU. Операндом должно быть 16-битное расположение в памяти.

   Итак, с основными командами разобрались. Теперь к радости и счастью студентов напишем две процедуры, которые преобразовывают строку в число с плавающей точкой и наоборот.
   Сначала функция, которая преобразовывает строку в число. Общий принцип процедуры такой: начала загружаем в st0 ноль в цикле, смотрим очередной символ в строке, преобразовываем этот символ в число и прибавляем к st0, предварительно умножив его на 10. После всего проделанного смотрим, сколько у нас символов после запятой и делим st0 на 10 столько раз, сколько у нас символов после запятой.

STR_to_FLOAT:
; converting string to float value
;IN
;   ESI – указатель на строку
;   EDI – указатель на переменную, в которую надо сохранить значение

    decimal_separator equ '.'

    pushad
    finit  

    xor ebp, ebp
    cmp byte [esi], '-'
    jnz @f
    inc esi
    inc ebp       
   @@:
    xchg edi, esi
    call GetZSLength ;получаем длину строки
    xchg edi, esi


    mov ecx, eax
    mov edx, eax

    mov ebx, 10

    fldz
    xor eax, eax
    push eax  ; [esp] - temp value

   .repeat:
    cmp byte [esi], '9'
    ja .error
    cmp byte [esi], '0'
    jnb @f
    cmp byte [esi], decimal_separator
    jnz .error
    jmp .continue
   @@:
    mov [esp], ebx
    fimul dword [esp]

    mov al, byte [esi]
    sub al, '0'
    mov [esp], eax
    fiadd dword [esp]

   .continue:
    dec ecx
    inc esi
    cmp ecx, 0
    jnz .repeat
   .endrepeat:


    xchg esi, edi
    mov al, decimal_separator
    mov ecx, edx
    sub edi, edx
    repnz scasb  ; в ecx количество символов после запятой

    cmp ecx, 0
    jz .end
    mov dword [esp], 10
   .rep:
    fidiv dword [esp]
    loop .rep
   .end:

    xor eax, eax
    cmp ebp, eax
    jz @f
    fld1
    fld1
    fld1
    faddp st1, st0 ; st0 = 2, st1 = 1
    fsubp st1, st0 ; st0 = -1
    fmulp st1, st0
   @@:
    fstp qword [esi]  ; <------ saving float value
   .error:
    pop eax ; delete temp value
    popad
    ret

Точность получаемого значения можно изменить, просто изменив директиву qword (на dword или tword) в строке, которую я пометил стрелкой.
Чуть не забыл, процедура GetZSLength

GetZSLength:
; get zero-string length
;IN
;       EDI ZS offset
;OUT
;       EAX ZS length

	push ecx
	push esi
	push edi

	cld
	xor   al, al
	mov ecx, 0FFFFFFFFh
	mov esi, edi
	repne scasb
	sub edi, esi
	mov eax, edi
	dec eax

	pop edi
	pop esi
	pop ecx
	ret


   Теперь напишем процедуру преобразования числа в строку. Общий принцип её работы такой: сначала нормализуем число, т.е. делаем его меньше единицы, в цикле деля его на 10 пока она не станет меньше нуля, заодно мерим, сколько у нас символов ДО запятой. После чего в цикле умножаем число на 10 и сохраняем его как целое число, отбросив дробную часть, после чего полученное число преобразовываем с символ, прибавив к нему 30h. Кстати, надо не забыть вычесть из st0 полученную целую часть. Делаем эту операцию столько раз, сколько у нас символов ДО запятой. После чего добавляем к строке символ разделитель. Дальше производим аналогичную операцию, но только столько раз чему у нас равна точность результата (количество символов после запятой).

FLOAT_to_STR:
;converting float value to ZS
;IN
;       EAX – указатель на переменную
;       EDX – точность, количество символов после запятой
;       ESI – указатель на буфер со строкой
    plus_one  equ 0031h; '1',0
    zero equ 0030h; '0',0

    pushad
    finit

    fld qword [eax]  ;<--- st0 = float value
    fldz
    fcomip st1	     ;
    jz .zero	     ; if value = 0
    jb @f

    xor ebx, ebx
    dec ebx
    push ebx
    fild dword [esp]
    pop ebx
    fxch  ; xchg st0, st1
	  ; st0 = float value
    fmul st0, st1      ; st0 = -st0
    mov byte [esi], '-'
    inc esi
   @@:

    fld1  ; st0 = 1

    fcomip st1
    jz .one
    jb .normalize
    jmp .translate


   .normalize:
    xor ecx, ecx
    mov eax, 0.1
    push eax

    fld1
    fxch
   .rep1:

    fmul dword [esp]
    inc ecx

    fcomi st1
    jb @f
    jmp .rep1
   @@:
    pop eax

   .translate:
    xchg edx, ecx; edx = digit count before spot
    add ecx, edx ; ecx = digit count before + after spot

    mov eax, 10
    push eax
    fild dword [esp] ; st0 = 10
		     ; in dword [esp] temp value  !!!!!!
    fxch	     ; st0 = float value
		     ; st1 = 10

   .rep2:

    fmul st0, st1
    fld1
    fcmovne st0, st1
    fisttp dword [esp]; trunc st0 and pop it to dword [esp]
    mov al, byte [esp];

    fild dword [esp]  ; st0 = current truncuated digit

    fsubp st1, st0

    add al, 30h ; al from number to char
    mov byte [esi], al
    inc esi
    dec edx
    cmp edx,0
    jnz @f
    mov byte [esi], '.'
    inc esi
   @@:
    loop .rep2

    pop eax
    jmp .end
   .zero:
    mov word [esi], zero
    jmp .end
   .one:
    mov word [esi],plus_one
    jmp .end
   .error:

   .end:
    popad
    ret


   Оба вышеприведённых примера не претендуют на звание самого оптимизированного, лучшего и быстродействующего. Впрочем и сам алгоритм не самый лучший. Это всего лишь примеры. Есть очень много исходников подобных преобразований. Также при программировании под Windows можно воспользоваться функцией sprinf из библиотеки С++, коей является библиотека msvcrt.dll и прочие с похожим названием. Так же можно воспользоваться библиотеками FASMLIB и FPULIB. Также помимо набора инструкций непосредственно самого FPU, есть наборы команд MMX, 3DNow! и SSE, SSE2, SSE3 и т.д. Но это уже совсем другая история.

Комментарии

  1. сергій
    10 мая, 2009 | 19:27

    дякую

  2. Treant
    29 мая, 2009 | 22:17

    Спасибо, почитаю

  3. Оркадий
    19 августа, 2009 | 17:06

    едИница пишется через «и», а так — спасибо — все понятно стало.

  4. Хранитель Пути
    1 июня, 2010 | 18:15

    Спасибо большое автору за обстоятельный и понятный материал!!!

  5. Дмитро
    29 октября, 2014 | 18:18

    Спасибі велике !! Дуже дякую.