Menu Zamknij

Sprawdzanie czy wynik jest NaN w NASMie

Podczas pisania zadania na laborki z programowania niskopoziomowego z powodu nieutrzymania konwencji C i zostawienia po sobie bałaganu na stosie FPU wyskoczyła mi wartość „-nan” czyli „Not a Number”. Postanowiłem bliżej przyjrzeć się możliwościom niskopoziomowej detekcji tego stanu w logice aplikacji (choć rozwiązaniem problemu z zadaniem było wyczyszczenie stosu a nie pisanie mijaka 😉 ).
W tym artykule zakładam że Czytelnik wie jak działa FPU i co to jest ST0, ST1 itd.

Problem wejściowy – mamy funkcję, która jest podatna na wystąpienie stanu NaN – dla celów artykułu przyjąłem zwykłe dzielenie. Mamy też kod w C, który służy tylko jako ułatwiacz wypisywania wartości na ekran. Jest też skrypt kompilujący i uruchamiający – zabrany wprost z mojego środowiska na laboratorium – oczekuje on plików NAZWA.c i NAZWA.asm i parametru wiersza poleceń NAZWA.

gcc -m32 -o $1_c.o -c $1.c &&
nasm -felf32 -o $1_a.o $1.asm &&
gcc -m32 -o $1 $1_a.o $1_c.o &&
./$1
#include <stdio.h>
extern int funkcja(double a, double b, double* c);

int main()
{
  double a=0.0, b=0.0, c;
  int statusOK = funkcja(a,b,&c);
  if (statusOK){
    printf("f(%f,%f)=%f\n", a,b,c);
  }
  else{
    printf("NaN catched!\n");
  }
  return 0;
}
segment .text
global funkcja
funkcja:

push ebp
mov ebp, esp

%define a qword [ebp+8]
%define b qword [ebp+16]
%define c dword [ebp+24]

mov eax, c
fld a       ; a
fld b       ; b,a
fdivp st1   ; b/a
fstp qword [eax]

mov eax, 1 ; always OK 🙂

fstp
mov esp, ebp
pop ebp

ret

W kodzie NASMowym linia 21 na razie zawsze zakłada że NaNa nie było. Na koniec będzie już lepiej 🙂

Pośród licznych rozkazów z FPU jest dostępny FXAM (Float eXAMine), który bada ST0 i ustawia odpowiednio flagi FPU CS0,CS1,CS2,CS3 (gdzie CS1 to bit znaku wartości z ST0) a pozostałe jak niżej:

Class C3 C2 C0
Unsupported 0 0 0
NaN 0 0 1
Normal finite number 0 1 0
Infinity 0 1 1
Zero 1 0 0
Empty 1 0 1
Denormal number 1 1 0

Tak ustawione flagi ściągamy do rejestru AH rozkazem FSTSW (Store Floating-Point Status Word), a potem przerzucamy do flag samego CPU rozkazem SAHF (Store AH into Flags) mapując jak niżej. Teraz rzecz jasna możemy wykonywać zwykłe skoki warunkowe (Jxx) choć w nieco niezwykłych warunkach bowiem nadpisaliśmy flagi – normalnie tym zajmuje się np. instrukcja CMP.

Flaga FPU Flaga CPU Rozkaz skoku Jxx Uwagi
C0 CF JC / JNC carry flag
C1 nie jest przenoszona
C2 PF JP=JNP / JPE=JPO parity flag (dwie konwencje Jxx)
C3 ZF JZ / JNZ zero flag

Tworząc kombinacje ifów możemy wyłapać NaN. Warto zwrócić uwagę, że można użyć tych flag do zwykłych porównań liczb (JG,JB itp.) ale zmiennoprzecinkowych. Ale dzisiaj nie o tym.

Można jednak uprościć program pomijając krok z SAHF (chociaż skoro już tak się zgłębiamy to warto wiedzieć o możliwościach tego rozkazu – dlatego nie pominąłem szczegółów) i sprawdzając sam rejestr AH po odczycie FSTSW.

SF:ZF:xx:AF:xx:PF:xx:CF

A więc NaN będzie odpowiadał pseudoregexowi: *0***0*1 – czyli XOR na 01000100 (*->0, reszta negowana) a potem OR na 10111010 (*->1, reszta na zera). Potem można zaNOTować wynik i użyć JZ.
Co lepsze? Tak czy inaczej kod będzie mało czytelny (jak chyba wszystko w assemblerze) więc pytanie czy potrzebujemy obsłużyć wszystkie stany z FXAM czy tylko jeden – jeśli jeden to 3 operacje bitowe i jeden skok warunkowy wydają się lepsze od etykiet na wszystkie kombinacje 3 stanów logicznych. Tak czy inaczej użycie któregokolwiek z tych rozwiązań bez kilku linijek komentarza to samobójstwo, albo umyślne zabójstwo naszego następcy…

Ja proponuję wersję bez SAHF jako prostszą realizację tytułowego problemu (nadpisujemy linię 21 z funkcja.asm):

fxam 
fstsw ax
sahf 

mov bh,01000100b
xor ah,bh
mov bh,10111010b
or  ah,bh
not ah
cmp ah,0
jz nan

ok:
mov eax, 1
jmp koniec

nan:
mov eax, 0
jmp koniec

koniec:

 

Pozostaje jedynie pytanie o znaczenie stanów innych niż NaN, normalna liczba skończona, nieskończoność i zero. Z pomocą przychodzi specyfikacja IEEE754 definiująca zapis liczb zmiennoprzecinkowych.

  • Denormal number to liczby mniejsze od epsilona maszynowego a więc mniejsze niż najmniejsza reprezentowalna liczba w danej arytmetyce – ich ślad pozostaje po różnych operacjach których wynik bliski jest zeru.
  • Unsupported  to stan niezgodny ze specyfikacją IEEE754 – niezgodny w tym sensie że żadna kombinacja bitów go nie spowoduje.
  • Empty to już stan samego FPU – oznacza że ten element (ST0) nie został jeszcze zapełniony lub został zwolniony.

Dodaj komentarz