Bezpośredni dostęp do ekranu pod Linuksem

Jeśli chodzi o wyświetlanie informacji na ekranie, nie jesteśmy ograniczeni tylko do pisania w miejscu, gdzie akurat znajduje się kursor. Na pewno widzieliście jakiś program, który mimo iż miał tekstowy interfejs, to jednak pisał po ekranie, gdzie mu się podobało. Tym właśnie się teraz zajmiemy.



Pisanie z wykorzystaniem sekwencji kontrolnych terminala


(przeskocz sekwencje kontrolne)

Każdy program terminala ma inne sekwencje kontrolne i jeśli chcecie pisać programy, które będą działać na każdym terminalu, zainteresujcie się biblioteką ncurses. Tutaj opiszę tylko kilka sekwencji standardowego terminala xterm.

Pierwsza sprawa: co to właściwie jest znak kontrolny (sekwencja kontrolna)?
Jest to specjalny ciąg znaków określających zachowanie się terminala. Kilka już na pewno znacie: BEL (dźwięk), CR/LF (przechodzenie do nowej linii), TAB (tabulator). Teraz dojdą jeszcze dwa: zmiana koloru tekstu i tła oraz przechodzenie do wyznaczonej pozycji na ekranie.

Korzystałem z pliku xterm_controls.txt. Możecie skorzystać także z tego pliku lub z informacji na stronie podręcznika - man 4 console_codes.


Kolorowanie tekstu


(przeskocz kolorowanie)

