Eigene Betriebssystementwicklung am PC  -  Teil 1


Stand 12.08.2011 - Dr. Erhard Henkes        
(begonnen wurde dieses Tutorial im März 2009)

Teil 2    Teil 3     Teil 4

Dieses Tutorial begleitet die Entwicklung eines eigenen Hobby-Betriebssystems, das Interessierten, die den Umgang mit Assembler und C beherrschen, den Einstieg in dieses - für manche spannende, für andere dagegen völlig unzugängliche - Gebiet erleichtern soll. Dieses Skript wird in nächster Zeit mehrfach geändert/erweitert werden, da es parallel zu der Entwicklung geschrieben wird. Aber nur auf diese Weise bleibt es authentisch.

Inhaltsübersicht

Einführung

Ergänzung (Stand 17. Juni 2010)

Dieses "Tutorial" ist entstanden als Log-Buch während meines Einstieges in die faszinierende Welt des OS Development. Inzwischen hat sich das beispielhaft entwickelte OS (unter MS Windows in C entwickelt, mit eigenem Bootloader) hier einen festen Platz erobert.
Ich möchte darauf hinweisen, dass einige Sachen, die ich zu Beginn eigesetzt habe, inzwischen obsolet sind (z:B. der DJGPP). Dieses Tutorial vermittelt auch nicht alle Fertigkeiten, die man braucht, um ein OS für den produktiven Gebrauch zu bauen, sondern liefert lediglich einen Einblick in den Einstieg in diese Materie mit der Idee, diesen "Berg von der MS Windows Talseite" (anstelle Linux) aus zu besteigen und hierbei ganz unten, also beim Booten zu beginnen. Dies ist ein Weg, der einem einen schrittweisen Einblick und Zugang gewährt, aber sicher nicht die professionellste Weise. Daher weise ich darauf hin, dass man alternativ zum eigenen Bootloader auch GRUB oder andere vorhandene Bootloader verwenden kann und dass man viele Dinge mit Linux etwas einfacher angehen kann, als dies MS Windows erlaubt. Ich werde an den Stellen im Skript, an denen ich heute eine andere Wahl treffen würde, in den nächsten Monaten noch entsprechende Hinweise setzen. Dennoch möchte ich den Charakter dieses Werkes, dass ich Mitte März 2009 parallel zu meiner persönlichen OSDev-Besteigung begonnen habe, nicht verändern. Wer richtig Lust an diesem Thema bekommt, den möchte ich einladen, mit uns gemeinsam an "PrettyOS" zu "basteln". In Teil 4 werde ich in dieses "Community-Projekt" einführen.

--------------------------------------------------------------------

ab hier:  Stand August 2009 (mit später hinzu gefügten Ergänzungen/Bemerkungen)

"The fundamental objective of an operating system is to present the computer to the user and to the programmer at a certain level of abstraction."
(Wirth, Gutknecht, "Project Oberon")

"A computer system can be divided roughly into four components: the hardware, the operating system, the application programs, and the users." (Silberschatz et.al., "Operating System Concepts", 7. Aufl., 2005)


"LINUX is obsolete" (Tanenbaum, Jan 1992, Kritik bezüglich des monolithischen Kernels und der Bindung an den Prozessor x86. Die liberale globale Entwicklung von Linux hielt er ebenso
für nicht umsetzbar.)

"Assembly language has its place in the world. But there is nothing like a high level language to speed development and make maintenance easier."
(Richard A. Burgess, "MMURTL V1.0", 1995)

"Low-level programming is good for the programmer's soul." (John Carmack)

"It is a powerful feeling for the only code to be running on a machine to be your own." (wiki.osdev.org).

"wenn man assembler-code auf tiefster low-level ebene schreibt, muss man noch lange keine prähistorischen tools verwenden."
(+fricky im Assembler-Forum c-plusplus.de, 2009)


Sehr herzlich bedanken möchte ich mich für die Unterstützung des Forums c-plusplus.de (Unterforum Assembler) bei der Erstellung und kritischen Durchsicht dieses Artikels. Namentlich erwähnen möchte ich hier die Forumsteilnehmer

Nobuo Tsukamoto, "abc.w"Jos Kuijpers (Jarvix), "Bitsy", Christoph, "+gjm+", "Mr X" (noch heute - Juni 2011 - als hervorragender Entwickler mit dabei)  et. al.

die mich beim Einstieg in dieses für mich neue Themengebiet hilfreich unterstützen
.

Ebenfalls große Unterstützung fand ich beim Forum lowlevel.brainsware.org/forum, dessen Administratoren PorkChicken und "taljeth" ich besonders erwähnen möchte. Sie haben mich auf dem Weg zum Multitasking und beim Boot-Algorithmus für größere Kernel unterstützt. Ein hervorragendes deutsch-sprachiges Forum zu diesem Thema. Hier kann man von Assembler- über C- bis OSDEV-Fragen alles diskutieren. Es gibt dort auch eine Community, die "týndur" (früher "lost", daher auch der Name #lost für den channel im chat) entwickelt.

Dann gibt es da noch viele andere, z.B. Christian Coors (Cuervo), der mich auf einen Fehler (RAM Disk Speicherbelegung) aufmerksam machte und mich auch sonst bei der Fehlersuche und umfangreichen Tests kräftig unterstützt. Er ist noch heute beim Team um PrettyOS dabei.

Anmerkung (06.06.2011): Inzwischen ist "PrettyOS" ein stabiles Hobby-OS geworden. Die Developer-Community findet man hier: http://www.c-plusplus.de/forum/262054

Dafür vielen Dank an alle! Mit positivem Feedback und konkreten Hilfestellungen kann man die Entwicklung eines eigenen Betriebssystems als Hobby-Projekt deutlich einfacher schaffen.

Wir werden in diesem Tutorial sowohl Assembler (NASM mit Intel Syntax und gcc inline Assembler mit AT&T Syntax) als auch C verwenden (wir beginnen aus historischen Gründen mit DJGPP (Portierung von gcc nach MS Windows), später setzen wir einen modernen Crosscompiler ein).
Der Emulator Bochs gilt weithin als Standardsystem und wird daher bevorzugt zum Testen eingesetzt. Bochs hat einen integrierten Debugger (Anmerkung: Qemu, VBox, VMWare-Player sind ebenfalls gut brauchbar in diesem Stadium).


Wichtiger Hinweis:
Alle in diesem Tutorial angebotenen Inhalte und Sourcecodes erfolgen unter Ausschluss jeglicher Haftung des Verfassers für Schäden, die aus der Anwendung herrühren. Zur sicheren Durchführung der Tests empfehlen wir die Verwendung eines x86-Emulationsprogramms. Bitte gehen Sie besonders achtsam mit binären Schreibprogrammen, z.B. partcopy oder dd, und beim Experimentieren mit den Geräten im Computer um. Assembler ist ein mächtiges Tool und kann die Hardware des Rechners nachhaltig beschädigen!


Warum sollte man ein Operating System (OS) selbst erstellen?

Heute wird noch an vielen Hochschulen Assembler und die Grundlagen von Hardware und Betriebssystem gelehrt. Es geht darum, ein Gefühl für den Rechner und seine inneren Abläufe zu bekommen. Manchen Entwickler reizt es sicher ab und zu, ein eigenes Operating System (OS) zu entwickeln. Es ist eine große Herausforderung und schafft breites Detailwissen über die Funktionen der einbezogenen Hardware. Als Gegenleistung bekommt man in der Regel nur den Genuss der intellektuellen Herausforderung, denn gegen MS Windows, Linux oder andere etablierte OS anzutreten, hat sicher keine Einzelperson vor. Wozu sollte das auch gut sein? Aber zum vertieften Verständnis, wie ein PC oder ein anderes elektronisches System mit einem Prozessor "in der Mitte" eigentlich funktioniert, mag es hilfreich sein, den gesamten internen Start- und  Kommunikations-Prozess selbst zu erstellen, wirklich praktisch zu verfolgen und damit experimentieren zu können.

Allerdings muss man klar stellen, dass dies für einen Einsteiger in die Programmierung mit hoher Wahrscheinlichkeit nicht machbar ist, weil man viele Punkte gleichzeitg beachten und darüber hinaus auch klar strukturiert und performant (wegen der Vorbildwirkung auf andere OS-Experimente) entwickeln sollte. Als Basis empfiehlt sich die Programmiersprache C und für einige Aufgaben Assembler (z.B. Boot-Prozess, Umschaltung von Real Mode zu Protected Mode, Interrupts).

Die Zusammenhänge und Abläufe sind vielfach beschrieben. In der Links-Sektion werden weitgehend verständliche Übersichten und praktische Anleitungen genannt. Komplizierte Abhandlungen theoretischer Art über Betriebssysteme helfen für den ersten praktischen Einstieg wenig, später umso mehr. Hier geht Probieren und praktisches Abschauen eindeutig über reines Studieren. Neben dem PC gibt es auch noch eine Menge Systeme mit Mikroprozessoren. Dies wird in diesem Tutorial nicht behandelt.

Ablauf beim Starten eines PC

Was erfolgt, wenn man einen PC mit x86 CPU startet?

Buchempfehlungen

Andrew S. Tanenbaum, Moderne Betriebssysteme, 3. Auflage, Pearson Studium, April 2009
(Übersetzung von "Modern Operating Systems", Prentice Hall, Dec 2007)
Dieses Buch liefert die theoretischen Hintergründe für das Thema Betriebssystem. Für die Praxis ist es allerdings weniger hilfreich. Hier findet man im Internet weitaus detailliertere Praxis-Informationen. Das "Netz" hat Bücher hier bei weitem geschlagen.

Links (Internet)

Tutorials


Betriebssystem 

wiki.osdev.org - Mini Kernel in "Real mode" ohne Bootlader

Lowlevel Community (gute und umfangreiche praxisorientierte Dokumentation)


Operating Systems Development (Tutorial verfügt inzwischen schon über 20 Teile, einfach Zahl im Link austauschen)

Real Mode, Protected Mode (FH Zwickau)


Kernel development

Maschinensprache i8086/ Urlader

Wikibook/Assembler (80x86)

Tutorial "Ein eigenes kleines Betriebssystem"


Diskussions-Foren

c-plusplus-Forum / Assembler 
http://lowlevel.brainsware.org/forum/
OSDEV.org  Forum 


Real Mode

Praxistests (Mini-Kernel und Bootloader)

Vorbild eines Mini Kernels zunächst ohne Bootlader

Wir wollen ganz schnell los legen. Daher testen wir sofort Folgendes unter MS Windows:

Hardware: PC mit Floppy-Disk
Link: wiki.osdev.org - Mini Kernel in "Real mode" ohne Bootlader

Notwendige Tools:
a) Editor für die asm-Datei, z.B. Notepad++ (Zeilennummern., Syntax Highlighting)
b) Assembler zur Erzeugung binärer Dateien, empfehlenswert ist NASM, weil breit eingesetzt (TASM-Kompatibilitäts-Modus)
c) Emulator, empfehlenswert ist für den Einstieg Bochs (einer der Standard-PC-Emulatoren bei der OS-Entwicklung)

Bei Vorliegen eines Floppy-Disk-Laufwerkes (nicht notwendig, weil es durch Bochs emuliert werden kann) empfehlenswert:
d) Schreibprogramm für binäre Dateien, z.B. partcopy
e) Hilfreich ist ein Hex-Editor, weil man damit ein unverfälschtes binäres Abbild einer Datei erhält.

Der Kernel ist in diesem sehr einfachen Beispiel komplett in Assembler geschrieben und wird ohne Bootlader direkt im ersten Sektor der Floppy Disk ausgeführt,
Sektor 1, Kopf 0, Zylinder 0 von Floppy-Laufwerk A.

Das "Betriebssystem" ist textbasiert, hat einen Prompt (>), "versteht" die Eingaben "hi" und "help" und reagiert auch auf falsche Eingaben.
Wir befinden uns nach dem Booten einer CPU 80x86 im sogenannten Real Mode.

Praktische Durchführung

1) Eingabe des Assemblercodes 
(Quelle für den Code: siehe oben)

  mov ax, 0x07C0  ; set up segments
mov ds, ax
mov es, ax

mov si, welcome
call print_string

loop:
mov si, prompt
call print_string

mov di, buffer
call get_string

mov si, buffer
cmp byte [si], 0  ; blank line?
je loop  ; yes, ignore it

mov si, buffer
mov di, cmd_hi  ; "hi" command
call strcmp
jc .helloworld

mov si, buffer
mov di, cmd_help  ; "help" command
call strcmp
jc .help

mov si,badcommand
call print_string
jmp loop

.helloworld:
mov si, msg_helloworld
call print_string

jmp loop

.help:
mov si, msg_help
call print_string

jmp loop

welcome db 'Welcome to My OS!', 0x0D, 0x0A, 0
msg_helloworld db 'Hello OSDev World!', 0x0D, 0x0A, 0
badcommand db 'Bad command entered.', 0x0D, 0x0A, 0
prompt db '>', 0
cmd_hi db 'hi', 0
cmd_help db 'help', 0
msg_help db 'My OS: Commands: hi, help', 0x0D, 0x0A, 0
buffer times 64 db 0

; ================
; calls start here
; ================

print_string:
lodsb  ; grab a byte from SI

or al, al  ; logical or AL by itself
jz .done  ; if the result is zero, get out

mov ah, 0x0E
int 0x10  ; otherwise, print out the character!

jmp print_string

.done:
ret

get_string:
xor cl, cl

.loop:
mov ah, 0
int 0x16  ; wait for keypress

cmp al, 0x08  ; backspace pressed?
je .backspace  ; yes, handle it

cmp al, 0x0D  ; enter pressed?
je .done  ; yes, we're done

cmp cl, 0x3F  ; 63 chars inputted?
je .loop  ; yes, only let in backspace and enter

mov ah, 0x0E
int 0x10  ; print out character

stosb  ; put character in buffer
inc cl
jmp .loop

.backspace:
cmp cl, 0 ; beginning of string?
je .loop ; yes, ignore the key

dec di
mov byte [di], 0 ; delete character
dec cl ; decrement counter as well

mov ah, 0x0E
mov al, 0x08
int 10h ; backspace on the screen

mov al, ' '
int 10h ; blank character out

mov al, 0x08
int 10h ; backspace again

jmp .loop ; go to the main loop

.done:
mov al, 0 ; null terminator
stosb

mov ah, 0x0E
mov al, 0x0D
int 0x10
mov al, 0x0A
int 0x10 ; newline

ret

strcmp:
.loop:
mov al, [si]  ; grab a byte from SI
mov bl, [di]  ; grab a byte from DI
cmp al, bl  ; are they equal?
jne .notequal  ; nope, we're done.

cmp al, 0  ; are both bytes (they were equal before) null?
je .done  ; yes, we're done.

inc di  ; increment DI
inc si  ; increment SI
jmp .loop  ; loop!

.notequal:
clc  ; not equal, clear the carry flag
ret

.done:
stc  ; equal, set the carry flag
ret

times 510-($-$$) db 0
db 0x55
 db 0xAA

2) Assemblieren mittels nasm.exe:
nasm kernel.asm -f bin -o kernel.bin 

Mit der Option -f wird festgelegt, dass die übersetze Datei ein Programm aus nur einem Segment (maximal 64 KB) ist, in dem sich Code, Daten und
Stack befinden. Parameter -o legt fest, dass man den Namen der assemblierten Datei selbst bestimmt.

3) Diskette formatieren (sonst gibt es eine Windows-Fehlermeldung), falls Sie noch ein  Diskettenlaufwerk am PC besitzen.
    Rohdaten mittels partcopy.exe in Sektor 1 schreiben (Vorsicht!  -f0  ist der Parameter für die Floppy disk 0)
   Alternativ können Sie auch die Binärdatei mittels Bochs emulieren (siehe Punkt 4).
partcopy kernel.bin 0 200 -f0
4) In Bochs das File "bochsrc-sample.txt" suchen.
    Durch Anpassung ein config-File mit anderem Namen anlegen mit z.B. folgenden Zeilen:

romimage: file=$BXSHARE/BIOS-bochs-latest
cpu: count=1, ips=10000000, reset_on_triple_fault=1
megs: 32
vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest
vga: extension=vbe
floppya: 1_44=a:, status=inserted
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata1: enabled=1, ioaddr1=0x170, ioaddr2=0x370, irq=15
ata2: enabled=0, ioaddr1=0x1e8, ioaddr2=0x3e0, irq=11
ata3: enabled=0, ioaddr1=0x168, ioaddr2=0x360, irq=9
boot: floppy
floppy_bootsig_check: disabled=0
log: bochsout.txt
panic: action=ask
error: action=report
info: action=report
debug: action=ignore
debugger_log: -
parport1: enabled=1, file="parport.out"
vga_update_interval: 300000
keyboard_serial_delay: 250
keyboard_paste_delay: 100000
mouse: enabled=0
private_colormap: enabled=0
keyboard_mapping: enabled=0, map=
i440fxsupport: enabled=0


Wichtiger Hinweis:
Falls Sie kein Diskettenlaufwerk am PC haben oder kein Sicherheitsrisiko mit partcopy o.ä. eingehen wollen, können Sie unter bochs auch direkt die binäre Datei starten. Anstelle von:
floppya: 1_44=a:, status=inserted
verwenden Sie nicht den Laufwerksbuchstaben a:,
sondern im Austausch den Pfad zur Binärdatei, bei mir z.B.:
floppya: 1_44=G:\OSDev\Test\kernel.bin, status=inserted

5)  Bochs starten, eigenes config-File laden und Simulation starten:



Das Mini-OS auf Textbasis funktioniert, wie erwartet. Sie können den PC bereits direkt von der Diskette starten, also nicht nur emuliert.

Wir sind neugierig und schauen uns die binäre Datei kernel.bin mit einem Hex-Editor an. Man findet z.B. folgende Darstellung:



Man sieht im Bereich der "Strings" entweder nur das terminierende 0x00 (NUL) oder die Folge 0x0D, 0x0A, 0x00.
0x0D steht für Carriage Return (CR) und 0x0A für Line Feed (LF).
Damit man die ASCII-Zeichen korrekt versteht, hat man am besten immer eine ASCII-Tabelle zur Hand.

Die Boot-Signatur 0x55 und 0xAA befindet sich in den letzten beiden Bytes.

Bereits an diesem einfachen Beispiel erkennt man, dass für die Betriebssystementwicklung die Beherrschung von Assembler notwendig ist.

Zur Verdeutlichung des Wesentlichen zeige ich hier einen extrem minimalistischen Bootloader, der außer dem eigentlichen Booten nichts bewirkt, sondern sofort die CPU anhält: Bootloader signalisert seine Aufgabe dem BIOS durch die letzten beiden Byte und startet bei 0x7c00.


org 0x7c00
cli
times 510-($-$$) hlt
db 0x55
db 0xAA 


In NASM steht der Dollar Operator ($) für die Adresse der aktuellen Programmzeile. Der zweifache Dollar Operator $$ repräsentiert die Adresse der ersten Anweisung im Programm (im Falle des Bootloaders: 0x7C00). Die Differenz aus aktueller Position und Start $-$$ liefert die entsprechende Anzahl an Bytes, die der Größe des Programms bis zu diesem Punkt entspricht. Die Sektorgröße 512 minus der zwei Byte umfassenden Bootsignatur ergibt 510, und 510-($-$$) liefert die noch zu füllende Lücke, um die Bootsignatur genau auf die beiden letzten Bytes im Sektor auszurichten.

Eigene Experimente

Nun werden wir ein wenig an obigem Mini-OS weiter entwickeln, um ein Gefühl für die Möglichkeiten zu bekommen.
Wir ergänzen die Befehle '?' und 'exit'.
Wir verkleinern den "buffer" (Puffer) auf 32 Bytes.
Wir optimieren an einigen Stellen den Code (Herzlichen Dank für wichtige Hinweise an "Nobuo T").

  mov ax, 0x07C0  ; set up segments
  mov ds, ax
  mov es, ax

  mov si, welcome
  call print_string

loop:
  mov si, prompt
  call print_string

  mov di, buffer
  call get_string

  mov si, buffer
  cmp byte [si], 0  ; blank line?
  je loop           ; yes, ignore it

  mov di, cmd_hi    ; "hi" command
  call strcmp
  jz .helloworld

  mov si, buffer
  mov di, cmd_help  ; "help" command
  call strcmp
  jz .help

  mov si, buffer
  mov di, cmd_questionmark  ; "?" command
  call strcmp
  jz .help
 
  mov si, buffer
  mov di, cmd_exit  ; "exit" command
  call strcmp
  jz .exit

  mov si,badcommand
  call print_string
  jmp loop 

.helloworld:
  mov si, msg_helloworld
  call print_string

  jmp loop

.help:
  mov si, msg_help
  call print_string

  jmp loop

.exit:
  mov si, msg_exit
  call print_string
  jmp 0xffff:0x0000  ; Reboot

welcome db 'HenkesSoft 0.01 (version from Mar 14, 2009)', 13, 10, 0
msg_helloworld db 'Hello World!', 13, 10, 0
badcommand db 'Command unknown.', 13, 10, 0
prompt db '>', 0
cmd_hi db 'hi', 0
cmd_help db 'help', 0
cmd_questionmark db '?', 0
cmd_exit db 'exit', 0
msg_help db 'Commands: hi, help, ?, exit', 13, 10, 0
msg_exit db 'Reboot starts now.', 13, 10, 0

buffer times 32 db 0

; ================
; calls start here
; ================

print_string:
  lodsb        ; grab a byte from SI

  or al, al    ; logical or AL by itself
  jz .done     ; if the result is zero, get out

  mov ah, 0x0E
  int 0x10       ; otherwise, print out the character!

  jmp print_string

.done:
  ret

get_string:
  xor cl, cl

.loop:
  xor ah, ah    ; mov ah, 0
  int 0x16      ; wait for keypress

  cmp al, 8     ; backspace pressed?
  je .backspace ; yes, handle it

  cmp al, 13    ; enter pressed?
  je .done      ; yes, we're done

  cmp cl, 31    ; 31 chars inputted?
  je .loop      ; yes, only let in backspace and enter

  mov ah, 0x0E
  int 0x10      ; print out character

  stosb  ; put character in buffer
  inc cl
  jmp .loop

.backspace:
  or cl, cl     ; zero? (start of the string)
  jz .loop      ; if yes, ignore the key

  dec di
  mov byte [di], 0  ; delete character
  dec cl        ; decrement counter as well

  mov ax, 0x0E08
  int 0x10      ; backspace on the screen

  mov al, ' '
  int
