Jak pisać programy w języku asembler pod Linuksem?

Część 7 - Porty, czyli łączność między procesorem a innymi urządzeniami

Nie zastanawialiście się kiedyś, jak procesor komunikuje się z tymi wszystkimi urządzeniami, które znajdują się w komputerze?
Teraz zajmiemy się właśnie sposobem, w jaki procesor uzyskuje dostęp do urządzeń zewnętrznych (zewnętrznych dla procesora, niekoniecznie tych znajdujących się poza obudową komputera).

Mimo że procesor może porozumiewać z urządzeniami przez wydzielone obszary RAM-u, to głównym sposobem komunikacji (gdy nie chcemy lub nie możemy używać sterowników) ciągle pozostają porty. Jeśli chcecie, możecie wykonać komendę cat /proc/ioports, która powie, które urządzenie zajmuje które porty.

Porty są to specjalne adresy, pod które procesor może wysyłać dane. Stanowią oddzielną strefę adresową (16-bitową, jak dalej zobaczymy, więc najwyższy teoretyczny numer portu wynosi 65535), choć czasami do niektórych portów można dostać się przez pamięć RAM. Są to porty mapowane do pamięci (memory-mapped), którymi nie będziemy się zajmować.

Lista przerwań Ralfa Brown'a (RBIL) zawiera plik ports.lst (który czasami trzeba osobno utworzyć - szczegóły w dokumentacji). W pliku tym znajdują się szczegóły dotyczące całkiem sporej liczby portów odpowiadającym różnym urządzeniom. I tak, mamy na przykład

No dobrze, wiemy co ma który port i tak dalej, ale jak z tego skorzystać?

Procesor posiada dwie instrukcje przeznaczone specjalnie do tego celu. Są to IN i OUT.
Ich podstawowa składnia wygląda tak:

	in al/ax/eax, numer_portu
	out numer_portu, al/ax/eax

Uwagi:

  1. Jeśli numer_portu jest większy niż 255, to w jego miejsce musimy użyć rejestru DX
  2. Do operacji na portach nie można używać innych rejestrów niż AL, AX lub EAX.
  3. Wczytane ilości bajtów zależą od rejestru, a ich pochodzenie - od rodzaju portu:
  4. Podobne uwagi mają zastosowane dla instrukcji OUT

Teraz byłaby dobra pora na jakiś przykład (mając na uwadze dobro swojego komputera, NIE URUCHAMIAJ PONIŻSZYCH KOMEND):

	in	al, 0	; pobierz bajt z portu 0
	out	60h, eax; wyślij 4 bajty na port 60h 

	mov	dx, 300	; 300 > 255, więc musimy użyć DX
	in	al, dx	; wczytaj 1 bajt z portu 300
	out	dx, ax	; wyślij 2 bajty na port 300 

Nie rozpisywałem się tutaj za bardzo, bo ciekawsze i bardziej użyteczne przykłady znajdują się w moich mini-kursach (programowanie diód na klawiaturze, programowanie głośniczka).

Jak już wspomniałem wcześniej, porty umożliwiają dostęp do wielu urządzeń. Jeśli więc chcesz poeksperymentować, nie wybieraj portów zajętych na przykład przez kontrolery dysków twardych, gdyż zabawa portami może prowadzić do utraty danych lub uszkodzenia sprzętu.
Dlatego właśnie w nowszych systemach operacyjnych (tych pracujących w trybie chronionym, jak na przykład Linux) dostęp do portów jest zabroniony dla zwykłych aplikacji (o prawa dostępu do portów trzeba prosić system operacyjny - zaraz zobaczymy, jak to zrobić).
Jak więc działają na przykład stare DOS-owe gry? Odpowiedź jest prosta: nie działają w trybie chronionym. Windows uruchamia je w trybie udającym tryb rzeczywisty (taki, w jakim pracuje DOS), co umożliwia im pełną kontrolę nad sprzętem.
Wszystkie programy, które dotąd pisaliśmy też uruchamiają się w tym samym trybie, więc mają swobodę w dostępie na przykład do głośniczka czy karty dźwiękowej. Co innego programy pisane w nowszych kompilatorach na przykład języka C - tutaj może już być problem. Ale na szczęście my nie musimy się tym martwić...

Jeszcze jeden ciekawy przykład - używanie CMOSu. CMOS ma 2 podstawowe porty: 70h, zwany portem adresu i 71h, zwany portem danych. Operacje są proste i składają się z 2 kroków:

  1. Na port 70h wyślij numer komórki (1 bajt), którą chcesz odczytać lub zmienić. Polecam plik cmos.lst z RBIL, zawierający szczegółowy opis komórek CMOS-u
  2. Na port 71h wyślij dane, jeśli chcesz zmienić komórkę lub z portu 71h odczytaj dane, jeśli chcesz odczytać komórkę

