Kurs pisania systemu operacyjnego, część 1

Marooned

Zapewne nieraz zastanawiałeś się, w jaki sposób jest zbudowany system operacyjny - być może
uważasz to za niezwykle trudną, bliską magii rzecz, albo też mniej więcej wiesz jak to działa,
ale nie znasz szczegółów. W obu przypadkach ten kurs powinien być pomocny :)

Co będzie potrzebne:

  • podstawowa znajomość assemblera x86 (32 bitowego, 16 wskazany)
  • komputer, co najmniej pentium 100 (dla emulatora pc), jednak system można stworzyć nawet na 386
  • assembler Fasm, można go ściągnąć z www.flatassembler.org

Przydatny będzie jakikolwiek emulator x86:

W tym kursie będę opierał się na Bochs.

     1 1. Segmenty w trybie chronionym - Global Descriptor Table
     2 2. Bootsector - odczytywanie dyskietki i odblokowanie a20
          2.1 Linia A20
     3 3. Przejscie do trybu chronionego i wypisanie napisu
     4 4. Przerwania programowe - ustawianie Interrupt Descriptor Table
               4.1.1 Konfiguracja Bochs
               4.1.2 Nagranie systemu na dyskietke i dysk<
               4.1.3 Plik os.inc

1. Segmenty w trybie chronionym - Global Descriptor Table

Segmenty w trybie chronionym są podobne do swoich kuzynów z trybu rzeczywistego.
W trybie rzeczywistym wyglądało to tak:

segment:offset

Prawidłowy, 20 bitowy adres procesor otrzymywał poprzez pomnozenie segmentu przez 16
(czyli przesuniecie bitowe w lewo o 4) i dodanie do powstałej wartości offsetu.
Czyli np 0x0100:0x543 to:

0x31543=0x3100*16+0x543

Stąd też limit pamięci do 1MB - 0xFFFFF = 1048575 = 1 MB.

W trybie chronionym sprawa przedstawia się nieco inaczej. Otóż mamy tablicę z informacji
o danym segmencie (o tym za chwilę) - taka informacja nazywa się deskryptor segmentu.

Pusty deskryptor
Deskryptor 1
Deskryptor 2
Deskryptor 3
...

Maksymalna liczba deskryptorów w jednej tablicy to 8192 - 13 bitów.
Numer segmentu znajduje się w jednym z rejestrów segmentowych:

cs - segment kodu
ds - segment danych
ss - segment stosu
es,fs,gs - segmenty dodatkowe
Informacje o pamięci (prawa, wielkość, itd) procesor otrzymuje poprzez pobranie zawartości z tablicy deskryptorów. Jeśli zapisana tam informacja zabrania wykonania operacji która właśnie próbujemy wykonąć, zostanie wywołane przerwanie 13 (General Protection Fault) wraz z numerem błędu.

Format selektora segmentu w rejestrze segmentowym wygląda tak:
seg_selector.png
Index (13 bitów) to numer deskryptora segmentu w jednej z tablic,
T - określa czy tą tablicą jest LDT czy GDT
RPL - Requested Privilege Level, 2 bity

Poziomów uprzywilejowania jest 4, nazywają sie one 'ring' (pierścień).
Ring0 to prawa największe (kernel), ring3 to prawa najmniejsze (zwykły program).
O poziomach uprzywilejowania więcej powiem później.

-GDT-

GDT (Global Descriptor Table) jest to tablica deskryptorów segmentu,
wspólna dla wszystkich procesów i całego systemu.
(LDT - Local Descriptor Table - może zostać dany każdemu procesowi z osobna).
LDT narazie nie jest nam potrzebne.

Format deskryptora segmentu (i GDT, i LDT) wygląda tak:
gdt_ldt.png
Base address - 32 bitowy adres pierwszego bajtu segmentu w 4 gb adresowalnej przestrzeni.
Dla największej wydajności powinienien być zalignowany na 16.

Segment limit - 20 bitowy limit segmentu, czyli adres gdzie kończy się segment.
Jego interpretacja zależy od stanu bitu Granularity

(B 31:24, S 19:16 oznacza bity zmiennej, czyli np Base 23:16 oznacza że w tym miejscu mają się
znaleźć bity od 16 do 23 Base address.)

Typ - rodzaj segmentu. Dane (read/read-write) albo kod (execute/execute-read/conforming).
O typach za chwilę.

S - System - 1 oznacza segment systemowy (LDT,TSS,Call-gate,przerwanie,Trap-gate, Task-gate),
0 segment danych albo kodu (GDT).

DPL - Descriptor Privilege Level - Poziom uprzywilejowania deskryptora. (czyli ring).

P - Present - obecny. Informuje, czy dany segment jest obecny w systemie.
Każda próba pobrania danych z takiego segmentu zaowocuje błedem 'General Protection Fault'.
Może być użyty do zaimplementowania swapowania (czyli zapisu rzadko używanej pamięci na dysk).
Po prostu po wystąpieniu takiego błędu system załadowałby pamięc z dysku, a następnie powrócił
do funkcji.

