Architektura Systemów Komputerowych - Instrukcja do laboratorium nr 3 i 4
Autor: mgr inż. Arkadiusz Chrobot


  1. Adresowanie pamięci operacyjnej w procesorze x86
    Pamięć operacyjną komputera możemy wyobrazić sobie jako tablicę, do której mikroprocesor może zapisywać, lub z której może odczytywać informacje. Aby określić miejsce do którego ma trafić zapisywana informacja, lub z którego ma być odczytana należy podać jej adres (odwołując się do analogii do tablicy-indeks). Najmniejszą porcją danych jaką może zaadresować mikroprocesor w pamięci jest jeden bajt (8 - bitów), ale możemy przesyłać informacje większymi porcjami, jak np. słowo (16-bajtów), wówczas wystarczy podać tylko adres przeznaczenia dla pierwszego bajta przesyłanej informacji (Sprawa jest trochę bardziej skomplikowana, ponieważ wszystkie procesory firmy Intel używają sposobu zapisu, który jest określany w języku angielskim jako little endian. Oznacza to, że najbardziej znaczący bajt informacji będzie zapisany w miejscu pamięci o najwyższym adresie, a więc niejako „od końca”. Przykładowo, liczba szesnastkowa 1234h zostanie zapisana w pamięci jako 3412h. Należy pamiętać o tym w niektórych sytuacjach, np.: zapisujemy do pamięci słowo (2 bajty), a następnie odczytujemy je bajt po bajcie.).
    Adres z procesora do pamięci przesyłany jest za pomocą magistrali adresowej. Procesor 8086 dysponuje 20-bitową magistralą adresową. Ponieważ jeden bit może przyjmować tylko wartości 1 i 0, za pomocą liczby 20-bitowej możemy zapisać maksymalnie 220 różnych adresów. Ponieważ jeden adres identyfikuje w sposób jednoznaczny tylko jeden bajt, mikroprocesor 8086 może zaadresować pamięć o maksymalnym rozmiarze 220 B, czyli 1 MB (megabajt). Programista piszący program w asemblerze procesora Intel ma do dyspozycji tylko rejestry szesnastobitowe. Konsekwencją tego jest, że w adresie umieszczonym na magistrali adresowej możemy wyróżnić dwie składowe. Pierwsza składowa zawiera 16 starszych bitów adresu, druga natomiast ma rozmiar czterech bitów. Pamięć operacyjną można więc wyobrazić sobie jako podzieloną na 65536 (216) obszarów, z których każdy ma rozmiar 16 bajtów (24). Pierwsze 16 bitów adresu określa adres początku odpowiedniego obszaru, natomiast ostatnie cztery wskazują konkretny bajt w jego obrębie.


    Niestety model ten nie uwzględnia faktu, że druga składowa adresu jest zapamiętywana również w rejestrze 16 bitowym. Do zapamiętania adresu początku obszaru możemy użyć rejestrów segmentowych i ten obszar będziemy nazywać segmentem. Ponieważ procesor nie dysponuje rejestrami czterobitowymi, to drugą składową, nazywaną offsetem przechowujemy w jednym z rejestrów ogólnego przeznaczenia, na przykład w BX. Oznacza to, że mamy do czynienia z dwoma rodzajami adresów: rzeczywistymi 20-bitowymi, które są wystawiane na magistralę adresową i adresami logicznymi, 32-bitowymi, które są przechowywane w rejestrach procesora. Należy zauważyć, że segmenty mają maksymalny rozmiar wynoszący 64KB (216B) i może ich być maksymalnie 65536. Ponieważ pamięć, w której się mieszczą ma maksymalną wielkość 1MB, więc oznacza to, że segmenty nakładają się na siebie. Wprowadza to niejednoznaczność do adresów logicznych, tzn. to samo miejsce w pamięci operacyjnej można określić za pomocą kilku różnych adresów logicznych. Dąży się więc do tego, aby adresy logiczne były zapisywane w formie kanonicznej, czyli należy je tak zapisać, aby offset był liczbą 4 bitową, a segment 16 bitową. Przykład: adres zapisany szesnastkowo 1AEEh:001Ch w postaci kanonicznej ma wartość 1AEFh:000Ch, gdzie znak ':' oddziela segment od przesunięcia. Przeliczenia dokonuje się w taki sam sposób, w jaki komputer przelicza adres logiczny na fizyczny: przesuwa się wartość segmentu o cztery bity w lewo (uzupełniając puste miejsce zerami) i dodaje się wartość przesunięcia. Cztery ostatnie bity wyniku stanowią offset adresu, a szesnaście pierwszych segment.
    Program, który jest wykonywany przez mikroprocesor jest zapisany w całości w pamięci operacyjnej. Gdybyśmy próbowali odczytać dowolną, losowo wybraną komórkę obszaru pamięci, w którym znajduje się program, to nie bylibyśmy w stanie stwierdzić, czy zawiera ona kod rozkazu, czy kod danej (Jest to zgodne z założeniami architektury von Neumanna.).
    Procesor musi jednak wiedzieć gdzie w pamięci znajdują się instrukcje programu, a gdzie przeznaczone dla niech dane. W każdym programie znajdującym się w pamięci możemy wyodrębnić trzy obszary: kodu, danych i stosu. Obszary te rozlokowane są w segmentach, niekoniecznie rozłącznych (mogą też mieścić się w jednym segmencie). Adresy początków odpowiednich segmentów umieszczane są przez program ładujący w odpowiednich rejestrach segmentowych procesora. Adresowanie poszczególnych komórek pamięci w segmentach kodu i danych przebiega tak, jak opisano to powyżej. Osobnego omówienia wymaga adresowanie danych w segmencie stosu. Dane na stosie zapamiętywane są według schematu LIFO (ang. Last In First Out). Oznacza to, informacja, która została umieszczona na stosie jako ostatnia zostanie odczytana z niego jako pierwsza. Dodatkowo operacje zapisu i odczytu danych dotyczą szczytu stosu, a więc możemy jedynie umieścić informację na szczycie stosu, lub pobrać ją ze szczytu stosu (Często przytaczaną analogią do opisu tego rodzaju pamięci jest stos talerzy. Jeśli chcemy na nim umieścić dodatkowy talerz, to musimy położyć go na wierzchu stosu. Podobnie wygląda pobieranie talerzy ze stosu.).
    Adres początku segmentu stosu znajduje się w rejestrze segmentowym SS, natomiast adres szczytu stosu w rejestrze SP.

  2. Tryby adresowania
    W mikroprocesorze 8086 istnieje 9 różnych sposobów na przesłanie danych z rejestrów do pamięci lub w odwrotnym kierunku. Te sposoby określa się mianem trybów adresowania i określają one w jaki sposób należy wyliczyć adres (tzw. adres efektywny) danej znajdującej się w pamięci komputera:
    • tryb domyślny-stosowany jest rozkazach operujących na pewnych, z góry ustalonych rejestrach, których nie trzeba wymieniać jako argumentów (zwanych też operandami) tych rozkazów,
    • tryb natychmiastowy-dana zawarta jest bezpośrednio w rozkazie mikroprocesora, ten tryb służy do załadowania do rejestru wartości inicjującej,
    • tryb bezpośredni-adres danej jest zawarty w kodzie programu,
    • indeksowy-adres danej zawarty jest w rejestrze SI lub DI,
    • bazowy-adres danej zawarty jest w rejestrze BX lub BP,
    • indeksowy z przemieszczeniem-adres danej jest sumą zawartości rejestru indeksowego i przemieszczenia, które może być bajtem lub słowem,
    • bazowy z przemieszczeniem-adres danej jest sumą zawartości rejestru bazowego (jeśli przyjmiemy za rejestr bazowy BP, to rejestrem segmentowym będzie SS) i przemieszczenia,
    • bazowy indeksowany-adres danej jest sumą zawartości rejestru bazowego i rejestru które może być bajtem lub słowem, indeksowego,
    • bazowy indeksowany z przemieszczeniem-adres danej jest sumą zawartości rejestrów bazowego i indeksowego oraz wartości przesunięcia.
    Adres danej, który trzeba wyznaczyć przez przeprowadzenie odpowiednich operacji arytmetycznych nazywamy adresem efektywnym.

    Uwaga:
    Powyższy opis odnosi się do procesora 8086. Współczesne procesory Intela są z nim wstecznie kompatybilne, jeśli pracują tzw. trybie rzeczywistym. Na laboratorium będziemy się posługiwać debuggerem pracującym w trybie chronionym, w którym obowiązuje inny model adresowania pamięci. Ćwiczenia zamieszczone poniżej zostały tak sformułowane, aby nie wymagały głębszej wiedzy na temat tego modelu. Jedyna zmiana polega na tym, że w rozkazach adresujących pamięć będziemy się posługiwać rejestrami rozszerzonymi: ebx, eax, esi, itd.

  3. Rozkazy transmisji danych
    1. MOV - rozkaz przesyłania danych pomiędzy pamięcią i rejestrami lub pomiędzy rejestrami.
      Rozkaz MOV jest rozkazem dwuargumentowym. Oba argumenty (operandy) tej instrukcji muszą mieć jednakowy rozmiar i mogą określać rejestr lub miejsce w pamięci. Pierwszy operand stanowi przeznaczenie, a drugi źródło. W przypadku tego rozkazu można stosować wszystkie tryby adresowania z wyjątkiem trybu domyślnego, jednakże przy niektórych kombinacjach operandów użycie niektórych trybów jest zabronione, np. nie można zastosować trybu natychmiastowego, jeśli argument docelowy jest rejestrem segmentowym.

      Przykłady:
      mov ax, bx      - przesłanie zawartości rejestru bx do rejestru ax;
      mov cx, 20h     - umieść w rejestrze cx wartość 32;
      mov ax, [di]    - umieść w rejestrze ax wartość z komórki pamięci, której offset adresu jest umieszczony w rejestrze di (segment adresu jest w rejestrze ds);
      mov ds, ax       - umieść w rejestrze ds zawartość rejestru ax (niedozwolona jest np. operacja: mov ds,0034h);

      Ćwiczenie:
      Umieść w pamięci, w kolejnych lokacjach poczynając od adresu ds:403000h wartości: 0Ah, 7, 0Ch, 0Eh, 5, 4. Następnie wykonaj podane rozkazy:
      mov esi, 2
      mov ebx, 403000h
      mov ax, bx
      mov ax, 0
      mov al, [ebx]
      mov al, [ebx+esi]
      mov al, [ebx+esi+1]

      Zaobserwuj, jak zmieniają się wartości rejestru ax.

    2. XCHG - rozkaz zamienia wartości rejestrów, które są podane jako jego argumenty.

      Przykład:
      xchg ax, bx - wartość, która była w rejestrze ax znajdzie się w bx, a wartość z bx trafi do ax;

      Ćwiczenie:
      mov ax, 02h
      mov bx, 04h
      xchg ax, bx

      Jak zmieniły się zawartości rejestrów ?

    3. XLATB - rozkaz zwraca wartość elementu tablicy, którego indeks znajduje się w rejestrze al. Adres tej tablicy jest zawarty w parze rejestrów ds (segment) i bx (offset - w przypadku środowiska 32 bitowego jest to rejestr ebx).

      Ćwiczenie:
      Umieść w kolejnych komórkach pamięci, zaczynając od adresu ds:403000h następujące wartości 0Ah, 0Bh, 0Ch, 0Dh, 0Eh, 0Fh, a następnie wykonaj rozkazy:

      mov al, 3                  ;umieść numer elementu w rejestrze al
      mov ebx, 403000h  ;umieść segment tablicy w rejestrze ebx
      xlatb

      Komentarz:
      Rozkaz XLATB stosuje tryb domyślny adresowania, nie pobiera żadnych operandów, ale oczekuje, że programista umieści odpowiednie wartości w wymaganych rejestrach. Wartość elementu jest zwracana w rejestrze al. Indeksy w tablicy zaczynają się od wartości zero. W ćwiczeniu wartość rejestru segmentowego została niezmieniona.

    4. LEA - ładuje adres efektywny do rejestru

      Ćwiczenie:
      Umieść w pamięci poczynając od adresu ds:403000h wartości: 00h 0Ah 01h 30h, 40h, a następnie wykonaj rozkazy:

      mov ebx, 403000h
      mov esi, 2
      lea ebx, [ebx+esi]
      mov ax, [ebx]
      lea bx, [ebx+esi+1]
      mov ax, [ebx]

      Jak zmieniały się wartości rejestrów ax i ebx?

      Komentarz:
      Rozkaz używany jest w celu zaoszczędzenia czasu potrzebnego na wyliczanie złożonego adresu efektywnego. Raz obliczonym adresem można posługiwać się wielokrotnie.

    5. LDS - rozkaz ładuje daleki wskaźnik (adres) do rejestru segmentowego ds i wybranego rejestru ogólnego przeznaczenia.

      Ćwiczenie:
      Umieść w pamięci poczynając od adresu ds:403000h wartości: 08h 30h 40h 00h 23h, 00h, a następnie wykonaj rozkaz:

      lds ebx, [403000]

      Jak zmieniły się wartości rejestrów ds i ebx ?
      Uwaga: Ponieważ rozkaz modyfikuje zawartość rejestru ds, należy przywrócić jego starą zawartość po wykonaniu tego ćwiczenia.

    6. LAHF - załaduj rejestr ah zawartością młodszego bajta rejestru flag

      Ćwiczenie:
      Wykonaj rozkaz: lahf. Jak zmieniła się zawartość rejestru ah?

      Komentarz:
      Rozkaz używa domyślnego trybu adresowania. Komplementarnym do niego jest rozkaz SAHF, czyli zapisania rejestru ah w młodszej połówce rejestru flag. Po wykonaniu instrukcji LAHF do ah kopiowane są znaczniki znaku, zera,parzystości, przeniesienia i przeniesienia pomocniczego.


  4. Rozkazy manipulacji bitami
    1. AND - rozkaz mnożenia logicznego

      Przykład:
      and ax, ax
      and ax, 02h
      and ax, zmienna

      Ćwiczenie:
      Umieść w kolejnych lokacjach pamięci wartości 02h, 03h, 04h 05h poczynając od adresu ds:403000h. Następnie wykonaj następujące rozkazy:

      mov ax, 000Fh
      mov bx, 000Ah
      and ax, bx
      mov ax, 000Fh
      mov ebx, 403000h
      mov al, [ebx]
      mov esi, 1
      mov dl, [ebx+esi]
      and al, dl

      Zaobserwuj jak zmieniały się wartości poszczególnych rejestrów, w tym jak zmieniała się wartość rejestru flag.

      Komentarz:
      Używając rozkazu AND można stosować wszystkie tryby adresowania, poza adresowaniem domyślnym. Operacja AND jest operacją bitową, tzn. dotyczy każdej pary bitów w operandach. Jeśli oba bity mają wartość „1”, to wynikiem operacji jest „1”, w pozostałych przypadkach „0”. Przykładowo, operacja AND na liczbach 101b (5d) i 011b (3d) daje w wyniku wartość 001b (1d). Operacja ta modyfikuje znaczniki zera, parzystości, nadmiaru, znaku, przeniesienia i przeniesienia dodatkowego. Wynik jest zapisywany w pierwszym operandzie, dlatego nie może to być stała.

    2. OR - rozkaz dodawania logicznego

      Przykład:
      or ax, bx
      or zmienna, ax
      or ax, 123h


      Ćwiczenie:
      Powtórz poprzednie ćwiczenie zmieniając rozkaz and na or.

      Komentarz:
      Rozkaz działa podobnie do rozkazu opisanego powyżej, z tym, że realizuje na poszczególnych parach bitów argumentów operację sumy logicznej tzn.: jeśli oba bity mają wartość „0”, to w wyniku otrzymujemy wartość „0”, w pozostałych przypadkach wartość „1”.

    3. XOR - rozkaz różnicy symetrycznej argumentów

      Przykład:
      xor ax, bx
      xor ax, 123h
      xor ax, zmienna

      Ćwiczenie:
      Powtórz poprzednie ćwiczenie zmieniając rozkaz or na xor.

      Komentarz:
      Rozkaz działa podobnie do rozkazów opisanych powyżej, z tym, że realizuje na poszczególnych parach bitów argumentów operację różnicy symetrycznej tzn.: jeśli oba bity mają taką samą wartość, to wynikiem operacji jest wartość „0”, w przeciwnym przypadku wartość „1”.

    4. NOT - rozkaz negacji bitów.

      Ćwiczenie:
      Wykonaj rozkazy:

      mov ax, 0FFFFh
      not ax

      Komentarz:
      Rozkaz wymaga tylko jednego argumentu. Jego działanie polega na zamianie wartości poszczególnych bitów na wartości przeciwne. Wynik działania jest zapisywany w operandzie.

  5. Zadania do samodzielnego wykonania

    1. Wykonaj przy pomocy rozkazu mov przesłania między dwoma rejestrami, oraz między pamięcią i rejestrami. Dokonaj pomiaru czasu wykonania tych operacji stosując operację rdtsc. W przypadku przesyłania do pamięci zastosuj dwa tryby adresowania: bazowy i bazowy z przemieszczeniem. Porównaj wyniki.
    2. Posługując się rozkazami mov napisz fragment kodu, który zastąpiłby instrukcję xchg ax,bx. Następnie zmierz czas wykonania tych tego rozkazu i odpowiadającego mu ciągu instrukcji (patrz zadanie 1). Porównaj wyniki.
    3. Zademonstruj w jaki sposób można do siebie dodać dwie liczby nie zmieniając wartości znaczników w rejestrze flag. Użyj w tym celu instrukcji lea.
    4. Załaduj do rejestru ah wartość młodszego bajta rejestru flag. Sprawdź używając odpowiedniego rozkazu logicznego, czy ustawiona jest flaga zera.
    5. Pokaż, jak stosując rozkaz and można wyznaczyć wartość odpowiadającą bitom od 4 do 7 w dowolnym rejestrze.
    6. Pokaż, jak stosując operację xor wyzerować rejestr ax.
    7. Stosując prawa de Morgana* zastąp instrukcje or i and odpowiednimi ciągami rozkazów. Dokonaj pomiarów czasu wykonania (patrz zadanie 1) tych instrukcji oraz odpowiadających im ciągów rozkazów.
    8. Zbadaj przy pomocy rozkazu rdtsc czas wykonania bloku dwudziestu rozkazów przesłań z pamięci do rejestru (mov) stosujących tryb adresowania bazowy indeksowany z przemieszczeniem. Następnie powtórz to badanie dla bloku rozkazów mov, posługując się adresem efektywnym wyliczonym przy pomocy rozkazu lea.
    9. Powtórz zadanie ósme dla dowolnej operacji bitowej wymagającej dwóch operandów (and, or, xor).
    10. Zbadaj czas wykonania instrukcji xlatb. Następnie napisz blok rozkazów, który odpowiadałby funkcjonalnie tej instrukcji i porównaj czas jego wykonania z czasem instrukcji xlatb.
    11. Zmierz czas wykonania instrukcji lds (np. lds ebx,[403000]). Następnie dokonaj pomiaru czasu wykonania bloku rozkazów, które osobno ładowałyby rejestr ds i zastosowane wcześniej przez Ciebie rejestr ogólnego przeznaczenia.
    12. Powtórz zadanie jedenaste dla instrukcji les.
    13. Umieść w pamięci wartość dwubajtową. Załaduj ją przy pomocy rozkazu mov do rejestru ax. Jaka jest kolejność bajtów w rejestrze w stosunku do kolejności w pamięci? Jak wytłumaczyć to zjawisko?
    14. Powtórz zadanie pierwsze dla instrukcji xor. Porównaj wyniki.

    *   Pierwsze prawo de Morgana: not(a or b) = not(a) and not(b).
         Drugie prawo de Morgana: not(a and b) = not(a) or not (b).