Kurs pisania systemu operacyjnego, część 1

Marooned
<style type="text/css"> .ramka { border-width : thin; border-style : dashed; text-align: center; background-color: aqua; padding: 10px; margin: 10px; } .ramkal { border-width : thin; border-style : dashed; text-align: left; background-color: aqua; padding: 10px; margin: 10px; } </style>

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 :)

  • <lh>
Co będzie potrzebne:</lh> 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 </ul>
  • <lh>
Przydatny będzie jakikolwiek emulator x86:</lh> Bochs (*nix/windows) - bochs.sourceforge.net, MS Virtual PC (windows) - http://www.microsoft.com/windows/virtualpc/default.mspx QEmu (*nix/windows/mac) - http://fabrice.bellard.free.fr/qemu/ </ul>

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

<lh>Spis treści:</lh> </li>Segmenty w trybie chronionym - Global Descriptor Table </li>Bootsector - odczytywanie dyskietki i odblokowanie a20 </li>Przejscie do trybu chronionego i wypisanie napisu </li>Przerwanie programowe - ustawianie Interrupt Descriptor Table </li>Konfiguracja Bochs </li>Nagranie systemu na dyskietke i dysk </ol>

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: ```asm 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. ```asm 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](//static.4programmers.net/uploads/attachment/4ccd36da2602b.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](//static.4programmers.net/uploads/attachment/4ccd36d9f1fd0.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: ```asm 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:
  • 1000b - 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. ```asm Expand-down data segment: 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 ``` ```asm Expand-up data/code segment: 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: ```asm 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. ```asm 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 ```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. (zob. konfiguracja Bochs) Wynik wygląda tak: ![boot1.png](//static.4programmers.net/uploads/attachment/4ccd36d9eb79b.png) Nie jest to specjalnie użyteczna funkcja. Zadaniem naszego bootsektora będzie załadowanie systemu i skok do niego: boot.asm ```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 ```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:
1 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 ```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
  1. <lh>
System:</lh> 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</ul>Ustaw poprawny rejestr segmentowy:
  • stosu (ss)
  • danych (ds)
  • dodatkowy (es)
  • Gs i fs najlepiej ustawic na 0
Ustaw w esp adres stosu </ol>Przejscie w tryb chroniony zakończone Kod boot.asm nie zmienił się. os.asm ```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. ```asm 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](//static.4programmers.net/uploads/attachment/4ccd36da1c09d.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.
  • <lh>
IDT Tablica deskryptorów przerwań jest to coś bardzo podobnego do GDT - format jest tylko trochę inny. W IDT mogą znajdować się:</lh> Task Gate Trap Gate Interrupt Gate</ul> ![idt.png](//static.4programmers.net/uploads/attachment/4ccd36da00063.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 ```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 ```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](//static.4programmers.net/uploads/attachment/4ccd36da25b31.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.</ul> ```asm 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 [email protected] = 0 if ~ arg eq \reverse pushd <arg> [email protected] = [email protected]+4 match =double any,arg \\{ [email protected] = [email protected]+4 \\} \common end if call [proc] if [email protected] add esp,[email protected] end if \} macro ccall proc,[arg] \{ \common [email protected] = 0 if ~ arg eq \reverse pushd <arg> [email protected] = [email protected]+4 match =double any,arg \\{ [email protected] = [email protected]+4 \\} \common end if call proc if [email protected] add esp,[email protected] 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

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 .

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...

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 :).