Wie kann man ein Programm von alleine ausführen, ohne dass ein Betriebssystem ausgeführt wird? Können Sie Assembly-Programme erstellen, die der Computer beim Start laden und ausführen kann, z. Starten Sie den Computer von einem Flash-Laufwerk und es läuft das Programm, das auf der CPU ist?
Wie kann man ein Programm von alleine ausführen, ohne dass ein Betriebssystem ausgeführt wird?
Sie platzieren Ihren Binärcode an einer Stelle, nach der der Prozessor nach dem Neustart sucht (z. B. Adresse 0 in ARM).
Können Sie Assembly-Programme erstellen, die der Computer beim Start laden und ausführen kann (z. B. den Computer von einem Flash-Laufwerk starten und das Programm ausführen, das sich auf dem Laufwerk befindet)?
Allgemeine Antwort auf die Frage: Es kann getan werden. Es wird oft als "Bare-Metal-Programmierung" bezeichnet. Um von einem Flash-Laufwerk zu lesen, möchten Sie wissen, was USB ist, und Sie möchten einen Treiber, der mit diesem USB funktioniert. Das Programm auf diesem Laufwerk muss auch in einem bestimmten Format vorliegen, auf einem bestimmten Dateisystem ... Dies ist etwas, was Bootloader normalerweise tun, aber Ihr Programm kann einen eigenen Bootloader enthalten, so dass es in sich geschlossen ist, wenn die Firmware dies nur tut Laden Sie einen kleinen Codeblock.
Auf vielen ARM Boards können Sie einige dieser Dinge ausführen. Einige haben Bootloader, die Sie bei der Grundeinstellung unterstützen.
Hier Hier finden Sie ein großartiges Tutorial, wie man ein grundlegendes Betriebssystem auf einem Raspberry Pi erstellt.
Bearbeiten: Dieser Artikel und die gesamte wiki.osdev.org beantworten die meisten Ihrer Fragen http://wiki.osdev.org/Introduction
Wenn Sie nicht direkt mit Hardware experimentieren möchten, können Sie sie auch als virtuelle Maschine mit Hypervisoren wie qemu ausführen. Erfahren Sie, wie Sie "hello world" direkt auf virtualisierter ARM Hardware hier ausführen.
Lauffähige Beispiele
Lassen Sie uns ein paar winzige Bare-Metal-Hallo-Welt-Programme erstellen und ausführen, die ohne Betriebssystem ausgeführt werden:
Wir werden sie auch so oft wie möglich auf dem QEMU-Emulator testen, da dies sicherer und bequemer für die Entwicklung ist. Die QEMU-Tests wurden auf einem Ubuntu 18.04-Host mit dem vorgefertigten QEMU 2.11.1 durchgeführt.
Der Code aller folgenden und weiterer x86-Beispiele ist auf diesem GitHub-Repo enthalten.
Ausführen der Beispiele auf realer x86-Hardware
Denken Sie daran, dass das Ausführen von Beispielen auf echter Hardware gefährlich sein kann, z. Sie könnten versehentlich Ihre Festplatte löschen oder die Hardware blockieren: Tun Sie dies nur auf alten Maschinen, die keine kritischen Daten enthalten! Oder noch besser, verwenden Sie günstige Einweg-Devboards wie das Raspberry Pi, siehe das nachstehende Beispiel ARM.
Für einen typischen x86-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 T430 sehe ich zum Beispiel Folgendes.
Nach dem Einschalten muss ich die Eingabetaste drücken, um das Startmenü aufzurufen:
Dann muss ich hier F12 drücken, um den USB als Boot-Gerät auszuwählen:
Von dort aus kann ich den USB-Stick wie folgt als Startgerät auswählen:
Um alternativ die Startreihenfolge zu ändern und den USB-Speicher mit höherer Priorität auszuwählen, damit ich ihn nicht jedes Mal manuell auswählen muss, drücke ich im Bildschirm "Startunterbrechungsmenü" die Taste F1 und navigiere dann zu:
Bootsektor
Auf x86 können Sie als einfachste und niedrigste Ebene einen Master-Boot-Sektor (MBR) erstellen. Dies ist eine Art Boot-Sektor , und installieren Sie es dann auf einer Festplatte.
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:
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.
Und auf dem T430 bekommen wir nur einen leeren Bildschirm mit einem blinkenden Cursor:
main.img
Enthält Folgendes:
\364
In Oktal == 0xf4
In Hex: Die Codierung für eine Anweisung hlt
, die 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.S
as -o a.o a.S
objdump -S a.o
welche Ausgänge:
a.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: f4 hlt
es ist aber natürlich auch im Intel-Handbuch dokumentiert.
%509s
Erzeugt 509 Leerzeichen. Muss die Datei bis zum Byte 510 ausfüllen.
\125\252
In Oktal == 0x55
Gefolgt von 0xaa
.
Dies sind 2 erforderliche magische Bytes, die die Bytes 511 und 512 sein müssen.
Das BIOS durchsucht alle unsere Festplatten nach bootfähigen und berücksichtigt nur solche, die über diese zwei magischen Bytes verfügen.
Ist dies nicht der Fall, wird dies von der Hardware nicht als startfähige Festplatte behandelt.
Wenn Sie kein printf
Master sind, können Sie den Inhalt von main.img
Bestätigen mit:
hd main.img
was das erwartete zeigt:
00000000 f4 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 |. |
00000010 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 | |
*
000001f0 20 20 20 20 20 20 20 20 20 20 20 20 20 20 55 aa | U.|
00000200
dabei ist 20
ein Leerzeichen in ASCII.
Die BIOS-Firmware liest diese 512 Bytes von der Festplatte, legt sie im Speicher ab und setzt den PC auf das erste Byte, um sie auszuführen.
Hallo Weltbootsektor
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:
serielle Schnittstelle . Dies ist ein sehr einfaches standardisiertes Protokoll, das Zeichen von einem Host-Terminal sendet und empfängt.
Auf Desktops sieht das so aus:
Quelle .
Es ist leider nicht auf den meisten modernen Laptops ausgesetzt, aber es ist der übliche Weg für Entwicklungsboards, siehe ARM Beispiele unten.
Das ist wirklich schade, da solche Schnittstellen wirklich nützlich sind , um beispielsweise den Linux-Kernel zu debuggen .
verwenden Sie Debug-Funktionen von Chips. ARM ruft ihr Semihosting auf. Auf realer Hardware erfordert es einige zusätzliche Hardware- und Softwareunterstützung, aber auf Emulatoren kann es ein freies bequemes sein option. 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
{
/* The BIOS loads the code from the disk to this location.
* We must tell that to the linker so that it can properly
* calculate the addresses of symbols we might jump to.
*/
. = 0x7c00;
.text :
{
__start = .;
*(.text)
/* Place the magic boot bytes at the end of the first 512 sector. */
. = 0x1FE;
SHORT(0xAA55)
}
}
Zusammenstellen und verknüpfen mit:
as -g -o main.o main.S
ld --oformat binary -o main.img -T link.ld main.o
qemu-system-x86_64 -hda main.img
Ergebnis:
Und auf dem T430:
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
: Deaktiviert Software-Interrupts. Diese könnten den Prozessor nach dem hlt
wieder zum Laufen bringen
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 Assembly-Code aus, verpacke ihn nicht in eine ELF-Datei, wie es bei normalen Userland-Programmen der Fall ist.Um den Linker-Skript-Teil besser zu verstehen, machen Sie sich mit dem Umsiedlungsschritt des Linkens vertraut: Was machen Linker?
Cooler x86 Bare-Metal-Programme
Hier sind einige komplexere Bare-Metal-Setups, die ich erreicht habe:
Verwenden Sie C anstelle von Assembly
Zusammenfassung: Verwenden Sie GRUB multiboot, um viele ärgerliche Probleme zu lösen, an die Sie nie gedacht haben.
Die Hauptschwierigkeit bei x86 besteht darin, dass das BIOS nur 512 Bytes von der Festplatte in den Speicher lädt und Sie diese 512 Bytes wahrscheinlich in die Luft jagen, wenn Sie C verwenden!
Um dies zu lösen, können wir einen zweistufigen Bootloader verwenden. Dies führt zu weiteren BIOS-Aufrufen, bei denen mehr Bytes von der Festplatte in den Speicher geladen werden. Hier ist ein minimales Stage 2 Assembly-Beispiel von Grund auf mit den int 0x13 BIOS-Aufrufen :
Alternative:
-kernel
, mit der eine gesamte ELF-Datei in den Speicher geladen wird. Hier ist ein ARM Beispiel, das ich mit dieser Methode erstellt habe .kernel7.img
, ähnlich wie bei QEMU -kernel
.Hier ist nur zu Ausbildungszwecken ein einstufiges minimales C-Beispiel :
haupt c
void main(void) {
int i;
char s[] = {'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd'};
for (i = 0; i < sizeof(s); ++i) {
__asm__ (
"int $0x10" : : "a" ((0x0e << 8) | s[i])
);
}
while (1) {
__asm__ ("hlt");
};
}
entry.S
.code16
.text
.global mystart
mystart:
ljmp $0, $.setcs
.setcs:
xor %ax, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %ss
mov $__stack_top, %esp
cld
call main
linker.ld
ENTRY(mystart)
SECTIONS
{
. = 0x7c00;
.text : {
entry.o(.text)
*(.text)
*(.data)
*(.rodata)
__bss_start = .;
/* COMMON vs BSS: https://stackoverflow.com/questions/16835716/bss-vs-common-what-goes-where */
*(.bss)
*(COMMON)
__bss_end = .;
}
/* https://stackoverflow.com/questions/53584666/why-does-gnu-ld-include-a-section-that-does-not-appear-in-the-linker-script */
.sig : AT(ADDR(.text) + 512 - 2)
{
SHORT(0xaa55);
}
/DISCARD/ : {
*(.eh_frame)
}
__stack_bottom = .;
. = . + 0x1000;
__stack_top = .;
}
lauf
set -eux
as -ggdb3 --32 -o entry.o entry.S
gcc -c -ggdb3 -m16 -ffreestanding -fno-PIE -nostartfiles -nostdlib -o main.o -std=c99 main.c
ld -m elf_i386 -o main.elf -T linker.ld entry.o main.o
objcopy -O binary main.elf main.img
qemu-system-x86_64 -drive file=main.img,format=raw
C-Standardbibliothek
Es macht mehr Spaß, wenn Sie auch die C-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:
Schreibe dein Eigenes. Am Ende sind es nur ein paar Header und C-Dateien, oder? Richtig??
Ausführliches Beispiel unter: https://electronics.stackexchange.com/questions/223929/c-standard-libraries-on-bare-metal/223931
Newlib implementiert alle langweiligen, nicht für das Betriebssystem spezifischen Dinge für Sie, z. memcmp
, memcpy
usw.
Anschließend werden einige Stubs bereitgestellt, mit denen Sie die von Ihnen benötigten Systemaufrufe implementieren können.
Zum Beispiel können wir exit()
auf ARM durch Semihosting mit:
void _exit(int status) {
__asm__ __volatile__ ("mov r0, #0x18; ldr r1, =#0x20026; svc 0x00123456");
}
wie bei in diesem Beispiel gezeigt .
Zum Beispiel könnten Sie printf
zu den UART oder ARM) Systemen umleiten oder exit()
mit semihosting .
eingebettete Betriebssysteme wie FreeRTOS und Zephyr .
Mit solchen Betriebssystemen können Sie in der Regel die vorbeugende Zeitplanung deaktivieren und so die vollständige Kontrolle über die Laufzeit des Programms behalten.
Sie können als eine Art vorimplementierte Newlib angesehen werden.
GNU GRUB Multiboot
Bootsektoren sind einfach, aber nicht sehr praktisch:
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 für mein GitHub-Beispielrepo , um problemlos alle Beispiele auf echter Hardware ausführen zu können, ohne den USB-Stick millionenfach zu brennen.
QEMU-Ergebnis:
T430:
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 unter /boot
Setzen.
Multiboot-Dateien sind im Grunde eine ELF-Datei mit einem speziellen Header. Sie werden von GRUB at: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html angegeben.
Sie können eine Multiboot-Datei mit grub-mkrescue
In eine bootfähige Disk verwandeln.
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:
Bekannte Firmwares sind:
Die Firmware macht Dinge wie:
durchlaufen Sie jede Festplatte, jedes USB-Gerät, jedes Netzwerk usw., bis Sie etwas Bootfähiges finden.
Wenn wir QEMU ausführen, besagt -hda
, Dass main.img
Eine mit der Hardware verbundene Festplatte ist, und hda
ist die erste, die ausprobiert wird, und sie wird verwendet.
laden Sie die ersten 512 Bytes in RAM Speicheradresse 0x7c00
], legen Sie den RIP der CPU dort ab und lassen Sie ihn laufen
zeigen Sie Dinge wie das Startmenü oder BIOS-Druckaufrufe auf dem Display an
Firmware bietet betriebssystemähnliche Funktionen, von denen die meisten Betriebssysteme abhängen. Z.B. Eine Python -Untergruppe wurde 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 ausführen müsste bedeutet nicht, dass das BIOS ROM normalerweise nicht vollständig zugeordnet ist. Es ist jedoch gerade genug zugeordnet, um den Startvorgang in Gang zu setzen. Alle anderen Geräte, vergessen Sie 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 wenig Gelegenheit, mit dem Startcode auf niedriger Ebene zu experimentieren. Zum einen funktioniert RAM von Anfang an.
BIOS-Anfangszustand
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 wie %ds
Und %es
Haben wichtige Nebenwirkungen. Sie sollten sie daher auf Null setzen, auch wenn Sie sie nicht explizit verwenden.
Beachten Sie, dass einige Emulatoren besser sind als echte Hardware und Ihnen einen netten Anfangszustand geben. Wenn Sie dann auf echter Hardware laufen, geht alles kaputt.
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 ) geschehen und wird auch vom Linux-Kernel unter make isoimage
Mit isohybrid
durchgeführt. .
[~ # ~] Arm [~ # ~]
In ARM sind die allgemeinen Vorstellungen dieselben.
Es gibt keine weit verbreitete halbstandardisierte vorinstallierte Firmware wie das BIOS, die wir für die E/A verwenden können. Die zwei einfachsten Arten von IO) sind also:
Ich habe hochgeladen:
einige einfache Beispiele für QEMU C + Newlib und Raw Assembly hier auf GitHub .
Das Beispiel Prompt.c nimmt zum Beispiel Eingaben von Ihrem Host-Terminal entgegen und gibt Ausgaben über den simulierten UART zurück:
enter a character
got: a
new alloc of 1 bytes at address 0x0x4000a1c0
enter a character
got: b
new alloc of 2 bytes at address 0x0x4000a1c0
enter a character
Siehe auch: Bare-Metal-Programme erstellen ARM= auf QEMU ausführen?
ein vollautomatisches Raspberry Pi Blinker Setup unter: https://github.com/cirosantilli/raspberry-pi-bare-metal-blinker
Siehe auch: Wie führe ich ein C-Programm ohne Betriebssystem auf dem Raspberry Pi aus?
Um die LEDs auf QEMU zu "sehen", müssen Sie QEMU aus dem Quellcode mit einem Debug-Flag kompilieren: https://raspberrypi.stackexchange.com/questions/56373/is-it-possible-to-get -der-zustand-der-leds-und-gpios-in-a-qemu-emulation-like-t
Als Nächstes sollten Sie eine UART Hallo Welt. Sie können vom Blinker-Beispiel ausgehen und den Kernel durch diesen ersetzen: https://github.com/ dwelch67/raspberrypi/tree/bce377230c2cdd8ff1e40919fdedbc2533ef5a00/uart01
Lassen Sie zuerst den UART mit Raspbian arbeiten, wie ich unter: https://raspberrypi.stackexchange.com/questions/38/prepare-for-ssh- erklärt habe. without-a-screen/54394 # 54394 Es wird ungefähr so aussehen:
Stellen Sie sicher, dass Sie die richtigen Pins verwenden, sonst können Sie Ihren UART zu USB-Konverter brennen, ich habe es bereits zweimal durch Kurzschließen von Masse und 5V getan ...
Stellen Sie abschließend vom Host aus eine Verbindung zur Seriennummer her mit:
screen /dev/ttyUSB0 115200
Für den Raspberry Pi verwenden wir eine Micro-SD-Karte anstelle eines USB-Sticks, um unsere ausführbare Datei zu speichern. Für diese benötigen Sie normalerweise einen Adapter, um eine Verbindung zu Ihrem Computer herzustellen:
Vergessen Sie nicht, den SD-Adapter wie folgt zu entsperren: https://askubuntu.com/questions/213889/microsd-card-is-set-to-read-only-state-how- Ich kann Daten darauf schreiben/814585 # 814585
https://github.com/dwelch67/raspberrypi sieht aus wie das beliebteste Bare-Metal-Raspberry-Pi-Tutorial, das heute erhältlich ist.
Einige Unterschiede zu x86 sind:
Die Eingabe erfolgt durch direktes Schreiben an magische Adressen, es gibt keine Anweisungen in
und out
.
Dies wird als speicherabgebildetes 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.
Ressourcen