Отправляет email-рассылки с помощью сервиса Sendsay

Assembler - Просто и Эффективно.

  Все выпуски  

Assembler - Просто и Эффективно. Глава# 12 - FPU


Assembler

Глава #12 - FPU

  FPU.
FPU (Floating Point Unit) используется для ускорения и упрощения вычислений с плавающей точкой.
Сопроцессор (другое название FPU) ориентирован на матиматические вычисления - в нем отсутсвуют операции с битами, зато расширен набор математических ф-ций: тригонометрические, логарифм и т.д.

До некоторых моделей 80486 FPU исполнялся ввиде отдельной микросхемы, устанавливаемой опционально. В некоторых 486 и во всех Pentium (и выше), FPU интегрирован в процессор, поэтому можно считать, что он присутствует в каждом более-менее современном компьютере. Начиная с Pentium Pro FPU имеет дополнительные команды, упрощающие сравнение чисел. В любом случае, если FPU недоступен, или неподдерживает какие-либо ф-ции, то возможна их программная эмуляция.

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

Программирование совсем старых (до 387) FPU несколько отличалось от последующих версий и рассматриваться не будет т.к это уже не актуально. С.м также Глава #5 - Команды Процессора
   -------------------------------

  Числа FPU
Сопроцессор поддерживает числа следующих форматов:
  • FP формат. Числа в формате с плавающей точкой, соответствуют стандарту IEEE 754. Могут быть следующих типов:
    НазваниеРазмер
    (в битах)
    Размер
    мантиссы
    Смещение
    порядка
    Диапозон
    нормализованных значений
    Single Precision
    Одинарная точность
    32231272-1262127
    Double Precision
    Двойная точность
    645210232-1022 21023
    Double Extended Precision
    Двойная расширенная точность
    806416 3832-16382 216383
    При загрузке числа, сопроцессор переводит его в формат двойной расширенной точности, но точность вычислений зависит от CW.PC

    Формат FP числа следующий: по смещению ноль находится мантисса, далее - экспонента+смещение порядка, и старший бит - знак (0-положительное, 1 - отрицательное). Размеры чисел указаны в таблице выше.
    32 и 64 битные числа не содержат первый бит мантиссы, он считается равным единице. Таким образом числа 32, 64 и 80 битного размера могут без искажения содержать 24, 53 и 64 битные числа

    Пример: переведем число 123,375 в FP формат
    Целая часть 123=1111011
    Дробная часть получается следующим образом:
    0.375 меньше 1/20
    0.375 больше 1/41
    0.375-1/4 равно 1/81
    Получим число 011 в двоичной системе. Т.е можно умножать числo на 2, записывая младший бит целого результата, пока число не ноль или не закончились свободные биты числа. Также, если количество свободных бит известно, можно умножить дробную часть на соответствующую степень двойки, не удаляя ведущие ноли.

    Получилось, что число 123,375 равно 1111011,011
    Умножим на такую степень двойки, чтобы получилось число вида 1,xxx: 1111011,011= 1,111011011*2 в степени 6

    Число 1,111011011 называется мантиссой, 6 - экспонентой, 1 - целая часть мантиссы.
    32 бита: 0100 0010 1111 0110 1100 0000 0000 0000
    64 бита: 0100 0000 0101 1110 1101 1000 0000 .... 0000
    80 бит: 0100 0000 0000 0101 1111 0110 1100 0000 .... 0000

    Для указания FP чисел в MASM используются типы real4, real8 и real10. Пример: AnyNumber real8 123.456
    Можно указывать данные типы директивами dd, dq и dt соответственно, но они не сообщат об ошибке, если число не FP (если число не содержит точки)

    Числа могут иметь специальные значения:
    • +0 и -0 : Все биты числа (кроме старшего у -0) равны нолю.
    • + и - Бесконечность: Все биты экспоненты установлены, мантисса вида 1,00..00
    • + и - Денормализованное число: Все биты экспоненты сброшены, мантисса вида 0,xxxx (не нулевая).
    • SNaN (сигнальное не-число): Все биты экспоненты установлены, мантисса вида 1,0xxxx (не нулевая)
    • QNaN (тихое не-число): Все биты экспоненты установлены, мантисса вида 1,1xxxx (не нулевая)
    • Неопределенность: Все биты экспоненты установлены, мантисса вида 1,10..00 ;Знаковый бит установлен.

    При операциях с SNaN возникает исключение #I (Если не установлен бит CR.IM). Причем, если SNaN real4 или real8 исключение происходит при загрузке, а если real10, то при математической операциии над ним, что позволяет использвать эти числа для разных методов отладки и тэстирования программы. Свободные биты мантиссы могут быть использованы для хранения дополнительной информации.

  • Целые знаковые числа. Обычные числа, описанные в Главе #2. Могут быть размером 16, 32 и 64 бита.
    64 битные числа имеют тип QWORD, и объявляются директивой dq. При выгрузке NaN'ов получается число, старший бит которого установлен, а остальные - сброшены.
  • Packed BCD (Binary-coded decimal) числа, размером 80 бит. Каждые 4 бита такого числа могут принимать значения от 0 до 9, бит #79 - знак, 78-72 не имеют значения. При выгрузке NaN'ов, получается число, 18 старших битов которого установлены, а остальные - сброшены.
    Пример: десятичное число -12345 в формате Packed BCD равно 80 00 00 00 00 00 00 01 23 45h. В памяти байты расположены наоборот.
    Эти числа имеют тип TBYTE, для объявления в программе используется директива dt
    Пример: десятичное число 123456789 можно объявить так: dt 123456789h
   -------------------------------

  Использование FPU
