Systemy operacyjne – książki

Wiedza, wiedza, wiedza…

Tworzenie systemów operacyjnych to niszowa dziedzina informatyki. Zajmują się nią nieliczni. Osobiście uważam jednak, że posiadanie wiadomości z zakresu systemów operacyjnych, to absolutny obowiązek każdego szanującego się informatyka. Nie myślę tu oczywiście o umiejętności obsługi Linuksa, czy też administracji Windowsem, tylko o zagadnieniach typu wieloprocesowość i wielowątkowość, synchronizacja procesów i wątków, szeregowanie, zarządzanie pamięcią, czy choćby znajomość zarysu historycznego początków informatyki. Wielu osobom może się wydawać, iż wiedza ta jest całkowicie zbędna „bo przecież ja programuję w .NET i nie robię żadnych wywołań systemowych i nie zarządzam pamięcią, tylko tworzę aplikacje webowe!”. Jasne, tak może być, tylko kompletna nieświadomość tego, co dzieje się „pod spodem”, może doprowadzić niejednokrotnie do katastrofy, a w innych sytuacjach wyjaśnienie takiego, a nie innego zachowania naszej aplikacji leży właśnie w systemie operacyjnym i jego „bebechach”. Jeśli to mimo wszystko nie przemawia, to po prostu powiem, że temat jest bardzo ciekawy i warto go zgłębić, choćby dla przyjemności :)

W dobie internetu, w którym wszystko można znaleźć, może się wydawać, że książki to przeżytek. Sam preferuję uczyć się nowych technologii z tutoriali i dokumentacji, gdyż zapewniają one absolutną aktualność zawartych informacji. Systemy operacyjne, jak już wspominałem wielokrotnie, to inna działka. Wiele wiadomości w tym zakresie to niezmienne od lat fundamenty, pozostałe natomiast zmieniają się bardzo powoli. Książki sprawdzają się tu bardzo dobrze, bo zapewniają całościowe i wyczerpujące podejście. Poza tym, kto nie lubi zapachu farby drukarskiej ponad świecący monitor?

Niekwestionowani liderzy

Rynek książek na temat systemów operacyjnych jest bardzo mały – tak jak mówiłem – nisza. Jak to się jednak mówi – nie ilość, ale jakość! Na polskim rynku niepodzielnie królują dwa tytuły i są to „Systemy operacyjne”, której autorem jest Andrew Stuart Tanenbaum, twórca kultowego systemu MINIX, od którego wiele się zaczeło oraz „Podstawy systemów operacyjnych” autorstwa Abrahama Silberschatza, Petera Baera Galvina oraz Grega Gagne’a. Najnowsze wydanie (trzecie) pierwszej z nich ukazało się w tym roku za sprawą wydawnictwa helion i pokrywa się z najnowszą, angielską wersją, natomiast druga z wymienionych książek wydawana jest przez WNT – najnowsze wydanie z 2005 roku, to wydanie szóste, podczas gdy na świecie mamy już wydanie ósme. Dodatkowo niepokojącym jest fakt, że już od długiego czasu na stronie wydawnictwa widnieje napis „Książka czasowo niedostępna”. Może to oznaczać, że nowe wydanie jest w drodze, albo niestety – jakieś problemy. Dość już jednak suchych faktów, przejdźmy do tego, co książki te oferują i która z nich jest lepsza.

Obie książki oferują całościowe ujęcie tematu. Znajdziemy w nich dużo wyczerpujących informacji na tematy takie jak historia systemów operacyjnych; podstawowe pojęcia; procesy i wątki – IPC, szeregowanie, synchronizacja; zarządzanie pamięcią – stronicowanie i segmentacja, algorytmy alokacji pamięci; system plików; wejście-wyjście; bezpieczeństwo. Dodatkowo „Podstawy systemów operacyjnych” zawierają obszerny rozdział poświęcony rozproszonym systemom operacyjnym, natomiast w ramach rekompensaty „Systemy operacyjne” posiadają duży rozdział o implementacji systemu operacyjnego. Na uwagę zasługują też rozdziały opisujące konkretne systemy operacyjne – co i jak zostało w nich zrealizowane. Oczywiście w książce Tanenbauma informacje dotyczą nowszych wydań konkretnych systemów.

Język obu książek nie jest szczególnie lekki, ale nie powiedziałbym, że czyta się je ciężko – tematyka wymaga (niestety, bądź stety) używania języka formalnego, jednak wszystko daje się zrozumieć. Ciężko też znaleźć fragmenty, w których autorzy „laliby wodę”. Moim zdaniem, udało się im zachować równowagę pomiędzy niejasną zwięzłością, a przesadną obszernością.

Żadna z książek nie wyczerpuje tematu, bo jest to raczej niemożliwe. Obie natomiast prezentują bardzo obszerny kawałek tematu na swoich ponad 1000 stronach. Która z nich jest zatem trafniejszym zakupem? Na dzień dzisiejszy odpowiedź jest prosta – jest to książka „Systemy operacyjne”, ze względu na łatwą dostępność (księgarnia helion 99 zł (!) na dzień dzisiejszy) i większą aktualność (mimo wszystko). „Podstawy systemów operacyjnych” daje się czasem trafić w antykwariatach, na kiermaszach, bądź na allegro, jednak jej cena sięga nierzadko… 300 zł ze względu na swoistą unikalność. Cena zabójcza. Aha, muszę jeszcze wspomnieć, iż obie książki posiadają niebanalne okładki :) Powiedziałbym nawet, że okładka „Systemów operacyjnych” to najciekawiej zaprojektowana okładka, jaką w życiu widziałem. Polecam zobaczyć ;>

