os




Kurs OS część III




















  Menu

var katalog="../";










Kurs Pisania OS #3


Po przeczytaniu 2 poprzednich części nauczyłeś się bardzo dużo. Wystarczyło by to
do napisania najbardziej debilnego OSa, no ale qrde, łi łant mor :).

W tej części będzie dość dużo teorii na temat wyjątków, przerwania zegarowego oraz
dodamy obsługę wyjątków procesora do naszego kernela. W 2 części zapomniałem opisać,
które przerwania sprzętowe służą do czego. Na początek zajmę się przerwaniami sprzętowymi.
UWAGA: Ta część będzie stosunkowo krótka !!! W kilku następnych częściach będzie dużo więcej wiedzy :).

Przerwania sprzętowe

Jak wspomniałem wyżej, nie napisałem, które przerwania sprzętowe służą do czego.
Śpieszę z wyjaśnieniem (opiszę tylko najczęściej wykorzystywane przerwania):


numer przerwania krótki opis
0 przerwanie zegarowe
1 klawiatura
3 UART
4 UART
7 port równoległy
13 FPU
14 IDE0
15 IDE1


klawiatura
Jest to domyślne przerwanie, które może być użyte wyłącznie przez klawiaturę.
Nawet jeśli masz klawiaturę na USB, to i tak używa ona przerwania 1.

UART (3) i UART (4)
Są to przerwania używane przez porty szeregowe

port równoległy
Chyba nie trzeba wyjaśniać...

FPU
Przerwanie to jest używane przez koprocesor matematyczny do raportowania błędów.
Jeśli przełączymy zadanie i użyjemy koprocesora matematycznego, wygeneruje on
to przerwanie, aby system mógł np. zmienić kontekst koprocesora (tzn. zapisać jego
stan w danych ostatniego zadania, które używało procesor i załadować kontekst
z aktualnego zadania).

IDE0 i IDE1
Są to przerwania używane przez kontroler IDE. Są one bardzo pomocne przy obsłudze
twardych dysków i transferów DMA (o tym będzie kiedy indziej), ponieważ nie zmuszają
do czekania na określony stan dysku przez system operacyjny.

Przerwanie zegarowe

Jest to chyba najczęściej wykorzystywane przerwanie w komputerze ;). Linia przerwania
zegara jest kontrolowana przez PIT (Programmable Interrupt Timer). Normalnie po starcie
komputera BIOS ustawia częstotliwość ok. 16Hz (czyli jest wywoływane 16 razy na sekundę,
jakby ktoś nie wiedział co to są Hertze :) ). To trochę wolno, no ale cóż. IBM tak
zrobił w swoich PC, i tak zostało do dzisiaj ze względu na kompatybilność wsteczną.
My w naszym OSie zmienimy tę częstotliwość na 100 Hz, tak jak jest to robione we wszystkich
innych OSach. Nasze przerwanie będzie wywoływane 100x na sekundę, więc jego procedura musi być
dość szybka. Ale najpierw mały kod jak zmienić częstotliwość tego przerwania.


void ustaw_czestotliwosc_pit(unsigned long hz)
{
unsigned long val=1193180/hz;
outb(0x36,0x43);
outb(val&0xff,0x40);
outb((val>>8)&0xff,0x40);
}


Wpisanie wartości 0x36 do portu 0x43, powie PITowi, że chcemy mu przeprogramować częstotliwość
przerwania. Może cię zdziwić, dla czego dzielimy 1193180 przez Hz. Już wyjaśniam: znowu względy
kompatybilności (aaargh !!!). 1193180 to była prawdopodobnie (nie jestem pewien) częstotliwość
jakiej karta CGA używała do synchronizacji. Narzekanie nic nie pomoże :).

Wklej tą procedurę do swojego OSa, można do pliku main.c. W dołączonym kodzie jest już to zrobione.
Po kbd_init(); w start_kernel() (main.c) wstaw ustaw_czestotliwosc_pit(100). Ustawi to nasz zegar systemowy
na 100Hz.

Zastosowanie przerwania zegarowego

