software center, download, programy, pliki, teledyski, mp3
Menu główne


line strona główna
line darmowy download
line baza artykułów i porad
line kontakt z nami
Programy
line Systemy
line Artykuły PDF

Security

line Skanery
line Sniffery
line Security

Windows

line Użytkowe
line Przeglądarki graficzne
line Kodeki
line Narzędzia plikowe
line Narzędzia dyskowe
line Narzędzia systemowe
line Sterowniki
line Szyfrowanie danych
line Zarządzanie hasłami
line Zarządzanie rejestrem
line Łaty i Patche
line Zarządzanie pamięcią
line Synchronizacja czasu
line Nagrywanie płyt
line Free Antivirus (Darmowe Antyvirusy)
line Sterowniki
line Obróbka dźwięku
line Edycja wideo

Internetowe

line Bezpieczeństwo
line Programy p2p
line Komunikatory
line Dodatki do przeglądarek
line Klienty poczty elektronicznej
line Narzędzia Antyspamowe
line Przeglądarki grup dyskusyjnych
line Przeglądarki Offline
line Serwery poczty elektronicznej
line Telefonia komórkowa
line Wyszukiwarki internetowe
line Zdalny dostęp
line Cybernianie
line Klienty FTP
line Narzędzia internetowe
line Prywatnośc
line Przeglądarki internetowe
line Serwery FTP
line Serwery WWW
line Wspomagacze ściągania
line Zarządzanie siecią lokalną

Tuning Systemu

line Diagnostyka i testowanie
line Inne
line Rozszerzenia pulpitu
line Tapety na pulpit
line Tuning Systemu
line Ikony
line Powłoki
line Tuning sprzętu
line Wygaszacze ekranu

Programowanie

line Kompilatory
line Biblioteki i komponenty
line Bazy danych
line Edytory programistyczne
line Środowiska programistyczne
line Debugery
line Tworzenie wersji instalacyjnych

Webmastering

line Użytkowe
line Kursy

Linux

line Użytkowe
line Internetowe
line Multimedialne

Programy biurowe

line Programy dla firm
line Pakiety biurowe
line Administracja
line Edytory tekstu
line Grafika prezentacyjna
line Kadry i płace
line Wspomaganie projektowania
line Zarządzanie projektami
line Bazy danych
line Finanse i księgowośc
line Handel
line Programy ewidencyjne
line Zarządzanie informacją osobistą (PIM)
Nasze serwisy

Programy download
Bramka SMS
Download
Gry
Gry Online
Linux
Muzyka
Newsy
Programowanie
Program TV
Śmieszne Filmy
Teledyski
Kobiety


Artykuły > Programowanie > Kurs Pisania OS - część I
Autor kursu: Jarek Pelczar [ e-mail ]
Artykuł na licencji Public Domain

 Kurs Pisania OS - cześć I
 Kurs Pisania OS - cześć II
 Kurs Pisania OS - cześć III
####################################################


1. Wstęp

Pisanie OSa jest sprawą dość skomplikowaną. Nie wystarczy wiedza o programowaniu, trzeba także rozumieć, w jaki sposób działa procesor, przerwania, karta graficzna, itp.
W tym kursie postaram się pokazać, jak napisać prostego OSa oraz wytłumaczę, w jaki sposób programować sprzęt i jak on działa. Co będzie nam potrzebne?
- DJGPP (www.delorie.com/djgpp)
- Netwide Assembler (NASM) >=0.98 (http://nasm.sourceforge.net/)
- bootloader GRUB
- emulator PC, np. Bochs, VMWare, VirtualPC (opcjonalnie)
Adresy:
http://bochs.sourceforge.net
http://www.vmware.com
http://www.connectix.com

Zestaw DJGPP+NASM+GRUB w zupełności wystarczy do pisania OSa. Emulator PC jest opcjonalny. Jeśli masz mocny sprzęt (procesor min. 800MHz, 256MB RAM) to możesz użyć emulatora PC. Pozwoli to pisać np. pod Windows czy Linuxem bez ciągłego restartowania komputera, aby spróbować, czy to, co piszemy działa. Jeszcze zaznaczam, że kod tutaj prezentowany będzie wyłącznie w C i assemblerze.
Dobra, dosyć wstępu, zabieramy się do roboty. Na początku jednak trzeba będzie opisać trochę sprzętu, aby wiedzieć co się robi. Dla chcących więcej polecam dokumentację Intela do procesora 386 ;) (ja przeczytałem).

2. Sprawy organizacyjne

Jeśli chcesz pisać OSa, musisz sobie zadać kilka ważnych pytań:

a) Po co piszę system operacyjny?
- aby nauczyć się, jak programować wielozadaniowość, pamięć wirtualną, drivery, system plików, itp.
- dostępne systemy mi nie wystarczą
- aby napisać OSa, który próbuje nauczyć projektowania OSów
- sława i chwała, władza nad światem :)