0x10      ; blank character out

  mov al, 8
  int
0x10      ; backspace again

  jmp .loop     ; go to the main loop

.done:
  mov al, 0     ; null terminator
  stosb

  mov ax, 0x0E0D
  int
0x10
  mov al, 0x0A
  int
0x10      ; newline

  ret

strcmp:
.loop:
  mov al, [si]   ; fetch a byte from SI
  cmp al, [di]   ; are SI and DI equal?
  jne .done      ; if no, we're done.

  or al, al      ; zero?
  jz .done       ; if yes, we're done.

  inc di         ; increment DI
  inc si         ; increment SI
  jmp .loop      ; goto .loop

.done:    
  ret

  times 510-($-$$) hlt  ; as alternative to db 0
  db 0x55        ; boot signature check (byte 511 in sector 1)
  db 0xAA       
; boot signature check (byte 512 in sector 1)


Schauen Sie sich die Datei kernel.bin mit einem Hex-Editor an und beachten Sie die veänderten Strings und den verkleinerten "buffer"-Bereich.
Hier eine Übersicht mit einem anderen Hex-Editor:



Der "buffer" verfügt hier nur über 32 Bytes. Wir können daher keine langen Befehle eingeben. Ein Stack wurde noch nicht angelegt.

Interessant ist für den Einsteiger, dass auf einer deutschen Tastatur das Fragezeichen an einer anderen Stelle liegt.
Unser "OS" verwendet die "US International" Tastaturbelegung. Hier findet man die entsprechende Belegung, damit man das Fragezeichen findet.

Background-Wissen: Register, Interrupts

Wer nun nicht sattelfest ist in Assembler-Programmierung im Umfeld der 80x86 Prozessoren, wird nun sicher fragen:
Was sind diese Register? Wie funktionieren Interrupts?

Zunächst der allgemeine Überblick über die Register (hier am Beispiel der 16-Bit-Version) der 80x86 CPU:


Da gibt es zunächst die sogenannten Mehrzweck-Register.
Diese 16-Bit-Register können selektiv als high oder low Byte angesprochen werden.
AX = AH + AL           Akkumulator 
BX = BH + BL Basisregister
CX = CH + CL Counter
DX = DH + DL Datenregister für Eingabe/Ausgabeoperationen
Weitere 16-Bit-Register:
BP  Base Pointer         (Basisregister)
SP Stack Pointer (Zeiger auf Stack)
SI Source Index (Indexregister für Quell-Operanden)
DI Destination Index (Indexregister für Ziel-Operanden)
Steuer- und Status-Register:
IP  Instruction Pointer (Offset-Adresse für den nächsten Befehl)
F Flag Register (enthält 16 einzelne Bits, die Informationen geben wie z.B.: NZ (Non Zero), ZR (Zero), NC (Non Carry), CY (Carry).

Segment-/Adressregister:
ES  Extra Segment (arbeitet mit DI)
SS Stack Segment (arbeitet mit SP)
DS Daten Segment (Segmentregister für weitere Daten)
CS Code Segment (Segmentregister für Instruktionen)
Heutige Prozessoren beruhen immer noch auf den gleichen Prinzipien, allerdings gibt es seit dem 386er echte 32-Bit-Register, denen stellt man noch ein E vorne weg, also EAX anstelle AX, etc.
Außerdem sind noch einige Register dazu gekommen:



Hierbei gibt es wichtige Paarungen, die man als Standard erwartet:

Feste Paarungen sind:
CS:IP adressiert den Programmspeicher.
SS:SP adressiert den Stack (Stapelspeicher mit LIFO = LastInFirstOut).

Im Datenbereich existieren typischerweise die Paarungen:
DS:SI  
DS:DI   DS:BX
ES:DI

DS:SI im Zusammenspiel mit ES:DI unterstützt dabei einige Kopierbefehle der 80x86 CPU, die ein sehr schnelles Verschieben von Daten im Speicher gewährleisten.
DS:SI zeigt dabei auf den Anfang der zu kopierenden Daten und ES:DI auf das Ziel. Daher rühren auch die Namen Source-Index und Destination-Index her.


Betrachten wir einige Teile unseres Programms:

  mov ax, 0x07C0  ; set up segments
  mov ds, ax
  mov es, ax

  mov si, welcome
  call print_string


Wir laden zunächst den Wert 0x07C0 in den Akkumulator AX. Anschließend schreiben wir den Inhalt dieses Allzweckregisters in die Segmentregister DS und ES.
Im nächsten Schritt schreiben wir die Adresse  des "welcome"-Strings in das Indexregister SI und rufen die Subroutine "print_string" auf.

print_string:
  lodsb        ; grab a byte from SI

  or al, al    ; AL zero?
  jz .done     ; if yes, get out

  mov ah, 0x0E
  int 0x10       ; otherwise, print out the character!

  jmp print_string

.done:
  ret


lodsb (load string byte) ist ein Befehl, der den String byteweise bearbeitet. Es wird ein Byte aus dem durch DS:SI adressierten Speicher in das Register AL geladen. Dabei wird SI um 1 erhöht (bei gelöschtem Direction-Flag). Auf diese Weise wird Byte für Byte des Strings "welcome", der mittels
welcome db 'HenkesSoft 0.01 (version from Mar 14, 2009)', 13, 10, 0 definiert wurde (db bedeutet: define byte), in das low byte des Akkumulators (AL) transferiert.

Sobald der terminierende Wert 0 (NUL) erscheint, setzt das Programm mittels OR-Funktion von AL mit sich selbst das ZERO-Flag.
Mittels jz (jump if zero) wird ein Sprung zum Label .done ausgelöst.
Ansonsten wird das ASCII-Zeichen, dem der Wert im AL entspricht, auf dem Bildschirm ausgegeben.

Schauen wir uns die Ausgabe eines Characters an, so stoßen wir auf einen sogenannten Interrupt:

  mov ah, 0x0E
  int 0x10       ; otherwise, print out the character!

Ein Interrupt ist ein Ereignis, das eine umgehende Aktion der CPU auslösen soll. Im täglichen Leben sind Telefon, Türgong, das Schreien des Babys solche Quellen für Interrupts. Bei einem Interrupt soll die CPU reagieren. In unserem Programm verwenden wir diese Interrupts selbst, um Aktionen zu triggern.

Den BIOS-Interrupt 0x10 mit AH = 0x0E finden wir hier:
AH=0x0E  Schreiben und Kursor weiterbewegen
AL zu schreibendes Zeichen im ASCII-Code
BH Seitennummer
BL Farbe (nur Grafikmodi)
Eine Gesamtübersicht über die BIOS-Interrupts findet sich an dieser Stelle.

Schauen wir uns noch diesen BIOS-Interrupt an:

  xor ah, ah  ; mov ah, 0
  int
0x16
    ; wait for keypress

Wir finden den BIOS-Interrupt 0x16 hier:
MOV   AH,00   ; Keyboard Read: auf Tastendruck warten
INT 0x16 ; AL = ASCII Code (=0 für Sondertasten), AH = Scancode

Hinweis: Für die Initialisierung eines Registers mit dem Wert Null verwendet man performant anstelle der Instruktion MOV die exklusive ODER-Verknüpfung (XOR).  MOV AX,0  erzeugt drei Bytes Maschinencode, während  XOR AX,AX  nur zwei Byte Code erzeugt. Bei den 32-Bit-Registern wirkt sich dieser Unterschied sogar verstärkt aus (sechs anstelle drei Byte Maschinencode). Dies ergibt eine Speicherplatzeinsparung und einen Zeitgewinn bei der Abarbeitung. Dies erfolgt leider auf Kosten der Verständlichkeit. Dafür nutzt man dann die leichter lesbare Anweisung im Kommentar.


Background-Wissen: Was ist ein Betriebssystem (OS = Operating System)?

Das ist keine leichte Frage, da es keine absolute Abgrenzung gibt. Wenn wir den PC anschalten und das BIOS starten, stehen uns bereits eine Reihe von Interrupts zur Verfügung, die uns die Arbeit erleichtern. Wir müssen uns nicht darum kümmern, wie wir ein Zeichen via Grafikkarte auf dem Bildschirm ausgeben. Darum hat man sich bereits im Rahmen der Abstraktion und Schaffung von Schnittstellen gekümmert.

Das sind auch bereits die wichtigsten Begriffe: Abstraktion und Schnittstellen:

Ein Betriebssystem stellt im Rahmen der Abstraktion und Schnittstellendefinition folgende Elemente zur Verfügung:
Weiterhin verwaltet das OS die Ressourcen:
Wenn man also in Blöcken bottom-up denkt, kann man z.B. folgende Bereiche sehen:
Bezüglich des "Kernels" - das ist der Betriebssystemkern - unterscheidet man den Microkernel (Beispiele) vom monolithischen Kernel (Beispiele).
Der Microkernel umfasst nur notwendige Basisfunktionen: Speicher- und Prozessverwaltung, Synchronisation und Kommunikation. Gerätetreiber sind normale User-Programme und keine Kernelmodule. Damit erhöht sich die Stabilität des OS, seine Geschwindigkeit wird aber erniedrigt. Der monolithische Kernel integriert daher aus Effizienzgründen zusätzlich Hardware-Treiber. In der Praxis hat sich - wohl aus Geschwindigkeitsgründen - der monolithische Kernel durchgesetzt: Linux, Windows. Daneben gibt es auch noch Mischformen, die man als Hybridkerne bezeichnet.

Professor Andreas S. Tanenbaum (Entwickler von Minix) ist einer der Verfechter der Microkernel-Struktur:
"MINIX 3, a highly-reliable new operating system based on a tiny (5000-line) kernel"

Tanenbaum kritisierte Linux zu Beginn (1992) scharf, behielt aber auf lange Sicht nicht Recht. Minix spielt heute dagegen nur als Lehrsystem eine Rolle. So kann man sich in der Einschätzung der Zukunft täuschen.

Wozu benötigt man einen Bootloader?

Wie man im Hexdump erkennt, ist unser Kernel in den 512 Byte ganz schön beengt. Zukunft kann so etwas nicht haben. Daher trennt man üblicherweise das Booten und den Start des Kernel in zwei Teile, nämlich in einen stabilen Bootloader und einen sich weiter entwickelnden Kernel. Der Bootloader wird nach Sektor 1 geschrieben und unser bisheriger Mini-Kernel zunächst nach Sektor 2. Der Bootloader lädt den Kernel von der Floppy Disk nach, speichert ihn an eine bestimmte Adresse und springt dann dorthin. Der Kernel benötigt also nur eine veränderte Startadresse im Programm, und die Boot-Signatur kann entfallen. Frisch ans Werk!

Bezüglich der Speicherzuordnung im unteren Bereich sei auf diese Zusammenstellung verwiesen: Memory_Map Overview

Wie sehen nun die ASM-Dateien aus?

Bootloader Sourcecode
     org 0x7C00 ; set up start address 
    ; setup a stack 
mov ax, 0x9000 ; address of the stack SS:SP
mov ss, ax ; SS = 0x9000 (stack segment)
xor sp, sp ; SP = 0x0000 (stack pointer)
    ; start
    mov [bootdrive], dl
; boot drive from DL
    call load_kernel   
; load kernel
 
    ; jump to kernel
    jmp 0x1000:0x0000  
; address of kernel
 
   
bootdrive db 0      ; boot drive
    loadmsg db "bootloader message: loading kernel ...",13,10,0
 
    ; print string
print_string:
    lodsb             ; grab a byte from SI
    or al, al         ; NUL?
    jz .done          ; if the result is zero, get out
    mov ah, 0x0E
    int 0x10          ; otherwise, print out the character!
    jmp print_string
 .done:
    ret
 
    ;
read kernel from floppy disk
load_kernel:

    mov dl,[bootdrive] ; select boot drive 
    xor ax, ax         ;
mov ax, 0  => function "reset"
    int 0x13          
    jc
load_kernel     ; trouble? try again

load_kernel1:
    mov ax, 0x1000    
    mov es, ax        
; ES:BX = 0x10000
    xor bx, bx         ; mov bx, 0
 
    ; set parameters for reading function
    ; 8-Bit-wise for better overview

    mov dl,[bootdrive] ; select boot drive
    mov al,10          ; read 10 sectors
    mov ch, 0          ; cylinder = 0
    mov cl, 2          ; sector   = 2

    mov dh, 0          ; head     = 0
    mov ah, 2          ; function "read"  
    int 0x13          

    jc
load_kernel1    ; trouble? try again

         
; show loading message
    mov si,loadmsg
    call print_string 
    ret

    times 510-($-$$) hlt
    db 0x55
    db 0xAA


Das Programm startet bei 07C00h, legt einen Stack an bei 90000h, lädt den Kernel nach 10000h und springt dorthin.
Adressierung im Real Mode siehe Abschnitt
4.5.4

Hinweis: Intel Prozessoren verwenden die Little Endian (Intel-Format) Byte-Reihenfolge im Gegensatz zu Big Endian (Motorola-Format), d.h. im Speicher werden die Bytes in der Reihenfolge beginnend vom niedrigwertigsten Byte abgelegt.

Zu Beginn der CPU-Entwicklung hatte Little Endian den Vorteil, dass man bei jeder Operation automatisch das niederwertigste Byte bereits laden konnte. Während dieses Vorgangs konnte der Befehl genau dekodiert werden. Falls für den Befehl notwendig wurden die höherwertigen Daten im nächsten Taktschritt ebenfalls bearbeitet. Dies war sehr effizient. Heute spielt dieser Zeitvorteil aufgrund breiter Datenbusse und hoher Taktraten nicht mehr diese entscheidende Rolle.

Daher müssen wir z.B.

times 510-($-$$) db 0
dw 0xAA55 ; dw = define word

schreiben, wenn wir die Byte-Folge 55AA als Bootsignatur abspeichern wollen!


Die Funktionen des BIOS-Interrupts 0x13 finden Sie im angegeben Link beschrieben.

Wir verwenden die Funktionen Reset und Read.

Hier die Parameter für Read:

AH 0X02
AL Sectors To Read Count
CX Cylinder + Sector
DH Head
DL Drive
ES:BX Buffer Address Pointer

    load_kernel1:
    mov ax, 0x1000    
    mov es, ax        
; ES:BX = 0x10000
    mov bx, 0
 
    ; set parameters for reading function  
    mov dl,[bootdrive] ; select boot drive
    mov al,10          ; read 10 sectors
    mov ch, 0          ; cylinder = 0
    mov cl, 2          ; sector   = 2

    mov dh, 0          ; head     = 0
    mov ah, 2          ; function "read"  
    int 0x13          

    jc
load_kernel1    ; trouble? try again

Wir laden großzügig bereits 10 Sektoren, obwohl momentan auch einer reichen würde.

Hinweis: Die Verwendung des High und Low Byte eines Registers in Verbindung mit dem Befehl mov, wenn genau so gut das gesamte 16-Bit-Register mit einem Befehl bedient werden könnte, ist eine Verschwendung von Speicherplatz und Prozessortakten (in diesem Fall: ein Byte und max. z.B. auf einem 486 ein Takt zusätzlich)! Dies geschieht in obigem Fall nur der besseren Übersicht wegen. Performanter Programmierstil ist dies nicht.

Also anstelle

mov al,10 (mov al, 0x0A)
mov ah, 2


verwendet man performant

mov ax, 0x020A


Welche Anpassungen sind am Kernel notwendig?

An unserem Mini-Kernel ändert sich nur wenig, nämlich nur am Anfang und Ende:

  mov ax, 0x1000  ; set up segments
  mov ds, ax
  mov es, ax

  mov si, welcome
  call print_string

loop:
  mov si, prompt
  call print_string

 ... (siehe oben)

.done:   
  stc            ; equal, set the carry flag
  ret


  times 512-($-$$) hlt ; no boot signature



Wir kombinieren Bootloader und Kernel und schreiben auf Floppy Disk

Die beiden Assemblerdateien werden zu Binärdateien assembliert.
Beide binden wir zusammen als MyOS.bin.

nasm boot.asm   -f bin -o boot.bin
nasm kernel.asm -f bin -o kernel.bin
copy /b boot.bin + kernel.bin MyOS.bin

Wenn z.B. folgende Meldung erscheint:

kernel.asm:177: error: TIMES value -22 is negative

dann hat man versucht, in die "kernel.asm" mehr als 512 Bytes hinein zu drücken, z.B. durch viel msg_... Text). In diesem Fall sollte man die
Zeile times 512-($-$$) hlt ; no boot signature einfach streichen, denn in der Datei kernel.asm ist sie nicht nötig, da dort nichts "gefüllt" werden muss,  oder die Zahl von 512 auf 1024 oder ein anderes Vielfaches von 512 erhöhen.

Nun kann unser Kernel wachsen.

Wichtiger Hinweis:
Falls es mit 
copy /b boot.bin + kernel.bin MyOS.bin
bei den nachstehenden Lösungen Probleme geben sollte,
verwenden Sie 
cmd /c copy /b boot.bin + kernel.bin MyOS.bin
als Work-around (copy ist Bestandteil von cmd.exe).


Abschließend schreiben wir auf Floppy Disk, falls vorhanden, in die ersten beiden Sektoren:
partcopy MyOS.bin 0 400 -f0

(Zahl entsprechend erhöhen, falls mehrere Sektoren bezüglich kernel.bin anfallen)

Ich empfehle ...

1) sich entweder für solche Aufgaben entsprechende bat-Dateien zu schreiben.
Bei mir sieht dies im Unterverzeichnis ..\OSDev\Test\3 wie folgt aus:



assemble.bat:
nasm boot.asm   -f bin -o boot.bin
nasm kernel.asm -f bin -o kernel.bin
copy /b boot.bin + kernel.bin MyOS.bin

partcopy.bat:
partcopy MyOS.bin 0 400 -f0

2) oder - was eindeutig der konsequentere und bessere Weg ist - das Werkzeug  make.exe  einzusetzen.
Hierzu verwendet man die Datei  make.exe  (GNU) aus folgenden Link:
GNU Make For Windows v3.75 + Source Code
Für die Ausführung von  make.exe  benötigt man ein sogenanntes "makefile" (bitte genau so nennen ohne Extension).
Dieses File enthält für unseren Zweck folgende Zeilen (die roten Zeilen alle mit TAB als Separator beginnen):



Alternativ: cmd /c copy /b boot.bin + kernel.bin MyOS.bin

Bei mir sieht dies im Unterverzeichnis ..\OSDev\Test\4 wie folgt aus:




Führen Sie den Befehl make am Anfang direkt in der Konsole aus.
Auf diese Weise erkennen Sie Fehler sofort.



Der Vorteil des make-Prozesses besteht darin, dass in dem Fall, dass
in der einen oder anderen Zeile ein Fehler auftritt, der make-Prozess abgebrochen wird.

Wenn Sie sich für die Regeln des make-Prozesses interessieren, finden Sie hier eine ausführliche und gut gegliederte Aufstellung:
Eine Einführung in Makefiles

Denken Sie auch bitte daran, dass partcopy als "Uralt-Programm" nur Filenamen mit maximal acht Zeichen verarbeiten kann. Daher das kurze "MyOS".

Wir testen unser neues MyOS im Emulator Bochs:

bootloader message: loading kernel ...
HenkesSoft 0.01 (version from Mar 14, 2009)
>hi
Hello World!
>?
Commands: hi, help, ?, exit

Hoffentlich klappt dies auch bei Ihnen.
Wenn es Probleme gibt, führen Sie die Assemblierung in der geöffeneten Konsole (cmd) aus, damit Sie die Fehlermeldungen sehen.

Denken Sie auch an die Möglichkeit, unter Bochs direkt die Binär-Datei zu emulieren:
floppya: 1_44=G:\OSDev\Test\MyOS.bin, status=inserted (Bochs config-Datei)

Schauen Sie sich bitte MyOS.bin mit dem Hex-Editor an.

Background-Wissen: Speicheradressierung im Real Mode (RM)

Bis zum 80286 gab es nur 16-Bit-Register für die Adressierung. Damit konnte man aber nur 2^16 = 64K direkt adressieren.
Um mit einem 20-Bit-Adressbus 1MB ansprechen zu können, verwendete man im Real Mode die sogenannte Segment:Offset-Adressierung.

Die Segmentregister enthalten im Real Mode lediglich 16-Bit-Segmentadressen (2^16 = 65536 = 64K).
Zur Errechnung der absoluten Adresse werden die Inhalte mit 16 (=0x10) multipliziert und der sogenannte Offset addiert.
Die Offset-Adresse ist ebenfalls auf 16 Bit begrenzt.
Real resultiert hiermit eine lineare Adressierung mit 20 Bit. Dies limitiert den direkt nutzbaren Adressraum auf 1 MB = 220 Byte.

Hinweis: Bei offener A20-Adressleitung werden im Real Mode weitere 64 KB minus 16 Byte erreichbar.
Diesen Speicherbereich von FFFF:0010h bis FFFF:FFFFh nennt man High Memory Area (HMA).

Im Real Mode errechnet sich die reale lineare Adressierung also wie folgt:

Physische Adresse = 16 * Segment + Offset

Aus den zwei vierstelligen Hexadezimalzahlen Segment und Offset wird nach obiger Regel eine fünfstellige absolute Hexadezimal-Adresse ermittelt:

Segment       Offset                   Absolute Adresse 
1000h * 10h + FFFFh = 10000h + FFFFh = 1FFFFh


Speicherinhalt
relative (segmentierte) Adresse
absolute Adresse
Bootloader-Programm 0000:7c00h 07c00h
Kernel-Programm
1000:0000h
10000h
Stack-Bereich
9000:0000h 90000h

Der Beginn des Bootloader-Programms hat daher folgenden Abstand zum Kernel-Programm:  10000h - 07c00h = 08400h
Rechnet man dies in Byte um, so geschieht dies wie folgt:

0h *  16^4 + 8h * 16^3 + 4h * 16^2 + 0h * 16^1 + 0h * 16^0  =
0  * 65536 + 8  * 4096 + 4  * 256  + 0  * 16   + 0  * 1     =
             32768     + 1024                               =  33792

Das Bootloader-Programm dürfte also theoretisch höchstens 33792 Byte / 1024 = 33 KB groß sein. Praktisch ist es auf 512 Byte, also ein halbes KByte, begrenzt.

Man kann somit 1 MB = 16 * 64 KB = 16 * 65536 Bytes =  1048576 Bytes im Real Mode direkt adressieren. Jedes Segment umfasst 64 KB.
Die Speicherbelegung in einem klassischen PC im ersten MegaByte kann man sich wie folgt vorstellen.
In unserem konkreten Fall haben wir Bootloader, Kernel und Stack wie folgt angeordnet:


Heute befindet sich das BIOS in einem Flash-Speicher, damit man es an neue Erfordernisse der Hardware updaten kann.
 

Background-Wissen: Real Mode, 16-Bit und 32-Bit-Protected Mode

Die Begrenzung der Adressierung auf 20 Bit und damit auf insgesamt 16 * 64 KB = 1 MB beim 8086 bei der direkten physischen Adressierung fand durch die Entwicklung und Kostensenkung bei der Speicherfertigung rasch ihre Grenzen. Mit dem 80286 wurde der Protected Mode eingeführt. Aber erst der Prozessor Intel 80386 (Markteinführung: 1985) öffnete hier durch seine 32-Bit-Adressierung den Horizont. Mit 2³² direkt erreichbaren Adressen konnte man nun 4294967296 Bytes = 4 GB physisch ansprechen. Heute, also etwa 25 Jahre später, kann man diesen Speicherumfang kostengünstig in jeden PC einbauen. Daher sind die heute erhältlichen 64-Bit-Systeme ein Schritt in die nächste Größenordnung. Es war historisch so, dass die Größe des in einem PC typischerweise vorhandenen physisch adressierbaren Speichers den dafür notwendigen Adressierungsmechanismus der CPU diktiert.

Der sogenannte Protected Mode (PM) schaffte - neben der Erweiterung der Adressierung - vor allem auch die Voraussetzungen für echtes Multitasking. Die grundlegende Idee für das Multitasking, also das Abarbeiten mehrerer Prozesse "zur gefühlt gleichen Zeit" war, dass bei einem laufenden Prozess die meiste Rechenzeit ungenutzt blieb, weil auf langsame, externe Ereignisse gewartet werden musste, z.B. der nächste Tastendruck. Diese Wartezeit geht dann für die Systemleistung der CPU verloren. Multitasking erlaubt es nun, die Wartezeit eines Prozesses einem anderen Prozess zur Nutzung bereit zu stellen.
Ein weiterer Vorteil von Multitasking besteht darin, dass man auf einem durch Berechnungen stark ausgelasteten Rechner parallele Prozesse zeitlich ineinander verschachteln kann. Hier dominiert die Idee der "Zeitscheiben", damit ein Prozess durch einen anderen nicht völlig ausgebremst wird, weil er auf dessen Ende warten muss.

Man unterscheidet
kooperatives Multitasking (Prozesse geben eigenständig die Regie an den Kernel zurück, keine Priorisierung möglich, Bsp.: MS Windows 3.x) und
präemptives Multitasking
(Der Kernel steuert über Zeitgeber, Interrupts und Scheduler die Abarbeitung der Prozesse gemäß Priorisierung, Bsp.: ab Windows NT).

Um die parallel laufenden Prozesse bezüglich ihres individuellen Speichers gegeneinander zu "schützen" (engl.: to protect), mussten diese den Prozessen zugeordneten Speicherbereiche gegen einander abgeschottet werden. Dieser Mechanismus gab dem neuen Modus auch seinen Namen, sonst hätte man ihn wohl eher "Virtual Mode" genannt.

Bezüglich des Speicherzugriffs trat nun also eine komplette Neuorientierung auf. Man muss historisch zwischen dem 16-Bit-Protected Mode des 80286 und dem bahnbrechenden 32-Bit-Protected Mode des 80386 unterscheiden. Nur auf Basis der historischen Entwicklung und Idee der Abwärtskompatibilität kann man den detailierten Aufbau der Adressierungsstrukturen wirklich verstehen.

Betrachten wir den Weg von der logischen Adresse (auch relativ genannt), über die lineare Adresse zur physischen Adresse (auch absolut genannt):

Beginnen wir mit dem Real Mode und 16-Bit-Protected Mode, in denen die lineare und physische Adresse den gleichen Wert haben. 

Real Mode
Im Real Mode (eigentlich müsste es Real Address Mode heißen, hat man wohl vermieden wegen der schon belegten Abkürzung RAM = Random Access Memory) befindet sich die Basisadresse direkt im Segmentregister. Man kann hier also durch eine einfache mathematische Formel aus der logischen Adresse die physische Adresse berechnen. Das Segment-Register wird mit 24 = 16 multipliziert. Zu diesem Ergebnis addiert man den Segment-Offset:


Abbildung: Adressierung im Real Mode   ( Physische Adresse = 16 * Segment-Selektor + Segment-Offset )

16-Bit-Protected Mode
Beim 16-Bit-Protected Mode wird für die Basis und die Segmentauswahl erstmals eine Deskriptorentabelle (z.B. GDT = Global Descriptor Table) zwischen die logische und physische Adresse eingeschoben. Man kann sich dies wie einen Zeigermechanismus vorstellen. Damit wird aus der logischen Adresse eine virtuelle Adresse, da man aus ihr nicht mehr die physische Adresse berechnen kann, ohne die Inhalte der Deskriptoren zu kennen.

Dieser Mechanismus erlaubte es, beim 80286 mittels 24-Bit-Adressbus insgesamt 224 Byte = 16777216 Byte = 16 MB zu adressieren.
Die Beschränkung durch die 16-Bit-Register auf maximal 216 Byte = 65536 Byte = 64 KB (Limit) große Segmente bestand immer noch.
Ein Segment wurde durch die Deskriptoren auf eine nahezu beliebige Adresse im 24-Bit-Adressraum gelegt.
In diesem Modus bestand noch kein Unterschied zwischen linearer Adresse und physischer Adresse:


Abbildung: Adressierung im 16-Bit-Protected Mode (Intel 80286, Physische Adresse = Basis aus selektiertem Deskriptor + Offset)

Der dazu passende Deskriptor (z.B. GDT) beinhaltete eine 24-Bit-Basisadresse und die Größe des Segmentes (max. 16 Bit, also 16 KB) und war wie folgt aufgebaut:

Typ und Zugriff auf die hiermit eingerichteten Segmente wurden durch das Byte "Zugriffsrechte" spezifiziert.

32-Bit-Protected Mode
Der wirkliche Durchbruch kam mit dem 32-Bit-Protected Mode des 80386.

Ein neuer Mechanismus, nämlich das Paging kommt nun hinzu. Eingeschaltet wird das Paging durch das Setzen des Bit 31 (PG, Paging-Bit) des Steuerregisters CR0. Dadurch teilt sich der physische Speicher in "Seiten" (Pages) zu jeweils 212 Byte = 4 KB. Die Berechnung einer physischen Adresse erfolgt nun aus der linearen Adresse noch über diesen Paging-Schritt. Die 32 Bit breite lineare Adresse wird dabei zweigeteilt. Die höherwertigen 20 Bit verweisen auf eine Speicherseite. Die niederwertigen 12 Bit sind der Seiten-Offset der selektierten Seite. Bit 31 ...12 werden also als Index in eine sogenannte Pagetable verwendet. Diese Pagetabelle enthält die physische Basisadresse der jeweiligen Seite.


Der Paging-Mechanismus ist aus Optimierungsgründen noch etwas komplexer (Zahlen-Beispiel siehe hier).

Der Deskriptor wuchs nun um weitere zwei auf insgesamt acht Byte, beinhaltete nun eine 32-Bit-Basisadresse, die Größe des Segmentes (max. 20 Bit), zusätzliche Flags und war wie folgt aufgebaut:



Wie man sieht, rührt die merkwürdige Anordnung der Indizes auf Basis und Länge von der Abwärtskompatibilität zum 80286 her. Die Basisadresse kann nun 32 Bit lang sein, und die Segmentgröße (Segmentlänge) kann mit 20 Bit angegeben werden. Je nach Größe des Grundelementes eines Segmentes, das entweder 1 Byte oder 4 KB betragen kann, werden hierdurch Segmente von 220 * 1 Byte = 1 MB oder 220 * 4 KB = 4 GB möglich.

Die Größe ("Granularität") des Grundelementes wird durch das Granularity-Bit des Sektors Flags (besteht aus insgesamt 4 Bit) bestimmt.


Fazit

Real Mode "Zeiger", z.B. CS:IP, auf die physische Adresse im Speicher
16-Bit-Protected Mode "Zeiger auf Zeiger" (Deskriptor -> Phys. Basis) und
"Zeiger" (Offset) auf die physische Adresse im Speicher
32-Bit-Protected Mode "Zeiger auf Zeiger auf Zeiger" (Selektor -> Deskriptor -> Page -> Phys. Basis) und
"Zeiger" (Offset) auf die physische Adresse im Speicher

Der entscheidende Punkt ist, dass man im Protected Mode eine virtuelle Adresse anspricht.
Man hat also keine aus den logischen Adresswerten berechenbare reale Adresse wie im Real Mode.
Dies erfolgt durch Entkoppelung mittels Indizes (Zeiger, Pointer) der Descriptor Table und Page Table.


Protected Mode

Eine der Aufgaben für das zu entwickelnde OS besteht nun darin, die dazwischen geschalteten Deskriptorentabellen vorzubereiten und danach in den PM umzuschalten.

Für die praktische Umsetzung bedeutet dies als Minimum:
1) GDT anlegen
2) Laden des GDTR mit GDT-Adresse
3) Protected Mode aktivieren: Setzen von Bit 0 ("PE-Bit") des Registers CR0
4) Durchführen eines "FAR-JMP" zur Leerung der Warteschlange

Wir werden zusätzlich die A20-Leitung aktivieren, damit kein 8088-kompatibler "wrap" der Speicher-Adressieruing erfolgt.

PM Sourcecode

Das folgende OS schaltet nun vom Real Mode (RM) in den Protected Mode (PM) um.
Zuerst wird der Code des überarbeiteten Bootloaders, Kernels und makefile dargestellt.
Anschließend gehen wir detailliert auf die durchgeführten Änderungen und Ergänzungen ein.


Bootloader (Sourcecode)

org 0x7C00  ; set up start address of bootloader

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; setup a stack and segment regs ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, ax

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; read kernel from floppy disk ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


    mov [bootdrive], dl ; boot drive stored by BIOS in DL.
                        ; we save it to [bootdrive] to play for safety.


load_kernel:
    xor ax, ax         ; mov ax, 0  => function "reset"
    int 0x13         
    jc load_kernel     ; trouble? try again

    mov bx, 0x8000     ; set up start address of kernel

    ; set parameters for reading function
    ; 8-bit-wise for better overview
    mov dl,[bootdrive] ; select boot drive
    mov al,10          ; read 10 sectors
    mov ch, 0          ; cylinder = 0
    mov cl, 2          ; sector   = 2
    mov dh, 0          ; head     = 0
    mov ah, 2          ; function "read" 
    int 0x13         
    jc load_kernel     ; trouble? try again

    ; show loading message
    mov si,loadmsg
    call print_string

;;;;;;;;;;;;;;;;;;
; jump to kernel ;
;;;;;;;;;;;;;;;;;;

    jmp 0x0000:0x8000   ; address of kernel. "jmp bx" might also work.

;;;;;;;;;;;;;;;;;;;;;;;
; call "print_string" ;
;;;;;;;;;;;;;;;;;;;;;;;
 
print_string:
    mov ah, 0x0E      ; VGA BIOS fnct. 0x0E: teletype
.loop:   
    lodsb             ; grab a byte from SI
    test al, al       ; NUL?
    jz .done          ; if the result is zero, get out
    int 0x10          ; otherwise, print out the character!
    jmp .loop
.done:
    ret

;;;;;;;;
; data ;
;;;;;;;;

    bootdrive db 0    ; boot drive
    loadmsg db "bootloader message: loading kernel ...",13,10,0
    
    times 510-($-$$) hlt
    db 0x55
    db 0xAA

GDT (Sourcecode)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Global Descriptor Table (GDT) ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

NULL_Desc:
    dd    0
    dd    0
  
CODE_Desc:
    dw    0xFFFF        ; segment length  bits 0-15 ("limit")   
    dw    0             ; segment base    byte 0,1     
    db    0             ; segment base    byte 2   
    db    10011010b     ; access rights
    db    11001111b     ; bit 7-4: 4 flag bits:  granularity, default operation size bit,
                        ; 2 bits available for OS

                        ; bit 3-0: segment length bits 16-19
    db    0             ; segment base    byte 3   

DATA_Desc:
    dw    0xFFFF        ; segment length  bits 0-15
    dw    0             ; segment base    byte 0,1
    db    0             ; segment base    byte 2
    db    10010010b     ; access rights
    db    11001111b     ; bit 7-4: 4 flag bits:  granularity,
                        ; big bit (0=USE16-Segm., 1=USE32-Segm.), 2 bits avail.

                        ; bit 3-0: segment length bits 16-19
    db    0             ; segment base    byte 3      

gdtr:
Limit    dw 24          ; length of GDT
Base     dd NULL_Desc   ; base of GDT ( linear address: RM Offset + Seg<<4 )


Die oben definierten drei Deskriptoren beschreiben nun die "Segmente" für "NULL", Code und Daten im Hauptspeicher. Diese Deskriptoren verwenden wir nur im Protected Mode. Real Mode hat Segmente mit konstanter Größe von 64 KByte. Jeder Deskriptor besteht aus 8 Byte. Die Deskriptoren CODE_Desc und DATA_Desc liefern Informationen über Größe, Position, Zugriffsberechtigungen und Verwendungstyp des Code- und Daten-Segmentes. Die GDT (Global Descriptor Table) kann maximal 8192 solcher Deskriptoren aufnehmen und steht jedem Prozess zur Verfügung, daher das "global". Die detaillierte Untersuchung des Inhaltes dieser Tabelle findet sich hier.


Kernel (Sourcecode)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; HenkesSoft 0.03 (version from Mar 22, 2009) ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

org 0x8000     ; code's start offset
 [BITS 16]     ; 16 Bit Code

;;;;;;;;;;;;;
; Real Mode ;
;;;;;;;;;;;;;

RealMode:
    xor ax, ax         ; set up segments
    mov es, ax
    mov ds, ax
    mov ss, ax
    mov sp, ax

    mov si, welcome
    call print_string

    add sp, -0x40     ; make room for input buffer (64 chars)
   
loop_start:
    mov si, prompt    ; show prompt
    call print_string

    mov di, sp        ; get input
    call get_string
    jcxz loop
_start   ; blank line? -> yes, ignore it   

    mov si, sp
    mov di, cmd_hi    ; "hi" command
    call strcmp
    je .helloworld

    mov si, sp
    mov di, cmd_help  ; "help" command
    call strcmp
    je .help

    mov si, sp
    mov di, cmd_questionmark  ; "?" command
    call strcmp
    je .help
 
    mov si, sp
    mov di, cmd_exit  ; "exit" command
    call strcmp
    je .exit

    mov si, sp
    mov di, cmd_pm    ; "pm (protected mode)" command
    call strcmp
    je .pm

    mov si,badcommand ; unknown command
    call print_string
    jmp loop
_start

.helloworld:
    mov si, msg_helloworld
    call print_string
    jmp loop
_start

.help:
    mov si, msg_help
    call print_string
    jmp loop
_start

.exit:
    mov si, msg_exit
    call print_string
    xor ax, ax
    int 0x16          ; Wait for keystroke

    jmp 0xffff:0x0000 ; Reboot

.pm:
    call clrscr
    mov si, msg_pm
    call print_string
    call Waitingloop

    cli               ; clear interrupts

    lgdt [gdtr]       ; load GDT via GDTR (defined in file "gtd.inc")

