Szkoła montażu: rozwój systemu operacyjnego. Napiszmy jądro! Tworzenie najprostszego działającego jądra systemu operacyjnego Co jest potrzebne do stworzenia systemu operacyjnego


Autor Gracz Vulfa zadał pytanie w dziale Inne języki i technologie

Jak stworzyć własny system operacyjny? i dostałem najlepszą odpowiedź

Odpowiedź od Alexander Bagrov[guru]
Pomysł jest godny pochwały.
Przede wszystkim musisz znać system poleceń maszyny, dla której zamierzasz napisać system operacyjny.
System poleceń jest bezpośrednio odzwierciedlony w języku asemblera.
Dlatego w pierwszej kolejności trzeba wymyślić własny język asemblera i napisać dla niego program (assembler) tłumaczący symbole alfanumeryczne na język maszynowy
kod.
Jeśli jesteś zainteresowany, możesz zobaczyć, jakie wymagania powinien mieć nowy (idealny) system operacyjny.
Niektóre takie funkcje są wymienione tutaj:
.ru/D_OS/OS-PolyM.html#IdealOS
Konieczne jest przestudiowanie materiałów ze stron twórców mikroprocesorów. Na przykład Intel i AMD.
Przydatny może okazać się kurs wykładu wideo na temat systemu operacyjnego, który przedstawiono tutaj:
.ru/D_OS/OS_General.html
PS: Nie słuchaj pesymistów. Opierając się na ideologii koguta goniącego kurczaka:
„Nie nadrobię zaległości, przynajmniej się rozgrzeję”.
Źródło: Strona internetowa „Używaj swojego komputera poprawnie!”

Odpowiedź od 2 odpowiedzi[guru]

Cześć! Oto wybór tematów z odpowiedziami na Twoje pytanie: Jak stworzyć własny system operacyjny?

Odpowiedź od Wadim Khprlamov[Nowicjusz]
Oczywiście) Są tu tylko bramy) Zapytaj Microsoft)



Odpowiedź od Irina Starodubcewa[Nowicjusz]
weź wszystkie systemy operacyjne i upchnij je w jeden


Odpowiedź od Aleksander Tuntow[guru]
Zapoznaj się z systemem operacyjnym Linux, naucz się programowania i działaj


Odpowiedź od ~W płomieniach~[guru]
Naucz się programowania na najwyższym poziomie, zbierz całą rzeszę tych samych geniuszy komputerowych, a wtedy dasz radę.


Odpowiedź od Rasul Magomedow[guru]
Zacznij od stworzenia zabawnej tapety


Odpowiedź od Kapitanie Google[guru]
Nie słuchajcie „10 lat nauki podstaw” – Torvalds napisał pierwszą wersję Linuksa w wieku 22 lat, a komputer dostał w wieku 12 lat. Jak rozumiesz, studiował nie tylko podstawy.
Zacznij od przestudiowania tego, co już istnieje - z jednej strony „Nowoczesne systemy operacyjne” Tannenbauma, z drugiej strony zbuduj Linuksa od podstaw, z trzeciej naucz się Asemblera, C, C++. W ciągu 3-4 lat możesz zrobić wszystko ze wszystkim. Potem możesz zacząć rozwijać swój system... jeśli nadal chcesz.


Odpowiedź od Yoanya Semenow[guru]
Czy wiesz, co zrobił Gates? spróbuj tego samego, mówią, że okazuje się opłacalne..
kiedy rodzice go ukarali, on nie mając nic lepszego do roboty, zaczął skakać tyłkiem po klawiaturze, po czym sprzedał ją, nazywając to, co dostał „oknami”
ps i jeśli to prawda, najpierw napisz „Hello World” w C++, a od razu zrozumiesz, że pomysł jest paranoiczny


Odpowiedź od Kostafey[guru]
Po co? Co zasadniczo nie podoba się obecnym? Czy naprawdę nie ma takiego, który przynajmniej częściowo spełnia Twoje wymagania dotyczące systemu operacyjnego? Może lepiej dołączyć do zespołu deweloperskiego? Sensu będzie 100500 razy więcej.
A potem porzucisz ten pomysł na kolejne 0,(0)1% jego wdrożenia.


Odpowiedź od Jewgienij Lomega[guru]
E. Tanenbaum „Systemy operacyjne: rozwój i wdrażanie”
Powodzenia
PS Niestety jest mało prawdopodobne, że odniesiesz sukces taki, jak Bill Gates. Jego matka jest fajną bankierką, a twoja?


Odpowiedź od Kora Kraba[guru]
Możesz sam napisać prosty system operacyjny, ale nigdy nie będzie on w stanie konkurować z systemem operacyjnym takim jak Windows, MAC OS czy Linux, nad którym setki, a nawet tysiące programistów pracowało przez co najmniej dziesięć lat. Poza tym system operacyjny to tylko podstawa. Konieczne jest, aby programiści sprzętu napisali własne sterowniki dla tego systemu operacyjnego, twórcy programów aplikacyjnych napisali dla niego edytory, odtwarzacze, przeglądarki, gry, diabeł w moździerzu... A bez tego system operacyjny pozostanie bezużytecznym fundamentem domu którego nikt nie zbuduje.


Odpowiedź od Wadim Stachanow[aktywny]
Byłoby lepiej, gdybym poszła na studia jako filolog. A potem krzyczał: „Darmowa kasa!”


Odpowiedź od =Serge=[guru]
Brawo! Na koniec pytanie 58 na stronie dotyczące tworzenia „własnego” systemu operacyjnego))
Oto pytania dotyczące „pisania własnego systemu operacyjnego” - jest ich tylko 34))
Przeczytaj...92 pytania*10 odpowiedzi = około 920 odpowiedzi))
Jednocześnie prawdopodobnie zrozumiesz, co oznacza „nie nudna tapeta”).


Odpowiedź od Nie do odtworzenia[guru]
kolejny Denis Popov z innym BolgenOS?


Odpowiedź od Iwan Tatarczuk[Nowicjusz]
uruchom notatnik, pobierz kompilator toadscript i zacznij skakać po klawiaturze
skompiluj w 60 minut i tyle
twoja oska jest gotowa


Odpowiedź od Owca Mila[Nowicjusz]
system operacyjny? Które dokładnie? Oryginalny system operacyjny Chester (oryginalny znak (przetłumaczony))
Potrzebuje dojenia swoich zdjęć w kreskówkach i filmach.
1. Zastanów się, jaki rodzaj kreskówki/filmu chcesz dla systemu operacyjnego
2. Weź pod uwagę styl rysunkowy/filmowy
3. Kim będzie Twoja postać (wróżka, kucyk, magik, robot itp.)
4. Opisz to w myślach, a następnie na papierze
5. Wymyśl projekt
6. Wymyśl imię i biografię
7. Narysuj postać!
8. A teraz przejdźmy do rzeczy z Paint Tooth Sai