b) Decyzje dotyczące budowy
- OS przenośny czy tylko dla architektury x86: 4 poziomy uprzywilejowania segmentów, ochrona pamięci, przełączanie zadań przy użyciu TSS (lub zamianę stosów)
- architektura: kernel monolityczny, mikrojądro, inne
- wielozadaniowość: brak, "udawana", prawdziwa
- wątki
- wieloprocesorowość: jeden procesor, SMP lub clustering
- wieloużytkowość: wielozadaniowość+zabezpieczenia
- OS używany do pisania naszego OSa: DOS, Windows, Linux, BeOS, inne
- język użyty do implementacji: C, C++, Pascal, assembler
- format plików wykonywalnych: czy ma obsługiwać biblioteki dynamiczne
- biblioteki dla języków wysokiego poziomu: GNU glibc, własna biblioteka, inne
- kompatybilność: te same wywołania, co inne OSy, emulacja innego OSa

Gdy piszesz pierwszego OSa, nie stawiaj sobie dużych wymagań. Jest to bardzo niedobre. Wiele OSów właśnie padło przez tzw. słomiany zapał twórców.

3. Tryby pracy procesorów

3.1. Tryb rzeczywisty

Jest to tryb bardzo "okrojony". Pamięć jest podzielona na segmenty. Każdy segment ma wielkość 64kB. W trybie rzeczywistym mamy dostępny tylko 1MB pamięci, ale i tak jest mniej, bo pamięć od 0xA0000-0xFFFFF jest zarezerwowana dla pamięci video i BIOSu. Mamy więc tylko ok. 640kB wolnych. Tryb ten pozostał nawet w najnowszych procesorach Athlon XP ze względu na kompatybilność ze starymi programami pisanymi dla procesora 8086. Do pisania prawdziwego, dobrego OSa tryb rzeczywisty nas w ogóle nie obchodzi.

3.2. Tryb chroniony

Tryb ten został wprowadzony dopiero w procesorze 286. Jest to specjalny tryb pracy procesora. Pamięć jest podzielona na segmenty, ale to ty decydujesz gdzie się segment zaczyna i jaką ma wielkość. Dla procesora 286 wynosiła ona max. 16MB, a dla procesorów od 386 wzwyż do 4GB (!). W opisach będziemy się opierać tylko na architekturze procesorów od 386 w górę. Nie ma sensu opisywać 286, bo nikt już go dziś nie używa.

4. Opis trybu rzeczywistego

Chcąc, nie chcąc, trzeba wiedzieć jak programować w trybie rzeczywistym. Potrzebne nam to będzie m.in. do napisania bootloadera oraz kodu inicjalizującego nasz OS.

4.1. Inicjalizacja komputera

Po włączeniu komputera procesor "budzi się" w trybie rzeczywistym. Jest to potrzebne, aby załadować BIOS, który jest przecież kodem 16-bitowym i używa specyficznych właściwości trybu rzeczywistego. Procesor skacze do specjalnego adresu w pamięci, gdzie znajduje się procedura skoku do właściwego punktu wejścia BIOSu.

4.2. Pamięć w trybie rzeczywistym

Jak już wspomniałem pamięć w RMODE (skrót od real mode - tryb rzeczywisty) jest podzielona na segmenty. Każdy segment ma limit 64kB. W trybie rzeczywistym jest tylko szesnaście segmentów: 0x0000,0x1000,0x2000,0x3000,...,0xF000. Segment 0x0000 jest zarezerwowany dla tablicy przerwań oraz niektórych danych komputera. Aby obliczyć fizyczne położenie takiego segmentu w pamięci, wystarczy wymnożyć numer segmentu przez 16, czyli w zapisie szesnastkowym dodać jedno 0 na koniec. Np. segment 0x4000 ma adres fizyczny 0x40000. Aby obliczyć adres fizyczny pary segment: offset wystarczy wykonać działanie:

adres_liniowy = (segment*16)|offset

Czasami widzisz numer segmentu, np. 0x5188. W rzeczywistości jest to segment 0x5000, tylko po przeliczeniu adresu segmentu segment: offset z powyższego równania wychodzi, że np. adres 0x5100:0x0010 to to samo, co 0x5000:0x1010.

Poniżej przedstawiam skróconą mapę pamięci w RMODE.

0x0000:0x0000 - tablica wektorów przerwań
0x0000:0x7C00 - tu zostaje załadowany boot-sector przez BIOS
0x1000:0x0000-0x9000:0xFFFF - pamięć użytkownika (najlepiej używać z tego przedziału)
0xA000:0x0000 - pamięć video karty VGA (tylko dla trybu graficznego)
0xB000:0x0000 - pamięć video karty Hercules Monochrome
0xB800:0x0000 - pamięć trybu tekstowego karty VGA
0xC000:0x0000-0xF000:0xFFFF - pamięć BIOSu i inne


4.3. Przerwania w trybie rzeczywistym

