background image

48

Inżynieria

oprogramowania

www.sdjournal.org

 Software Developer’s Journal   7/2006

Jądro systemu operacyjnego

P

rojektowanie  oraz  programowanie  syste-

mów  operacyjnych  stanowi  z  całą  pewno-

ścią jedną z bardziej rozwojowych dziedzin 

informatyki. Na rynku pojawiają się nowe typy pro-

cesorów, które oferują coraz to bardziej zaawanso-

wane możliwości. Obecnie największą popularno-

ścią wśród zastosowań domowych cieszą się pro-

cesory z rodziny x86. Ich rozwój jest ściśle powią-

zany  z  rosnącymi  wymaganiami  użytkowników. 

Pierwsze  procesory  80186  działały  jedynie  w  try-

bie rzeczywistym (ang. real mode/real address mo-
de
), który ograniczał się do możliwości adresowa-

nia megabajta pamięci operacyjnej (20 bitowa szy-

na adresowa). Począwszy od procesora 80286 te 

restrykcyjne  ograniczenia  powoli  zmniejszały  się, 

i tak w procesorze 80286 po raz pierwszy wprowa-

dzono  tryb  chroniony  (ang.  protected  mode)  oraz 

umożliwiono adresowanie 16 megabajtów pamięci 

(24 bitowa szyna adresowa, ale procesor pozostał 

nadal 16 bitowy). Jednak prawdziwe zmiany wniósł 

procesor  386DX,  który  w  pewnym  sensie  zrewo-

lucjonizował  rynek  komputerowy,  a  wykorzystane 

w nim rozwiązania są powszechnie stosowane do 

dziś. Główną zaletą tego procesora było wprowa-

dzenie możliwości  32-bitowego, chronionego trybu 

pracy. Umożliwiało to adresowanie do 4GB pamię-

ci.  32-bitowy  tryb  chroniony  wniósł  szereg  innych 

udogodnień.  Stary  sposób  zarządzania  pamięcią 

poprzez  segmentację  został  wyparty  przez  stron-

nicowanie (ang. paging), które jest bardziej dosto-

sowane do wymagań programisty oraz nie zawie-

ra tylu ograniczeń co segmentacja (chodzi głównie 

o limit wpisów w lokalnej tablicy deskryptorów). 

Jednym z ważniejszych udogodnień, jakie wpro-

wadził tryb chroniony jest możliwość tworzenia wie-

lu procesów, z których każdy wykonuje pewien pro-

gram. W architekturze x86 do tych celów stworzono 

specjalne  segmenty  w  globalnej  tablicy  deskrypto-

rów, które określają stan każdego procesu (ang. Task 
State Segment
). Wszystkie te rozwiązania są jednak 

bezużyteczne  bez  odpowiedniego  programu,  któ-

ry mógłby je rozsądnie wykorzystać, przez co praca 

z komputerem stała by się prostsza  i mniej zawod-

na.  W  tym  miejscu  pojawia  się  miejsce  na  system 

operacyjny,  który  jest  programem  mającym  za  za-

danie zarządzanie sprzętem i udostępnianie w pro-

stej formie zestawu funkcji, dzięki którym przeciętny 

człowiek odnajdzie się binarnym świecie.

Zarządzanie pamięcią 

w trybie rzeczywistym

Tak jak już wspomniałem, w trybie rzeczywistym mo-

żemy zaadresować jedynie 1MB pamięci, a całe za-

rządzanie tym obszarem sprowadza się do odgórne-

go podzielenia go na segmenty. Do dyspozycji pro-

gramisty  oddano  64K  segmentów,  każdy  segment 

zaczyna się co 16-ty bajt. Jako że limit segmentu wy-

nosi  64KB,  muszą  one  nachodzić  na  siebie.  Takie 

rozwiązanie  umożliwia  adresowanie  każdej  komórki 

pamięci na wiele sposobów. W trybie rzeczywistym 

adresowanie  odbywa  się  zawsze  poprzez  podanie 

dwóch 16bitowych wartości: numeru segmentu oraz 

przesunięcia w nim. Aby obliczyć adres liniowy nale-

ży użyć wzoru: segment*16+przesunięcie.

Przerwania w trybie rzeczywistym

W trybie rzeczywistym mamy do dyspozycji 256 prze-

rwań.  Wliczamy  w  to  przerwania  programowe  oraz 

