Bootloader erklärt: Wie sie Geräte starten

Inhaltsverzeichnis

BootLoader in Embedded System

Was ist ein Bootloader?

Der Bootloader ist der erste Codeabschnitt, der nach dem Einschalten in einem eingebetteten System ausgeführt wird. Sobald er die Initialisierung der CPU und der relevanten Hardware abgeschlossen hat, lädt er das Betriebssystem-Image oder das eingebettete Anwendungsprogramm in den Speicher und wechselt dann zu dem Bereich, in dem sich das Betriebssystem befindet, wodurch der Betrieb des Betriebssystems initiiert wird. Dieser Prozess ist für alle, die sich mit der Programmierung spezifischer Mikrocontroller wie STM32 befassen, von grundlegender Bedeutung.

Ähnlich wie ein Anwendungsprogramm ist ein Bootloader ein eigenständiges Programm, das wesentliche Komponenten wie Startcode, Interrupts, ein Hauptprogramm (Boot_main-Funktion) und optional ein Betriebssystem enthält. Trotz seiner geringen Größe umfasst der Bootloader wichtige Funktionen.

Bootloader sind in der Regel stark von der Hardware abhängig und spielen insbesondere im Bereich der eingebetteten Systeme eine wichtige Rolle. Daher ist es nahezu unmöglich, einen universell einsetzbaren Bootloader für den Embedded-Bereich zu entwickeln. Dennoch können wir einige Konzepte zu Bootloadern verallgemeinern, um Anwendern bei der Entwicklung und Implementierung spezifischer Bootloader als Orientierung zu dienen.

Betriebsmodi eines Bootloaders

Die meisten Bootloader verfügen über zwei unterschiedliche Betriebsmodi: den Boot-Modus und den Download-Modus.

Beim Einschalten initialisiert der Bootloader die Software- und Hardwareumgebung des Systems und wählt basierend auf den aktuellen Hardwarebedingungen einen der Betriebsmodi aus. Dazu gehören die Konfiguration des CPU-Betriebsmodus, die Initialisierung des Speichers, die Deaktivierung von Interrupts und die Ausführung von Aufgaben wie das Ausschalten der MMU/des Caches.

Boot-Lademodus:

In diesem Modus, der auch als „autonomer“ Modus bezeichnet wird, lädt der Bootloader das Betriebssystem selbstständig von einem Solid-State-Speichergerät auf dem Zielcomputer in den Arbeitsspeicher. Dieser gesamte Vorgang erfolgt ohne jegliches Zutun des Benutzers. Dieser Modus entspricht dem normalen Betrieb des Bootloaders.

Download-Modus:

In diesem Modus initiiert der Bootloader auf dem Zielrechner die Kommunikation über serielle oder Netzwerkverbindungen mit einem Hostrechner, um Dateien herunterzuladen. Die vom Hostrechner bezogenen Dateien werden in der Regel vom Bootloader im RAM des Zielrechners gespeichert, bevor sie in den Flash-Speicher oder einen ähnlichen Solid-State-Speicher auf dem Zielrechner geschrieben werden.

Wie funktioniert ein Bootloader?

Es gibt zwei Arten von Startprozessen für einen Bootloader: einstufige und mehrstufige. Im Allgemeinen verfügen mehrstufige Bootloader über komplexere Funktionen und eine verbesserte Portabilität. Bootloader, die von Solid-State-Speichergeräten gestartet werden, verwenden häufig einen zweistufigen Prozess, der in Stufe 1 und Stufe 2 unterteilt ist: Stufe 1 führt die Hardware-Initialisierung durch, bereitet den Speicherplatz für Stufe 2 vor, kopiert Stufe 2 in den Speicher, richtet den Stack ein und wechselt dann zu Stufe 2.

Bootloader Stufe 1