Na samym początku pamięci RAM (adres 0x0000:0x0000) znajduje się tablica wektorów przerwań.
Zostaje zainicjalizowana przez BIOS. Zawiera ona punkty wejścia do różnych procedur systemowych.
Np. przerwanie 0x10 służy do obsługi karty graficznej, przerwanie 0x13 do obsługi dysków.
Tablica wektorów przerwań to po prostu tablica par offset: segment. Przerwań może być tylko 256.
Przerwania dzielą się na programowe i sprzętowe. Programowe są wywoływanie jedynie przez użytkownika, a sprzętowe może wywołać procesor. W dokumentacji Intela do procesora 386 jest jak byk napisane, że przerwania od 0x00 do 0x2F są zarezerwowane na wyjątki procesora oraz przerwania sprzętowe to i tak jakiś matoł projektujący BIOS umieścił tam przerwania użytkownika, przekierowując przerwania sprzętowe pod inne numery. I teraz jeśli w trybie chronionym chcesz obsługiwać przerwania sprzętowe i wyjątki procesora to musisz wysłać do PICa (Programmable Interrupt Controller) żądanie, aby przekierował przerwania na właściwe miejsce (ok. 15 linijek w C używając outportb).

Kiedy procesor wykonuje przerwanie (sprzętowe, lub przy użyciu intrukcji int) na stos kładzione są następujące rejestry (w kolejności): ss, sp, flags, cs, ip. Znaczenie rejestrów:
- SS - segment stosu
- SP - aktualna pozycja stosu
- FLAGS - tu zawarte są flagi procesora
- CS - segment kodu
- IP - licznik programu (pozycja aktualnie wykonywanej instrukcji)

Gdy procesor otrzyma żądanie przerwania, liczy sobie (w RMODE) adres przerwania w następujący sposób:
Segment = wartość przy 0x0000:numer_przerwania*4
Offset = wartość przy 0x0000:(numer_przerwania*4)+2
Potem zapisuje stan ww. rejestrów i skacze do procedury obsługi przerwania.

5. Opis trybu chronionego

O trybie rzeczywistym już wystarczy. Teraz zajmijmy się trybem chronionym, ponieważ jego opis będzie trochę dłuższy. Na początek trochę nudnych rzeczy.
UWAGA: Opisywane tutaj rzeczy są w większości przypadków prawdziwe tylko dla procesorów 386 i lepszych.

5.1 Rejestry

W trybie chronionym wszystkie rejestry procesora są rejestrami 32-bitowymi. Mamy więc rejestry: eax, ebx, ecx, edx, esi, edi, ebp, esp oraz cs, ds, es, fs, gs. W trybie rzeczywistym raczej nie używa się rejestrów fs i gs. Mimo, że rejestry segmentowe są 32-bitowe, to i tak procesor używa tylko ich "dolną" połowę.

5.2 Stos

Tutaj stos jest 32-bitowy. Jest to rozmiar domyślny elementu kładzionego na stos. Jeśli chcesz położyć element 16-bitowy, to musisz użyć rejestru 16-bitowego, lub wartości 16-bitowej (np. push word 1 lub push word [wartosc]), jednak prawie nigdy nie używa się argumentów 16-bitowych.

5.3 Rejestry dodatkowe

W trybie chronionym dostępne są także rejestry do kontroli procesora (są 32-bitowe). Są to rejestry:
CR0 - podstawowy rejestr kontroli procesora. Można go używać także w RMODE, ale to on właśnie służy do przełączania CPU w tryb chroniony i do kilku innych rzeczy

CR1 - rejestr zarezerwowany

CR2 - tutaj jest zapisany fizyczny adres, w którym wystąpił ostatni błąd strony (ang. Page Fault), ale o tym będzie później

CR3 - tutaj zapisany jest fizyczny adres katalogu stron (o tym też później)

CR4 - rejestr sterowania dostępny tylko dla nowych procesorów (o ile się nie mylę, jest on dostępny dla procesorów o architekturze 80686 i lepszych). Na razie nie będziemy o nim mówić, gdyż nie jest on nam potrzebny.

Pominąłem tutaj m.in. rejestry debugowania, ale na razie to się nam i tak nie przyda.

5.4 Poziomy uprzywilejowania

W trybie chronionym istnieją tzw. poziomy uprzywilejowania (DPL). Dla architektury x86 są 4 poziomy uprzywilejowania od 0 do 3. Poziom 0 jest przewidziany dla jądra systemu, na tym poziomie można robić wszystko. Poziom 3 jest dla aplikacji użytkownika, ponieważ ma duże ograniczenia, np. nie można dokonywać operacji na portach czy na rejestrach sterowania procesora.

5.5 Pamięć w trybie chronionym

Tutaj jest znacznie lepiej niż w trybie rzeczywistym, gdzie wszystko było ustalone na "sztywno" (jednak są sposoby, aby to ominąć). Najważniejsze jest to, że TY decydujesz, jakie segment ma położenie (mówimy: bazę) i rozmiar (mówimy: limit).
W trybie chronionym bazę i limit segmentu definiujemy w Globalnej Tablicy Deskryptorów (GDT - Global Descriptor Table). Jest to tablica o 64-bitowych wpisach. Wpisów tych może być max. 8192, jednak zazwyczaj używa się tylko kilku. GDT składa się z 2 części:
- nagłówka
- tablica właściwa
Nagłówek na format:

word - 8*ilosc_wpisow-1
dword - fizyczne położenie GDT w pamięci

