Lauffähige Beispiele
Technisch gesehen ist ein Programm, das ohne Betriebssystem läuft, ein Betriebssystem. Schauen wir uns also an, wie man ein paar winzige Hallo-Welt-Betriebssysteme erstellt und ausführt.
Der Code aller folgenden Beispiele ist auf diesem GitHub-Repo vorhanden .
Bootsektor
Auf x86 können Sie am einfachsten und niedrigsten einen Master- Bootsektor (MBR) erstellen , der eine Art Bootsektor darstellt , und diesen dann auf einer Festplatte installieren.
Hier erstellen wir eine mit einem einzigen printf
Aufruf:
printf '\364%509s\125\252' > main.img
sudo apt-get install qemu-system-x86
qemu-system-x86_64 -hda main.img
Ergebnis:
Getestet unter Ubuntu 18.04, QEMU 2.11.1.
main.img
enthält folgendes:
\364
in oktal == 0xf4
in hex: die Kodierung für einen hlt
Befehl, der die CPU auffordert, nicht mehr zu arbeiten.
Deshalb wird unser Programm nichts tun: nur starten und stoppen.
Wir verwenden Oktal, da \x
Hexadezimalzahlen von POSIX nicht angegeben werden.
Wir könnten diese Kodierung leicht erhalten mit:
echo hlt > a.asm
nasm -f bin a.asm
hd a
Die 0xf4
Kodierung ist aber natürlich auch im Intel-Handbuch dokumentiert.
%509s
produzieren 509 Plätze. Muss die Datei bis zum Byte 510 ausfüllen.
\125\252
in oktal == 0x55
gefolgt von 0xaa
: magischen Bytes, die von der Hardware benötigt werden. Sie müssen die Bytes 511 und 512 sein.
Ist dies nicht der Fall, wird dies von der Hardware nicht als startfähige Festplatte behandelt.
Beachten Sie, dass auch ohne etwas zu tun, einige Zeichen bereits auf dem Bildschirm gedruckt sind. Diese werden von der Firmware ausgedruckt und dienen zur Identifikation des Systems.
Laufen auf echter Hardware
Emulatoren machen Spaß, aber Hardware ist das einzig Wahre.
Beachten Sie jedoch, dass dies gefährlich ist und Sie Ihre Festplatte versehentlich löschen können: Tun Sie dies nur auf alten Computern, die keine kritischen Daten enthalten! Oder noch besser, Devboards wie der Raspberry Pi, siehe das ARM-Beispiel unten.
Für einen typischen Laptop müssen Sie Folgendes tun:
Brennen Sie das Image auf einen USB-Stick (zerstört Ihre Daten!):
sudo dd if=main.img of=/dev/sdX
Stecken Sie den USB an einen Computer
Mach es an
Sagen Sie ihm, er soll von USB booten.
Dies bedeutet, dass die Firmware USB vor der Festplatte auswählt.
Wenn dies nicht das Standardverhalten Ihres Computers ist, drücken Sie nach dem Einschalten die Eingabetaste, F12, ESC oder andere ungewöhnliche Tasten, bis Sie ein Startmenü erhalten, in dem Sie auswählen können, ob Sie vom USB-Stick starten möchten.
In diesen Menüs kann häufig die Suchreihenfolge konfiguriert werden.
Auf meinem alten Lenovo Thinkpad T430, UEFI BIOS 1.16, sehe ich beispielsweise Folgendes:
Hallo Welt
Nachdem wir ein minimales Programm erstellt haben, begeben wir uns in eine hallo Welt.
Die offensichtliche Frage ist: Wie mache ich IO? Ein paar Möglichkeiten:
- Bitten Sie die Firmware, z. B. BIOS oder UEFI, dies für uns zu tun
- VGA: Spezieller Speicherbereich, der beim Schreiben auf den Bildschirm gedruckt wird. Kann im geschützten Modus verwendet werden.
- Schreiben Sie einen Treiber und sprechen Sie direkt mit der Display-Hardware. Dies ist der "richtige" Weg, dies zu tun: leistungsstärker, aber komplexer.
serielle Schnittstelle . Dies ist ein sehr einfaches standardisiertes Protokoll, das Zeichen von einem Host-Terminal sendet und abruft.
Quelle .
Es ist leider nicht auf den meisten modernen Laptops ausgesetzt, aber es ist der übliche Weg für Entwicklungsboards, siehe die ARM-Beispiele unten.
Das ist wirklich schade, da solche Schnittstellen zum Beispiel für das Debuggen des Linux-Kernels sehr nützlich sind .
Verwenden Sie Debug-Funktionen von Chips. ARM nennt sie zum Beispiel Semihosting . Bei echter Hardware sind einige zusätzliche Hardware- und Softwareunterstützungen erforderlich, bei Emulatoren kann dies jedoch eine kostenlose, praktische Option sein. Beispiel .
Hier machen wir ein BIOS-Beispiel, da es auf x86 einfacher ist. Beachten Sie jedoch, dass dies nicht die robusteste Methode ist.
Netz
.code16
mov $msg, %si
mov $0x0e, %ah
loop:
lodsb
or %al, %al
jz halt
int $0x10
jmp loop
halt:
hlt
msg:
.asciz "hello world"
link.ld
SECTIONS
{
. = 0x7c00;
.text :
{
__start = .;
*(.text)
. = 0x1FE;
SHORT(0xAA55)
}
}
Zusammenstellen und verknüpfen mit:
gcc -c -g -o main.o main.S
ld --oformat binary -o main.img -T linker.ld main.o
Ergebnis:
Getestet auf: Lenovo Thinkpad T430, UEFI BIOS 1.16. Die Festplatte wurde auf einem Ubuntu 18.04-Host erstellt.
Neben den Standardanweisungen für die Userland-Montage haben wir:
.code16
: Weist GAS an, 16-Bit-Code auszugeben
cli
: Software-Interrupts deaktivieren. Diese könnten dazu führen, dass der Prozessor nach dem erneut gestartet wirdhlt
int $0x10
: führt einen BIOS-Aufruf durch. Dies ist, was die Zeichen eins nach dem anderen druckt.
Die wichtigen Link-Flags sind:
--oformat binary
: Gebe rohen binären Assembler-Code aus. Verwerfe ihn nicht in einer ELF-Datei, wie dies bei normalen Userland-Programmen der Fall ist.
Verwenden Sie C anstelle von Assembly
Da C zu Assembly kompiliert wird und die Verwendung von C ohne die Standardbibliothek ziemlich einfach ist, benötigen Sie im Grunde nur Folgendes:
- Ein Linker-Skript, um Dinge an der richtigen Stelle in Erinnerung zu rufen
- Flags, die GCC anweisen, die Standardbibliothek nicht zu verwenden
- Ein winziger Assembly-Einstiegspunkt, der den erforderlichen C-Status für
main
Folgendes festlegt :
TODO: so ein x86 Beispiel auf GitHub verlinken. Hier ist ein ARM, den ich erstellt habe .
Es macht jedoch mehr Spaß, wenn Sie die Standardbibliothek verwenden möchten, da wir keinen Linux-Kernel haben, der einen Großteil der Funktionen der C-Standardbibliothek über POSIX implementiert .
Einige Möglichkeiten, ohne auf ein vollwertiges Betriebssystem wie Linux umzusteigen, sind:
Newlib
Ausführliches Beispiel unter: https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931
In Newlib müssen Sie die Syscalls selbst implementieren, aber Sie erhalten ein sehr minimales System, und es ist sehr einfach, sie zu implementieren.
Beispielsweise könnten Sie printf
zu den UART- oder ARM-Systemen umleiten oder exit()
mit Semihosting implementieren .
Embedded-Betriebssysteme wie FreeRTOS und Zephyr .
Mit solchen Betriebssystemen können Sie in der Regel die präventive Zeitplanung deaktivieren und so die vollständige Kontrolle über die Laufzeit des Programms behalten.
Sie können als eine Art vorimplementierte Newlib angesehen werden.
ARM
In ARM sind die allgemeinen Vorstellungen dieselben. Ich habe hochgeladen:
Für den Raspberry Pi scheint https://github.com/dwelch67/raspberrypi das beliebteste Tutorial zu sein, das derzeit erhältlich ist.
Einige Unterschiede zu x86 sind:
Die Eingabe erfolgt durch direktes Schreiben an magische Adressen, es gibt keine in
und out
Anweisungen.
Dies wird als speicherabgebildete E / A bezeichnet .
Für echte Hardware wie den Raspberry Pi können Sie die Firmware (BIOS) selbst zum Festplatten-Image hinzufügen.
Das ist eine gute Sache, da es die Aktualisierung dieser Firmware transparenter macht.
Firmware
In Wahrheit ist Ihr Bootsektor nicht die erste Software, die auf der CPU des Systems ausgeführt wird.
Was tatsächlich zuerst ausgeführt wird, ist die sogenannte Firmware , bei der es sich um eine Software handelt:
- von den Hardware-Herstellern gemacht
- Typischerweise geschlossene Quelle, aber wahrscheinlich C-basiert
- im Nur-Lese-Speicher gespeichert und daher ohne Zustimmung des Anbieters schwerer / unmöglich zu ändern.
Bekannte Firmwares sind:
- BIOS : alte allgegenwärtige x86-Firmware. SeaBIOS ist die von QEMU standardmäßig verwendete Open Source-Implementierung.
- UEFI : BIOS-Nachfolger, besser standardisiert, aber leistungsfähiger und unglaublich aufgebläht.
- Coreboot : der edle Cross Arch Open Source Versuch
Die Firmware macht Dinge wie:
Durchlaufen Sie jede Festplatte, jedes USB-Gerät, jedes Netzwerk usw., bis Sie etwas finden, das gebootet werden kann.
Wenn wir QEMU ausführen, -hda
heißt das, dass main.img
eine Festplatte an die Hardware angeschlossen ist, und
hda
ist der erste, der ausprobiert wird, und er wird verwendet.
Laden Sie die ersten 512 Bytes in die RAM-Speicheradresse 0x7c00
, platzieren Sie den RIP der CPU dort und lassen Sie ihn laufen
Zeigen Sie Dinge wie das Boot-Menü oder BIOS-Druckaufrufe auf dem Display an
Firmware bietet betriebssystemähnliche Funktionen, von denen die meisten Betriebssysteme abhängen. Beispielsweise wurde eine Python-Teilmenge für die Ausführung unter BIOS / UEFI portiert: https://www.youtube.com/watch?v=bYQ_lq5dcvM
Es kann argumentiert werden, dass Firmwares nicht von Betriebssystemen zu unterscheiden sind und dass Firmware die einzige "echte" Bare-Metal-Programmierung ist, die man durchführen kann.
Wie dieser CoreOS-Entwickler es ausdrückt :
Der schwierige Teil
Wenn Sie einen PC einschalten, werden die Chips, aus denen der Chipsatz besteht (Northbridge, Southbridge und SuperIO), noch nicht ordnungsgemäß initialisiert. Obwohl das BIOS-ROM so weit wie möglich von der CPU entfernt ist, kann die CPU darauf zugreifen, da dies erforderlich ist, da die CPU sonst keine Anweisungen zur Ausführung hätte. Dies bedeutet nicht, dass das BIOS-ROM vollständig zugeordnet ist, normalerweise nicht. Es ist jedoch gerade genug zugeordnet, um den Startvorgang in Gang zu setzen. Alle anderen Geräte, vergiss es einfach.
Wenn Sie Coreboot unter QEMU ausführen, können Sie mit den höheren Schichten von Coreboot und mit den Nutzdaten experimentieren, QEMU bietet jedoch kaum Gelegenheit, mit dem Startcode auf niedriger Ebene zu experimentieren. Zum einen funktioniert RAM von Anfang an.
Post-BIOS-Ausgangszustand
Wie viele Dinge in der Hardware ist die Standardisierung schwach, und eines der Dinge, auf die Sie sich nicht verlassen sollten, ist der Anfangszustand der Register, wenn Ihr Code nach dem BIOS ausgeführt wird.
Tun Sie sich selbst einen Gefallen und verwenden Sie einen Initialisierungscode wie den folgenden: https://stackoverflow.com/a/32509555/895245
Register mögen %ds
und %es
haben wichtige Nebenwirkungen, daher sollten Sie sie auf Null setzen, auch wenn Sie sie nicht explizit verwenden.
Beachten Sie, dass einige Emulatoren besser sind als echte Hardware und Ihnen einen schönen Anfangszustand geben. Wenn Sie dann auf echter Hardware laufen, geht alles kaputt.
GNU GRUB Multiboot
Bootsektoren sind einfach, aber nicht sehr praktisch:
- Sie können nur ein Betriebssystem pro Festplatte haben
- Der Ladecode muss wirklich klein sein und in 512 Bytes passen. Dies könnte mit dem int 0x13 BIOS-Aufruf behoben werden .
- Sie müssen viel selbst starten, beispielsweise in den geschützten Modus
Aus diesen Gründen hat GNU GRUB ein praktischeres Dateiformat namens Multiboot erstellt.
Minimales Arbeitsbeispiel: https://github.com/cirosantilli/x86-bare-metal-examples/tree/d217b180be4220a0b4a453f31275d38e697a99e0/multiboot/hello-world
Ich benutze es auch in meinem GitHub-Beispielrepo , um alle Beispiele problemlos auf echter Hardware ausführen zu können, ohne den USB-Stick millionenfach zu brennen. Auf QEMU sieht es so aus:
Wenn Sie Ihr Betriebssystem als Multiboot-Datei vorbereiten, kann GRUB es in einem regulären Dateisystem finden.
Dies ist, was die meisten Distributionen tun, indem sie OS-Images unterbringen /boot
.
Multiboot-Dateien sind im Grunde eine ELF-Datei mit einem speziellen Header. Sie werden von GRUB unter https://www.gnu.org/software/grub/manual/multiboot/multiboot.html angegeben
Sie können eine Multiboot-Datei mit in eine bootfähige Disk verwandeln grub-mkrescue
.
El Torito
Format, das auf CDs gebrannt werden kann: https://en.wikipedia.org/wiki/El_Torito_%28CD-ROM_standard%29
Es ist auch möglich, ein Hybrid-Image zu erstellen, das entweder auf ISO oder USB funktioniert. Dies kann mit grub-mkrescue
( Beispiel ) erfolgen und wird auch vom Linux-Kernel bei der make isoimage
Verwendung durchgeführt isohybrid
.
Ressourcen