AVL - Available - dostepny do czegokolwiek, może być użyty przez system operacyjny,
dla procesora nie ma znaczenia.

L - Long - ustawiony na 1 oznacza, ze deskryptor segmentu jest 64 bitowy.
Na procesorach 32 bitowych zawsze musi byc ustawiony na 0.

D/B - W GDT oznacza rozmiar operacji - 16 i 32. (domyslny rozmiar adresu, rejestrów, ...).
0 oznacza 16 bit, 1 - 32.

G - Granularity. Bardzo ważny bit - ma wpływ na interpretację Segment Limit.
Ustawiony na 0 nie robi nic - segment limit jest interpretowany 'dosłownie' - może mieć
od 1 bajta do 1 megabajta.
Jesli jest ustawiony na 1, limit segmentu jest przesuwany o 12 bitow w lewo (shl),
następnie powstałe zerowe bity sa ustawiane na 1, i dopiero wtedy jest interpretowany.

Przykład:

Granularity = 0
Segment limit = 2

Limit segmentu będzie wynosić dwa bajty. Wywołanie adresu 2 spowoduje błąd 
General Protection Exception (#GP). Dostępne adresy to 0 i 1.

Granularity = 1
Segment limit = 2

Limit segmentu = (2 shl 12) or 0xfff = 12287 bajtów.
Limit jest od base address do 12287. Wyzszy adres spowoduje błąd General Protection Exception.

Granularity = 1
Segment limit = 0

Limit segmentu = (0 shl 12) or 0xfff = 0xfff = 4096

Itd.

Typy deskryptorów segmentu:
Danych:

  • 0000b - Data Read Only
  • 0010b - Data Read/Write
  • 0100b - Data Read Only, Expand Down
  • 0110b - Data Read/Write, Expand Down

Kodu:

  • 000b - Execute Only
  • 1010b - Execute/Read
  • 1100b - Execute Only, conforming
  • 1110b - Execute/Read, conforming

Ostatni bit typu jest to tzw. Access Bit. Jest on ustawiany gdy segment jest używany.
Może zostać użyty do implementacji swapowania pamięci albo do debuggingu.

Expand Down data - oznacza deskryptor segmentu, w którym segment limit oznacza DOLNY limit,
a base address - adres bazowy.

<b>Expand-down data segment:</b>
Adres
linearny:
#GP             Adresy niższe niż Segment Limit dają General Protection Exception
0x000AAAAA      SEGMENT LIMIT
...
0x00FFFFFF      BASE ADDRESS
--              Adresy wyższe niż Base Address są niemożliwe
<b>Expand-up data/code segment:</b>
Adres
linearny:
--              Adresy niższe niż Base Address są niemożliwe
0x000AAAAA      BASE ADDRESS
...
0x00FFFFFF      SEGMENT LIMIT
#GP             adresy wyższe niż Segment Limit dają #GP

Adresowanie w danym segmencie jest relatywne do base address.

W segmentach typu expand-up adres 0 daje adres linearny o wartości Base Address,
adres 1 daje Base Address+1, itd.

Expand down data segment (segment stosu):
W segmentach expand-down jest trochę inaczej:

Adres 0xFFFFFFFF w segmencie da w rezultacie base address

Wygląda to tak:

Adres w segmencie:    Adres linearny
(Base-segment limit)    Segment Limit
0xFFFFFFFF              Base Address

Czyli - najwyższy adres to 0xFFFFFFF - jest on ładowany do esp.

Każdy push odejmuje od esp 4.
Gdy stos dojdzie do segment limit, zostanie wywołane #GP.
Funkcja do obsługi błędów zaalokuje wtedy pamięć (gdziekolwiek),
skopiuje cały stos na koniec zaalokowanego obszaru, odpowiednio zmieni base address i
segment limit - base address na adres linearny nowego obszaru, a segment limit na nowy
dolny limit segmentu (większy niż poprzednio).
Dzięki temu pamięć na stos można alokowac 'w miarę potrzeb',
gdyż cały ten mechanizm jest całkowicie niewidoczny dla programu -

  • esp i segment ss są ciągle takie same.

Conforming code
'Conforming code' (ulegający kod) jest to segment dla kodu, pozwalający na wykonanie go z
mniej uprzywilejowanych segmentów. Call do funkcji w ring0, wykonany w ring3,
nie spowoduje błędu.
Kod będzie jednak wykonywany w ring3, tak samo segment stosu i danych się nie zmieni.
Conforming code działa tylko 'w górę' tzn. call albo jmp z ring0 do ring3 (nawet conforming)
skończy się błędem (zamiast tego trzeba uzyć tzw. call gate/task gate).

Załadowanie deskryptora GDT odbywa się przy pomocy polecenia lgdt.

lgdt [gdt_descriptor]
gdt_descriptor:
dw ilosc_deskryptorów_segmentu*8-1
dd linearny_adres_gdt

2. Bootsector - odczytywanie dyskietki i odblokowanie a20

Bootsector jest to pierwszy sektor danego dysku. Jego 'znakiem rozpoznawczym' jest to,
że jego dwa ostatnie bajty to 0x55,0xAA. Po starcie systemu i wykonaniu POST-u
(Power on self test), bios rozpoczyna wyszukiwanie napędów.
Gdy napotka taki, którego dwa ostatnie bajty pierwszego sektora to 0x55 0xAA,
ładuje całość do pamięci, po czym uruchamia go.

Tutaj mała ciekawostka - skąd się biorą napisy typu
'Nieprawidłowy dysk; wyjmij dyskietkę po czym naciśnij klawisz'
i inne podobne wiadomości?
Otóż - takie dyskietki również posiadają bootsektor. Jego jedyną funkcja jest wypisanie
tego tekstu i oczekiwanie na klawisz, po czym zainicjowanie restartu.

Wiemy teraz jaki warunek musi spełniać bootsektor, napiszmy więc jeden:

boot.asm

use16
org 0
start:
push 0xb800 ;segment ekranu
pop es
xor di,di
mov al,'a'
mov ah,9
stosw
hlt
times 510 - ($ - start) db 0
db 0x55
db 0xAA

Skompilujmy to, i uruchommy.

Wynik wygląda tak:
boot1.png
Nie jest to specjalnie użyteczna funkcja.
Zadaniem naszego bootsektora będzie załadowanie systemu i skok do niego:

boot.asm

use16
org 0
start:
push 0x0010 ;segment 0x0010
pop es
mov ax,0x0201 ;funkcja czytania sektorow, 1 sektor do odczytania
mov cx,2 ;numer sektora
xor dx,dx ;glowica 0,dysk 0 = dyskietka a
xor bx,bx ;adres 0x0010:0000
int 0x13
jc blad

jmp 0x0010:0000

blad:
hlt
times 510 - ($ - start) db 0
dw 0aa55h

include 'os.asm'

os.asm

use16
push 0xb800 ;segment ekranu
pop es
xor di,di
mov al,'a'
mov ah,9
stosw
hlt

Po skompilowaniu pliku boot.asm (ctrl+f9 w gui) powinniśmy otrzymać 524 bajtowy plik boot.bin.
Po uruchomieniu tak powstałego 'systemu' powinniśmy ujrzeć błękitną literkę 'a' w
lewym górnym rogu ekranu, tak samo jak w poprzednim przykładzie - jednak tym razem bootsector
jedynie ładuje nasz 'system', a ten zajmuje się wyświetleniem napisu.

Do powstałego pliku bootsectora nalezy jeszcze dodać tylko odblokowanie linii a20.

Linia A20

Co to jest A20 - jest to 20 bit (21, liczac od 1) linii adresowej (Address Line) x86. W procesorze 80x88 i 80x86 było tylko 20 linii adresowych (od 0 do 19) - wystarczająco dużo dla jednego megabajta. Sposób adresowania zezwałał jednak na adresy większe niż 1 MB:

MB = 1048575 = 0xFFFF:0x000F = 0xFFFFF (dokładnie 20 bitów zapalonych)
0xFFFF:0xFFFF = 0x10FFEF

0x10FFEF - 0xFFFFF = 0xFFFF = 65520 bajtów.

Można więc było zaadresować o 65520 bajtów 'za dużo' niż pozwalała na to architektura procesora.
Jako że bit 20 nie istniał, adres linearny 0x10FFEF ( czyli
100001111111111101111b) stawał się 0xFFEF (1111111111101111b) - bit 20 był zawsze zerem.

Wszystko było dobrze aż do czasu pojawienia się procesora 286 -

  • ten posiadał już tryb chroniony i potrafił zaadresować 16 megabajtów pamięci (24 linie adresowe).
    Dla zachowania kompatybilności z starszymi procesorami posiadał dwa tryby: tryb rzeczywisty i
    tryb chroniony, które dla tego samego powodu posiadają dzisiejsze procesory.

Posiadał jednak 24 linie adresowe, więc 'zakręcanie' adresów nie występowało - szybko się okazało,
że są programy korzystające z tej właściwości i że nie działają one na 286
(dzięki zaokragląniu adresu można było szybko dostać się do pierwszych 64K pamięci,
bez zmiany rejestrów segmentowych).

IBM postanowił rozwiązać ten problem, podłączając pod kontroler klawiatury
(8042, bedzie o nim mowa później) bramkę AND połączoną z bitem 20 linii adresowej.
Od tej pory całkowita kompatybilność została uzyskana.

Ma to jednak wpływ na adresowanie nawet teraz - dla zachowania kompatybilności bramka A20 jest
domyślnie wyłączona.

Można tą bramkę włączyć zapisując bezpośrednio do kontrolera klawiatury,
można tez skorzystać z System Port A (dostępny właściwie na każdej dzisiejszej płycie) i
odblokować A20 przez niego (tzw. Fast A20 Gate).
Jest to lepsza posunięcie, gdyż wśród tzw. embedded devices kontroler klawiatury nie musi występować.
Co prawda powinien on być wtedy emulowany, ale po co ryzykować :)