Sekwencja kontrolna odpowiedzialna za kolor tekstu i tła wygląda tak:
ESC[(atr);(lit);(tło)m,
gdzie:

Na przykład, aby napisać coś na czerwono i przywrócić oryginalne kolory konsoli, należy normalnie (czyli przy użyciu int 80h z EAX=4, EBX=1, ECX=adres, EDX=długość) wyświetlić taki oto ciąg znaków:

1bh, "[0;31;40m Napis", 1bh, "[0;37;40m".

Ten ostatni ciąg przywraca domyślne kolory terminala (szarobiały na czarnym tle). Jeśli używacie terminala używającego innego zestawu kolorów niż szarobiały na czarnym tle, możecie wstawić własne wartości, tak samo jak dla zwykłych napisów - terminal zapamięta ustawienia. Możecie też spróbować takiej sekwencji:

1bh, "[0;31;40m Napis", 1bh, "[0;39;49m".

Wartości 39 i 49 przywracają domyślne kolory, odpowiednio dla znaków i tła.
Można też spróbować przywrócenia domyślnych wartości wszystkich atrybutów (nie tylko kolorów) bez ustawiania nowych wartości:

1bh, "[0;31;40m Napis", 1bh, "[0m".


Zmiana bieżącej pozycji kursora


(przeskocz teorię)

Sekwencja kontrolna odpowiedzialna za ustalanie pozycji kursora wygląda tak:
ESC [ w ; k H,
gdzie:

Na przykład, jeśli chcemy coś napisać w dziesiątym wierszu dziesiątej kolumny, należy normalnie (czyli przy użyciu int 80h z EAX=4, EBX=1, ECX=adres, EDX=długość) wyświetlić ciąg znaków:

1bh, "[10;10HNapis"


A oto obiecany program do rysowania ramek:


(przeskocz program)
; Rysowanie okienek z ramka
;
; Autor: Bogdan D.
;
; nasm -O999 -o ramki.o -f elf ramki.asm
; ld -s -o ramki ramki.o

section .text
global _start


_start:
	mov	eax, 4
	mov	ebx, 1
	mov	ecx, czysc
	mov	edx, czysc_dl
	int	80h			; wyświetlamy sekwencję,
					; która wyczyści ekran

	mov	ax, (36<<8)+44		; kolor znaków, kolor tła:
					; żółty na niebieskim
	mov	bx, 1			; kolumna Lewa-Górna (L-G)
	mov	cx, 1			; wiersz L-G
	mov	si, 9			; kolumna Prawa-Dolna (P-D)
	mov	bp, 9			; wiersz P-D
	call	rysuj_okienko

	mov	ax, (37<<8)+40		; biały na czarnym
	mov	bx, 10
	mov	cx, 10
	mov	si, 20
	mov	bp, 16
	call	rysuj_okienko

	mov	eax, 4
	mov	ebx, 1
	mov	ecx, nwln
	mov	edx, 1
	int	80h			; wyświetlamy znak przejścia
					; do nowej linii

	mov	eax, 1
	xor	ebx, ebx
	int	80h			; wyjście z programu


rysuj_okienko:

; wejście:
;
;  AH = atrybut znaku (kolor)
;  AL = kolor tła
;  BX = kolumna lewego górnego rogu
;  CX = wiersz lewego górnego rogu
;  SI = kolumna prawego dolnego rogu
;  BP = wiersz prawego dolnego rogu
;
; wyjście:
;  nic

; podwójne ramki ASCII
;r_p	equ	0bah			; prawa boczna
;r_pg	equ	0bbh			; prawa górna (narożnik)
;r_pd	equ	0bch			; prawa dolna

;r_g	equ	0cdh			; górna
;r_d	equ	r_g			; dolna

;r_l	equ	r_p			; lewa boczna
;r_lg	equ	0c9h			; lewa górna
;r_ld	equ	0c8h			; lewa dolna


r_p	equ	"|"			; prawa boczna
r_pg	equ	"\"			; prawa górna (narożnik)
r_pd	equ	"/"			; prawa dolna

r_g	equ	"="			; górna
r_d	equ	r_g			; dolna

r_l	equ	r_p			; lewa boczna
r_lg	equ	"/"			; lewa górna
r_ld	equ	"\"			; lewa dolna

spacja	equ	20h


	push	bx
	push	cx

	mov	dl, r_lg
	call	znak			; rysujemy lewy górny narożnik

	push	bx
	mov	dl, r_g			; będziemy rysować górną krawędź

					;dopóki BX<SI, rysuj górną krawędź
.rysuj_gora:
	inc	bx
	cmp	bx, si
	je	.dalej
	call	znak
	jmp	short .rysuj_gora

.dalej:
	mov	dl, r_pg
	call	znak			; rysujemy prawy górny narożnik
	pop	bx
	push	bx

					; rysujemy środek
					;dopóki CX<BP, rysuj wnętrze ramki
.rysuj_srodek:
	inc	cx
	cmp	cx, bp
	je	.ostatni

	mov	dl, r_l
	call	znak			; zaczynamy od lewego brzegu ramki

	push	bx
	mov	dl, spacja		; w środku będą spacje
.rysuj_srodek2:
	inc	bx
	cmp	bx, si		; dopóki BX<SI, rysuj wnętrze (spacje)
	je	.dalej2
	call	znak
	jmp	short .rysuj_srodek2

.dalej2:
	mov	dl, r_p
	call	znak			; rysujemy prawy brzeg
	pop	bx

	jmp	short .rysuj_srodek

.ostatni:
	mov	dl, r_ld
	call	znak			; rysujemy lewy dolny narożnik
	pop	bx

	mov	dl, r_d		; będziemy rysować dolną krawędź ramki
.rysuj_dol:
	inc	bx
	cmp	bx, si			;dopóki BX<SI, rysuj dolną krawędź
	je	.dalej3
	call	znak
	jmp	short .rysuj_dol

.dalej3:
	mov	dl, r_pd
	call	znak			; rysujemy prawy dolny narożnik

	pop	cx
	pop	bx

	ret


znak:

;  AH = atrybut znaku (kolor)
;  AL = kolor tła
;  BX = kolumna znaku
;  CX = wiersz znaku
;  DL = znak

	push	eax
	push	ebx
	push	ecx
	push	edx

	push	ax
	mov	dh, 10
	shr	ax, 8			; AX = kolor znaku
	div	dh			; AL = AL/10, AH = AL mod 10
	add	ax, "00"		; do ilorazu i reszty dodajemy
					; kod ASCII cyfry zero
	mov	[fg], ax		; do [fg] zapisujemy numer
					; koloru znaku

	pop	ax
	and	ax, 0FFh		; AX = kolor tła
	div	dh			; dzielimy przez 10
	add	ax, "00"
	mov	[bg], ax

	mov	ax, bx			; AX = kolumna znaku
	and	ax, 0FFh
	div	dh			; dzielimy przez 10
	add	ax, "00"
	mov	[kolumna], ax

	mov	ax, cx			; AX = wiersz znaku
	and	ax, 0FFh
	div	dh			; dzielimy przez 10
	add	ax, "00"
	mov	[wiersz], ax

	mov	[znaczek], dl		; zapisujemy, jaki znak
					; mamy wyświetlić


	mov	eax, 4
	mov	ebx, 1
	mov	ecx, pozycja
	mov	edx, napis_dl
	int	80h			; wyświetlamy napis wraz z
					; przejściem na odpowiednią pozycję

	pop	edx
	pop	ecx
	pop	ebx
	pop	eax

	ret


section .data

ESC		equ	1Bh

pozycja		db	ESC, "["	; sekwencja zmiany pozycji kursora
wiersz		db	"00;"
kolumna		db	"00H"
napis		db	ESC, "["	; sekwencja zmiany koloru
atr		db	"0;"
fg		db	"00;"
bg		db	"00m"
znaczek		db	"x"		; znak, który będziemy wyświetlać
napis_dl	equ	$ - pozycja

czysc		db	ESC, "[2J"	; sekwencja czyszcząca cały ekran
czysc_dl	equ	$ - czysc

nwln		db	10

Pisanie z wykorzystaniem urządzeń znakowych /dev/vcsaN


(przeskocz vcsa)

Innym sposobem na poruszanie się po ekranie jest zapis do specjalnych urządzeń znakowych - plików /dev/vcsaN (możliwe, że potrzebne będą uprawnienia roota).

Na stronach podręcznika man vcsa (a konkretnie to w przykładowym programie) widać, że format tych plików jest dość prosty - na początku są 4 bajty, odpowiadające: liczbie wierszy, liczbie kolumn (bo przecież mogą być różne rozdzielczości) oraz pozycji x i y kursora. Potem idą kolejno znaki widoczne na ekranie (od lewego górnego rogu wzdłuż wierszy) i ich atrybuty. Atrybuty te są takie same, jak w kursie dla DOSa i podobnie jak tam, starsze 4 bity oznaczają kolor tła, a młodsze - kolor znaku.

Teraz widzicie, że to nic trudnego - wystarczy otworzyć plik, odczytać wymiary ekranu i zapisywać odpowiednie bajty na odpowiednich pozycjach (używając funkcji poruszania się po pliku lub, po zmapowaniu pliku do pamięci, po prostu pisać po pamięci).

Oto przykładowy program:


(przeskocz program z vcsa)
; Program bezpośrednio zapisujący do pliku konsoli
;
; Autor: Bogdan D., bogdandr MAŁPKA op KROPKA pl
;
; kompilacja:
;
; nasm -O999 -f elf -o konsola.o konsola.asm
; ld -s -o konsola konsola.o


%idefine	sys_exit		1
%idefine	sys_read		3
%idefine	sys_write		4
%idefine	sys_open		5
%idefine	sys_close		6
%idefine	sys_lseek		19
%define         SEEK_SET		0
%define		O_RDWR			02o

; pozycja, pod którą coć wyświetlimy
%define		nasz_wiersz		10
%define		nasza_kolumna		10

section .text

global _start

_start:
	mov	eax, sys_open		; otwieranie pliku
	mov	ebx, plik		; nazwa pliku
	mov	ecx, O_RDWR		; odczyt i zapis
	mov	edx, 600q		; odczyt i zapis dla użytkownika
	int	80h			; otwieramy plik

	cmp	eax, 0
	jl	.koniec

	mov	ebx, eax		; uchwyt do pliku

	mov	eax, sys_read		; czytanie z pliku (najpierw
					; atrybuty konsoli)
	mov	ecx, konsola		; dokąd czytać
	mov	edx, 4			; ile czytać
	int	80h


	mov	eax, sys_lseek		; przejście na właściwa pozycję

	movzx	ecx, byte [l_kolumn]
	imul	ecx, nasz_wiersz
	add	ecx, nasza_kolumna	;ECX=wiersz*długość wiersza+kolumna

	shl	ecx, 1			; ECX *= 2, bo na ekranie są: bajt
					; znaku i bajt atrybutu
	add	ecx, 4			; +4, bo będziemy szli
					; od początku pliku

	mov	edx, SEEK_SET		; od początku pliku
	int	80h

	mov	eax, sys_write		; pisanie do pliku
	mov	ecx, znak		; co zapisać
	mov	edx, 2			; ile zapisać
	int	80h

	mov	eax, sys_close		; zamknięcie pliku
	int	80h

	xor	eax, eax		; EAX = 0 = bez błędu

.koniec:
	mov	ebx, eax
	mov	eax, sys_exit
	int	80h			;wyjście z kodem zero lub z błędem,
					; który był przy otwarciu pliku


section .data

plik	db	"/dev/vcsa1", 0	; plik pierwszej konsoli tekstowej

					; atrybuty czytanej konsoli:
konsola:
l_wierszy	db	0
l_kolumn	db	0
kursor_x	db	0
kursor_y	db	0

				; znak z atrybutem, który wyświetlimy:
znak		db	"*"
atrybut		db	43h		; błękit na czerwonym

Pisanie z wykorzystaniem mapowania pamięci


(przeskocz mapowanie pamięci)

Jeszcze jednym sposobem na pisanie po ekranie jest zapisywanie bezpośrednio do pamięci trybu tekstowego. Pamięć ta znajduje się w segmencie B800, co odpowiada liniowemu adresowi B8000, licząc od adresu 0. Oczywiście system, ze względów bezpieczeństwa, nie pozwoli nam bezpośrednio pisać pod ten adres, więc musimy sobie poradzić w inny sposób. Sposób ten polega na otwarciu specjalnego pliku urządzenia, który symbolizuje całą pamięć w komputerze - /dev/mem. Na większości systemów otwarcie tego pliku wymaga uprawnień administratora.

Po otwarciu pliku mamy dwie możliwości. Pierwsza to poruszać się po nim funkcjami do zmiany pozycji w pliku, oraz odczytywać i zapisywać funkcjami odczytu i zapisu danych z i do pliku. Może to być powolne, ale sposób jest. Druga możliwość to zmapować plik do pamięci, po czym korzystać z niego jak ze zwykłej tablicy. Tę możliwość opiszę teraz szczegółowo.

Otwieranie pliku odbywa się za pomocą tradycyjnego wywołania:

	mov	eax, 5		; sys_open
	mov	ebx, pamiec	; adres nazwy pliku "/dev/mem", 0
	mov	ecx, 2		; O_RDWR, zapis i odczyt
	mov	edx, 666o	; pełne prawa
	int	80h
	...
	pamiec		db	"/dev/mem", 0

Drugim krokiem jest zmapowanie naszego otwartego pliku do pamięci. Odbywa się to za pomocą funkcji systemowej sys_mmap2. Przyjmuje ona 6 argumentów:

  1. EBX = adres, pod jaki chcielibyśmy zmapować plik. Najlepiej podać zero, wtedy system sam wybierze dogodny adres
  2. ECX = długość mapowanego obszaru pliku, w bajtach. Podamy to 100000h, by na pewno objąć obszar zaczynający się B8000 i długości 4000 bajtów (tyle, ile trzeba na jeden ekran w trybie tekstowym, na znaki i ich atrybuty)
  3. EDX = tryb dostępu do zmapowanej pamięci. Jeśli chcemy odczyt i zapis, podamy tutaj PROT_READ=1 + PROT_WRITE=2
  4. ESI = tryb współdzielenia zmapowanej pamięci. Podamy tu MAP_SHARED=1 (współdzielona, nie prywatna)
  5. EDI = deskryptor otwartego pliku, który chcemy zmapować
  6. EBP = adres początkowy w pliku, od którego mapować. Adres ten jest podawany w jednostkach strony systemowej, której wielkość może być różna na różnych systemach. Najłatwiej podać tu zero, a do adresów dodawać potem B8000

Po pomyślnym wykonaniu, system zwróci nam w EAX adres zmapowanego obszaru pamięci, którego możemy używać (w przypadku błędu otrzymujemy wartość od -4096 do -1 włącznie). Przykładowe wywołanie wygląda więc tak:

	mov	eax, 192		; sys_mmap2
	xor	ebx, ebx		; jądro wybierze adres
	mov	ecx, 100000h		; długość mapowanego obszaru
	mov	edx, 3			; PROT_READ | PROT_WRITE, możliwość
					; zapisu i odczytu
	mov	esi, 1			; MAP_SHARED - tryb współdzielenia
	mov	edi, [deskryptor]	; deskryptor pliku pamięci, otrzymany
					; z sys_open w poprzednim kroku
	mov	ebp, 0			; adres początkowy w pliku
	int	80h

Teraz wystarczy już korzystać z otrzymanego wskaźnika, na przykład:

	mov	byte [eax+0b8000h], 'A'

Ekran w trybie tekstowym składa się z 80*25=2000 znaków, a każdy z nich ma po sobie bajt argumentu, mówiący o kolorze znaku i tła:

b8000 - znak 1, w lewym górnym rogu
b8001 - atrybut znaku 1
b8002 - znak 2, znajdujący się o 1 pozycję w prawo od znaku 1
b8003 - atrybut znaku 2
i tak dalej

Czym zaś jest atrybut?
Jest to bajt mówiący o kolorze danego znaku i kolorze tła dla tego znaku. Bity w tym bajcie oznaczają:

Wartości kolorów:
Czarny - 0, niebieski - 1, zielony - 2, błękitny - 3, czerwony - 4, różowy - 5, brązowy - 6, jasnoszary (ten standardowy) - 7, ciemnoszary - 8, jasnoniebieski - 9, jasnozielony - 10, jasnobłękitny - 11, jasnoczerwony - 12, jasnoróżowy - 13, żółty - 14, biały - 15.

Zmiany, które zapiszemy w pamięci, mogą jednak nie od razu pojawić się w pliku (czyli na ekranie w tym przypadku). Aby wymusić fizyczny zapis danych, korzysta się z funkcji sys_msync. Przyjmuje ona 3 argumenty:

  1. EBX = adres początku danych do synchronizacji
  2. ECX = liczba bajtów do zsynchronizowania
  3. EDX = 0 lub zORowane flagi: MS_ASYNC=1 (wykonaj asynchronicznie), MS_INVALIDATE=2 (unieważnij obszar po zapisaniu), MS_SYNC (wykonaj synchronicznie)

Przykładowe wywołanie wygląda więc tak:

	mov	eax, 144		; sys_msync
	mov	ebx, 0b8000h		; adres startowy
	mov	ecx, 4000		; ile zsynchronizować
	mov	edx, 0			; flagi
	int	80h

Po zakończeniu pracy z plikiem, możemy go odmapować:

	mov	eax, 91			; sys_munmap
	mov	ebx, [wskaznik]		; wskaźnik otrzymany z sys_mmap2
	mov	ecx, 100000h		; liczba bajtów
	int	80h

i zamknąć:

	mov	eax, 6			; sys_close
	mov	ebx, [deskryptor]	; deskryptor pliku "/dev/mem"
	int	80h

Jak widać, mapowanie plików do pamięci jest wygodne, gdyż nie trzeba ciągle skakać po pliku funkcją sys_lseek i wykonywać kosztownych czasowo wywołań innych funkcji systemowych. Warto więc się z tym zaznajomić. Należy jednak pamiętać, że nie wszystkie pliki czy urządzenia dają się zmapować do pamięci - nie należy wtedy zamykać swojego programu z błędem, lecz korzystać z tradycyjnego interfejsu funkcji plikowych.


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