Linux-Kernel-Boot. Teil 1

Vom Bootloader zum Kernel

Wenn Sie die vorherigen Artikel lesen, kennen Sie mein neues Hobby für Low-Level-Programmierung. Ich schrieb mehrere Artikel über Assembler-Programmierung für x86_64 Linux und begann gleichzeitig, in den Quellcode des Linux-Kernels einzutauchen.

Ich bin sehr daran interessiert zu verstehen, wie Dinge auf niedriger Ebene funktionieren: wie Programme auf meinem Computer ausgeführt werden, wie sie sich im Speicher befinden, wie der Kernel Prozesse und Speicher verwaltet, wie der Netzwerkstapel auf niedriger Ebene funktioniert und vieles mehr. Deshalb habe ich beschlossen, eine weitere Artikelserie über den Linux-Kernel für die x86_64-Architektur zu schreiben.

Bitte beachten Sie, dass ich kein professioneller Kernel-Entwickler bin und bei der Arbeit keinen Kernel-Code schreibe. Dies ist nur ein Hobby. Ich mag nur Dinge auf niedriger Ebene und es ist interessant, mich mit ihnen zu beschäftigen. Wenn Sie also Verwirrung oder Fragen / Kommentare bemerken, kontaktieren Sie mich auf Twitter , per E-Mail oder erstellen Sie einfach ein Ticket . Ich wäre dankbar.

Alle Artikel werden im GitHub-Repository veröffentlicht. Wenn etwas mit meinem Englisch oder dem Inhalt des Artikels nicht stimmt, zögern Sie nicht, eine Pull-Anfrage zu senden.

Bitte beachten Sie, dass dies keine offizielle Dokumentation ist, sondern lediglich Schulung und Wissensaustausch.

Erforderliche Kenntnisse

  • C-Code verstehen
  • Grundlegendes zum Assembler-Code (AT & T-Syntax)

In jedem Fall werde ich versuchen, in diesem und den folgenden Artikeln etwas zu erklären, wenn Sie gerade erst anfangen, solche Werkzeuge zu lernen. Okay, wenn die Einführung abgeschlossen ist, ist es Zeit, sich mit dem Linux-Kernel und den Dingen auf niedriger Ebene zu beschäftigen.

Ich habe in den Tagen des Linux 3.18-Kernels angefangen, dieses Buch zu schreiben, und seitdem hat sich viel geändert. Bei Änderungen werde ich die Artikel entsprechend aktualisieren.

Magischer Netzschalter, wie geht es weiter?


Obwohl dies Artikel über den Linux-Kernel sind, haben wir ihn - zumindest in diesem Abschnitt - noch nicht erreicht. Sobald Sie den magischen Netzschalter auf Ihrem Laptop oder Desktop-Computer drücken, funktioniert es. Das Motherboard sendet ein Signal an die Stromversorgung . Nach dem Empfang des Signals versorgt es den Computer mit der erforderlichen Strommenge. Sobald das Motherboard ein "Power OK" -Signal empfängt, versucht es, die CPU zu starten. Er speichert alle verbleibenden Daten in seinen Registern und legt für jeden von ihnen vordefinierte Werte fest.

Prozessoren 80386 und spätere Versionen sollten nach einem Neustart die folgenden Werte in den CPU-Registern haben:

  IP 0xfff0
 CS-Selektor 0xf000
 CS-Basis 0xffff0000 

Der Prozessor beginnt im Real-Modus zu arbeiten . Gehen wir etwas zurück und versuchen, die Speichersegmentierung in diesem Modus zu verstehen. Der Real-Modus wird von allen x86-kompatiblen Prozessoren unterstützt: von 8086 bis zu modernen 64-Bit-Intel-Prozessoren. Der 8086-Prozessor verwendet einen 20-Bit-Adressbus, 0-0xFFFFF er kann mit einem Adressraum von 0-0xFFFFF oder 1 . Es gibt jedoch nur 16-Bit-Register mit einer maximalen Adresse von 2^16-1 oder 0xffff (64 Kilobyte).

Eine Speichersegmentierung ist erforderlich, um den gesamten verfügbaren Adressraum zu nutzen. Der gesamte Speicher ist in kleine Segmente mit einer festen Größe von 65536 Byte (64 KB) unterteilt. Da wir mit 16-Bit-Registern nicht auf Speicher über 64 KB zugreifen können, wurde eine alternative Methode entwickelt.