Для работы с FPU существует отдельная группа команд, название которых начинается с символа "f".
Сопроцессор работает паралельно CPU, и, поэтому, раньше (до 287) необходимо было перед многими командами FPU вставлять команду fwait, чтобы приостановить выполнение до окончания вычислений.
В последующих сопроцессорах эта команда встроена в инструкции FPU, поэтому ее использовать не нужно. Но остались также не ожидающие команды, имеющие приставку "n". Пример fsave/fnsave - одно и тоже действие, но первая команда вызовет исключение, если оно произошло до этого, но еще не вызывлось. На самом деле эти две команды - одни и теже, но перед fsave компилятор добавляет команду fwait.

Операндом команд является, как правило, один из регистров st(), или ячейка памяти. Другой операнд обычно подразумевается st(0). Комнады с операндом "память" могут иметь приставку "i", что означает операнд - обычное число, без приставки - в FP формате.
Большинство команд не работает с регистрами общего назначения. Для работы с числом из регистра общего назначения можно, например, использовать стэк:
Пример: сложение eax с ecx, используя FPU
Это именно пример использования FPU, для сложения двух регистров есть команда add.
 push eax
 fild dword ptr [esp]
 push ecx
 fiadd dword ptr [esp]
 pop eax   ;очистим стэк
 fistp dword ptr [esp]
 pop eax   ;eax= результат

Несмотря на то, что многие команды работают только с st(0), можно легко использовать число из другого FP регистра, поменяв значения регистров командой fxch.

Некоторые команды имеют суффикс "r", что означает назначение операции - второй операнд.
Также, если в команде присутствует суффикс "p", то происходит выталкивание числа из стэка сопроцессора.

Процедуры, работающие с FPU обычно придерживаются следующего соглашения:
На входе и выходе стэк сопроцессора пуст. Иногда для возврата FP числа используется st(0).
Можно использовать все регистры FPU, за исключением CW.

Команд FPU не много, и их легко отличить по первой букве "f", поэтому не будет дано их описание.
Для справки о командах, и получения другой разнообразной информации о процессоре, желательно загрузить документацию с сайта Intel. Последний раз она была по адресу:
http://www.intel.com/design/Pentium4/documentation.htm
Программирование процессоров Intel описано в документах из раздела "Manuals" ~10мб, они очень полезны для программиста на Assembler'е.
Также описание команд можно найти в пакете MASM32, и в комелекте с компилятором.
   -------------------------------

  Программная модель.