Na zakończenie

Naprawdę gorąco zachęcam do przeczytania jednego z tych tytułów – i nieważne czy jesteś programistą, administratorem, architektem, serwisantem, czy reinstalatorem Windowsów – zawsze znajdziesz tam wiele ciekawych informacji, które – nóż, widelec – mogą się kiedyś przydać. A nawet jeśli nie – wiedza jest zawsze sexy i w modzie :)

EDIT 11.10.2010:

Na rynku brak książek rodzimych autorów o tematyce tworzenia czy budowy systemów operacyjnych, jednak właśnie dostałem informację, o pozycji, która niebawem się ukaże – Programowanie systemowe mikroprocesorów rodziny x86 autorstwa Włodzimierza Stanisławskiego i Damiana Raczyńskiego. Myślę, że warto spróbować!

Dostęp do sprzętu peryferyjnego – podstawy

Zarys

W życiu każdego projektanta systemów operacyjnych nadchodzi moment, w którym chciałby się on skontaktować z urządzeniami zewnętrznymi, gdyż samotny procesor przestaje spełniać jego wygórowane oczekiwania :) Jak to bywa prawie ze wszystkim, istnieje kilka sposobów dostępu do sprzętu peryferyjnego, co wynika oczywiście z naturalnej ewolucji hardware’u – w tym poście zajmę się jednak tylko dwoma, najprostszymi metodami – port I/O oraz memory-mapped I/O, co dałoby się przetłumaczyć jako odpowiednio – wejście/wyjście za pomocą portów oraz wejście wyjście odwzorowane w pamięci. Wśród bardziej zaawansowanych metod, na które jeszcze przyjdzie czas, warto wspomnieć o DMA oraz dostępie przez przerwania.

Memory-mapped I/O

Memory-mapped I/O to bardzo prosty sposób na dostęp do urządzeń zewnętrznych polegających na tym, iż w dostępnej pamięci operacyjnej – a dokładniej w dostępnej przestrzeni adresowej – posiadamy zarezerwowany zakres adresów pozwalających na odwoływanie się do urządzeń. Rezerwacja może być zarówno stała, jak i tymczasowa. Technicznie, w uproszczeniu rzecz ujmując, realizacja wygląda tak, iż każde z urządzeń monitoruje szynę adresową procesora i reaguje na każdy jego dostęp do odpowiedniego zakresu. Zaletami takiego podejścia są oczywiście – prostota i jasność w korzystaniu (łatwo programuje się to w C), gdyż dostęp do pamięci jest prosty; procesor oparty o memory-mapped I/O jest tańszy, mniejszy, prostszy i mniej energochłonny; dostęp do pamięci jest szybki, a więc dostęp do urządzeń poprzez szynę adresową też – również dzięki możliwości wykorzystania różnych trybów adresowania. Wady? Takie podejście „przerywa” ciągłość pamięci w konwencjonalnym ujęciu, a więc zmniejsza też ilość dostępnej pamięci. Dla formalności, przykładowy kod:

unsigned char *videoram = (unsigned char *) 0xB8000;
videoram[0] = 65; /* character 'A' */
videoram[1] = 0x07; /* forground, background color */

W ten sposób wypiszemy literkę ‚A’ na ekranie, odwołując się do karty graficznej. Wygląda prosto, prawda? I o to właśnie chodzi :) Pozostaje jeszcze pytanie, na które nie ma odpowiedzi – skąd wiedzieć która cześć pamięci za co odpowiada? Rozwiązania należy szukać w mapie pamięci, która dla każdej architektury może być inna. Warto też pamiętać, że istnieje możliwość dynamicznego przydzielania adresów dla urządzeń.

Port I/O

Port I/O to kolejna, starsza i generalnie gorsza metoda dostępu do urządzeń, która ciągle jest obecna w architekturze x86 z powodu.. tak tak, zgodności wstecznej. Sposób ten polega na istnieniu dodatkowej, obok RAM-u, przestrzeni adresowej, w której każde urządzenie ma swój adres. Dostęp do portów odbywa się za pomocą specjalnych instrukcji, charakterystycznych dla architektury, które nie mają swojego odpowiednika w języku C. W assemblerze x86 są to: IN, INS/INSB/INSW/INSD, OUT, OUTS/OUTSB/OUTSW/OUTSD pozwalające wczytać/pisać odpowiednią porcję danych z/do wskazanego portu. Technicznie, transfer danych odbywa się za pomocą specjalnego I/O pina w CPU lub za pomocą przeznaczonej do tego szyny adresowej, która nieco przyspiesza ten proces. Zaletami tego rozwiązania jest oszczędność przestrzeni adresowej,  a więc i samej pamięci oraz fakt, iż dostęp przez odpowiednie instrukcje, w jasny sposób wskazuje czytającemu kod programiście, kiedy odbywa się komunikacja ze sprzętem. Wadą, jak już wspomniałem, jest powolność oraz brak wsparcia dla operacji 64 bitowych. Przykładowy kod:

a20wait:
        in      al, 0x64
        test    al, 2
        jnz     a20wait
        ret

Kod ten, jak widać, wczytuje do rejestru al (ax) bajt z urządzenia o adresie 0x64, o którym będzie jeszcze mowa ;) Warto zwrócić uwagę, że używany jest tu rejestr (e)ax, gdyż… jest to jedyny rejestr dostępny do użycia z portami w architekturze x86 – niestety. Podobnie – adres może być stałą natychmiastową lub wartością wyłącznie z rejstru DX. Pozostaje nam jeszcze odpowiedź na pytanie zadane dla dostępu przez memory-mapped I/O – czyli skąd wiedzieć, który port połączony jest z którym urządzeniem. Odpowiedź i tym razem jest niełatwa. Niektóre urządzenia takie jak timery, kontrolery przerwań, porty PS/2, serial, parallel, dyskietka, dysk twardy, karta graficzna mają przypisane konkretne adresy z powodów kompatybilności. Pozostałe urządzenia typu plug-and-play mają adresy przypisane przez BIOS i aby je otrzymać, należy poprosić o to szynę PCI :) Inne urządzenia (np. karty ISA) posiadają zworki, którymi da się ustawić konkretny port. Pamiętacie ustawianie portów Soundblastera w grach za czasów DOS-a? Tak – to właśnie dlatego :)

Podsumowanie

Nowoczesne architektury – 32 i 64 bitowe odchodzą od modelu z portami na rzecz zunifikowanej przestrzeni adresowej – ze względu na wysoką wydajność i elegancję takiego rozwiązania. W obecnych czasach, mała ilość pamięci nie jest już przeszkodą. Są jednak chwile, w których dostęp za pomocą portów jest słuszną (lub jedyną ;>) metodą dostępu do urządzeń – należy więc o niej pamiętać. Konkretne przykłady użycia – już wkrótce.

Po co branchuję?

Ludzie branche tworzą…

Motywów do tworzenia gałęzi w projektach jest co nie miara. W nie tak prehistorycznych czasach (które chyba z resztą trwają do dziś) królowania CVS, a później SVN większość rzeczy trafiała bezpośrednio do trunka – głównej gałęzi. Rozgałęzienia były tworzone w momentach, gdy projekt rzeczywiście obierał dwa, dosyć odmienne biegi rozwoju (np. v1 i v2 – obie rozwijane). Inną strategią (nie wykluczającą pierwszej!) było tworzenie gałęzi – stabilnej, rozwojowej, a czasem eksperymentalnej i odpowiednie „code promotion”, czyli „przenoszenie” kodu między gałęziami: stabilna <- rozwojowa <- eksperymentalna. Jeszcze inny sposób wykorzystywania gałęzi, to oddzielny branch dla każdego podzespołu. Tak stworzone gałęzie były stopniowo (zwykle po zakończeniu pracy nad modułem) włączane do wspomnianego trunka. Powodem stosunkowo niedużej liczby rozgałęzień była największa bolączka wspomnianych scentralizowanych systemów kontroli wersji – nieprzyjemny merge.

W nowoczesnych-trendy-lux-czasach rozproszonych systemów kontroli wersji takich jak git, hg, bzr promuje się podejście „branch-per-feature” – czyli tworzenie nowej gałęzi dla każdego, nowego „ficzera”, każdego naprawianego buga itd. Nic co jest „work-in-progress” nie powinno trafić do gałęzi głównej. Wszystko to jest możliwe dzięki mniej bolesnemu merge’owaniu gałęzi, a takie podejście jest samo w sobie niezwykle wygodne i efektywne.

Nieco kreatywności

Przy okazji serii postów którą tworzę, potrzebowałem prostego i efektywnego sposobu udostępniania kodów dla poszczególnych odcinków serii. W grę wchodziło uploadowanie paczek z aktualnym kodem, dla danego odcinka, na serwer i linkowanie ich w notce. Szczerze powiedziawszy – nie znoszę takiego podejścia. Wydaje mi się, że łatwo doprowadza to do bałaganu, nieaktualnych wersji i niedziałających linków. Potrzebowałem lepszego sposobu i do głowy przyszedł mi pomysł.. aby każdy odcinek serii był oddzielnym branchem. Takie rozwiązanie ma szereg zalet: Czytelnicy mogą z łatwością pobrać aktualny, dla danego odcinka, kod; mogę bezproblemowo wnosić zmiany do kodu dla konkretnej części; struktura tego podejścia jest bardzo przejrzysta, a ponadto wyjątkowo łatwo się tym wszystkim zarządza. Z dodatkowych zalet warto wspomnieć o przeglądarce kodu na githubie, która umożliwia Czytelnikom natychmiastowy podgląd kodu dla danej części, bez potrzeby ściągania go. Jak dla mnie – rewelacja :) Przedstawione przeze mnie podejście ma oczywiście sens tylko w serii postów ze stopniowo przyrastającym kodem. Puryści mogliby jednak powiedzieć, że takie rzeczy powinno załatwiać się labelami, ale jak już wspomniałem – mam zamiar w miarę potrzeb aktualizować kod dla konkretnych odcinków, a zarządzanie etykietami, to nic przyjemnego.

Nowa, lepsza przyszłość? ;)

Cieszy mnie, że era linkowania paczek z kodem dobiega końca, a repozytoria wchodzą w coraz to nowe obszary działalności – ludzie za pomocą systemów kontroli wersji rozpowszechniają najróżniejsze rzeczy – począwszy od configów, aż do dokumentów. Niewątpliwa w tym zasługa ogólnodostępnych, darmowych, świetnie przygotowanych serwisów takich jak github, gitorious, czy bitbucket. Wydaje mi się, że używanie systemów kontroli wersji jest po prostu efektywne, łatwe, szybkie i pewne, zatem ich (nadchodząca) wszechobecność jest rzeczą naturalną. Koniec broken linków! :)

Tworzenie systemu operacyjnego – część 0×02: Dobrodziejstwa BIOS-u

512 bajtów (tak samo jak 640K) nie wystarczy każdemu