Die Adresse besteht aus zwei Teilen: 1) einem Segmentwähler mit einer Basisadresse; 2) Versatz von der Basisadresse. Im Real-Modus ist die Basisadresse des Segmentwählers * 16 . Um die physikalische Adresse im Speicher zu erhalten, müssen Sie einen Teil des Segmentwählers mit 16 multiplizieren und den Offset hinzufügen:

   =   * 16 +  

Wenn das CS:IP Register beispielsweise den Wert 0x2000:0x0010 , 0x2000:0x0010 die entsprechende physikalische Adresse wie folgt:

 >>> hex((0x2000 << 4) + 0x0010) '0x20010' 

Wenn Sie jedoch den Selektor des größten Segments und den Offset 0xffff:0xffff , erhalten Sie die Adresse:

 >>> hex((0xffff << 4) + 0xffff) '0x10ffef' 

das sind 65520 Bytes nach dem ersten Megabyte. Da im Real-Modus nur ein Megabyte verfügbar ist, wird 0x00ffef bei deaktivierter A20-Leitung zu 0x00ffef .

Nun wissen wir etwas über den Real-Modus und die Speicheradressierung in diesem Modus. Kehren wir zur Diskussion der Registerwerte nach dem Zurücksetzen zurück.

Das CS Register besteht aus zwei Teilen: einem sichtbaren Segmentwähler und einer versteckten Basisadresse. Obwohl die Basisadresse normalerweise durch Multiplizieren des Werts des Segmentwählers mit 16 gebildet wird, ist während eines Hardware-Resets der Segmentwähler im CS-Register 0xf000 und die Basisadresse 0xffff0000 . Der Prozessor verwendet diese spezielle Basisadresse, bis sich die CS ändert.

Die Startadresse wird durch Hinzufügen der Basisadresse zum Wert im EIP-Register gebildet:

 >>> 0xffff0000 + 0xfff0 '0xfffffff0' 

Wir erhalten 0xfffffff0 , was 16 Bytes unter 4 GB liegt. Dieser Punkt wird als Rücksetzvektor bezeichnet . Dies ist die Stelle im Speicher, an der die CPU nach einem Zurücksetzen auf die Ausführung des ersten Befehls wartet: eine Sprungoperation ( jmp ), die normalerweise den BIOS-Einstiegspunkt angibt. Wenn Sie sich beispielsweise den Quellcode von coreboot ( src/cpu/x86/16bit/reset16.inc ) src/cpu/x86/16bit/reset16.inc , werden wir sehen:

  .section ".reset", "ax", %progbits .code16 .globl _start _start: .byte 0xe9 .int _start16bit - ( . + 2 ) ... 

Hier sehen wir den Operationscode ( jmp ) jmp , nämlich 0xe9 , und die Zieladresse _start16bit - ( . + 2) .

Wir sehen auch, dass der reset Abschnitt 16 Bytes umfasst und kompiliert wird, um von der Adresse 0xfffff0 ( src/cpu/x86/16bit/reset16.ld ) ausgeführt zu werden:

 SECTIONS { /* Trigger an error if I have an unuseable start address */ _bogus = ASSERT(_start16bit >= 0xffff0000, "_start16bit too low. Please report."); _ROMTOP = 0xfffffff0; . = _ROMTOP; .reset . : { *(.reset); . = 15; BYTE(0x00); } } 

Das BIOS wird jetzt gestartet. Nach dem Initialisieren und Überprüfen der BIOS-Hardware müssen Sie das Startgerät finden. Die Startreihenfolge wird in der BIOS-Konfiguration gespeichert. Beim Versuch, von der Festplatte zu booten, versucht das BIOS, den Bootsektor zu finden. Auf MBR-partitionierten Festplatten wird der Startsektor in den ersten 446 Bytes des ersten Sektors gespeichert, wobei jeder Sektor 512 Bytes umfasst. Die letzten beiden Bytes des ersten Sektors sind 0x55 und 0xaa . Sie zeigen dem BIOS, dass es sich um ein Startgerät handelt.

Zum Beispiel:

 ; ; :       Intel x86 ; [BITS 16] boot: mov al, '!' mov ah, 0x0e mov bh, 0x00 mov bl, 0x07 int 0x10 jmp $ times 510-($-$$) db 0 db 0x55 db 0xaa 

Wir sammeln und betreiben:

nasm -f bin boot.nasm && qemu-system-x86_64 boot