FPU имеет собственную среду, образ котрой можно получить, например, командой fnsave.
Формат контекста в памяти зависит от разрядности кода и операнда. Состоит из:
  • Control Word (CW) - слово управления или Control Register (CR) - регистр управления. Позволяет задать некоторые свойства работы сопроцессора.
    1514131211-109-876543210
          X RC PC     P
    M
    U
    M
    O
    M
    Z
    M
    D
    M
    I
    M
    Для записи слова управления обычно используется команда fldcw, для считывания - fstcw, fnstcw
    После команд finit/fninit и fsave/fnsave CW устанавливается равным 37Fh Значения бит слова управления:
    • 0-5 *M маскируют исключения сопроцессора. Если бит установлен, исключение не происходит.
      Об обработке исключений будет рассказано в следующих главах рассылки. Если обработчик исключения не установлен, то по происхождению ошибки (например, деления на ноль) "Программа выполнит ошибку и будет закрыта.". Эти биты чаще всего установлены.
      • #I - некорректная операция: #IS переполнение/антипереполнение стэка, #IA некорректная арифметическая операция
      • #D - Денормализованный операнд
      • #Z - Деление на ноль
      • #O - Переполнение числа
      • #U - Антипереполнение числа
      • #P - Округление
    • 8-9 PC - контроль точности: 00 -24 бита, 01 - зарезервированно, 10 - 53 бита и 11 - 64 бита.
      Чем меньше точность, тем больше скорость выполнения некоторых команд FPU
    • 10-11 RC - контроль направления округления. 00 - к ближайлему числу, 01 - к минус бесконечности, 10 - к плюс бесконечности, 11 - к нолю (усечение).
    • 12 X не используется, и присутсвует для совместимости 287.
  • Status Word (SW) - слово состояния
    151413-11109876543210
    B C
    3
    TOP C
    2
    C
    1
    C
    0
    E
    S
    S
    F
    P
    E
    U
    E
    O
    E
    Z
    E
    D
    E
    I
    E
    Значения бит слова состояния:
    • 0-5 *E устанавливаются при возникновении соответствующих исключений.
    • 6 SF устанавливается при ошибке работы со стэком
    • 7 ES устанавливается при возникновении немаскированного исключения.
    • 8,9,10,14 C* отображают результат операций сравнения и арифметических операций. Для организации ветвлений обычно использовалась последовательность команд fstsw ax / sahf, за которыми следовала команда условного перехода. Начиная с Pentium Pro, введены команды сравнения, непосредственно устанавливающие eflags. Их использовать предпочтительней.
    • 11-13 TOP указывает на вершину стэка сопроцессора.
    • 15 B не используется, оставлен для совместимости.
  • Tag Word (TW) - слово тегов Слово, каждые два бита которого отображают тип информации в регистрах R0-R7. Т.е биты #0-1 отображают состояние R0, #2-3 - R1 и т.д.
    Каждая пара бит может принимать следующие значения:
    00 - Действителен, 01 - Ноль, 10 - NaN, бесконечность, денормализованное или неподдерживаемое число, 11 - Пустой.
    TW модифицируется FPU автоматически, при восстановлении контекста используется только пустой / не пустой.
  • Контекст сопроцессора содержит также адрес последней выполненой инструкции и ее операнда (селектор и смещение в 32 битном режиме) и 11 бит последней команды (старшие 5 бит заполнены нолями, а у инструкции они равны 11011b)
  • R0-R7 8 десятибайтных регистров, образующих стэк FPU. Обращение к ним присходит по именам st(номер0-7) Номер относителен вершины стэка (SW.TOP).
Некотрые свойства FPU зависят от содержимого регистра CR0 и будут рассмотренны позже.
   -------------------------------

  Пример