Initialisierung von Hardwaregeräten

  • Alle Interrupts deaktivieren: Die Verarbeitung von Interrupts obliegt in der Regel den Gerätetreibern des Betriebssystems, sodass der Bootloader während seiner Ausführung Interrupt-Antworten ignorieren kann. Die Interrupt-Maskierung kann durch Ändern des Interrupt-Mask-Registers oder des Statusregisters der CPU (z. B. das CPSR-Register von ARM) erreicht werden.
  • CPU-Geschwindigkeit und Taktfrequenz einstellen.
  • Initialisieren des RAM: Dazu gehört die korrekte Konfiguration der Funktionsregister des Speichercontrollers des Systems und verschiedener Speicherbank-Steuerregister.
  • LED-Initialisierung: LEDs werden häufig über GPIO angesteuert, um den Status des Systems anzuzeigen (OK oder Fehler). Wenn keine LEDs vorhanden sind, kann die Initialisierung von UART zum Drucken des Bootloader-Logos oder von Zeicheninformationen über die serielle Kommunikation diesen Zweck erfüllen.
  • Deaktivieren Sie den internen Befehls-/Datencache der CPU.

RAM-Speicherplatz für das Laden des Bootloaders Stufe 2 vorbereiten

Für eine schnellere Ausführung wird Stufe 2 üblicherweise in den RAM geladen. Daher muss ein verfügbarer Speicherbereich für das Laden der Stufe 2 des Bootloaders zugewiesen werden.

Da Stufe 2 in der Regel C-Sprachcode enthält, muss der benötigte Speicherplatz sowohl die Größe der ausführbaren Datei von Stufe 2 als auch den Stapelspeicher berücksichtigen. Außerdem sollte der Speicherplatz vorzugsweise mit der Speicherseitengröße (in der Regel 4 KB) übereinstimmen. Im Allgemeinen sind 1 MB RAM-Speicherplatz ausreichend. Der spezifische Adressbereich kann beliebig gewählt werden. Ein gängiger Ansatz besteht beispielsweise darin, das ausführbare Image von Stufe 2 für die Ausführung innerhalb eines 1-MB-Speicherbereichs zuzuweisen, der bei der Basisadresse 0xc0200000 des System-RAM beginnt. Es wird jedoch empfohlen, Stufe 2 dem obersten 1 MB des gesamten RAM-Speicherbereichs (d. h. (RamEnd-1MB) – RamEnd) zuzuweisen.

Bezeichnen wir die Größe des zugewiesenen RAM-Speicherbereichs als „stage2_size” (in Byte) und die Start- und Endadressen als „stage2_start” und „stage2_end” (beide Adressen sind an 4-Byte-Grenzen ausgerichtet). Somit gilt:

stage2_end = stage2_start + stage2_size

Darüber hinaus muss unbedingt sichergestellt werden, dass der zugewiesene Adressbereich tatsächlich beschreibbarer und lesbarer RAM-Speicherplatz ist. Um dies zu gewährleisten, ist eine Prüfung des zugewiesenen Adressbereichs erforderlich. Eine geeignete Prüfmethode, wie sie beispielsweise von „blob” verwendet wird, besteht darin, die ersten beiden Wörter jeder Speicherseite auf ihre Lese- und Schreibfähigkeit zu prüfen.

Kopieren Sie die Stufe 2 des Bootloaders in den RAM-Speicher.

Um dies zu tun, stellen Sie bitte zwei Punkte sicher:

  1.  Die Position des ausführbaren Images von Stufe 2 auf dem Solid-State-Speichergerät.
  2. Die Startadresse des RAM-Speicherplatzes.

Stack-Zeiger (SP) setzen

Durch das Setzen des Stack-Pointers (sp) wird die Ausführung von C-Code vorbereitet. In der Regel kann der Wert von sp auf (stage2_end-4) gesetzt werden, was dem obersten Ende des in Abschnitt 3.1.2 zugewiesenen 1-MB-RAM-Speicherplatzes entspricht (der Stack wächst nach unten).

Zusätzlich ist es möglich, vor dem Setzen des Stack-Pointers die LED zu deaktivieren, um den Benutzern zu signalisieren, dass ein Übergang zu Stufe 2 bevorsteht.

Nach diesen Ausführungsschritten sollte das physische Speicherlayout des Systems dem folgenden Diagramm entsprechen.

Bootloader RAM Memory Layout
Bootloader RAM Memory Layout

Springe zum C-Einstiegspunkt von Stufe 2