Ciekawe do czego można zastosować przerwanie wykonywane 100x na sekundę? :))). Oczywiście do napisania
schedulera (czyli najprościej mówiąc podprogramu, który decyduje jakie zadanie ma być wykonane w danym
momencie). Można też wykonywać jakąś inną procedurę, np. wyświetlanie literek na ekranie. Jest to może
zbytnio wyszukane, ale trzeba od czegoś zacząć.

Piszemy procedurę obsługi przerwania zegarowego.

Nasza procedura będzie identyczna jak procedura obsługi klawiatury.


GLOBAL _irq0
_irq0:
push gs
push fs
push es
push ds
pusha
mov ax,0x10
mov ds,ax
mov es,ax
mov al,0x60
out 0x20,al
EXTERN _do_irq0
call _do_irq0
popa
pop ds
pop es
pop fs
pop gs
iret


Skopiuj ją zaraz po kodzie procedury irq1. W dołączonych plikach jak zwykle czeka gotowa wersja, aby ją
skompilować i odpalić :) (lenistwo rulez ;) ).

Teraz musimy "zainstalować" tę procedurę do IDT. Najpierw piszemy w main.c po includach


extern void irq0(void);


Po ustaw_czestotliwosc .... i przed sti() dajemy:


set_intr_gate(0x20,&irq0);


Teraz zróbmy sobie plik sched.c. Tam będzie się znajdował nasz przyszły scheduler.
Na razie napiszemy tylko kretyńską procedurę, która będzie wyświetlała kolejne znaki
ASCII z lewym górnym rogu ekranu. Ma ona postać:


void do_irq0(void)
{
__asm__ __volatile__("incb 0xb8000");
}


Zastosowałem asembler AT&T. W NASMie wyglądało by to tak:


inc byte [0xb8000]


UWAGA !!!
Nigdy nie stosuj samego __asm__, tylko __asm__ __volatile__, bo gcc tak Ci zoptymalizuje
program, że kod w assemblerze zniknie dla gcc (tak się dzieje w niektórych przypadkach)
i nie będzie go w kodzie wyjściowym.

Teraz dodajemy przed sti() w start_kernel():


enable_irq(0);


Kompilujemy kernel i odpalamy. Zapomniałem dodać, że do listy OBJS w Makefile dodajemy
także sched.o. Piszemy make i naciskamy enter.

Jak odpalisz kernela, w lewym górnym rogu ekranu będą migać literki. Jeśli tego nie widzisz,
to znaczy, że albo kod jest skopany, albo musisz kupić sobie okulary ;). Najbardziej prawdopodobna
jest ta pierwsza możliwość (kod zawsze testuję na moim PC i pod emulatorem, więc nie powinno być
problemów).

Wyjątki procesora

Teraz będzie kupa teorii. Nie ma lekko ;). Teoria też jest potrzebna w niektórych przypadkach.
Na początek pod obróbkę idzie lista wyjątków (na podstawie manuala do 386. W procesorach Pentium
i nowszych jest jeszcze kilka innych wyjątków - może je kiedyś opiszę.).


Numer Opis
0 Bład dzielenia
1 Wyjątek debug
2 NMI
3 Breakpoint (int 3)
4 Przepełnienie (instrukcja INTO)
5 Sprawdzenie granic (BOUND)
6 Błędna instrukcja
7 Koprocesor nie dostępny
8 Podwójny błąd
9 (zarezerwowane)
10 Błędny TSS
11 Segment nie obecny
12 Wyjątek stosu
13 Generalny bład ochrony
14 Bład strony
15 (zarezerwowane)
16 Bład koprocesora


Stos po wywołaniu wyjątku

Wyjątki dzielą się na takie co posiadają kod błędu i nie posiadają kodu błedu. Kod błędu jest to po prostu wartość na stosie. W [esp] jest kod błędu, w [esp+4] jest EIP, w [esp+8] jest CS, itd. Zawartość stosu jest dokładnie taka sama jak po wykonaniu przerwania.

Typy wyjątków

Wyróżniamy 3 typy wyjątków:

Faults

Wartości CS i EIP na stosie przerwania wskazują _NA INSTRUKCJĘ_, która wywołała błąd.

Traps