boot.asm

use16
org 0x7C00
start:
push 0x0010 ;segment 0x0010
pop es
mov ax,0x0201 ;funkcja czytania sektorow, 1 sektor do odczytania
mov cx,2 ;numer sektora
xor dx,dx ;glowica 0,dysk 0 = dyskietka a
xor bx,bx ;adres 0x0010:0000
int 0x13
jc blad

unlock_a20:
;fast a20 unlock
in al,0x92
test al,2
jnz @f ;juz ustawiona
or al,2
out 0x92,al
@@:

jmp 0x0010:0000
blad:
hlt
times 510 - ($ - start) db 0
dw 0aa55h

include 'os.asm'

Jest to już właściwie końcowa wersja bootsektora - jedyne co od tej pory będziemy zmieniać to
liczba sektorów jaka nasz bootsektor ma nam załadować do pamięci.

3. Przejscie do trybu chronionego i wypisanie napisu

Wiemy już właściwie wszystko, co jest nam potrzebne aby napisać prosty system w trybie chronionym.
Poszczególne kroki wyglądają tak:

Bootsektor:

  1. Załaduj sektory z systemem pod ustalony przez nas adres pamięci
  2. Odblokuj A20
  3. Skocz do załadowanego kodu systemu w pamięci System:
  • Wyłącz przerwania instrukcją CLI
  • Ustal początkowe dwa segmenty GDT:
    -- Segment danych (read/write), base address 0, segment limit 4GB, DPL 0
    -- Segment kodu (execute/read), base address 0, segment limit 4GB, DPL 0
    -- Ustal adres linearny gdt
  • Załaduj deskryptor gdt instrukcją LGDT
  • Przejdź do trybu chronionego poprzez ustawienie bitu 1 rejestru CR0
  • Ustaw rejestr segmentowy cs poprzez far jmp do dalszej czesci kodu systemu
    -- Kod 32 bitowy
    -- Ustaw poprawny rejestr segmentowy:
    -- stosu (ss)
    -- danych (ds)
    -- dodatkowy (es)
    -- Gs i fs najlepiej ustawic na 0
    -- Ustaw w esp adres stosu