QEMU erhält den Befehl, die soeben erstellte boot Binärdatei als Disk-Image zu verwenden. Da die oben generierte Binärdatei die Anforderungen des Bootsektors erfüllt (beginnend bei 0x7c00 und endend mit einer magischen Sequenz), betrachtet QEMU die Binärdatei als Master Boot Record (MBR) des Disk-Images.

Sie werden sehen:



In diesem Beispiel sehen wir, dass der Code im 16-Bit-Real-Modus ausgeführt wird und an der Adresse 0x7c00 im Speicher beginnt. Nach dem Start wird ein 0x10- Interrupt ausgelöst , der einfach ein Zeichen druckt ! ;; Füllt die verbleibenden 510 Bytes mit Nullen und endet mit zwei magischen Bytes 0xaa und 0x55 .

Sie können den binären Speicherauszug mit dem Dienstprogramm objdump anzeigen:

nasm -f bin boot.nasm
objdump -D -b binary -mi386 -Maddr16,data16,intel boot


Natürlich gibt es im realen Bootsektor Code, um den Bootvorgang fortzusetzen, und eine Partitionstabelle anstelle einer Reihe von Nullen und einem Ausrufezeichen :). Ab diesem Moment überträgt das BIOS die Kontrolle an den Bootloader.

Hinweis : Wie oben erläutert, befindet sich die CPU im Real-Modus. wobei die Berechnung der physikalischen Adresse im Speicher wie folgt ist:

   =   * 16 +  

Wir haben nur 16-Bit-Allzweckregister, und der Maximalwert des 16-Bit-Registers ist 0xffff . Bei den größten Werten ist das Ergebnis also:

 >>> hex((0xffff * 16) + 0xffff) '0x10ffef' 

Dabei ist 0x10ffef 1 + 64 - 16 . Der 8086-Prozessor (der erste Prozessor mit Real-Modus) verfügt über eine 20-Bit-Adressleitung. Da 2^20 = 1048576 , beträgt der tatsächlich verfügbare Speicher 1 MB.

Im Allgemeinen lautet die Speicheradressierung im Real-Modus wie folgt:

  0x00000000 - 0x000003FF - Tabelle der Interruptvektoren des Realmodus
 0x00000400 - 0x000004FF - BIOS-Datenbereich
 0x00000500 - 0x00007BFF - wird nicht verwendet
 0x00007C00 - 0x00007DFF - unser Bootloader
 0x00007E00 - 0x0009FFFF - wird nicht verwendet
 0x000A0000 - 0x000BFFFF - Video-RAM (VRAM) 
 0x000B0000 - 0x000B7777 - monochromer Videospeicher
 0x000B8000 - 0x000BFFFF - Videospeicher im Farbmodus
 0x000C0000 - 0x000C7FFF - Video-ROM-BIOS
 0x000C8000 - 0x000EFFFF - Schattenbereich (BIOS Shadow)
 0x000F0000 - 0x000FFFFF - System-BIOS 

Am Anfang des Artikels steht, dass sich der erste Befehl für den Prozessor bei 0xFFFFFFF0 , was viel mehr als 0xFFFFF (1 MB) ist. Wie kann die CPU im Real-Modus auf diese Adresse zugreifen? Antwort in der Coreboot- Dokumentation:

0xFFFE_0000 - 0xFFFF_FFFF: 128 ROM

Zu Beginn der Ausführung befindet sich das BIOS nicht im RAM, sondern im ROM.

Bootloader


Der Linux-Kernel kann mit verschiedenen Bootloadern wie GRUB 2 und Syslinux geladen werden . Der Kernel verfügt über ein Boot-Protokoll, das die Bootloader-Anforderungen für die Implementierung der Linux-Unterstützung definiert. In diesem Beispiel arbeiten wir mit GRUB 2.

Das BIOS setzte den Startvorgang fort, wählte das Startgerät aus und übertrug die Kontrolle auf den Startsektor . Die Ausführung beginnt mit boot.img . Aufgrund seiner begrenzten Größe ist dies ein sehr einfacher Code. Es enthält einen Zeiger, um zum Haupt-GRUB 2-Image zu gelangen. Es beginnt mit diskboot.img und wird normalerweise unmittelbar nach dem ersten Sektor in nicht verwendetem Speicherplatz vor der ersten Partition gespeichert. Der obige Code lädt den Rest des Images in den Speicher, der den GRUB 2-Kernel und Treiber für die Verarbeitung von Dateisystemen enthält. Danach wird die Funktion grub_main ausgeführt .