; we actually only need to do this ONCE, but for now it doesn't hurt to do this more often when
; switching between RM and PM
    in  al, 0x92      ; switch A20 gate via fast A20 port 92
    cmp al, 0xff      ; if it reads 0xFF, nothing's implemented on this port
    je .no_fast_A20
   
    or  al, 2         ; set A20_Gate_Bit (bit 1)
    and al, ~1        ; clear INIT_NOW bit (don't reset pc...)
    out 0x92, al
    jmp .A20_done
   
.no_fast_A20:         ; no fast shortcut -> use the slow kbc...
    call empty_8042  
   
    mov al, 0xD1      ; kbc command: write to output port
    out 0x64, al
    call empty_8042
   
    mov al, 0xDF      ; writing this to kbc output port enables A20
    out 0x60, al
    call empty_8042

.A20_done:
    mov eax, cr0      ; switch-over to Protected Mode
    or  eax, 1        ; set bit 0 of CR0 register
    mov cr0, eax      ;

    jmp 0x8:ProtectedMode ; http://www.nasm.us/doc/nasmdo10.html#section-10.1
 
;;;;;;;;;
; Calls ;
;;;;;;;;;

empty_8042:
    call Waitingloop
    in al, 0x64
    cmp al, 0xff      ; ... no real kbc at all?
    je .done
   
    test al, 1        ; something in input buffer?
    jz .no_output
    call Waitingloop
    in al, 0x60       ; yes: read buffer
    jmp empty_8042    ; and try again
   
.no_output:
    test al, 2        ; command buffer empty?
    jnz empty_8042    ; no: we can't send anything new till it's empty
.done:
ret

print_string:
    mov ah, 0x0E
.loop_start:
    lodsb              ; grab a byte from SI
    test al, al        ; test AL
    jz .done           ; if the result is zero, get out
    int 0x10           ; otherwise, print out the character!
    jmp .loop_start
.done:
    ret

get_string:
    xor cx, cx
.loop_start:
    xor ax, ax
    int 0x16           ; wait for keypress
    cmp al, 8          ; backspace pressed?
    je .backspace      ; yes, handle it
    cmp al, 13         ; enter pressed?
    je .done           ; yes, we're done
    cmp cl, 63         ; 63 chars inputted?
    je .loop_start     ; yes, only let in backspace and enter
    mov ah, 0x0E
    int 0x10           ; print out character
    stosb              ; put character in buffer
    inc cx
    jmp .loop_start

.backspace:
    jcxz .loop_start   ; zero? (start of the string) if yes, ignore the key
    dec di
    mov byte [di], 0   ; delete character
    dec cx             ; decrement counter as well
    mov ah, 0x0E
    int 0x10           ; backspace on the screen
    mov al, ' '
    int 0x10           ; blank character out
    mov al, 8
    int 0x10           ; backspace again
    jmp .loop_start    ; go to the main loop

.done:
    mov byte [di], 0   ; null terminator
    mov ax, 0x0E0D
    int 0x10
    mov al, 0x0A
    int 0x10           ; newline
    ret

strcmp:
.loop_start:
    mov al, [si]       ; grab a byte from SI
    cmp al, [di]       ; are SI and DI equal?
    jne .done          ; no, we're done.

    test al, al        ; zero?
    jz .done           ; yes, we're done.

    inc di             ; increment DI
    inc si             ; increment SI
    jmp .loop_start    ; loop!

.done:   
    ret

clrscr:
    mov ax, 0x0600
    xor cx, cx
    mov dx, 0x174F
    mov bh, 0x07
    int 0x10
    ret

;;;;;;;;;;;;;;;;;;
; Protected Mode ;
;;;;;;;;;;;;;;;;;;

[Bits 32]

ProtectedMode:
    mov    ax, 0x10
    mov    ds, ax      ; data descriptor --> data, stack and extra segment
    mov    ss, ax            
    mov    es, ax
    xor    eax, eax    ; null desriptor --> FS and GS
    mov    fs, ax
    mov    gs, ax
    mov    esp, 0x200000 ; set stack below 2 MB limit

    call clrscr_32
    mov ah,  0x01
.endlessloop:
    call Waitingloop
    inc ah
    and ah, 0x0f
    mov esi, msg_pm2   ; 'OS currently uses Protected Mode.'
    call PutStr_32
    cmp dword [PutStr_Ptr], 25 * 80 * 2 + 0xB8000
    jb .endlessloop
    mov dword [PutStr_Ptr], 0xB8000  ; text pointer wrap-arround
    jmp .endlessloop

Waitingloop:                   
    mov ebx,0x9FFFF
.loop_start:
    dec ebx     
    jnz .loop_start
    ret         

PutStr_32:     
    mov edi, [PutStr_Ptr]
.nextchar:
    lodsb
    test al, al         
    jz .end     
    stosw
    jmp .nextchar 
  .end:
    mov [PutStr_Ptr], edi
    ret 

clrscr_32:
    mov edi, 0xb8000
    mov [PutStr_Ptr], edi
    mov ecx, 40 * 25
    mov eax, 0x07200720 ; two times: 0x07 => white text & black background 0x20 => Space
    rep stosd
    ret
 
PutStr_Ptr dd 0xb8000
   
; You load the address you want to output to in [PutStr_Ptr],
; the address of the string (has to be NUL terminated)
; you want to print in esi and the attributes in ah
; lodsb loads one byte from esi into al, then increments esi,
; then it checks for a NUL terminator,

; then it moves the char into the write position in video memory,
; then increments edi and writes the attributes,

; loops until it finds NUL pointer at which point it breaks ...

;;;;;;;;;;;
; Strings ;
;;;;;;;;;;;

welcome db 'HenkesSoft 0.03 (version from Mar 22, 2009)', 13, 10, 0
msg_helloworld db 'Hello World!', 13, 10, 0
badcommand db 'Command unknown.', 13, 10, 0
prompt db '>', 0
cmd_hi db 'hi', 0
cmd_help db 'help', 0
cmd_questionmark db '?', 0
cmd_exit db 'exit', 0
cmd_pm db 'pm', 0
msg_help db 'Commands: hi, help, ?, pm, exit', 13, 10, 0
msg_exit db 'Reboot starts now. Enter keystroke, please.', 13, 10, 0
msg_pm db 'Switch-over to Protected Mode.', 13, 10, 0
msg_pm2 db 'OS currently uses Protected Mode.  ', 0

;;;;;;;;;;;;
; Includes ;
;;;;;;;;;;;;

%include "gdt.inc"

;;;;;;;;;;;;;;;;;;;
; Set Bits to HLT ;
;;;;;;;;;;;;;;;;;;;
 
times 1024-($-$$) hlt


makefile

all:
    nasmw -O32 -f bin -o boot.bin boot.asm
    nasmw -O32 -f bin -o kernel.bin kernel.asm
    cmd /c copy /b boot.bin + kernel.bin MyOS.bin
    partcopy MyOS.bin 0 600 -f0





Analyse des Sourcecodes

Bootloader
Der Bootloader hat die Aufgabe den PC zu booten und unseren Kernel bei der physischen Adresse 0x8000 in den Speicher zu laden. Sie wissen bereits, dass ein Programm, das im ersten Sektor mit 0x55AA an den letzten Bytes des Sektors endet, den Bootvorgang einleitet, der dazu führt, dass der erste Sektor des Laufwerks (512 Byte) an die physische Adresse 7C00h (Segment 0, Offset 7C00h) geladen wird. Die org-Anweisung teilt dem Assembler mit, an welcher Stelle im Speicher das Programm in Bezug auf den Segmentanfang (Offset) ausgeführt werden soll.

org 0x7C00  ; set up start address of bootloader

Anschließend wird das Allzweckregister AX performant auf Null gesetzt.
Die Register DS, ES, das Stack-Register SS und der Stack-Pointer werden ebenfalls auf Null gesetzt.
Solche Vorgänge werden oft mit cli/sti, also einem Aus- und Wiedereinschalten der Interrupts, eingerahmt. Dies ist aber nicht notwendig.

    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, ax

Im Register DL befindet sich nach dem Booten die Laufwerksinformation:

00h first floppy, 80h first hard disk
Diese Information transportieren wir zur Sicherheit nach "bootdrive", falls DL bis zum Sektor-Lesen zwischenzeitlich "geschrottet" würde.

    mov [bootdrive], dl ; boot drive from DL

Der nächste Schritt umfasst den Datentransfer von Sektor 2 des Boot-Laufwerks in den Speicher an die Adresse 8000h.
In unseren Vorversuchen wählten wir hier die Adresse 10000h. Aber warum so viel Platz zwischen Bootloader und Kernel lassen?
Wir lesen bereits 10 Sektoren ein, falls unser Kernel (zur Zeit noch zwei Sektoren) weiter wächst.

load_kernel:
    xor ax, ax         ; mov ax, 0  => function "reset"
    int 0x13         
    jc load_kernel     ; trouble? try again

    mov bx, 0x8000     ; set up start address of kernel
 
    ; set parameters for reading function
    ; 8-bit-wise for better overview
    mov dl,[bootdrive] ; select boot drive
    mov al,10          ; read 10 sectors
    mov ch, 0          ; cylinder = 0
    mov cl, 2          ; sector   = 2
    mov dh, 0          ; head     = 0
    mov ah, 2          ; function "read" 
    int 0x13         
    jc load_kernel     ; trouble? try again

    ; show loading message
    mov si,loadmsg
    call print_string

Ausführliche Informationen zum Verständnis des BIOS-Interrupts 0x13 findet man hier.
Den Sprungbefehl kann man unter Verwendung von BX (jmp bx erfordert, dass cs gleich null ist)
oder der logischen Adresse verwenden:


    jmp 0x0000:0x8000   ; address of kernel

Ansonsten findet sich nichts Neues im Bootloader.  

GDT
Zunächst sei hier nochmals an den Aufbau der Deskriptoren erinnert:


Ein Deskriptor im Stil ab dem Intel 80386 Protected Mode umfasst also 8 Byte. Nachfolgend findet man diesen Typ als CODE-Deskriptor (CODE_Desc) und DATA-Deskriptor ( DATA_Desc). Das erste Word (= 2 Byte = 16 Bit) umfasst die Segmentlänge (auch "limit" genannt), dann folgt als zweites Word die Basisadresse. Das dritte Byte der Basisadresse findet sich im Anschluss als fünftes Byte des Deskriptors.

Dann muss man auf die Bit-Ebene herunter. Es folgen das Byte Zugriffsrechte mit folgenden acht Bits.
Diese Zugriffsrechte waren ein Schlüsselelement im Protected Mode:

Bit
Meaning
0
1
7
P (Present Bit) Descriptor is undefined
Descriptor contains a valid base and limit
6
DPL (High)
see below see below
5
DPL (Low)
see below see below
4
S (Segment Bit) System Descriptor
Code, Data or Stack Descriptor
3
Type
see below
see below
2
Type
see below see below
1
Type
see below see below
0
A (Accessed Bit) Segment not accessed
Segment has been accessed

Die beiden DPL (
Deskriptor Privilege Level) Bits setzen das Privileg-Level:   0 (00b), 1 (01b), 2 (10b) oder 3 (11b)

Die drei "Type" Bits schaffen acht Möglichkeiten:
000  Data read only
001  Data read/write
010  Stack read only
011  Stack read/write
100  Code execute only
101  Code execute/read
110 
Code execute only, conforming
111  Code execute/read, conforming

Nun müssen wir nur noch verstehen wie die Tabelle GDT (Global Descriptor Table) selbst aufgebaut ist:
Die Beschreibung der GDT erfolgt im GDTR (
Global Descriptor Table Register). Dieses besteht aus sechs Byte (48 Bit). Die ersten beiden niederwertigen Byte  beschreiben die Länge der GDT. Die Zahl der Deskriptoren ergibt sich hieraus durch Anzahl = Länge GDT / 8. In unserem konkreten Fall ist dies 24/8 = 3. Anschließend folgen vier Byte (32-Bit-Adresse), die die Basisadresse der GDT enthalten, in unserem Fall beginnt die Tabelle beim Label  NULL_Desc.

gdtr:
Limit    dw 24          ; length of GDT
Base     dd NULL_Desc   ; base of GDT ( linear address: RM Offset + Seg<<4 )


Der erste Deskriptor muss ein Null-Descriptor sein. Daher enthält er acht Bytes mit lauter Nullen:

NULL_Desc:
    dd    0
    dd    0


Im CODE-Descriptor wird es schon inhaltsvoller.

Die Segmentlänge beträgt 0xFFFFF.
Die Segmentbasis ist 0x0
Die Zugriffsrechte dekodieren sich wie folgt:
Deskriptor besitzt eine gültige Basis und ein gültiges Limit (Bit 7)
DPL: Privileg 0 (Bit 6, Bit 5)
S: Code, Data, Stack (Bit 4)
Typ:
101 "Code execute/read" (Bit 3, Bit 2, Bit 1)
A: Segment not accessed (Bit 0)

Nun bleiben noch die vier Flag Bits:
Granularität: 1 (bedeutet 4KB als Grundelelement, damit können
220 * 4 KB = 4 GB adressiert werden)
Default Operation Size Bit: 1 (
0 = USE16-Segment; 1 = USE32-Segment)
Die beiden nachfolgenden Bits sind zum freien Gebrauch des OS gedacht (bzw. reserviert).
 
CODE_Desc:
    dw    0xFFFF        ; segment length  bits 0-15 ("limit")   
    dw    0             ; segment base    byte 0,1     
    db    0             ; segment base    byte 2   
    db    10011010b     ; access rights
    db    11001111b     ; bit 7-4: 4 flag bits:  granularity, default operation size bit,
                        ; 2 bits available for OS

                        ; bit 3-0: segment length bits 16-19
    db    0             ; segment base    byte 3   

Im DATA-Descriptor ist alles analog aufgebaut. Der Unterschied besteht bei den Zugriffsrechten. Hier sitzt Bit 3 auf Null.
Also 001, das
"Data read/write" bedeutet:

DATA_Desc:
    dw    0xFFFF        ; segment length  bits 0-15
    dw    0             ; segment base    byte 0,1
    db    0             ; segment base    byte 2
    db    10010010b     ; access rights
    db    11001111b     ; bit 7-4: 4 flag bits:  granularity,
                        ; big bit (0=USE16-Segm., 1=USE32-Segm.), 2 bits avail.

                        ; bit 3-0: segment length bits 16-19
    db    0             ; segment base    byte 3      


Kernel
Obwohl unser Kernel bisher wenig für uns leistet, hat er doch schon einiges an Code-Zeilen zu bieten.
Der Bootloader ist nach 0x0000:0x8000 gesprungen. Daher findet sich diese Adresse in der org-Anweisung von kernel.asm.
Da wir im Real Mode starten, verwenden wir zunächst den 16-Bit-Modus.

org 0x8000     ; code's start offset
 [BITS 16]     ; 16 Bit Code

Wir setzen die Register DS, ES, SS und den Stack Pointer SP auf Null.

    xor ax, ax         ; set up segments
    mov es, ax
    mov ds, ax
    mov ss, ax
    mov sp, ax

    mov si, welcome
    call print_string

Für den Stack Pointer verwenden wir hier eine "wrap around" Technik. So führt der Befehl

    add sp, -0x40     ; make room for input buffer (64 chars)
   
zu einem Wert SP = 0x0 - 0x40 = 0xFFC0

Die folgende Schleife ist für die Abarbeitung der Befehle
im Real Mode zuständig.
Interessant wird es bei der Marke .pm, die zum Umschalten in den Protected Mode führt.
Der Bildschirm wird gelöscht, die Interrupts abgeschaltet und die Global Descriptor Table (GDT) mittels GDTR geladen.

.pm:

    call clrscr
    mov si, msg_pm
    call print_string
    call Waitingloop

    cli               ; clear interrupts

    lgdt [gdtr]       ; load GDT via GDTR (defined in file "gtd.inc")

Im nächsten Schritt aktivieren wir das A20-Gate, damit wir ohne "wrap around" den Speicher über 1 MB adressieren können.
Das wurde im Kapitel A20 bereits detailliert besprochen. Weiter geht es bei der Marke .A20_done.
Nun kommt der entscheidende Schritt, der den PM aktiviert. Wir setzen Bit 0 der Steuerregisters CR0:

.A20_done:
    mov eax, cr0      ; switch-over to Protected Mode
    or  eax, 1        ; set bit 0 of CR0 register
    mov cr0, eax      ;

Anschließend springen wir zur Adresse ProtectedMode, an der wir mit 32-Bit-Code weiter fahren.
Dies geschieht unter
Angabe des Selektors 0x8 für das Segment zu dem gesprungen werden soll.
Der 16-Bit-Selektor hat nur einen 13-Bit-Index, der als Zeiger auf die Deskriptortabelle fungiert.


Wir wollen auf Deskriptor 1 (Zählweise beginnt bei 0 mit dem Null-Deskriptor) zeigen.
Dieser Index beginnt im 16-Bit-Selektor (siehe oben) aber erst ab Bit 3 (von Bit 0 an gezählt), weil noch drei Flag-Bits enthalten sind.
Für unseren Index bleiben also nur 13 Bit. Daher kann es maximal 213 = 8192 Einträge in der Deskriptoren-Tabelle geben.
Unsere Deskriptoren-Tabelle ist die GDT (TI = 0) und das angeforderte Privileg die höchste Stufe 0 (RPL = 0).
Daher geben wir hier die 0x8 = 1000b im Selektor ein.
Man kann sich das als Links-Shift des binären Index um drei Stellen vorstellen.
Rechnen Sie es unter Verwendung des Windows-Zubehörs "Calculator" im wissenschaftlichen Modus nach:



    jmp 0x8:ProtectedMode
     ...

[Bits 32]
ProtectedMode:

Im nächsten Schritt werden die Register DS, ES und SS mit dem Wert 0x10 = 10000b geladen.
Kontrolle mit dem wissenschaftlichen "Calculator" mit Hexadezimalzahlen:



    mov    ax, 0x10

    mov    ds, ax      ; data descriptor --> data, stack and extra segment
    mov    ss, ax            
    mov    es, ax
    xor    eax, eax    ; null desriptor --> FS and GS
    mov    fs, ax
    mov    gs, ax

Der Stack Pointer wird auf den Wert 0x200000 = 2097152 = 2048 K (entspricht 2MB) gesetzt.

    mov    esp,
0x200000 ; set stack below 2 MB limit

Nachfolgend wird der Bildschirm gelöscht und die Zeichenfolge msg_pm2 mit wechselnden Farben ausgegeben.
Der passende Video RAM startet bei 0xB8000 (
0xA0000 Graphical Mode, 0xB0000 Monochrome Text Mode, 0xB8000 Color Text Mode).

    call clrscr_32
    mov ah,  0x01
.endlessloop:
    call Waitingloop
    inc ah
    and ah, 0x0f
    mov esi, msg_pm2   ; 'OS currently uses Protected Mode.'
    call PutStr_32
    cmp dword [PutStr_Ptr], 25 * 80 * 2 + 0xB8000
    jb .endlessloop
    mov dword [PutStr_Ptr], 0xB8000  ; text pointer wrap-arround
    jmp .endlessloop

Eine einfache Warteschleife, die von 0x9FFFF nach Null zählt:

Waitingloop:                   
    mov ebx,0x9FFFF
.loop_start:
    dec ebx     
    jnz .loop_start
    ret         

Diese Routine sorgt für die Ausgabe der Strings, die mittels Register ESI übergeben werden,
beginnend bei 0xB8000.

PutStr_32:     
    mov edi, [PutStr_Ptr]
.nextchar:
    lodsb
    test al, al         
    jz .end     
    stosw
    jmp .nextchar 
  .end:
    mov [PutStr_Ptr], edi
    ret 

PutStr_Ptr dd 0xb8000

; You load the address you want to output to in [PutStr_Ptr],
; the address of the string (has to be NUL terminated)
; you want to print in esi and the attributes in ah
; lodsb loads one byte from esi into al, then increments esi,
; then it checks for a NUL terminator,

; then it moves the char into the write position in video memory,
; then increments edi and writes the attributes,

; loops until it finds NUL pointer at which point it breaks ...

Bildschirm-Löschen-Routine für den Protected Mode:

clrscr_32:
    mov edi, 0xb8000
    mov [PutStr_Ptr], edi
    mov ecx, 40 * 25
    mov eax, 0x07200720
    rep stosd
    ret
 
Unser erstes Include ist die Datei gdt.inc, in der die Global Descriptor Table (GDT) und das
Global Descriptor Table Register (GDTR) definiert wird:

%include "gdt.inc"

Zum Schluss füllen wir bis zur nächsten N * 512 Byte-Grenze mit dem Befehl HLT (Die Instruktion HLT
setzt den Prozessor in den Haltezustand, in dem die CPU nichts weiter macht, als den Zustand der Hardware-Leitungen Reset und NMI zu überwachen. Sind maskierbare Unterbrechungen nicht gesperrt, dann wird der Haltezustand auch verlassen, wenn am Eingang INTR ein Signal anliegt.). Man kann natürlich auch das übliche  db 0  verwenden, aber beim Disassemblieren macht HLT vielleicht mehr Sinn.
 
times 1024-($-$$) hlt



Emulatoren

Bochs

Das Ergebnis unser bisherigen Bemühungen sollte im Emulator Bochs nun hoffentlich auch bei Ihnen wie folgt aussehen:



Bochs emuliert leider nur relativ langsam, weshalb man es nur in Grenzen als Alternative zu MS Virtual PC oder Sun VirtualBox sieht.
Die Software VMware Workstation ist noch kostenpflichtig, kann aber als 30 Tage-Prüfversion bezogen werden.
Im Bereich des OS Developments hat Bochs einen festen Platz, vielleicht gerade, weil es "langsam" ist.
Der interne Bochs-Debugger trägt sicher ebenfalls zu dem soliden Ruf des Systems bei.
Das Programm Bochs (gesprochen "box") wurde von Kevin Lawton in C++ zunächst als kommerzielle Software entwickelt und später als open
source für viele Plattformen angeboten. Dazu gehören z.B. Windows, Linux, MacOS, Solaris, BeOS, IRIX, Digital Unix und AIX.
Diese geniale Software "emuliert" einen Computer des Typs Intel x86.
Man kann damit das Booten des selbst entwickelten OS und die Interaktion mit Tastatur, Maus, Grafikkarte, Laufwerke etc. austesten. Dies spart gegenüber dem
"echten" Testen eine Menge Arbeitszeit. Man kann diese Emulations-Software als 386er, 486er, Pentium oder x86-64 CPU einstellen.
Hierbei werden auch MMX, SSEx und 3DNow! Instruktionen berücksichtigt, falls erwünscht.
Bochs wird neben anderen Anwendungen zum Debuggen bei der Entwicklung von Betriebssystemen verwendet.
Hierbei kann man sich bequem die Inhalte des Speichers und der CPU Register anschauen oder die Verwendungshäufigkeit und Leistungsfähigkeit einzelner
Programmabschnitte testen.

Die Verhaltenseigenschaften von Bochs werden über eine config-Datei eingestellt.

Beispiel:  cpu: count=1,
ips=10000000, reset_on_triple_fault=1


Hier ist eingestellt, dass ein Prozessor mit 10 Mips (Million Instructions Per Second) emuliert wird. Den berühmten Triple Fault
werden Sie bei der OS-Entwicklung häufig erleben. Hierbei tritt eine "Exception" (Fehlermeldung) auf, während die CPU versucht den Double
Fault
Exception Handler aufzurufen, der wiederum Exceptions behandelt, während er den regulären Exception Handler aufruft. Hier hilft nur noch die Fehlersuche.

Wenn Sie Fehler suchen oder einfach nur den Ablauf detailliert verfolgen wollen, benötigen Sie den Bochs Debugger (.../Bochs-2.3.7/bochsdbg.exe).
Die in diesem Rahmen zur Verfügung stehenden Befehle findet man hier: .../Bochs-2.3.7/docs/user/internal-debugger.html

c continue executing
continue
s [count] execute count instructions, default is 1
step [count]
stepi [count]
Ctrl-C stop execution, and return to command line prompt
Ctrl-D if at empty line on command line, exit
q quit debugger and execution
quit
exit
 info cpu        List of all CPU registers and their contents

MS Virtual PC

Wenn Sie eine deutlich schnellere Emulation als Bochs benötigen, so empfiehlt sich MS Virtual PC 2007 (kostenloser Download).
Dieses Programm bietet vor allem etwas mehr Komfort beim Einrichten des virtuellen PC. Nachfolgend unser System im Test:



Allerdings ist es hoch geschlossen und bietet keinen internen Debugger.

Sun xVM VirtualBox

Sun bietet uns die kostenlose Virtual Box als "künstlichen" x86 PC. Bei der Installation motzt MS Windows XP wegen mangelnder Kompatibilität (kein Problem, immer tapfer OK drücken). Beim Betrieb wird man seitens Sun unterbrochen durch Popup-Windows (Updates, Registrierung). Allerdings bietet dieses Programm eine log-Datei mit Register- u. Table-Informationen.



Bezüglich der Geschwindigkeit liegt die Sun VirtualBox zwischen dem Rennwagen MS VirtualPC und dem Oldtimer Bochs.
Persönlich habe ich alle Emulatoren installiert. Man kann als OS Developer nie genug PCs auf dem Desktop und um sich herum haben. ;-)


Background-Wissen: A20-Gate

Wenn wir über den Protected Mode sprechen, beginnen wir praktisch sicher nicht unter einem 80386, da wir heuzutage zumindest von 32-Bit-Systemen ausgehen. 
Die Entwicklung vom 8088 zu den heutigen modernen CPUs ist allerdings von vielen historischen Zwängen geplagt, die man aus Gründen der Kompatibilität
nicht mehr los wird, obwohl man sie schon lange nicht mehr braucht. So ein Relikt ist das A20-Gate.

Die Geschichte ist ganz einfach. Rechnet man die logische Adresse FFFF:FFFF (Real Mode) in eine physische Adresse um,
so erhält man mit der Formel 0xFFFF * 0x10 + 0xFFFF = 0xFFFF0 + 0xFFFF = 0x10FFEF = 1114095.
Das übersteigt den adressierbaren Speicherbereich von 1 MB = 1048576 Byte = 0x100000 Byte.

Im 8088 ergab dies einen "wrap", so dass z.B. aus 0x10FFEF einfach 0x0FFEF wurde. Beim 80286, der nun 24 Adressleitungen bot,
musste man daher die Adressleitung A20 künstlich lahm legen, damit er kompatibel zum 8088 war. Dafür benutzte man einen freien Pin am Keyboard Controller.
Wenn der PC heute im Real Mode startet ist genau dieses A20-Gate, die es immer noch gibt, abgeschaltet.

Daher muss diese Leitung im Protected Mode bei Zugriffen oberhalb 1 MB aktiviert werden, denn im Protected Mode kann man problemlos mit 32 Bit arbeiten.
Daher muss man das A20-Gate wieder aktivieren, da ansonsten bei bestimmten Speicherzugriffen Fehler auftreten, wenn die 21. Adressleitung deaktiviert ist.
Das bedeutet, das man auf eine andere Speicherstelle zugreifen kann als beabsichtigt.
Daher schaltet man das A20-Gate ein, bevor man den Kernel und weitere Programmteile startet.

Das A20-Gate ist wirklich ein lästiges Relikt aus grauer Vorzeit, das aus heutiger Sicht überflüssigen Codes notwendig macht.

Es gibt mehrere Methoden, diesen Einschaltvorgang durchzuführen:
1) Keyboard Controller
2) System Control Port A
3) Modernes BIOS

Im Falle unseres Kernel-Sourcecodes wählen wir die schnelle Variante via System Control Port A und, falls diese schief geht, die langsame via Keyboard Controller.
Der Befehl in bedeutet input from port. Nachfolgend wird somit der Wert des Ports 0x92 in das Register AL geladen.
in al, 0x92      ; switch A20 gate via fast A20 port 92

Da ältere Systeme die schnelle System Control Port A - Variante nicht bieten, muss man evtl. auf die langsame Keyboard Controller - Variante ausweichen.
Das wird hier geprüft.


cmp al, 0xff     
; if it reads 0xFF, nothing's implemented on this port
je .no_fast_A20

Falls der schnelle Weg via Port 0x92 funktioniert, setzt man Bit 1 (A20_Gate_Bit) und löscht aber sicherheitshalber Bit 0 (Reset).
Anschließend sendet man den Wert mit dem Befehl out vom Register zum Port 0x92. Das ist der schnelle und einfache Weg.