Nachdem alle oben genannten Schritte abgeschlossen sind, können Sie zur Ausführung von Stage 2 des Bootloaders übergehen. Auf ARM-Systemen kann dies beispielsweise durch Ändern des PC-Registers auf die entsprechende Adresse erfolgen. Die Systemspeicherstruktur, wenn das ausführbare Image von Stage 2 des Bootloaders gerade in den RAM-Speicher kopiert wurde, ist in der obigen Abbildung dargestellt.

Bootloader Stufe 2

Initialisierung von Hardwaregeräten

  • Initialisieren Sie mindestens einen seriellen Port für die E/A-Ausgabekommunikation mit Terminalbenutzern.
  • Initialisieren Sie Timer und andere Hardwarekomponenten.

Vor der Initialisierung dieser Geräte kann auch die LED aufleuchten, um den Beginn der Ausführung der Funktion main() anzuzeigen. Nach der Initialisierung der Geräte können bestimmte Informationen wie Programmnamenszeichenfolgen und Versionsnummern ausgegeben werden.

Erkennung der Systemspeicherzuordnung

Memory Mapping bezeichnet die Zuweisung von Adressbereichen innerhalb des gesamten physischen Adressraums von 4 GB für die Adressierung von System-RAM-Einheiten. Bei der SA-1100-CPU dient beispielsweise ein Adressraum von 512 MB, beginnend bei 0xC000,0000, als RAM-Adressraum des Systems. Bei der Samsung S3C44B0X-CPU wird ein 64 MB großer Adressraum zwischen 0x0c00,0000 und 0x1000,0000 für den RAM-Adressraum des Systems verwendet. Während CPUs in der Regel einen erheblichen Teil des Adressraums für den System-RAM reservieren, wird bei der Konstruktion bestimmter eingebetteter Systeme möglicherweise nicht der gesamte reservierte RAM-Adressraum genutzt. Daher ordnen eingebettete Systeme oft nur einen Teil des reservierten RAM-Adressraums der CPU den RAM-Einheiten zu, sodass ein Teil des reservierten RAM-Adressraums ungenutzt bleibt. Angesichts dieser Tatsache muss die Stufe 2 des Bootloaders die gesamte Speicherzuordnung des Systems überprüfen, bevor sie irgendwelche Aktionen ausführt (z. B. das Lesen eines im Flash-Speicher gespeicherten Kernel-Images in den RAM-Speicher). Sie muss wissen, welche Teile des reservierten RAM-Adressraums der CPU tatsächlich RAM-Adresseneinheiten zugeordnet sind und welche sich in einem „unbenutzten” Zustand befinden.

Beschreibung der Speicherzuordnung

Die folgende Datenstruktur kann verwendet werden, um einen kontinuierlichen Adressbereich im RAM-Adressraum zu beschreiben:

				
					typedef struct memory_area_struct {
    u32 start; /* the base address of the memory region */
    u32 size; /* the byte number of the memory region */
    int used;
} memory_area_t;

				
			

Solche zusammenhängenden Adressbereiche innerhalb des RAM-Adressraums können einen von zwei Zuständen haben:

  1. used=1 gibt an, dass der zusammenhängende Adressbereich implementiert wurde und tatsächlich RAM-Einheiten zugeordnet ist.
  2. used=0 bedeutet, dass der zusammenhängende Adressbereich nicht im System implementiert ist und ungenutzt bleibt.

Basierend auf der oben beschriebenen Datenstruktur memory_area_t kann der gesamte reservierte RAM-Adressraum der CPU durch ein Array vom Typ memory_area_t dargestellt werden, wie unten gezeigt:

				
					memory_area_t memory_map[NUM_MEM_AREAS] = {
    [0 ... (NUM_MEM_AREAS - 1)] = {
        .start = 0,
        .size = 0,
        .used = 0
    },
};

				
			
Speicherzuordnung erkennen

Hier ist ein einfacher, aber effektiver Algorithmus zur Erkennung der Speicherzuordnung innerhalb des gesamten RAM-Adressraums:

				
					/* Initialize the array */
for(i = 0; i < NUM_MEM_AREAS; i++)
    memory_map[i].used = 0;