W poprzednim odcinku załadowaliśmy 512 bajtów zawartości dysku do pamięci. W taki sposób uruchomiliśmy nasz bootloader, który niestety nic nie robił. Teraz wypadałoby, aby zmienił on tryb operacyjny, wyświetlił kilka diagnostycznych komunikatów no i załadował jądro za pomocą sterownika ATA… tylko to wszystko nie zmieści się w 512 bajtach! Jeśli postawimy na minimum, tj. załadowanie jądra z dysku – również może być ciężko, sterownik jednak swoje waży. Z pomocą przychodzi nam jednak BIOS!

Funkcje BIOS-a – przerwania

BIOS udostępnia nam bardzo wiele funkcji poprzez przerwania. Wśród oferowanego asortymentu są np.: pisanie na ekran, wczytywanie z dysku, sprawdzanie stanu sprzętu, uzyskiwanie mapy pamięci, aktywowanie dodatkowej pamięci i wiele, wiele innych. My zajmiemy się dla przykładu tylko dwoma. Pełna ich lista znajduje się tutaj i tutaj. Widzicie funkcje opisane jako „???”? Uważajcie na nie! Skoro nikt, nawet twórcy BIOS-ów nie wiedzą do czego służą, to możecie nimi przez przypadek przejąć władzę nad światem lub wyłączyć prąd u sąsiada w domu.. :)

Dla niezaznajomionych z programowaniem systemowym: przerwanie wywołuje się poleceniem int numer_przerwania, natomiast argumenty przekazuje się za pomocą odpowiednich rejestrów.

int 0x10

Na początek postaramy się wypisać coś na ekran. W tym celu skorzystamy z przerwania 0x10, którego dokładny opis jest tutaj. Oto kod:

	mov	ax, 0x0000
	mov	ds, ax		; round way to do that...
	mov	si, msg

	mov	ah, 0x0E	; mode - teletype (advance and scroll)
	mov	bh, 0x00	; page number
	mov	bl, 0x07	; colors

.next_char:
	lodsb
	or	al, al		; letter here
	jz	.continue
	int	0x10		; BIOS video interrupt
	jmp	.next_char

msg:
	db	'Hello from bootloader!', 13, 10, 0

Idąc od początku, warto zwrócić uwagę na linie 5 i 6 i na dziwną, okrężną drogę ładowania rejestru DS (data segment). Niestety – takie są ograniczenia architektury – nie można bezpośrednio ładować rejestrów segmentowych. Do rejestru SI ładujemy adres pamięci pierwszej litery (czyli mówiąc najprościej – napisu) i tak uzyskana para (DS:SI) wskazuje nam jednoznacznie, gdzie jest napis. Pora na kolejne ustawienia. W rejestrze AH, zgodnie z opisem musi znaleźć się wartość 0xE, która definiuje tryb wyświetlania liter – jest to przewijanie i przeskakiwanie na następną pozycję (w dokumentacji nazwany teletype). W rejestrze BH przechowujemy numer strony – u nas jest to strona zerowa. Rejestr BL zawiera natomiast kolory (tła i liter) – białe litery na czarnym tle.

Możemy teraz przejść do pętli wypisującej literę po literze. Instrukcja lodsb ładuje literę wskazywaną przez DS:SI do rejestru AL, a następnie przesuwa wskaźnik do następnej litery. Polecenie o tyle przydatne,  gdyż oszczędzamy w ten sposób cenne miejsce w naszych 512 bajtach. Kolejnym krokiem jest sprawdzenie, czy przypadkiem nie dotarliśmy do końca napisu i jeśli tak, to opuszczenie pętli (linie 15 i 16), następnie wywołanie samego przerwania (linia 17) oraz skok zamykający pętle. Bajecznie proste prawda? Po wykonaniu tego kawałka kodu powinniśmy ujrzeć na ekranie napis „Hello from bootloader!”. Czas na trochę bardziej przydatne i skomplikowane przerwanie.

int 0x13

Szczęśliwa „trzynastka” ;) pozwala nam na wczytywanie danych z dysku za pomocą adresowania typu LBA (jeśli nie wiesz co to, przeczytaj koniecznie podlinkowaną stronę Wikipedii). Rzut oka na kod:

	mov	si, read_pocket
	mov	ah, 0x42	; extension
	mov	dl, 0x80	; drive number (0x80 should be drive #0)
	int	0x13

read_pocket:
	db	0x10		; size of pocket
	db	0		; const 0
	dw	1		; number of sectors to transfer
	dw	0x7E00, 0x0000	; address to write
	dd	1		; LBA
	dd	0		; upper LBA

Tym razem w rejestrze SI znajduje się adres „kieszeni” (o której szerzej – zaraz) zawierającej informacje o tym co i gdzie wczytać. Rejestr AH zawiera numer identyfikujący tę właśnie funkcję (wśród dostępnych funkcji znajdują się również takie, które pozwalają na dostęp CHS). Natomiast rejestr DL zawiera numer dysku z którego chcemy czytać… zupełnie spodziewanie, dysk numer 0 znajduje się pod numerem 0x80 ;> Przerwanie wywołujemy w linii 24 i o ile wszystko poszło gładko, powinniśmy mieć dane załadowane pod wskazany adres. Co do samej kieszeni, to komentarze chyba mówią wszystko. Pierwszy bajt zawiera rozmiar kieszeni, drugim powinno być zawsze 0, kolejne dwa to liczba sektorów do wczytania, następne dwa słowa to człony adresu (u nas to 0x0000:0x7E00), potem następuje adres sektora startowego na dysku (zaczynamy od sektora numer 1, czyli efektywnie od drugiego) i jego „górna” część. Prosto, łatwo i przyjemnie! :) W prawdziwym, profesjonalnym systemie powinniśmy jakoś obsłużyć błędy wczytywania, których kody zwracane są w odpowiednich rejestrach (opis tu).

