Asembler x86/Zaawansowana architektura

Z Wikibooks, biblioteki wolnych podręczników.

Wstęp[edytuj]

Ten rozdział ma na celu przybliżenie jak system operacyjny, we współpracy ze sprzętem, uzyskuje adres pojedynczej komórki pamięci przy pomocy takich wyszukanych mechanizmów, jak np. stronicowanie. Wiedza na ten temat nie jest konieczna do zrozumienia języka Asembler, co więcej, nie trzeba nic wiedzieć na temat katalogów stron czy ramki strony, jednak programiści asemblerowi wykorzystują swoje umiejętności do pisania niskopoziomowych procedur systemu operacyjnego. Jeśli będziesz planował/a w przyszłości pisać swój system operacyjny bądź pracować nad istniejącym, wiedza zgromadzona w tym rozdziale będzie nieoceniona. Nawet ci, którzy nie chcą podejmować się tak karkołomnych zadań, jak pisanie kodu działającego w trybie jądra, mogą przeczytać ten rozdział dla zaspokojenia swojej ciekawości i odpowiedzenia na pytanie: Jak, co i dlaczego?


Tryb chroniony[edytuj]

W trybie chronionym procesora nie istnieje już model obliczania adresu w pamięci fizycznej jak to miało miejsce w trybie rzeczywistym w notacji 'segment:offset'. W tym trybie pamięć w ogóle nie jest podzielona na segmenty, jeden wielki segment gdzie program może zaadresować całe 4 GB pamięci. Zwróć uwagę na słowo „zaadresować”. Komputer nie musi mieć w ogóle tyle pamięci operacyjnej, po prostu programowi wydaje się, że tyle ma i może obliczyć adres dla każdej komórki z tej puli. Wracając do modelu pamięci w trybie chronionym, program chcąc coś zapisać w pamięci operacyjnej musi liczyć się z tzw. stronicowaniem pamięci. Teraz są dwa kroki aby uzyskać pamięć w trybie chronionym:

  • 1. Wyznaczenie adresu liniowego danej komórki.
  • 2. Translacja adresu liniowego na adres fizyczny.

1. Uzyskiwanie adresu Liniowego[edytuj]

Dawniej w trybie rzeczywistym w danym rejestrze segmentowym zawarty był adres startowy danego segmentu w pamięci (adres mówiący nam gdzie dany segment się rozpoczyna). Teraz w rejestrze segmentowym nie przechowujemy adresu segmentu. Zapisana tam 16-bitowa informacja nazywana jest „selektorem segmentu”. Poniżej znajduje się rysunek przedstawiający budowę selektora segmentu.



Oto wyjaśnienia poniższych skrótów:

  • RPL – (ang. Requestor Privilege Level) opisuje obecny stan uprzywilejowania CPU (ang. Current Privilege Level), gdy odpowiedni selektor jest ładowany do rejestru CS. Mówiąc prościej – RPL zawiera informacje o uprawnieniach, które posiada procesor w momencie zapisywania wartości w rejestrze kodu. Jeśli program z trybu użytkownika wywołuje taką operację, wtedy CPL zawiera uprawnienia właśnie danego programu, które zostaną zapisane w RPL. Jak można się domyśleć, uprawnienia w przypadku programu z przestrzeni użytkownika będą mniejsze niż te z przestrzeni jądra. RPL zajmuje bity 0 oraz 1.
  • Index – jest to numer deskryptora segmentu zawartego w Globalnej Tablicy Deskryptorów (GDT) lub Lokalnej Tablicy Deskryptorów (LDT). Pole Index zajmuje bity od 3. do 15.
  • TI – (ang. Table Indicator) oznacza, czy dany deskryptor segmentu znajduje się w GTK (gdy TI = 0) lub w LDT (gdy TI = 1). Pole to zajmuje bit nr 2.

Selektor segmentu zapisywany jest w rejestrze segmentowym jak CS, DS, SS, ES, FS, GS. Gdy posiadamy pewny adres logiczny np. 0xc00d:1001 wówczas wartość 0xC00D będzie właśnie w pewnym rejestrze segmentowym i będzie stanowić selektor segmentu, nie zaś jego adres (de facto takowy segment w rzeczywistości nie istnieje – patrz „Płaski model pamięci”).