/* Write 0 to all memory locations */
for(addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE)
    *(u32 *)addr = 0;
for(i = 0, addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE) {
    /*
     * Check whether the address space starting from base address
     * MEM_START + i * PAGE_SIZE, with a size of PAGE_SIZE, is a valid RAM address space.
     */
    Call the algorithm test_mempage() from section 3.1.2;
    if (current memory page is not a valid RAM page) {
        /* no RAM here */
        if (memory_map[i].used )
            i++;
        continue;
    }
     
    /*
     * The current page is a valid address range mapped to RAM.
     * However, we need to determine if it's an alias of some address page within the 4GB address space.
     */
    if (*(u32 *)addr != 0) { /* alias? */
        /* This memory page is an alias of an address page within the 4GB address space. */
        if (memory_map[i].used )
            i++;
        continue;
    }
     
    /*
     * The current page is a valid address range mapped to RAM,
     * and it's not an alias of an address page within the 4GB address space.
     */
    if (memory_map[i].used == 0) {
        memory_map[i].start = addr;
        memory_map[i].size = PAGE_SIZE;
        memory_map[i].used = 1;
    } else {
        memory_map[i].size += PAGE_SIZE;
    }
} /* end of for (…) */

				
			

Nach Ausführung des oben genannten Algorithmus zur Erkennung des Speicherzuordnungsstatus des Systems kann der Bootloader auch detaillierte Informationen zur Speicherzuordnung an den seriellen Port ausgeben.

Kernel- und Root-Dateisystem-Images laden

Speicherlayoutplanung

Dies umfasst zwei Aspekte:

  1. den vom Kernel-Image belegten Speicherbereich;
  2. den vom Root-Dateisystem belegten Speicherbereich. Berücksichtigen Sie bei der Planung des Speicherlayouts die Basisadresse und die Größe der Images.

Das Kernel-Image wird in der Regel in einen Speicherbereich kopiert, der bei (MEM_START + 0x8000) beginnt und etwa 1 MB groß ist (eingebettete Linux-Kernel sind in der Regel kleiner als 1 MB). Warum wird ein Speicherplatz von 32 KB zwischen MEM_START und MEM_START + 0x8000 freigelassen? Der Grund dafür ist, dass der Linux-Kernel bestimmte globale Datenstrukturen in diesem Speichersegment ablegt, z. B. Boot-Parameter und Kernel-Seitentabellen.

Das Root-Dateisystem-Image wird in der Regel an den Speicherort ab MEM_START + 0x0010,0000 kopiert. Wenn ein Ramdisk als Root-Dateisystem-Image verwendet wird, beträgt die unkomprimierte Größe in der Regel etwa 1 MB.

Kopieren aus Flash

Da eingebettete CPUs wie ARM in der Regel Flash- und andere Solid-State-Speichergeräte innerhalb eines einheitlichen Speicheradressraums adressieren, ähnelt das Lesen von Daten aus dem Flash-Speicher dem Lesen aus RAM-Einheiten. Eine einfache Schleife reicht aus, um das Image vom Flash-Gerät zu kopieren:

				
					while (count) {
    *dest++ = *src++; /* they are all aligned with word boundary */
    count -= 4; /* byte number */
};

				
			

Einstellung der Kernel-Boot-Parameter

Nachdem das Kernel-Image und das Root-Dateisystem-Image in den RAM-Speicher kopiert wurden, kann der Start des Linux-Kernels vorbereitet werden. Vor dem Aufruf des Kernels ist jedoch ein Vorbereitungsschritt erforderlich: die Einstellung der Boot-Parameter des Linux-Kernels.

Linux-Kernel ab Version 2.4.x erwarten, dass Boot-Parameter in Form einer getaggten Liste übergeben werden. Die getaggte Liste der Boot-Parameter beginnt mit dem Tag ATAG_CORE und endet mit dem Tag ATAG_NONE. Jedes Tag besteht aus einer tag_header-Struktur, die den Parameter identifiziert, gefolgt von Datenstrukturen, die Parameterwerte enthalten. Die Datenstrukturen tag und tag_header sind in der Header-Datei include/asm/setup.h des Linux-Kernel-Quellcodes definiert.