Wszystko do kupy

Finalnie nasz kod wygląda tak:

[ORG 0x7C00]			; here is where our code will be loaded by BIOS
[BITS 16]

bootloader:
	mov	ax, 0x0000
	mov	ds, ax		; round way to do that...
	mov	si, msg

	mov	ah, 0x0E	; mode -> teletype (advance and scroll)
	mov	bh, 0x00	; page number
	mov	bl, 0x07	; colors

.next_char:
	lodsb
	or	al, al		; letter here
	jz	.continue
	int	0x10		; BIOS video interrupt
	jmp	.next_char

.continue:
	mov	si, read_pocket
	mov	ah, 0x42	; extension
	mov	dl, 0x80	; drive number (0x80 should be drive #0)
	int	0x13
	cli			; turn off maskable interrupts, we don't need them now
	hlt
.halt:	jmp	.halt

msg:
	db	'Hello from bootloader!', 13, 10, 0
read_pocket:
	db	0x10		; size of pocket
	db	0		; const 0
	dw	1		; number of sectors to transfer
	dw	0x7E00, 0x0000	; address to write
	dd	1		; LBA
	dd	0		; upper LBA 

times 510-($-$$) db 0		; fill rest with zeros
dw 0xAA55			; bootloader indicator, used by BIOS

Do celów testowych musimy stworzyć dalszą część naszego wirtualnego dysku, jako że ma on tylko 512 bajtów i wczytanie kolejnego sektora zaowocuje błędem. Proponuję zrobić to tak:

echo 'Ala ma kota' > ./bin/text
dd if=/dev/zero of=./bin/zeros bs=1  count=500
cat ./bin/boot.bin ./bin/text ./bin/zeros > ./bin/hda.img

Kolejno: tworzę plik z zawartością „Ala ma kota” (plus nowa linia!), tworzę plik 500 bajtowy z samymi zerami, a następnie wszystko łącze w kolejności: kod + tekst + zera, co nam daje 1024 bajty.

Po uruchomieniu naszego bootloadera w QEMU zobaczymy na ekranie „Hello from bootloader!”, natomiast w pamięci pod adresem 0x7E00 powinien być napis „Ala ma kota”. Dla pewności, w trybie monitora sprawdźmy to komendą xp /12bc 0x7E00. Wspaniale!

Single stage

Nasz bootloader aktualnie posiada tylko jeden etap (ang. stage) działania. Bardziej złożone bootloadery, jak np. GNU GRUB posiadają kilka etapów działania (najczęściej 2) ze względu na wspomniane ograniczenie 512 bajtów. W kolejnej części cyklu spróbujemy napisać stage 2 dla naszego bootloadera, który odpowiednio skonfiguruje środowisko pracy dla naszego jądra. Tymczasem zachęcam do zapoznania się z pełnymi źródłami części 0x02:

git clone git://github.com/luksow/OS.git --branch 0x02

Jeśli chcesz go tylko przejrzeć, wejdź tutaj.

Edytor tekstu zamiast wypasionego IDE?

Filozoficzne pytanie

Mam silny background .NET-owy, zdarzyło mi się dłużej programować w Javie. Właściwie od kiedy zacząłem programować bardziej „na serio”, towarzyszyło mi Visual Studio, Eclipse, czy NetBeans. Od zawszę więc myślałem, że dobre środowisko pracy, to podstawa. Bynajmniej, nie twierdzę teraz inaczej – ale czy dobre środowisko oznacza ciężkie, w pełni wyposażone, dopasowane do danego języka IDE? A może „zwykły” edytor tekstu wystarczy?

Rys historyczny

Pierwszą styczność z edytorem tekstu, konkretniej z vim-em zawdzięczam mojemu Wspaniełemu Koledze. Wtedy, szczerze powiedziawszy, nie spodziewałem się, że ktokolwiek tego używa – nie działa backspace, trzeba wcisnąć „i”, żeby pisać i nie da się z niego wyjść? Żartów z niego było co niemiara ;> ale Michał poruszał się w nim całkiem sprawnie. Niedługo i ja musiałem się nieco do niego przekonać, gdyż jest on podstawowym (jedynym) wyposażeniem w części pracowni na mojej zacnej uczelni. Hmm.. przekonać to za dużo powiedziane – wypadałoby to raczej nazwać – powstrzymywaniem nienawiści :)

Sytuacja uległa nieco zmianie, gdy przeczytałem (bardzo dobry moim zdaniem) artykuł Roba Conery’ego właśnie o edytorach tekstu, głównie o vim-ie. Pomyślałem wtedy, że coś w tym musi być! Poszperałem więc w internecie, obejrzałem screencast podlinkowany przez Roba.. i dałem vim-owi drugą szansę. Realizowałem akurat całkiem duży (jak na realia szkolne) uczelniany projekt, więc szansa była. Tym razem miałem w sobie więcej entuzjazmu i po niełatwej przeprawie udało się – dostrzegłem urodę edytorów tekstu!

Bolesna przesiadka

Nie ma co ukrywać, że zmiana IDE na edytor jest bolesna, bardzo bolesna. Zaryzykowałbym stwierdzenie, że większość osób, która nawet próbuje, odpada bardzo szybko. Tak było i w moim przypadku, ale za którymś podejściem się przełamałem. Edytory takie jak vim lub Emacs wprost „z pudełka”, z przydatnych rzeczy, oferują podświetlanie składni dla kilku języków.. i to by było na tyle. Dodajmy do tego, że nawet podstawowa obsługa i nawigacją są skomplikowane i katastrofa gotowa ;)