Tablica właściwa jest po prostu listą wartości. Uwaga: pierwszy wpis musi być 0, ponieważ jest on zarezerwowany, tzw. NULL descriptor. Nie można go używać. Format GDT jest trochę skomplikowany. Każdy wpis ma po 64 bity. Najpierw jednak wyjaśnię format selektora.
Selektor jest to po prostu numer segmentu w GDT lub LDT (wyjaśnię później) zawierający dodatkowe informacje. Numer selektora liczy się następująco:
selektor = (numer_w_GDT*8)+poziom_uprzywilejowania+(4 jeśli to selektor z LDT).
Więc np. nasz segment kodu ma poziom uprzywilejowania 3 a numer deskryptora w tablicy GDT to selektor=2*8+3=19=0x13. Gdyby to był selektor w LDT, to jego wartość wynosiłaby 0x17. Adres tablicy GDT jest pamiętany w specjalnym rejestrze procesora GDTR. Rejestr ten nie jest dostępny bezpośrednio. Można go załadować rozkazem asemblerowym:

lgdt rejestr lub adres w pamięci

np.

lgdt [eax]
lgdt [tablica_gdt]

Aby zapamiętać wartość GDTR, należy użyć rozkazu sgdt.

np.

sgdt edi
sgdt [stara_tablica_gdt]

Teraz pora na format wpisu do GDT. Pokażę wpis jako parę liczb 32-bitowych.

Format dla segmentów KODU i DANYCH

Kod:
31 0
----------------------------------------------------------------  ---------------
| Baza 31..24 | G | X | O | A | Limit | P | DPL | 1 | TYP | A | BAZA 23..16   |
|             |   |   |   | V | 19..16|   |     |   |     |   |               |
|             |   |   |   | L |       |   |     |   |     |   |               |
----------------------------------------------------------------  ---------------
| Baza                        | Limit                                         |
| 15..0                       | 15..0                                         |
----------------------------------------------------------------  ---------------


Format dla specjalnych segmentów systemowych

Kod:
31 0
----------------------------------------------------------------  ---------------
| Baza 31..24 | G | X | O | A | Limit | P | DPL | 0 | TYP | A | BAZA 23..16   |
|             |   |   |   | V | 19..16|   |     |   |     |   |               |
|             |   |   |   | L |       |   |     |   |     |   |               |
----------------------------------------------------------------  ---------------
| Baza                        | Limit                                         |
| 15..0                       | 15..0                                         |
----------------------------------------------------------------  ---------------

A - czy odwołano się do segmentu
AVL - czy segment jest dostępny dla programisty
DPL - poziom uprzywilejowania deskryptora
G - granularność
P - segment dostępny

Teraz wyjaśnię, o co chodzi w bicie G. Jak widać na limit jest tylko 20 bitów. Jeśli bit ten ma wartość 0, to limit segmentu liczymy taki, jaki jest w tablicy. Jeśli bit ten jest zapalony, to procesor mnoży sobie tę liczbę przez 4096 i dopiero w tedy uzyskuje prawdziwy limit segmentu. Jeśli granularność jest wyłączona to można ustalić limit na max. 1MB,
a jeśli włączona to na 4GB.

Bit A jest zapalany, gdy odczytano lub zapisano do segmentu. Może być to używany gdy chcemy np. "wykopać" cały segment do swapa na dysk i zwolnić miejsce dla innego procesu. Jest to używane w 99% przypadków tylko i wyłącznie ze stronnicowaniem (o tym później) aby nie tracić czasu.

Bit P mówi, czy segment jest dostępny, czyli czy można na nim wykonać jakąś operację:
zapis, odczyt, wykonanie kodu.

DPL na to 2 bity. Zobacz punkt 5.4

LDT różni się tylko tym od GDT, że musi mieć swój wpis w GDT, tzn. w GDT musi być wydzielony selektor dla LDT. Tablicę LDT ładuje się rozkazem lldt, np.

Kod:
mov ax,0x18
lldt ax

Teraz przykładowa tablica GDT (w assemblerze):

Kod:
gdt_descr:
dw 5*8-1
dd gdt

gdt:
dd 0,0 ; NULL Descriptor
dd 0x0000FFFF,0x00CF9A00 ; Deskryptor kodu (baza: 0, limit: 4GB, DPL:0)
dd 0x0000FFFF,0x00CF9200 ; Deskryptor danych (baza: 0, limit: 4GB, DPL:0)
dd 0x0000FFFF,0x00CFFA00 ; Deskryptor kodu (baza: 0, limit: 4GB, DPL:3)
dd 0x0000FFFF,0x00CFF200 ; Deskryport danych (baza: 0, limit: 4GB: DPL:3)

Można teraz napisać:

lgdt [gdt_descr]

i już mamy załadowaną nową tablicę. Oczywiście w trybie rzeczywistym musimy przeliczyć sobie adres naszego gdt, aby załadować. Przykład:

Kod:
xor eax,eax
mov ax,ds
shl eax,4
or ax,gdt_descr
lgdt [eax]

Teraz jeszcze jedna ważna rzecz: gdy chcemy ustawić limit segmentu, pamiętajmy, że limit np. 4GB to 0xFFFFF, 1MB to 0xFFF. Po załadowaniu GDT należy opróżnić, tzw. prefetch queue w procesorze. Można to zrobić, np. instrukcją ret. Można napisać np.:

Kod:
lgdt [gdt_descr]
push dword po_gdt
ret
po_gdt:
; Tutaj dalszy ciąg programu

Przykładowe funkcje w C:


Kod:
/* Ta funkcja ustawia adres bazowy segmenty. Jako desc należy podać wskaźnik do wpisu w GDT, jake base, należy podać adres bazowy segmentu
*/
void set_descriptor_base(unsigned long * desc,unsigned long base)
{
desc[0]&=0x0000FFFF;
desc[0]|=(base<<16);
base&=0xFFFF0000;
desc[1]|=((base>>16)&0xFF)|(base&0xFF000000);
}

/* Ta funkcja ustawia limit segmentu */
void set_descriptor_limit(unsigned long * desc,unsigned long limit)
{
desc[0]&=0xFFFF0000;
desc[0]|=(base&0x0000FFFF);
desc[1]&=0xFFFFFFF0;
desc[1]|=(base>>16)&0xF;
}

5.6 Stronnicowanie

Wraz z procesorem i386, Intel wprowadził, tzw. stronnicowanie pamięci (ang. paging).
Jest to specjalny tryb pracy procesora, który daje bardzo duże możliwości zarządzania pamięcią. Pamięć w tym trybie jest podzielona na tzw. strony. Każda strona ma rozmiar 4096 bajtów (4kB). Przy stronnicowaniu mamy tzw. katalog stron (page directory, w skrócie pgdir lub PGD) oraz tablice stron (tzw. page tables). Katalog stron jest strukturą o rozmiarze 1 strony (4kB). Zawiera on 1024 wpisy typu dword. Każdy wpis do takiego katalogu może zawierać adres to 1 tablicy stron. Tablice stron znowu zawierają po 1024 wpisy, które są używane do tzw. mapowania pamięci, tzn. każdemu adresowi rzeczywistemu, możesz przypisać adres wirtualny, np. gdy adresom 0x500000-0x501000 przypiszesz adres wirtualny 0xB8000-0xB900 (czyli jedna strona), to procesor przy odwołaniach, np. pod adres 0x500123 przetłumaczy to na adres fizyczny 0xB8123. Adres aktualnego katalogu stron pamiętany jest z rejestrze CR3. Rejestr ten musi zawierać FIZYCZNY adres katalogu stron.
UWAGA: Wszytkie adresy używane przez tablice stronnicowania oraz rejestr CR3 muszą mieć dolne 4kB zerowe, np. adres 0x12345000 jest poprawny. Musi tak być, ponieważ dolne 12 bitów jest wykorzystane przez procesor na kilka bitów definiujących prawa dostępu do strony.
Dostęp do rejestru CR3 (jak i innych rejestrów sterowania) można uzyskać tylko za pomocą innego rejestru, więc aby zmienić wartość CR3, należy załadować tą wartość do, np. eax a potem z eax do cr3. Aby odblokować stronnicowanie można użyć poniższej procedury:

Kod:
mov eax,adres_do_katalogu_stron
mov cr3,eax
mov eax,cr0
or eax,0x80000000
mov cr0,eax
jmp PgEnabled
PgEnabled:

Wyjaśnienia dot. procedury:
Najpierw należy załadować rejestr CR3, ponieważ CR3 może zawierać niewłaściwą wartość i procesor może wykonać tzw. triple fault. Procesor, gdy 3 razy pod rząd wykona błędną operację, to sam się resetuje, np. gdy żadąna strona nie istnieje a my wykonamy na niej operacją, procesor zanotuje pierwszy błąd i skoczy do procedury obsługi błędu strony.
Procedura obsługi błędu strony może nie istnieć (2 błąd). Procesor skoczy do jakiegoś przypadkowego adresu i wykona błędna instrukcję (3 błąd). Wtedy nastąpi reset procesora.
I właśnie dla tego najlepiej używać emulatora PC do testowania OSa, ponieważ emulator wyświetli błąd, jeśli emulowany procesor się zresetuje. Wtedy w logach emulatora powinien być podany stan rejestrów procesora; Później trzeba ustawić najstarszy bit w rejestrze CR0.
Teraz wystarczy tylko wykonać skok, aby unieważnić cache wewnętrzny procesora (procesor przeładowuje w cache wszystkie rejestry oraz "zauważy", że odblokowaliśmy stronnicowanie). Sam Intel zaleca w swojej dokumentacji, aby po operacji odblokowania stronnicowania wykonać krótki skok (tzw. near jump).

UWAGA: Mówimy tutaj o rzeczach, które będą chodzić na procesorach od 386SX. W nowych procesorach jest specjalne rozszerzenie, które pozwala mieć strony o rozmiarze 4MB, ale nie będę go tutaj omawiał.

Teraz wyjaśnię, jak obliczać adres do wpisu w katalogu stron i tablic stronnicowania, który ma każda strona. Przykładowy program w C:

Kod:
int pobierz_adres_w_pgd(unsigned long addr)
{
return addr>>22;
}

int pobierz_adres_w_pte(unsigned long addr)
{
return (addr>>12)&4095;
}

Uwaga: Liczymy tutaj adresy względem początków tych tablic, więc są to tylko offsety. Teraz bardziej zaawansowany przykład:

Kod:
extern unsigned long pgdir[]; /* Tutaj jest nasz katalog stron */

unsigned long wskaznik_na_pte(unsigned long addr)
{
return pgdir[addr>>24]; /* (addr>>22)>>2 bo tablica pgdir ma elementy typu long o rozmiarze 4 */
}

unsigned long * wskaznik_w_pte(unsigned long addr)
{
unsigned long * pte;
pte=(unsigned long *)wskaznik_na_pte(addr); /* Tutaj pobieramy wskaźnik z katalogu stron do interesującej nas tablicy stron */
if(!((*pte)&1)) return NULL; /* Sprawdzamy, czy tablica stron istnieje */
return pte+((addr>>12)&4095); /* Tutaj pobieramy wskaźnik do wpisu w PTE */
}

Pobieramy wskaźnik do wpisu w tablicy stron, aby móc później zmodyfikować zawartość wpisu.
Jeśli chcielibyśmy tylko pobrać zawartość wpisu, to funkcja miałaby postać:

Kod:
unsigned long wpis_w_pte(unsigned long addr)
{
unsigned long * pte;
pte=(unsigned long *)wskaznik_na_pte(addr); /* Tutaj pobieramy wskaźnik z katalogu stron do interesującej nas tablicy stron */
if(!((*pte)&1)) return NULL; /* Sprawdzamy, czy tablica stron istnieje */
return pte[((addr>>12)&4095)]; /* Tutaj pobieramy wartość wpisu w PTE */
}

Jak już wspominałem, bity 0-11 we wpisach są zarezerwowane dla informacji, które wykorzysta procesor. Dla wpisu do katalogu stron przeważnie używa się tylko 3 bity (tutaj podaję od razu wartości 1<<bit):

Kod:
0x01 - czy strona jest obecna
0x02 - czy stronę można zapisać
0x04 - poziom uprzywilejowania strony (przeważnie ustawia się ten bit także dla kernela)

Dla wpisu do katalogu stron są ww. bity oraz jest kilka dodatkowych

Kod:
0x20 - czy na stronie wykonano jakąś operację (zapis, odczyt, wykonanie kodu)
0x40 - czy do strony coś zapisano

bity 0x200,0x400,0x800 są dostępne dla użytkownika. Reszta nieomównionych bitów musi mieć wartość zero (czyli bity 0x08,0x10,0x80,0x100); tak pisze Intel w swojej dokumentacji.

Gdy, np. zapiszemy do strony zabezpieczonej przed zapisem lub odwołamy się do strony nieistniejącej procesor wykonuje tzw. Page Fault (Błąd Strony), czyli wywołuje przerwanie nr 14 (o przerwaniach w następnym podpunkcie).

5.7 Przerwania

Podobnie jak w trybie rzeczywistym, w trybie chronionym również są przerwania. Jednak sposób ich definiowania różni się znacząco. Nie ma takiego czegoś jak tablica wektorów przerwań w trybie rzeczywistym, która ma ustaloną pozycję. W trybie chronionym jest tzw. Tablica Deskryptorów Przerwań (IDT - Interrupt Descriptor Table). Ma ona podobny format jak GDT. Tak samo jest nagłówek, który zawiera informacje o tablicy i jej położeniu oraz tablica właściwa, w której zapisane są adresy oraz flagi przerwania, więc procedura obsługi przerwania może w ogóle nie istnieć, czyli procesor nie wykona żadnego kodu (w trybie rzeczywistym skoczyłby pod zdefiniowany w tablicy wektorów, prawdopodobnie losowy adres i by wykonał potrójny błąd).
W trybie chronionym rozróżniamy kilka typów przerwań: przerwania sprzętowe, przerwania użytkownika oraz wyjątki procesora. Wpisy od 0x00-0x1F są zarezerwowane na wyjątki procesora, od 0x20-0x2F na przerwania sprzętowe, oraz od 0x30-0xFF na przerwania użytkownika (tutaj mogłem się pomylić czy do 0xFF, ponieważ w systemach wieloprocesorowych (SMP) wpisy gdzieś pod koniec są używane przez procesor oraz APIC, ale systemy SMP nas tutaj nie interesują).
Rejestr IDTR przechowuje liniowy adres i wielkość IDT. IDT można załadować rozkazem:

Kod:
lidt [eax]
lidt [polozenie]

Pewnie już zauważyłeś(aś), że istnieje także instrukcja sidt, która ma podobne działanie do sgdt.

W trybie chronionym każdy typ przerwań ma swój priorytet, czyli te z większym priorytetem są najpierw wykonane przez procesor.

Priorytet Klasa przerwania lub wyjątku
NAJWYŻSZY Wyjątki, oprócz tych służących do debugowania
Instrukcje "pułapki" INTO, INT n, INT 3
Pułapki debugujące dla AKTUALNIE WYKONYWANEJ instrukcji
Pułapki debugujące dla NASTĘPNEJ instrukcji
Przerwania niemaskowalne (NMI)
NAJNIŻSZY Zwykłe przerwanie