Przejscie w tryb chroniony zakończone

Kod boot.asm nie zmienił się.

os.asm

include '%fasminc%/os.inc'
include 'macro.asm'

org 0
use16
cli ;wylacz przerwania
cld
mov ax, 0x0010
mov ds, ax
;ustaw prawidlowy, linearny adres gdt
add dword [gdt_descriptor+2], 0x100
lgdt [gdt_descriptor]

mov eax, cr0
or eax, 1
;przejscie do pmode
mov cr0, eax
jmp (1 shl 3):pm ;ustawienie cs na 32 bitowy selektor segmentu

;descriptor seg_limit*,base_address*,type*,system*,dpl*,present*,avl*,_64*,d_b*,granularity*
gdt:
dq 0 ;null segment
gdt_ldt 0xffff,0,execute_ro,1,0,1,1,0,32,1
gdt_ldt 0xffff,0,rw,1,0,1,1,0,32,1
gdt_end:

gdt_descriptor:
    dw gdt_end - gdt - 1
    dd gdt
    org 0x0100+$
use32
START:
pm:

;ustaw poprawne segmenty
mov ax, (2 shl 3)
mov ss,ax
mov ds,ax
xor edx,edx
mov fs,dx
mov es,ax
mov gs,dx
mov esp,0x000A0000

;wyczysc ekran
mov edi,[ekran]
xor eax,eax
mov ecx,(80*25)/2
rep stosd

;ustaw jasnobłękitny kolor
stdcall setcolor,9

;wypisz tekst
stdcall puts,witaj
hlt

;funkcja ustawia zmienna oznaczajaca kolor na podany argument
setcolor: ;(kolor)
mov al,byte[esp+4]
mov byte[kolor],al
ret 4

;funkcja puts wyswietla lancuch znakow (zakonczony znakiem zerowym),
;na ekran
puts: ;(asciiz *napis)
push esi
mov esi,[esp+8]
push edi
mov edi,0xb8000 ;0xb8000 = adres pierwszego znaku ekranu
mov ah,[kolor]
@@:
lodsb
test al,al
jz @f
stosw
jmp @b
@@:
mov [ekran],edi
pop edi
pop esi
ret 4

;dane
ekran dd 0xb8000
witaj db "Witaj w moim systemie operacyjnym!",0
kolor db ?

macro.asm - przydatne makra, w obecnej chwili posiada tylko jedno - do tworzenia deskryptorów GDT i LDT.
Posiada prostą kontrolę błędów na wypadek pomyłki.

