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.
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.
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"
.
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:
; 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
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:
; 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
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:
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:
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.