CS i EIP wskazują zaraz po instrukcji, która wywołała błąd. Jeśli ten typ wyjątku zostanie
wykonany podczas instrukcji, która zmienia bieg programu CS i EIP wskazują miejsce docelowe.
Na przykład instrukcja jmp narusza bieg programu. CS i EIP będą wskazywać na miejsce gdzie
miałby być wykonany skok.

Aborts

Wyjątek typu Abort nie wskazuje dokładnego miejsca gdzie mógł wystąpić błąd. Wyjątki tego typu
są używane, np. gdy procesor chce przekazać do programu gdy wystąpił błąd sprzętowy, np. w nowych
procesorach wyjątek Machine Check może mówić, że procesor się przegrzał.

Opisy wyjątków

Wyjątek 0

Błąd dzielenia może wystąpić, gdy wykonamy instrukcję DIV lub IDIV a dzielnikiem będzie liczba 0.

Wyjątek 1

Procesor wywołuje ten wyjątek z różnych powodów. To przerwanie jest typu Exception albo Fault.
Procesor nie kładzie na stos kodu błędu. Program może sprawdzić, co wywołało wyjątek za pomocą
rejestrów debugujących.

Wyjątek 3

Jest on wywoływany przez instrukcję int 3. Ma ona kod jednobajtowy. CS:EIP wskazuje na bajt
przed tą instrukcją. Instrukcja ta jest używana przeważnie przez debuggery.

Wyjątek 4

Ten wyjątek jest wywoływany, jeżeli wykonamy instrukcję INTO oraz flaga OF (overflow) jest ustawiona
w rejestrze FLAGS.

Wyjątek 5

Ten wyjątek zostanie wywołany, gdy wykonamy instrukcję BOUND, a jej parametry będą poza granicami
ustalonymi przez programistę.

Wyjątek 6

Ten wyjątek zostanie wywołany gdy jednostka wykonawcza procesora napotka niewłaściwą instrukcję.
Wyjątek zostanie wykonany gdy procesor spróbuje wykonać taką instrukcję. Wczesne pobieranie (ang.
prefetch) nie powoduje tego wyjątku. Procesor nie kładzie kodu błędu na stos.
Wyjątek zostanie także wywołany, gdy parametry instrukcji będą niepoprawne.

Wyjątek 7

Ten wyjątek może zostać wykonany w dwóch przypadkach:

Procesor wykona instrukcję ESC (escape), a flaga EM (emulate) jest ustawiona w rejestrze
sterowania 0 (CR0).

Procesor wykona instrukcję ESC lub WAIT oraz flagi MP (monitor coprocessor) i TS (task
switched) są ustawione w CR0.

Wyjątek 9

Wyjątek ten zostanie wykonany, gdy koprocesor matematyczny będzie próbował uzyskać dostęp do
pamięci w niewłaściwym obszarze.

Wyjątek 10

Wyjątek ten zostanie wykonany gdy wykonamy skok do błędnego TSS.

Wyjątek 11

Wyjątek ten zostanie wykonany, gdy spróbujemy uzyskać dostęp do nieistniejącego segmentu.

Wyjątek 12

Wyjątek ten zostanie wykonany, gdy załadujemy do SS niewłaściwą wartość lub stos przekroczy rozmiar
segmentu.

Wyjątek 13

Ten wyjątek zostanie wywołany, gdy zrobimy błąd, który nie wywołałby żadnego innego wyjątku, np.
przekroczenie limitu segmentu, gdy uzywany CS,DS,ES,FS,GS
przekroczenie limitu segmentu, gdy odwołujemy się do tablicy deskryptorów
zapis do segmentu kodu lub segmentu danych, który ma flagę read-only
ładowanie CR0 z flagami PG=1 i PE=0

Wyjątek 14

Ten wyjątek, to tzw. błąd strony. (zob. opis stronicowania, część 1).
Adres błędu strony jest zapisywany w rejestrze CR2. Na stosie zapisywany jest kod błędu.
O tym wyjątku będzie więcej, gdy będziemy pisać menedżer pamięci.

Wyjątki z kodem błędu i bez kodu błędu