Istnieją 3 rodzaje wpisów w IDT: TASK GATE, INTERRUPT GATE i TRAP GATE. INTERRUPT GATE jest dla zwykłych przerwań. TRAP GATE są dla przerwań z kodem błędu (procesor kładzie taki kod na stos). TASK GATE jest dla bramki przełączającej zadania.
Teraz przykładowy kod, jak zresetować komputer :)

Kod:
cli
lidt [temp_gdt]
sti
int 0

temp_gdt:
dw 0
dd 0

Nazywa się to Deliberate Triple Fault. Procesor sam się resetuje.
Gdy jest wykonywane przerwanie w trybie chronionym, podobnie jak w trybie rzeczywistym, na stos kładzione są rejestry: ss, esp, eflags, cs, eip tylko, że te rejestry są 32-bitowe, a nie 16-bitowe, jak ma to miejsce w trybie rzeczywistym.

6. Trochę informacji o programowaniu sprzętu

Dość tej teorii, teraz kolej na praktykę. W tym podpunkcie od razu będę prezentował gotowe, działające procedury (tzw. code snippets).

6.1 Odblokowywanie linii A20


Linia A20 służy do odblokowywania dostępu do całej pamięci. Jeśli byśmy tego nie zrobili, pamięć byłaby dostępna tylko, co 1MB. To rozwiązanie istnieje jeszcze, ze względu na zaszłości historyczne.
Swoją drogą, myślę, że w nowych płytach głównym mogliby wykurzyć to rozwiązanie. Ktoś może powiedzieć, że jest to potrzebne, aby się uruchamiały stare programy. Ciekawe jakie? No, ale nic. Trzeba wklepać kod, aby wejść w tryb chroniony. Podaję kod w assemblerze:

Kod:

setup_a20:
call empty_8042
mov al,0xD1
out 0x64,al
call empty_8042
mov al,0xDF
out 0x60,al
call empty_8042

empty_8042:
dw 0xEB,0xEB
in al,0x64
test al,2
jnz empty_8042


6.2 Przekierowywanie przerwań sprzętowych pod ich właściwy adres

Tę procedurę trzeba wykonać, jeśli chcemy w trybie chronionym użyć przerwań sprzętowych. Jak wam się to nie podoba, to zwalcie na IBM, bo to wszystko ich wina :)
Podaję kod w assemblerze. Zamiana na C jest bardzo prosta:

Kod:
mov al,0x11
out 0x20,al
dw 0xEB,0xEB
out 0xA0,al
dw 0xEB,0xEB
mov al,0x20
out 0x21,al
dw 0xEB,0xEB
out 0xA1,al
dw 0xEB,0xEB
mov al,0x04
out 0x21,al
dw 0xEB,0xEB
mov al,0x02
out 0x21,al
dw 0xEB,0xEB
mov al,0x01
out 0x21,al
dw 0xEB,0xEB
out 0xA1,al
dw 0xEB,0xEB
mov al,0xFF
out 0x21,al
dw 0xEB,0xEB
out 0xA1,al