Die Funktion grub_main initialisiert die Konsole, gibt die Basisadresse für die Module zurück, legt das Root-Gerät fest, lädt / analysiert die Grub-Konfigurationsdatei, lädt die Module usw. Am Ende der Ausführung wird grub in den normalen Modus versetzt. Die Funktion grub_normal_execute (aus der grub_normal_execute grub-core/normal/main.c ) schließt die letzten Vorbereitungen ab und zeigt ein Menü zur Auswahl des Betriebssystems an. Wenn wir einen der Grub-Menüpunkte auswählen, wird die Funktion grub_menu_execute_entry , die den Befehl grub boot ausführt und das ausgewählte Betriebssystem lädt.

Wie im Kernel-Boot-Protokoll angegeben, muss der Bootloader einige Felder des Kernel-Installations-Headers lesen und ausfüllen, der mit dem Offset 0x01f1 vom Kernel-Installationscode beginnt. Dieser Offset wird im Linker-Skript angegeben . Der Kernel-Header arch / x86 / boot / header.S beginnt mit:

  .globl hdr hdr: setup_sects: .byte 0 root_flags: .word ROOT_RDONLY syssize: .long 0 ram_size: .word 0 vid_mode: .word SVGA_MODE root_dev: .word 0 boot_flag: .word 0xAA55 

Der Bootloader sollte diesen und andere Header (die im Linux-Bootprotokoll wie in diesem Beispiel nur als Typ write gekennzeichnet sind) mit Werten füllen, die von der Befehlszeile empfangen oder beim Boot berechnet werden. Jetzt werden wir nicht auf die Beschreibungen und Erklärungen für alle Headerfelder eingehen. Wir werden später diskutieren, wie der Kernel sie verwendet. Eine Beschreibung aller Felder finden Sie im Download-Protokoll .

Wie Sie im Kernel-Boot-Protokoll sehen können, wird der Speicher wie folgt angezeigt:

  |  Geschützter Kernel-Modus |
 100000 + ------------------------ +
          |  E / A-Zuordnung |
 0A0000 + ------------------------ +
          |  Reserve  für BIOS |  Lass so viel wie möglich frei
          ~ ~
          |  Befehlszeile |  (kann auch unter X + 10000 liegen)
 X + 10000 + ------------------------ +
          |  Stapel / Haufen |  Um echten Kernel-Modus-Code zu verwenden
 X + 08000 + ------------------------ +
          |  Kernel-Installation |  Kernel Real Mode Code
          |  Kernel Boot Sektor |  Legacy-Kernel-Bootsektor
        X + ------------------------ +
          |  Lader |  <- Einstiegspunkt 0x7C00 Bootsektor
 001000 + ------------------------ +
          |  Reserve  für MBR / BIOS |
 000800 + ------------------------ +
          |  Normalerweise verwenden  MBR |
 000600 + ------------------------ +
          |  Gebraucht  Nur BIOS |
 000000 + ------------------------ +

Wenn der Loader die Kontrolle an den Kernel überträgt, beginnt er mit der Adresse:

 X + sizeof (KernelBootSector) + 1 

Dabei ist X die Adresse des Kernel-Bootsektors. In unserem Fall ist X 0x10000 , wie im Speicherauszug zu sehen ist:



Der Bootloader hat den Linux-Kernel in den Speicher verschoben, die Header-Felder ausgefüllt und dann an die entsprechende Speicheradresse verschoben. Jetzt können wir direkt zum Kernel-Installationscode gehen.

Beginn der Kernel-Installationsphase


Endlich sind wir im Kern! Obwohl technisch läuft es noch nicht. Zunächst muss der Kernel-Installationsteil etwas konfigurieren, einschließlich eines Dekomprimierers und einiger Dinge mit Speicherverwaltung. Nach all dem wird sie den wahren Kern auspacken und dorthin gehen. Die Installation beginnt in arch / x86 / boot / header.S mit dem Zeichen _start .

Auf den ersten Blick mag dies etwas seltsam erscheinen, da mehrere Anweisungen vor ihm liegen. Vor langer Zeit hatte der Linux-Kernel einen eigenen Bootloader. Wenn Sie jetzt zum Beispiel ausführen,

qemu-system-x86_64 vmlinuz-3.18-generic

Sie werden sehen:



Tatsächlich beginnt die Datei header.S mit der magischen Nummer MZ (siehe Screenshot des Dumps oben), dem Text der Fehlermeldung und dem PE- Header:

 #ifdef CONFIG_EFI_STUB # "MZ", MS-DOS header .byte 0x4d .byte 0x5a #endif ... ... ... pe_header: .ascii "PE" .word 0 