Wyjątek Kod błędu
0 NIE
1 NIE
3 NIE
4 NIE
5 TAK
6 NIE
7 NIE
8 TAK
9 NIE
10 TAK
11 TAK
12 TAK
13 TAK
14 TAK


Dodajemy obsługę wyjątków

Wystarczy tej teorii. Jak zwykle polecam manual Intela do 386 :))) (dla tych, którzy chcą dokładne
opisy).

Teraz dodamy obsługę wyjątków do naszego mini-kernela. Na razie jądro tylko wyświetli błąd i się
zawiesi. Więcej nam nie potrzeba ;) (na razie nie mamy zaprogramowanej wielozadaniowości, więc
lepsza jakościowo obsługa błędów na nic nam się nie przyda).
Stworzymy plik exc.asm, który będzie zawierał kod procedur obsługi błędów napisany w assemblerze.
Będzie on wywoływał procedury napisane w C. Od razu będziemy mieli przygotowaną podstawę pod
trudniejsze rzeczy, bez późniejszego przepisywania kodu na nowo.

Procedury obsługi wyjątków będą identyczne jak te, które służą do obsługi przerwań, będą tylko
bardziej zaawansowane :).

Na początek będzie coś takiego (oczywiście w pliku exc.asm) :


[section .text]
[bits 32]

EXTERN _do_exc0
EXTERN _do_exc1
EXTERN _do_exc2
EXTERN _do_exc3
EXTERN _do_exc4
EXTERN _do_exc5
EXTERN _do_exc6
EXTERN _do_exc7
EXTERN _do_exc8
EXTERN _do_exc9
EXTERN _do_exc10
EXTERN _do_exc11
EXTERN _do_exc12
EXTERN _do_exc13
EXTERN _do_exc14

exception_table:
dd _do_exc0,_do_exc1,_do_exc2,_do_exc3,_do_exc4
dd _do_exc5,_do_exc6,_do_exc7,_do_exc8,_do_exc9
dd _do_exc10,_do_exc11,_do_exc12,_do_exc13,_do_exc14


Co to jest EXTERN, chyba nie muszę już tłumaczyć (to są podstawy :)). Nasze procedury
w C będą się nazywać do_exc0,do_exc1,do_exc2,...,do_exc14.


UWAGA !!! WARNING !!! ACHTUNG !!!

Przerywamy program aby nadać ważny komunikat:

Zapamiętajcie to raz na zawsze, bo nie będę powtarzać ;) Kompilatory
pod Linuxa generują nazwy bez prefixów "_". Kompilatory pod DOS/Win
(ogólnie, które używają formatów COFF lub PE-COFF, OBJ) generują nazwy
w assemblerze, które zawierają dodatkowo prefix "_". Czyli DJGPP wygeneruje
z void test(void) kod w assemblerze: _test: . Dla tego przy EXTERN są
znaczki "_" przed każdą nazwą (także w start.asm). Dostawałem wiele maili,
dla czego to się nie linkuje pod Linuxem.

Dla Linuxa:
W Makefile zmieniamy -f coff na -f elf przy nasmie.
W kodach źródłowych w assemblerze usuwamy wszystkie
prefixy "_" w nazwach, które zostaną użyte w kodzie C,
lub które są zaimportowane z kodu skompilowanego przez
kompilator C.


Następnie zapisujemy wskaźniki do tych wszystkich _exc_ w tablicy o nazwie exception_table.
Przyda się to nam później, gdy napiszemy właściwy program obsługi wyjątków (czyli za chwilę).
Teraz będzie trochę długa lista:


GLOBAL _exc0
_exc0:
push dword 0
push dword 0
jmp handle_exception


GLOBAL _exc1
_exc1:
push dword 0
push dword 1
jmp handle_exception

GLOBAL _exc2
_exc2:
push dword 0
push dword 2
jmp handle_exception

GLOBAL _exc3
_exc3:
push dword 0
push dword 3
jmp handle_exception

GLOBAL _exc4
_exc4:
push dword 0
push dword 4
jmp handle_exception

GLOBAL _exc5
_exc5:
push dword 5
jmp handle_exception

GLOBAL _exc6
_exc6:
push dword 0
push dword 6
jmp handle_exception

GLOBAL _exc7
_exc7:
push dword 0
push dword 7
jmp handle_exception

