Zeichnen eines Zeichens im VGA-Speicher mit GNU C-Inline-Assembly

7

Ich lerne, eine Low-Level-VGA-Programmierung in DOS mit C und Inline-Assembly zu machen. Im Moment versuche ich eine Funktion zu erstellen, die ein Zeichen auf dem Bildschirm ausgibt.

Das ist mein Code:

%Vor%

Ich leite mich von dieser Serie von Tutorials ab, die in PASCAL geschrieben wurden: Ссылка .

Ich habe die Assembly-Syntax nach dem gcc-Compiler geändert, bekomme aber immer noch folgende Fehler:

%Vor%

BEARBEITEN:

Ich habe an der Verbesserung meines Codes gearbeitet und jetzt sehe ich etwas auf dem Bildschirm. Hier ist mein aktualisierter Code:

%Vor%

Wenn Sie das Bild oben sehen, habe ich versucht, den Buchstaben "S" zu schreiben. Die Ergebnisse sind die grünen Pixel, die Sie oben links auf dem Bildschirm sehen. Egal, welches x und y ich der Funktion gebe, es zeichnet immer die Pixel an derselben Stelle auf.

Kann mir jemand helfen, meinen Code zu korrigieren?

    
Pablo Estrada 12.01.2016, 16:22
quelle

1 Antwort

19

Im Folgenden finden Sie eine Analyse einiger Dinge, die mit Ihrer put_char -Funktion speziell falsch sind, und einer Version, die möglicherweise funktioniert. (Ich bin mir nicht sicher über die %cs Segmentüberschreibung, aber ansonsten sollte es tun, was Sie vorhaben).

DOS und 16-bit asm zu lernen ist nicht der beste Weg asm

zu lernen

Zunächst einmal sind DOS und 16-Bit x86 völlig veraltet und nicht einfacher zu erlernen als normales 64-Bit x86. Sogar 32-Bit x86 ist veraltet, wird aber in der Windows-Welt immer noch häufig verwendet.

32-Bit- und 64-Bit-Code müssen sich nicht um viele 16-Bit-Einschränkungen / Komplikationen wie Segmente oder begrenzte Registerwahl in Adressierungsmodi kümmern. Einige moderne Systeme verwenden Segmentüberschreibungen für Thread-Local-Speicher, aber das Erlernen der Verwendung von Segmenten in 16-Bit-Code ist kaum damit verbunden.

Einer der größten Vorteile, asm zu kennen, ist das Debuggen / Profilieren / Optimieren von echten Programmen. Wenn Sie verstehen möchten, wie Sie C oder einen anderen High-Level-Code schreiben können Blick auf Compiler-Ausgabe . Dies wird 64-Bit (oder 32-Bit) sein. (Siehe zB Matt Godbolts CppCon2017-Talk: "Was hat mein Compiler in letzter Zeit für mich getan? Den Deckel des Compilers abschrauben" mit einem exzellenten Einstieg in das Lesen x86 asm für total Anfänger und zum Betrachten der Compilerausgabe).

Das Wissen von Asm ist nützlich, wenn Sie sich die Leistungszähler-Ergebnisse ansehen, die eine Disassemblierung Ihrer Binärdatei enthalten ( perf stat ./a.out & amp; & amp; perf report -Mintel : siehe CppCon2015 von Chandler Carruth:" Tuning C ++: Benchmarks, CPUs und Compiler! Oh My! "). Aggressive Compiler-Optimierungen bedeuten, dass die Betrachtung von Zyklus- / Cache-Miss / Stall-Zählungen pro Quellzeile viel weniger informativ ist als pro Anweisung.

Damit Ihr Programm tatsächlich tun kann, muss es entweder direkt mit der Hardware sprechen oder Systemaufrufe tätigen. Das Lernen von DOS-Systemaufrufen für Dateizugriff und Benutzereingaben ist eine völlige Zeitverschwendung (außer dem Beantworten des stetigen Stroms von SO-Fragen darüber, wie mehrstellige Zahlen in 16-Bit-Code gelesen und gedruckt werden). Sie unterscheiden sich erheblich von den APIs in den aktuellen Hauptbetriebssystemen. Das Entwickeln neuer DOS-Anwendungen ist nicht sinnvoll, daher müssen Sie eine andere API (sowie ABI) lernen, wenn Sie mit Ihrem asm-Wissen etwas tun.

