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.