
Also genau auf den Punkt. Wir werden unter Linux, auf NASM und mit QEMU schreiben. Dies ist einfach zu installieren, überspringen Sie diesen Schritt.
Es versteht sich, dass der Leser mit der Syntax von NASM zumindest auf der Basisebene vertraut ist (es wird hier jedoch nichts besonders Kompliziertes geben) und versteht, was Register sind.
Grundlegende Theorie
Das erste, was den Prozessor startet, wenn der Computer eingeschaltet wird, ist der BIOS-Code (oder UEFI, aber hier werde ich nur über das BIOS sprechen), der im Speicher des Motherboards "verkabelt" ist (insbesondere bei 0xFFFFFFF0).
Unmittelbar nach dem Einschalten des BIOS startet der Power-On Self-Test (POST) - Selbsttest nach dem Einschalten. Das BIOS überprüft den Zustand des Speichers, erkennt und initialisiert die angeschlossenen Geräte, überprüft die Register, bestimmt die Größe des Speichers usw.
Der nächste Schritt besteht darin, die Startdiskette zu identifizieren, von der Sie das Betriebssystem starten können. Eine Bootdiskette ist eine Diskette (oder ein anderes Laufwerk) mit den letzten 2 Bytes des ersten Sektors (der erste Sektor bedeutet die ersten 512 Bytes des Laufwerks, da 1 Sektor = 512 Bytes ist) 55 und AA (im hexadezimalen Format). Sobald eine Bootdiskette gefunden wurde, lädt das BIOS seine ersten 512 Bytes unter der Adresse 0x7c00 in den RAM und überträgt die Steuerung unter dieser Adresse an den Prozessor.
In diesen 512 Bytes funktioniert es natürlich nicht für ein vollwertiges Betriebssystem. Setzen Sie daher normalerweise in diesem Sektor den primären Loader ein, der den Hauptbetriebssystemcode in den RAM lädt und die Steuerung an diesen überträgt.
Der Prozessor lief von Anfang an im Real-Modus (= 16-Bit-Modus). Dies bedeutet, dass es nur mit 16-Bit-Daten arbeiten kann und eine segmentierte Speicheradressierung verwendet und auch nur 1 MB Speicher adressieren kann. Aber wir werden die zweite hier nicht verwenden. Das Bild unten zeigt den RAM-Status beim Übertragen der Steuerung auf unseren Code (das Bild stammt von hier ).