Ten cykl artykułów poświęcony jest programowaniu niskiego poziomu, czyli architekturze komputerów, projektowaniu systemów operacyjnych, programowaniu w języku asemblera i dziedzinom pokrewnym. Jak dotąd pisaniem zajmuje się dwóch habrauserów – i . Dla wielu uczniów szkół średnich, studentów, a nawet zawodowych programistów tematy te okazują się bardzo trudne do nauczenia. Istnieje wiele literatury i kursów poświęconych programowaniu niskiego poziomu, ale trudno jest uzyskać pełny i obejmujący wszystko obraz. Trudno jest, po przeczytaniu jednej lub dwóch książek o języku asemblera i systemach operacyjnych, przynajmniej ogólnie wyobrazić sobie, jak faktycznie działa ten złożony system złożony z żelaza, krzemu i wielu programów - komputera.

Każdy rozwiązuje problem uczenia się na swój sposób. Niektórzy ludzie czytają dużo literatury, niektórzy starają się szybko przejść do praktyki i na bieżąco zdobywać wiedzę, jeszcze inni próbują wyjaśniać przyjaciołom wszystko, czego się uczą. Postanowiliśmy połączyć te podejścia. Dlatego w tym toku artykułów pokażemy krok po kroku, jak napisać prosty system operacyjny. Artykuły będą miały charakter przeglądowy, czyli nie będą zawierały wyczerpujących informacji teoretycznych, zawsze jednak będziemy starali się podawać linki do dobrych materiałów teoretycznych i odpowiadać na wszystkie pojawiające się pytania. Nie mamy jasnego planu, więc wiele ważnych decyzji zostanie podjętych po drodze, biorąc pod uwagę Wasze uwagi.

Możemy celowo utrudniać proces rozwoju, aby umożliwić Tobie i sobie pełne zrozumienie pełnych konsekwencji złej decyzji, a także udoskonalenie pewnych umiejętności technicznych w tym zakresie. Nie należy więc uważać naszych decyzji za jedyne słuszne i ślepo nam wierzyć. Jeszcze raz podkreślmy, że oczekujemy od czytelników aktywności w dyskusji nad artykułami, co powinno znacząco wpłynąć na całościowy proces opracowywania i pisania kolejnych artykułów. Idealnie byłoby, gdyby z biegiem czasu część czytelników włączyła się w rozwój systemu.

Zakładamy, że czytelnik zna już podstawy języków asemblera i C, a także elementarne pojęcia architektury komputerowej. Oznacza to, że nie będziemy wyjaśniać, czym jest rejestr lub, powiedzmy, pamięć RAM. Jeśli nie masz wystarczającej wiedzy, zawsze możesz sięgnąć po dodatkową literaturę. Na końcu artykułu znajduje się krótka lista odnośników i linków do stron z dobrymi artykułami. Wskazane jest również, aby wiedzieć, jak korzystać z Linuksa, ponieważ wszystkie instrukcje kompilacji zostaną podane specjalnie dla tego systemu.

A teraz - bliżej sedna. W dalszej części artykułu napiszemy klasyczny program „Hello World”. Nasz Helloworld będzie trochę specyficzny. Nie zostanie uruchomiony z żadnego systemu operacyjnego, ale bezpośrednio, że tak powiem, „na gołym metalu”. Zanim zaczniemy pisać kod, zastanówmy się dokładnie, jak chcemy to zrobić. W tym celu musimy wziąć pod uwagę proces uruchamiania komputera.

Weź więc swój ulubiony komputer i naciśnij największy przycisk na jednostce systemowej. Widzimy wesoły wygaszacz ekranu, jednostka systemowa radośnie piszczy swoim głośnikiem i po pewnym czasie ładuje się system operacyjny. Jak rozumiesz, system operacyjny jest przechowywany na dysku twardym i tutaj pojawia się pytanie: w jaki sposób system operacyjny w magiczny sposób załadował się do pamięci RAM i zaczął działać?

Wiedz o tym: odpowiedzialny jest za to system znajdujący się na dowolnym komputerze, a jego nazwa - nie, nie Windows, chwyć język - nazywa się BIOS. Jego nazwa oznacza Podstawowy System Wejścia-Wyjścia, czyli podstawowy układ wejścia-wyjścia. BIOS znajduje się na małym chipie na płycie głównej i uruchamia się natychmiast po naciśnięciu dużego przycisku ON. BIOS ma trzy główne zadania:

  1. Wykryj wszystkie podłączone urządzenia (procesor, klawiaturę, monitor, pamięć RAM, kartę graficzną, głowę, ramiona, skrzydła, nogi i ogony...) i sprawdź ich funkcjonalność. Odpowiada za to program POST (Power On Self Test). Jeśli nie zostanie wykryty niezbędny sprzęt, żadne oprogramowanie nie będzie w stanie pomóc i w tym momencie głośnik systemowy napisze coś złowieszczego, a system operacyjny w ogóle nie przejdzie do sedna. Nie rozmawiajmy o smutnych rzeczach, załóżmy, że mamy w pełni działający komputer, cieszmy się i przejdźmy do rozważań nad drugą funkcją BIOS-u:
  2. Zapewnienie systemowi operacyjnemu podstawowego zestawu funkcji do pracy ze sprzętem. Na przykład poprzez funkcje BIOS-u można wyświetlić tekst na ekranie lub odczytać dane z klawiatury. Dlatego nazywa się go podstawowym systemem wejścia/wyjścia. Zwykle system operacyjny uzyskuje dostęp do tych funkcji poprzez przerwania.
  3. Uruchamianie modułu ładującego system operacyjny. W tym przypadku z reguły odczytywany jest sektor rozruchowy - pierwszy sektor nośnika danych (dyskietka, dysk twardy, płyta CD, dysk flash). Kolejność odpytywania multimediów można ustawić w BIOS SETUP. Sektor rozruchowy zawiera program nazywany czasem głównym programem ładującym. Z grubsza rzecz biorąc, zadaniem bootloadera jest uruchomienie systemu operacyjnego. Proces ładowania systemu operacyjnego może być bardzo specyficzny i w dużym stopniu zależny od jego funkcji. Dlatego główny program ładujący jest pisany bezpośrednio przez twórców systemu operacyjnego i zapisywany w sektorze rozruchowym podczas instalacji. Po uruchomieniu programu ładującego procesor znajduje się w trybie rzeczywistym.
Smutna wiadomość jest taka, że ​​bootloader powinien mieć rozmiar tylko 512 bajtów. Dlaczego tak niewielu? Aby to zrobić, musimy zapoznać się ze strukturą dyskietki. Oto zdjęcie informacyjne:

Zdjęcie przedstawia powierzchnię dysku. Dyskietka ma 2 powierzchnie. Każda powierzchnia ma ścieżki (ścieżki) w kształcie pierścienia. Każda ścieżka jest podzielona na małe fragmenty w kształcie łuku zwane sektorami. Historycznie rzecz biorąc, sektor dyskietki ma rozmiar 512 bajtów. Pierwszy sektor dysku, sektor rozruchowy, jest odczytywany przez BIOS do zerowego segmentu pamięci przy przesunięciu 0x7C00, a następnie sterowanie jest przekazywane na ten adres. Program ładujący zwykle ładuje do pamięci nie sam system operacyjny, ale inny program ładujący przechowywane na dysku, ale z jakiegoś powodu (najprawdopodobniej jest to rozmiar) nie mieszczą się w jednym sektorze.A ponieważ na razie rolę naszego systemu operacyjnego pełni banalny Helloworld, naszym głównym celem jest sprawienie, aby komputer uwierzył o istnieniu naszego systemu operacyjnego, nawet w jednym sektorze, i uruchom go.

Jak zbudowany jest sektor rozruchowy? Na komputerze PC jedynym wymaganiem dla sektora rozruchowego jest to, że jego ostatnie dwa bajty zawierają wartości 0x55 i 0xAA - sygnaturę sektora rozruchowego. Zatem jest już mniej więcej jasne, co musimy zrobić. Napiszmy kod! Podany kod jest napisany dla asemblera Yasm.

Sekcja .text use16 org 0x7C00 ; nasz program jest ładowany pod adresem 0x7C00 start: mov ax, cs mov ds, ax ; wybierz segment danych mov si, wiadomość cld; kierunek poleceń łańcuchowych mov ah, 0x0E ; Numer funkcji BIOS mov bh, 0x00 ; strona pamięci wideo puts_loop: lodsb ; załaduj następny symbol do al test al, al ; znak null oznacza koniec linii jz puts_loop_exit int 0x10 ; wywołaj funkcję BIOS-u jmp puts_loop puts_loop_exit: jmp $ ; wieczna pętla komunikatu: db "Hello World!", 0 wykończenie: razy 0x1FE-finish+start db 0 db 0x55, 0xAA ; podpis sektora rozruchowego

Ten krótki program wymaga kilku ważnych wyjaśnień. Linia org 0x7C00 jest potrzebna, aby asembler (czyli program, a nie język) poprawnie obliczył adresy etykiet i zmiennych (puts_loop, puts_loop_exit, komunikat). Informujemy go więc, że program zostanie załadowany do pamięci pod adresem 0x7C00.
W liniach
mov topór, cs mov ds, topór
segment danych (ds) jest równy segmentowi kodu (cs), ponieważ w naszym programie zarówno dane, jak i kod są przechowywane w tym samym segmencie.

Następnie w pętli znak po znaku wyświetlany jest komunikat „Hello World!”. W tym celu wykorzystywana jest funkcja 0x0E przerwania 0x10. Posiada następujące parametry:
AH = 0x0E (numer funkcji)
BH = numer strony wideo (nie przejmuj się, wskaż 0)
AL = kod znaku ASCII

Przy linii „jmp$” program zawiesza się. I słusznie, nie ma potrzeby wykonywania dodatkowego kodu. Aby jednak komputer mógł ponownie działać, konieczne będzie ponowne uruchomienie komputera.

W wierszu „times 0x1FE-finish+start db 0” pozostała część kodu programu (z wyjątkiem dwóch ostatnich bajtów) jest wypełniona zerami. Odbywa się to tak, że po kompilacji dwa ostatnie bajty programu zawierają sygnaturę sektora rozruchowego.

Wygląda na to, że uporządkowaliśmy kod programu, spróbujmy teraz skompilować to szczęście. Do kompilacji będziemy potrzebować, ściśle mówiąc, asemblera - wspomnianego yasma. Jest dostępny w większości repozytoriów Linuksa. Program można skompilować w następujący sposób:

$ yasm -f bin -o hello.bin hello.asm

Powstały plik hello.bin należy zapisać w sektorze rozruchowym dyskietki. Robi się to mniej więcej tak (oczywiście zamiast fd musisz zastąpić nazwę swojego dysku).

$ dd if=hello.bin of=/dev/fd

Ponieważ nie wszyscy nadal mają dyski i dyskietki, możesz użyć maszyny wirtualnej, na przykład qemu lub VirtualBox. Aby to zrobić, będziesz musiał utworzyć obraz dyskietki za pomocą naszego programu ładującego i włożyć go do „wirtualnej stacji dyskietek”.
Utwórz obraz dysku i wypełnij go zerami:

$ dd if=/dev/zero of=disk.img bs=1024 liczba=1440

Nasz program piszemy na samym początku obrazu:
$ dd if=hello.bin of=disk.img conv=notrunc

Uruchamiamy wynikowy obraz w qemu:
$ qemu -fda dysk.img -boot a

Po uruchomieniu powinieneś zobaczyć okno qemu ze szczęśliwym wierszem „Hello World!” W tym miejscu kończy się pierwszy artykuł. Będziemy szczęśliwi, jeśli zapoznamy się z Twoimi opiniami i życzeniami.

Co musisz wiedzieć, aby napisać system operacyjny

Tworzenie systemu operacyjnego jest jednym z najtrudniejszych zadań w programowaniu, ponieważ wymaga rozległej i złożonej wiedzy na temat działania komputera. Które? Dowiedzmy się poniżej.

Co to jest system operacyjny

System operacyjny (OS) to oprogramowanie współpracujące ze sprzętem komputerowym i jego zasobami oraz stanowiące pomost pomiędzy sprzętem i oprogramowaniem komputera.

Komputery pierwszej generacji nie posiadały systemów operacyjnych. Programy na pierwszych komputerach zawierały kod do bezpośredniej obsługi systemu, komunikacji z urządzeniami peryferyjnymi i obliczeń, dla których ten program został napisany. Z powodu tej sytuacji nawet programy o prostej logice były trudne do wdrożenia w oprogramowaniu.

W miarę jak komputery stawały się coraz bardziej zróżnicowane i złożone, pisanie programów, które działały zarówno jako system operacyjny, jak i aplikacja, stało się po prostu niewygodne. Dlatego, aby ułatwić pisanie programów, właściciele komputerów zaczęli opracowywać oprogramowanie. Tak pojawiły się systemy operacyjne.

System operacyjny zapewnia wszystko, co niezbędne do działania programów użytkownika. Ich pojawienie się sprawiło, że programy nie musiały już kontrolować całego wolumenu pracy komputera (jest to doskonały przykład enkapsulacji). Teraz programy musiały współpracować specjalnie z systemem operacyjnym, a sam system dbał o zasoby i współpracę z urządzeniami peryferyjnymi (klawiatura, drukarka).

Krótka historia systemów operacyjnych

Język C