Es wird benötigt, um ein Betriebssystem mit UEFI- Unterstützung zu laden. Wir werden das Gerät in den folgenden Kapiteln betrachten.

Tatsächlicher Einstiegspunkt für die Installation des Kernels:

 // header.S line 292 .globl _start _start: 

Der Bootloader (grub2 und andere) kennt diesen Punkt (Offset 0x200 von MZ ) und geht direkt zu diesem Punkt, obwohl header.S vom Abschnitt .bstext , in dem sich der Text der Fehlermeldung befindet:

 // // arch/x86/boot/setup.ld // . = 0; // current position .bstext : { *(.bstext) } // put .bstext section to position 0 .bsdata : { *(.bsdata) } 

Einstiegspunkt für die Kernelinstallation:

  .globl _start _start: .byte 0xeb .byte start_of_setup-1f 1: // // rest of the header // 

Hier sehen wir den Operationscode jmp ( 0xeb ), der zum Punkt start_of_setup-1f . In der Nf Notation bezieht sich 2f beispielsweise auf das lokale Label 2: In unserem Fall ist dies die Bezeichnung 1 , die unmittelbar nach dem Übergang vorhanden ist und den Rest des Setup-Headers enthält. Unmittelbar nach dem Installationsheader sehen wir den Abschnitt .entrytext , der mit der Bezeichnung start_of_setup beginnt.

Dies ist der erste tatsächlich ausgeführte Code (außer natürlich den vorherigen Sprunganweisungen). Nachdem ein Teil der jmp die Kontrolle vom Loader erhalten hat, befindet sich der erste jmp Befehl am Offset 0x200 ab dem Beginn des realen Kernelmodus, 0x200 nach den ersten 512 Bytes. Dies ist sowohl im Linux-Kernel-Boot-Protokoll als auch im grub2-Quellcode zu sehen:

 segment = grub_linux_real_target >> 4; state.gs = state.fs = state.es = state.ds = state.ss = segment; state.cs = segment + 0x20; 

In unserem Fall 0x10000 der Kernel unter der Adresse 0x10000 . Dies bedeutet, dass die Segmentregister nach dem Start der Kernelinstallation die folgenden Werte haben:

gs = fs = es = ds = ss = 0x10000
cs = 0x10200


Nach dem start_of_setup von start_of_setup Kernel Folgendes tun:


Mal sehen, wie das umgesetzt wird.

Segmentfallausrichtung


Zunächst prüft der Kernel, ob die Segmentregister ds und es auf dieselbe Adresse verweisen. Anschließend wird das Richtungsflag mit der cld :

  movw %ds, %ax movw %ax, %es cld 

Wie ich bereits geschrieben habe, lädt grub2 standardmäßig den Kernel-Installationscode bei 0x10000 und cs bei 0x10200 , da die Ausführung nicht am Anfang der Datei beginnt, sondern beim Übergang hier:

 _start: .byte 0xeb .byte start_of_setup-1f 

Dies ist ein Versatz von 512 Byte von 4d 5a . Wie bei allen anderen Segmentregistern ist es auch erforderlich, cs von 0x10200 bis 0x10000 auszurichten. Danach installieren Sie den Stack:

  pushw %ds pushw $6f lretw 

Dieser Befehl schiebt den ds Wert auf den Stapel, gefolgt von der Adresse des Etiketts 6 und dem lretw , der die Adresse des Etiketts 6 in das Register des Befehlszählers lädt und cs mit dem Wert ds lädt. Danach haben ds und cs die gleichen Werte.

Stack-Setup


Fast der gesamte Code ist Teil des Prozesses zur Vorbereitung der C-Umgebung im Real-Modus. Der nächste Schritt besteht darin, den ss Registerwert zu überprüfen und den richtigen Stapel zu erstellen, wenn der ss Wert falsch ist:

  movw %ss, %dx cmpw %ax, %dx movw %sp, %dx je 2f 

Dies kann drei verschiedene Szenarien auslösen:

  • ss gültigen Wert von 0x1000 (wie bei allen anderen Registern außer cs )
  • ss ungültigen Wert und das CAN_USE_HEAP Flag CAN_USE_HEAP gesetzt (siehe unten)
  • ss ungültigen Wert und das CAN_USE_HEAP Flag CAN_USE_HEAP nicht gesetzt (siehe unten)

