Lowlevel
Lowlevel => Lowlevel-Coding => Thema gestartet von: Survari am 11. May 2021, 16:18
-
Spiele in den 512 Bytes des Boot-Sektors unterzubringen ist keine Neuheit, daher wollte ich auch mal etwas kleines probieren. Wie mit meinem letzten Projekt, bei dem ich hier um Rat gebeten habe, versuche ich etwas mehr über die Funktionsweise von Rechnern und x86-Assembly zu erfahren. Aktuell ist es erstmal mein Ziel, ein Rechteck mit Pfeiltasten über den Bildschirm zu bewegen.
Gesagt, getan. Das Programm läuft in QEMU super, Eingabe und Anzeige funktionieren wie gewollt. Jetzt das Problem: Es läuft nur auf QEMU (und Bochs). Sobald ich das Programm von einem USB-Stick auf meinem Rechner starte, macht es gar nichts mehr (Bildschirm bleibt leer, Cursor blinkt). Dabei ist mir ein möglicher Zusammenhang aufgefallen (bitte schlagt mich nicht, es folgen gefährliches Halbwissen und wahrscheinlich haufenweise falsche Annahmen): Es gibt Ausnahmen. Ich hatte viel Zeit das Programm an vielen Computern meiner Schule zu testen. Dabei ist mir aufgefallen, dass besonders Computer, die neuer waren, das Programm nicht richtig ausgeführt haben. Ich hatte die Überlegung, dass es möglicherweise am vom UEFI simulierten BIOS liegen könnte. Auf Rechnern ohne UEFI funktioniert es tatsächlich (zumindest gehe ich davon aus, dass die jeweiligen Rechner kein UEFI hatten, ich bin mir unsicher, wie genau ich das herausfinde). Soweit ich das beurteilen kann, würde ich außerdem eine Inkompatibilität aufgrund der Prozessorarchitektur ausschließen, da die meisten Computer auf x86/x64 laufen und sonst wahrscheinlich gar nichts passiert wäre (es gab Phasen, in denen Teile des Programmes auch auf den UEFI-Rechnern funktioniert haben, daher schließe ich das Problem mit den Architekturen aus).
Aktuell hat das Programm nette 118 Zeilen (NASM):
K_LEFT equ 75
K_RIGHT equ 77
K_UP equ 72
K_DOWN equ 80
MEM_VIDEO equ 0xb800
SCREEN_WIDTH equ 80
SCREEN_HEIGHT equ 25
TICK_LENGTH equ 4
org 0x7C00
bits 16
init:
mov ax, 0x0002 ; 16 farben, 80x25 Zeichen
int 0x10
; -- Stack initialisieren
xor ax, ax
mov ds, ax
mov ss, ax
mov sp, 0x9c00
mov ax, MEM_VIDEO ; zeiger auf bildschirmspeicher
mov es, ax ; ES=MEM_VIDEO
mov al, 0x03
int 0x10
mov ah, 1
mov ch, 0x26
int 0x10
main:
; -- Bildschirm löschen und neu zeichnen
call clear_screen
call draw
; -- Eingabe und zurück zum Anfang springen
call input
jmp main
input: ; steuerung
xor ah, ah
int 0x16 ; tasteneingabe
cmp ah, K_RIGHT ; Pfeiltaste nach Rechts
je .right_arrow
cmp ah, K_LEFT ; Pfeiltaste nach Links
je .left_arrow
cmp ah, K_DOWN ; Pfeiltaste nach Unten
je .down_arrow
cmp ah, K_UP ; Pfeiltaste nach Oben
je .up_arrow
jmp .end
.right_arrow:
cmp word [x_pos], word SCREEN_WIDTH-2
jg .end ; x_pos > SCREEN_WIDTH-2, keine veränderänderung
add word [x_pos], 1
jmp .end
.left_arrow:
cmp word [x_pos], word 1
jl .end ; x_pos < 1, keine veränderung
sub word [x_pos], 1
jmp .end
.down_arrow:
cmp word [y_pos], word SCREEN_HEIGHT-2
jg .end ; y_pos > SCREEN_HEIGHT-2, keine veränderung
add word [y_pos], 1
jmp .end
.up_arrow:
cmp word [y_pos], word 1
jl .end ; y_pos < 1, keine veränderung
sub word [y_pos], 1
jmp .end
.end:
ret
clear_screen:
; -- Ganzen Bildschirm mit Schwarz füllen
xor di, di
mov cx, 0x07d0
mov ax, 0x0000
rep stosw
ret
draw: ; Block an der Stelle aus [x_pos] und [y_pos] zeichnen
; -- Umwandeln der Koordinaten in Index für den Bildschirmspeicher, Ergebnis in dx
mov ax, word [y_pos]
mov bx, 160
mul bx
mov dx, ax
mov ax, word [x_pos]
add dx, ax
add dx, ax
; -- Datenindex setzen und zeichnen
mov di, dx
mov ax, 0xa020
stosw
ret
done:
jmp $ ; Endlosschleife am Ende (was eigentlich nie erreicht wird)
x_pos: times 10 dw 0
y_pos: times 10 dw 0
times 510-($-$$) db 0
dw 0xaa55 ; Signatur für's BIOS
Meine Frage dazu wäre dann wahrscheinlich klar: Warum läuft das Programm nicht? Gibt es Unterschiede am vom UEFI simulierten BIOS? Liegt es vielleicht an etwas völlig Anderem? Ich bin für jede Hilfe und Erklärung dankbar!
-
Spontan würde ich mal den Stack initialisieren, bevor ich das BIOS aufrufe. Dein "int 10h" sollte zwar einen eigenen Stack benutzen, aber garantiert ist das m.W. nicht. Vor allem nicht bei modernen VBIOS-UEFI-Fallback-Implementationen, die garantiert nicht mehr im Detail auf sowas getestet werden.
Als nächstes fällt mir das hier auf:
mov ax, MEM_VIDEO ; zeiger auf bildschirmspeicher
mov es, ax ; ES=MEM_VIDEO
mov al, 0x03
int 0x10
Damit rufst du int 10h / AH=B8h / AL=03h auf. RBIL kennt diese Funktion nicht; kann gut sein, dass dein Video-BIOS dir das übel nimmt.
Ich kann mir gut vorstellen, dass die Legacy-VBIOS-Emulation auf UEFI-Systemen von Intels Referenzimplementation stammt und sich auf so ziemlich allen UEFI-Systemen ähnlich verhält (ein generisches VBIOS für den UEFI-Grafiktreiber). Das würde so ein Verhalten erklären.
Gruß,
Svenska
-
Hi Svenska, danke für die Antwort!
Ich habe jetzt die Initialisierung des Stacks nach oben verschoben und den Interrupt mit 0xb8 entfernt. (Der war überflüssig, wahrscheinlich noch ein Überbleibsel von irgendetwas und ich habe vergessen es zu entfernen.) Läuft wieder in der Simulation, doch die PCs starten jetzt neu. (Aufgrund von Fehlern? Ich sehe sonst keinen Interrupt, der das rechtfertigen würde.)
So sieht der init-Abschnitt jetzt bei mir aus:
init:
; -- Stack initialisieren
xor ax, ax
mov ds, ax
mov ss, ax
mov sp, 0x9c00
mov ax, MEM_VIDEO ; zeiger auf bildschirmspeicher
mov es, ax ; ES=MEM_VIDEO
mov ax, 0x0003 ; 16 farben, 80x25 Zeichen
int 0x10
mov ah, 1
mov ch, 0x26
int 0x10
-
Du setzt DS und SS auf 0x0000. Das ist unklug, denn da befindet sich die IVT (Interruptvektortabelle).
In UEFI-Code habe ich noch nicht gewühlt, aber zumindest DOSBox und mein :YES schalten in ihren Interrupthandlern explizit Interrupts ein. Wenn du ungünstig an der IVT gespielt hast, dürfte der nächste Timerinterrupt das System neu starten.
Dein Code wird an die lineare Adresse 0x07C00 geladen, aber das BIOS kann daraus verschiedene Segment:Offset-Paare erzeugen. Du solltest das erstmal normalisieren. Im "tiny"-Speichermodell setzt du dann CS=DS=SS und hast dort erstmal garantiert 64 KB Speicher, in dem du arbeiten kannst.
Also ungefähr so:
; CS:IP ist unbekannt
.org 0x0000
mov ax, 0x07C0
mov ds, ax
mov ss, ax
mov sp, 0xFFFE
jmp WORD 0x07C0:main
main:
; CS=DS=SS=0x07C0
; hier geht's los
-
Dein Code wird an die lineare Adresse 0x07C00 geladen, aber das BIOS kann daraus verschiedene Segment:Offset-Paare erzeugen. Du solltest das erstmal normalisieren.
Danke dir, das scheint das Problem gewesen zu sein! Aber kannst du das vielleicht etwas genauer erklären oder mich auf eine Seite verweisen, damit ich mich da mal näher zu belesen kann? Ich verstehe noch nicht genau, warum man das machen muss und was dahinter steckt.
-
Das hängt mit der Speicheradressierung im Real Mode zusammen, und die wiederum ist eine Eigenheit des 8086/8088 und seiner Nachfolger. Der 8086 hat einen 20 Bit-Adressbus (kann also 1 MB adressieren), aber nur 16 Bit-Register (kann also nur 64 KB am Stück adressieren).
Weil ein Adressregister - einschließlich des Instruction Pointers IP - nur 64 KB am Stück adressieren kann, wird der Speicher in "Segmente" aufgeteilt. Jedes dieser Segmente ist 64 KB groß und hat eine Nummer (die steht in den Segmentregistern). Jetzt haben die Segmentregister aber auch 16 Bit, und zusammen mit einem normalen Adressregister kommt man auf 32 Bit - aber der Chip hat nur 20 Adressbits.
Also hat Intel die Segmente nicht brav hintereinander gelegt, sondern mit einem Abstand von jeweils 16 Bytes übereinander:
- Segment 0 geht von 0x00000 bis 0x0FFFF (64 KB ab Adresse 0)
- Segment 1 geht von 0x00010 bis 0x1000F (64 KB ab Adresse 16)
- Segment 2 geht von 0x00020 bis 0x1001F (64 KB ab Adresse 32)
- Segment 3 geht von 0x00030 bis 0x1003F (64 KB ab Adresse 48)
- ... und so weiter ...
- Segment 61439 geht von 0xEFFF0 bis 0xFFFEF (64 KB ab Adresse 983024)
- Segment 61440 geht von 0xF0000 bis 0xFFFFF (64 KB ab Adresse 983040)
- Segment 61441 geht von 0xF0010 bis 0x10000F (64 KB ab Adresse 983056)
- ... und so weiter ...
- Segment 65535 geht von 0xFFFF0 bis 0x10FFEF (64 KB ab Adresse 1048560)
Eine Nebenwirkung dieses Systems ist, dass die Segmente ab 61441 (Segment-Nummer 0xF001) hinter den 20 Bit-Adressraum zeigen (beachte den Übertrag in der Hex-Darstellung). Da der Chip aber nur 20 Adresspins hat, fällt das obere Bit weg und diese Segmente zeigen wieder auf das erste Segment.
Das heißt, die zweiten 16 Byte von Segment 0 (0x0000:0x0010 bis 0x0000:0x001F) sind gleichzeitig die ersten 16 Byte von Segment 1 (0x0001:0x0000 bis 0x0001:0x000F) und die letzten 16 Byte von Segment 61442 (0xF002:0xFFF0 bis 0xF002:0xFFFF) und die vorletzten 16 Byte von Segment 61441 und so weiter und so fort.
Alle diese Segmente überlagern sich. Das heißt, es gibt viele Adressen, die auf das gleiche Byte zeigen. Innerhalb eines Segments gibt es nur eine Adresse für jedes Byte (die Segment-Nummer ist ja festgelegt), aber es gibt 4096 Segmente, die auf dieses Byte zeigen können. Und in jedem dieser Segmente heißt das Byte anders.
Dein BIOS lädt den Bootloader an Adresse 0x07C00, aber du kennst den Wert von CS nicht. Das kann CS=0x0000 sein (dann ist IP=0x7C00), es kann aber auch CS=0x07C0 sein (dann ist IP=0x0000). Oder CS=0x03E0 (dann ist IP=0x3E00) oder über 4000 andere Möglichkeiten.
Du kannst den Bootloader theoretisch so schreiben, dass er an jeder möglichen Adresse funktioniert, aber das ist ineffizient (besonders auf dem 8086), daher macht man das nicht. Dein Code ist also normalerweise so gebaut, dass das erste Byte an Adresse 0x0000 liegt (das ist das ".org 0x0000"), aber davon weiß die CPU ja nichts. Also machst du noch einen "FAR JMP" hinterher, der gleichzeitig CS und IP setzt. Danach hat CS den Wert 0x07C0 und IP 0x000F (die Adresse von "main", wenn man ab 0 anfängt zu zählen).
Verständlich?
-
Was Svenska geschrieben hat, ist korrekt, aber ich will trotzdem meinen Senf dazu geben, der Spekulation meinerseits ist. In den 80ern war denen Ingenieuren vermutlich egal, was in CS oder IP steht. Hauptsache MS DOS läuft. Der Bootsektor muss an die Adresse 0x07C00 geladen werden und DOS macht den Rest. Die Entwickler diverser PC-Klone haben beim Klonen des BIOS von IBM vielleicht gemerkt, dass es egal ist, was in CS oder IP steht, solange die Rechnung, die Svenska beschrieben hat, aufgeht, weil DOS damit klar kommt. Vielleicht haben die auch eine Münze geworfen oder Option 2 gewählt, um behaupten zu können: "Wir haben andere Werte in CS oder IP als IBM. Verklagt uns nicht.". Oder auch nicht.
Meine Behauptung ist also, dass es keine Dokumentation gibt, weil es keinen Masterplan gibt.
-
Okay. Diese Funktionsweise wirkt auf mich etwas schräg, aber ich denke ich habe es recht gut verstanden. Manchmal frage ich mich, warum man das nicht einfacher und damit verständlicher konzipiert hat, aber ich stecke nicht genug in der Materie drin, um das beurteilen zu können. Ich danke euch beiden für die Hilfe dabei, auf diese Lösung wäre ich nie gekommen!
Eine Frage hätte ich tatsächlich doch noch: Es ist etwas umständlich das Programm auf einem echten System zu testen, da die ganzen Emulatoren anders damit umgehen. Kann ich mit QEMU das UEFI-BIOS (=das VBIOS?) simulieren?
-
Qemu kann UEFI, aber ob er auch im UEFI-Modus das CSM unterstützt, weiß ich nicht.
Du kannst halt das normale BIOS benutzen.
Außerdem ist es sinnvoll, in verschiedenen Emulatoren zu testen, z.B. VirtualBox und VMware sind eher wie moderne Systeme gebaut (vllt funktioniert UEFI mit CSM dort), PCem eher wie ältere Systeme. DOSBox (mit Floppy-Image) ist auch interessant.