Jak wspomniano powyżej, istnieje kilka języków programowania wysokiego poziomu do pisania systemu operacyjnego. Jednak najpopularniejszym z nich jest C.

Tutaj możesz rozpocząć naukę tego języka. Ten zasób wprowadzi Cię w podstawowe pojęcia i przygotuje Cię do bardziej zaawansowanych zadań.

„Naucz się C na własnej skórze” to tytuł innej książki. Oprócz zwykłej teorii zawiera wiele praktycznych rozwiązań. Ten samouczek obejmie wszystkie aspekty języka.

Możesz też wybrać jedną z tych książek:

  • „Język programowania C” autorstwa Kernighana i Ritchiego;
  • „Absolutny przewodnik dla początkujących po programowaniu w C” autorstwa Parry'ego i Millera.

Rozwój systemu operacyjnego

Po opanowaniu wszystkiego, co musisz wiedzieć na temat informatyki, języka asemblera i C, powinieneś przeczytać co najmniej jedną lub dwie książki na temat faktycznego rozwoju systemów operacyjnych. Oto kilka zasobów na ten temat:

„Linux od zera”. Omówiono tutaj proces asemblowania systemu operacyjnego Linux (podręcznik został przetłumaczony na wiele języków, w tym na rosyjski). Tutaj, podobnie jak w innych podręcznikach, otrzymasz całą niezbędną wiedzę podstawową. Polegając na nich, możesz spróbować swoich sił w tworzeniu systemu operacyjnego. Aby oprogramowanie będące częścią systemu operacyjnego było bardziej profesjonalne, do podręcznika dodano dodatki: „

Jeśli dojdziemy do sedna...

System operacyjny to element, który implementuje wielozadaniowość (zwykle) i zarządza dystrybucją zasobów między tymi zadaniami i ogólnie. Trzeba uważać, aby zadania nie szkodziły sobie nawzajem i pracować w różnych obszarach pamięci oraz pracować po kolei z urządzeniami, to przynajmniej. Musisz także zapewnić możliwość przesyłania komunikatów z jednego zadania do drugiego.

Ponadto system operacyjny, jeśli istnieje pamięć długoterminowa, musi zapewniać do niej dostęp: to znaczy zapewniać wszystkie funkcje do pracy z systemem plików. To minimum.

Niemal wszędzie pierwszy kod startowy musi być napisany w języku asemblera - istnieje kilka zasad mówiących, gdzie powinien się znajdować, jak powinien wyglądać, co powinien robić i jakiego rozmiaru nie powinien przekraczać.

W przypadku komputera PC należy napisać bootloader na ASMA, który zostanie wywołany przez BIOS i który powinien, nie przekraczając czterech i kilkuset bajtów, coś zrobić i uruchomić główny system operacyjny - przenieść kontrolę do głównego kodu, który w najbliższą przyszłość można zapisać w języku C.

W przypadku ARM należy utworzyć tabelę przerwań na ACMA (reset, różne błędy, przerwania IRQ, przerwania FIQ itp.) i przenieść kontrolę do kodu głównego. Chociaż w wielu środowiskach programistycznych taki kod jest dostępny dla prawie każdego kontrolera.

Oznacza to, że do tego potrzebujesz:

  1. Znajomość języka asemblera platformy docelowej.
  2. Znajomość architektury procesora oraz wszelkiego rodzaju poleceń serwisowych i rejestrów w celu skonfigurowania go do pracy w pożądanym trybie. W komputerze PC jest to na przykład przejście do trybu chronionego lub do trybu 64-bitowego... W ARM jest to ustawienie taktowania rdzenia i urządzeń peryferyjnych.
  3. Dowiedz się dokładnie, jak uruchomi się system operacyjny, gdzie i jak musisz przekazać swój kod.
  4. Znajomość języka C - bez doświadczenia trudno napisać duży kod w Asmie, utrzymanie go będzie jeszcze trudniejsze. Dlatego musisz napisać jądro w C.
  5. Znajomość zasad działania systemu operacyjnego. Cóż, jest wiele książek w języku rosyjskim na ten temat, chociaż nie wiem, czy wszystkie są dobre.
  6. Miejcie dużo cierpliwości i wytrwałości. Błędy będą, trzeba je znaleźć i poprawić. Będziesz także musiał dużo czytać.
  7. Mieć dużo, dużo czasu.

Dalej. Powiedzmy, że coś napisałeś. Musimy to przetestować. Potrzebujesz albo fizycznego urządzenia, na którym będą odbywać się eksperymenty (płytka debugowania, drugi komputer), albo jego emulatora. Drugi sposób jest zazwyczaj łatwiejszy i szybszy w użyciu. Na PC, na przykład VMWare.

W Internecie jest też mnóstwo artykułów na ten temat, jeśli dobrze poszukasz. Istnieje również wiele przykładów gotowych systemów operacyjnych wraz z kodami źródłowymi.

Jeśli naprawdę chcesz, możesz nawet zajrzeć do kodu źródłowego starego jądra systemów NT (Windows), zarówno osobno (który został opublikowany przez Microsoft, z komentarzami i różnego rodzaju materiałami referencyjnymi), jak i w połączeniu ze starym System operacyjny (wyciekł).

Opracowanie jądra jest słusznie uważane za zadanie niełatwe, ale każdy może napisać proste jądro. Aby doświadczyć magii hackowania jądra, wystarczy przestrzegać pewnych konwencji i opanować język asemblera. W tym artykule pokażemy, jak to zrobić.


Witaj świecie!

Napiszmy jądro, które będzie uruchamiane przez GRUB na systemach kompatybilnych z x86. Nasze pierwsze jądro wyświetli komunikat na ekranie i zatrzyma się na tym.

Jak uruchamiają się maszyny x86

Zanim pomyślimy o tym, jak napisać jądro, przyjrzyjmy się, jak komputer uruchamia się i przekazuje kontrolę do jądra. Większość rejestrów procesorów x86 ma określone wartości po uruchomieniu. Rejestr wskaźników instrukcji (EIP) zawiera adres instrukcji, która będzie wykonywana przez procesor. Jego zakodowana na stałe wartość to 0xFFFFFFF0. Oznacza to, że procesor x86 zawsze rozpocznie wykonywanie od adresu fizycznego 0xFFFFFFF0. Jest to ostatnie 16 bajtów 32-bitowej przestrzeni adresowej. Adres ten nazywany jest wektorem resetowania.

Karta pamięci zawarta w chipsecie stwierdza, że ​​adres 0xFFFFFFF0 odnosi się do konkretnej części BIOS-u, a nie do pamięci RAM. Jednak BIOS kopiuje się do pamięci RAM w celu szybszego dostępu - proces ten nazywa się „shadowing”, tworząc kopię w tle. Zatem adres 0xFFFFFFF0 będzie zawierał jedynie instrukcję skoku do lokalizacji w pamięci, do której BIOS się skopiował.