macro gdt_ldt seg_limit*,base_address*,type*,system*,dpl*,present*,avl*,_64*,d_b*,granularity*
 {
        local _db
        if d_b eq 16
        _db=0
        else if d_b eq 32
        _db=1
        else
        display "Bad default operation size",0x0d,0x0a
        err
        end if
        if granularity > 1 | granularity < 0
        display "Bad granularity",0x0d,0x0a
        err
        end if
        if _64 > 1 | _64 < 0
        display "Bad 64 bit code segment",0x0d,0x0a
        err
        end if
        if avl > 1 | avl < 0
        display "Bad available bit",0x0d,0x0a
        err
        end if
        if present > 1 | present < 0
        display "Bad present bit",0x0d,0x0a
        err
        end if
        if dpl > 3 | dpl < 0
        display "Bad descriptor privilege level",0x0d,0x0a
        err
        end if
        if system > 1 | system < 0
        display "Bad system bit",0x0d,0x0a
        err
        end if
        if type > 15 | type < 0
        display "Bad type",0x0d,0x0a
        err
        end if
        if seg_limit > 0xfffff
        display "Bad system bit",0x0d,0x0a
        err
        end if
        dw (seg_limit and 0xffff)
        dw (base_address and 0xffff)
        db ((base_address shr 16) and 0x00ff)
        db (present shl 7) or (dpl shl 5) or (system shl 4) or type
        db (granularity shl 7) or (_db shl 6) or (_64 shl 5) or (avl shl 4) or ((seg_limit shl 16) and 0x0F)
        db (base_address shr 24)
 }
;data
ro equ 0
accessed equ 1
rw equ 2
ro_expand_down equ 4
rw_expand_down equ 6
;code
execute equ 8
execute_ro equ 10
conforming_execute equ 12
conforming_execute_ro equ 14

%fasminc%/os.inc - różne standardowe makra istniejące w fasmie dla windows,
wywalone trochę niepotrzebnych. Jest on dość duży, na dodatek nie ma większego związku z samym
pisaniem osa, więc umieściłem go na samym koncu tego kursu.
%fasminc% jest to zmienna środowiskowa, oznaczająca katalog Include Fasma,
np C:\Fasm\Include.

Kompilujemy (ctrl+f9) plik boot.asm, uruchamiamy Bochs... i cieszymy się napisem wyświetlonym w
trybie chronionym :)
os1.png

4. Przerwania programowe - ustawianie Interrupt Descriptor Table

Przerwania sa to po prostu zdarzenia przerywające normalną pracę procesora i wywołujące inną,
'swoją' procedurę.
Przerwania mogą być programowe albo sprzętowe:
Przerwania programowe to nic innego jak instrukcja int n, gdzie n jest numerem przerwania,
np. znany chyba wszystkim int3 (wywołujący przerwanie numer 3).
Istnieją jeszcze przerwania sprzętowe - IRQ (Interrupt Request) - takim przerwaniem np.
klawiatura (dokładniej układ 8042) informuje nas, że został naciśniety klawisz. Są one zarządzane
przez PIC - Programmable Interrupt Controller (teraz się właściwie już tego nie używa - obecnym
standardem jest APIC - Advanced Interrupt ... - ogromną różnicą w stosunku do starego układu
jest możliwość nadawania priorytetów przerwaniom; PIC jest jednak dużo prostszy do obsługi,
więc w tym kursie nie opiszę APIC, ew. opiszę dużo później).
Tymi zagadnieniami zajmiemy się jednak później - teraz zajmiemy się poprawną obsługa przerwań
programowych.

IDT
Tablica deskryptorów przerwań jest to coś bardzo podobnego do GDT - format jest tylko trochę inny.
W IDT mogą znajdować się:

  • Task Gate
  • Trap Gate
  • Interrupt Gate

idt.png
Task Gate - dokonuje sprzętowej zmiany kontekstu (context switch). O tym później.

Trap Gate i Interrupt Gate - jest w nich zapisany adres funkcji obsługującej przerwanie.
Różnią się jednym istotnym szczegółem: Wywołanie poprzez interrupt gate zeruje flage IF
(tzn. wyłącza przerwania), natomiast poprzez Trap Gate - nie.

D - Tryb pracy procesora: 1 - 32 bity; 0 - 16.

Myślę że znaczenie pozostałych pól jest zrozumiałe - jest ono takie samo jak w przypadku GDT.

Ustawmy sobie dla przykładu jedno przerwanie:

os.asm

include '%fasminc%/os.inc'
include 'macro.asm'

org 0
use16
cli ;wylacz przerwania
cld
mov ax, 0x0010
mov ds, ax
;ustaw prawidlowy, linearny adres gdt
add dword [gdt_descriptor+2], 0x100
lgdt [gdt_descriptor]

mov eax, cr0
or eax, 1
;przejscie do pmode
mov cr0, eax
jmp (1 shl 3):pm ;ustawienie cs na 32 bitowy selektor segmentu