Obrazek ten jest humorystyczną ilustracją tego o czym mówię :) Co zatem przyciąga licznych wyznawców tekstowych edytorów?

Nieskończone dobro

Oba wspomniane edytory posiadają nieograniczone możliwości konfiguracyjne – skonfigurować można dosłownie wszystko za sprawą specjalnych, charakterystycznych dla danego edytora języków. Jasnym chyba jest, że dzięki temu zaprogramować można w nich wszystko, nawet przejmowanie kontroli nad światem :>

Użytkownicy nie muszą jednak programować wszystkiego sami – istnieją setki, tysiące (miliony?) pluginów zarówno do Emacsa jak i vima. Pomyśl nad najbardziej absurdalną rzeczą, jaka przychodzi Ci do głowy i jaką może mieć edytor (i nie, przeglądarka WWW, gry, organizator czasu to nie są absurdalne rzeczy) – tak Emacs ma ją już napisaną :)

Właśnie za sprawą owych dodatków (własnych, ściągniętych, czy też dostosowanych) dostajemy do edytorów wsparcie do rozmaitych języków programowania (podświetlanie składni, snippety, uzupełnianie, połączenie z debuggerami, integracja z narzędziami do testowania itd. itp.). W ten sposób ujawnia się jedna z największych zalet edytorów – uczymy się narzędzia RAZ i używamy go do wszystkich języków programowania, robiąc tylko odpowiednie dostosowania. W ten sposób zwraca nam się zainwestowany na początku czas – z nawiązką.

Last but not least pamiętajmy, że edytory nastawione są na.. edycję. W związku z tym mają niezwykle bogate możliwości edycji tekstu (a tym przecież jest kod!) i to w sposób łatwy i szybki. Pomyślcie teraz – czy więcej czasu podczas kodzenia spędzacie na pisaniu, czy edycji tego co już powstało? No właśnie :)

Co edytor zrobił dla mnie?

O tym postaram się napisać w przyszłości, prezentując przy okazji używane przeze mnie dodatki. Niestety, nie będą to tutoriale step-by-step, gdyż sam jeszcze jestem lamką :)

Poniżej jeszcze kilka linków do poczytania:

Wpis na blogu Roba Conery’ego podlinkowany wyżej

O programowaniu C# w Emacsie

Bardzo dobry wpis o edycji tesktu w vimie, przedstawiający „filozofię” tego edytora

Tworzenie systemu operacyjnego – część 0×01: Włączam komputer i…, bootloader

Power on!

W życiu każdego młodego informatyka nastaje moment, gdy zaczyna się zastanawiać, co tak właściwie się dzieje, gdy włącza komputer. Na ten temat można by mówić długo i na różnym poziomie szczegółowości. Ja nie będę zajmował się jednak skaczącymi elektronami i zmieniającymi się poziomami energetycznymi w krzemie, a tym co interesuje programistę systemów operacyjnych – poziomem oprogramowania.

Włączenie komputera rozpoczyna się od naciśnięcia przycisku (genialne spostrzeżenie, prawda?), który włącza zasilanie komputera, a konkretniej – zasila płytę główną. Ta z kolei, w pierwszej chwili uruchamia swój własny firmware. Ciężko określić jak dokładnie proces ten przebiega, gdyż dla każdej płyty głównej może być to trochę inne. Tak naprawdę chodzi o to, że płyta główna potrzebuje programu, który uruchomi procesor. Jeśli w tym momencie coś pójdzie nie tak, będziemy oglądać ciemny ekran, słyszeć kręcące się wiatraczki i piski z głośniczka na płycie głównej. Załóżmy jednak, że udało się uruchomić procesor. Co ciekawsi zastanawiają się pewnie – „a co jeśli komputer posiada dwa lub więcej procesorów?”. Otóż w systemach wieloprocesorowych jeden z procesorów jest tzw. bootstrap processor (BSP), który odpowiedzialny jest za uruchomienie BIOS-u oraz zainicjowanie jądra. Drugi z procesorów pozostaje w stanie zatrzymania (halt), aż do momentu kiedy explicite zostanie obudzony przez jądro systemu operacyjnego. Wracając jednak do naszej sekwencji – nasz świeżo uruchomiony procesor znajduje się w dobrze zdefiniowanym stanie – tj. znane są zawartości części jego rejestrów, w szczególności rejestru wskaźnika instrukcji CS:IP. Rejestr ten przechowuje magiczny adres nazwany reset vector. W tym momencie płyta główna musi się upewnić, że instrukcja znajdująca się pod tym adresem, to skok do miejsca w pamięci, w którym znajduje się zmapowany BIOS – jest to obszar adresów nieco poniżej magicznej granicy 1MB (np. 0x9FC00 – 0xFFFFF). Warto zwrócić uwagę na słowo zmapowany – tzn. dokładnie w tym miejscu w pamięci RAM znajdują się śmiecie, a płyta główna tworzy iluzję, jakoby znajdował się tam BIOS (wie jak to zrobić dzięki przechowywanej przez nią mapy pamięci). Od momentu gdy zacznie wykonywać się BIOS, ciężko powiedzieć co dzieje się dalej, gdyż to zależy od konkretnego BIOS-u – a więc od producenta płyty głównej. Standardowo jest to wykonanie „Power-on self-test”, czyli tzw. POST-u, który testuje – integralność samego BIOS-u, niektóre podzespoły, pamięć itp. Kolejnym krokiem po POST jest przejście do sekwencji bootowania. Pierwszym krokiem bootowania jest przejrzenie (predefiniowanej przez użytkownika w ustawieniach BIOS-u) listy urządzeń do wystartowania i odszukaniu urządzenia (np. dysku), którego pierwszy (a w zasadzie zerowy) sektor, czyli 512 bajtów, kończy się bajtami 0x55 oraz 0xAA. Bajty te oznaczają, iż urządzenie jest bootowalne – zawiera bootloader. Następnie, wspomniane 512 bajtów jest kopiowane pod adres 0x7C00, a procesor zaczyna wykonywać instrukcje spod tegoż adresu. Warto wspomnieć, że istnieje pewien standard, jak owe 512 bajtów powinno wyglądać – tzn. powinien być tam MBR. Nie jest to jednak istotne, gdyż w tym momencie pełną władzę przejmujemy my – programiści! :)

