Pewnie zdarzyło się już wam usłyszeć o kimś innym:
Ależ on(a) jest świetnym(ą) programistą(ką)! Nawet pisze własne biblioteki!
Pokażę teraz, że nie jest to trudne, nie ważne jak przerażającym się to może wydawać.
Osoby, które przeczytają ten artykuł i zdobędą troszkę wprawy będą mogły mówić:
Phi, a co to za filozofia pisać własne biblioteki!
Zacznijmy więc od pytania: co powinno się znaleźć w takiej bibliotece?
Mogą to być:
po prostu mogą się przydać
Co to zaś jest to owa biblioteka
?
Jest to plik (najczęściej z rozszerzeniem .lib), na który składa
się skompilowany kod, a więc na przykład pliki .obj. Biblioteka eksportuje na zewnątrz nazwy
procedur w niej zawartych, aby linker wiedział, jaki adres podać programowi, który chce
skorzystać z takiej procedury.
Będę w tym artykule używał składni i linii poleceń Turbo Assemblera (TASMa) firmy Borland z linkerem TLink i bibliotekarzem TLib oraz NASMa (Netwide Assembler) i FASMa (Flat Assembler z linkerem ALink i darmowym bibliotekarzem znalezionym w Internecie (patrz linki na dole strony).
Napiszmy więc jakiś prosty kod źródłowy. Oto on:
; wersja TASM public _graj_dzwiek biblioteka_dzwiek segment byte public "bibl" assume cs:biblioteka_dzwiek _graj_dzwiek proc far ; wejście: BX = żądana częstotliwość dźwięku w Hz, co najmniej 19 ; CX:DX = czas trwania dźwięku w mikrosekundach ; ; wyjście: CF = 0 - wykonane bez błędów ; CF = 1 - błąd: BX za mały czasomierz equ 40h ;numer portu programowalnego czasomierza klawiatura equ 60h ;numer portu kontrolera klawiatury pushf ; zachowujemy modyfikowane rejestry push ax push dx push si cmp bx,19 ;najniższa możliwa częstotliwość to ok. 18Hz jb _graj_blad in al,klawiatura+1 ; port B kontrolera klawiatury or al,3 ; ustawiamy bity: 0 i 1 - włączamy głośnik i ; bramkę od licznika nr. 2 czasomierza ; do głośnika out klawiatura+1,al mov si,dx ;zachowujemy DX mov dx,12h mov ax,34ddh div bx ;AX = 1193181 / częstotliwość, DX=reszta mov dl,al ;zachowujemy młodszy bajt dzielnika ; częstotliwości mov al,0b6h out czasomierz+3,al ;wysyłamy komendę: ; (bity 7-6) wybieramy licznik nr. 2, ; (bity 5-4) będziemy pisać najpierw bity 0-7 ; potem bity 8-15 ;(bity 3-1) tryb 3:generator fali kwadratowej ; (bit 0) licznik binarny 16-bitowy mov al,dl ; odzyskujemy młodszy bajt out czasomierz+2,al ; port licznika nr. 2 i bity 0-7 dzielnika ; częstotliwości mov al,ah out czasomierz+2,al ; bity 8-15 mov dx,si ;odzyskujemy DX _graj_pauza: mov ah,86h int 15h ; pauza o długości CX:DX mikrosekund jnc _graj_juz dec dx sbb cx,0 ; w razie błędu zmniejszamy CX:DX jmp short _graj_pauza _graj_juz: in al,klawiatura+1 and al,not 3 ; zerujemy bity: 0 i 1 - wyłączamy głośnik ; i bramkę out klawiatura+1,al pop si ; przywracamy rejestry pop dx pop ax popf clc ; brak błędu retf _graj_blad: pop si ; przywracamy rejestry pop dx pop ax popf stc ; błąd retf _graj_dzwiek endp biblioteka_dzwiek ends end
Teraz ten sam kod w składni NASMa/FASMa:
; wersja NASM global _graj_dzwiek ; w FASMie: ; format COFF ; use16 ; PUBLIC _graj_dzwiek segment biblioteka_dzwiek ; FASM: section ".text" code _graj_dzwiek: ; wejście: BX = żądana częstotliwość dźwięku w Hz, co najmniej 19 ; CX:DX = czas trwania dźwięku w mikrosekundach ; ; wyjście: CF = 0 - wykonane bez błędów ; CF = 1 - błąd: BX za mały czasomierz equ 40h ;numer portu programowalnego czasomierza klawiatura equ 60h ;numer portu kontrolera klawiatury pushf push ax push dx push si cmp bx,19 ;najniższa możliwa częstotliwość to ok. 18Hz jb _graj_blad in al,klawiatura+1 ; port B kontrolera klawiatury or al,3 ; ustawiamy bity: 0 i 1 - włączamy głośnik i ; bramkę od licznika nr. 2 czasomierza ; do głośnika out klawiatura+1,al mov si,dx ;zachowujemy DX mov dx,12h mov ax,34ddh div bx ;AX = 1193181 / częstotliwość, DX=reszta mov dl,al ;zachowujemy młodszy bajt dzielnika ; częstotliwości mov al,0b6h out czasomierz+3,al ;wysyłamy komendę: ; (bity 7-6) wybieramy licznik nr. 2, ; (bity 5-4) będziemy pisać najpierw bity 0-7 ; potem bity 8-15 ;(bity 3-1) tryb 3:generator fali kwadratowej ; (bit 0) licznik binarny 16-bitowy mov al,dl ; odzyskujemy młodszy bajt out czasomierz+2,al ; port licznika nr. 2 i bity 0-7 dzielnika ; częstotliwości mov al,ah out czasomierz+2,al ; bity 8-15 mov dx,si ;odzyskujemy DX _graj_pauza: mov ah,86h int 15h ; pauza o długości CX:DX mikrosekund jnc _graj_juz dec dx sbb cx,0 ; w razie błędu zmniejszamy CX:DX jmp short _graj_pauza _graj_juz: in al,klawiatura+1 and al,~3 ; zerujemy bity: 0 i 1 - wyłączamy głośnik ; i bramkę ; w FASMie:AND AL, not 3out klawiatura+1,al pop si pop dx pop ax popf clc retf _graj_blad: pop si pop dx pop ax popf stc retf
Jest to moja procedura wytwarzająca dźwięk w głośniczku (patrz artykuł o programowaniu głośniczka). Trochę tego jest, co? Ale jest tu dużo spraw, które można omówić.
Zacznijmy więc po kolei:
public... / global...
Funkcje, które mają być widoczne na zewnątrz
tego pliku, a więc możliwe do użycia przez
inne programy, muszą być zadeklarowane jako public
(TASM/FASM)
(w NASMie: global
). To jest na wszelki wypadek
.
Niektóre kompilatory domyślnie traktują wszystkie symbole jako publiczne, inne nie.
Jeśli w programie będziemy chcieli korzystać z takiej funkcji, należy ją zadeklarować
jako extrn
(TASM/FASM) lub extern
(NASM).
Żaden przyzwoity kompilator nie pozwoli na pisanie kodu poza jakimkolwiek segmentem
(no chyba, że domyślnie zakłada segment kodu, jak NASM).
Normalnie, w zwykłych
programach, np typu .com, rolę tę pełni dyrektywa
.code
.
assume
Mówimy kompilatorowi, że rejestr CS będzie wskazywał na ten segment
Mogą się wydawać śmieszne lub niepotrzebne, ale gdy liczba procedur w pliku zaczyna sięgać 10-20, to NAPRAWDĘ zwiększają czytelność kodu, oddzielając procedury, dane itd.
Znak podkreślenia z przodu jest tylko po to, by w razie czego nie był identyczny z jakąś
etykietą w programie korzystającym z biblioteki. Deklaracja jest typu far
, żeby
zmienić CS na bieżący segment i uniknąć kłopotów z 64kB limitem długości skoku
(konkretnie to są to +/- 32kB).
Jedną procedurę łatwo zapamiętać. Ale co zrobić, gdy jest ich już 100? Analizować kod każdej, aby sprawdzić, co robi, bo akurat szukamy takiej jednej....? No przecież nie.
equ(lub #define w C).
Zamiast nic nie znaczącej liczby można użyć wiele znaczącego zwrotu, co przyda się dalej w kodzie. I nie kosztuje to ani jednego bajtu. Oczywiście, ukrywa to część kodu (tutaj: numery portów), ale w razie potrzeby zmienia się tę wielkość tylko w 1 miejscu, a nie w 20.
push...
Poza wartościami zwracanymi nic nie może być zmienione! Nieprzyjemnym uczuciem byłoby spędzenie kilku godzin przy odpluskwianiu (debugowaniu) programu tylko dlatego, że ktoś zapomniał zachować jakiegoś rejestru, prawda?
Procedura może mieć dowolnie wiele punktów wyjścia. Tutaj zastosowano dwa, dla dwóch różnych sytuacji:
endnie zawsze jest konieczne, ale nie zaszkodzi. Wskazuje, gdzie należy skończyć przetwarzanie pliku.
Mamy więc już plik źródłowy. Co z nim zrobić? Skompilować, oczywiście!
tasm naszplik.asm /z /m
(/z - wypisz linię, w której wystąpił błąd
/m - pozwól na wielokrotne przejścia przez plik)
lub, dla NASMa:
nasm -f obj -o naszplik.obj naszplik.asm
(-f - określ format pliku wyjściowego
-o - określ nazwę pliku wyjściowego)
lub, dla FASMa:
fasm naszplik.asm naszplik.obj
Mamy już plik naszplik.obj
. W pewnym sensie on już jest biblioteką! I można go używać w innych
programach, na przykład w pliku program2.asm
mamy:
... extrn _graj_dzwiek:far ; NASM: extern _graj_dzwiek ; FASM: extrn _graj_dzwiek ... ... mov bx,440 mov cx,0fh mov dx,4240h call far ptr _graj_dzwiek ; NASM: call far _graj_dzwiek ; FASM: call _graj_dzwiek ...
I możemy teraz zrobić:
tasm program2.asm /z /m tlink program2.obj naszplik.obj
lub, dla NASMa:
nasm -f obj -o program2.obj program2.asm alink program2.obj naszplik.obj -c- -oEXE -m-
lub, dla FASMa:
fasm program2.asm program2.obj alink program2.obj naszplik.obj -c- -oEXE -m-
a linker zajmie się wszystkim za nas - utworzy plik program2.exe
, zawierający także
naszplik.obj
. Jaka z tego korzyść? Plik program2.asm
może będzie zmieniany w przyszłości
wiele razy, ale naszplik.asm/.obj
będzie ciągle taki sam. A w razie chęci zmiany procedury
_graj_dzwiek wystarczy ją zmienić w 1 pliku i tylko jego ponownie skompilować, bez potrzeby
wprowadzania tej samej zmiany w kilkunastu innych programach. Te programy wystarczy
tylko ponownie skompilować z nową biblioteką
, bez jakichkolwiek zmian kodu.
No dobra, ale co z plikami .lib?
Otóż są one odpowiednio połączonymi plikami .obj. I wszystko działa tak samo.
Ale jak to zrobić?
Służą do tego specjalne programy, nazywane librarian
(bibliotekarz). W pakiecie TASMa
znajduje się program tlib.exe
. Jego właśnie użyjemy (działa jak LLIB i wszystko
robimy tak samo). Pliki .obj, które chcemy połączyć w
bibliotekę można podawać na linii poleceń, ale jest to męczące, nawet jeśli napisze się plik
wsadowy tlib.bat
uruchamiający tlib. My skorzystamy z innego rozwiązania.
Programowi można na linii poleceń podać, aby komendy czytał z jakiegoś pliku. I to właśnie
zrobimy. Piszemy plik tlib.bat
:
tlib.exe naszabibl.lib @lib.txt
i plik lib.txt
(zwykłym edytorem tekstu):
+- ..\obj\pisz.obj & +- ..\obj\wej.obj & +- ..\obj\procesor.obj & +- ..\obj\losowe.obj & +- ..\obj\f_pisz.obj & +- ..\obj\dzwiek.obj & +- ..\obj\f_wej.obj & +- ..\obj\fn_pisz.obj & +- ..\obj\fn_wej.obj
(użyłem tutaj nazw modułów, które składają się na moją bibliotekę).
+-
oznacza zamień w pliku dany moduł
&
oznacza sprawdzaj jeszcze w kolejnej linijce
Przy pierwszym tworzeniu można użyć +
zamiast +-
, aby uniknąć ostrzeżeń o uprzedniej
nieobecności danego modułu w bibliotece.
Teraz uruchamiamy już tylko tlib.bat
a w razie potrzeby zmieniamy tylko lib.txt
.
Gdzie zdobyć narzędzia:
Kopia mojej biblioteki powinna znajdować się na stronach, gdzie znaleźliście ten kurs.
Miłej zabawy.