GLOBAL _exc8
_exc8:
push dword 8
jmp handle_exception

GLOBAL _exc9
_exc9:
push dword 0
push dword 9
jmp handle_exception

GLOBAL _exc10
_exc10:
push dword 10
jmp handle_exception

GLOBAL _exc11
_exc11:
push dword 11
jmp handle_exception

GLOBAL _exc12
_exc12:
push dword 12
jmp handle_exception

GLOBAL _exc13
_exc13:
push dword 13
jmp handle_exception

GLOBAL _exc14
_exc14:
push dword 14
jmp handle_exception


Umieszczony tutaj kod zapoda procedurze handle_exception (która będzie obsługiwać
wyjątki) numer wyjątku. Jeśli wyjątek nie posiadał kodu błędu na stos zostanie
umieszczona liczba 0. Jeśli wyjątek posiadał kod błędu, na stos położony zostanie
wyłącznie jego numer. Dalej sterowanie przechodzi do handle_exception.

Procedury exc* musimy ustawić w kodzie c przy użyciu set_trap_gate oraz
set_system_gate (tylko te dla debugowania).

Teraz pora na handle_exception:


handle_exception:
xchg eax,[esp]
xchg ebx,[esp+4]
push gs
push fs
push es
push ds
push ebp
push edi
push esi
push edx
push ecx
push ebx
mov ecx,0x10
mov ds,cx
mov es,cx
call dword [exception_table+eax*4]
pop eax
pop ecx
pop edx
pop esi
pop edi
pop ebp
pop ds
pop es
pop fs
pop gs
pop eax
pop ebx
iret


Procedura ta jest trochę bardziej skomplikowana niż procedury obsługi przerwania
w start.asm.

Jak wiadomo po wywołaniu handle_exception mamy 2 wartości na stosie. My zrobimy
małą podmianę. Zmienimy wartości ([esp] z eax oraz [esp+4] z ebx). Teraz mamy
w ebx kod błędu a w eax numer wyjątku (sprytne ... ;) ). Potem jak zwykle
rejestry gs,fs,es,ds,ebp,edi,esi,edx,ecx na stos. Ktoś może zapytać po co dajemy
drugi raz ebx na stos. Odpowiadam: edx jest teraz parametrem dla funkcji w C.

Następnie wywołujemy wskaźniki z exception_table+eax*4, czyli czytamy wskaźniki
do procedur po numerach wyjątków i wykonujemy procedurkę w C. Potem dla czego
zdejmujemy ze stosu najpierw eax ? Ponieważ procedura w C i tak zmodyfikuje
ten rejestr (w eax procedury w C zwracają wartości), a nie jest jego wartość
nam do szczęścia potrzebna. Dalej zdejmujemy ze stosu rejestry ecx,edx,esi,edi,
ebp,ds,es,fs,gs. Na końcu eax oraz ebx, ponieważ "podmieniliśmy" ich wartości
na początku naszej procedurki. Teraz standardowo iret i wracamy do programu.

UWAGA: Na razie kod powrotu i tak nie zadziała, ponieważ nasze procedury w C
wyświetlą komunikat o błędzie i zawieszą komputer. Jak? np. parą cli i hlt.
cli blokuje przerwania, a hlt wstrzymuje procesor, dopóki nie nadejdzie
przerwanie (czyli do momentu, gdy naciśniesz reset na obudowie lub wyłączysz
i włączysz komputer. Kombinacja Ctrl+Alt+Del nie zadziała, ponieważ ona jest
tylko programowa, tzn. mogę sobie w sterowniku klawiatury zażyczyć, żeby po
naciśnięciu Ctrl+Alt+Del był wyświetlany komunikat).

Piszemy procedury w C

Teraz przyszła kolej na procedury w C. Nie będę ich tu opisywał, ponieważ nie
ma sensu, a wszystko będzie w pliku traps.c.

TSS

TSS to skrót od Task State Segment. Procesor zapisuje w nim stan aktualnie wykonywanego
procesu. TSS zawiera praktycznie wszystkie rejestry procesora oraz kilka innych rzeczy.
Struktury tej używa się głównie, aby zaprogramować wielozadaniowość. Procesor korzysta
z zawartych w niej danych np. przy przejściach między różnymi poziomami uprzywilejowania.