;descriptor seg_limit*,base_address*,type*,system*,dpl*,present*,avl*,_64*,d_b*,granularity*
gdt:
dq 0 ;null segment
gdt_ldt 0xffff,0,execute_ro,1,0,1,1,0,32,1
gdt_ldt 0xffff,0,rw,1,0,1,1,0,32,1
gdt_end:

gdt_descriptor:
    dw gdt_end - gdt - 1
    dd gdt
    org 0x0100+$
use32
START:
pm:
;ustawianie poprawnych segmentow
mov ax, (2 shl 3)
mov ss,ax
mov ds,ax
xor edx,edx
mov fs,dx
mov es,ax
mov gs,dx
mov esp,0x000A0000
;wyczysc ekran
mov edi,[ekran]
xor eax,eax
mov ecx,(80*25)/2
rep stosd
stdcall setcolor,9
;stdcall puts,witaj
;ustaw idt
lidt [idt_descriptor]
;sprawdz, czy int0 dziala
int 0
hlt

setcolor: ;(kolor)
mov al,byte[esp+4]
mov byte[kolor],al
ret 4

puts: ;(asciiz *napis)
push esi
mov esi,[esp+8]
push edi
mov edi,0xb8000
mov ah,[kolor]
@@:
lodsb
test al,al
jz @f
stosw
jmp @b
@@:
mov [ekran],edi
pop edi
pop esi
ret 4
ekran dd 0xb8000
kolor db ?

_int0:
stdcall puts,"Przerwanie 0"
iretd

;idt
idt:
;int0
idt_int (1 shl 3),_int0,32,0,1 ;makro do ustawiania idt
;seg_selector*,offset*,size*,dpl*,p*
idt_end:
idt_descriptor:
    dw idt_end - idt - 1
    dd idt

macro.asm

macro idt_int seg_selector*,offset*,size*,dpl*,p*
{
        local _db
        if size eq 16
        _db=0
        else if size eq 32
        _db=1
        else
        display "Bad default operation size",0x0d,0x0a
        err
        end if
        if seg_selector > 0xFFFF | seg_selector < 0
        display "Bad Segment Selector",0x0d,0x0a
        end if
        if dpl > 3 | dpl < 0
        display "Bad descriptor privilege level",0x0d,0x0a
        err
        end if
        if p > 1 | p < 0
        display "Bad present bit",0x0d,0x0a
        err
        end if
        dw (offset and 0xFFFF)
        dw seg_selector
        db 0
        db (00000110b or (p shl 7) or (dpl shl 5) or (_db shl 3))
        dw (offset shr 16)
}


macro gdt_ldt seg_limit*,base_address*,type*,system*,dpl*,present*,avl*,_64*,d_b*,granularity*
 {
        local _db
        if d_b eq 16
        _db=0
        else if d_b eq 32
        _db=1
        else
        display "Bad default operation size",0x0d,0x0a
        err
        end if
        if granularity > 1 | granularity < 0
        display "Bad granularity",0x0d,0x0a
        err
        end if
        if _64 > 1 | _64 < 0
        display "Bad 64 bit code segment",0x0d,0x0a
        err
        end if
        if avl > 1 | avl < 0
        display "Bad available bit",0x0d,0x0a
        err
        end if
        if present > 1 | present < 0
        display "Bad present bit",0x0d,0x0a
        err
        end if
        if dpl > 3 | dpl < 0
        display "Bad descriptor privilege level",0x0d,0x0a
        err
        end if
        if system > 1 | system < 0
        display "Bad system bit",0x0d,0x0a
        err
        end if
        if type > 15 | type < 0
        display "Bad type",0x0d,0x0a
        err
        end if
        if seg_limit > 0xfffff
        display "Bad system bit",0x0d,0x0a
        err
        end if
        dw (seg_limit and 0xffff)
        dw (base_address and 0xffff)
        db ((base_address shr 16) and 0x00ff)
        db (present shl 7) or (dpl shl 5) or (system shl 4) or type
        db (granularity shl 7) or (_db shl 6) or (_64 shl 5) or (avl shl 4) or ((seg_limit shl 16) and 0x0F)
        db (base_address shr 24)
 }
;data
ro equ 0
accessed equ 1
rw equ 2
ro_expand_down equ 4
rw_expand_down equ 6
;code
execute equ 8
execute_ro equ 10
conforming_execute equ 12
conforming_execute_ro equ 14

Skompilujmy to (przypominam - kompilujemy plik boot.asm, zeby os dołączył się nam na koniec),
uruchamiamy Bochs... i cieszymy się z ustawionego przerwania :)
os2.png
Resztę przerwań ustaw sam korzystając z podanego schematu.

Tabela przerwań:

00Błąd dzielenia przez zero
01Zarezerwowane
02NMI
03Breakpoint
04Overflow (przepełnienie)
05Przerwanie instrukcji BOUND
06Błędna instrukcja (undefined opcode)
07Brak FPU
08Double Fault
09FPU Segment Overrun
10Błąd Task Switch (Invalid TSS)
11Segment nieobecny (segment not present)
12Błąd segmentu stosu (Stack segment fault)
13Ogólny błąd ochrony (General Protection Fault)
14Błąd strony (page fault)
15Zarezerwowane
16Błąd operacji zmiennoprzecinkowej
17Błąd alignacji (alignmen check error)
18Machine Check
19Błąd operacji SSE/SSE2/SSE3
20-31Zarezerwowane przez Intela