Tak więc BIOS zaczyna działać. Najpierw szuka urządzeń, z których może uruchomić system, w kolejności określonej w ustawieniach. Sprawdza nośnik pod kątem „magicznej liczby”, która odróżnia dyski startowe od zwykłych: jeśli bajty 511 i 512 w pierwszym sektorze mają wartość 0xAA55, wówczas dysk nadaje się do rozruchu.

Gdy BIOS znajdzie urządzenie startowe, skopiuje zawartość pierwszego sektora do pamięci RAM, zaczynając od adresu 0x7C00, a następnie przeniesie wykonanie pod ten adres i rozpocznie wykonywanie właśnie załadowanego kodu. Ten kod nazywa się bootloaderem.

Program ładujący ładuje jądro pod adresem fizycznym 0x100000. Tego właśnie używają najpopularniejsze jądra x86.

Wszystkie procesory kompatybilne z x86 rozpoczynają pracę w prymitywnym trybie 16-bitowym zwanym „trybem rzeczywistym”. Program ładujący GRUB przełącza procesor w 32-bitowy tryb chroniony, ustawiając dolny bit rejestru CR0 na jeden. Dlatego jądro zaczyna się ładować w 32-bitowym trybie chronionym.

Należy pamiętać, że GRUB w przypadku jąder Linuksa wybiera odpowiedni protokół rozruchowy i uruchamia jądro w trybie rzeczywistym. Jądra Linuksa automatycznie przełączają się w tryb chroniony.

Czego potrzebujemy

  • Komputer kompatybilny z x86 (oczywiście)
  • Linuksa
  • Asembler NASM,
  • ld (GNU Linker),
  • ŻARCIE.

Punkt wejścia języka asemblera

Chcielibyśmy oczywiście wszystko napisać w C, jednak nie uda nam się całkowicie uniknąć użycia asemblera. Napiszemy mały plik w asemblerze x86, który stanie się punktem wyjścia dla naszego jądra. Wszystko, co zrobi kod asemblera, to wywołanie zewnętrznej funkcji, którą napiszemy w C, a następnie zatrzymanie wykonywania programu.

Jak możemy uczynić kod asemblera punktem wyjścia dla naszego jądra? Używamy skryptu linkera, który łączy pliki obiektowe i tworzy końcowy plik wykonywalny jądra (wyjaśnię więcej poniżej). W tym skrypcie bezpośrednio wskażemy, że chcemy, aby nasz plik binarny został pobrany pod adresem 0x100000. Jest to adres jak już pisałem, pod którym bootloader spodziewa się zobaczyć punkt wejścia do jądra.

Oto kod asemblera.

jądro.asm
bity 32 sekcja .text global start extern kmain start: cli mov esp, stack_space wywołanie kmain hlt sekcja .bss resb 8192 stack_space:

Pierwsza instrukcja 32-bitowa nie jest asemblerem x86, ale dyrektywą NASM nakazującą wygenerowanie kodu dla procesora, aby działał w trybie 32-bitowym. W naszym przykładzie nie jest to konieczne, ale dobrą praktyką jest wyraźne wskazanie tego.

Druga linia rozpoczyna sekcję tekstową, zwaną także sekcją kodu. Cały nasz kod trafi tutaj.

global to kolejna dyrektywa NASM, deklaruje, że symbole w naszym kodzie są globalne. Umożliwi to linkerowi znalezienie symbolu startu, który służy jako nasz punkt wejścia.

kmain to funkcja, która zostanie zdefiniowana w naszym pliku kernel.c. extern deklaruje, że funkcja jest zadeklarowana gdzie indziej.

Następnie następuje funkcja start, która wywołuje kmain i zatrzymuje procesor instrukcją hlt. Przerwania mogą obudzić procesor po hlt , dlatego najpierw wyłączamy przerwania za pomocą instrukcji cli (kasowanie przerwań).

Idealnie byłoby, gdybyśmy przydzielili pewną ilość pamięci dla stosu i skierowali na niego wskaźnik stosu (esp). Wydaje się, że GRUB i tak robi to za nas i w tym momencie wskaźnik stosu jest już ustawiony. Jednak na wszelki wypadek przydzielmy trochę pamięci w sekcji BSS i skierujmy wskaźnik stosu na jej początek. Używamy instrukcji resb - rezerwuje ona pamięć podaną w bajtach. Następnie pozostawiany jest znak wskazujący krawędź zarezerwowanego fragmentu pamięci. Tuż przed wywołaniem kmain wskaźnik stosu (esp) jest kierowany do tego obszaru za pomocą instrukcji mov.

Jądro w C

W pliku kernel.asm wywołaliśmy funkcję kmain(). Zatem w kodzie C wykonanie rozpocznie się od tego miejsca.

jądro.c
void kmain(void) ( const char *str = "moje pierwsze jądro"; char *vidptr = (char*)0xb8000; unsigned int i = 0; unsigned int j = 0; while(j< 80 * 25 * 2) { vidptr[j] = " "; vidptr = 0x07; j = j + 2; } j = 0; while(str[j] != "\0") { vidptr[i] = str[j]; vidptr = 0x07; ++j; i = i + 2; } return; }

Wszystko, co zrobi nasze jądro, to wyczyści ekran i wydrukuje wiersz „moje pierwsze jądro”.

Najpierw tworzymy wskaźnik vidptr, który wskazuje adres 0xb8000. W trybie chronionym jest to początek pamięci wideo. Pamięć ekranu tekstowego jest po prostu częścią przestrzeni adresowej. Na ekranowe wejścia/wyjścia przydzielona jest część pamięci, która rozpoczyna się pod adresem 0xb8000 i umieszcza się w niej 25 linii po 80 znaków ASCII.

Każdy znak w pamięci tekstowej jest reprezentowany przez 16 bitów (2 bajty), a nie 8 bitów (1 bajt), do których jesteśmy przyzwyczajeni. Pierwszy bajt to kod ASCII znaku, a drugi bajt to bajt atrybutu. Jest to definicja formatu znaku, łącznie z jego kolorem.

Aby wyprowadzić znak zielony na czarnym, musimy umieścić s w pierwszym bajcie pamięci wideo i wartość 0x02 w drugim bajcie. 0 oznacza tutaj czarne tło, a 2 oznacza kolor zielony. Użyjemy koloru jasnoszarego, jego kod to 0x07.

W pierwszej pętli while program wypełnia wszystkie 25 linii po 80 znaków pustymi znakami z atrybutem 0x07. Spowoduje to wyczyszczenie ekranu.

W drugiej pętli while zakończony znakiem null ciąg mojego pierwszego jądra jest zapisywany w pamięci wideo i każdy znak otrzymuje bajt atrybutu o wartości 0x07. To powinno wyprowadzić ciąg.

Układ