Pola TSS (wszystkie wartości typu dword):


back_link
esp0
ss0
esp1
ss1
esp2
ss2
cr3
eip
eflags
eax,ecx,edx,ebx
esp,ebp,esi,edi
es,cs,ss,ds,fs,gs
ldt
trace (word)
bitmap (word)


Teraz opiszę po kolei pola:

back_link

Pole to jest ustawiane jedynie przez procesor. Wskazuje ono na poprzednio użyty TSS.
Gdy flaga NT (Nested Task) jest ustawiona, procesor przy wykonaniu instrukcji iret skoczy
do TSS używając pola back_link.

esp0,ss0
Stos dla poziomu uprzywilejowania 0. Są to rejestry stosu, czyli ss i esp, które zostaną załadowane
po przejściu na poziom uprzywilejowania 0 z poziomu niższego. Jest to bardzo przydatne, ponieważ
gdy pracujemy na poziomie 3 i nie mamy ustawionego stosu, wywołanie przerwania nie wysypie
nam programu.

esp1,ss1 i esp2,ss2
Podobnie jest z esp0 i ss0.
Ktoś może się zapytać gdzie esp3 i ss3 ? Odpowiadam: nie ma niższego poziomu niż 3, więc nie można przejść
na poziom 3 z poziomu 4 :), więc w procesorze nie zaimplementowano esp3 i ss3.

cr3
Adres naszego katalogu stron.

eip,eflags,eax,ecx,edx,ebx,esp,ebp,esi,edi
To chyba każdy się domyśli :).

bitmap
Zawiera wskaźnik do tzw. I/O permission bitmap. Offset ten musi być względem początku TSS a nie
adres fizyczny w pamięci !!! Gdy nie chcemy, żeby procesor używał bitmapy I/O, to ustawiamy offset
WIĘKSZY niż rozmiar segmentu TSS ustawiony w GDT.

To tylko wstęp do TSS. Nie ma sensu robić dokładnych opisów. Wszystko dowiecie się w trakcie
kursu, tzn. np. przy pisaniu menedżera pamięci lub wielozadaniowości z TSS. Nie ma sensu
ładować całej wiedzy na raz. Będę wyjaśniał tylko to, co jest nam w danym momencie potrzebne.

FAQ

Tutaj zamieszczam odpowiedzi na kilka pytań, które zadawali mi przyszli programiści OSów :).

Co to jest selektor ?
Selektor jest to tylko numer. Podobnie jak w trybie rzeczywistym numer segmentu. Różnica jest
taka, że w trybie chronionym nie możesz wpisać dowolnej wartości.

Kiedy będzie następna część ?
Nie wiem. Jak dostanę wenę twórczą i nie będę zajęty pisaniem własnego OSa, ale prawdopodobnie następna część już w następnym numerze magazynu!

Co to jest DJGPP ?
Jest to port kompilatora GCC pod DOS wykonany przez DJ Delorie.

Errata

W pierwszej części pomyliłem się w opisie ładowania GDT z trybu rzeczywistego. Nie trzeba
liczyć adresu liniowego GDT w pamięci, tylko np. wystarczy:


lgdt [cs:moj_gdt]


Zakończenie

UWAGA !!!
Jeśli zauważysz błędy rzeczowe - pisz !!! Zawsze mogę się gdzieś pomylić. Człowiek nie
jest nieomylny, a pisanie OSów to szczególnie trudna dziedzina informatyki.

Pliki

Tutaj zamieszczam archiwum z plikami stworzonymi w tej części kursu - ARCHIWUM








  Prenumerata






Jarosław Pelczar








Wyszukiwarka

Podobne podstrony:
os path
Acronis OS Selector 5
Acronis OS Selector 8
os liczbowa do 20 1
notes specifiques os
wyklady os
Mac OS X Leopard cwiczenia praktyczne cwmacl
odpady do wykorzystania dla os b fizycznych
G500 OS?? update notification
os
os weka3 pdf
indeks zagrożone os ,

więcej podobnych podstron