Пример использования сопроцессора. Команды FPU используются в процедуре CalcXY, работу которой желательно посмотреть под отладчиком (с.м также Глава #8 - Отладка). Окно стэка сопроцессора отображается командой wf.
Также, данный пример практически дополняет предыдущую главу, показывая программирование графики, используя GDI.

Программа - часы. Можно двигать удерживая левую кнопку мыши, выход - щелчек правой. Алгоритм работы более-менее стандартный:
После создания окна и т.д установим таймер, который будет посылать сообщения WM_TIMER с заданым интервалом. Обработчик сообщения объявит недействительным содержимое окна, и WM_PAINT нарисует часы.
Как всегда, программа максимально проста, чтбы было легче в ней разобраться.
.486
 .model flat,stdcall
;используем inc и lib файлы из пакета masm32 (с.м главу #8)
include c:\masm32\include\windows.inc
include c:\masm32\include\kernel32.inc
includelib c:\masm32\lib\kernel32.lib
include c:\masm32\include\user32.inc
includelib c:\masm32\lib\user32.lib
include c:\masm32\include\gdi32.inc
includelib c:\masm32\lib\gdi32.lib

;Константы для настройки программы
ClockX=61
ClockY=61
ClockColor=0FFFFFFh
BorderColor=0903030h
HourColor=0FF4040h
MinuteColor=0FFh
SecondColor=0
;Разница радиуса с максимальным
HourRadius=15
MinuteRadius=12
SecondRadius=5

 .data
ColorArray dd HourColor,MinuteColor,SecondColor
RadiusArray dd HourRadius,MinuteRadius,SecondRadius

ProgramClassName db 'SmallClock',0 ;класс основонго окошка
WinTitle db 'Clock',0
WClass WNDCLASSEX 
MsgS MSG<>
DragCords dd ?
 .code
;Процедура для вычисления координат точки на окружности, 
; длинной MaxVal и определяемой прямоугольником EllipseRect
;Алгоритм работы:
; RadsLen=CurrentVal/MaxVal*2*PI
; XSize=EllppseRect.right-EllppseRect.left-1
; X=EllipseRect.left+XSize/2*(1+sin(RadsLen))
; YSize=EllipseRect.bottom-EllipseRect.top-1
; Y=EllipseRect.top+YSize/2*(1-cos(RadsLen))
;out: eax=x; edx=y
DivideBy2 real4 0.5 ;заменим деление на 2 умножением на 0.5.
 ;Это занчительно быстрее. Об оптимизации будет рассказано позже.
CalcXY proc EllipseRect:ptr RECT,CurrentVal:DWORD,MaxVal:DWORD
 mov ecx,[EllipseRect]
 assume ecx:ptr RECT

 fild [CurrentVal]
 fidiv dword ptr [MaxVal]
 fldpi
 fmulp st(1),st(0)
 fadd st(0),st(0)  ;st(0)=RadsLen
 fsincos

 fild [ecx].bottom
 fisub [ecx].top
 fld1
 fsub st(1),st(0)
 fsubrp st(2),st(0)
 fmulp st(1),st(0)
 fmul [DivideBy2]
 push edx
 fistp dword ptr [esp]
 pop edx
 add edx,[ecx].top  ;edx=y

 fild [ecx].right
 fisub [ecx].left
 fld1
 fsub st(1),st(0)
 faddp st(2),st(0)
 fmulp st(1),st(0)
 fmul [DivideBy2]
 push eax
 fistp dword ptr [esp]
 pop eax
 add eax,[ecx].left ;eax=x

 assume ecx:nothing
 ret
CalcXY endp

;Макрос для уменьшения RECT'а со всех сторон.
;параметры: Rect - указатель на структуру RECT
;   SubValue - значение, на которое нужно уменьшить RECT
SubRect macro Rect,SubValue
 add [Rect].left,SubValue
 add [Rect].top,SubValue
 sub [Rect].right,SubValue
 sub [Rect].bottom,SubValue
endm

start:
;зарегистрируем класс главного окна (см главу #7)

 mov edi,offset WClass
 assume edi:ptr WNDCLASSEX

 invoke GetModuleHandle,0
 mov [edi].hInstance,eax
 invoke LoadIcon,0,IDI_APPLICATION
 mov [edi].hIcon,eax
 mov [edi].hIconSm,eax
 invoke LoadCursor,0,IDC_ARROW
 mov [edi].hCursor,eax
 assume edi:nothing

 invoke RegisterClassEx,edi

;Получим размер экрана для расположения окна по центру.
 invoke GetSystemMetrics,SM_CXSCREEN
 sub eax,ClockX
 shr eax,1
 xchg eax,ebx
 invoke GetSystemMetrics,SM_CYSCREEN
 sub eax,ClockY
 shr eax,1
;создадим главное окно
 invoke CreateWindowEx,WS_EX_TOOLWINDOW or WS_EX_TOPMOST,\
  offset ProgramClassName,offset WinTitle,\
  WS_VISIBLE or WS_POPUP,ebx,eax,ClockX,ClockY,\
  0,0,[WClass.hInstance],0
 xchg eax,ebx
;установим таймер для периодической перерисовки часов
 invoke SetTimer,ebx,0,200,0
;создадим и установим регион для окна, чтобы окно было круглой формы.
 invoke CreateEllipticRgn,-1,-1,ClockX+2,ClockY+2
 invoke SetWindowRgn,ebx,eax,1

MsgLoop:
 invoke GetMessage,offset MsgS,0,0,0
 inc eax
 jz EndMsgLoop
 dec eax
 jz EndMsgLoop
 invoke TranslateMessage,offset MsgS
 invoke DispatchMessage,offset MsgS
 jmp MsgLoop
EndMsgLoop:
 invoke ExitProcess,0

MainWindowProc proc hwnd:dword,uMsg:dword,wParam:dword,lParam:dword
local ps:PAINTSTRUCT
local ClientRect:RECT
local MemDC:dword
local CurrentTime:SYSTEMTIME

 cmp [uMsg],WM_PAINT
 jnz NotWmPaint
;обработчик WM_PAINT
 invoke GetLocalTime,addr CurrentTime
 invoke GetClientRect,[hwnd],addr ClientRect

 invoke BeginPaint,[hwnd],addr ps
;создадим MemDC
 invoke CreateCompatibleDC,ps.hdc
 mov [MemDC],eax
 invoke CreateCompatibleBitmap,ps.hdc,ClientRect.right,ClientRect.bottom
 invoke SelectObject,[MemDC],eax
 push eax    ;хэндл предыдущего Bitmap

;нарисуем пустой циферблат
 invoke CreateSolidBrush,ClockColor
 invoke SelectObject,[MemDC],eax
 push eax    ;хэндл предыдущего Brush
 invoke CreatePen,PS_SOLID,3,BorderColor
 invoke SelectObject,[MemDC],eax
 push eax   ;хэндл предыдущего Pen
 invoke Ellipse,[MemDC],0,0,ClientRect.right,ClientRect.bottom

;цикл рисования стрелок
 push ebx
 xor ebx,ebx
DrawClockLines:
 mov eax,ebx
 xor al,11b
 invoke CreatePen,PS_SOLID,eax,dword ptr [ColorArray+ebx*4]
 invoke SelectObject,[MemDC],eax
 invoke DeleteObject,eax

 invoke GetClientRect,[hwnd],addr ClientRect
 mov eax,ClientRect.right
 shr eax,1
 mov ecx,ClientRect.bottom
 shr ecx,1
 invoke MoveToEx,[MemDC],eax,ecx,0
 mov eax,dword ptr [RadiusArray+ebx*4]
 SubRect ClientRect,eax

 movzx eax,word ptr [CurrentTime.wHour+ebx*2]
 or ebx,ebx
 jnz NotDrawHour
 lea ecx,[eax*4+eax]  ;*5
 movzx eax,word ptr [CurrentTime.wMinute]
 cdq
 push 12
 div dword ptr [esp]
 pop edx
 add eax,ecx
NotDrawHour:
 invoke CalcXY,addr ClientRect,eax,60
 invoke LineTo,[MemDC],eax,edx
 inc ebx
 cmp bl,3
 jnz DrawClockLines
 pop ebx

 invoke GetClientRect,[hwnd],addr ClientRect
 invoke CreatePen,PS_SOLID,1,BorderColor
 invoke SelectObject,[MemDC],eax
 invoke DeleteObject,eax

;нарисуем метки времени (каждые 5 минут)
 mov ecx,12
DrawTimeLabels:
 push ecx
 SubRect ClientRect,4
 invoke CalcXY,addr ClientRect,dword ptr [esp+4],12
 invoke MoveToEx,[MemDC],eax,edx,0
 SubRect ClientRect,-4
 invoke CalcXY,addr ClientRect,dword ptr [esp+4],12
 invoke LineTo,[MemDC],eax,edx
 pop ecx
 loop DrawTimeLabels

;скопируем MemDC в DC окна
 invoke BitBlt,ps.hdc,0,0,ClientRect.right,ClientRect.bottom,\
  [MemDC],0,0,SRCCOPY

;удалим созданные объекты
 push [MemDC]
 call SelectObject  ;2 pushed
 invoke DeleteObject,eax
 push [MemDC]
 call SelectObject  ;2 pushed
 invoke DeleteObject,eax
 push [MemDC]
 call SelectObject  ;2 pushed
 invoke DeleteObject,eax

 invoke DeleteDC,[MemDC]
 invoke EndPaint,[hwnd],addr ps
 xor eax,eax
 ret

NotWmPaint:
 cmp [uMsg],WM_TIMER
 jnz NotWmTimer
;обработчик таймера. Все окно в Update Region
 invoke InvalidateRect,[hwnd],0,0
 xor eax,eax
 ret
NotWmTimer:

;до WM_DESTROY - обработка сообщений от мыши
 cmp [uMsg],WM_LBUTTONDOWN
 jnz NotWmLButtonDown
 mov eax,[lParam]
 mov [DragCords],eax
 invoke SetCapture,[hwnd]
 jmp CallDefProc
NotWmLButtonDown:

 cmp [uMsg],WM_LBUTTONUP
 jnz NotWmLButtonUp
 invoke ReleaseCapture
 jmp CallDefProc
NotWmLButtonUp:

 cmp [uMsg],WM_MOUSEMOVE
 jnz NotWmMouseMove
 cmp [wParam],MK_LBUTTON
 jnz NotWmMouseMove
 push eax
 push eax
 invoke GetCursorPos,esp
 pop eax
 movzx ecx,word ptr [DragCords]
 sub eax,ecx
 pop edx
 movzx ecx,word ptr [DragCords+2]
 sub edx,ecx
 invoke SetWindowPos,[hwnd],0,eax,edx,0,0,SWP_NOSIZE
 jmp CallDefProc
NotWmMouseMove:
 
 cmp [uMsg],WM_RBUTTONUP
 jz ExitFromClock

;стандартная часть процедуры обработки сообщений с.м главу #7.
 cmp [uMsg],WM_DESTROY
 jnz NotWmDestroy
ExitFromClock:
 invoke PostQuitMessage,0
 xor eax,eax
 ret
NotWmDestroy:
CallDefProc:
 invoke DefWindowProc,[hwnd],[uMsg],[wParam],[lParam]
 ret

MainWindowProc endp
end start
   -------------------------------

 Содержание дальнейших выпусков зависит от вас. Будут разъясняться те темы, которые непонятны большинству читателей. Присылайте свои вопросы и предложения по адресу: asm32@nm.ru .
   -------------------------------


Автор рассылки Владимир Пронин. Любое коммерческое использование материалов рассылки без ведома и прямого согласия автора запрещено.
Письма присланные автору рассылки могут быть опубликованы целиком или частично без предварительного уведомления. Если вы не хотите, чтобы ваше письмо было опубликовано укажите это в начале письма.
Перепечатка материалов разрешена только с непосредственной ссылкой на asm32.nm.ru


В избранное