Teraz musimy skompilować kernel.asm do pliku obiektowego przy użyciu NASM, a następnie użyć GCC do skompilowania kernel.c do innego pliku obiektowego. Naszym zadaniem jest połączenie tych obiektów w wykonywalne jądro nadające się do załadowania. Aby to zrobić będziemy musieli napisać skrypt dla linkera (ld), który przekażemy jako argument.

link.ld
FORMAT_WYJŚCIOWY(elf32-i386) WPIS (start) SEKCJE ( . = 0x100000; .text: ( *(.text) ) .data: ( *(.data) ) .bss: ( *(.bss) ) )

Tutaj najpierw ustawiamy format (OUTPUT_FORMAT) naszego pliku wykonywalnego na 32-bitowy ELF (Executable and Linkable Format), standardowy format binarny dla systemów uniksowych dla architektury x86.

ENTRY przyjmuje jeden argument. Określa nazwę symbolu, który będzie służyć jako punkt wejścia pliku wykonywalnego.

SEKCJE to dla nas najważniejsza część. Tutaj definiujemy układ naszego pliku wykonywalnego. Możemy zdefiniować, w jaki sposób różne sekcje zostaną połączone i gdzie każda sekcja zostanie umieszczona.

W nawiasach klamrowych znajdujących się po wyrażeniu SECTIONS kropka wskazuje licznik lokalizacji. Jest on automatycznie inicjowany na 0x0 na początku bloku SECTIONS, ale można go zmienić, przypisując nową wartość.

Pisałem wcześniej, że kod jądra powinien zaczynać się od adresu 0x100000. Dlatego przypisujemy licznikowi pozycji wartość 0x100000.

Spójrz na line.text: ( *(.text) ). Gwiazdka tutaj określa maskę, która może pasować do dowolnej nazwy pliku. W związku z tym wyrażenie *(.text) oznacza wszystkie wejściowe sekcje .text we wszystkich plikach wejściowych.

W rezultacie linker połączy wszystkie sekcje tekstowe wszystkich plików obiektowych z sekcją tekstową pliku wykonywalnego i umieści ją pod adresem określonym w liczniku pozycji. Sekcja kodu naszego pliku wykonywalnego rozpocznie się pod adresem 0x100000.

Gdy linker utworzy sekcję tekstową, wartość licznika pozycji będzie wynosić 0x100000 plus rozmiar sekcji tekstowej. Podobnie sekcje data i bss zostaną scalone i umieszczone pod adresem wskazanym przez licznik pozycji.

GRUB i multiboot

Teraz wszystkie nasze pliki są gotowe do zbudowania jądra. Ponieważ jednak będziemy uruchamiać jądro za pomocą GRUB-a, pozostaje jeszcze jeden krok.

Istnieje standard ładowania różnych jąder x86 przy użyciu programu ładującego. Nazywa się to „specyfikacją wielu rozruchów”. GRUB załaduje tylko pasujące jądra.

Zgodnie z tą specyfikacją jądro może zawierać nagłówek (nagłówek Multiboot) w pierwszych 8 kilobajtach. Nagłówek ten musi zawierać trzy pola:

  • magia- zawiera „magiczny” numer 0x1BADB002, po którym identyfikowany jest nagłówek;
  • flagi- to pole nie jest dla nas istotne, możesz pozostawić zero;
  • suma kontrolna- suma kontrolna, powinna dać zero jeśli zostanie dodana do pól magii i flag.

Nasz plik kernel.asm będzie teraz wyglądał tak.

jądro.asm
bity 32 sekcja .text ;specyfikacja multiboot Align 4 dd 0x1BADB002 ;magic dd 0x00 ;flags dd - (0x1BADB002 + 0x00) ;suma kontrolna global start extern kmain start: cli mov esp, stack_space wywołanie kmain hlt sekcja .bss resb 8192 stack_space:

Instrukcja dd określa 4-bajtowe podwójne słowo.

Składanie jądra

Wszystko jest więc gotowe do utworzenia pliku obiektowego z kernel.asm i kernel.c i połączenia ich za pomocą naszego skryptu. W konsoli piszemy:

$ nasm -f elf32 kernel.asm -o kasm.o

Używając tego polecenia, asembler utworzy plik kasm.o w formacie ELF-32-bitowym. Teraz kolej na GCC:

$ gcc -m32 -c jądro.c -o kc.o

Parametr -c wskazuje, że plik nie musi być łączony po kompilacji. Zrobimy to sami:

$ ld -m elf_i386 -T link.ld -o jądro kasm.o kc.o

To polecenie uruchomi linker z naszym skryptem i wygeneruje plik wykonywalny o nazwie kernel.

OSTRZEŻENIE

Hakowanie jądra najlepiej wykonywać w środowisku wirtualnym. Aby uruchomić jądro w QEMU zamiast GRUB-ie, użyj polecenia qemu-system-i386 -kernel kernel .

Konfigurowanie GRUB-a i uruchamianie jądra

GRUB wymaga, aby nazwa pliku jądra była zgodna z jądrem-<версия>. Zmieńmy więc nazwę pliku - ja nazwę mój plik kernel-701.

Teraz umieszczamy jądro w katalogu /boot. Będzie to wymagało uprawnień administratora.

Będziesz musiał dodać coś takiego do pliku konfiguracyjnego GRUB grub.cfg:

Tytuł myKernel root (hd0,0) kernel /boot/kernel-701 ro

Nie zapomnij usunąć dyrektywy hidemenu, jeśli jest uwzględniona.

GRUB 2

Aby uruchomić jądro, które stworzyliśmy w GRUB 2, które jest domyślnie dostarczane w nowych dystrybucjach, twoja konfiguracja powinna wyglądać następująco:

Pozycja menu „kernel 701” ( set root="hd0,msdos1" multiboot /boot/kernel-701 ro )

Dziękuję Rubenowi Laguanie za ten dodatek.

Uruchom ponownie komputer, a powinieneś zobaczyć swoje jądro na liście! A kiedy go wybierzesz, zobaczysz tę samą linię.



To jest twój rdzeń!

Pisanie jądra z obsługą klawiatury i ekranu

Zakończyliśmy prace nad minimalnym jądrem, które uruchamia się przez GRUB, działa w trybie chronionym i wyświetla pojedynczą linię na ekranie. Czas go rozwinąć i dodać sterownik klawiatury, który będzie odczytywał znaki z klawiatury i wyświetlał je na ekranie.

Z urządzeniami I/O będziemy komunikować się poprzez porty I/O. Zasadniczo są to po prostu adresy na magistrali we/wy. Istnieją specjalne instrukcje procesora do operacji odczytu i zapisu.

Praca z portami: odczyt i wyjście

read_port: mov edx, in al, dx ret write_port: mov edx, mov al, out dx, al ret

