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 | |||
NaN | 1 | ||
Normal finite number | 1 | ||
Infinity | 1 | 1 | |
Zero | 1 | ||
Empty | 1 | 1 | |
Denormal number | 1 | 1 |
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.