Pamięć komputera jest rzeczą bardzo ważną, o czym już chyba wspominaliśmy. Jest to miejsce, gdzie przechowywane są zmienne, różne adresy itp. Kiedyś istniały komputery bez pamięci, lecz byli to potomkowie znanego Eniac'a czyli olbrzymie klocki o niewielkiej mocy obliczeniowej. Nieco później komputerom dano pamięć o pojemności 640kB. Była to wtedy tak olbrzymia rezerwa, że nikt nie przypuszczał, że kiedyś będzie większe zapotrzebowanie. Dziś liczymy pamięć w MB lecz już wcześniej, kiedy magiczna bariera pamięci konwencjonalnej została przekroczona i dążyła do 1MB, pojawił się problem adresowania pamięci. Można byłoby to robić po prostu za pomocą liczebników porządkowych, czyli po kolei numerować jej komórki od zera i tak np. dla 1MB ostatnia komórka miała by numer FFFFFh. Rozwiązanie logiczne i jednocześnie strasznie niewygodne (gdyby np. czas nie dzielił się na godziny, minuty, sekundy itp. to również byłoby to rozwiązanie strasznie niewygodne:). Dlatego właśnie postanowiono, że pamięć komputera będzie dzielić się na mniejsze jednostki -
segmenty. Każdy segment ma wielkość FFFFh bajtów i jest przesunięty o 16B względem poprzedniego:
SEGMENT1
16B|SEGMENT2
16B|16B|SEGMENT3
\------/
FFFF
W ten sposób, każdy bajt w pamięci otrzymał swój adres. Aby się do jakiegoś dostać, musimy najpierw podać numer segmentu, a po dwukropku numer tego właśnie bajtu. Np. ostatni bajt naszej megabajtowej pamięci ma adres [FFFF:FFFF]. Warto zwrócić uwagę, że ostatni bajt jakiegoś segmentu jest pierwszym bajtem segmentu następnego, czyli [0:16]=[1:0]. Ot, takie sobie dziwactwo. Do adresowania większych bloków pamięci używa się innych sposobów, ale nas to nie interesuje (narazie 0% przydatności). Pora na trochę konkretów. Aby zapisać coś do pamięci wystarczy posłużyć się zwykła instrukcją mov, czyli:
asm
mov $B800:0, 33
end;
I... nie zadziała. Wszystko wydaje się OK, ale żeby utrudnić nam sprawę ktoś wymyślił, że numer segmentu
musimy podawać w rejestrze. Do tego celu służą tzw. rejestry segmentowe:
CS - Code Segment | rejestr segmentu kodowego
DS - Data segment | rejestr segmentu danych
SS - Stack Segment | rejestr segmentu stosowego
ES - Extra Segment | rejestr dodatkowego segmentu danych
Teoretycznie moglibyśmy użyć dowolnego z tych rejestrów, lecz aby nic nie popsuć będziemy używać tego ostatniego. ES jest jedynym rejestrem, którego zawartość możemy bez obaw zmieniać, więc tylko on nas interesuje. No to do dzieła:
asm
mov es, $B800
mov es:0, 33
end;
Mam złą wiadomość. Teraz również nie zadziała, a to dlatego, że do rejestrów segmentowych można przenosić jedynie wartości rejestrów ogólnego przeznaczenia. A więc kolejna poprawka:
asm
mov ax, $B800 {przykładowy rejestr ogólnego przeznaczenia}
mov es, ax
mov es:0, 32
end;
Pewnie domyślasz się, że i tym razem ni z tego? Zgadza się. A to dlatego, że pewna zasada mówi: do pamięci wrzucaj tylko bajtowe wartości czyli albo połówki rejestrów ogólnego przeznaczenia, albo zmienne typu byte. AL jest dolną połówką rejestru AX czyli:
asm
mov ax, $B800
mov es, ax
mov al, 33
mov es:0, al
end;
Uff... Nareszcie się udało. Trochę się trzeba było namęczyć, a czy coś nam to dało (poza wiedzą oczywiście:). Otóż dało. Segment $B800 jest segmentem, który odpowiada, za pamięć karty graficznej (a raczej ekranu w trybie tekstowym). Przeniesienie do pierwszego bajtu tego segmentu liczby 32 da w efekcie pojawienie się wykrzyknika w lewym górnym rogu ekranu. Można domyśleć się, że 32 to kod ASCII wykrzyknika, a więc równie dobrze można postawić dowolny inny znak. Gdybyśmy chcieli postawić drugi wykrzyknik tuż obok to musimy zmienić nie, jak by się wydawało, bajt [$B800:1] lecz [$B800:2]. Jest to spowodowane tym, że każdy znak zajmuje w pamięci dwa bajty: pierwszy to, jak już wiemy, kod ASCII, a drugi to jego kolor. Wykorzystamy to i napiszemy własną procedurę clrscr. Przy okazji nauczymy się nowej skaczącej instrukcji:
asm
mov ax, $B800
mov es, ax
mov al, 32 {kod ASCII spacji}
mov ah, 0 {kolor}
mov bx, 0 {bajt}
@spacja:
mov es:[bx], al {nr bajtu w rejestrze czyli nawiasy kwadratowe}
inc bx {teraz bx czeka na kolor}
mov es:[bx], ah {niech ma}
inc bx
cmp bx, 4000 {czy już wyczyściliśmy cały ekran?}
jnz @spacja {jesli nie to czyścimy dalej}
end;
Numer koloru 0 oznacza, że znak będzie czarny na czarnym tle, czyli spacja mogłaby właściwie zostać zastąpiona innym znakiem i efekt byłby ten sam. Bajt, który odpowiada za kolor (jest to tzw. atrybut znaku), można zapisać tak:
bajt= kolor_znaku + kolor_tła*16. Wykorzystując to możesz dowolnie manipulować wyglądem znaku. Bardzo łatwo robi się to za pomocą systemu szesnastkowego: atrybut:= $2F. 2 - zielony kolor tła, F (dziesiętnie 15) - biały kolor znaku. Jeżeli chodzi o tła, to numery od 8-go w górę spowodują, że znak będzie mrugał (blink). Wracajmy do programu. Rejestr BX porównywany jest z liczbą 4000 ponieważ standartowo na ekranie mamy 2000 znaków (80 kolumn x 25 wierszy), a każdy zajmuje w pamięci dwa bajty. Instrukcji
jnz chyba nie muszę tłumaczyć.
Jeszcze tylko jedna uwaga. Mimo wszystko pamięć jest delikatną sferą komputera i nieumiejętne jej modyfikowanie może doprowadzić do niezłego bałaganu. Musisz więc uważać i lepiej nie eksperymentować. W niektórych przypadkach naprawdę lepiej jest obsługę pamięci zostawić kompilatorowi.