Dostęp do portów we/wy można uzyskać za pomocą instrukcji wejścia i wyjścia zawartych w zestawie x86.

W read_port numer portu jest przekazywany jako argument. Kiedy kompilator wywołuje funkcję, wypycha wszystkie argumenty na stos. Argument jest kopiowany do rejestru edx za pomocą wskaźnika stosu. Rejestr dx to dolne 16 bitów rejestru edx. Instrukcja in odczytuje numer portu podany w dx i umieszcza wynik w al . Rejestr al to 8 niższych bitów rejestru eax. Być może pamiętasz ze studiów, że wartości zwracane przez funkcje przekazywane są przez rejestr eax. Zatem read_port pozwala nam czytać z portów I/O.

Funkcja write_port działa w podobny sposób. Przyjmujemy dwa argumenty: numer portu i dane, które zostaną zapisane. Instrukcja out zapisuje dane do portu.

Przerywa

Teraz, zanim wrócimy do pisania sterownika, musimy zrozumieć, skąd procesor wie, że jedno z urządzeń wykonało operację.

Najprostszym rozwiązaniem jest odpytywanie urządzeń - na bieżąco sprawdzaj ich stan w kółku. Jest to z oczywistych względów nieskuteczne i niepraktyczne. Dlatego właśnie w tym miejscu wchodzą w grę przerwania. Przerwanie to sygnał wysyłany do procesora przez urządzenie lub program, który wskazuje, że nastąpiło zdarzenie. Stosując przerwania możemy uniknąć konieczności odpytywania urządzeń i będziemy reagować tylko na zdarzenia, które nas interesują.

Za przerwania w architekturze x86 odpowiada układ zwany programowalnym kontrolerem przerwań (PIC). Obsługuje przerwania sprzętowe i trasy i zamienia je w odpowiednie przerwania systemowe.

Kiedy użytkownik robi coś z urządzeniem, do układu PIC wysyłany jest impuls zwany żądaniem przerwania (IRQ). PIC tłumaczy odebrane przerwanie na przerwanie systemowe i wysyła do procesora komunikat, że nadszedł czas, aby przerwać to, co robi. Dalsza obsługa przerwań jest zadaniem jądra.

Bez funkcji PIC musielibyśmy odpytywać wszystkie urządzenia obecne w systemie, aby sprawdzić, czy wystąpiło zdarzenie z udziałem któregokolwiek z nich.

Przyjrzyjmy się, jak to działa z klawiaturą. Klawiatura zawiesza się na portach 0x60 i 0x64. Port 0x60 wysyła dane (po naciśnięciu przycisku), a port 0x64 wysyła status. Musimy jednak wiedzieć, kiedy dokładnie czytać te porty.

Tutaj przydają się przerwy. Po naciśnięciu przycisku klawiatura wysyła sygnał PIC poprzez linię przerwania IRQ1. PIC przechowuje wartość przesunięcia zapisaną podczas inicjalizacji. Dodaje numer linii wejściowej do tego dopełnienia, tworząc wektor przerwania. Następnie procesor wyszukuje strukturę danych zwaną tablicą deskryptorów przerwań (IDT), aby nadać procedurze obsługi przerwań adres odpowiadający jej numerowi.

Następnie wykonywany jest kod znajdujący się pod tym adresem i obsługuje przerwanie.

Ustaw IDT

struct IDT_entry( unsigned short int offset_lowerbits; unsigned short int selektor; unsigned char zero; unsigned char type_attr; unsigned short int offset_higherbits; ); struktura IDT_entry IDT; void idt_init(void) ( unsigned long adres_klawiatury; unsigned długi adres_idt; unsigned long idt_ptr; adres_klawiatury = (długi bez znaku) obsługa_klawiatury; IDT.offset_lowerbits = adres_klawiatury & 0xffff; IDT.selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */ IDT.zero = 0 ; IDT.type_attr = 0x8e; /* INTERRUPT_GATE */ IDT.offset_higherbits = (adres_klawiatury & 0xffff0000) >> 16; write_port(0x20, 0x11); write_port(0xA0, 0x11); write_port(0x21, 0x20); write_port(0xA1, 0x28); write_port(0x21, 0x00); write_port(0xA1, 0x00); write_port(0x21, 0x01); write_port(0xA1, 0x01); write_port(0x21, 0xff); write_port(0xA1, 0xff); idt_address = (długi bez znaku )IDT ; idt_ptr = (rozmiar (struktury wpisu IDT) * IDT_SIZE) + ((adres_idt i 0xffff)<< 16); idt_ptr = idt_address >> 16; obciążenie_idt(idt_ptr); )

IDT jest tablicą struktur IDT_entry. Później omówimy powiązanie przerwania klawiaturowego z procedurą obsługi, ale teraz przyjrzyjmy się, jak działa PIC.

Nowoczesne systemy x86 mają dwa chipy PIC, każdy z ośmioma liniami wejściowymi. Nazwiemy je PIC1 i PIC2. PIC1 odbiera IRQ0 do IRQ7, a PIC2 odbiera IRQ8 do IRQ15. PIC1 używa portu 0x20 dla poleceń i 0x21 dla danych, a PIC2 używa portu 0xA0 dla poleceń i 0xA1 dla danych.

Obydwa układy PIC są inicjowane ośmiobitowymi słowami zwanymi słowami poleceń inicjujących (ICW).

W trybie chronionym oba PIC muszą najpierw wydać polecenie inicjalizacji ICW1 (0x11). Mówi PICowi, aby poczekał, aż trzy kolejne słowa inicjujące dotrą do portu danych.

Te polecenia przekażą PIC:

  • wektor wcięcia (ICW2),
  • jakie są relacje master/slave pomiędzy PIC (ICW3),
  • dodatkowe informacje o środowisku (ICW4).

Drugie polecenie inicjalizacji (ICW2) jest również wysyłane na wejście każdego PIC. Przypisuje offset, czyli wartość, do której dodajemy numer linii, aby otrzymać numer przerwania.

Układy PIC pozwalają na kaskadowe łączenie pinów ze sobą. Odbywa się to za pomocą ICW3 i każdy bit reprezentuje status kaskady dla odpowiedniego przerwania. Teraz nie będziemy używać przekierowania kaskadowego i ustawimy je na zero.

ICW4 określa dodatkowe parametry środowiskowe. Musimy jedynie zdefiniować dolny bit, aby obrazy PIC wiedziały, że pracujemy w trybie 80x86.

Ta-dam! Pliki PIC są teraz inicjowane.

Każdy układ PIC posiada wewnętrzny ośmiobitowy rejestr zwany rejestrem maski przerwań (IMR). Przechowuje mapę bitową linii IRQ, które trafiają do PIC. Jeśli bit jest ustawiony, PIC ignoruje żądanie. Oznacza to, że możemy włączyć lub wyłączyć konkretną linię IRQ, ustawiając odpowiednią wartość na 0 lub 1.