Potrzebujemy ustawić co najmniej 20 przerwań. Zachęcam aby zrobić je samemu - w ramach ćwiczenia.
Na początek starczy ustawić wszystkie na pustą procedurę obsługi przerwań - tzn. taką, która
wykonuje tylko iretd. Można również ustawić tylko obsługę przerwania Double Fault, a resztę na
'segment not present' (flaga present na 0). W takim wypadku każde przerwanie wywoła w rezultacie
procedurę obsługi Double Fault.


Jest to część pierwsza mojego kursu pisania systemu operacyjnego. W następnej części zajmiemy się obsługą PIC i przerwań, a następnie napiszemy prosty shell z kilkoma komendami.
Autor: Fr3

Konfiguracja Bochs

Konfiguracja Bochs jest zawarta w pliku bochsrc.txt. Można go skonfigurować ręcznie, edytując plik,
albo użyć menu uruchamianego po starcie.
Nie jest to szczególnie skomplikowana sprawa dlatego sądzę że instrukcje 'krok po kroku' są zbędne.
Najlepiej po prostu użyc przykładowego pliku bochsrc.txt:

config_interface: textconfig
display_library: win32
megs: 4
romimage: file="BIOS-bochs-latest"
vgaromimage: file="VGABIOS-lgpl-latest"
boot: floppy
floppy_bootsig_check: disabled=0
floppya: 1_44="<!TUTAJ WSTAW ADRES DO PLIKU BOOT.BIN!>", status=inserted
# no floppyb
ata0: enabled=0
ata1: enabled=0
ata2: enabled=0
ata3: enabled=0
parport1: enabled=0
parport2: enabled=0
com1: enabled=1, mode=null, dev=""
com2: enabled=0
com3: enabled=0
com4: enabled=0
usb1: enabled=0
i440fxsupport: enabled=1
vga_update_interval: 100000
vga: extension=vbe
cpu: count=1, ips=10000000, reset_on_triple_fault=1
text_snapshot_check: enabled=0
private_colormap: enabled=0
clock: sync=none, time0=local
# no cmosimage
ne2k: enabled=0
pnic: enabled=0
sb16: enabled=0
# no loader
log: log.txt
logprefix: %t%e%d
debugger_log: -
panic: action=ask
error: action=report
info: action=report
debug: action=ignore
pass: action=fatal
keyboard_type: mf
keyboard_serial_delay: 250
keyboard_paste_delay: 100000
keyboard_mapping: enabled=0, map=
user_shortcut: keys=none
mouse: enabled=0, type=ps2

Można również stworzyć sobie plik srun.bat do szybkiego uruchamiania bochsa
(bez wyświetlania menu).

bochs.exe -q

Nagranie systemu na dyskietke i dysk<

Jeśli chcemy sprawdzić nasz system w rzeczywistości, musimy go nagrać na dyskietkę albo
dysk twardy*.
Na systemach windows do nagrywania na dyskietkę pomocny będzie program rawwrite.
Jest on okienkowy, jego obsługa nie powinna więc sprawiać problemów.
Na systemach unixowych (*bsd, linux, ...) istnieje za to program o nazwie dd.
Jego składnia to:

dd [bs=SIZE[SUFFIX]] [count=BLOCKS] if=FILE of=FILE [seek=BLOCKS] [skip=BLOCKS] [--size] [--list] [--progress]

Instrukcja obsługi i krótki opis znajduje się na Wikipedii.
Port dd na system windows można znaleźć na http://www.chrysocome.net/dd

*Boot sektor twardego dysku wygląda trochę inaczej niż ten dla fdd, będziesz więc musiał sam
poszukać informacji na ten temat (albo poczekać na III część kursu, w której to opiszę).

Plik os.inc

os.inc - plik z przydatnymi makrami np. stdcall - jest to zmodyfikowany (bez niepotrzebych rzeczy)
plik win32a.inc. Najlepiej umieścić w katalogu Include Fasma.