or  al, 2         ; set A20_Gate_Bit (bit 1)
and al, ~1        ; clear INIT_NOW bit (don't reset pc...)
out 0x92, al
jmp .A20_done

Der langsame Weg führt über
Port 0x60 und Port 0x64 des Keyboard Controllers (Mainboard) und den Tastatur-Chip (Tastatur).
Man schreibt
0xD1 (11010001b) nach Port 0x64, damit das naechste Byte auf Port 0x60 beim Output-Port landet.
Darüber kann man dann seriell Informationen an die Tastatur schicken. Wir senden 0xDF zur Aktivierung des A20 Gate.

Der Port 0x64 des Keyboard Controller hat folgende Funktionen:

Bit 0 bewirkt einen Reset der CPU, falls dieses auf 0 gesetzt wird.
Bit 1 kontrolliert das A20 Gate. Bei 1 ist es eingeschaltet, bei 0 ausgeschaltet.
Den gewüschten Wert des
Output Port schreibt man also nach Port 0x60:
0xDD = 11011101b (disable A20) bzw.
0xDF = 11011111b (
enable A20).

.no_fast_A20:     ; no fast shortcut -> use the slow
keyboard controller command
call empty_8042  
mov al, 0xD1      ; keyboard controller command: write to output port
out 0x64, al
call empty_8042
mov al, 0xDF      ; writing this to kbc output port enables A20
out 0x60, al
call empty_8042

.A20_done: ... Hier geht es anschließend weiter mit der Umschaltung zum Protected Mode.

Die Subroutine empty_8042 kümmert sich um das Input-Handling des Keyboard Controllers, das etwas Zeit benötigt.
Daher die Warteschleifen.

empty_8042:
call Waitingloop
in al, 0x64
  cmp al, 0xff      ; ... no real kbc at all?
  je .done
  test al, 1        ; something in input buffer?
  jz .no_output
  call Waitingloop
  in al, 0x60       ; yes: read buffer
  jmp empty_8042    ; and try again 

.no_output:
  test al, 2      ; command buffer empty?
  jnz empty_8042    ; no: we can't send anything new till it's empty

.done:
 ret



Hier einige vertiefende Links zu diesem Thema:
http://www.elektronik-kompendium.de/sites/com/0811181.htm
http://de.wikipedia.org/wiki/A20-Gate
http://lowlevel.brainsware.org/wiki/index.php/A20-Gate
http://www.win.tue.nl/~aeb/linux/kbd/A20.html



C-Kernel

Wenn Sie sich im Umfeld des OS-Developments umschauen, dann findet man vor allem die Hardware-nahe Hochsprache C als Entwicklungstool. Der Grund liegt auf der Hand. Man will etwas mehr Unabhängigkeit von der Hardware, und die Lesbarkeit für den Entwickler ist erhöht. Aber wie schafft man nun von unserer Bootloader & Assembler-Kernel Kombination den Sprung zum C-Kernel?

Compiler, Linker, Bin Tools

Zunächst benötigen wir einen C-Compiler und einen Linker. Da gibt es vor allem eine Wahl: gcc. Da wir unter MS Windows entwickeln, verwenden wir die entsprechende "Portierung" DJGPP. Wir verwenden diesen Download: C-Compiler (ohne C++)

Hinweis:
Ich empfehle diese DJGPP-Toolchain (gcc 3.1, ld 2.13, ...), da bei Einsatz der
MinGW (GCC) Compiler Suite über Linker-Probleme mit dem in diesem Tutorial erfolgreich verwendeten NASM Output-Format a.out (assembler output)-Objektdatei-Format (inzwischen durch COFF und ELF abgelöst) berichtet wurde.

Inzwischen wurde im zweiten Ansatz (siehe Teil 2 dieses Tutorials) ein Work-around gefunden:
http://www.henkessoft.de/OS_Dev/OS_Dev2.htm#mozTocId42018
http://www.c-plusplus.de/forum/viewtopic-var-p-is-1736328.html#1736328

Nach der Installation finden Sie den Compiler gcc.exe (Version 3.1) und Linker ld.exe (Version 2.13) im Verzeichnis C:\djgpp\bin\...



Innerhalb DJGPP befindet sich eine Auswahl der sogenannten GNU Binutils. Hier eine Gesamtübersicht mit kurzen Erläuterungen:

The GNU Binutils are a collection of binary tools. The main ones are:

But they also include:


Damit Sie DJGPP erfolgreich aufrufen können, müssen Sie noch den Pfad und die Umgebungsvariable korrekt setzen.
Dazu hangeln Sie sich unter MS Windows XP in der Kette "Arbeitsplatz - Eigenschaften - Erweitert - Umgebungsvariablen - Systemvariablen " durch und stellen sicher, dass dort folgende Einträge stehen:



Die Umgebungsvariable sollte man auch mit setdjgpp.bat setzen können. Stellen Sie auf jeden Fall sicher, dass alles geklappt hat.
Damit können Sie nun assemblieren (nasmw), compilieren (gcc) und linken (ld).

Um den Sprung von unserem Assembler-Kernel zu einem in C geschriebenen Kernel bewerkstelligen zu können, müssen wir den bisherigen Kernel etwas modifizieren:

Die org-Anweisung muss wegfallen, damit wir das File als Objekt-Datei compilieren und linken können. Nur für das Binär-Format ist org zugelassen.
Dann definieren wir die Sprungmarke RealMode als global, damit man diese auch von außerhalb erreichen kann. Wichtig ist die Deklaration [extern _main], denn diese erlaubt uns einen Sprung nach "außen" in eine andere Datei zur Hauptfunktion main(...) des C-Programmteils.

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; HenkesSoft 0.04 (version from Mar 26, 2009) ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;org 0x8000     ; code's start offset
[BITS 16]     ; 16 Bit Code
[global RealMode]
[extern _main] ; this is in the c file

;;;;;;;;;;;;;
; Real Mode ;
;;;;;;;;;;;;;

RealMode:
...


Die nächste Änderung kommt am Ende unserer Videoausgabe im Protected Mode. Wir beenden diesen wirklich netten Ausgabenmechanismus nach einer Seite und springen in die zuvor als extern deklarierte Funktion _main.

;;;;;;;;;;;;;;;;;;
; Protected Mode ;
;;;;;;;;;;;;;;;;;;

[Bits 32]

ProtectedMode:
    mov    ax, 0x10
    mov    ds, ax      ; data descriptor --> data, stack and extra segment
    mov    ss, ax           
    mov    es, ax
    xor    eax, eax    ; null desriptor --> FS and GS
    mov    fs, ax
    mov    gs, ax
    mov    esp, 0x200000 ; set stack below 2 MB limit

    call clrscr_32
    mov ah,  0x01
.endlessloop:
    call Waitingloop
    inc ah
    and ah, 0x0f
    mov esi, msg_pm2   ; 'OS currently uses Protected Mode.'
    call PutStr_32
    cmp dword [PutStr_Ptr], 25 * 80 * 2 + 0xB8000
    jb .endlessloop
    mov dword [PutStr_Ptr], 0xB8000  ; text pointer wrap-arround


  ;jmp .endlessloop

  call _main ; ->-> C-Kernel
  jmp $
   


Die Anweisung  times 1024-($-$$) hlt  am Ende der Datei, die diesen Assemblerkernel im Binärformat bisher auf 1024 byte aufgefüllt hat, lassen wir nun ebenfalls entfallen. Wir wollen unser OS performant halten, damit es auf eine Diskette passt.

Denken Sie auch an die Aktualisierung der Versions-Info:  welcome db 'HenkesSoft 0.04 (version from Mar 26, 2009)', 13, 10, 0

Warum wird dieses "Underscore" für "_main" verwendet? In C deklariert man es lediglich als "main". Der Grund ist, dass der Compiler gcc verwendet den Underscore vor allen Funktions- und Variablennamen. Daher muss man einen Underscore hinzufügen, wenn man eine Funktion im C-Code seitens Assemblercode  anspricht.

Wie sieht nun der C-Kernel aus? Als Editor verwende ich hier aus Gewohnheit Code::Blocks.

Die Herausforderung besteht darin, dass wir die C run-time Bibliothek nicht verwenden können.
Wir müssen dennoch nicht alles neu erfinden. Wir können von anderen Entwicklungen bereits eine Menge lernen und diese an unsere Bedürfnisse anpassen.

Auf Basis dieses weiter vereinfachten Beispiels probieren wir den Sprung zu unserem C-Kernel aus:


C-Kernel (Sourcecode)

// ckernel.c

void k_clear_screen()

{
    char* vidmem = (char*) 0xb8000;
    unsigned int i=0;
    while(i<(80*2*25))
    {
        vidmem[i] = ' ';
        ++i;
        vidmem[i] = 0x07;
        ++i;
    };
};

unsigned int k_printf(char* message, unsigned int line)
{
    char* vidmem = (char*) 0xb8000;
    unsigned int i = line*80*2;

    while(*message!=0)
    {
        if(*message==0x2F)
        {
            *message++;
            if(*message==0x6e)
            {
                line++;
                i=(line*80*2);
                *message++;
                if(*message==0){return(1);};
            };
        };
        vidmem[i]=*message;
        *message++;
        ++i;
        vidmem[i]=0x7;
        ++i;
    };
    return 1;
};

inline void outportb(unsigned int port,unsigned char value)
{
    asm volatile ("outb %%al,%%dx"::"d" (port), "a" (value));
};

void update_cursor(int row, int col)
{
    unsigned short    position=(row*80) + col;
    // cursor LOW port to vga INDEX register
    outportb(0x3D4, 0x0F);
    outportb(0x3D5, (unsigned char)(position&0xFF));
    // cursor HIGH port to vga INDEX register
    outportb(0x3D4, 0x0E);
    outportb(0x3D5, (unsigned char)((position>>8)&0xFF));
};

int main()
{
    k_clear_screen();
    k_printf("Welcome to HenkesSoft OS.", 0);
    k_printf("The C kernel has been loaded.", 2);
    update_cursor(3, 0);
    return 0;
};


Diese Datei nennen wir ckernel.c
In unserem Verzeichnis finden sich nun folgende Dateien (ohne das makefile):



kernel.ld  ist das sogenannte Linkerscript. Das sieht bei uns konkret wie folgt aus:

OUTPUT_FORMAT("binary")
ENTRY(RealMode)
SECTIONS
{
  .text 0x8000 : { *(.text) }
  .data        : { *(.data) }
  .bss         : { *(.bss)  }
}


Unser  makefile  hat sich ebenfalls gewandelt:

all:
    nasmw -O32 -f bin boot.asm -o boot.bin
    nasmw -O32 -f aout kernel.asm -o kernel.o
    gcc -c ckernel.c -o ckernel.o
    ld -T kernel.ld kernel.o ckernel.o
    rename a.out ckernel.bin

    cmd /c copy /b boot.bin + ckernel.bin MyOS.bin

    partcopy MyOS.bin 0 1000 -f0


Ausführung von make.exe liefert ein  MyOS.bin, das gebootet und ausgeführt sich erwartungsgemäß wie folgt meldet:



Probieren Sie es selbst aus:

Background-Wissen: Was ist ein Gerätetreiber?

Ein "hardware driver" (schlechte deutsche Übersetzung: "Gerätetreiber", oft kurz "Treiber" genannt) dient zur Steuerung einer Hardwarekomponente. Es handelt sich hierbei um einen Programmteil, der die Wechselwirkung, z.B. Ein-/Ausgabe, Laden/Speichern oder ein anderer Daten-/Informationsaustausch, mit angeschlossener oder eingebauter Hardware steuert. Steuersignale und/oder Daten werden zwischen Gerät und das Programm ausführender Einheit ausgetauscht.

MS DOS hatte im Gegensatz zu MS Windows noch keinen Standard für diese Schnittstelle definiert. Dadurch war eine individuelle Adaption an jede mögliche Hardwarekonfiguration nötig. Bedingt durch ihre Funktion sind Hardware Driver abhängig von der Hardware und dem Betriebssystem.

Welche Geräte hängen denn typischerweise an unserer CPU und müssen in einem OS mittels Hardware Driver betrieben werden?
Hier zeigen wir eine kleine Auswahl möglicher PC-Hardware, die ohne Treiber nutzlos ist, da sie in der Regel nicht autonom arbeitet:

Output Input
- Grafikkarte / Monitor
- Soundkarte / Lautsprecher / Verstärker

- Printer / Plotter

- Tastatur
- Maus
- Trackball, Joystick, Pad, ...
- Soundkarte / Mikrofon, Line-In
- TV/Video-In-Karte

Read only
Read & Write
- CD-ROM
- DVD-ROM
- Scanner
- Floppy Disk
- Festplatten
- CD-/DVD-Brenner
- "Solid State Disk" (SSD)
- Flash-Speicherkarten

Computer Netzwerk / Kommunikation
- Mainboard, Bios, Chipsatz
- SATA, IDE, SCSI
- USB, Firewire
- Serielle Schnittstelle
-
Parallele Schnittstelle
- Gameport
- Netzwerkkarte & Protokolle
-
Telefon, Fax
- Modem, ISDN,
DSL
- WebCam, DigiCam

In unserem OS müssen wir uns nun zumindest auf die rudimentärsten Operationen konzentrieren, nämlich Videoausgabe (monochrome/farbige Text/Grafik-Ausgabe), Dateneingabe über Tastatur und Daten lesen/schreiben von/auf formatierte(r) Floppy Disk. Maus, Drucker, Festplatte könnten später folgen, wenn notwendig.

Background-Wissen: Video-RAM 0xB8000

Damit wir unserem Betriebssystem bei der "Arbeit" zusehen können, benötigen wir eine Grafikkarte mit angeschlossenem Monitor.
Der Datenaustausch läuft hierbei über den sogenannten Videospeicher. Dies ist also unser erster "Treiber", den wir benötigen. 

Dafür gibt es prinzipiell verschiedene Möglichkeiten:
1) Ausgabe von Zeichen auf den Bildschirm mittels BIOS-Funktion 0x10,
2) Zeichen direkt in den Video-RAM (Teil des Hauptspeichers: 0xB8000 - 0xBC000) speichern.

Möglichkeit 1 steht uns nach der Umschaltung in den Protected Mode momentan nicht zur Verfügung.
Daher wird typischerweise Möglichkeit 2 genutzt.
Also müssen wir zunächst verstehen, wie unser Video-Speicher funktioniert.
Im sogenannten Video-RAM stehen zwischen 0xB8000 und 0xB8FFF insgesamt 4096 Byte zur Verfügung.
Zur Darstellung für 25 Zeilen (0...24) und 80 Spalten (0...79) benötigen wir nur 2000 Zeichen.



Es stehen also für jedes Zeichen auf dem Bildschirm im Speicher zwei Byte zur Verfügung:
Für die Zeichendarstellung wird der sogenannte ASCII-Zeichensatz verwendet.
Für die Attribute Zeichen-/Hintergrund-Farbe u. Blinken gilt folgende Codierung:


Dieses Basiswissen ist nötig, um Zeichen auf einem modernen Farb-Monitor darstellen zu können.


Analyse des C-Kernels (Sourcecode)

Beid er Analyse gehen wir davon aus, dass die Programmiersprache C zumindest grundsätzlich bekannt ist. Gerüstet mit dem Wissen über den Videospeicher bei 0xB8000 können wir folgende Routine verstehen:
Die Aufgabe ist das "Löschen" des Bildschirms. Dies steckt bereits im Funktionsnamen. Der Typ void der Funktion zeigt uns, dass wir keinen Rückgabewert erwarten. Wir stellen ein k_ (für kernel) davor, um Verwechslungen der selbst geschriebenen mit namensgleichen Funktionen anderer Bibliotheken zu vermeiden:

void k_clear_screen()
{

Im nächsten Schritt definieren wir einen Zeiger auf den Typ char namens vidmem an der Adresse 0xB8000 und die Indexvariable i.
Damit haben wir das notwendige Rüstzeug , um den Videospeicher als Array  von 4000 = 2*80*25 Byte (daher der Typ char) ansprechen zu können.
Zum "Abklappern" der 4000 Byte verwenden wir eine while-Schleife, in der wir die Indexvariable selbst nach dem Setzen des Zeichens und Attributes erhöhen müssen. 


Als Zeichen zum Löschen verwenden wir das Zeichen "space" (SP, Leerzeichen).
Das Attribut 0x07 = 00000111b dekodiert sich zu:
schwarzer Hintergrund mit hellgrauem (matt weißen) - nicht blinkenden - Text.
Damit haben wir die Schriftfarbe hellgrau und den Hintergrund schwarz für alle 2000 Zeichen gesetzt. Das Zeichen selbst ist ein Leerzeichen.

    char* vidmem = (char*) 0xb8000;
    unsigned int i=0;
    while(i<(80*2*25))
    {
        vidmem[i] = ' '// Space (ASCII: 0x20)
        ++i;
        vidmem[i] = 0x07; // white on black
        ++i;
    };
};


Nun verstehen Sie auch etwas leichter den PM-Assemblercode im Assembler-Kernel (s.o.):

clrscr_32:
    mov edi, 0xb8000
    mov [PutStr_Ptr], edi
    mov ecx, 40 * 25    ; we address two screen characters
    mov eax, 0x07200720 ; two times (eax has 4 byte):
             0x07 => white text & black background
             0x20 => Space

    rep stosd
  
Im Real Mode erledigt dies übrigens ein BIOS-Interrupt für uns, der für uns momentan nicht zugänglich ist.
Ich denke, es hilft etwas im Grundverständnis, diese beiden Lösungen für das Bildschirmlöschen darzustellen.
In Assembler spricht man jeweils vier Byte gleichzeitig an, in unserer C-Routine erledigen wir dies Byte per Byte.

Vorschlag:
Um etwas Routine zu bekommen, sollten Sie die "Löschroutine", die nichts anderes macht, als die 4000 Byte des Videospeichers Byte per Byte mit Werten zu füllen, so verändern, dass diese Zeichen, Vorder- und Hintergrundfarben nach Wunsch verwendet. Vielleicht kommt dabei eine Routine heraus, die Sie sich für andere Zwecke unter anderem Namen aufbewahren wollen?!

Wenn Sie z.B. nicht im Byte- (char), sondern im Word- (int mit 16 Bit bei dem verwendeten gcc-Compiler) Rhythmus durch das Video-RAM gleiten wollen, sieht der Code wie folgt aus:

void k_clear_screen()
{
    unsigned int* vidmem = (unsigned int*) 0xb8000;
    unsigned int i=0;
    while(i<(80*25))
    {
        vidmem[i] = 0x0720; // white on black, Space (0x20)
        ++i;
    };
};

 
Dieser Code repräsentiert z.B. sehr gut den Aufbau des Video-RAM aus 80*25 einzelnen Zeichen.
Daher empfehle ich diesen Code für eine "Lösch"-Routine.

Wer wie in der Assembler-Routine einen 32-Bit-Wert, also ein Double Word, verwenden will, der setzt folgenden Code ein:

void k_clear_screen()
{
    unsigned long* vidmem = (unsigned long*) 0xb8000;
    unsigned long i=0;
    while(i<(40*25))
    {
        vidmem[i] = 0x07200720; // white on black, Space (0x20)
        ++i;
    };
};

Nachdem Sie diese Funktion verstanden haben, ist der Rest für Sie vermutlich leicht verdaulich.
Nun wollen wir mit einer Funktion eine Nachricht auf dem Bildschirm ausgeben. Das f des printf steht für formatiert.
Wir nutzen diese Funktion in main() bisher wie folgt:

k_printf("Welcome to HenkesSoft OS.", 0);
k_printf("The C kernel has been loaded.", 2);


Schauen wir uns an, was dabei im Detail mit den Bytes des Video-Ram passiert. Die erste Zeile mit dem char-Zeiger auf 0xB8000 kennen Sie ja bereits.
Jede Zeile mit 80 Zeichen umfasst 2*80 = 160 Byte. Wir verarbeiten also den Zeilen-Parameter "line" mit der Formel i = line*80*2 und erhalten damit das erste Zeichen der Zeile, in der ausgedruckt werden soll. Da wir mit dem char-Zeiger "message", den wir als Parameter übergeben, einen C-String "abarbeiten", müssen wir nach besonderen Zeichen suchen. Da wäre z.B. die NUL ('\0'), die durch die Zahl 0 repräsentiert wird. Dieses Zeichen beendet einen String. Daher lassen wir die while-Schleife nur solange laufen, bis dieses Ende des Strings eintritt. Daher der Code while(*message!=0)

Die if-Kontrollstruktur prüft auf Gleichheit mit 0x2F. Wenn gleich, wird ein Zeichen weiter auf 0x6E geprüft. was bedeuten diese beiden ASCII-Werte?
0x2F steht für / und 0x6E für n. Das ergibt /n. Diese Kombination steht hier für den Umbruch in eine neue Zeile.  Dies erfolgt durch Erhöhung der Variable line um eins und Berechnung des zu besetzenden Byte. Dann wird wieder auf den terminierenden Zahlenwert Null geprüft und ggf. abgebrochen. Solange kein '\0' und kein /n kommt, wird unser String "matt-weiß auf schwarz" mit 0x7 = 111b ausgegeben. Erst kommt das Zeichen-Byte, dann das Attribut-Byte. Als Erfolgswert der Funktion wird die 1 zurück gegeben.

unsigned int k_printf(char* message, unsigned int line)
{
    char* vidmem = (char*) 0xb8000;
    unsigned int i = line*80*2;

    while(*message!=0)
    {
        if(*message==0x2F)
        {
            *message++;
            if(*message==0x6e)
            {
                line++;
                i=(line*80*2);
                *message++;
                if(*message==0){return(1);};
            };
        };
        vidmem[i]=*message;
        *message++;
        ++i;
        vidmem[i]=0x7;
        ++i;
    };
    return 1;
};

Sind wir mit diesem Sourcecode aus dem Beispiel zufrieden? Ich würde nein sagen. Aber das ist Ansichtssache.
Was würde ich ändern und warum?

Also zunächst haben die Autoren (Joachim Nock et. al.) des Sourcecodes selbst etwas geändert, denn in ihrem Tutorial präsentieren Sie gegenüber der Download-Version folgende Veränderung, die ich für sinnvoll erachte, weil '\n' das Zeichen in C-Strings für newline ist.
Dennoch könnte die vorstehende Variante für den einen oder anderen Anregung geben. Es geht ja nicht darum, Altes exakt nach zu empfinden, sondern Neues zu schaffen. Trotzdem lohnt es sich bei anderen OS-Versionen, sei es linux 0.01, ein Hobby- oder Lehr-OS, einen Blick in die Sources und in den Aufbau zu werfen.
Ich habe bereits leichte Veränderungen gegenüber dem Original angebracht, z.B. das hier geeignetere ++i anstelle i++.

unsigned int k_printf(char *message, unsigned int line) 
{
char *vidmem = (char *) 0xb8000;
unsigned int i=(line*80*2);

while(*message!=0)
{
if(*message=='\n') // check for a new line
{
line++;
i=(line*80*2);
*message++;
}
else
{
vidmem[i]=*message;
*message++;
++i;
vidmem[i]=0x7;
++i;
};
};
return(1);
};

Das printf(...) hat eigentlich noch keine Format-Funktionen, verdient das f also noch nicht. Das heben wir uns für später auf.
Nun analysieren wir erstmal den Rest, der ziemlich "wild" aussieht. Die Funktion outportb(... port, ... value) hat die Aufgabe, ein Byte an einem Port (es gibt bei Intel maximal 216 =  65536 mögliche Ports) auszugeben.

Um die IBM-PC Kompatibilität zu gewährleisten, wurden die meisten Ports im unteren Bereich industriell standardisiert, vor allem die für Funktionen des sogenannten Video Graphics Array (VGA):

0x03B0 – 0x03BB  CRT controller (mono)
0x03C0 – 0x03CF  other VGA elements
0x03D0 – 0x03DF  CRT controller (color) 

Bezüglich VGA-Programmierung in C kann man diese Tutorials empfehlen.

Unsere Funktion update_cursor sendet einen Befehl  an Port 0x3D4 und Port 0x3D5 im CRT Control Register des VGA Controller.
Dies sind die High und Low Bytes des VGA Index Register, die den blinkenden Cursor an die entsprechende Position dirigieren.

inline void outportb(unsigned int port,unsigned char value)
{
    asm volatile ("outb %%al,%%dx"::"d" (port), "a" (value));
};

void update_cursor(int row, int col)
{
    unsigned short position = (row*80) + col;

    // cursor HIGH port to vga INDEX register
    outportb(0x3D4, 0x0E);
    outportb(0x3D5, (unsigned char)((position>>8)&0xFF));

    // cursor LOW port to vga INDEX register
    outportb(0x3D4, 0x0F);
    outportb(0x3D5, (unsigned char)(position&0xFF));

};

Eine sehr schöne Funktion zur Steuerung des Cursors. So etwas kann man immer brauchen.

Der Compiler gcc bietet die Möglichkeit, im Assemblercode Werte zu verarbeiten, die nicht direkt im Assemblerstring stehen, sondern in C-Variablen gespeichert sind. Diese Möglichkeit wurde in der Funktion outportb(...) genutzt. Die Syntax ist allgemein wie folgt:

asm ( "outb %%reg1, %%reg2" : "d" (port) : "a" (value) );

Hier sind die allgemeinen Regeln für dieses Vorgehen:

Die Funktion main(...) arbeitet nun mit den erstellten Funktionen und ist dadurch gut leserlich, ein wichtiger Punkt bei einem OS!
Wir bohren die Textausgabe nun etwas auf:

int main()
{
    k_clear_screen();

    k_printf("   ************************************************", 0);
    k_printf("   *                                              *", 1);
    k_printf("   *          Welcome to HenkesSoft OS.           *", 2);
    k_printf("   *                                              *", 3);
    k_printf("   *        The C kernel has been loaded.         *", 4);
    k_printf("   *                                              *", 5);
    k_printf("   ************************************************", 6);

    update_cursor(8, 0);
    return 0;
};


Nach dem Booten sieht das klassisch wie folgt aus:



Aufgabe: Schreiben Sie printf(...) so um, dass man einen Attribut-Parameter eingeben kann.

Hier ist der aktuelle Stand mit einer möglichen Lösung:




Der historische Sprung von matt-weiß nach leuchtend grün war visuell beeindruckend. Die Farbe war endlich im Spiel! Später kamen sogar kurzzeitig bernsteinfarbene Monitore hinzu. Richtig farbig wurde es Anfang der 80er Jahre mit dem berühmten Commodore C64, den man an ein Farb-TV anschloss.

makefile

Noch ein Wort zum "makefile". Dieses bauen wir etwas um, damit vorhandene Dateien ckernel.bin automatisch überschrieben werden.
Wir haben beim Linker nun 
-o ckernel.bin --verbose ergänzt:
Die Option -o sorgt dafür, dass ein File mit dem gewählten Namen ausgegeben wird. Bereits vorhandene Dateien dieses Namens werden überschrieben.
--verbose (engl. verbose:  „wortreich“ bzw. „weitschweifig“) gewährt uns Informationen über interne Vorgänge - wortreich - durch Ausgaben aller Status- und. Fehlermeldungen. Das hilft vielleicht beim Verfolgen von Fehlern.

all:
    nasmw -O32 -f bin boot.asm -o boot.bin            
    nasmw -O32 -f aout kernel.asm -o kernel.o         
    gcc -c ckernel.c -o ckernel.o                     
    ld -T kernel.ld kernel.o ckernel.o -o ckernel.bin 
    cmd /c copy /b boot.bin + ckernel.bin MyOS.bin    
    partcopy MyOS.bin 0 800 -f0
    del kernel.o
    del ckernel.o
    del ckernel.bin
    del boot.bin   

So sieht es ohne 
--verbose aus:

...>make
nasmw -O32 -f bin boot.asm -o boot.bin
nasmw -O32 -f aout kernel.asm -o kernel.o
gcc -c ckernel.c -o ckernel.o
ld -T kernel.ld kernel.o ckernel.o -o ckernel.bin
cmd /c copy /b boot.bin + ckernel.bin MyOS.bin
boot.bin
ckernel.bin
        1 Datei(en) kopiert.
partcopy MyOS.bin 0 800 -f0
del kernel.o
del ckernel.o
del ckernel.bin
del boot.bin

Folgende zusätzlichen Meldungen (blau) werden durch  --verbose  in der Konsole (cmd) ausgegeben:

...>make
nasmw -O32 -f bin boot.asm -o boot.bin
nasmw -O32 -f aout kernel.asm -o kernel.o
gcc -c ckernel.c -o ckernel.o
ld -T kernel.ld kernel.o ckernel.o -o ckernel.bin --verbose

GNU ld version 2.13
  Supported emulations:
   i386go32
using external linker script:
==================================================
OUTPUT_FORMAT("binary")
ENTRY(RealMode)
SECTIONS
{
  .text  0x8000 : {
    *(.text)
  }
  .data  : {
    *(.data)
  }
  .bss  :  {
    *(.bss)
  }
}

==================================================
attempt to open kernel.o succeeded
kernel.o
attempt to open ckernel.o succeeded
ckernel.o
cmd /c copy /b boot.bin + ckernel.bin MyOS.bin
boot.bin
ckernel.bin
        1 Datei(en) kopiert.
partcopy MyOS.bin 0 800 -f0
del kernel.o
del ckernel.o
del ckernel.bin
del boot.bin

Die Anzahl der zu schreibenden Byte für den aktuellen Stand kann man auf 800h (4 Sektoren) reduzieren.
Das passt aber exakt:  MyOS.bin  umfasst zur Zeit 2048 =  800h Byte.
Dies sollte man mit einem Hexeditor im Auge behalten, damit hinterher auf Diskette keine Byte fehlen. ;-)

Module in C

Sie fragen sich sicher, wie das nun weiter gehen soll. Wird die Datei ckernel.c immer weiter wachsen bis zu einer Unzahl teilweise voneinder abhängigen kleinen Funktionen? Das macht sicher keinen Sinn. Man teilt in C daher das Programm in selbständig übersetzbare Programmeinheiten auf. Diese bündeln typischerweise verschiedene logisch zusammengehörige Funktionen, die auch durch andere Programmteile genutzt werden können. Diese Programmeinheiten vom Typ xxx.c nennt man Module. Ein Modul kann nicht nur Funktionen, sondern auch andere Datentypen "exportieren". Damit ein Modul Ressourcen aus anderen Modulen "importieren" kann verwendet man das Schlüsselwort extern. Man deklariert also die nicht im Modul befindlichen Funktionen mit dem vorgestellten Schlüsselwort extern.

Wir werden dies nun praktisch an unserem Beispiel üben, damit Sie genau verstehen, wie dies zusammen hängt.

Die Programme, die sich mit dem Video RAM beschäftigen, werden wir im Modul video.c zusammen fassen. Diese Funktionen müssen wir dann im Modul ckernel.c als  extern  deklarieren ( = bekannt machen ). Nachfolgend zeigen wir den Inhalt der beiden Module:

// Modul ckernel.c

extern void k_clear_screen();
extern unsigned int k_printf(char* message, unsigned int line, char attribute);
extern void outportb(unsigned int port,unsigned char value);
extern void update_cursor(int row, int col);

int main()
{
    k_clear_screen();

    k_printf("   ************************************************", 0, 0xA);
    k_printf("   *                                              *", 1, 0xA);
    k_printf("   *          Welcome to HenkesSoft OS.           *", 2, 0xA);
    k_printf("   *                                              *", 3, 0xA);
    k_printf("   *        The C kernel has been loaded.         *", 4, 0xA);
    k_printf("   *                                              *", 5, 0xA);
    k_printf("   ************************************************", 6, 0xA);

    update_cursor(8, 0);
    return 0;
};

// video.c

void k_clear_screen()
{
    unsigned long* vidmem = (unsigned long*) 0xb8000;
    unsigned long i=0;
    while(i<(40*25))
    {
        vidmem[i] = 0x0A200A20; // intensive green on black, Space (0x20)
        ++i;
    };

};

unsigned int k_printf(char* message, unsigned int line, char attribute)
{
    char* vidmem = (char*) 0xb8000;
    unsigned int i = line*80*2;

    while(*message!=0)
    {
        if(*message=='\n') // check for a new line
        {
          line++;
          i=(line*80*2);
          *message++;
        }
        else
        {
          vidmem[i]=*message;
          *message++;
          ++i;
          vidmem[i]=attribute;
          ++i;
        }
    };
    return 1;

};

inline void outportb(unsigned int port,unsigned char value)
{
    asm volatile ("outb %%al,%%dx"::"d" (port), "a" (value));
};

void update_cursor(int row, int col)
{
    unsigned short position=(row*80) + col;

    // cursor HIGH port to vga INDEX register
    outportb(0x3D4, 0x0E);
    outportb(0x3D5, (unsigned char)((position>>8)&0xFF));
    // cursor LOW port to vga INDEX register
    outportb(0x3D4, 0x0F);
    outportb(0x3D5, (unsigned char)(position&0xFF));

};

Im  makefile  müssen nun einige Änderungen vorgenommen werden:

all:
    nasmw -O32 -f bin boot.asm -o boot.bin            
    nasmw -O32 -f aout kernel.asm -o kernel.o         
    gcc -c ckernel.c -o ckernel.o                     
    gcc -c video.c -o video.o            
    ld -T kernel.ld kernel.o ckernel.o video.o -o ckernel.bin --verbose
    cmd /c copy /b boot.bin + ckernel.bin MyOS.bin    
    del video.o
    del kernel.o
    del ckernel.o
    del ckernel.bin
    del boot.bin   
    partcopy MyOS.bin 0 800 -f0


Unser OS wird also aus vielen kleinen Einheiten bestehen, die mittels verschiedener Tools assembliert, compiliert, gelinkt, gemergt und transferiert werden.
Ich versuche, den bisherigen Prozess grafisch darzustellen, damit die Zusammenhänge des make-Prozesses möglichst klar werden:


Objdump

Im Verzeichnis  C:\djgpp\bin\... finden Sie das Programm  objdump.exe.
Verwenden Sie dieses Hilfsmittel, um Objektdateien zu analysieren. Wir schauen uns das konkret an:

objdump -D kernel.o >objdump_kernel_o_.txt

objdump_kernel_o_text


objdump -D ckernel.o >objdump_ckernel_o_.txt

objdump_ckernel_o_.txt

Beispielsweise können Sie damit feststellen, welchen Typ die Objektdatei wirklich hat, denn die Endung o sagt darüber nichts aus.
Damit Sie die Objekt-Files überhaupt finden, müssen Sie oben im makefile temporär die del (delete = löschen) Anweisung außer Kraft setzen.
Wir finden beispielsweise die verschiedenen Objektformate von kernel.o (a.out) und ckernel.o (COFF) heraus:

kernel.o: file format a.out-i386   ckernel.o: file format coff-go32

Darüber hinaus bietet objdump.exe die Möglichkeit des Disassemblierens, so dass man gerade bei der Umsetzung aus C heraus die Ergebnisse des Compilers auf der Assemblerstufe analysieren kann. Dies ist sehr wichtig, da wir mittels Compiler Flags (Optionen) z.B. die Optimierung des Assemblercodes steuern können. Dies betrachten wir an einem konkreten Fall. In video.c haben wir die Funktion

inline void outportb(unsigned int port,unsigned char value)
{
    asm volatile ("outb %%al,%%dx"::"d" (port), "a" (value));
};

Die Umsetzung schauen wir uns nun mit verschiedenen Optimierungsstufen an. Zunächst die Ausgabe, die die Zeile 
gcc -c video.c -o video.o
in unserem makefile innerhalb der Objektdatei video.o erzeugt hat. Erzeugen Sie bitte mit objdump.exe ein File objdump_video_o_.txt.

000000be <_outportb>:
  be:    55                       push   %ebp
  bf:    89 e5                    mov    %esp,%ebp
  c1:    83 ec 04                 sub    $0x4,%esp
  c4:    8b 45 0c                 mov    0xc(%ebp),%eax
  c7:    88 45 ff                 mov    %al,0xffffffff(%ebp)
  ca:    8b 55 08                 mov    0x8(%ebp),%edx
  cd:    8a 45 ff                 mov    0xffffffff(%ebp),%al
  d0:    ee                       out    %al,(%dx)
  d1:    c9                       leave 
  d2:    c3                       ret     


Stellen wir nun die Optimierungsstufe 1 mittels folgender Änderung (im makefile) 
gcc -c video.c -o video.o -O1 ein, so verändert sich das Ergebnis des Compiliervorgangs deutlich:

00000074 <_outportb>:
  74:   55                      push   %ebp
  75:   89 e5                   mov    %esp,%ebp
  77:   8b 55 08                mov    0x8(%ebp),%edx
  7a:   8a 45 0c                mov    0xc(%ebp),%al
  7d:   ee                      out    %al,(%dx)
  7e:   5d                      pop    %ebp
  7f:   c3                      ret


Sie können hieraus zumindest drei Punkte erkennen:
1) Es lohnt sich in die Ergebnisse eines Compiliervorgangs einzusteigen, denn wir haben die Assembler-Programmierung nun der Hochsprache C überlassen.
2) Option Optimierungen nutzen, falls für den Sourcecode nutzbringend anwendbar.
3) Es ist nützlich, neben der Intel Syntax auch die AT&T Syntax für Assembler verstehen und schreiben zu können.

Selbst für ckernel.c lohnt sich die Optimierung. Alles funktioniert noch wie beabsichtigt.

Damit ändert sich unser makefile nun wie folgt ab:

all:
    nasmw -O32 -f bin boot.asm -o boot.bin           
    nasmw -O32 -f aout kernel.asm -o kernel.o        
    gcc -c ckernel.c -o ckernel.o -O1                    
    gcc -c video.c -o video.o -O1            
    ld -T kernel.ld kernel.o ckernel.o video.o -o ckernel.bin --verbose
    cmd /c copy /b boot.bin + ckernel.bin MyOS.bin   
    del video.o
    del kernel.o
    del ckernel.o
    del ckernel.bin
    del boot.bin  
    partcopy MyOS.bin 0 800 -f0




Vorbereitungen - Hilfsmodule

Wenn wir Zahlen als Text auf dem Bildschirm oder Drucker ausgeben wollen, müssen wir diese zuvor in das sogenannte Stringformat umwandeln. Diese Routinen benötigen wir immer wieder. Daher bauen wir uns ein Modul util.c auf, in dem wir notwendige Hilfsroutinen ablegen. Ein Beispiel ist die Umwandlung von Integer in Text entweder im Dezimal- oder Hexadezimal-Format:

void k_itoa(int value, char* valuestring)
{
  int tenth, min_flag;
  char swap, *p;
  min_flag = 0;

  if (0 > value)
  {
    *valuestring++ = '-';
    value = -INT_MAX > value ? min_flag = INT_MAX : -value;
  }

  p = valuestring;

  do
  {
    tenth = value / 10;
    *p++ = (char)(value - 10 * tenth + '0');
    value = tenth;
  }
  while (value != 0);

  if (min_flag != 0)
  {
    ++*valuestring;
  }
  *p-- = '\0';

  while (p > valuestring)
  {
    swap = *valuestring;
    *valuestring++ = *p;
    *p-- = swap;
  }
}

void k_i2hex(unsigned int val, unsigned char* dest, int len)
{
    unsigned char* cp;
    char x;
    unsigned int n;
    n = val;
    cp = &dest[len];
    while (cp > dest)
    {
        x = n & 0xF;
        n >>= 4;
        *--cp = x + ((x > 9) ? 'A' - 10 : '0');
    }
    return;
}


Nach util.c lagern wir z.B. auch die beiden Funktionen zum Einlesen von einem Port oder zum Schreiben auf einen Port:

inline unsigned inportb(unsigned port)
{
    unsigned ret_val;
    asm volatile ("inb %w1,%b0"    : "=a"(ret_val)    : "d"(port));
    return ret_val;
}

inline void outportb(unsigned port, unsigned val)
{
    asm volatile ("outb %b0,%w1" : : "a"(val), "d"(port));
}


Background-Wissen: Scancodes und ASCII

Eine PC Tastatur ist ein eigenständiges „Computer“ System, d.h. ein Mikrocontroller Chip überwacht (scan) ständig die Betätigung der Tasten. Durch diesen Prozess gehen durch die "Unabhängigkeit" der Tastatur keine Anschläge verloren, nur weil der Prozessor des PC beschäftigt ist. Die Programmlogik der Tastatur kümmert sich durch geeignete Scanzeiten (im 10 Millisekunden-Bereich) um das Unterdrücken der Tastenprellung (keybounce). Zusätzlich muss auch das andauernde Drücken einer Taste verarbeitet werden. Die noch heute gültige Basis mit mindestens 101 verschiedenen Tasten ist das „IBM PC/AT 101 Key Enhanced Keyboard“.

Beim Betätigen einer Taste wird der entsprechende Scancode der Taste gesendet. Diese Scancodes wurden von IBM in den 80er Jahren definiert. Die Scancodes helfen, die physischen Tasten von den länderspezifische Belegungen zu entkoppeln. Erst im Keyboard Treiber wird dem Scancode eine konkrete Bedeutung gemäß dem ASCII-Code mittels Keymaps zugeordnet. Durch die Shift-Taste entsteht eine Doppelbelegung vieler Tasten (Groß-/Kleinbuchstaben, Zahlen, Satz- und Sonderzeichen), weshalb man entsprechende Keymaps für die Shift- und die Non-Shift-Tastaturbelegung anbieten muss. Bezüglich der Scancodes gibt es prinzipiell drei Codesets, die durch Ein-/Ausschalten der sogenannten Scancodeumwandlung auf insgesamt sechs Möglichkeiten variiert werden. Man hat sich aber darauf geeinigt, dass alle gängigen OS heute das Scancode Set 2 mit eingeschalteter Umwandlung einsetzen.Der Mikrocontroller in der Tastatur überträgt die Codes also mit Scancode Set 2 zum Keyboard Controller auf dem Mainboard. Dieser setzt diese Codes um auf Scancode Set 1.

Die Tastatur erzeugt übrigens zwei Scancodes pro Tastenanschlag, nämlich einen "down code" beim Drücken und einen "up code" beim Loslassen der Taste:
Wird beispielsweise die Taste mit dem aufgedruckten "A" gedrückt, so ist der Scancode für Down 0x1E und der Scancode für Up 0x9E.
Die Scancodes für Down und Up unterscheiden sich immer durch das Bit7 = 10000000b = 0x80, also 0x9E = 0x1E + 0x80.
Der Scancode für die Taste, die das 'A' trägt (links 2 Reihe bei den Buchstaben) hat also den 7-Bit-Scancode 0x1E. Das höchste Bit signalisiert, ob die Taste losgelassen (1) oder gedrückt (0) wird.

Es gibt auch später von IBM beim Entwurf der MF 101 hinzu gefügte Tasten, die
zwei oder mehr Scancodes an den PC senden. So überträgt die rechte Control-Taste beim Down 0xE0 und 0x1D und beim Up 0x9D.

Kommt ein Scancode zum PC, so empfängt der Keyboard Controller den Scan Code (Set 2), wandelt diesen um (Set 1), stellt diesen Code am I/O Port 0x60 zur Verfügung und sendet einen Interrupt an die CPU. Nun muss der Scancode der jeweiligen Taste ausgewertet und ermittelt werden, welche der  modifizierenden Tasten - Shift, Ctrl, Alt, Alt Gr, Feststelltaste, Num, Rollen, Einf - aktiv ist. Das Resultat wandert in den Tastaturpuffer, der  normalerweise 16 Plätze hat, wobei ein Überlauf möglich ist. Diesen Puffer gilt es zu verarbeiten und den Scancode nun mit einem Zeichen wie Ziffer, Buchstabe, Sonderzeichen oder Satzzeichen zu verknüpfen. Die Tastatur liefert also nur Tastennummern und keine ASCII-Zeichen! Die Verknüpfung findet erst in der Software des PC-Betriebssystems mittels Keymaps statt.

Was ist nun dieser ASCII? 

ASCII (American Standard Code for Information Interchange) stellt einen 7-Bit-Code (dezimal: 0 ...127) für Zeichen dar, der 1968 durch ANSI standardisiert wurde. Nachfolgend eine kompakte tabellarische Darstellung mit Hexzahlen:

     0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
--------------------------------------------------------------------
0 NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI
1 DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US
2 SP ! " # $ % & ' ( ) * + , - . /
3 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
4 @ A B C D E F G H I J K L M N O
5 P Q R S T U V W X Y Z [ \ ] ^ _
6 ` a b c d e f g h i j k l m n o
7 p q r s t u v w x y z { | } ~ DEL

Der Großbuchstaben 'A' besitzt den ASCII 0x41, während der Kleinbuchstabe 'a' mit 0x61 codiert ist.

Als Ergänzung zu diesen 128 grundlegenden Zeichen wurde später der "Extended ASCII Character Set" geschaffen.
Dieser umfasst weitere 128 Zeichen (128 ... 255).


Keyboard Driver

Na, da hat man doch schon richtig Lust, etwas einzugeben?
Es fehlt uns aber noch ein Keyboard-"Treiber" und die entsprechenden Befehle unseres OS.

Gehen wir es analytisch Schritt für Schritt durch:

Wir benötigen den Scancode:

Hierfür lesen wir einen Wert von Port 0x60:

unsigned int FetchScancode()
{
    return( inportb(0x60)); // port 0x60: get scan code from the keyboard
}

Wir analysieren den Scancode auf die Stellung der Shift-Taste (Upper oder Lower) und unterdrücken diesen Scancode:

Den Scancode holen wir uns mittels Port 0x64 und 0x60. Diesen müssen wir zunächst in das höchste Bit und die sieben niederwertigen Bits zerlegen.
Das höchste Bit 7 teilt uns nämlich mit, ob die Taste gedrückt (0) oder losgelassen (1) wurde. Dies ist wichtig, um die Shift-Taste sofort nach dem Umsetzen (Drücken oder Loslassen) abzufangen und den Scancode der Shift-Taste bei der Funktions-Rückgabe auszublenden.
Für den Zustand der Shift-Taste verwenden wir eine Variable, die den aktuellen Zustand der Shift-Taste vor dem von der Funktion zurück gelieferten Scancode der gedrückten Taste speichert. Der Zustand der Shift-Taste wird in der Variable ShiftKeyDown und der Scancode in der Variable scancode festgehalten.

unsigned int FetchAndAnalyzeScancode()
{
    unsigned int scancode; // For getting the keyboard raw scancode
    while(1) // Loop until a key to be pressed
    {
        // Wait for the key
        while ( !(inportb(0x64)&1) ); // 0x64: read keyboard µC status register
        scancode = FetchScancode();

        if ( scancode & 0x80 ) // Key released? Check bit 7 (10000000b = 0x80) of scan code for this
        {
            scancode &= 0x7F; // Key was released, compare only low seven bits: 01111111b = 0x7F
            if ( scancode == KRLEFT_SHIFT || scancode == KRRIGHT_SHIFT ) // A key was released, shift key up?
                ShiftKeyDown = 0;    // yes, it is up --> NonShift
            continue;    // Loop
        }

        // Key was pressed. Capture scan code of shift key, if pressed
        if ( scancode == KRLEFT_SHIFT || scancode == KRRIGHT_SHIFT )
        {
            ShiftKeyDown = 1; // It is down, use asciiShift characters
            continue; // Loop, so it will not return a scan code for the shift key
        }
        return scancode;
    }
}


Wir benötigen Keymaps, um ASCII zu erzeugen und den länderspezifischen Tastaturtyp festzulegen:

Die Werte für
KRLEFT_SHIFT bzw. KRRIGHT_SHIFT sind in der Datei keyboard.h mittels #define festgelegt.
Dort befindet sich vor allem das Array, das die entsprechenden ASCII Zeichen enthält. Wir haben eine Keymap für gedrückte und nicht-gedrückte Shift-Taste.

#ifndef KEYBOARD_H
#define KEYBOARD_H

#define NULL 0
#define ESC    27
#define BACKSPACE '\b'
#define TAB       '\t'
#define ENTER     '\n'
#define RETURN    '\r'
#define NEWLINE ENTER

// Non-ASCII special scancodes // Esc in scancode is 1
#define    KESC         1
#define    KF1          0x80
#define    KF2         (KF1 + 1)
#define    KF3         (KF2 + 1)
#define    KF4         (KF3 + 1)
#define    KF5         (KF4 + 1)
#define    KF6         (KF5 + 1)
#define    KF7         (KF6 + 1)
#define    KF8         (KF7 + 1)
#define    KF9         (KF8 + 1)
#define    KF10        (KF9 + 1)
#define    KF11        (KF10 + 1)
#define    KF12        (KF11 + 1)

// Cursor Keys
#define    KINS         0x90
#define    KDEL        (KINS + 1)
#define    KHOME       (KDEL + 1)
#define    KEND        (KHOME + 1)
#define    KPGUP       (KEND + 1)
#define    KPGDN       (KPGUP + 1)
#define    KLEFT       (KPGDN + 1)
#define    KUP         (KLEFT + 1)
#define    KDOWN       (KUP + 1)
#define    KRIGHT      (KDOWN + 1)

// "Meta" keys
#define    KMETA_ALT     0x0200                                // Alt is pressed
#define    KMETA_CTRL    0x0400                                // Ctrl is pressed
#define    KMETA_SHIFT   0x0800                                // Shift is pressed
#define    KMETA_ANY    (KMETA_ALT | KMETA_CTRL | KMETA_SHIFT)
#define    KMETA_CAPS    0x1000                                // CapsLock is on
#define    KMETA_NUM     0x2000                                // NumLock is on
#define    KMETA_SCRL    0x4000                                // ScrollLock is on

// Other keys
#define    KPRNT    ( KRT + 1 )
#define    KPAUSE   ( KPRNT + 1 )
#define    KLWIN    ( KPAUSE + 1 )
#define    KRWIN    ( KLWIN + 1 )
#define    KMENU    ( KRWIN + 1 )

#define    KRLEFT_CTRL        0x1D
#define    KRRIGHT_CTRL       0x1D   

#define    KRLEFT_ALT         0x38
#define    KRRIGHT_ALT        0x38   

#define    KRLEFT_SHIFT       0x2A
#define    KRRIGHT_SHIFT      0x36

#define    KRCAPS_LOCK        0x3A
#define    KRSCROLL_LOCK      0x46
#define    KRNUM_LOCK         0x45
#define    KRDEL              0x53

#define MAXKEYBUFFER 64               // max keyboard buffer

// Keymaps: US International

// Non-Shifted scan codes to ASCII:
static unsigned char asciiNonShift[] = {
NULL, ESC, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', BACKSPACE,
TAB, 'q', 'w',   'e', 'r', 't', 'y', 'u', 'i', 'o', 'p',   '[', ']', ENTER, 0,
'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '`', 0, '\\',
'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', 0, 0, 0, ' ', 0,
KF1, KF2, KF3, KF4, KF5, KF6, KF7, KF8, KF9, KF10, 0, 0,
KHOME, KUP, KPGUP,'-', KLEFT, '5', KRIGHT, '+', KEND, KDOWN, KPGDN, KINS, KDEL, 0, 0, 0, KF11, KF12 };

// Shifted scan codes to ASCII:
static unsigned char asciiShift[] = {
NULL, ESC, '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', BACKSPACE,
TAB, 'Q', 'W',   'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P',   '{', '}', ENTER, 0,
'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '\"', '~', 0, '|',
'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', 0, 0, 0, ' ', 0,
KF1,   KF2, KF3, KF4, KF5, KF6, KF7, KF8, KF9, KF10, 0, 0,
KHOME, KUP, KPGUP, '-', KLEFT, '5',   KRIGHT, '+', KEND, KDOWN, KPGDN, KINS, KDEL, 0, 0, 0, KF11, KF12 };

#endif

 
Um die Nutzung der Funktion k_getch() gemeinsam mit den Hilfsfunktionen k_itoa() und k_i2hex() darzustellen, verwenden wir ein einfaches Programm in main(), das bei einem Tastendruck das ASCII-Zeichen, den Dezimal- und Hexadezimal-ASCII-Wert darstellt:

extern void k_clear_screen();
extern unsigned int k_printf(char* message, unsigned int line, char attribute);
extern void update_cursor(int row, int col);
extern unsigned char const k_getch();
extern void k_itoa(int value, char* valuestring);
extern void k_i2hex(unsigned int val, unsigned char* dest, int len);

int main()
{
    k_clear_screen();

    k_printf("   ************************************************", 0, 0xA);
    k_printf("   *                                              *", 1, 0xA);
    k_printf("   *          Welcome to HenkesSoft OS.           *", 2, 0xA);
    k_printf("   *                                              *", 3, 0xA);
    k_printf("   *        The C kernel has been loaded.         *", 4, 0xA);
    k_printf("   *                                              *", 5, 0xA);
    k_printf("   ************************************************", 6, 0xA);

    update_cursor(8, 0);

    unsigned char KeyGot=0;
    char bufferKEY[10];
    char bufferASCII[10];
    char bufferASCII_hex[10];

    int i;
    for(i=0;i<10000000;++i)
    {
        int j;
        for(j=0;j<1000000;++j)/*do nothing*/;
       
        KeyGot = k_getch();   // port 0x60 -> scancode + shift key -> ASCII
       
        bufferKEY[0] = KeyGot;
        k_itoa(KeyGot,bufferASCII);
        k_i2hex(KeyGot,bufferASCII_hex,2);
        k_clear_screen();
        k_printf(bufferKEY,      0,0xA); // the ASCII character
        k_printf(bufferASCII,    1,0xA); // ASCII decimal
        k_printf(bufferASCII_hex,2,0xA); //
ASCII hexadecimal
    };

    return 0;
};


Unter Bochs konnte ich schneller tippen, als unser durch Warteschleifen und Bochs massiv "ausgebremstes" OS die eingegebenen Zeichen verarbeitet. Damit können Sie fühlbar den Abholmechanismus aus dem
Tastaturpuffer beobachten.


Interrupts und Exceptions

Mit dem Booten hat es angefangen. Dann wurde es im ersten Sektor mit seinen 512 Byte zu eng. Ein Betriebssystemkern (kernel) musste her. Der Real Mode (RM) hatte bei 1 MB ausgedient und musste dem Protected Mode (PM) weichen. Hiermit trat anstelle der realen Adressierung die virtuelle Adressierung, die über Tabellen mit  "Zeigern" oder genauer mit umfangreiche Deskriptoren arbeitet. Hier haben wir die Global Descriptor Table (GDT) kennen gelernt. Der Sprung von Assembler nach C (wir hätten auch C++ einsetzen können) diente lediglich der besseren Lesbarkeit des Sourcecodes. Die Funktionalität des Betriebssystems wurde nun auch durch verschiedene C-Module dargestellt.  Hierzu gehörte die Darstellung von Textzeichen auf dem Bildschirm über den Videospeicher bei 0xB8000 und eine rudimentäre Dateneingabe über die Tastatur. Hierbei wurde ein wesentlicher Unterschied deutlich: Die Ausgabe auf dem Bildschirm ist in der Regie der CPU, aber bei der Tastatureingabe musste sich die CPU zum Affen machen. Nur durch ständiges Nachschauen im "Briefkasten", also im Tastaturpuffer, erhielt die CPU ihre Informationen. Dieses "Polling" (engl. abfragen) ist aber völlig ineffizient, so dass hier die "Klingel" erfunden werden musste.
Hier zur Übersicht die bisherigen Entwicklungsschritte:

- Bootbarer Minikernel
- Bootloader + nachgeladener Kernel
- Auf Instruktionen im RM reagieren (dank BIOS einfache Handhabung)
- A20-Gate und PM aktivieren, GDT/GDTR
- Sprung vom ASM- zum C-Kernel (Lesbarkeit, Module)
- Rudimentäre Textausgabe auf Bildschirm (Video RAM 0xB8000)
- Rudimentäre Texteingabe mit Tastatur ("Polling")

Nun schlagen wir im Protected Mode ein neues Kapitel auf. Die CPU lässt sich von ihrem Umfeld "unterbrechen" um "ungefragt" Informationen entgegen zu nehmen. Dies nennt man "Interrupts". Wir haben diese Technik im Real Mode bereits eingesetzt, aber durch die Umschaltung auf den Protected Mode leider verloren. Nun müssen wir daran gehen, diese Technik erneut aufzubauen.    

Interrupt Requests IRQ

Was wollen wir erreichen? Zunächst wollen wir die Meldungen der die Zentraleinheit umgebenden Hardware erfassen und darauf reagieren. Das können Sie sich wie eine Klingel mit verschiedenen Tönen vorstellen, je nachdem wer anruft. Die klassische "Systemuhr" (system clock) erzeugt standardmäßig Ticks im Abstand von ca. 18,222 Millisekunden und liefert diese Information als IRQ0, das heißt Interrupt Request Nr. 0. Die Tastatur meldet sich bei leerem Tastaturpuffer und Bereitstellung eines neuen Zeichens mit IRQ1. Es geht also darum, nun im Betriebssystem auf die anstehend Interrupts zu reagieren. Es gibt im unteren Bereich eine Normierung bezüglich der IRQ-Nummern ähnlich wie bei den Ports. Nachfolgend finden Sie eine Übersicht:

00 System Clock (Ticks alle 18,2 sec)
01 Tastatur
02 Programmierbarer Interrupt-Controller
03 Serielle Schnittstelle COM2 (E/A-Bereich 0x02F8)
04 Serielle Schnittstelle COM1 (E/A-Bereich 0x03F8)
05 Frei, oft Soundkarte bzw. LPT2
06 Diskettenlaufwerk
07 Parallele Schnittstelle LPT1 (E/A-Bereich 0x0378)
08 Echtzeitsystemuhr
09 Frei
10 Frei
11 Frei
12 PS/2-Mouse
13 Mathematischer Coprozessor
14 Primärer IDE-Kanal
15 Sekundärer IDE-Kanal

Wir führen uns dies im Betriebssystem MS Windows XP an IRQ0, IRQ1, IRQ6 und IRQ12 näher zu Gemüte.
Sie finden diese Informationen im Arbeitsplatz bei dem Gerätemanager:




Man kann Interrupts "remappen", das bedeutet, dass man eine IRQ-Nummer auf eine andere IRQ-Nummer umlenkt.
Üblich ist, dass man im Protected Mode den Bereich von IRQ0 bis IRQ15 dem Bereich von IRQ32 bis IRQ47 zuordnet.
Diese Vorgehensweise werden wir übernehmen (siehe unten).

Exceptions

Neben diesen Hardware-Interrupts gibt es - bedingt durch Programmfehler - sogenannte Exceptions, also "Ausnahmen", die durch Fehler verursacht werden.
Hier zeigen wir einen Ausschnitt aus einer Datei mit Fehlermeldungen, die wir verwenden werden:

/* Message string corresponding to the exception number 0-31: exception_messages[interrupt_number] */
unsigned char* exception_messages[] =
{
    "Division By Zero",        "Debug",                         "Non Maskable Interrupt",    "Breakpoint",
    "Into Detected Overflow",  "Out of Bounds",                 "Invalid Opcode",            "No Coprocessor",
    "Double Fault",            "Coprocessor Segment Overrun",   "Bad TSS",                   "Segment Not Present",
    "Stack Fault",             "General Protection Fault",      "Page Fault",                "Unknown Interrupt",
    "Coprocessor Fault",       "Alignment Check",               "Machine Check",             "Reserved",
    "Reserved",                "Reserved",                      "Reserved",                  "Reserved",
    "Reserved",                "Reserved",                      "Reserved",                  "Reserved",
    "Reserved",                "Reserved",                      "Reserved",                  "Reserved"
};

Wenn wir durch Null dividieren - was uns die Mathematiker verbieten - so sollte eine entsprechende Exception  Nr. 0 bei der CPU anfallen.
Wir werden in unserem OS daraufhin die Message "Division By Zero" ausgeben und geeignet agieren.

Interrupt Description Table (IDT)

Analog zur GDT arbeitet man auch bei der Suche nach einem Handler für einen IRQ mit einer Zeigertabelle auf entsprechende Speicherbereiche.
Dies nennt sich Interrupt Descriptor Table (IDT). Ein sogenanntes Interrupt Descriptor Table Register (IDTR) verweist auf die Basis und das Limit (Größe) dieser Tabelle. Wie ist die IDT nun aufgebaut? Wir benötigen dort 256 Einträge für die maximal möglichen IRQ Nummern. Bei einem IRQ ohne Eintrag in der IDT stürzt die CPU ab. Nun müssen wir noch wissen, wie solch ein Eintrag aussieht. Schauen wir zunächst in den C Sourcecode, bei dem wir uns an folgendem Skript, das sich auch bei anderen Weiterentwicklungen z.B. hier als Grundlage bewährt hat, im Bereich Interrupts und Exceptions orientieren und darauf aufbauen:

// IDT entry
struct idt_entry
{
    unsigned short base_lo;
    unsigned short sel;
    unsigned char always0;
    unsigned char flags;
    unsigned short base_hi;
}__attribute__((packed)); //prevent compiler optimization

struct idt_ptr
{
    unsigned short limit;
    unsigned int base;
}__attribute__((packed)); //prevent compiler optimization

// Declare an IDT of 256 entries and a pointer to the IDT
struct idt_entry idt[256];
struct idt_ptr   idt_register;

idt_register.limit = (sizeof (struct idt_entry) * 256)-1;
idt_register.base  = (unsigned int) &idt;

k_memset(&idt, 0, sizeof(struct idt_entry) * 256); // Clear out the entire IDT

static void idt_load(){ asm volatile("lidt %0" : "=m" (idt_register)); } // load IDT register (IDTR)


Zunächst ein Eintrag im IDT:



Bei den Flags gestaltet sich die Aufteilung analog zum GDT (siehe oben), allerdings ist das Type Flag hier 4 Bit breit:

Bit
Meaning
0
1
7
P (Present Bit) Descriptor is undefined
Descriptor contains a valid base and limit
6
DPL (High)
see below see below
5
DPL (Low)
see below see below
4
S (Segment Bit) System Descriptor
Code, Data or Stack Descriptor
3
Type
see below
see below
2
Type
see below see below
1
Type
see below see below
0
Type see below see below


Die beiden DPL (
Deskriptor Privilege Level) Bits setzen das Privileg-Level:   0 (00b), 1 (01b), 2 (10b) oder 3 (11b)

Type (in Flags)
Bedeutung
0000b
0001b 80286-TSS
0010b Local Descriptor Table (LDT)
0011b aktives 80286-TSS
0100b 80286 Call Gate
0101b Task Gate
0110b 80286 Interrupt Gate
0111b 80286 Trap Gate
1000b reserviert
1001b 80386-TSS
1010b reserviert
1011b aktives 80386-TSS
1100b 80386 Call Gate
1101b reserviert
1110b 80386 Interrupt Gate
1111b 80386 Trap Gate

Wie Sie sehen, ist unser Interrupt Gate Typ, den wir für die IDT benötigen, nur ein Sonderfall von 16 möglichen Gate-Typen.
Der Begriff "Gate", also Tor, ist gut gewählt, denn man muss die richtige Privilegstufe mitbringen, um auf einen Bereich direkt zugreifen zu können.
Die Flags DPL beschränken den Zugriff auf den Deskriptor. Das Gate muss folglich eine geringere bzw. die gleiche Privilegstufe wie der Aufrufer (Programm oder Task) besitzen. Bei geringerer Privilegstufe, reagiert der Prozessor schon bei dem Versuch, das "höherrangige" Gate zu benutzen, mit einer Exception.

Der Typ-Wert 1110b ist 0x0E oder 14.

Das Flag P besitzt die gleiche Funktion wie die entsprechenden Felder in der GDT.
Ist das "Präsenz-Bit" gesetzt, befindet sich das beschriebene Segment aktuell im Speicher.

Die folgende Inline-Assembler-Funktion in C entspricht in Assembler (Intel-Syntax)  lidt [_idtr]

static void idt_load(){ asm volatile("lidt %0" : "=m" (idt_register)); } // load IDT register (IDTR)

und lädt die Basisadresse und die Größe (Bytes) in das entsprechende IDT Register (IDTR) der CPU.

Beim Initialisieren der IDT werden 256 Einträge vorgenommen und zunächst komplett alle Werte auf 0 gesetzt.

Wir benötigen nun die nachfolgende Funktion, um einen Eintrag in der IDT entsprechend den eigegebenen Parametern einzufügen:

// Put an entry into the IDT
void idt_set_gate(unsigned char num, unsigned long base, unsigned short sel, unsigned char flags)
{
    idt[num].base_lo = (base        & 0xFFFF);
    idt[num].base_hi = (base >> 16) & 0xFFFF;
    idt[num].sel     =   sel;
    idt[num].always0 =     0;
    idt[num].flags   = flags;
}


Wir bauen uns ein Array auf, um 16 eigenen IRQ-Handlern, also Behandlungsroutinen im Falle des Auftretens eines IRQ, eine Funktion über einen Funktionszeiger zuordnen zu können.

