Pisanie własnych bibliotek w języku asembler

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ć:

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:


(przeskocz przykładowy moduł biblioteki)
; 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:


(przeskocz moduł 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 3
	out  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:

  1. 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).

  2. Deklaracja segmentu

    Ż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.

  3. assume

    Mówimy kompilatorowi, że rejestr CS będzie wskazywał na ten segment

  4. Gwiazdki lub inne elementy oddzielające (tu usunięte)

    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.

  5. Deklaracja procedury (wcześniej zadeklarowanej jako publiczna)

    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).

  6. To, czego procedura oczekuje i to, co zwraca.

    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.

  7. Dobrą techniką programowania jest deklaracja stałych w stylu 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.

  8. 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?

  9. Sprawdzanie warunków wejścia, czy są prawidłowe. Zawsze należy wszystko przewidzieć.

  10. Kod procedury. Z punktu widzenia tego artykułu jego treść jest dla nas nieistotna.

  11. Punkt(y) wyjścia

    Procedura może mieć dowolnie wiele punktów wyjścia. Tutaj zastosowano dwa, dla dwóch różnych sytuacji:

    1. parametr był dobry, procedura zakończyła się bez błędów
    2. parametr był zły, zwróć informację o błędzie

  12. Koniec procedury, segmentu i pliku źródłowego. Słowo end nie 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:

  1. NASM
  2. Alink
  3. Lib (LLIB, a nie ten z pakietu Borlanda czy Microsoft-u):
    www.dunfield.com/downloads.htm (szukaj SKLIB31.ZIP)
    www2.inf.fh-rhein-sieg.de/~skaise2a/ska/sources.html
    Jeśli tam go nie ma, to poszukajcie na stronach FreeDOS-a

Kopia mojej biblioteki powinna znajdować się na stronach, gdzie znaleźliście ten kurs.

Miłej zabawy.



Spis treści off-line (Alt+1)
Spis treści on-line (Alt+2)
Ułatwienia dla niepełnosprawnych (Alt+0)