Rozkaz EB to nie piwo:( tylko instrukcja, która mówi procesorowi, żeby skoczył instrukcję do przodu. W C nie trzeba dawać tych opóźnień, ponieważ gdy używasz instrukcji outportb, to i tak występuje opóźnienie przy jej wywoływaniu. 4 ostatnie linijki służą do zablokowania wszystkich przerwań sprzętowych. Istnieje też procedura BIOSu, aby odblokować linię A20, ale zacytuję Linusa Torvaldsa "This is how REAL programmers do it".

6.3. Skrypt LD

Do naszego prostego systemu możemy sobie napisać skrypt dla linkera LD. Ma on następującą postać:

Kod:
OUTPUT_FORMAT("binary")
ENTRY("_start")
SECTIONS {
.text 0x100000 : {
code = . ; _code = . ;
*(.text)
}
.data : {
*(.data)
}
.bss : {
bss = . ; _bss = . ;
*(.bss)
*(.COMMON)
}
end = . ; _end = . ;
}

Skorzystamy z niego przy użyciu bootloadera GRUB.

6.4. Użycie GRUBa

Jest to bardzo dobry bootloader. Gdy załaduje nasze jądro, to odblokuje A20, wejdzie w tryb chroniony i skoczy do punktu wejścia jądra, więc odpada nam wiele roboty.
GRUB jest kompatybilny z tzw. standardem Multiboot, tzn. nasz kernel musi zawierać specjalny nagłówek (chyba max. w pierwszych 4kB pliku). Teraz nasz przykładowy nagłówek

Kod:
EXTERN code,bss,end
mboot:
dd 0x1BADB002 ; Sygnatura
dd 0x10001 ; Flagi dla bootloadera
dd -(0x1BADB002+0x10001) ; suma kontrolna nagłówka
dd mboot ; Pozycja nagłówka w pliku
dd code
dd bss
dd end
dd _start

_start:
; Tutaj jest początek kodu jądra

Tutaj właśnie korzystamy z symboli, jakie zdefiniowaliśmy w skrypcie linkera ld.
Oczywiście moglibyśmy napisać swojego bootloadera, ale po co skoro GRUB jest bardzo dobry.

7. Piszemy proste jądro systemu!!!

Najpierw zapisz sobie skrypt ld z podpunktu 6.3 do jakiegoś pliku, np. kernel.ld
Teraz piszemy tzw. startup code w assemblerze (plik start.asm):

Kod:
; ------------------------ UTNIJ TU ------------------------------

[BITS 32]
[SECTION .text]
EXTERN code,bss,end
mboot:
dd 0x1BADB002 ; Sygnatura
dd 0x10001 ; Flagi dla bootloadera
dd -(0x1BADB002+0x10001) ; suma kontrolna nagłówka
dd mboot ; Pozycja nagłówka w pliku
dd code
dd bss
dd end
dd _start

GLOBAL _start
_start:
cli
mov esp,kstack+4096
mov ax,0x10
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
lgdt [gdt_descr]
jmp .1
.1:
push dword 0
push dword 0
push dword 0
push dword L6
EXTERN _start_kernel
push dword _start_kernel
ret
L6:
jmp L6

[SECTION .bss]
kstack: resd 1024

[SECTION .data]
gdt_descr:
dw 256*8-1
dd _gdt

GLOBAL _gdt
_gdt:
dd 0,0
dd 0x0000FFFF,0x00CF9A00
dd 0x0000FFFF,0x00CF9200
dd 0,0
times 254 dd 0,0

; ------------------------ UTNIJ TU ------------------------------

Teraz kod prostego jądra systemu (plik kernel.c) :

Kod:
; ------------------------ UTNIJ TU ------------------------------

static char * video_fb=(char *)0xb8000;
void putc(char c)
{
*video_fb++=c;
video_fb++;
}

void puts(char * s)
{
for(;*s;) putc(*s++);
}

void start_kernel(void)
{
puts("Hello World !!!");

for(;;);
}

; ------------------------ UTNIJ TU ------------------------------

Teraz kompilujemy;

Kod:
nasm start.asm -f coff -o start.o
gcc kernel.c -O2 -fomit-frame-pointer -c -o kernel.o
ld -Tkernel.ld -o kernel.bin start.o kernel.o

I teraz w pliku kernel.bin mamy najbardziej debilne i proste jądro systemu :) Teraz tylko przy użyciu GRUBa uruchamiamy je.

7. Zakończenie i wyjaśnienia
Część druga pojawi się niedługo. W drugiej części napiszemy obsługę ekranu i klawiatury.
Cały kurs planuję rozłożyć na kilka części:

1. From zero do hero :) (to co czytacie teraz)
2. Tu nauczymy się obsługi przerwań sprzętowych, wyjątków procesora oraz jak zaprogramować obsługę konsoli tekstowej (czyli ekran+klawiatura).
3. W tej części opiszę przerwanie zegarowe oraz do czego można je wykorzystać.
4. Tu nauczymy się programować wielozadaniowość z użyciem setjmp i longjmp.
5. Tutaj bardziej rozbudowana rzecz: wielozadaniowość z zamianą stosu oraz napiszemy prosty scheduler. Będzie też o kolejkach oczekiwania i zaimplementujemy proste procedury sleep_on i wake_up.
6. ... Na razie nie mam pomysłu. Czekam na sugestie.

Ilość części będzie zależała od tego, na ile lekcji rozłożę całą moją wiedzę (może nawet napiszemy stos TCP/IP, ale do tego jeszcze bardzo daleko, conajmniej kilkanaście lekcji).

UWAGA: Nie będziemy zajmować się żadnymi wzorami matematycznymi, bo po co. Chyba tylko wykłady na studiach są takie powalone, gdzie przy omawianiu np. lexa czy bisona, podane są wzory matematyczne. Po co mi to potrzebne ?
Jak będę znał nawet 88273489237489234798234 wzorów to i tak nie napiszę kompilatora czy systemu operacyjnego. Tutaj liczy się tylko praktyka.
UWAGA 2: Do wszystkich "wielkich algorytmików": bez takich jak my, którzy piszą systemy operacyjne i kompilatory, to byście wiele nie narobili. Dla tego proszę nie przysyłać bluzgów, że algorytmy są lepsze.
UWAGA 3: Nie myśl, że wiesz wszystko. Trzeba naprawdę dużo wiedzieć, żeby zrozumieć jak mało się wie.

8. Pliki

Tutaj zamieszczam archiwum .ZIP, zakodowane programem UUencode z Windows Commandera.
Linii z napisem "UTNIJ TU" oczywiście nie przenosimy do nowego pliku. Zawartość po linii z napisem "UTNIJ TU" przenosimy do jakiegoś pliku i dekodujemy. Najlepiej przy użyciu Windows Commandera.

; ------------------------ UTNIJ TU ------------------------------
sum -r/size 33514/1123


----------------------------------------------------------------  ----------------

Autor: Jarek Pelczar [ e-mail ]

Licencja: Cały tekst jest na licencji Public Domain, czyli brak licencji :)))
komentarz[5] |

programy download hacking program tv bramka sms teledyski kody do gier
trailery filmiki gry online antywirusy artykuły tutoriale systemy
© 2006-2009 haksior.com. Wszelkie prawa zastrzeżone.
Design by jPortal.info
0.035 |