Oto przykład. Odczytamy tutaj czas w komputerze, a konkretnie - sekundy:

	mov	eax, 101	; funkcja systemowa "sys_ioperm":
	mov	ebx, 70h	; poczynając od portu 70h
	mov	ecx, 20		; tyle bajtów będziemy mogli wysłać/odebrać
	mov	edx, 71h	; końcowy numer portu
	int	80h		; niestety, musimy być rootem

	cmp	eax, 0		; sprawdzamy, czy błąd. Nie wiem,
				; co ta funkcja ma
				; zwracać, ale ten sposób zdaje
				; się działać

	jl	koniec		; jeśli wystąpił błąd, to zapis do
				; portów, do których nie mamy uprawnień,
				; zakończy się  "Segmentation fault"
				; ( "Naruszenie ochrony pamięci" )
	mov	al, 0
	out	70h, al

				; ustaw przerwę na milion nanosekund, czyli
				; jedną milisekundę
	mov	dword [ts1+timespec.tv_sec], 0
	mov	dword [ts1+timespec.tv_nsec], 1000000
; w FASMie:
;	mov	dword [ts1.tv_sec], 0
;	mov	dword [ts1.tv_nsec], 1000000

	mov	eax, 162	; sys_nanosleep
	mov	ebx, ts1	; adres struktury timespec
	mov	ecx, 0		; adres wynikowej struktury timespec
	int	80h		; wykonaj przerwę w programie

	in	al, 71h

    koniec:
    	; ...

; w FASMie:
;	segment readable writeable
section .data

; w FASMie:
;struc timespec
;{
;	.tv_sec:		rd 1
;	.tv_nsec:		rd 1
;}
;
;ts1: timespec

struc timespec
                .tv_sec:	resd 1
                .tv_nsec:	resd 1
endstruc

ts1 istruc timespec

Wszystko jasne, oprócz bloku z wywołaniem sys_nanosleep. Po co to komu, pytacie?
Przy współczesnych częstotliwościach procesorów, CMOS (jak z resztą i inne układy) może po prostu nie zdążyć z odpowiedzią na naszą prośbę, gdyż od chwili wysłania numeru komórki do chwili odczytania danych mija za mało czasu. Dlatego robimy sobie przerwę na kilkanaście taktów zegara procesora.
Kiedyś między operacjami na CMOSie zwykło się pisać jmp short $+2, co też oczywiście nie robiło nic, poza zajmowaniem czasu (to jest po prostu skok o 2 bajty do przodu od miejsca, gdzie zaczyna się ta dwubajtowa instrukcja, czyli skok do następnej instrukcji), ale ta operacja już nie trwa wystarczająco długo, aby ją dalej stosować.

Komunikacja z urządzeniami nie zawsze jednak musi wymagać uprawnień administratora i korzystania z funkcji sys_ioperm. Sporo rzeczy (na przykład z klawiaturą) można zrobić, korzystając z funkcji sys_ioctl.

W dzisiejszych czasach porty już nie są tak często używane, jak były kiedyś. Jest to spowodowane przede wszystkim wspomnianym trybem chronionym oraz tym, że wszystkie urządzenia mają już własne sterowniki (mające większe uprawnienia do manipulowania sprzętem), które zajmują się wszystkimi operacjami I/O. Programista musi jedynie uruchomić odpowiednią funkcję i niczym się nie przejmować.

Dawniej, portów używało się do sterowania grafiką czy wysyłania dźwięków przez głośniczek lub karty dźwiękowe. Teraz tym wszystkim zajmuje się za nas system operacyjny. Dzięki temu możemy się uchronić przed zniszczeniem sprzętu.

Mimo iż rola portów już nie jest taka duża, zdecydowałem się je omówić, gdyż po prostu czasami mogą się przydać. I nie będziecie zdziwieni, gdy ktoś pokaże wam kod z jakimiś dziwnymi instrukcjami IN i OUT...

Szczegóły dotyczące instrukcji dostępu do portów także znajdziecie, jak zwykle, u AMD i Intela.

Miłej zabawy.



Poprzednia część kursu (Alt+3)
Kolejna część kursu (Alt+4)
Spis treści off-line (Alt+1)
Spis treści on-line (Alt+2)
Ułatwienia dla niepełnosprawnych (Alt+0)



Ćwiczenia

  1. Zapoznaj się z opisem CMOSu i napisz program, który wyświetli bieżący czas w postaci gg:mm:ss (z dwukropkami). Pamiętaj o umieszczeniu opóźnień w swoim programie i o uprawnieniach.