Przed dalszą lekturą upewnij się, że zapoznałeś się informacjami zawartymi w:

QEMU – podstawy

Tryby operacyjne – tryb rzeczywisty

Bootloader

Pierwsza wersja naszego bootloadera będzie robiła.. nic :) a dokładniej nasz bootloader „zatrzyma” procesor. W kolejnej części nieco go rozbudujemy. W tym miejscu trzeba zaznaczyć, że komputer pracuje w trybie rzeczywistym. Spójrzmy na kod naszego bootloadera:

[ORG 0x7C00]			; here is where our code will be loaded by BIOS
[BITS 16]

bootloader:
	cli			; turn off maskable interrupts, we don't need them now
	hlt			; halt CPU
	jmp	bootloader	; this should not happen, but.. ;)

times 510-($-$$) db 0		; fill rest with zeros
dw 0xAA55			; bootloader indicator, used by BIOS

Teraz będą objaśnienia. Pierwsza linia to wskazówka dla naszego kompilatora, żeby przyjął odpowiednie przesunięcie w adresach. Gdyby tej linii nie było (co możecie sprawdzić) kompilator „myślałby”,  że kod wskazywany przez etykietę bootloader znajduje się pod adresem 0x0000, a co za tym idzie skok z linii 7 by się nie powiódł.

Druga linia, to kolejna wskazówka, mówiąca tym razem, że chcemy kod wynikowy 16 bitowy – pamiętacie o trybie rzeczywistym?

Linie 5, 6 i 7 należy rozpatrywać razem. Instrukcja cli wyłącza maskowalne przerwania. O tym co to dokładniej znaczy będzie innym razem, ważne żeby zapamiętać iż przerwania wybudzają procesor, ze stanu halt, aktywowanego poleceniem hlt. W stanie halt procesor zatrzymuje się i nie wykonuje następującej instrukcji, aż do wznowienia. Tak więc brak instrukcji cli spowodowałby chwilowe wstrzymanie procesora, a następnie (gdyż przerwań w tle dzieje się dużo!) wykonanie instrukcji z linii 7, która jest swoistą „ostatnią deską ratunku” :) Polecam poeksperymentować usuwając linie 5 i 7 i zobaczyć dokąd zabrnie nasz IP i czy nie skończy się to restartem.

Linia 9 to makro NASM-a mówiące, aby wypełnić następujące bajty, aż do 510, zerami.

Linia 10 to wspomniany indykator świadczący o tym, iż jest to kod bootloadera.

W ten sposób otrzymujemy 512 bajtów bootloadera.

Nasz kod kompilujemy poleceniem:

nasm boot.asm -f bin -o ./bin/boot.bin

Przełącznik -f bin mówi, iż chcemy, aby wynikowy kod był tzw. płaską binarką (ang. flat binary), czyli ma nie zawierać żadnych nagłówków, ani informacji pomocniczych, a całość kodu źródłowego jest bezpośrednio zamieniana na kod. Możemy to sprawdzić używając narzędzia hexdump – zobaczymy, iż pierwszym bajtem wynikowego pliku boot.bin jest 0xFA, które jest kodem operacji cli :)

Pozostaje nam teraz uruchomienie naszego bootloadera za pomocą polecenia:

qemu -hda ./bin/boot.bin

I voila! Nasz bootloader działa i nic nie robi :) Będąc w QEMU polecam pobawić się trybem monitora, w szczególności wyświetlić fragmenty pamięci.

Końcowe słowa

To tyle w dzisiejszym odcinku. W następnym postaramy się rozbudować nasz bootloader w sposób, który pozwoli załadować nam nasz przyszły kernel. W repozytorium znajduje się pełen kod wraz z plikiem Makefile (niezbyt pięknym, swoją drogą), który pozwala na automatyzację kompilacji i uruchamiania kodu.

Aby ściągnąć kod części 0x01 wykonaj:

git clone git://github.com/luksow/OS.git --branch 0x01

Jeśli chcesz go tylko przejrzeć, wejdź tutaj.

Tryby operacyjne – tryb rzeczywisty

Tryby operacyjne

Tryb operacyjny (ang. operating mode) procesora to po prostu pewien dobrze zdefiniowany tryb, w którym procesor zachowuje się w ściśle określony, charakterystyczny dla danego trybu sposób. Ciężko o formalną definicję tego pojęcia, ale myślę że zamieszczone niżej opisy trybów operacyjnych IA-32 (architektura potocznie zwana x86) i Intel 64 (architektura x86-64) rozwieją wszystkie wątpliwości.

Intel Architecture 32 bit (x86)

Architektura ta posiada 3 tryby operacyjne plus jeden quasi-tryb:

  • Real mode (tryb rzeczywisty) – inicjalny tryb pracy procesora, posiada interfejs programistyczny procesora Intel 8086 wraz z kilkoma rozszerzeniami – między innymi z możliwością przejścia do trybu chronionego. Dużo szczegółów poniżej.
  • Protected mode (tryb chroniony) – natywny tryb procesora, jeśli czytasz tę notkę z komputera PC, to bardzo prawdopodobne, że Twój procesor właśnie używa tego trybu. Tryb ten pozwala na użycie pełnego potencjału nowoczesnego procesora i to on będzie nam towarzyszył przez znacznie większą część programowania systemu operacyjnego. Dużo szczegółów wkrótce.
  • System management mode (tryb zarządzania) – bardzo nietypowy tryb, w którym mamy wysoko uprzywilejowane środowisko, pozwalające na zarządzanie energią, obsługiwanie krytycznych błędów itp. Na razie tryb ten nie będzie nas interesował.
  • Virtual-8086 mode – wspomniany quasi-tryb, który pozwala na uruchamianie programów przeznaczonych na procesor 8086 w trybie chronionym.

Intel 64 (x86-64)

Architektura ta posiada wszystkie tryby, które posiada IA-32 plus:

  • IA-32e mode, który posiada dwa podtryby:
    • Compatibility mode (tryb zgodności) – podtryb pozwalający uruchamiać aplikacje 16 i 32-bitowe bez potrzeby rekompilacji ich dla procesora 64-bitowego. Co ciekawe, aplikacje działające w trybie Virtual 8086 nie będą działały w tym trybie.
    • 64-bit mode – natywny dla 64-bitowego procesora tryb, który pozwala na korzystanie z dobrodziejstw architektury x86-64. Jeśli Twój system jest 64-bitowy, to najprawdopodobniej Twój procesor pracuje właśnie w tym trybie.

Warto wspomnieć, iż niektórzy wyróżniają tryb nazywany potocznie Unreal mode (tryb nierzeczywisty), jednak nie będę go opisywał.

Tryb rzeczywisty

Wady i zalety

Tryb rzeczywisty to inicjalny tryb procesora, który istnieje do dziś ze względu na inżynierów procesorów, którzy chcą zachować wsteczną zgodność z procesorami sprzed ery 80386. Jakie są tego konsekwencje? Niestety bardzo dotkliwe.

  • Nieco ponad 1 MiB adresowalnej pamięci, a faktycznie nieco poniżej 1 MiB pamięci do użycia
  • Brak zdecydowanej większości ficzerów architektury IA-32, w tym m. in.:
    • Brak jakiejkolwiek ochrony pamięci, co za tym idzie – brak pamięci wirtualnej
    • Brak wielowątkowośći/wieloprocesowości (nikłe możliwości emulacji)
    • Każdy „proces” może wszystko – np. mazać dowolny fragment pamięci, wykonywać instrukcje systemowe itp.
    • Niemożność użycia wszystkich rejestrów, które w trybie chronionym są ogólnodostępne
    • Ograniczone i skomplikowane możliwości adresowania pamięci

Jedyne zalety trybu rzeczywistego, to dość bogate funkcje oferowane przez BIOS, z których będziemy mieli okazję skorzystać.

Adresowanie

Adresowanie w trybie rzeczywistym nie odbywa się w typowy, liniowy sposób. Pamięć adresowana jest w następujący sposób:

Segment : Przesunięcie

Tak zaadresowana pamięć mapowana jest na fizyczny adres o postaci:

16 * Segment + Przesunięcie

Dziwne, prawda? Jeszcze dziwniejsze jest to, że pojedynczy adres fizyczny ma wiele reprezentacji w formacie „rzeczywistotrybowym”, gdyż segmenty zachodzą na siebie. I tak np. 0x12B1 : 0x0069 odpowiada adresowi 0x12B79 (0x10 * 0x12B1 + 0x0069 = 0x12B79), natomiast 0x1000 : 0x2B79 odpowiada adresowi… 0x12B79! (0x10 * 0x1000 + 0x2B79 = 0x12B79).

Do dyspozycji mamy 6 16-bitowych rejestrów segmentowych: CS, DS, ES, FS, GS oraz SS. Stos obsługuje się używając pary rejestrów SS : SP.

Dostępne tryby adresowania to:

  • [BX + offset]
  • [SI + offset]
  • [DI + offset]
  • [BP + offset]
  • [BX + SI + offset]
  • [BX + DI + offset]
  • [BP + SI + offset]
  • [BP + DI + offset]
  • [offset]

Gdzie offset zawiera się pomiędzy -32768 oraz 32767.

Z adresowaniem związane jest jeszcze jedno ciekawe zjawisko, mianowicie, gdy segment ustawimy na 0xFFFF, natomiast przesunięcie na wartość pomiędzy 0x10, a 0xFFFF, to zaadresujemy 64 KiB powyżej granicy 1 MiB, co najprawdopodobniej zaowocuje „zawinięciem” pamięci. Dokładniej zjawisko to potraktujemy przy okazji omawiania linii A20.

Funkcje oferowane przez BIOS

BIOS oferuje do użycia w trybie rzeczywistym szereg funkcji dostępnych poprzez przerwania (o przerwaniach będzie jeszcze rozlegle w kolejnych odcinkach). Opis niektórych funkcji można znaleźć tu. Przykład użycia funkcji BIOS będzie można prześledzić w następnych częściach cyklu.

Podsumowanie

Jak widać, tryb rzeczywisty ma bardzo niewiele do zaoferowania programiście systemów operacyjnych. Tryb rzeczywisty powinien nam zatem posłużyć do szybkiego przejścia do trybu chronionego i ewentualnego wykonania niezbędnych czynności poprzedzających. Aha i tak, system operacyjny DOS działał w całości w trybie rzeczywistym :)