Das Letzte, was vor dem praktischen Teil zu sagen ist, sind Unterbrechungen. Ein Interrupt ist ein spezielles Signal (z. B. von einem Eingabegerät wie einer Tastatur oder einer Maus) an einen Prozessor, das besagt, dass die Ausführung des aktuellen Codes sofort unterbrochen und der Interrupt-Handler-Code ausgeführt werden muss. Alle Adressen von Interrupt-Handlern befinden sich in der Interrupt Descriptor Table (IDT) im Hauptspeicher. Jeder Interrupt hat einen eigenen Interrupt-Handler. Wenn beispielsweise eine Tastaturtaste gedrückt wird, wird ein Interrupt aufgerufen, der Prozessor stoppt, merkt sich die Adresse des unterbrochenen Befehls, speichert alle Werte seiner Register (auf dem Stapel) und fährt mit der Ausführung des Interrupt-Handlers fort. Sobald seine Ausführung endet, stellt der Prozessor die Werte der Register wieder her und springt zurück zum unterbrochenen Befehl und setzt die Ausführung fort.
Um beispielsweise etwas auf dem Bildschirm anzuzeigen, verwendet das BIOS den 0x10-Interrupt (Hexadezimalformat) und den 0x16-Interrupt, um auf das Drücken einer Taste zu warten. In der Tat sind dies alles Interrupts, die wir hier brauchen werden.
Außerdem hat jeder Interrupt seine eigene Unterfunktion, die die Besonderheit seines Verhaltens bestimmt. Um etwas im Textformat (!) Anzuzeigen, müssen Sie den Wert 0x0e in das AH-Register eingeben. Darüber hinaus haben Interrupts ihre eigenen Parameter. 0x10 übernimmt Werte von ah (definiert eine bestimmte Unterfunktion) und al (das zu druckende Zeichen). Auf diese Weise,
mov ah, 0x0e mov al, 'x' int 0x10
zeigt das Zeichen 'x' an. 0x16 nimmt den Wert von ah (spezifische Unterfunktion) und lädt den Wert des eingegebenen Schlüssels in das Register al. Wir werden die 0x0-Funktion verwenden.
Praktischer Teil
Beginnen wir mit dem Hilfecode. Wir benötigen die Funktion zum Vergleichen von zwei Linien und die Funktion zum Anzeigen einer Linie auf dem Bildschirm. Ich habe versucht, die Funktionsweise dieser Funktionen in den Kommentaren so klar wie möglich zu beschreiben.
str_compare.asm:
compare_strs_si_bx: push si ; push bx push ax comp: mov ah, [bx] ; , cmp [si], ah ; ah jne not_equal ; , cmp byte [si], 0 ; , je first_zero ; inc si ; bx si inc bx jmp comp ; first_zero: cmp byte [bx], 0 ; bx != 0, , jne not_equal ; , not_equal mov cx, 1 ; , cx = 1 pop si ; pop bx pop ax ret ; not_equal: mov cx, 0 ; , cx = 0 pop si ; pop bx pop ax ret ;
Die Funktion akzeptiert die SI- und BX-Register als Parameter. Wenn die Linien gleich sind, wird CX auf 1 gesetzt, andernfalls auf 0.
Es ist auch erwähnenswert, dass die Register AX, BX, CX und DX in zwei Einzelbyte-Teile unterteilt sind: AH, BH, CH und DH für das High-Byte und AL, BL, CL und DL für das Low-Byte.
Zunächst versteht es sich, dass es in bx und si Zeiger (!) Gibt (dh die Adresse im Speicher speichert) auf eine Adresse im Speicher, in der sich der Zeilenanfang befindet. Operation [bx] nimmt einen Zeiger von bx, geht zu dieser Adresse und nimmt von dort einen Wert. inc bx bedeutet, dass der Zeiger jetzt auf die Adresse unmittelbar nach der ursprünglichen Adresse verweist.
print_string.asm:
print_string_si: push ax ; ax mov ah, 0x0e ; ah 0x0e, call print_next_char ; pop ax ; ax ret ; print_next_char: mov al, [si] ; cmp al, 0 ; si jz if_zero ; int 0x10 ; al inc si ; jmp print_next_char ; ... if_zero: ret
Als Parameter nimmt die Funktion das SI-Register und druckt byteweise eine Zeichenfolge.
Kommen wir nun zum Hauptcode. Definieren wir zunächst alle Variablen (dieser Code befindet sich ganz am Ende der Datei):
; 0x0d - , 0xa - wrong_command: db "Wrong command!", 0x0d, 0xa, 0 greetings: db "The OS is on. Type 'help' for commands", 0x0d, 0xa, 0xa, 0 help_desc: db "Here's nothing to show yet. But soon...", 0x0d, 0xa, 0 goodbye: db 0x0d, 0xa, "Goodbye!", 0x0d, 0xa, 0 prompt: db ">", 0 new_line: db 0x0d, 0xa, 0 help_command: db "help", 0 input: times 64 db 0 ; - 64 times 510 - ($-$$) db 0 dw 0xaa55
Das Wagenrücklaufzeichen verschiebt den Wagen an den linken Bildschirmrand, dh an den Zeilenanfang.
input: times 64 db 0
bedeutet, dass wir 64 Bytes unter dem Puffer für die Eingabe zuweisen und sie mit Nullen füllen.
Der Rest der Variablen wird benötigt, um einige Informationen anzuzeigen. Weiter unten im Code werden Sie verstehen, warum sie alle benötigt werden.
times 510 - ($-$$) db 0 dw 0xaa55
bedeutet, dass wir die Größe der Ausgabedatei (mit der Erweiterung .bin) explizit auf 512 Bytes festlegen, die ersten 510 Bytes mit Nullen füllen (natürlich werden sie gefüllt, bevor der gesamte Code ausgeführt wird) und die letzten beiden Bytes mit denselben „magischen“ Bytes 55 und AA . $ bedeutet die Adresse der aktuellen Anweisung und $$ ist die Adresse der allerersten Anweisung unseres Codes.
Fahren wir mit dem eigentlichen Code fort:
org 0x7c00 ; (1) bits 16 ; (2) jmp start ; start %include "print_string.asm" ; %include "str_compare.asm" ; ==================================================== start: mov ah, 0x00 ; (3) mov al, 0x03 int 0x10 mov sp, 0x7c00 ; (4) mov si, greetings ; call print_string_si ; mainloop
(1). Dieser Befehl macht NASM klar, dass wir Code ab 0x7c00 ausführen. Dies ermöglicht es, alle Adressen relativ zu dieser Adresse automatisch zu verzerren, so dass wir dies nicht explizit tun.
(2). Dieser Befehl weist NASM an, dass wir im 16-Bit-Modus arbeiten.
(3). Beim Start druckt QEMU viele Informationen, die wir nicht benötigen. Stellen Sie dazu ah 0x00 auf al 0x03 ein und rufen Sie 0x10 auf, um den Bildschirm von allem zu löschen.
(4). Um Register auf dem Stapel zu speichern, müssen Sie mithilfe des SP-Stapelzeigers angeben, an welcher Adresse sich der Scheitelpunkt befindet. SP gibt den Bereich im Speicher an, in den der nächste Wert geschrieben wird. Addiere den Wert zum Stapel - SP geht um 2 Bytes in den Speicher (da wir uns im Real-Modus befinden, in dem alle Registeroperanden 16-Bit-, d. H. Doppelbyte-Werte sind). Wir haben 0x7c00 angegeben, damit die Werte auf dem Stapel direkt neben unserem Code im Speicher gespeichert werden. Noch einmal - der Stapel wächst nach unten (!). Dies bedeutet, je mehr Werte sich auf dem Stapel befinden, desto weniger Speicher zeigt der Zeiger des SP-Stapels an.
mainloop: mov si, prompt ; call print_string_si call get_input ; jmp mainloop ; mainloop...
Hauptschleife. Hier drucken wir bei jeder Iteration das Zeichen ">", woraufhin wir die Funktion get_input aufrufen, die die Arbeit mit Tastaturunterbrechung implementiert.
get_input: mov bx, 0 ; bx input_processing: mov ah, 0x0 ; 0x16 int 0x16 ; ASCII cmp al, 0x0d ; enter je check_the_input ; , , ; cmp al, 0x8 ; backspace je backspace_pressed cmp al, 0x3 ; ctrl+c je stop_cpu mov ah, 0x0e ; - ; int 0x10 mov [input+bx], al ; inc bx ; cmp bx, 64 ; input je check_the_input ; , enter jmp input_processing ;
(1) [Eingabe + bx] bedeutet, dass wir die Adresse des Anfangs der Eingabe des Eingabepuffers nehmen und bx hinzufügen, dh wir erhalten bx + das 1. Element des Puffers.
stop_cpu: mov si, goodbye ; call print_string_si jmp $ ; ; $
Hier ist alles einfach - wenn Sie Strg + C gedrückt haben, führt der Computer die Funktion jmp $ endlos aus.
backspace_pressed: cmp bx, 0 ; backspace , input , je input_processing ; mov ah, 0x0e ; backspace. , int 0x10 ; , mov al, ' ' ; , int 0x10 ; mov al, 0x8 ; int 0x10 ; backspace dec bx mov byte [input+bx], 0 ; input jmp input_processing ;
Um das Zeichen '>' beim Drücken der Rücktaste nicht zu löschen, prüfen wir, ob die Eingabe leer ist. Wenn nicht, dann nichts tun.
check_the_input: inc bx mov byte [input+bx], 0 ; , ; ( '\0' ) mov si, new_line ; call print_string_si mov si, help_command ; si help mov bx, input ; bx - call compare_strs_si_bx ; si bx ( help) cmp cx, 1 ; compare_strs_si_bx cx 1, ; je equal_help ; => ; help jmp equal_to_nothing ; , "Wrong command!"
Hier denke ich, dass aus den Kommentaren alles klar hervorgeht.
equal_help: mov si, help_desc call print_string_si jmp done equal_to_nothing: mov si, wrong_command call print_string_si jmp done
Je nachdem, was eingegeben wurde, wird entweder der Text der Variablen help_desc oder der Text der Variablen false_command angezeigt.
; done input done: cmp bx, 0 ; input je exit ; , mainloop dec bx ; , mov byte [input+bx], 0 jmp done ; exit: ret
Eigentlich lautet der gesamte Code:
prompt.asm:
org 0x7c00 bits 16 jmp start ; start %include "print_string.asm" %include "str_compare.asm" ; ==================================================== start: cli ; , ; mov ah, 0x00 ; mov al, 0x03 int 0x10 mov sp, 0x7c00 ; mov si, greetings ; call print_string_si ; mainloop mainloop: mov si, prompt ; call print_string_si call get_input ; jmp mainloop ; mainloop... get_input: mov bx, 0 ; bx input_processing: mov ah, 0x0 ; 0x16 int 0x16 ; ASCII cmp al, 0x0d ; enter je check_the_input ; , , ; cmp al, 0x8 ; backspace je backspace_pressed cmp al, 0x3 ; ctrl+c je stop_cpu mov ah, 0x0e ; - ; int 0x10 mov [input+bx], al ; inc bx ; cmp bx, 64 ; input je check_the_input ; , enter jmp input_processing ; stop_cpu: mov si, goodbye ; call print_string_si jmp $ ; ; $ backspace_pressed: cmp bx, 0 ; backspace , input , je input_processing ; mov ah, 0x0e ; backspace. , int 0x10 ; , mov al, ' ' ; , int 0x10 ; mov al, 0x8 ; int 0x10 ; backspace dec bx mov byte [input+bx], 0 ; input jmp input_processing ; check_the_input: inc bx mov byte [input+bx], 0 ; , ; ( '\0' ) mov si, new_line ; call print_string_si mov si, help_command ; si help mov bx, input ; bx - call compare_strs_si_bx ; si bx ( help) cmp cx, 1 ; compare_strs_si_bx cx 1, ; je equal_help ; => ; help jmp equal_to_nothing ; , "Wrong command!" equal_help: mov si, help_desc call print_string_si jmp done equal_to_nothing: mov si, wrong_command call print_string_si jmp done ; done input done: cmp bx, 0 ; input je exit ; , mainloop dec bx ; , mov byte [input+bx], 0 jmp done ; exit: ret ; 0x0d - , 0xa - wrong_command: db "Wrong command!", 0x0d, 0xa, 0 greetings: db "The OS is on. Type 'help' for commands", 0x0d, 0xa, 0xa, 0 help_desc: db "Here's nothing to show yet. But soon...", 0x0d, 0xa, 0 goodbye: db 0x0d, 0xa, "Goodbye!", 0x0d, 0xa, 0 prompt: db ">", 0 new_line: db 0x0d, 0xa, 0 help_command: db "help", 0 input: times 64 db 0 ; - 64 times 510 - ($-$$) db 0 dw 0xaa55
Geben Sie den folgenden Befehl ein, um dies alles zu kompilieren:
nasm -f bin prompt.asm -o bootloader.bin
Und wir bekommen die Binärdatei mit unserem Code am Ausgang. Führen Sie nun den QEMU-Emulator mit dieser Datei aus (mit -monitor stdio können Sie den Registerwert jederzeit mit dem Befehl print $ reg anzeigen):
qemu-system-i386 bootloader.bin -monitor stdio
Und wir bekommen die Ausgabe:
