Stand 17.03.2013 - Dr. Erhard Henkes
Teil 1 Teil 2
Teil 3
Inhaltsübersicht
Am Ende des dritten
Teils wies
ich auf das von mir ins Leben gerufene Projekt
"OS-Development" hin. Der Sinn der "Community", die sich um das Entwickler-Team
gebildet
hat, und die wesentlichen Inhalte von PrettyOS,
der
Gegenstand
des
Projektes, sollen in diesem Teil näher beschrieben
werden.
Was unterscheidet das Entwickeln in einer Gruppe von der Arbeit
alleine?
Zuallererst gibt es da die Meinungs- und Ideenvielfalt. Das ist sowohl
bezüglich Tools, Kommunikationswege und Design des OS ein nicht zu
unterschätzender Aufwand. Wir haben den Vorzug, bei c-plusplus
ein Subforum
für OS-Development betreiben zu dürfen.
Dafür bin ich
Marcus Bäckmann
sehr dankbar. Zusätzlich legte ich einen chat-Kanal
#PrettyOS an bei irc.euirc.net,
damit Interessierte real-time
kommunizieren zu
können. Ebenfalls wichtig für die gemeinsame Arbeit am Sourcecode
ist ein Repositorium.
Wir entschieden uns für SVN auf sourceforge.net.
Dort sind auch Bug- und Request-Tracker,
die wir
nutzen.
Ansonsten bietet sicj unter MS Windows das
Projektmanagement (zur Verwaltung der Files) des kostenfreien
MSVC++
Express 2010 an. Die Projektfiles findet man im "Repo".
Forum, Chat, Repo, Projektverwaltung,
damit kann
man ganz brauchbar loslegen. Wir verzichteten auf ein eigenes Wiki, da es davon bereits genügend gibt, an
denen man sich
beteiligen kann.
Das OS befindet sich in ständiger Entwicklung. Nachfolgend werden die wesentlichen Elemente dargestellt, die man zum Verständnis der Zusammenhänge und Abläufe in PrettyOS benötigt. Einen allgemeinen Überblick findet man hier.
Entwickler "Badestrand" hat unser Paging
und Heap Modul komplett überarbeitet. Die
Beschreibung findet man hier.
Die Aufteilung und das Management des Speichers ist eine wichtige
Angelegenheit. Wir haben uns prinzipiell für folgenden Aufbau
entschieden:
0 MB -
16
MB For DMA only.
Kernel code resides somewhere in between
16 MB - 20 MB For placement allocation while
the
heap is not set up.
Here
resides:
- "Physical" bit table
(128 KB)
- Kernel page directory
(circa 8 KB)
- Kernel's page tables for 0 MB - 20 MB (20
KB)
- Kernel's page tables for 3 GB - 4 GB
(1024
KB)
- Heap's "region" list (nongrowable)
(Remaining)
20 MB - 3 GB Intended for user programs'
code,
stack, heap
3 GB - 0xFFF00000 For the kernel heap only; malloc'd memory resides here
0xFFF00000 - 4 GB Memory mapped stuff, e.g. for the PCI
area
Der Kernel befindet
sich momentan bei
0x100000 (früher bei
0x40000) und hat eine Größe,
die
man
am
besten aktuell
aus kernel.map
ablesen kann.
Dieser
Datei-Typ "map" verschafft einen
hervorragenden Überblick über das aus dem Sourcecode
entstandene binäre Konstrukt. Wir verwenden es momentan für den Bootloader Stage 2,
den Kernel und User-Programme.
Erstellt wird eine Map-Datei mittels Parameter -Map im makefile:
bootloader stage
2
(Assembler NASM):
[map
symbols boot2.map]
kernel:
$(LD) $(LDFLAGS) $(addprefix
$(OBJDIR)/,$(KERNEL_OBJECTS)) -T $(KERNELDIR)/kernel.ld
-Map $(KERNELDIR)/kernel.map
-o $(KERNELDIR)/KERNEL.BIN
user:
$(LD) *.o -T $(USERTOOLS)/user.ld -Map user.map
$(LDFLAGS)
-o HELLO.ELF
Beispielhaft zeige ich hier die Map-Datei
des Kernels in gekürzter Form:
Memory
Configuration |
... und zum Vergleich die gekürtzte Map-Datei eines User-Programms:
Memory
Configuration |
... sowie die gekürtzte Map-Datei
des Bootloaders Stage
2,
die von NASM erzeugt wurde:
-
NASM Map file
--------------------------------------------------------------- |
Diese Map-Dateien sind für die Überprüfung
des
Aufbaus und für die Orientierung hilfreich, beispielsweise wenn man bei
einem
Debugger einen Haltepunkt einstellen will.
Auch zur Deutung von Adressen in Fehlermeldungen (z.B. #PF) sind diese
Tabellen
wichtig.
Die beiden Markierungen
0x00100000
__kernel_beg = .
0x00122524
__kernel_end = .
zeigen uns den Umfang des Kernels im
Speicher. Er
belegt dort 22524h = 140580 Byte
Die Shell wird übrigens hier "im Bauch des Kernels"
in
den
Speicher
und von dort in die zu erstellende RAMDisk
transportiert:
.text
0x00111000
0x3f1d
object_files/kernel/data.o
0x00111000
_file_data_start
0x00114f1d
_file_data_end
Den Sourcecode findet man in data.asm:
; data for ramdisk |
Der Heap
verwendet sogenannte Regions, die eine
Adresse, einen
Reservierungsstatus und eine Größe besitzen. Zusätzlich Haben wir die regions Struktur noch mit einer laufenden
Nummerierung und
- das ist ausgefallen - mit einem string
versehen,
der die Verwendung beschreibt.
Damit kann man auf einfache Weise einen Heap-Logger
erstellen, der jederzeit über die aktuelle detaillierte Verwendung des Heaps Auskunft erteilt.
Hier ein Beispiel für die Verwendung unserers
ausgefallenen didaktischen mallocs mit
einem String,
der die Anwendung kurz erläutert:
floppy_t*
fdd = malloc(sizeof(floppy_t),
0, "flpydsk-FDD");
Diese Strings kann man auch während der Laufzeit für die Diagnose von malloc und free
verwenden, sodass
man damit memory leaks
kraftvoll begegnen kann.
Bootloader
Stage 1 (BL1), der im Startsektor einer
Partition
residiert, wird vom BIOS per Boot-Signatur gefunden und gestartet. BL1
verwendet einen kleinen FAT12-Treiber, um BL2 namens boot2.bin im
FAT12-Filesystem zu finden und zu laden. BL2 bewirkt den gleichen
Vorgang, nur
diesmal mit der Datei kernel.bin. Als
Boot-Medium
kann man eine Diskette oder ein usb Mass Storage Device
(z.B. usb-stick) mit FAT12 Filesystem
verwenden. Die Herstellung eine solchen usb-Mediums
wird hier
beschrieben.
BL2 kopiert zunächst im Real Mode den Kernel
nach
0x3000. Dieser wird im Protected Mode nach
0x100000
umkopiert. Die Memory Maps
(siehe GetMemoryMap.inc, INT 0x15, eax = 0xE820) werden im Real Mode an
Speicherstelle 0x1000
übergeben. Dort holt sie der Kernel später
ab.
paging.h:
#define MEMORY_MAP_ADDRESS 0x1000
paging.c, phys_init():
mem_map_entry_t* const
entries = (mem_map_entry_t*)MEMORY_MAP_ADDRESS;
Wie oben gezeigt transportieren wir die "shell"
(shell.elf, eingebunden in initrd.dat)
via incbin in den kernel.
Sie
landet
in
der RAMDisk und wird von dort
"geladen":
ckernel.c:
if (strcmp(node->name, "shell")
==
0)
{
shell_found = true;
if (!elf_exec(buf, sz, "Shell"))
printf("Cannot
start shell!\n");
}
elf_exec
ist eine Funktion, die die ELF-Datei parst
und das Programm an den
vorgesehen
Speicherort (0x1400000) transportiert.
Der Speicherort für den Code-Start wird im Linker-Script
user.ld eingestellt:
ENTRY(_start)
OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386)
SECTIONS
{
. =
0x1400000;
.text : { __code_start
= .;
*(.text*) }
.data : { __data_start = .; *(.data)
}
.rodata : { __rodata_start
= .; *(.rodata)
}
.bss :
{ __bss_start = .; *(.bss)
*(COMMON) }
__end = .;
}
Der Aufbau der ELF-Datei ist hier
beschrieben.
Nachdem die Shell geladen wurde, können wir durch Eingabe von
Pfad-Programm-Kombinationen ein Programm von einem Medium laden und
starten.
Bei der Pfadangabe muss man den Aufbau, der in PrettyOS
Verwendung findet, kennen:
Available
ports:
Type Number
Name Inserted disk
----------------------------------------------------------------------
FDD A
Floppy Dev
1
PRETTYOS
FDD B
Floppy Dev
2
RAM C
RAM
RAMDisk
----------------------------------------------------------------------
Attached disks:
Type Number
Name
Part. Serial
----------------------------------------------------------------------
Floppy 1
PRETTYOS
0
PRETTYOS
RAMdisk
3 RAMDisk
0
786436
----------------------------------------------------------------------
PrettyOS unterscheidet zunächst Ports,
Disks,
Partition, Filesystem, File.
Beispiele:
Port ist der usb-Slot, Disk der usb-Stick.
Darauf befindet sich eine FAT32-Partition mit den entsprechenden Files.
Port ist das Floppydisk-Laufwerk, Disk die eingelegte Diskette mit
FAT12-Partition.
Port ist das RAM, Disk die RAMDisk mit
Anfang und
Ende (Partition) und mit eigenem Filesystem.
Laden wir z.B. ttt.elf
von Diskette, so geben wir ein: A:\ttt.elf
oder 1:\ttt.elf
oder A:1:\ttt.elf,
und
das
File
wird von Diskette geladen. Ist der usb-Stick
auf
Platz drei der Disk-Liste, dann entsprechend 3:\ttt.elf.
Die Ports sind hierbei statisch, während die Disk-Reihenfolge von der
Abfolge
beim Einbinden abhängt.
Das User-Programm wird dann entsprechend mit dem elf-Parser
bearbeitet analog zu shell.elf.
User-Programme greifen auf Funktionen im Kernel
über
sogenannte syscalls zu. Nehmen wir hier das konkrete
Beispiel
eines User-Programms mit setScrollField(...). In
der Datei userlib.c
findet sich das Durchreichen zum Syscall:
void setScrollField(uint8_t
top, uint8_t bottom)
{
__asm__ volatile("int $0x7F" : :
"a"(21), "b"(top), "c"(bottom));
}
Wie geht dies genau weiter bis zur Funktion im Kernel?
In syscall.c
findet sich die Definition: DEFN_SYSCALL2(setScrollField,
21,
uint8_t,
uint8_t)
Im Array static void*
syscalls[] steht an der Stelle Nr.
21: &setScrollField
Damit gelangen wir zu dieser Kernel-Funktion.
Solche syscalls kosten Zeit durch den Overhead beim Wechsel von Ring 3 zu Ring 0 und
zurück.
Abschottung im Protected Mode hat nun mal
ihren
Preis.
Daher versucht PrettyOS, die Zahl der syscalls zu beschränken und möglichst viele
Funktionen in
der userlib.h/c direkt umzusetzen.
Dies führt leider zu doppeltem Code sowohl in der Userlib
als auch im Kernel (z.B. util.c).
Ein definiertes Ziel von PrettyOS war es, neben der Floppydisk auch USB Geräte wie z.B. usb-Sticks oder usb-Festplatten zu verwenden. Um diese Idee zu verwirklichen, musste zunächst ein EHCI-Treiber für das Highspeed-USB installiert werden. Anschließend konnte diese neue Funktionalität, die bisher auf root-ports begrenzt ist, für USB 2.0 bulk-Transfers verwendet werden. In Verbindung mit einem neuen FAT-Modul (FAT12/16/32) kann man damit Files von/auf usb-Mass Storage Devices (usb msd) lesen/schreiben.
EHCI steht für Enhanced Host Controller Interface.
Die
Spezifikation
findet
man hier.
EHCI stellt nur High Speed USB
Funktionalität zur
Verfügung.. Im Normalfall residiert daneben
ein
sogenannter "companion controller",
entweder
OHCI
oder
UHCI, der sich um die langsameren full
speed und low speed USB Geräte kümmert. Deshalb findet man
beim PCI scan beide Typen von Controllern,
also UHCI und EHCI bzw.
OHCI und EHCI. Ältere Systeme (vor dem Jahr 2000) bieten nur UHCI oder
OHCI an
und können nicht von unserem EHCI- und USB-Treiber
profitieren. PrettyOS verfügt bisher noch
nicht über
einen UHCI oder OHCI Treiber. Wir steuern z.Z.
auch
nur
EHCI-root-ports an. Highspeed-usb-sticks
an Root-ports mit FAT-Formatierung
können jedoch problemlos betrieben werden.
Es gibt zu USB zwei
hervorragende Übersichten bei osdev.org
und bei lowlevel
wie auch eine knappe
Einführung, die hier zum ersten Studium empfohlen seien. Die
ausführliche
Spezifikation findet man hier.
Wir
verwenden
in
PrettyOS bisher nur Control-Transfers und Bulk-Transfers.