/* Array of function pointers handling custom IRQ handlers for a given IRQ */
void* irq_routines[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

/* Implement a custom IRQ handler for the given IRQ */
void irq_install_handler(int irq, void (*handler)(struct regs* r)) {irq_routines[irq] = handler;}

/* Clear the custom IRQ handler */
void irq_uninstall_handler(int irq) {irq_routines[irq] = 0;}



Remapping IRQ 0-15 ==> IDT-Einträge 32-47


Zunächst die Verschaltung der beiden PIC mit der CPU:

Am Anfang gab es nur einen PIC mit acht Eingängen. Später wurde ein zweiter PIC hinzu gefügt, der mit Eingang 2 des ersten PIC fest verbunden wurde. Heute wird dies nicht mehr so aufgebaut, aber die Methodik ist aus Kompatibilitätsgründen die gleiche geblieben.

Diese beiden PIC haben nun jeweils einen eigenen Port:



Command
Data        
PIC1
Master
0x20
0x21
PIC2 Slave
0xA0
0xA1

Im Real Mode werden IRQ 0-15 automatisch "remappt", nämlich wie folgt:
Master PIC 
0 to 7 0x08 - 0x0F 
Slave  PIC 8 to 15 
0x70 - 0x77
Dort passt dies gut zu den BIOS defaults.
Im Protected Mode stört die Überlappung der IRQ 0-7 mit möglichen Software-Interrupts.
Daher wird alles um 32 Einträge weiter geschoben.

Beim Remapping verwenden wir die oben aufgeführten Command und Data Ports:
Der Data-Offset des Master PIC wird auf  0x20 (32) und der des Slave PIC auf  0x28 (40) gesetzt.
Das führt zu dem Remapping-Vorgang.

/* Remap: IRQ0 to IRQ15 have to be remapped to IDT entries 32 to 47 */
void irq_remap(void)
{
    // starts the initialization sequence
        outportb(0x20, 0x11);
    outportb(0xA0, 0x11);
    

    // define the PIC vectors
   
    outportb(0x21, 0x20);
    outportb(0xA1, 0x28);
   
    // continue initialization sequence
    
    outportb(0x21, 0x04);

    outportb(0xA1, 0x02);
    outportb(0x21, 0x01);
    outportb(0xA1, 0x01);
    outportb(0x21, 0x00);
    outportb(0xA1, 0x00);
    
// Weitere Details zur Initialisierung findet man
hier

Nun setzen wir unsere Funktion

idt_set_gate(
unsigned char num, unsigned long base, unsigned short sel, unsigned char flags)

zum Einfügen/Verändern von IDT-Einträgen für diese "umgebogenen" Interrupts ein, damit diese IRQs einer Handler-Funktion zugeführt werden können:

/* After remap of the interrupt controllers the appropriate ISRs are connected to the correct entries in the IDT. */
void irq_install()
{
    irq_remap();
    idt_set_gate(32, (unsigned) irq0,  0x08, 0x8E);   idt_set_gate(33, (unsigned) irq1,  0x08, 0x8E);
    idt_set_gate(34, (unsigned) irq2,  0x08, 0x8E);   idt_set_gate(35, (unsigned) irq3,  0x08, 0x8E);
    idt_set_gate(36, (unsigned) irq4,  0x08, 0x8E);   idt_set_gate(37, (unsigned) irq5,  0x08, 0x8E);
    idt_set_gate(38, (unsigned) irq6,  0x08, 0x8E);   idt_set_gate(39, (unsigned) irq7,  0x08, 0x8E);
    idt_set_gate(40, (unsigned) irq8,  0x08, 0x8E);   idt_set_gate(41, (unsigned) irq9,  0x08, 0x8E);
    idt_set_gate(42, (unsigned) irq10, 0x08, 0x8E);   idt_set_gate(43, (unsigned) irq11, 0x08, 0x8E);
    idt_set_gate(44, (unsigned) irq12, 0x08, 0x8E);   idt_set_gate(45, (unsigned) irq13, 0x08, 0x8E);
    idt_set_gate(46, (unsigned) irq14, 0x08, 0x8E);   idt_set_gate(47, (unsigned) irq15, 0x08, 0x8E);
}


Der Codesegment-Selektor ist 0x08. Das Flag 0x8E kennzeichnet unseren IDT-Eintrag und setzt sich aus dem höherwertigen Nibble 1000b (0x8) (Präsenzbit: 1b, Privileg: 00b, System-Bit: 0b) und dem Typ-Nibble 1110b (0xE) für "80386 Interrupt Gate" zusammen.
 

IRQ Handler

Die eigentliche Verknüpfung des IRQ mit dem Handler für den Interrupt sowie die Rücksetzung des Master PIC bzw. Master und Slave PIC erfolgen in dieser Funktion:

/*  EOI command to the controllers. If you don't send them, any more IRQs cannot be raised */
void irq_handler(struct regs* r)
{
    /* This is a blank function pointer */
    void (*handler)(struct regs* r);

    /* Find out if we have a custom handler to run for this IRQ, and then finally, run it */
    handler = irq_routines[r->int_no - 32];
    if (handler) { handler(r); }

    /* If the IDT entry that was invoked was greater than 40 (IRQ8 - 15),
    *  then we need to send an EOI to the slave controller */
    if (r->int_no >= 40) { outportb(0xA0, 0x20); }

    /* In either case, we need to send an EOI to the master interrupt controller too */
    outportb(0x20, 0x20);
}

In der Datei isr.asm finden sich noch einige Hilfsfunktionen in Assembler zum Thema Interrupt Service Routine (ISR).

; Interrupt Service Routine isr0 ... isr32 
global _isr0
...
global _isr31

;  0: Divide By Zero Exception
_isr0:
    cli
    push byte 0
    push byte 0
    jmp isr_common_stub

...

; 31: Reserved
_isr31:
    cli
    push byte 0
    push byte 31
    jmp isr_common_stub

; Call of the C function fault_handler(...)
extern _fault_handler

; Common ISR stub saves processor state, sets up for kernel mode segments,
; calls the C-level fault handler,
and finally restores the stack frame.
isr_common_stub:
    pusha
    push ds
    push es
    push fs
    push gs
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov eax, esp
    push eax
    mov eax, _fault_handler
    call eax
    pop eax
    pop gs
    pop fs
    pop es
    pop ds
    popa
    add esp, 8
    iret

global _irq0
...
global _irq15

; 32: IRQ0
_irq0:
    cli
    push byte 0
    push byte 32
    jmp irq_common_stub

...

; 47: IRQ15
_irq15:
    cli
    push byte 0
    push byte 47
    jmp irq_common_stub

extern _irq_handler

irq_common_stub:
    pusha
    push ds
    push es
    push fs
    push gs
    mov ax, 0x10
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov eax, esp
    push eax
    mov eax, _irq_handler
    call eax
    pop eax
    pop gs
    pop fs
    pop es
    pop ds
    popa
    add esp, 8
    iret




System Clock / Programmable Interval Timer (PIT)

Zeit und Raum zu beherrschen, das ist ein wichtiges Ziel, auch beim Betriebssystem eines PC.

Die Dimension "Raum" bedeutet für uns, die an die CPU angeschlossene Hardware zu beherrschen und Speicherplatz im vergänglichen RAM und später auf  Langzeit-Datenträgern zu managen. Ausprägungen hiervon sind Code und Daten im Speicher. Die Ausgabe von Zeichen auf dem Bildschirm erfolgt beispielsweise durch Manipulation des Video RAM.

Die Dimension "Zeit" benötigt einen periodischen Taktgeber ("ticks"). Seit den Tagen des IBM-kompatiblen PC wird ein 1,193182 MHz Quarzoszillator (1/3 des NTSC color burst) als Quelle verwendet.

Praktisch stehen uns abgeleitet aus dieser Schwingungsquelle drei "Timer" zur Verfügung:
Standardiisert zählt der Timer von 65535 nach 0 (16 Bit) und beginnt beim nachfolgenden Überlauf von vorne. Hierbei sendet der Programmable Interval Timer (PIT) den Timer-Interrupt IRQ0 zum Prozessor. Die Frequenz ergibt sich zu 1193182 Hz / 65536 = 18,2065 Hz. Es kommt auf diese Art ein Intervall zwischen zwei "ticks" von 1000 Millisekunden  / 18,2065 = 54,9254 Millisekunden zustande. Man erhält also vom Timer 0 ca 55 mal den IRQ0 pro Sekunde.

Diesen periodischen IRQ0 können wir für zeitgesteuerte Aktionen einsetzen. Im Sourcecode findet sich z.B. die Ausgabe von timer_ticks und eticks (expected ticks) auf dem Bildschirm. Timer 0 erhöht die Variable timer_ticks alle 18,2065 sec, während die Variable eticks von einem gesetzten Ausgangswert bis zur Null abwärts gezählt werden. Die Variable eticks wird zur Zeit von den Funktionen timer_wait(...) und sleepSeconds(...) als Counter-Variable verwendet.

Sourcecode von timer.c:
#include "os.h"

unsigned long timer_ticks = 0;
unsigned long eticks;

void timer_handler(struct regs* r)
{
    ++timer_ticks;
    if (eticks)
        --eticks;

    //TEST
    char bufferTimerticks[20];           
    k_itoa (timer_ticks, bufferTimerticks);

    k_printf("             ",  6, 0x0B); 
    k_printf(bufferTimerticks, 6, 0x0B);

    char bufferWaitticks[20];              
    k_itoa (eticks, bufferWaitticks);

    k_printf("             ",  7, 0x0B); 
    k_printf(bufferWaitticks,  7, 0x0B);

    //TEST
}

void timer_wait (unsigned long ticks)
{
    timer_uninstall();
    eticks = ticks;
    timer_install();

    // busy wait...
    while (eticks)
    {
        k_printf("waiting time runs",   8, 0x0B);
        /* do nothing */;
    };
    k_printf("waiting time has passed", 9, 0x0B);
}

void sleepSeconds (unsigned long seconds)
{
    // based upon timer tick frequence of 18.222 Hz
    timer_wait((unsigned long)18.222*seconds);
}

void timer_install()
{
    /* Enable 'timer_handler' by IRQ0 */
    irq_install_handler(0, timer_handler);
}

void timer_uninstall()
{
    /* Disable 'timer_handler' by IRQ0 */
    irq_uninstall_handler(0);
}


Keyboardtreiber wird von Polling auf IRQ1 weiter entwickelt

Der bisher zu Demonstrationszwecken eingesetzte Keyboardtreiber ist für den IRQ1 noch nicht ausreichend brauchbar.
Folgende Änderungen werden daher in der Funktion
FetchAndAnalyzeScancode() im Modul keyboard.c durchgeführt:

#include "keyboard.h"
#include "os.h"

int ShiftKeyDown; // Variable for Shift Key Down

/* Wait until buffer is empty */
void keyboard_init()
{
    while (inportb(0x64)&1)
      inportb(0x60);
};

unsigned int FetchAndAnalyzeScancode()
{
    unsigned int scancode; // For getting the keyboard scancode
    while(TRUE) // Loop until a key (w/o shift key) has been pressed
    {
        scancode = inportb(0x60);   // 0x60: get scan code from the keyboard

        // ACK: toggle bit 7 at port 0x61
        unsigned char port_value = inportb(0x61);
        outportb(0x61,port_value |  0x80); // 0->1
        outportb(0x61,port_value &~ 0x80); // 1->0

        if ( scancode & 0x80 ) // Key released? Check bit 7 (10000000b = 0x80) of scan code for this
        {
            scancode &= 0x7F; // Key was released, compare only low seven bits: 01111111b = 0x7F
            if ( scancode == KRLEFT_SHIFT || scancode == KRRIGHT_SHIFT ) // A key was released, shift key up?
            {
                ShiftKeyDown = 0;    // yes, it is up --> NonShift
            }
        }
        else // Key was pressed
        {
            // Capture scan code of shift key, if pressed
            if ( scancode == KRLEFT_SHIFT || scancode == KRRIGHT_SHIFT )
            {
                ShiftKeyDown = 1; // It is down, use asciiShift characters
                continue; // Loop, so it will not return a scan code for the shift key
            }
        }
        break; // Leave the loop
    }
    return scancode;
}




Das OS-Projekt erhält einen Codenamen: PrettyOS

Operating Sytems erhalten während der Entwicklungsphase zumeist einen Codennamen.
Ich "taufe" unser Entwicklungsprojekt hiermit auf den Namen " PrettyOS ".



Modul video.c wird ausgebaut

Ein Schwachpunkt ist unsere Bildschirmausgabe. Unser rudimentäres k_printf(...) und die Positionierung des Cursors funktioniert ja bereits, aber wir wünschen uns mehr, z.B. will man auch einzelne Character an einen beliebigen Punkt P(x,y) ausgeben können. Daher führen wir nun ein "brush-up" dieses Moduls durch.

Wir führen globale Variablen für die "Character Attributes", die aktuelle und gespeicherte Bildschirmposition P(x,y) sowie einige Funktionen zum Positionieren des Cursors ein.

Es gibt Funktionen für die Ausgabe von Characters bzw. Strings und das Setzen der Character Attributes. Wir haben eine "lean"-Version (gegenüber der echten C-Variante ) von printf(...), die auch mit NUL-terminierten Strings arbeiten kann und beispielhaft die Formate %X, %d bzw. %i, %c und %s unterstützt. Für diese Funktion benötigen wir den Standard-Header <stdarg.h>, der es einer Funktion erlaubt, eine unbestimmte Anzahl an Argumenten zu übernehmen. Daher können wir in printformat(...) nun mit  va_list, va_start, va_arg etc. arbeiten.

#include "os.h"
#include <stdarg.h>

UCHAR  csr_x  = 0;
UCHAR  csr_y  = 0;
UCHAR  saved_csr_x  = 0;
UCHAR  saved_csr_y  = 0;
UCHAR  attrib = 0x0F;
USHORT* vidmem = (USHORT*) 0xb8000;

void k_clear_screen()
{
    k_memsetw (vidmem, 0x20 | (attrib << 8), 80 * 25);
    csr_x = 0; csr_y = 0; update_cursor();
}

void settextcolor(UCHAR forecolor, UCHAR backcolor)
{
    // Top 4 bytes: background, bottom 4 bytes: foreground color
    attrib = (backcolor << 4) | (forecolor & 0x0F);
}

void move_cursor_right()
{
    ++csr_x;
    if(csr_x>=80)
    {
      ++csr_y;
      csr_x=0;
    }
}

void move_cursor_left()
{
    if(csr_x)
        --csr_x;
    if(!csr_x && csr_y>0)
    {
        csr_x=79;
        --csr_y;
    }
}

void move_cursor_home()
{
    csr_x = 0; update_cursor();
}

void move_cursor_end()
{
    csr_x = 79; update_cursor();
}

void set_cursor(UCHAR x, UCHAR y)
{
    csr_x = x; csr_y = y; update_cursor();
}

void update_cursor()
{
    USHORT position = csr_y * 80 + csr_x;
    // cursor HIGH port to vga INDEX register
    outportb(0x3D4, 0x0E);
    outportb(0x3D5, (UCHAR)((position>>8)&0xFF));
    // cursor LOW port to vga INDEX register
    outportb(0x3D4, 0x0F);
    outportb(0x3D5, (UCHAR)(position&0xFF));
};

void putch(UCHAR c)
{
    USHORT* pos;
    UINT att = attrib << 8;

    if(c == 0x08) // backspace: move the cursor back one space and delete
    {
        if(csr_x)
        {
            --csr_x;
            putch(' ');
            --csr_x;
        }
        if(!csr_x && csr_y>0)
        {
            csr_x=79;
            --csr_y;
            putch(' ');
            csr_x=79;
            --csr_y;
        }
    }
    else if(c == 0x09) // tab: increment csr_x (divisible by 8)
    {
        csr_x = (csr_x + 8) & ~(8 - 1);
    }
    else if(c == '\r') // cr: cursor back to the margin
    {
        csr_x = 0;
    }
    else if(c == '\n') // newline: like 'cr': cursor to the margin and increment csr_y
    {
        csr_x = 0; ++csr_y;
    }
    /* Any character greater than and including a space, is a printable character.
    *  Index = [(y * width) + x] */
    else if(c >= ' ')
    {
        pos = vidmem + (csr_y * 80 + csr_x);
        *pos = c | att; // Character AND attributes: color
        ++csr_x;
    }

    if(csr_x >= 80) // cursor reaches edge of the screen's width, a new line is inserted
    {
        csr_x = 0;
        ++csr_y;
    }

    /* Scroll the screen if needed, and finally move the cursor */
    scroll();
    update_cursor();
}

void puts(UCHAR* text)
{
    for(; *text; putch(*text), ++text);
}

void scroll()
{
    UINT blank, temp;
    blank = 0x20 | (attrib << 8);
    if(csr_y >= 25)
    {
        temp = csr_y - 25 + 1;
        k_memcpy (vidmem, vidmem + temp * 80, (25 - temp) * 80 * 2);
        k_memsetw (vidmem + (25 - temp) * 80, blank, 80);
        csr_y = 25 - 1;
    }
}

void k_printf(UCHAR* message, UINT line, UCHAR attribute)
{
    // Top 4 bytes: background, bottom 4 bytes: foreground color
    settextcolor(attribute & 0x0F, attribute >> 4);
    csr_x = 0; csr_y = line;
    update_cursor();
    puts(message);
};

/* Lean version of printf: printformat(...): supports %u, %d, %x/%X, %s, %c */
void printformat (char *args, ...)
{
    va_list ap;
    va_start (ap, args);
    int index = 0, d;
    UINT u;
    char c, *s;
    char buffer[100];

    while (args[index])
    {
        switch (args[index])
        {
        case '%':
            ++index;
            switch (args[index])
            {
            case 'u':
                u = va_arg (ap, UINT);
                k_itoa(u, buffer);
                puts(buffer);
                break;
            case 'd':
            case 'i':
                d = va_arg (ap, int);
                k_itoa(d, buffer);
                puts(buffer);
                break;
            case 'X':
            case 'x':
                d = va_arg (ap, int);
                k_i2hex(d, buffer,8);
                puts(buffer);
                break;
            case 's':
                s = va_arg (ap, char*);
                puts(s);
                break;
            case 'c':
                c = (char) va_arg (ap, int);
                putch(c);
                break;
            default:
                putch('%');
                putch('%');
                break;
            }
            break;

        default:
            putch(args[index]); //printf("%c",*(args+index));
            break;
        }
        ++index;
    }
}

void save_cursor()
{
    cli();
    saved_csr_x  = csr_x;
    saved_csr_y  = csr_y;
    sti();
}

void restore_cursor()
{
    cli();
    csr_x  = saved_csr_x;
    csr_y  = saved_csr_y;
    sti();
}


Modul keyboard.c wird ausgebaut

Der Keyboard-Treiber wird ausgebaut, um Texte von links nach rechts eingeben zu können. Sondertasten wurden nun auch in Ansätzen berücksichtigt.
Wir gehen nun einem Durchgang durch die Funktion FetchAndAnalyzeScancode(). Dies verlagert die Reaktion auf den Scancode nach k_getch(). Dort wird untersucht, ob es sich das Loslassen oder Drücken einer Taste handelt. Nur beim Drücken einer Taste wird der Zeichencode zurück gegeben. Die Shift-Taste wird ausgefiltert.

#include "keyboard.h"
#include "os.h"

UCHAR ShiftKeyDown = 0; // Variable for Shift Key Down
UCHAR KeyPressed   = 0; // Variable for Key Pressed
UCHAR scan         = 0; // Scan code from Keyboard

/* Wait until buffer is empty */
void keyboard_init()
{
    while( inportb(0x64)&1 )
        inportb(0x60);
};

UCHAR FetchAndAnalyzeScancode()
{
    if( inportb(0x64)&1 )
        scan = inportb(0x60);   // 0x60: get scan code from the keyboard

    // ACK: toggle bit 7 at port 0x61
    UCHAR port_value = inportb(0x61);
    outportb(0x61,port_value |  0x80); // 0->1
    outportb(0x61,port_value &~ 0x80); // 1->0

    if( scan & 0x80 ) // Key released? Check bit 7 (10000000b = 0x80) of scan code for this
    {
        KeyPressed = 0;
        scan &= 0x7F; // Key was released, compare only low seven bits: 01111111b = 0x7F
        if( scan == KRLEFT_SHIFT || scan == KRRIGHT_SHIFT ) // A key was released, shift key up?
        {
            ShiftKeyDown = 0;    // yes, it is up --> NonShift
        }
    }
    else // Key was pressed
    {
        KeyPressed = 1;
        if( scan == KRLEFT_SHIFT || scan == KRRIGHT_SHIFT )
        {
            ShiftKeyDown = 1; // It is down, use asciiShift characters
        }
    }
    return scan;
}

UCHAR k_getch() // Scancode --> ASCII
{
    UCHAR retchar;                       // The character that returns the scan code to ASCII code
    scan = FetchAndAnalyzeScancode();    // Grab scancode, and get the position of the shift key

    if( ShiftKeyDown )
        retchar = asciiShift[scan];      // (Upper) Shift Codes
    else
        retchar = asciiNonShift[scan];   // (Lower) Non-Shift Codes

    if( ( !(scan == KRLEFT_SHIFT || scan == KRRIGHT_SHIFT) ) && ( KeyPressed == 1 ) ) //filter Shift Key and Key Release
        return retchar; // ASCII version
    else
        return 0;
}

void keyboard_handler(struct regs* r)
{
   UCHAR KEY;
   KEY = k_getch();
   restore_cursor();
   switch(KEY)
   {
       case KINS:
           break;
       case KDEL:
           move_cursor_right();
           putch('\b'); //BACKSPACE
           break;
       case KHOME:
           move_cursor_home();
           break;
       case KEND:
           move_cursor_end();
           break;
       case KPGUP:
           break;
       case KPGDN:
           break;
       case KLEFT:
           move_cursor_left();
           break;
       case KUP:
           break;
       case KDOWN:
           break;
       case KRIGHT:
           move_cursor_right();
           break;
       default:
           printformat("%c",KEY); // the ASCII character
           break;
   }
   save_cursor();
}

void keyboard_install()
{
    /* Installs 'keyboard_handler' to IRQ1 */
    irq_install_handler(1, keyboard_handler);
    keyboard_init();
}



Frequenz des System Timer verändern

Man kann die Frequenz des System Timer selbst in gewissen Grenzen einstellen. Dies erlaubt die nachstehende Funktion systemTimer_setFrequency( ULONG freq ). Wir entscheiden uns im Modul für feste 100 Hz (daher die Funktion als static, also nur innerhalb des Moduls gültig), erhalten also in regelmäßigen Abständen von jeweils 10 Millisekunden einen "Tick", oder genauer gesagt einen IRQ0.

Via Port 0x43 senden wir unser "Kommando" und über Port 0x40 stellen wir den Teiler (Divisor) ein. Man kann dies fest einstellen bzw. eine globale Frequenzvariable hinterlegen. In unserem Fall wurde die Frequenz fest eingestellt.

#include "os.h"

ULONG const FREQ  = 100; // 100 "ticks" per second
ULONG timer_ticks =   0;
ULONG eticks;

void timer_handler(struct regs* r)
{
    ++timer_ticks;
    if (eticks)
        --eticks;
}

void timer_wait (ULONG ticks)
{
    timer_uninstall();
    eticks = ticks;
    timer_install();

    // busy wait...
    while (eticks)
    {
        update_cursor();
    }
}


void sleepSeconds (ULONG seconds)

{
    timer_wait(FREQ*seconds);
}

void sleepMilliSeconds (ULONG ms)
{
    timer_wait(FREQ * ms/1000UL);
}

static void systemTimer_setFrequency( ULONG freq )
{
    ULONG divisor = 1193180 / freq; //divisor must fit into 16 bits

    // Send the command byte
    outportb(0x43, 0x36);

    // Send divisor
    outportb(0x40, (UCHAR)(  divisor     & 0xFF )); // low  byte
    outportb(0x40, (UCHAR)( (divisor>>8) & 0xFF )); // high byte
}

void timer_install()
{
    /* Installs 'timer_handler' to IRQ0 */
    irq_install_handler(0, timer_handler);
    systemTimer_setFrequency( FREQ ); // FREQ Hz, meaning a tick every 1000/FREQ milliseconds
}

void timer_uninstall()
{
    /* Uninstalls IRQ0 */
    irq_uninstall_handler(0);
}



Operating System Design - Was soll unser OS können und was nicht?

Unser PrettyOS wirkt bisher eher wie eine elektronische Schreibmaschine auf dem PC, wobei der Kontakt zum Drucker und zu dauerhaften Speichermedien, z.B. Floppy Disk, Festplatte oder USB-Stick noch fehlen. Im Überblick sieht man das Zusammenspiel von CPU, RAM, video.c und keyboard.c, wobei die Interrupt-Steuerung und der System-Timer nicht dargestellt wurden. Die CPU befindet sich im Protected Mode, und der Speicher wird über die Zeigertabellen GDT und IDT verwaltet. Das Modul video.c bewirkt die Ansteuerung des Monitors über die Speicheradresse 0xB8000, und das Modul keyboard.c verarbeitet im Zusammenspiel mit IRQ1 ("Zeichen steht bereit") und Keymap ("Welches Zeichen gehört zu welcher Taste?") die Auswertung des Scancodes der Tastatur.



Ausblick

Nachdem wir nun die grundlegenden Interaktionen - nämlich Daten über das Keyboard eingeben und am Bildschirm im Textmodus farbig ausgeben - sowie die Reaktion auf Interrupts (Beispiele IRQ0 und IRQ1) und Exceptions im ersten Ansatz minimalistisch erreicht haben, ist es an der Zeit, weitere notwendige Techniken wie Speichermanagement (Paging, Heap, Virtual File System, Ram Disk), Prozessmanagement (Multitasking) und Systemaufrufe seitens User-Applikationen grundsätzlich kennen zu lernen. Dieser Themenkreis wird in Teil 2 dieses Tutorials behandelt.


weiter zu:   Teil 2