Das Lernen auf einem 8086-Simulator ist noch begrenzender: 186, 286 und 386 haben viele praktische Anweisungen wie imul ecx, 15 hinzugefügt, was ax less "special" macht. Wenn Sie sich nur auf Anweisungen beschränken, die auf 8086 funktionieren, werden Sie "schlechte" Wege finden, Dinge zu tun. Andere große sind movzx / movsx , Verschiebung um eine sofortige Anzahl (anders als 1) und push immediate . Neben der Leistung ist es auch einfacher, Code zu schreiben, wenn diese verfügbar sind, da Sie keine Schleife schreiben müssen, um um mehr als 1 Bit zu verschieben.

Vorschläge für bessere Möglichkeiten, sich selbst etwas beizubringen

Ich habe hauptsächlich asm vom Lesen der Compiler-Ausgabe gelernt und dann kleine Änderungen vorgenommen. Ich habe nicht versucht, Sachen in asm zu schreiben, wenn ich die Dinge nicht wirklich verstanden habe, aber wenn du schnell lernen wirst (anstatt nur ein Verständnis während des Debuggens / Profilierens von C zu entwickeln), musst du wahrscheinlich dein Verständnis testen Deinen eigenen Code schreiben. Sie müssen die Grundlagen verstehen, dass es 8 oder 16 Ganzzahlregister + die Flags und den Anweisungszeiger gibt, und dass jede Anweisung eine wohldefinierte Änderung des aktuellen Architekturzustands der Maschine vornimmt. (Eine vollständige Beschreibung aller Anweisungen finden Sie im Handbuch zu Intel insn ref (Links in Wiki, zusammen mit viel mehr guten Sachen ).

Vielleicht möchten Sie mit einfachen Dingen wie dem Schreiben einer einzelnen Funktion in asm als Teil eines größeren Programms beginnen. Es ist nützlich, die Art von asm zu verstehen, die für Systemaufrufe benötigt wird, aber in echten Programmen ist es normalerweise nur nützlich, asm für innere Schleifen zu schreiben, die keine Systemaufrufe beinhalten. Es ist zeitaufwendig, asm zu schreiben, um Eingabe- und Druckergebnisse zu lesen, daher würde ich vorschlagen, diesen Teil in C zu schreiben. Stellen Sie sicher, dass Sie die Compilerausgabe lesen und verstehen, was passiert, und den Unterschied zwischen einer Ganzzahl und einer Zeichenfolge strtol und printf tun, auch wenn Sie sie nicht selbst schreiben.

Sobald Sie denken, dass Sie genug von den Grundlagen verstehen, finden Sie eine Funktion in einem Programm, das Sie kennen und / oder daran interessiert sind, und sehen Sie, ob Sie den Compiler schlagen und Anweisungen speichern können (oder schnellere Anweisungen verwenden).Oder implementieren Sie es selbst ohne mit der Compiler-Ausgabe als Ausgangspunkt, je nachdem, was Sie interessanter finden. Diese Antwort könnte interessant sein, obwohl der Schwerpunkt darin lag, eine C-Quelle zu finden, die den Compiler dazu gebracht hat, die optimale ASM zu erzeugen.

Wie Sie versuchen, Ihre eigenen Probleme zu lösen (bevor Sie eine SO Frage stellen)

Es gibt viele SO-Fragen von Leuten, die fragen "Wie mache ich X in asm", und die Antwort ist normalerweise "das gleiche wie in C". Sei nicht so verwirrt, dass du nicht weißt, wie du programmieren sollst. Finde heraus, was mit den Daten geschehen muss, auf denen die Funktion läuft, und finde heraus, wie das in asm gemacht wird. Wenn Sie stecken bleiben und eine Frage stellen müssen, sollten Sie die meisten einer funktionierenden Implementierung haben, mit nur einem Teil, den Sie nicht wissen, welche Anweisungen für einen Schritt zu verwenden sind.

Sie sollten dies mit 32 oder 64 Bit x86 tun. Ich würde 64 Bit empfehlen, da die ABI schöner ist, aber 32-Bit-Funktionen werden Sie zwingen, den Stack besser zu nutzen. Das könnte Ihnen helfen, zu verstehen, wie ein Befehl call die Rücksprungadresse auf den Stapel legt und wo die Argumente, die der Aufrufer ausgegeben hat, danach liegen. (Dies scheint das zu sein, was Sie vermeiden wollten, indem Sie inline asm verwenden).

Die Hardware direkt zu programmieren ist nett, aber keine allgemein nützliche Fähigkeit

Es ist nicht sinnvoll, zu lernen, wie man Grafiken durch direktes Ändern des Video-RAMs macht, außer um Neugier zu befriedigen, wie Computer früher gearbeitet haben. Sie können dieses Wissen für nichts verwenden. Es gibt moderne Grafik-APIs, die es mehreren Programmen ermöglichen, in ihren eigenen Bildschirmregionen zu zeichnen und Indirection zuzulassen (z. B. direkt auf eine Textur anstatt auf den Bildschirm zeichnen, so dass 3D-Fenster-Wechsel von Alt-Tab schicke aussehen kann). Es gibt zu viele Gründe, hier aufzulisten, um nicht direkt auf Video-RAM zu zeichnen.

Es ist möglich, einen Pixmap-Puffer zu verwenden und dann eine Grafik-API zu verwenden, um sie auf den Bildschirm zu kopieren. Dennoch ist das Ausführen von Bitmap-Grafiken mehr oder weniger veraltet, es sei denn, Sie erzeugen Bilder für PNG oder JPEG oder etwas (z. B. Optimieren der Konvertierung von Histogramm-Bins in ein Streudiagramm im Back-End-Code für einen Webdienst). Moderne Grafik-APIs abstrahieren die Auflösung, sodass Ihre App unabhängig von der Größe jedes Pixels Objekte in einer angemessenen Größe zeichnen kann. (klein, aber extrem hohen Bildschirm im Vergleich zu großen Fernseher bei niedrigen Rez).

Es ist irgendwie cool, in den Speicher zu schreiben und etwas auf dem Bildschirm zu sehen. Oder noch besser, schließen Sie LEDs (mit kleinen Widerständen) an die Datenbits an einem parallelen Port an und führen Sie eine Anweisung outb aus, um sie ein- / auszuschalten. Ich habe das vor Jahren auf meinem Linux-System gemacht. Ich habe ein kleines Wrapper-Programm erstellt, das iopl(2) und inline asm verwendet und es als root ausgeführt hat. Das können Sie unter Windows wahrscheinlich ähnlich machen. Sie brauchen keine DOS oder 16-Bit-Code, um Ihre Füße nass mit der Hardware zu sprechen.

in / out Instruktionen und normale Lasten / Speicher auf speicherzuordnende IO und DMA, sind, wie echte Treiber mit Hardware sprechen, einschließlich Dinge, die viel komplizierter sind als parallele Ports. Es macht Spaß zu wissen, wie deine Hardware "wirklich" funktioniert, aber verbringe nur Zeit damit, wenn du wirklich interessiert bist oder Treiber schreiben willst. Die Linux-Source-Struktur enthält Treiber für Boot-Loads von Hardware und ist oft gut kommentiert. Wenn Sie also Code genauso gerne lesen wie Code schreiben, ist das ein anderer Weg, ein Gefühl dafür zu bekommen, was Lesetreiber tun, wenn sie mit Hardware reden.

>

Es ist generell gut, eine Idee zu haben, wie die Dinge unter der Haube funktionieren. Wenn Sie wollen lernen, wie Grafiken, die vor langer Zeit verwendet wurden (mit VGA-Textmodus und Farb- / Attributbytes), dann sind Sie sicher, verrückt. Seien Sie sich jedoch bewusst, dass moderne Betriebssysteme keinen VGA-Textmodus verwenden, sodass Sie nicht einmal lernen, was auf modernen Computern unter der Haube passiert.

Viele Menschen genießen Ссылка und erleben eine einfachere Zeit, als Computer weniger komplex waren und nicht mehr so ​​viele Abstraktionsschichten unterstützen konnten. Sei dir nur bewusst, dass du das machst. Ich könnte ein gutes Sprungbrett sein, um zu lernen, Treiber für moderne Hardware zu schreiben, wenn Sie sicher sind, das , warum Sie ASM / Hardware verstehen wollen.

Inline asm

Sie verwenden einen völlig falschen Ansatz für die Verwendung von Inline-ASM. Sie scheinen ganze Funktionen in asm schreiben zu wollen, also sollten Sie das tun . z.B. Setze deinen Code in asmfuncs.S oder so. Verwenden Sie .S , wenn Sie weiterhin die GNU / AT & amp; T-Syntax verwenden möchten; oder verwenden Sie .asm , wenn Sie die Intel / NASM / YASM-Syntax verwenden möchten (was ich empfehlen würde, da die offiziellen Handbücher alle die Intel-Syntax verwenden. Siehe x86 Wiki für Anleitungen und Handbücher.)

GNU inline asm ist der härteste Weg, um ASM zu lernen .Sie müssen alles verstehen, was Ihr asm tut und was der Compiler darüber wissen muss. Es ist wirklich schwer, alles richtig zu machen. In Ihrem Edit ändert dieser Block von Inline-Asm beispielsweise viele Register, die Sie nicht als überladen aufgelistet haben, einschließlich %ebx , das ein Register ist, das durch Aufrufe erhalten wird (dies ist also kaputt, selbst wenn diese Funktion nicht inline ist). Zumindest haben Sie die ret herausgenommen, damit die Dinge nicht so spektakulär brechen, wenn der Compiler diese Funktion in die Schleife einfügt, die sie aufruft. Wenn das wirklich kompliziert klingt, dann ist das der Grund dafür, warum Sie nicht inline asm verwenden sollten, um asm zu lernen.

Diese Antwort auf eine ähnliche Frage aus dem Missbrauch von Inline asm beim Versuch, asm in erster Linie zu lernen hat mehr Links über Inline-Asm und wie man es gut verwendet.

Vielleicht funktioniert das Chaos, vielleicht

Dieser Teil könnte eine separate Antwort sein, aber ich lasse es zusammen.

Abgesehen davon, dass Ihr gesamter Ansatz grundsätzlich eine schlechte Idee ist, gibt es mindestens ein spezifisches Problem mit Ihrer Funktion put_char : Sie verwenden offset als reinen Ausgabeoperanden. gcc kompiliert ganz glücklich Ihre gesamte Funktion zu einer einzigen Anweisung ret , da die Anweisung asm nicht volatile ist und ihre Ausgabe nicht verwendet wird. (Inline-asm-Anweisungen ohne Ausgaben werden als volatile angenommen.)

I setzen Sie Ihre Funktion auf Godbolt , so konnte ich schauen, was Montage der Compiler Umgebung erzeugt es. Dieser Link bezieht sich auf die feststehende, vielleicht funktionierende Version mit korrekt deklarierten Clobbern, Kommentaren, Bereinigungen und Optimierungen. Siehe unten für den gleichen Code, wenn dieser externe Link jemals bricht.

Ich habe gcc 5.3 mit der Option -m16 verwendet, was sich von der Verwendung eines echten 16-Bit-Compilers unterscheidet. Es macht immer noch alles 32-Bit (32-Bit-Adressen, 32bit int s, und 32-Bit-Funktionsarg auf dem Stapel), aber teilt dem Assembler mit, dass die CPU im 16-Bit-Modus ist, also weiß, wann Operandengröße ausgegeben werden soll und Adressgrößenpräfixe.

Auch wenn Sie Ihre ursprüngliche Version mit -O0 kompilieren, berechnet der Compiler offset = (y<<8) + (y<<6) + x; , aber setzt es nicht in %edi , weil du es nicht gefragt hast. Die Angabe als anderer Eingabeoperand hätte funktioniert. Nach dem Inline-Asm speichert es %edi in -12(%ebp) , wo offset lebt.

Andere Sachen sind falsch mit put_char :

Sie übergeben 2 Dinge ( ascii_char und current_color ) anstelle von Funktionsargumenten in globale Funktionen. Yuck, das ist widerlich. VGA und characters sind Konstanten, so dass das Laden von Globalen nicht so schlecht aussieht.Schreiben in asm bedeutet, dass Sie gute Programmierpraktiken nur dann ignorieren sollten, wenn sie die Leistung um einen angemessenen Betrag verbessern. Da der Aufrufer diese Werte wahrscheinlich in den Globals speichern musste, speichern Sie nichts im Vergleich zum Aufrufer, der sie als Funktionsargumente auf dem Stack speichert. Und für x86-64 würden Sie Leistung verlieren, weil der Anrufer sie einfach in Registern übergeben konnte.

Auch:

%Vor%

Alle lokalen Variablen werden nicht verwendet, außer offset . Würdest du es in C schreiben oder so?

Sie verwenden 16-Bit-Adressen für characters , aber 32-Bit-Adressierungsmodi für VGA-Speicher. Ich nehme an, das ist beabsichtigt, aber ich habe keine Ahnung, ob es richtig ist. Sind Sie sicher, dass Sie eine CS: override für die Ladevorgänge von characters verwenden sollten? Geht der Abschnitt .rodata in das Codesegment? Obwohl du uint8_t characters[464] nicht als const deklariert hast, ist es wahrscheinlich nur im Abschnitt .data . Ich halte mich für glücklich, dass ich eigentlich keinen Code für ein segmentiertes Speichermodell geschrieben habe, der aber immer noch verdächtig aussieht.

Wenn du wirklich djgpp verwendest, dann wird laut Michael Petchs Kommentar im 32-Bit-Modus laufen . Die Verwendung von 16-Bit-Adressen ist daher eine schlechte Idee.

Optimierungen

Sie können die Verwendung von %ebx ganz vermeiden, indem Sie dies tun, anstatt in ebx zu laden und dann %ebx zu %edi hinzuzufügen.

%Vor%

Sie benötigen lea nicht, um eine Adresse in ein Register zu bekommen. Sie können einfach

verwenden %Vor%

$_characters bedeutet die Adresse als unmittelbare Konstante. Wir können viele Anweisungen speichern, indem wir dies mit der vorherigen Berechnung des Offsets in das Array characters von Bitmaps kombinieren. Die Sofortoperandenform von imul lässt uns das Ergebnis in %si erzeugen:

%Vor%

Da diese Form von imul nur die niedrige 16b des 16 * 16 - & gt; 32b multiplizieren, die Operandformulare 2 und 3 imul kann für signierte oder unsignierte Multiplikationen verwendet werden , weshalb nur imul (nicht mul ) hat diese zusätzlichen Formulare. Für größere Multiplikationen mit Operandengröße sind 2 und 3 Operand imul schneller , da die obere Hälfte nicht gespeichert werden muss %[er]dx .

Sie könnten die innere Schleife ein wenig vereinfachen, aber es würde die äußere Schleife etwas komplizierter machen: Sie könnten auf das Null-Flag verzweigen, wie es in shl , %al festgelegt ist, anstatt einen Zähler zu verwenden. Das würde es auch unberechenbar machen, wie der Jump-Over-Speicher für Nicht-Vordergrund-Pixel, so dass die erhöhten Verzweigungsfehlvorhersagen schlechter sein könnten als die zusätzlichen Do-Nothing-Schleifen. Es würde auch bedeuten, dass Sie %edi in der äußeren Schleife jedes Mal neu berechnen müssen, da die innere Schleife nicht konstant laufen würde. Aber es könnte so aussehen:

%Vor%

Beachten Sie, dass die Bits in Ihren Zeichen-Bitmaps Bytes im VGA-Speicher wie {7 6 5 4 3 2 1 0} zugeordnet werden, da Sie das um eine linke Verschiebung verschobene Bit testen. Es beginnt also mit dem MSB. Bits in einem Register sind immer "Big Endian". Eine linke Verschiebung multipliziert sich mit zwei, sogar auf einer Little-Endian-Maschine wie x86. Little-Endian beeinflusst nur die Anordnung von Bytes im Speicher, nicht Bits in einem Byte und nicht einmal Bytes innerhalb von Registern.

Eine Version Ihrer Funktion, die möglicherweise Ihren Vorstellungen entspricht.

Das ist das gleiche wie der Gottbolt-Link.

%Vor%

Ich habe keine Dummy-Ausgabeoperanden verwendet, um die Zuweisung von Registern dem Ermessen des Compilers zu überlassen, aber das ist eine gute Idee, um den Aufwand für das Abrufen von Daten an den richtigen Stellen für Inline-Asm zu reduzieren. (extra mov Anweisungen). Zum Beispiel musste der Compiler hier nicht gezwungen werden, offset in %edi zu setzen. Es könnte jedes Register sein, das wir nicht bereits benutzen.

    
Peter Cordes 21.01.2016, 08:22
quelle

Tags und Links