Asembler
Obsługa pamięci
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.


Copyright © 2000-2001 Bartosz 'SILV' Jaworski
All rights reserved