In eingebetteten Linux-Systemen gehören zu den gängigen Boot-Parametern, die vom Bootloader gesetzt werden müssen, ATAG_CORE, ATAG_MEM, ATAG_CMDLINE, ATAG_RAMDISK und ATAG_INITRD.

So wird beispielsweise ATAG_CORE festgelegt:

				
					params = (struct tag *)BOOT_PARAMS;
params->hdr.tag = ATAG_CORE;
params->hdr.size = tag_size(tag_core);
params->u.core.flags = 0;
params->u.core.pagesize = 0;
params->u.core.rootdev = 0;
params = tag_next(params);

				
			

Hier steht BOOT_PARAMS für die Startbasisadresse der Kernel-Boot-Parameter im Speicher, und der Zeiger params ist vom Typ struct tag. Das Makro tag_next() berechnet die Startadresse des nächsten Tags, das unmittelbar auf das aktuelle Tag folgt. Es ist wichtig zu beachten, dass hier die Geräte-ID für das Root-Dateisystem des Kernels festgelegt wird.

Nachfolgend finden Sie einen Beispielcode zum Festlegen von Speicherzuordnungsinformationen:

				
					for (i = 0; i < NUM_MEM_AREAS; i++) {
    if (memory_map[i].used) {
        params->hdr.tag = ATAG_MEM;
        params->hdr.size = tag_size(tag_mem32);
        params->u.mem.start = memory_map[i].start;
        params->u.mem.size = memory_map[i].size;
        params = tag_next(params);
    }
}

				
			

Im Array memory_map[] entspricht jedes gültige Speichersegment einem ATAG_MEM-Parameter-Tag.

Der Linux-Kernel kann während des Startvorgangs Informationen als Befehlszeilenparameter empfangen. Auf diese Weise können wir Hardware-Parameterinformationen bereitstellen, die der Kernel selbst nicht erkennen kann, oder Informationen überschreiben, die der Kernel erkannt hat. Beispielsweise verwenden wir die Befehlszeilenparameterzeichenfolge „console=ttyS0,115200n8“, um den Kernel anzuweisen, ttyS0 als Konsole mit den Einstellungen „115200 bps, keine Parität, 8 Datenbits“ zu verwenden. Hier ist ein Beispielcode zum Festlegen der Befehlszeilenparameterzeichenfolge des Kernels:

				
					char *p;
for (p = commandline; *p == ' '; p++)
    ;
if (*p == '\0')
    return;
params->hdr.tag = ATAG_CMDLINE;
params->hdr.size = (sizeof(struct tag_header) + strlen(p) + 1 + 4) >> 2;
strcpy(params->u.cmdline.cmdline, p);
params = tag_next(params);

				
			

Beachten Sie, dass im obigen Code bei der Festlegung der Größe von tag_header das abschließende Zeichen „\0” in der Zeichenfolge enthalten sein muss und auf das nächste Vielfache von 4 Byte aufgerundet werden muss, da das Größenelement der tag_header-Struktur die Anzahl der Wörter angibt.

Nachfolgend finden Sie einen Beispielcode für die Einstellung von ATAG_INITRD, der den Speicherort im RAM angibt, an dem sich das initrd-Image (im komprimierten Format) befindet, sowie dessen Größe:

				
					params->hdr.tag = ATAG_INITRD2;
params->hdr.size = tag_size(tag_initrd);
params->u.initrd.start = RAMDISK_RAM_BASE;
params->u.initrd.size = INITRD_LEN;
params = tag_next(params);

				
			

Setzen Sie abschließend das Tag ATAG_NONE, um die gesamte Liste der Startparameter abzuschließen:

				
					static void setup_end_tag(void) {
    params->hdr.tag = ATAG_NONE;
    params->hdr.size = 0;
}
				
			

Aufruf des Kernels