Każdy nasz „logiczny segment” jest reprezentowany przez tzw deskryptor segmentu, o długości 8 bajtów (64 bity). Deskryptor ten zaś, jak sama nazwa wskazuje, opisuje nasz segment. Wspomniany deskryptor znajduje się w tablicy deskryptorów. Mogłeś sobie postawić pytanie: w której? Przecież są dwie tablice: GDT i LDT. Odpowiedź: to zależy od pola TI w selektorze segmentu. Poniżej znajduje się obrazek prezentujący jak wygląda deskryptor segmentu wraz z wytłumaczeniem danych pól.

  • Base – tutaj znajduje się adres liniowy pierwszego bajtu danego segmentu.
  • G – (ang. Granularity flag) jeśli ta flaga jest ustawiona na 0 (wyczyszczona), wówczas wielkość segmentu jest podawana w pojedynczych bajtach, np. 78 B albo 256 B. W przeciwnym wypadku rozmiar wyraża się w wielokrotności 4096 bajtów (4 KB).
  • Limit – Przetrzymuje offset (przesunięcie) ostatniej komórki pamięci danego segmentu. Ta flaga ma związek z flagą G. Gdy G = 0, wówczas wielkość segmentu może wynosić od 1 bajta do 1 MB (czyli maksimum pamięci jaki mógł mieć procesor w trybie rzeczywistym), gdy G = 1 wówczas segment może mieć wielkość od 4 KB (rozmiar strony – o tym nieco później) aż do 4 GB.
  • S – (ang. system flag) jeśli ta flaga jest wyzerowana, dany segment jest tzw. segmentem systemowym, który przechowuje bardzo ważne struktury danych systemowych, jak np. położenie LDT. Jeśli S = 1, wówczas dany segment jest zwykłym segmentem kodu albo danych. Zwróć uwagę na nieco nietypowe ustawienie flag. Do tej pory, jeśli dana flaga jest ustawiona na 1, wówczas spełnia swoją funkcję. W fladze S jest odwrotnie, tylko jeśli jest wyczyszczona zaczyna spełniać swoją rolę.
  • Type – ta flaga determinuje typ segmentu. Może oznaczać, że dany segment (również deskryptor segmentu) jest albo Deskryptorem Segmentu Kodu (ang. Code Segment Descriptor – CSD), albo Deskryptorem Segmentu Danych (ang. Data Segment Descriptor – DSD), albo Deskryptorem Segmentu Stanu Zadania (ang. Task State Segment Descriptor – TSSD).
  • DPL – (ang. Descriptor Privilege Level) służy do ograniczenia dostępu do danego segmentu. DPL reprezentuje minimalny stan uprzywilejowania CPU wymagany do użycia tego segmentu. Dla przykładu, jeśli pole DPL danego segmentu ma wartość 0, wówczas jest adresowalne tylko dla programu z CPL równym 0 (czyli działającego w trybie jądra). Jeśli DPL wynosi 3, wówczas rejestr jest dostępny dla każdej wartości CPL (zarówno w trybie użytkownika jak i jądra).
  • P – (ang. Segment-Present Flag) jeśli ta flaga jest równa 0, to dany segment nie jest w ogóle w pamięci operacyjnej, czyli jest odłożony na dysku twardym w postaci pliku wymiany (za ten mechanizm odpowiada pamięć wirtualna).
  • D lub B – flaga nazywana D lub w zależności od tego czy dany segment jest segmentem kodu czy danych. Znaczenie tego pola zależy ściśle od kontekstu w jakim został użyty.
    • D – Ta flaga w deskryptorze segmentu kodu mówi nam o „szerokości stosu”. Jeśli flaga jest wyzerowana, szerokość stosu wynosi 16 bit (czyli ESP jest inkrementowany i dekrementowany o 2 bajty). Jeśli jest ustawiona na 1, szerokość wynosi 32 bity (analogicznie ESP rośnie lub maleje o 4 bajty).
    • B – Ta flaga determinuje maksymalny adres, który może wskazywać ESP. Jeśli flaga jest wyczyszczona, wartość adresu jest 16 bitowa (zatem maksymalny adres to 0FFFFh). W przeciwnym wartość wynosi 32 bity (maksymalny adres równy jest 0FFFFFFFFh).
  • AVL – pole, które może używać dany system operacyjny, ale nie musi tego robić, przykładowo Linuks ignoruje te pole. Jest ono dostępne dla oprogramowania i może być dowolnie wykorzystane przez programistów i projektantów systemów operacyjnych.


Co musi zrobić system operacyjny, aby uzyskać adres liniowy z adresu logicznego? Dla przykładu mamy wcześniejszy adres logiczny 0xc00d:1001. Najpierw brana jest część 0xC00D. Indeks z selektora segmentu jest pomnożony przez 8 (ponieważ taką długość w bajtach ma deskryptor segmentu). Do tej liczby dodawana jest wartość Base z deskryptora segmentu. Odpowiedni Deskryptor segmentu system operacyjny bierze z GDT lub LDT, a to zaś wybiera na podstawie wartości pola TI w selektorze. Po zsumowaniu, na sam koniec dodawana jest wartość offsetu z naszego adresu logicznego czyli 0x1001. Poniżej znajduje się rysunek pokazujący jak powstaje adres liniowy:

2. Translacja na adres fizyczny[edytuj]

Tutaj aby dany adres liniowy mógł być przetłumaczony na adres fizyczny, należy skorzystać z mechanizmu stronicowania pamięci. Pamięć operacyjna została podzielona na małe obszary zwane stronami, zaś ich adresy to ramki stron. Nie myl obu tych pojęć. Strona jest to pewien blok danych, zaś ramka strony to adres początkowy tego bloku. Procesory firmy Intel oraz zgodne z nimi mają rozmiar strony równy 4 KB. Adres liniowy o długości 32 bitów (ten uzyskany z poprzedniego etapu) jest dzielony na następujące części:

  • Katalog (Directory)
  • Tablica (Table)
  • Przesunięcie (Offset)

Samą translację podzielono na dwa etapy w celu oszczędzenia pamięci RAM. Najpierw jest translacja z użyciem Katalogu Stron, następnie przy użyciu Tablicy Stron. Każdy uruchomiony proces ma przydzielony odpowiedni wpis w Katalogu, ale nie oznacza to, że każdy proces musi go mieć od razu. Dany proces może zgłosić żądanie do systemu operacyjnego o przeprowadzenie tej operacji. Zjawisko to nazywa się stronicowaniem na żądanie. Przedstawię teraz translację adresu liniowego. W rejestrze CR3 umieszczony jest surowy, fizyczny adres Katalogu Stron (Page Directory). W Katalogu Stron znajduje się adres liniowy właściwej Tablicy Stron dla danego procesu. Z kolei Tablica Stron przechowuje informację o adresie fizycznym początku danej ramki strony. Ostatnie pole zwane Przesunięciem (Offset) determinuje, jak o ile należy się przemieścić wewnątrz strony (innymi słowy, ile należy się przemieścić od adresu ramki strony). Całość prezentuje poniższy schemat:


Ponieważ to pole ma 12 bitów zatem pojedyncza strona może mieć rozmiar właśnie 4 KB. Zarówno pola Katalogu Stron jak i Tablicy Stron mają po 10 bitów, więc obie te lokacje mogą mieć maksymalnie 1024 pól. W ten sposób Katalog Stron może zaadresować tyle komórek pamięci:



Jeżeli potraktować jedną komórkę jako jeden bajt, dzieląc sukcesywnie przez 1024, otrzymamy liczbę 4 GB pamięci możliwej do zaadresowania, zatem dokładnie tyle, ile mogą mieć pamięci RAM komputery z serii 80386 i nowsze. Poszczególne wejścia z Katalogu Stron i Tablicy Stron mają taką samą strukturę. Każe pole ma poniższe swoje własne „pole”:

  • Flaga Dostępu – (ang. Accessed Flag) – jest ustawiana za każdym razem przez jednostkę stronicującą, gdy obliczany jest adres ramki strony. Ta flaga może być użyta przez system operacyjny przy wyborze stron pamięci, które mają być odłożone do pliku wymiany. Sama jednostka stronicująca nigdy nie czyści (zeruje) tej flagi. To musi być wykonane przez system operacyjny.
  • Dirty Flag – ta flaga dotyczy tylko Tablicy Stron. Jest ustawiana za każdym razem, gdy przeprowadzana jest operacja zapisu na danej ramce strony. Jej zastosowanie jest podobne do flagi dostępu.
  • Flaga Zapisu/Odczytu – (ang. Read/Write Flag) flaga ta determinuje prawa dostępu do strony lub całej Tablicy Stron. Jest ona częścią sprzętowej ochrony stronicowania.
  • Flaga Użytkownika/Nadzorcy – (ang.User/Supervisor flag) zawiera uprawnienia dla strony lub całej Tablicy Stron. Również jest fragmentem sprzętowej ochrony mechanizmu stronicowania.
  • Flagi PCD i PWT – wykorzystywane są w celu kontroli przemieszczenia strony lub całej Tablicy Stron do pamięci cache.
  • Flaga Rozmiaru Strony – (ang. Page Size Flag) dotyczy tylko Katalogu Stron. Jeśli jest ustawiona, to poszczególne wejścia z Katalogu Stron dotyczą ramek stron o rozmiarze 4 kB, w przeciwnym wypadku 4 MB.
  • Flaga Globalna – (ang. Global Flag) dotyczy wyłącznie Tablicy Stron. Została ona wprowadzona w procesorach Pentium Pro przy mechanizmie pamięci cache.


Przypisy