Odczyt z portu danych zwraca wartość w rejestrze IMR, natomiast zapis zmienia rejestr. W naszym kodzie po inicjalizacji PIC ustawiamy wszystkie bity na jeden, co dezaktywuje wszystkie linie IRQ. Później aktywujemy linie odpowiadające przerwaniom klawiaturowym. Ale najpierw to wyłączmy!

Jeśli linie IRQ działają, nasze PIC mogą odbierać sygnały na IRQ i konwertować je na numer przerwania, dodając przesunięcie. Musimy wypełnić IDT w taki sposób, aby numer przerwania wychodzący z klawiatury odpowiadał adresowi funkcji obsługi, którą napiszemy.

Z jakim numerem przerwania musimy powiązać obsługę klawiatury w IDT?

Klawiatura wykorzystuje IRQ1. Jest to linia wejściowa 1 i jest przetwarzana przez PIC1. Zainicjowaliśmy PIC1 z offsetem 0x20 (patrz ICW2). Aby uzyskać numer przerwania, musisz dodać 1 i 0x20, otrzymasz 0x21. Oznacza to, że adres modułu obsługi klawiatury zostanie powiązany w IDT z przerwaniem 0x21.

Zadanie sprowadza się do wypełnienia IDT dla przerwania 0x21. Zamapujemy to przerwanie na funkcję obsługi klawiatury, którą zapiszemy w pliku asemblera.

Każdy wpis w IDT składa się z 64 bitów. We wpisie odpowiadającym przerwaniu nie przechowujemy całego adresu funkcji obsługi. Zamiast tego podzieliliśmy go na dwie 16-bitowe części. Bity młodszego rzędu są przechowywane w pierwszych 16 bitach wpisu IDT, a 16 bitów wyższego rzędu są przechowywane w ostatnich 16 bitach wpisu. Wszystko to odbywa się w celu zapewnienia kompatybilności z procesorami 286. Jak widać, Intel produkuje takie liczby regularnie i w wielu, wielu miejscach!

We wpisie IDT wystarczy zarejestrować typ, wskazując w ten sposób, że wszystko to jest robione, aby przechwycić przerwanie. Musimy także ustawić przesunięcie segmentu kodu jądra. GRUB ustawia dla nas GDT. Każdy wpis GDT ma długość 8 bajtów, gdzie deskryptor kodu jądra jest drugim segmentem, więc jego przesunięcie będzie wynosić 0x08 (szczegóły wykraczają poza zakres tego artykułu). Bramka przerwań jest reprezentowana jako 0x8e. Pozostałe 8 bitów w środku jest wypełnionych zerami. W ten sposób wypełnimy wpis IDT odpowiadający przerwaniu klawiaturowemu.

Kiedy już zakończymy mapowanie IDT, musimy poinformować procesor, gdzie znajduje się IDT. Służy do tego instrukcja asemblera o nazwie lidt, która wymaga jednego operandu. Jest to wskaźnik do deskryptora struktury opisującej IDT.

Z deskryptorem nie ma żadnych trudności. Zawiera rozmiar IDT w bajtach i jego adres. Użyłem tablicy, aby uczynić ją bardziej zwartą. W ten sam sposób możesz wypełnić deskryptor za pomocą struktury.

W zmiennej idr_ptr mamy wskaźnik, który przekazujemy do instrukcji lidt w funkcji Load_idt().

Load_idt: mov edx, lidt sti ret

Dodatkowo funkcja loading_idt() zwraca przerwanie podczas korzystania z instrukcji sti.

Po wypełnieniu i załadowaniu IDT możemy uzyskać dostęp do przerwania IRQ klawiatury za pomocą maski przerwań, o której mówiliśmy wcześniej.

Void kb_init(void) ( write_port(0x21 , 0xFD); )

0xFD to 11111101 - włącz tylko IRQ1 (klawiatura).

Funkcja - obsługa przerwań klawiaturowych

Zatem pomyślnie powiązaliśmy przerwania klawiaturowe z funkcją obsługi klawiatury, tworząc wpis IDT dla przerwania 0x21. Funkcja ta będzie wywoływana po każdym naciśnięciu przycisku.

Obsługa_klawiatury: wywołaj obsługę_klawiatury_main iretd

Ta funkcja wywołuje inną funkcję napisaną w C i zwraca sterowanie za pomocą instrukcji klasy iret. Moglibyśmy napisać tutaj cały nasz moduł obsługi, ale znacznie łatwiej jest kodować w C, więc przejdźmy dalej. Instrukcji iret/iretd należy używać zamiast ret, gdy sterowanie powraca z funkcji obsługi przerwań do przerwanego programu. Ta klasa instrukcji wywołuje rejestr flagowy, który jest umieszczany na stosie, gdy wywoływane jest przerwanie.

Void klawiatura_handler_main(void) ( unsigned char status; char keycode; /* Zapis EOI */ write_port(0x20, 0x20); status = read_port(KEYBOARD_STATUS_PORT); /* Dolny bit stanu zostanie ustawiony, jeśli bufor nie jest pusty */ if (status i 0x01) ( kod klucza = port_odczytu (KEYBOARD_DATA_PORT); if (kod klucza< 0) return; vidptr = keyboard_map; vidptr = 0x07; } }

Tutaj najpierw podajemy sygnał EOI (koniec przerwania), zapisując go do portu poleceń PIC. Tylko wtedy PIC zezwoli na dalsze żądania przerwań. Musimy odczytać dwa porty: port danych 0x60 i port poleceń (inaczej port stanu) 0x64.

Przede wszystkim czytamy port 0x64, aby uzyskać status. Jeżeli dolny bit statusu ma wartość zero, wówczas bufor jest pusty i nie ma danych do odczytania. W pozostałych przypadkach możemy odczytać port danych 0x60. Podaje nam kod wciśniętego klawisza. Każdy kod odpowiada jednemu przyciskowi. Używamy prostej tablicy znaków zdefiniowanej w klawiaturze_map.h do mapowania kodów na odpowiednie znaki. Symbol jest następnie wyświetlany na ekranie przy użyciu tej samej techniki, którą zastosowaliśmy w pierwszej wersji jądra.

Aby kod był prosty, przetwarzam tylko małe litery od a do z i cyfry od 0 do 9. Możesz łatwo dodawać znaki specjalne, Alt, Shift i Caps Lock. Możesz dowiedzieć się, że klawisz został naciśnięty lub zwolniony z wyjścia portu poleceń i wykonać odpowiednią akcję. W ten sam sposób możesz powiązać dowolne skróty klawiaturowe z funkcjami specjalnymi, takimi jak zamykanie.

Teraz możesz zbudować jądro i uruchomić je na prawdziwej maszynie lub na emulatorze (QEMU) w taki sam sposób, jak w pierwszej części.