sprzętowe.  Przerwania  programowe,  jak  sama  na-

zwa wskazuje dają nam możliwość zaprogramowania 

się, czyli ustalenia gdzie procesor ma przeskoczyć po 

wywołaniu instrukcji INT. Przerwania sprzętowe – IRQ 

różnią się tylko tym, że oprócz możliwości wywołania 

ich bezpośrednio z kodu programu, mogą być również 

wywołane przez fizyczne urządzenie. Każda linia IRQ 

jest  przypisana  do  innego  wektora  przerwań  w  IVT 

(ang. Interrupt Vectors Table).

IVT  jest  tablicą,  która  zawiera  256  wpisów  typu 

segment:offset. Każdy taki wpis jest nazywany wek-

torem przerwania. Kiedy procesor otrzymuje polece-

nie wykonania przerwania, musi znać nowy CS i IP 

punktu wejścia programu obsługi.

Wartości te pobiera z IVT wykonując następują-

ce obliczenia:

•  

Segment=IVT[int*4]

•  

Offset=IVT[int*4+2]

Tak więc jeden wektor zajmuje w IVT dokładnie 4baj-

ty. Przed przejściem do programu obsługi przerwa-

nia, procesor odkłada na stos następujące rejestry:

•  

SS

 - segment stosu,

•  

SP

 – obecne przesunięcie w segmencie stosu,

Grzegorz Pełechaty

Autor  jest  od  7  lat  programistą  języka  C.  Interesuje  się 
zagadnieniami systemów operacyjnych, elektroniką i sie-
ciami neuronowymi. Obecnie pracuje nad projektem dar-
mowego  systemu  sieciowego,  opartego  o  jądro  monoli-
tyczne,  oraz  w  pełni  zgodnego  ze  standardami  POSIX 
(http://www.netcorelabs.org).  System  jest  rozpowszech-
niany na warunkach licencji General Public License v2.
Kontakt z autorem: grzegorz.pelechaty@areoos.com

background image

Programowanie systemów operacyjnych

49

www.sdjournal.org

Software Developer’s Journal   7/2006

•  

FLAGS

 – flagi procesora,

•  

CS

 - segment kodu,

•  

IP

  –  obecne  przesunięcie  w  segmencie  kodu  (licznik  in-

strukcji).

Rejestry  są  odkładane  po  to,  aby  po  powrocie  z  przerwa-

nia, program mógł dalej kontynuować swoje działanie. Stos 

wraca  do  poprzedniej  wartości,  ponieważ  dane  odłożone 

na  nim  przez  program  obsługi  przerwania  są  teraz  bezu-

żyteczne.

Tryb chroniony i pierścienie ochrony

Pierścienie  ochrony  (ang.  Protection  rings)  są  to  pozio-

my  uprzywilejowania,  jakie  zastosowano  w  procesorach  IA-

286p+.  System  uprawnień  jest  dosyć  rozbudowany  i  opiera 

się  o czteropoziomowy układ zabezpieczeń, w którym  pier-

ścień zerowy jest najbardziej uprzywilejowany, a trzeci posia-

da znaczne ograniczenia. Uprawnienia te obowiązują w pra-

wie wszystkich elementach trybu chronionego. Jedynym wy-

jątkiem jest stronicowanie, które będzie dokładniej omówione 

w kolejnej części cyklu.

Zarządzanie pamięcią 

w trybie chronionym

W trybie chronionym istnieją dwa mechanizmy zarządzania 

pamięcią.  Poprzez  segmentację  oraz  stronicowanie.  Naj-

pierw  postaram  się  przybliżyć  pojęcie  segmentacji,  ponie-

waż wygląda ona trochę inaczej, niż miało to miejsce w try-

bie rzeczywistym. 

Znana z trybu rzeczywistego zamiana zawartości reje-

strów segmentowych i przesunięcia na adres fizyczny tra-

ci  sens  w  trybie  chronionym.  Tutaj  segmenty  są  od  sie-

bie odseparowane i chociaż nadal są dostępne programo-

wo, interpretacja ich zawartości jest zupełnie inna. Rejestr 

segmentowy  przechowuje  teraz  selektor  segmentu,  a  nie 

wprost jego adres. 13 najstarszych bitów tego rejestru sta-

nowi  adres  8bajtowej  struktury  opisującej  dany  segment 

(ang. Segment Descriptor). Z pozostałych trzech bitów dwa 

poświęcone  zostały  na  implementację  czteropoziomowe-

go  systemu  praw  dostępu  do  segmentu,  a  jeden  określa 

czy  wspomniany  powyżej  adres  odnosi  się  do  tzw.  tablicy 
lokalnej 
czy globalnej. Rekordami w tych tablicach są wła-

śnie deskryptory segmentów. Każdy z nich zawiera jedno-

znaczną informację o lokalizacji segmentu w pamięci i jego 

rozmiarach. W ten sposób zdefiniowany jest spójny obszar 

o  adresie  początkowym  wyznaczonym  przez  liczbę  32-bi-

tową. Na liczbę określającą rozmiar takiego bloku przezna-

czone zostało pole 20-bitowe. Istnieją dwie możliwości in-

Mapa pierwszego megabajtu pamięci

• 

00000000 – 000003FF

  Tablica wektorów przerwań

•  

00000400 – 000004FF

  Obszar danych biosu

•  

00000500 – 0009FBFF

  Pamięć konwencjonalna (640KB)

•  

00007C00 – 00007DFF

  Program rozruchowy

•  

0009FC00 – 0009FFFF

  Rozszerzony obszar danych biosu 

 

  

 

 

 

 

 

(EBDA)

•  

000A0000 – 000BFFFF

  Pamięć VGA (128KB)

•  

000A0000 – 000AFFFF

  Bufor ramki VGA(64KB)

•  

000B0000 – 000B7FFF

  Pamięć dla kart monochromatycznych 

 

  

 

 

 

 

 

(32KB) 

•  

000B8000 – 000BFFFF

  Pamięć dla kart kolorowych (32KB)

•  

000C0000 – 000C7FFF

  BIOS karty graficznej (32KB – ROM)

•  

000F0000 – 000FFFFF

  BIOS płyty głównej (64KB – ROM)

Spis wektorów linii IRQ w trybie 

rzeczywistym

•   Linia IRQ  Wektor  

Urządzenie generujące syngnał IRQ

•  

 0      08h

  

 

Zegar systemowy

•  

 1      09h

  

 

Klawiatura

•  

 2      0Ah

  

 

Wyjście kaskadowe do układu Slave

•  

 3      0Bh

  

 

Port COM2

•  

 4      0Ch

  

 

Port COM1

•  

 5      0Dh

  

 

Port LPT2

•  

 6      0Eh

  

 

Kontroler napędu dysków elastycznych

•  

 7      0Fh

  

 

Port LPT1

•  

 8      70h

  

 

Zegar czasu rzeczywistego (RTC)

•  

 9      71h

  

 

Wywołuje przerwanie IRQ2

•  

10      72h

  

 

Zarezerwowane

•  

11      73h

  

 

Zarezerwowane

•  

12      74h

  

 

Zarezerwowane

•  

13      75h

  

 

Koprocesor  arytmetyczny

•  

14      76h 

 

 

Kontroler dysku twardego

•  

15      77h

  

 

Zarezerwowane

Listing 1. 

Struktura deskryptora segmentu w Globalnej 

Tablicy Deskryptorów

struct

 

gdt_seg_desc

 

{

    

unsigned

 

short

 

len15_0

;

    

unsigned

 

short

 

base15_0

;

    

unsgined

 

char

 

base23_16

;

    

unsigned

 

char

 

flags1

;

    

unsigned

 

char

 

flags2

;

    

unsigned

 

char

 

base31_24

;

}

;

Listing 2. 

Funkcja tworząca nowy segment w Globalnej 

Tablicy Deskryptorów

struct

 

gdt_seg_desc

 

*

gdt_table

=

      (

struct

 

gdt_seg_desc

)

GDT_ADDRESS

;

void

 

createSegment

(

 

int

 

pos

unsigned

 

long

 

base

unsigned

 

long

 

len

      

unigned

 

char

 

flags1

unsigned

 

char

 

flags2

 

)

 

{

    

gdt_table

[

pos

]

.

len15_0

 

=

 

(

unsigned

 

short

)(

          

len

 

&

 0xFFFF

);

    

gdt_table

[

pos

]

.

base15_0

 

=

 

(

unssigned

 

short

)(

          

base

 

&

 0xFFFF

);

    

gdt_table

[

pos

]

.

base23_16

 

=

 

(

unsigned

 

char

)(

          (

base

 

>>

 16

)

 

&

 0xFF

);

    

gdt_table

[

pos

]

.

flags1

 

=

 

flags1

;

    

gdt_table

[

pos

]

.

flags2

 

=

 

flags2

 

|

 

((

len

 

>>

 16

)

 

&

 0xf

);

    

gdt_table

[

pos

]

.

base31_24

 

=

 

(

unsigned

 

char

)(

          (

base

 

&

 0xF000

)

 

>>

 24

);

}

background image

50

Inżynieria

oprogramowania

www.sdjournal.org

 Software Developer’s Journal   7/2006

bitowy,  budowany  jest  ze  złożenia  zawartości  16bitowe-

go  rejestru  segmentowego  i  32bitowego  rejestru  przesu-

nięcia.  W  przypadku  ziarnistości  4KB  maksymalny  roz-

miar  segmentu  wynosi  4GB.  Liczba  możliwych  segmen-

tów  to  2^14(2^13  deskryptorów  lokalnych  i  tyle  samo  glo-

balnych),  co  daje  w  sumie  astronomiczną  objętość  64TB 

(2^14*2^32).  Właściwie  już  jeden  taki  segment  stanowi 

wielkość  optymalną  –  4GB  przestrzeni  adresowej  zaspo-

kaja  przy  obecnym  rozwoju  techniki  PC  dość  wygórowa-

ne  wymagania.  Rozwiązanie  takie,  określane  jako  “płaski 

model  pamięci”  (ang.  flat  memory  model),  stosowane  jest 

w systemie Windows NT. 

Zarządzanie 

Globalną Tablicą Deskryptorów

Listing  2  zawiera  funkcję,  która  tworzy  nowy  segment  w 

GDT.  Struktura  opisująca  pojedynczy  deskryptor  segmen-

tu znajduje się na Listingu 1. Ustalmy jeszcze raz jak obli-

czyć selektor danego segmentu. Na selektor składa się ad-

res deskryptora względem początku Globalnej Tablicy De-

skryptorów  oraz  poziom  uprzywilejowania  i  informacja  o 

tym czy segment znajduje się w tablicy lokalnej, czy global-
nej

selector=numer_segmentu*sizeof(struct gdt_desc)+DPL+ (
      4 –- jeżeli deskryptor jest w LDT)

Sama  tablica  GDT  jest  opisana  specjalnym,  48bitowym  de-

skryptorem.  Struktura  opisująca  ten  deskryptor  wygląda  na-

stępująco:

struct gdt_desc {
    unsigned short gdt_size;
    unsigned long gdt_address;
} __atribute__((packed));

Pierwsze 16 bajtów powinno zawierać rozmiar tablicy GDT mi-

nus 1bajt, czyli 8192*8-1.

Tablicę ładujemy poleceniem lgdt, które przyjmuje fizycz-

ny  adres  deskryptora  w  pamięci  (w  postaci  bezpośredniego 

adresu, bądź też rejestru). Musimy pamiętać, że pierwszy de-

skryptor jest zawsze pusty. Nie ma możliwości, aby został on 

wykorzystany w jakikolwiek sposób przez system.

Jądro systemu

Teraz  spróbujemy  zebrać  te  wszystkie  informacje  w  spójną 

całość  i  napiszemy  proste  jądro  systemu  operacyjnego.  Nie 

będzie  to  oczywiście  jądro,  które  byłoby  w  stanie  zrobić  co-

kolwiek, ale po uruchomieniu zobaczymy napis hello world i to 

powinno nam na razie wystarczyć.

Listing 3. 

Definicje poszczególnych bitów we flagach 

deskryptora segmentu

Opis

 

definicji

 

poszczeg

ó

lnych

 

flag

 

segmentu

:

// FLAGS1 (P + DPL + SYS/APP + TYPE)

#define GDT_PRESENT   0x80
#define GDT_DPL3        0x60 
#define GDT_DPL1        0x20
#define GDT_DPL2        0x40
#define GDT_DPL0        0x00

// GDT_SYS będzie poruszone podczas omawiania 
// wielozadaniowości. Obecnie interesuje nas 
// tylko GDT_APP, które przeznaczone jest dla segmentu 
// danych lub kodu.

#define GDT_SYS 0x00
#define GDT_APP 0x10

// Dodatkowe flagi dla segmentów innych niż data lub 
// code (GDT_SYS)

#define GDT_RESERVED   0x0
#

define

 

GDT_TSS16

      0x1 

 // 0001 16 bitowy TSS (dostępny)

#

define

 

GDT_LDT

        0x2 

 // 0010 LDT

#

define

 

GDT_TSS16_BUSY

 0x3 

 // 0011 16 bitowy TSS (zajęty)

#

define

 

GDT_CALL16

     0x4 

 // 0100 16 bitowa bramka wywołań

#

define

 

GDT_TASK

       0x5 

 // 0101 Bramka zadania

#

define

 

GDT_INT16

      0x6 

 // 0110 16 bitowa bramka 

                           // przerwania

#

define

 

GDT_TRAP16

     0x7 

 // 0111 16 bitowa bramka pułapki

#

define

 

GDT_TSS32

      0x9 

 // 1001 32 bitowy TSS (dostępny)

#

define

 

GDT_TSS32_BUSY

 0xB 

 // 1011 32 bitowy TSS (zajęty)

#

define

 

GDT_CALL32

     0xC 

 // 1100 32 bitowa bramka wywołań

#

define

 

GDT_INT32

      0xE 

 // 1110 32 bitowa bramka 

                           // przerwania

#

define

 

GDT_TASK_GATE

  0xF 

 // 1111 32 bitowa bramka pułapki

// Dla GDT_APP

#define GDT_DATA       0x00
#define GDT_WRITE      0x02
#define GDT_EXP_DOWN   0x04
#define GDT_CODE       0x08
#define GDT_READ       0x02
#define GDT_CONF       0x04

// FLAGS2 (G + D/B + 0 + AVL)
// Ziarnistość danego segmentu

#define GDT_GRANULARITY 0x80

// Rodzaj segmentu – 32 lub 16 bitowy

#define GDT_USE32      0x40
#define GDT_USE16      0x00

// Segment do dowolnego użytku przez system

#

define

 

GDT_AVAIL

      0x00

Listing 4. 

Nagłówek multiboot dla wykonywalnych plików 

ELF

struct

 

multiboot_header

 

{

    

unsigned

 

long

 

magic

;

    

unsigned

 

long

 

flags

;

    

unsigned

 

long

 

checksum

;

}

;

terpretowania  liczby  w  tym  polu.  W  trybie  1:1  (ziarnistość 

1B)  rozmiar  maksymalny  wynosi  po  prostu  2^20  =  1MB. 

Gdyby jednak przyjąć jednostkę 4KB (ziarnistość 4KB), roz-

miar  segmentu  może  sięgać  do  2^20*2^12  =  2^32  =  4GB. 

Informacja  o  tym,  która  z  konwencji  aktualnie  obowiązuje, 

zawarta jest w deskryptorze.

Adres  logiczny,  do  którego  odwołuje  się  procesor  32-

background image

Programowanie systemów operacyjnych

Listing 5. 

Kod inicjalizacyjny jądra

Zestaw Narzędzi

Do rozpoczęcia prac nad pisaniem własnego systemu ope-

racyjnego niezbędny będzie nam pewien zestaw narzędzi, 

który w znacznej mierze ułatwi nam to zadanie:

•   edytor tekstu

•   GCC & GAS

•   GNU binutils (ld, make)

•   opcjonalnie emulator (qemu/vmware)

•   program rozruchowy zgodny ze standardem multiboot

Edytor tekstu będzie nam oczywiście potrzebny do pisania ko-

du. Kompilatory gcc i gas posłużą do jego skompilowania. Po-

nieważ  nie  jest  możliwe  napisanie  jądra  jedynie  przy  użyciu 

języka wysokiego poziomu, musimy również posiadać kompi-

lator asemblera. Szereg programów z pakietu binutils pomo-

że nam w skonsolidowaniu całego obrazu. Użycie emulatora 

PC jest wysoce wskazane, ponieważ dzięki niemu nie będzie-

my zmuszeni za każdym razem restartować komputera w ce-

lu sprawdzenia poprawności naszego kodu. Ostatnim progra-

mem jaki musimy posiadać jest program rozruchowy zgodny 

ze standardem multiboot. Przykładem takiego programu jest 

GRUB, który staje się coraz bardziej powszechny. Oczywiście 

możemy napisać własny program rozruchowy, jednak mija się 

to z celem. GRUB zagwarantuje nam zgodność z większością 

sprzętu oraz  przejdzie za nas w tryb chroniony tym samym 

oddając  w  nasze  ręce  w  pełni  32-bitowe  środowisko  pracy. 

Wszystkie  wymienione  powyżej  narzędzia  są  rozpowszech-

.text
.globl 

_start

_start

:

   

jmp 

    

multiboot_entry

.align  4

multiboot_header

:

   .

long

   0x1BADB002

   .

long

   0x00000003

   .

long

   -

(

0x1BADB002 + 0x00000003

)

multiboot_entry

:

   

movl

 $

(

stack

+100

)

 ,%

esp

   

call 

setup_gdt

   

call 

__main

mbi

:

   .

long

 0x0

setup_gdt

:

   

movl

 $

gdt_table

, %

esi

   

movl

 $0xA000, %

edi

   

movl

 $8, %

ecx

   

rep

   

movsl

   

movl

 $0xA000+8*8, %

edi

   

movl

 $0x2000-8, %

ecx

fill_gdt

:

   

movl

 $0, 

(

%

edi

)

   

movl

 $0, 4

(

%

edi

)

   

addl

 $8, %

edi

   

dec 

%

ecx

   

jne 

fill_gdt

1:
   

lgdt

 

gdt

   

ljmp

  $

(

0x10

)

, $

go

go

:

   

movl

  $

(

0x18

)

, %

eax

   

movl

  %

eax

, %

ds

   

movl

 %

eax

, %

es

           

   

movl

 %

eax

, %

fs

   

movl

 %

eax

, %

gs

   

movl

 %

eax

, %

ss

   

ret

.data

gdt

:   .

word

 0x2000*8-1

         .

long

 0xA000

gdt_table

:

   .

quad

 0x0000000000000000   # 

pusty

 

deskryptor

   .

quad

 0x0000000000000000   # 

nie

 

u

ż

ywamy

   .

quad

 0x00cf9a000000ffff   # 0x10 

kernel

 4

GB

 

code

 

at

 

0x00000000 

   .

quad

 0x00cf92000000ffff   # 0x18 

kernel

 4

GB

 

data

 

at

 

0x00000000 

.comm 

stack

, 0x500 

R

E

K

L

A

M

A

background image

52

Inżynieria

oprogramowania

www.sdjournal.org

 Software Developer’s Journal   7/2006

niane na warunkach General Public License, więc są całkowi-

cie darmowe.

Kod inicjacyjny jądra

Aby GRUB mógł załadować jądro, musi ono posiadać specjalny 

nagłówek informacyjny. Adres tego nagłówek musi być wyrów-

nany do czterech bajtów. Struktura opisująca nagłówek multi-

boot dla wykonywalnych plików ELF znajduje się na Listingu 4.

Standardowe wartości jakie powinny być użyte w naszym 

wypadku to:

magic=0x1BADB002
flags=0x00000003
checksum=-(0x1BADB002 + 0x00000003)

Na  Listingu  5  wypełniamy  GDT  czterema  deskryptorami.  De-

skryptor numer 2 wskazuje na segment kodu, którego limit jest 

ustawiony na 4GB, a co za tym idzie ziarnistości segmentu mu-

si wynosić 4KB. Segment ten ma uprawnienia pierścienia 0. Ko-

lejny segment jest przeznaczony na dane. Ma on taki sam adres 

bazowy i limit jak segment kodu, jednak różni się typem. Po wy-

pełnieniu GDT,  wywołujemy funkcję 

_ main()

, która będzie po-

czątkiem naszego właściwego jądra.

Kernel

Na razie nasze jądro nie będzie zbytnio rozbudowane. Napi-

szemy  prostą  funkcję  czyszczącą  ekran  w  tekstowym  trybie 

VGA oraz funkcję wypisującą ciąg znaków w lewym górnym 

rogu ekranu

Jak  widać  na  Listingu  6,  po  wypisaniu  komunikatu  koń-

czymy funkcję nieskończoną pętlą. Jest to jedyne wyjście po-

nieważ nie mamy systemu do którego moglibyśmy powrócić. 

Gdybyśmy jednak kontynuowali działanie, licznik instrukcji (re-

jestr  EIP)  wskazywałby  na  pamięć,  nie  zawierającą  instruk-

cji procesora, co spowodowałoby wywołanie 6 wyjątku. Pro-

cesor próbując wywołać przerwanie nr 6 natrafiłby na kolejny 

problem,  ponieważ  nie  mamy  załadowanej  tablicy  IDT  (ang. 
Interrupt  Descriptors  Table).  Wtedy  wystąpiłby  potrójny  błąd 

(ang. Triple fault), który wiąże się z natychmiastowym restar-

tem procesora.

Kompilacja

Przy kompilacji tak dużych projektów, jakimi są systemy ope-

racyjne  bardzo  dobrym  rozwiązaniem  jest  zastosowanie  pli-

ków make. N a Listingu 7 przedstawiono sposób użycia tych 

plików, w oparciu o źródłowe pliki, które stworzyliśmy wcze-

śniej.  Mowa  o  kodzie  inicjacyjnym,  który  powinniśmy  zapi-

sać w pliku init.S oraz źródle jądra, które powinno nosić na-

zwę main.c

Listing  7  zapisujemy  pod  nazwą  makefile  w  katalogu  ze 

źródłami. 

Rysunek 1. 

Jądro systemu uruchomione pod emulatorem 

Vmware

Listing 7. 

Plik makefile dla jądra

CC=gcc
LD=ld
OBJS=init.o main.o

CFLAGS = -fno-builtin -nostdlib -nostdinc -Wno-main -O2
all: $(OBJS)
   $(LD) -Tkernel.lds -S -X -o kernel --start-group $(OBJS)
         --end-group
.c.o: 
   $(CC) $(CFLAGS) -c $

<

 

-o $@

.S.o:
 

  

$(CC) $(CFLAGS) -traditional -c 

$

<

 

-o $@

CLEAN_FILES = $(OBJS)
clean:
 

  

rm -rf $(CLEAN_FILES)

dep:
 

  

find . -name 

'*.c'

 

-o -name 

'*.S'

|xargs gcc -M $(CFLAGS) 

>

 .depend

ifeq (.depend,$(wildcard .depend))
include .depend
endif

Listing 6. 

Przykładowe jądro systemu, wypisujące napis 

“hello world” w lewym górnym rogu ekranu

static

 

char

 

*

video

=(

char

 

*)

0xB8000

;

void

 

clrscr

(

void

)

 

{

   

int

 

i

;

   

for

(

i

=

0

;

i

<

80

*

50

;

i

+=

2

)

 

{

      

video

[

i

]=

32

;

      

video

[

i

+

1

]=

0x7

;

   

}

}

void

 

puts

(

char

 

*

msg

)

 

{

   

char

 

*

ptr

=

video

;

   

while

(*

msg

)

 

{

      

*

ptr

++=*

msg

++;

      

*

ptr

++=

0x7

;

   

}

}

void

 

__main

(

void

)

 

{

   

clrscr

();

   

puts

(

"hello world!"

);

   

for

(;;);

}

background image

53

Programowanie systemów operacyjnych

www.sdjournal.org

Software Developer’s Journal   7/2006

Konsolidacja

Jak zapewne zauważyliście na Listingu 7 jednym z argumen-

tów programu LD był plik kernel.lds.

Pod tą nazwą zapisujemy skrypt dla programu konsoli-

dującego, który przechowuje informacje o tym, gdzie umie-

ścić poszczególne segmenty w pliku oraz jaki ma być for-

mat  wyjściowy  i  offset,    pod  który  zostanie  skompilowane 

nasze  jądro.  Przykład  takiego  skryptu  znajduje  się  na  Li-

stingu 8.

Jak widzicie jądro zostanie skompilowane z 1MB prze-

sunięciem  względem  początku  segmentu  kodu.  Tak  więc 

zostawiamy  praktycznie  nienaruszony  pierwszy  megabajt 

pamięci  (pomijając  64KB  tablicę  GDT,  która  znajduje  się 

poniżej pierwszego megabajata). Ponadto obraz wyjściowy 

będzie w formacie pliku wykonywalnego ELF, dzięki czemu 

GRUB nie będzie miał problemu ze zidentyfikowaniem pliku 

(GRUB  obsługuje  wiele  formatów  plików  wykonywalnych, 

min. pliki executable ELF oraz pliki binarne)

Konfiguracja GRUB'a

Ostatnią  czynnością,  jaką  musimy  wykonać  jest  skonfigu-

rowanie  naszego  programu  rozruchowego,  czyli  GRUB'a. 

W tym celu edytujemy plik „ /boot/grub/grub.conf” i dodajemy 

na końcu następujące linie:

   title MyOS
   root (hdD,P)
   kernel /boot/kernel

W miejsce D i P wstawiamy odpowiednio numer dysku i party-

cji, na której mamy nasze jądro (jest ta sama partycja z której 

został uruchomiony obecnie działający system).

Testowanie

Teraz  przyszła  kolej  na  przetestowanie  naszego  syste-

mu,  jednak  przed  tym  musimy  go  skompilować  i  skopio-

wać tak powstały obraz do katalogu /boot. W celu skompi-

lowania naszego jądra wpisujemy polecenie make, w kata-

logu gdzie znajdują się źródła wraz z plikami makefile i ker-

nel.lds. Po skopiowaniu obrazu do katalogu /boot restartu-

jemy komputer i w menu wyboru GRUB'a wybieramy pozy-

cję  MyOS.  Jeżeli  wszystko  poszło  pomyślnie  powinniśmy 

zobaczyć identyczny efekt jak na Rysunku 1.

Podsumowanie

Tak  jak  się  przekonaliście,  pisanie  systemu  operacyjnego  to 

dość trudne i czasochłonne zajęcie,

które zabierze cenne chwile z waszego życia, ale da rów-

nież niesamowitą satysfakcję z tego, że stworzyliśmy coś wy-

jątkowego. Sami wytyczyliśmy standardy i nie musieliśmy być 

podporządkowani innym. I chyba to w tym wszystkim jest naj-

ważniejsze. Jeżeli zdecydujecie się na pisanie własnego sys-

temu, pamiętajcie, że najlepiej uczyć się na własnych błędach. 

Starajcie się znajdować rozwiązanie na konkretny problem po-

przez testowanie własnych pomysłów, a dopiero potem sięgaj-

cie do kursów, tutoriali itp... Ta dziedzina informatyki wymaga 

przede wszystkim kreatywności, a nie biernego implementowa-

nia gotowych rozwiązań.

W  następnej  części  cyklu  przedstawię  najbardziej  znane 

metody programowego zarządzania pamięcią oraz napiszemy 

prosty alokator pamięci oparty o listę dwukierunkową. Mowa 

będzie również o stronicowaniu, przerwaniach i pamięci wirtu-

alnej, omówimy także sposób działania mechanizmu wymiany 

pamięci pomiędzy dyskiem a  pamięcią fizyczną. n

W Sieci

•   Polski  portal  poświęcony  programowaniu  systemów  operacyj-

nych: http://www.areoos.com/osdevpl

•   Ogólnoświatowe  forum  programistów  systemów  operacyj-

nych: http://www.osdev.org

•   Portal związany z programowaniem systemów, z wieloma kur-

sami oraz przykładami: http://www.osdever.net/

•   Strona poświęcona głównie systemowi operacyjnemu Alt+Ctrl+

Del,  zawiera  również  obszerną  listę  innych  projektów:  http://
www.acd.prv.pl

Literatura

•   [1]  Piotr  Metzger,  Michał  Siemieniacki, Anatomia  PC  wydanie 

IX, Helion 2004,

•   [2]  Uresh  Vahalia,  Jądro  systemu  Unix  –-  nowe  horyzonty, 

Wydawnictwa Naukowo-Techniczne Warszawa 2001

Listing 8. 

Przykładowy skrypt dla programu 

konsolidującego

OUTPUT_FORMAT

(

"elf32-i386"

)

OUTPUT_ARCH

(

i386

)

ENTRY

(

_start

)

SECTIONS

{

  . 

=

 0x100000 

+

 

SIZEOF_HEADERS

;

  .

text

 

:

 

{

   

*(

.

text

)

   

}

 

  . 

=

 

ALIGN

(

16

);

  .

rodata

 

:

 

{

   

*(

.

rodata

)

 

   

}

  . 

=

 

ALIGN

(

16

);

  .

data

 

:

 

{

   

*(

.

data

)

   

CONSTRUCTORS

   

}

  . 

=

 

ALIGN

(

16

);

  

_edata

 

=

 .

;

  .

bss

 

:

 

{

   

*(

.

bss

)

   

}

  . 

=

 

ALIGN

(

16

);

  

_end

 

=

 .

;

}