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
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
);
}
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-
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
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
(;;);
}
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
=
.
;
}