include '/macro/if.inc'
include '/macro/struct.inc'
include '/macro/proc32.inc'
macro allow_nesting
{ macro pushd value
  \{ match ,value \\{
      pushx equ \\}
     match =pushx =invoke proc,pushx value \\{
      allow_nesting
      invoke proc
      purge pushd,invoke,stdcall,cinvoke,ccall
      push eax
      pushx equ \\}
     match =pushx =stdcall proc,pushx value \\{
      allow_nesting
      stdcall proc
      purge pushd,invoke,stdcall,cinvoke,ccall
      push eax
      pushx equ \\}
     match =pushx =cinvoke proc,pushx value \\{
      allow_nesting
      cinvoke proc
      purge pushd,invoke,stdcall,cinvoke,ccall
      push eax
      pushx equ \\}
     match =pushx =ccall proc,pushx value \\{
      allow_nesting
      ccall proc
      purge pushd,invoke,stdcall,cinvoke,ccall
      push eax
      pushx equ \\}
     match =pushx,pushx \\{
      pushd <value>
      pushx equ \\}
     restore pushx \}
  macro invoke proc,[arg]
  \{ \reverse pushd <arg>
     \common call [proc] \}
  macro stdcall proc,[arg]
  \{ \reverse pushd <arg>
     \common call proc \}
  macro cinvoke proc,[arg]
  \{ \common size@ccall = 0
             if ~ arg eq
     \reverse pushd <arg>
              size@ccall = size@ccall+4
              match =double any,arg \\{ size@ccall = size@ccall+4 \\}
     \common end if
             call [proc]
             if size@ccall
             add esp,size@ccall
             end if \}
  macro ccall proc,[arg]
  \{ \common size@ccall = 0
             if ~ arg eq
     \reverse pushd <arg>
              size@ccall = size@ccall+4
              match =double any,arg \\{ size@ccall = size@ccall+4 \\}
     \common end if
             call proc
             if size@ccall
             add esp,size@ccall
             end if \} }

macro pushd value
{ match first=,more, value \{ \local ..continue
   call ..continue
   db value,0
   ..continue:
   pushd equ \}
  match pushd =addr var,pushd value \{ \local ..opcode,..address
   virtual at 0
    label ..address at var
    mov eax,dword [..address]
    load ..opcode from 0
   end virtual
   if ..opcode = 0A1h
    push var
   else
    lea edx,[..address]
    push edx
   end if
   pushd equ \}
  match pushd =double [var],pushd value \{
   push dword [var+4]
   push dword [var]
   pushd equ \}
  match pushd =double =ptr var,pushd value \{
   push dword [var+4]
   push dword [var]
   pushd equ \}
  match pushd =double num,pushd value \{ \local ..high,..low
   virtual at 0
    dq num
    load ..low dword from 0
    load ..high dword from 4
   end virtual
   push ..high
   push ..low
   pushd equ \}
  match pushd,pushd \{ \local ..continue
   if value eqtype ''
    call ..continue
    db value,0
    ..continue:
   else
    push value
   end if
   pushd equ \}
  restore pushd }

allow_nesting

9 komentarzy

Mam problem. Używam QEMU jako emulatora i przy bardzo prostym bootsektorze zamiast tekstu pokazuje "=S ". Przy Oracle VM VirtualBox tak samo. Link do kodu: http://www.mediafire.com/?op4vg202enjq57q
Proszę o pomoc!

Bardzo fajny tekst, powodzenia przy nastepnych :)

Art dobry, tylko miałem jakieś dziwne wrażenie że był pisany w pośpiechu.
Mam nadzieje że to będzie coś dłuższego niż te 3 części mówiące o tym samym które można spotkać w sieci...

Artykuł super! - przynajmniej jeśli chodzi o formatowanie, tabele i przejrzystość treści. Reszty nie oceniam bo nie mam o tym pojęcia.

Super artykuł! :)
Czekam na następne części. Mam nadzieję, że kurs będzie bardziej rozwinięty niż inne, które są w sieci. Zwykle po 3 części kursu nic więcej nie ma już wtedy się gubię... Mam też nadzieję że wspomnisz coś o systemie plików i robieniu komend dla osa albo coś w tym stylu...

Coldpeer dnia 2007-05-04 22:37
hmm, trzeba usunac tekst 'kurs_os_czesc_1', bo jest to przekierowanie do tekstu 'Kurs pisania systemu operacyjnego, część 1'.

fr3m3n: przy zakładce 'Przenieś' nie ustawiaj 'Ustaw przekierowanie' ;)

Ktos dnia 2007-05-04 22:12
Brawo, świetny tekst.
Aczkolwiek moja mała uwaga jest taka, że Virtual PC nie jest emulatorem (w przeciwieństwie np. do Bochsa), a programem do wirtualizacji. Niewielka różnica.
Ale brawo. Choć akurat moja znajomość assemblera jest mierna i nie rozumiem za wiele ;)

Coldpeer dnia 2007-05-04 21:55
Nie czytałem, choć zapowiada się ciekawie :) Małe uwagi co do formatowania artykułu: spis treści możesz wygenerować automatycznie za pomocą znacznika , html-owe tabelki otaczaj znacznikiem .

ech, super
obecne przekierowanie bardzo dziwnie działa :-/
byłem święcie przekonany, że fr3m3n zamiast przenieść art to dodał drugi - jeden usunąłem, poszły oba - powinno usunąć się wtedy tylko przekierowanie :/

sorry za kasację komentarzy przy okazji

Artykuł świetny, tylko że u mnie z assemblerem problem, marnie go znam :/. Może kiedyś pojmę nieco więcej :).