Start | Super Packer | Atari Graphics Studio | Graph2Font | Mads | MadPascal | Atari Zines | YouTube | http://madteam.atari8.info |
Charlie
Syzygy #3
Na początek parę słów tytułem wstępu. Pierwsza lekcja ukazała się podobno w pierwszym numerze Syzygy. Nie uważam się za jakiś autorytet w tej dziedzinie jednak zostałem poproszony przez redakcję o poprowadzenie tej rubryki. Mam nadzieję, że komuś się to moje kwaszenie przyda. Wszelkie źródłówki będą napisane w formacie Quick Assemblera i dołączone do zina w postaci plików z rozszerzeniem "ASM". Rubrykę tę powinny czytać osoby mające już jakieś pojęcie o assemblerze, dla początkujących może to być zupełna abstrakcja. - INTEGER -
Istnieje wiele sposobów reprezentacji liczb we wnętrzu komputera. Chciałbym jednak zająć się bliżej bardzo popularnym formatem zapisu dwubajtowych liczb całkowitych o nazwie integer. Jeżeli ktoś zajmował się programowaniem na PC, na pewno musiał się z nim zetknąć. Występuje on zarówno we wszelkich kompilatorach języka Pascal, C, a także w assemblerze.
1. Dodawanie i odejmowanie.Z tym nie ma żadnego problemu. Trzeba tylko pamiętać o znaczniku C procesora.
iadd clc lda inta adc intb sta intc lda inta+1 adc intb+1 sta intc+1 rts
isub sec lda inta sbc intb sta intc lda inta+1 sbc intb+1 sta intc+1 rts Uwidacznia się tu wielka zaleta takiego zapisu liczb całkowitych. Gdybyśmy znak zapisali w osobnym bajcie, wówczas musielibyśmy rozpatrywać możliwe przypadki: czy która z liczb jest ujemna, która jest większa itd. W oparciu o odejmowanie możemy zapisać procedurę negacji liczby. W końcu jest to odjęcie jej od zera. Poprzez negację rozumiem tu zmianę znaku liczby...
ineg sec ; -c := 0 - c lda #0 sbc intc sta intc lda #0 sbc intc+1 sta intc+1 rts Jeżeli mamy procedurę negującą, możemy zapisać wartość bezwzględną:
iabs lda intc+1 ; jeśli ujemna, bmi ineg ; to zmień znak rts W prosty sposób można również zapisać funkcję signum, zwracając w akumulatorze znak liczby. Jest ona zupełnie zbyteczna, ale dla zasady należy ją zapisać:
isgn lda intc+1 bpl isg1 lda #256-1 ; liczba ujemna rts isg1 beq isg2 isg3 lda #1 ;liczba dodatnia rts isg2 lda intc bne isg3 rts ; zero Funkcja ta przyjmuje trzy wartości:
Procedury te są bardzo proste, dlatego jeżeli zależy nam na prędkości, operacje należy umieścić bezpośredni w programie. Przykłady te ilustrują tylko, jak te procedury powinny wyglądać...
2. Mnożenie i dzielenie.Tutaj zaczynają się problemy. Można te operacje zaimplementować na wiele sposobów, ale należy pamiętać o następujących rzeczach:
Na początek proponuję napisać procedurę, która zadba o znak wynik i sprawdzi czy która z liczb jest równa zero. Aby uzyskać znak wyniku, wystarczy zEORować znaki czynników. isig lda #0 sta intc ; wyzerowanie sta intc+1 ; wyniku lda inta ora inta+1 ; czy a = 0? bne sg0 sta intb ; 0 mod b = 0! sta intb+1 sg1 pla ; wynik zerowy, pla ; wyjdź rts sg0 lda intb ora intb+1 ; czy b = 0? beq sg1 lda inta+1 eor intb+1 sta ints ; znak wyniku lda inta+1 ; a := abs( a ) bpl sg2 sec lda #0 sbc inta sta inta lda #0 sbc inta+1 sta inta+1 sg2 lda intb+1 ; b := abs( b ) bpl sg3 sec lda #0 sbc intb sta intb lda #0 sbc intb+1 sta intb+1 sg3 rts ; wyjście do nadrzędnej ;procedury...
Należy się tutaj parę wyjaśnień. Dla uproszczenia przyjmijmy, że dowolna liczba podzielona przez zero, daje w rezultacie zero. Jest to sprzeczne, ale człowiek myślący, przez zero i tak dzielił nie będzie. To uproszczenie pozwoli na to, że procedury tej będziemy mogli używać zarówno podczas mnożenia, ja i dzielenia. Jeśli któraś z liczb jest równa zero, wówczas procedura powraca do programu głównego z wyzerowanym wynikiem. Ponadto dba ona o znak wyniku, zapamiętując go w bajcie ints, następnie oblicza wartości bezwzględne z licz a i b, gdyż jest to konieczne, aby procedury mnożenia i dzielenia działały poprawnie.
Implementacja tego w assemblerze byłaby bardzo prosta, tym bardziej, że punkty 1-4 odwala już nasza procedura isig. Jednak algorytm ten jest zbyt wolny... Nie zawsze prosty, znaczy szybki. Należy poszukać bardziej efektywnej metody.
imul jsr isig ; zadbaj o znak ldx #0 ; wyniku ldy #0 _ml1 lsr intb+1 ; przesuwanie ror intb ; bitó w prawo bcc _ml2 ; czy pomnożyć txa ; przez 2n? clc adc inta ; dodanie do tax ; wyniku tya adc inta+1 tay _ml2 lda intb ; czy jeszcze ora intb+1 ; trzeba coś beq _ml3 ; dodać? asl inta ; a := a * 2 rol inta+1 jmp _ml1 _ml3 stx intc ; zapamiętaj sty intc+1 ; wynik lda ints ; jeśli znak "-" bmi ineg ; to wynik musi rts ; być ujemny!
Teraz nasza procedura jest już całkiem szybka. Jednak należy się parę wyjaśnień. Liczbę b przesuwamy w prawo. Jest to równoznaczne z dzieleniem przez 2, zaś resztę z tego dzielenia (0 lub 1) znajdziemy w znaczniku C procesora. A po co to w ogóle robimy? Ano po to, żeby zbadać, czy w zapisie dwójkowym bit o danym numerze jest zapalony. Równorzędnie z dzieleniem b, mnożymy a przez 2. Jeśli n-ty bit liczby b jest zapalony, wówczas do wyniku dodajemy liczbę a pomnożoną przez 2n, np.
Przypominam, iż w celu uproszczenia nie uwzględniamy błędu dzielenia przez 0. Darujmy sobie idiotoodporność...
idiv jsr isig ; zadbaj o znak ldx inta ldy inta+1 _dv1 sec txa sbc intb ; a := a - b tax tya sbc intb+1 tay bcc _dv2 inc intc ; c := c + 1 bne _dv1 inc intc+1 bne _dv1 ; (jmp) _dv2 stx inta sty inta+1 lda ints ; zadbaj o znak bmi ineg rts Prosto i zwięźle. Przy okazji możemy napisać procedurę obliczającą resztę z dzielenia.
imod lda inta+1 ; zapamiętaj php ; znak a jsr idiv ; podziel jsr iadd ; dodaj plp ; jeśli znak "-" bmi ineg ; to zaneguj rts ; wynik...
Oczywiście, jest ona zupełnie zbyteczna, pokazuje tylko jak to zrobić. Po wywołaniu procedury dzielenia wystarczy wywołać dodawanie, a wynikowi nadać znak dzielnej...
* * * Na dysku powinny znaleźć się również dwie inne źródłówki. Pierwsza z nich to 'NTSTR.ASM'
Moduł ten jest powiązany z 'INTOP.ASM i korzysta z zawartych w nim procedur. Ponadto wymaga zadeklarowania jednego słowa na stronie zerowej (ipnt).
Charlie |