wake-up-neo.com

Wie führe ich ein Programm ohne Betriebssystem aus?

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?

220
user2320609

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.

142
Kissiel

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:

  1. Brennen Sie das Image auf einen USB-Stick (zerstört Ihre Daten!):

    Sudo dd if=main.img of=/dev/sdX
    
  2. stecken Sie den USB an einen Computer

  3. mach es an

  4. 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:

enter image description here

Dann muss ich hier F12 drücken, um den USB als Boot-Gerät auszuwählen:

enter image description here

Von dort aus kann ich den USB-Stick wie folgt als Startgerät auswählen:

enter image description here

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:

enter image description here

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:

enter image description here

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:

enter image description here

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:

  • fragen Sie die Firmware, z. BIOS oder UEFI, um es 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 empfängt.

    Auf Desktops sieht das so aus:

    enter image description here

    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"

GitHub upstream .

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:

enter image description here

Und auf dem T430:

enter image description here

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:

  • wenn Sie es nur für QEMU benötigen, aber nicht für echte Hardware, verwenden Sie die Option -kernel, mit der eine gesamte ELF-Datei in den Speicher geladen wird. Hier ist ein ARM Beispiel, das ich mit dieser Methode erstellt habe .
  • für den Raspberry Pi übernimmt die Standardfirmware das Laden des Images aus einer ELF-Datei mit dem Namen 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??

  • Newlib

    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:

  • sie können nur ein Betriebssystem pro Festplatte haben
  • der Ladecode muss wirklich klein sein und in 512 Bytes passen
  • 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 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:

enter image description here

T430:

enter image description here

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:

  • 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 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:

  • seriennummer, die auf Devboards weit verbreitet ist
  • blinkt die LED

Ich habe hochgeladen:

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