Der Bootloader ruft den Linux-Kernel auf, indem er direkt zur ersten Anweisung des Kernels springt, d. h. direkt zur Adresse MEM_START + 0x8000. Beim Springen müssen die folgenden Bedingungen erfüllt sein:

  1. CPU-Registereinstellungen:
    R0 = 0
    R1 = Maschinentyp-ID (die Maschinentypnummer finden Sie unter linux/arch/arm/tools/mach-types)
    R2 = Startbasisadresse der mit Boot-Parametern versehenen Liste im RAM

  2. CPU-Modus: Interrupts (
    IRQs und FIQs)
    deaktivieren CPU muss sich im SVC-Modus befinden

  3. Cache- und MMU-Einstellungen:
    MMU muss deaktiviert sein
    . Der Befehlscache kann ein- oder ausgeschaltet sein
    . Der Datencache muss ausgeschaltet sein.

Bei Verwendung von C kann der Kernel wie folgt aufgerufen werden:

				
					void (*theKernel)(int zero, int arch, u32 params_addr) =
    (void (*)(int, int, u32))KERNEL_RAM_BASE;

theKernel(0, ARCH_NUMBER, (u32)kernel_params_start);

				
			

Beachten Sie, dass der Funktionsaufruf to theKernel() niemals zurückkehren sollte. Wenn er zurückkehrt, ist ein Fehler aufgetreten.

Bootloader für eingebettete Systeme vs. Bootloader für PCs

In PC-Architekturen besteht der Bootloader aus dem BIOS (im Wesentlichen Firmware) und dem Betriebssystem-Bootloader, der sich im MBR der Festplatte befindet (z. B. LILO, GRUB). Nachdem das BIOS die Hardwareerkennung und die Ressourcenzuweisung abgeschlossen hat, lädt es den Bootloader aus dem MBR der Festplatte in den Arbeitsspeicher des Systems und übergibt die Kontrolle an den Bootloader des Betriebssystems. Die Hauptaufgabe des Bootloaders besteht darin, das Kernel-Image von der Festplatte in den Arbeitsspeicher zu lesen und dann zum Einstiegspunkt des Kernels zu springen, um das Betriebssystem zu starten.

In eingebetteten Systemen gibt es in der Regel kein Firmware-Programm wie BIOS (obwohl einige eingebettete CPUs ein kleines eingebettetes Boot-Programm enthalten können). Folglich wird die gesamte Lade- und Startaufgabe des Systems vom Bootloader ausgeführt. In einem eingebetteten System auf Basis des ARM7TDMI-Kerns beispielsweise startet das System während des Einschaltens oder Zurücksetzens in der Regel die Ausführung an der Adresse 0x00000000, und diese Adresse enthält normalerweise das Bootloader-Programm des Systems.

CPU und Embedded-Board, unterstützt durch Bootloader

Jede CPU-Architektur hat einen anderen Bootloader. Einige Bootloader unterstützen auch CPUs mit mehreren Architekturen. Beispielsweise unterstützt U-Boot sowohl die ARM-Architektur als auch die MIPS-Architektur. Der Bootloader hängt nicht nur von der Architektur der CPU ab, sondern auch von der Konfiguration bestimmter eingebetteter Geräte auf Board-Ebene. Das heißt, der Bootloader ist nicht unbedingt für beide unterschiedlichen eingebetteten Boards geeignet, selbst wenn sie auf derselben CPU basieren.

Bootloader-Installationsmedium

Wenn ein System hochfährt oder zurückgesetzt wird, holen CPUs in der Regel Befehle von einer vom CPU-Hersteller festgelegten Adresse. Beispielsweise holen CPUs, die auf dem ARM7TDMI-Kern basieren, nach einem Reset in der Regel ihren ersten Befehl von der Adresse 0x00000000. Eingebettete Systeme, die auf CPU-Architekturen basieren, ordnen dieser festgelegten Adresse häufig eine Art von Solid-State-Speichergerät (wie ROM, EPROM oder FLASH) zu. Daher führt die CPU nach dem Einschalten des Systems zunächst das Bootloader-Programm aus.

Abonnieren

Tragen Sie sich in unsere Abonnentenliste ein, um monatliche Blog-Updates, Technologie-News und Fallstudien zu erhalten. Wir versenden niemals Spam, und Sie können sich jederzeit wieder abmelden.

Nach oben scrollen

Instant Quote