Betrachten Sie alle Szenarien der Reihe nach:

  • ss gültigen Wert ( 0x1000 ). In diesem Fall gehen wir zu Label 2:

 2: andw $~3, %dx jnz 3f movw $0xfffc, %dx 3: movw %ax, %ss movzwl %dx, %esp sti 

Hier setzen wir die Ausrichtung des dx Registers (das den vom Bootloader angegebenen sp Wert enthält) auf 4 Bytes und prüfen auf Null. Wenn es Null ist, geben wir den Wert 0xfffc dx ( 4 Byte-ausgerichtete Adresse vor der maximalen Segmentgröße von 64 KB). Wenn es nicht gleich Null ist, verwenden wir weiterhin den vom Bootloader angegebenen sp Wert (in unserem Fall 0xf7f4 ). Dann setzen wir den ax Wert in ss , wodurch die richtige Segmentadresse 0x1000 und der richtige sp . Jetzt haben wir den richtigen Stapel:



  • Im zweiten Szenario ist ss != ds . Zuerst setzen wir den Wert _end (die Adresse des Endes des Installationscodes) in dx und überprüfen die loadflags des Headerfelds. Verwenden Sie die Anweisung testb um zu überprüfen, ob der Heap verwendet werden kann. loadflags ist ein Bitmasken-Header, der wie folgt definiert ist:

 #define LOADED_HIGH (1<<0) #define QUIET_FLAG (1<<5) #define KEEP_SEGMENTS (1<<6) #define CAN_USE_HEAP (1<<7) 

und wie im Boot-Protokoll angegeben:

: loadflags

.

7 (): CAN_USE_HEAP
1, ,
heap_end_ptr . ,
.


Wenn das CAN_USE_HEAP Bit gesetzt ist, CAN_USE_HEAP wir in dx den Wert heap_end_ptr (der auf _end ) und fügen STACK_SIZE (die minimale STACK_SIZE beträgt 1024 Bytes). Gehen Sie danach zu Etikett 2 (wie im vorherigen Fall) und erstellen Sie den richtigen Stapel.



  • Wenn CAN_USE_HEAP nicht festgelegt ist, verwenden Sie einfach den Mindeststapel von _end bis _end + STACK_SIZE :



BSS-Setup


Bevor Sie mit dem Haupt-C-Code fortfahren, sind zwei weitere Schritte erforderlich: Hiermit wird der BSS-Bereich eingerichtet und die „magische“ Signatur überprüft. Überprüfung der Unterschrift zuerst:

  cmpl $0x5a5aaa55, setup_sig jne setup_bad 

Die Anweisung vergleicht setup_sig einfach mit der magischen Zahl 0x5a5aaa55. Wenn sie nicht gleich sind, wird ein schwerwiegender Fehler gemeldet.

Wenn die magische Zahl übereinstimmt und wir einen Satz korrekter Segmentregister und einen Stapel haben, müssen Sie nur noch den BSS-Abschnitt konfigurieren, bevor Sie mit dem C-Code fortfahren.

Der BSS-Abschnitt wird zum Speichern statisch zugeordneter nicht initialisierter Daten verwendet. Linux prüft sorgfältig, ob dieser Speicherbereich zurückgesetzt wird:

  movw $__bss_start, %di movw $_end+3, %cx xorl %eax, %eax subw %di, %cx shrw $2, %cx rep; stosl 

Zunächst wird die Startadresse von __bss_start nach di verschoben. Dann wird die Adresse _end + 3 (+3 für die Ausrichtung um 4 Bytes) nach cx verschoben. Das eax Register wird gelöscht (unter Verwendung des xor Befehls), die Größe der bss-Partition ( cx-di ) wird berechnet und in cx platziert. Dann wird cx in vier geteilt (die Größe des „Wortes“) und der stosl wird stosl , wobei der Wert (Null) in der auf di zeigenden Adresse gespeichert wird, di automatisch um vier erhöht wird und dies wiederholt wird, bis Null erreicht). Der Nettoeffekt dieses Codes besteht darin, dass Nullen in alle Wörter im Speicher von __bss_start bis _end :



Gehe zur Hauptstraße


Das war's: Wir haben einen Stack und BSS, also können Sie zur main() C-Funktion gehen:

  calll main 

Die Funktion main() befindet sich in arch / x86 / boot / main.c. Wir werden im nächsten Teil über sie sprechen.

Fazit


Dies ist das Ende des ersten Teils über das Linux-Kernelgerät. , , . C, Linux, , memset , memcpy , earlyprintk , .

Referenzen


Source: https://habr.com/ru/post/de428664/


All Articles