Programming4Fun

Programming4Fun - programujemy z pasją :)

  • Nie jesteś zalogowany.

Ogłoszenie


  • Index
  •  » Poradniki
  •  » Pisanie systemów operacyjnych cz. I - tryb rzeczywisty

#1 2014-07-10 11:06:02

 arnon

http://winclub.pl/public/style_extra/team_icons/admin1.png

Skąd: Rzeszów
Zarejestrowany: 2014-07-08
Posty: 56
Punktów :   

Pisanie systemów operacyjnych cz. I - tryb rzeczywisty

Jak napisać własny, PRAWDZIWY SYSTEM OPERACYJNY - TAKI CO MA JĄDRO I MOŻNA NORMALNIE ZAINSTALOWAĆ NA KOMPUTERZE!!!!!!
Kurs napisał użytkownik "Wolverine", ja tylko ten kurs tutaj re-uploaduję. Mam nadzieję, że wam się spodoba


Na początek chciałbym wyjaśnić co skłoniło mnie do napisania tego kursu, bo przecież w sieci jest tego pełno. Powodem jest to, że żaden kurs nie odpowie ci na możliwie dużą ilość pytań, które pojawiają się przy pierwszym kontakcie z pisaniem OSa. Omówię tu jedynie elementarne podstawy w sztuce pisania systemów operacyjnych, które mogą jedynie posłużyć tym, że łapiąc za inny kurs będziesz znał te podstawy. Jeśli jednak tekst będzie do bani - zawsze jest przycisk usuń

01h. Założenia.

Nasz mini OS nie będzie robił nic poza wyświetleniem komunikatu i restartem komputera po naciśnięciu klawisza Escape, więc nie, nie zostaniesz Billem tak szybko , za to zrozumiesz każdą napisaną linijkę twojego OSa (mam nadzieję). Będzie posiadał bootloadera, który załaduje i uruchomi kernela, wszystko będzie pracowało w trybie Real Mode.

02h. Co nam będzie potrzebne?

Ponieważ nasz OS będzie w całości napisany w assemblerze potrzebujemy kompilatora. Ja wybrałem Netwide Assembler (http://nasm.sourceforge.net/), który jest prosty, dobry a co najważniejsze darmowy. Dodatkowo możesz zaopatrzyć w program Lizard, który zawiera NASMa i wygodny edytor (http://4programmers.net/file.php/id=1766). Będziemy potrzebować również aplikacji do zapisywania obrazów na dyskietkę, polecam Rawrite (http://uranus.it.swin.edu.au/~jn/linux/rawwrite.htm). Ponieważ nie będziemy implementować obsługi systemu plików (nawet minimalnej do znalezienia kernela na dyskietce) będziemy musieli potrafić łączyć dwa pliki w jeden (program, który to zrobi napiszemy w delphi). Oczywiście moglibyśmy połączyć bootloadera i kernela w jeden plik z poziomu kodu asm, ale chodzi tu o ukazanie istoty bootloadera i kernela jako oddzielnych programów.

Kod:

program merge;
 
{$APPTYPE CONSOLE}
 
var
  final, f: file of byte;
  buf, q: byte;
begin
  if (paramcount < 3) or (paramstr(1) = 'help') then begin
    writeln('uzycie: merge.exe <1 plik> <2 plik> <3...> <plik wynikowy>');
    readln;
  end else begin
    assignfile(final, paramstr(paramcount));
    rewrite(final);
    for q := 1 to paramcount -1 do begin
      writeln(paramstr(q));
      assignfile(f, paramstr(q));
      reset(f);
      while not eof(f) do begin
        blockread(f, buf, sizeof(buf));
        blockwrite(final, buf, sizeof(buf));
      end;
      closefile(f);
    end;
    closefile(final);
  end;
  writeln('-> ' + paramstr(paramcount));
end.

Myśle, że kodu nie trzeba objaśniać, między innymi dlatego, że nie jest to cześć naszego OSa a jedynie narzędzie przy jego tworzeniu.

03h. Budowa dyskietki 1.44.

Przed jakimikolwiek operacjami na nośniku danych musimy poznać jego budowę. My nasz system umieścimy na dyskietce 1.44 i dlatego w skrócie opisze tu jej budowę. Wielkiej filozofii tutaj nie ma, bo taka dyskietka posiada dwie strony (stacja ma dwie głowice), po 80 cylindrów po każdej stronie i 18 sektorów w każdym cylindrze. Każdy sektor może pomieścić 512b danych (2*80*18*512 = 1474560b). Pierwszy sektor nazywamy bootsektorem, ponieważ to właśnie w nim znajduje się program inicjujący system operacyjny, jak również informacje o systemie plików itp. Kolejne sektory są tylko dla nas i możemy umieszczać tam co tylko chcemy, na przykład kernela.

04h. Basic Input/Output System.

BIOS (Basic Input Output System) jest najbardziej podstawowym systemem jaki mamy w naszych komputerach, to dzięki niemu będziemy mogli wyświetlić tekst na ekranie (oczywiście możemy odwołać się bezpośrednio do pamięci, ale mi zależy bardziej na prostocie), czy wczytać kernela z dyskietki - od razu zapomnij o wyświetlaniu np listy plików etc. to musielibyśmy zrobić sami (może innym razem), dzięki niemu będziemy mieli jedynie dostęp do poszczególnych części nośnika. I w końcu to właśnie BIOS rozpoznaje, ładuje do pamięci bootsektor i go uruchamia.

05h. W jaki sposób BIOS ładuje system, czyli wreście coś konkretnego.

Większość z nas wie, że w programie Setup (taka część BIOSa) możemy skonfigurować kolejność napędów, na których komputer będzie szukał systemu. BIOS ładuje pierwsze 512 bajtów z dysku (dyskietki, płyty CD/DVD czy czegokolwiek innego) do pamięci pod adresem 07C0:0000 i skacze w to miejsce (wykonuje znajdujący się tam kod). Aby to nastąpiło BIOS musi wiedzieć, czy pierwsze pół kilobajta jest na pewno bootloaderem (bo przecież komendy procesora /opcodes/ to przecież to samo co inne dane - bajty, a nie na każdym dysku musi być system), w tym celu sprawdzane są 511 i 512 bajt, które powinny wynosić odpowiednio 55h i AAh.

06h. Więc piszemy bootloadera!

Zacznijmy od najprostszego kodu, który po skompilowaniu da nam gotowy do zapisania bootsektor.

Kod:

;tutaj zaczyna się nasz program
org 7C00h
 
;tworzymy nieskończoną pętlę
start:
  jmp start
 
;dopełniamy program do 510 bajtów
times 510 - ($ - start) db 0
 
;tworzymy znacznik bootsektora
db 55h
db AAh

Taki program kompilujemy jako flat-binary (nasm.exe bootsector.asm -f bin -o bootsector.bin /program Lizard utworzy plik *.com/) i za pomocą np Rawrite umieszczamy na dyskietce (traktując skompilowany plik jako jej obraz). Teraz mamy na dyskietce najprawdziwszy bootsektor, który sami napisaliśmy .

OK, ale obiecałem system wyświetlający napis więc jedziemy dalej. Jak już wiemy bootloader służy jedynie do załadowania kernela (w dzisiejszych systemach preloadera, a ten dopiero kernela, ale my zrobimy to łatwiej), tak więc co zrobimy? Otóż użyjemy funkcji BIOSa aby załadować kernela pod adres 0800:0000 (między bootloaderem a kernelem będziemy mieli 512 wolnych bajtów, które wykorzystamy jako stos, ale o tym później). Aby uprościć sobie sprawę nasz kernel nie będzie typowym plikiem, tylko kodem binarnym w 2 sektorze dyskietki (pamiętasz, w 1 jest nasz bootloader). Użyjemy do tego celu funkcji numer 2 przerwania 13h. Więc do dzieła, najpierw zobaczmy czego wymaga od nas ta funkcja (http://www.ctyme.com/intr/rb-0607.htm), tak więc

Kod:

mov ah, 2      ;funkcja 2 przerwania 13h
mov al, 10     ;ilość sektorów do przeczytania (10*512 = 5kb)
mov ch, 0      ;cylinder
mov cl, 2      ;sektor (w 1 jest bootsector)
mov dh, 0      ;głowica
mov bx, 0800h  ;gdzie załadowac kernel (es:bx)
mov es, bx     ;dane do ES możemy umieścić tylko przez inny rejestr
xor bx, bx     ;bx równy 0
int 13h        ;wywołujemy przerwanie

Podsumowując załadowaliśmy 5kb z dyskietki pod adres 0800:0000, aby uruchomić znajdujący się tam kod, po prostu skaczemy do niego

Kod:

jmp 0800h:0000h

Cały bootloader wygląda tak

Kod:

org 7C00h
 
start:
  mov ah, 2
  mov al, 10
  mov ch, 0
  mov cl, 2
  mov dh, 0
  mov bx, 0800h
  mov es, bx
  xor bx, bx
  int 13h
 
  jmp 0800h:0000h
 
times 510 - ($ - start) db 0
 
db 55h
db 0AAh

07h. Szkielet jądra systemu.

Pisząc jądro systemu nie musimy się już martwić znacznikiem, ani tym, że nasz kod musi zmieścić się w 512b, po prostu kodujemy . Jednak mamy pewien haczyk, otóż pisząc wlany system musimy pamiętać, że nie będziemy mieli dobrodziejstw wysokopoziomowych funkcji załadowanego systemu, bo - go nie ma. Czyli musimy zapomnieć np o przerwaniu 21h. Ale nie martw się, BIOS oferuje wystarczającym dla nas zestawem funkcji, a z resztą przecież obiecałem . Pierw napiszemy najprostszy kod jądra, który jedyne co będzie robił to...działał

Kod:

; skąd ja to znam
;pamiętasz, kernela załadowaliśmy pod adres 0800h:0000h, wiec
;zaczynamy od 0000h
org 0000h
 
start:
  jmp start

Teraz kompilujemy bootloadera i kernela, i łączymy je w jeden plik używając programu merge.exe, który napisaliśmy i skompilowaliśmy na początku tego tekstu (skompilowaliśmy?)

Kod:

merge.exe bootsector.bin kernel.bin image.img

Po czym zapisujemy otrzymany plik na dyskietkę programem Rawrite traktując go jako obraz.

Na pozór działanie naszego systemu nie różni się od samego szkieletu bootloadera, lecz wykonuje on następujące czynności: ładuje kernela z 2 sektora dyskietki do pamięci pod adres 0800:0000 i przekazuje mu kontrole, po czym ten zapętla komputer . Należałoby tez wytłumaczyć dlaczego zapętamy komputer, więc gdybyśmy tego nie zrobili procesor powędrował by dalej i wykonywał przypadkowe komendy znajdujące się na jego drodze (czyli tam gdzie wskazuje IP) co jest nieprzewidywalne w skutkach.

08h. Co ze stosem?

Stos jest takim kawałkiem pamięci do którego mamy szybki dostęp, możemy w nim przechowywać parametry procedury, jej adres powrotny i zmienne lokalne. Bez stosu za wiele byśmy nie zrobili, wiec przydzielimy mu teraz pamięć. Rzecz ta jest trywialnie prosta, bo ogranicza się jedynie do ustawienia jego segmentu, czyli rejestru ss i jego wierzchołka, czyli rejestru sp. Przy ładowaniu kernela do pamięci zostawiliśmy lukę w pamięci o rozmiarze 512b, tam właśnie będzie znajdował się nasz stos.

Kod:

+-------------------+
| 512b | BOOTLOADER |
|------+------------|
| 512b | STOS       |
|------+------------|
| 5kb  | KERNEL     |
+-------------------+
Mapa pamięci naszego OSa

Powyższa mapa pozwoli ci lepiej zrozumieć podział pamięci w naszym systemie. Skoro juz wszystko wiadomo zajmijmy się kodowaniem.

Kod:

mov ax, 07C0h
mov ss, ax    ;segment stosu
mov sp, 03FEh ;wierzcholek stosu

Jak łatwo się domyślić po tym kroku mamy dostępny 512 bajtowy (256 slow) stos.

09h. Wybieramy tryb graficzny.

Co prawda interesujący tryb graficzny (tekstowy 80x25) jest juz wybrany, ale wybierzemy go jeszcze raz, z dwóch powodów. Pierwszy to taki, ze po wybraniu trybu bufor ekranu się wyczyści, a drugi - będziemy wiedzieć jak to się robi . Aby to zrobić wystarczy wywołać przerwanie 10h.

Kod:

xor ah, ah  ;funkcja 0
mov al, 3   ;standardowy tryb tekstowy
int 10h     ;jedziemy

0Ah. Wyświetlamy tekst.

Wyświetlanie tekstu jest chyba najbardziej skomplikowana rzeczą w naszym systemie, a to dlatego, ze musimy w pętli przenieść wszystkie znaki naszego łańcucha na ekran i przestać w odpowiednim momencie. Do tego musimy przesunąć kursor na ekranie. Do zapisania łańcucha użyjemy "formatu" PChar, który jest ciągiem bajtów zakończonym bajtem #0 (NULL). Pozwoli to nam na wykrycie końca naszego tekstu.

Na początek napiszemy procedurę wyświetlającą jeden znak na ekranie i przesuwająca kursor. Jak zwykle posłużymy sie przerwaniami, które są po prostu łatwe w obsłudze. Do wyświetlenia znaku skorzystamy z funkcji 9 przerwania 10h, za parametry przyjmuje ona wartość ASCII znaku (rejestr AL), numer strony (BH i u nas będzie to 0), atrybut znaku (jego kolor, kolor tła, czy znak ma migać - rejestr BL) i ilość powtórzeń (CX). Po wyświetleniu znaku przesuniemy kursor w prawo funkcjami 2 (zapisz pozycje) i 3 (odczytaj pozycje). Obie swoje parametry maja w DH (wiersz) i DL (kolumna), z jakim wyjątkiem, ze 2 je zapisuje a 3 pobiera.

Kod:

char:
  ;procedura wyświetla znak i przesuwa kursor
  ;wejście: al: znak, bl: atrybut
 
  push bx     ;kładziemy BX na stos, aby na końcu procedury go przywrócić
 
  mov ah, 9   ;numer funkcji wyświetlającej znak w miejscu kursora
  xor bh, bh  ;numer strony ustawiamy na 0
  mov cx, 1   ;znak wyświetlimy 1 raz
  int 10h     ;do dzieła!
 
  ;pobierz pozycje
  mov ah, 3   ;funkcja pobierania pozycji kursora
  xor bh, bh  ;numer strony (0)
  int 10h     ;odczytaj pozycje
 
  ;dodaj i zapisz pozycje
  add dl, 1   ;dodajemy 1 kolumnie
  mov ah, 2   ;funkcja zapisywania
  int 10h     ;zapisz pozycje
 
  pop bx      ;przywróć poprzedni stan BX
ret           ;wyjdź z podprogramu

Teraz, gdy mamy juz procedurę do wyświetlania znaku napiszemy procedurę, która wyświetli nam cały łańcuch. Będzie ona polegała na tym, ze w pętli będziemy wyświetlać kolejne bajty danych zaczynając od adresu pierwszego znaku podanego w parametrze (u nas będzie to AX) aż do wystąpienia znaku pustego - NULL (to nie jest spacja!). Znamy juz teorie, teraz przeniesiemy ja do assemblera:

Kod:

write:
  ;procedura wyświetla tekst na ekranie
  ;wejście: ax: wskaźnik do tekstu, bl: atrybut
 
  mov si, ax        ;musimy użyć rejestru segmentowego aby zaadresować wskaźnik
  .next:            ;poczatek petli
    mov al, [cs:si] ;zapisz do al wartość aktualnego znaku (patrz parametry dla procedury char)
    cmp al, 0       ;porównaj aktualny znak z NULL
    je end          ;jeśli są równe, skocz do wyjścia
    call char       ;jeśli nie, wyświetl znak
    add si, 1       ;przesuń się w prawo do następnego znaku
  jmp .next         ;skocz do początku pętli
  end:              ;tutaj skoczymy, jeśli wystąpi NULL
ret                 ;wyjdź z podprogramu

I juz możemy zadeklarować łańcuch i go wyświetlić.

0Bh. "Obsługujemy klawiaturę"

Tytuł w cudzysłowie ponieważ nasza klawiatura nie będzie miała nawet własnego bufora, za to będziemy wiedzieli, kiedy użytkownik naciśnie klawisz, będziemy nawet wiedzieli jaki! A na to "wszystko" pozwoli nam funkcja 0 przerwania 16h (http://www.ctyme.com/intr/rb-1754.htm). Zalożenie będzie takie, że w pętli będziemy odczytywać klawisz i sprawdzać który to. Więc do dzieła

Kod:

start:
  xor ah, ah   ;takie xorowanie jest szybsze od mov ah, 0
  int 16h      ;i w AH mamy scancode, w AL kod ASCII klawisza
  cmp al, 1Bh  ;porównaj al z 1Bh (kod ASCII klawisza ESC)
  je reset     ;jeśli równe, skocz do procedury resetowania (napiszemy później)
  jmp start    ;powracamy na początek

0Ch. Resetujemy komputer.

Resetowanie komputera będzie polegać na skoku w cześć BIOSu, którą wykonuje sie po starcie komputera. Jest to adres FFFF:0000. Po drodze do komórki 40:72 zapisujemy wartość, która "powie" BIOSowi, czy ma wykonywać ponownie testy pamięci etc. Aby to zrobił przypisujemy do niej wartość 0, jeśli nie (tzw. gracy reset) wartość 1234h. Po tym po prostu skaczemy do FFFF:0000. Jak zwykle kod

Kod:

reset:
  mov bx, 40h               ;używamy BX do zapisania wartości w rejestrze segmentowym
  mov ds, bx                ;BX ładuje w DS
  mov word [ds:72h], 1234h  ;ustawiamy gorący reset
  jmp 0FFFFh:0000h          ;skaczemy do FFFF:0000

0Dh. Sklejamy wszystko w kupe.

Gdy juz mamy wszystko czego potrzebowaliśmy, możemy złożyć nasz pierwszy (pierwszy?) mini system operacyjny. Kompilowanie bootloadera i kernela opisałem wcześniej, wiec nie pozostaje mi nic innego jak podać kompletny kod jadra

Kod:

org 0000h
 
;ustawiamy stos
mov ax, 07C0h
mov ss, ax    ;segment stosu
mov sp, 03FEh ;wierzchołek stosu
 
;wybieramy tryb ekranu
xor ah, ah  ;funkcja 0
mov al, 3   ;standardowy tryb tekstowy
int 10h     ;jedziemy
 
;wyświetlamy komunikat
mov ax, welcome  ;wskaźnik do tekstu
mov bl, 2        ;na zielono
call write       ;wykonujemy procedurze
 
mov ax, name     ;wskaźnik do tekstu
mov bl, 5        ;na fioletowo
call write       ;wykonujemy procedurę
 
mov ax, last     ;wskaźnik do tekstu
mov bl, 2        ;na zielono
call write       ;wykonujemy procedurę
 
;główna petla
start:
  xor ah, ah   ;takie xorowanie jest szybsze od mov ah, 0
  int 16h      ;i w AH mamy scancode, w AL kod ASCII klawisza
  cmp al, 1Bh  ;porownaj al z 1Bh (kod ASCII klawisza ESC)
  je reset     ;jeśli równe, skocz do procedury resetowania (napiszemy później)
  jmp start    ;powracamy na początek
 
char:
  ;procedura wyświetla znak i przesuwa kursor
  ;wejście: al: znak, bl: atrybut
 
  push bx     ;kładziemy BX na stos, aby na końcu procedury go przywrócić
 
  mov ah, 9   ;numer funkcji wyświetlającej znak w miejscu kursora
  xor bh, bh  ;numer strony ustawiamy na 0
  mov cx, 1   ;znak wyświetlimy 1 raz
  int 10h     ;do dzieła!
 
  ;pobierz pozycje
  mov ah, 3   ;funkcja pobierania pozycji kursora
  xor bh, bh  ;numer strony (0)
  int 10h     ;odczytaj pozycje
 
  ;dodaj i zapisz pozycje
  add dl, 1   ;dodajemy 1 kolumnie
  mov ah, 2   ;funkcja zapisywania
  int 10h     ;zapisz pozycje
 
  pop bx      ;przywróć poprzedni stan BX
ret           ;wyjdź z podprogramu
 
write:
  ;procedura wyświetla tekst na ekranie
  ;wejście: ax: wskaźnik do tekstu, bl: atrybut
 
  mov si, ax        ;musimy użyć rejestru segmentowego aby zaadresować wskaźnik
  .next:            ;początek pętli
    mov al, [cs:si] ;zapisz do al wartość aktualnego znaku (patrz parametry dla procedury char)
    cmp al, 0       ;porównaj aktualny znak z NULL
    je end          ;jeśli są rożne, skocz do wyjścia
    call char       ;jeśli nie, wyświetl znak
    add si, 1       ;przesuń się w prawo do następnego znaku
  jmp .next         ;skocz do początku pętli
  end:              ;tutaj skoczymy, jeśli wystąpi NULL
ret                 ;wyjdź z podprogramu
 
reset:
  mov bx, 40h               ;używamy BX do zapisania wartości w rejestrze segmentowym
  mov ds, bx                ;BX ładuje w DS
  mov word [ds:72h], 1234h  ;ustawiamy gorący reset
  jmp 0FFFFh:0000h          ;skaczemy do FFFF:0000
 
;zmienne
welcome: db 'Witaj w ',0
name:    db 'Krzeslo Operating System',0
last:    db ', wciśnij ESC aby zrestartowac komputer :)',0

Taki kod kompilujemy, wraz z bootloaderem umieszczamy na dyskietkę za pomocą Rawrite, restartujemy komputer i cieszymy się naszym dziełem.

0Eh. Co dalej?

Jeśli naprawdę myślisz o pisaniu systemów operacyjnych (właściwie jednego na całe życie) to ten artykuł jest dopiero jednym centymetrem w kilometrach, które cię czekają na drodze do napisania funkcjonalnego OSa. Czynność ta jest nie tylko pracochłonna, ale stwarza wiele problemów, których czasem nie można rozwiązać (jest to bardzo trudne).

Jeśli to cię nie zniechęciło pamiętaj o podstawowych zasadach pisania systemów. Po pierwsze nie myśl o stworzeniu super systemu, tylko o tym, żeby on działał. Wielu ludzi przedstawia bitmapy prezentujące bajeranckie GUI i myśli, ze polowa pracy za nimi, pierwsze co należy zrobić to działające jadro i powłokę tekstowa (konsole), która potrafi przyjmować komendy od użytkownika. Musisz tez zaplanować w jaki sposób twój system ma uruchamiać programy, musisz przydzielić im pamięć, stos, napisać mechanizm kolejkowania zadań. Do tego należy obsłużyć system plików (istniejący, lub wymyslec nowy) i mase innych rzeczy.

0Fh. Zakończenie

Mam nadzieję, że osiągnąłem cel, jakim było pokazanie jak napisać swój własny mini (mikro) system operacyjny podzielony na bootloadera i jądro. Postaram się w miarę nowych pomysłów i wiedzy dopisywać kolejne części . Będę wdzięczny za wszelkie uwagi, komentarze odnośnie tego artykułu, które możesz kierować pod maila, ew. na ircu (#netsoft @ irc.ircnet.pl).

Pozdrowienia dla wszystkich użytkowników 4p i deweloperów grupy Netsoft.
Wolverine (wolverine at daminet dot pl).


XD

Offline

 
  • Index
  •  » Poradniki
  •  » Pisanie systemów operacyjnych cz. I - tryb rzeczywisty

Stopka forum

RSS
Powered by PunBB
© Copyright 2002–2008 PunBB
Polityka cookies - Wersja Lo-Fi

[ Generated in 0.164 seconds, 8 queries executed ]


Darmowe Forum | Ciekawe Fora | Darmowe Fora
www.djronnie.pun.pl www.talesofshinobi.pun.pl www.chemiaumcs.pun.pl www.chomiki-pbf.pun.pl www.sgaming.pun.pl