Wir können die Effizienz unseres Codes erhöhen, indem wir die Ein- und Ausgabe puffern. Wir erzeugen einen Eingabepuffer und lesen dann eine Folge von Bytes auf einmal. Danach holen wir sie Byte für Byte aus dem Puffer.
Wir erzeugen ebenfalls einen Ausgabepuffer. Darin speichern
wir unsere Ausgabe bis er voll ist. Dann bitten wir den Kernel
den Inhalt des Puffers nach stdout
zu
schreiben.
Diese Programm endet, wenn es keine weitere Eingaben gibt.
Aber wir müssen den Kernel immernoch bitten den Inhalt des
Ausgabepuffers ein letztes Mal nach stdout
zu schreiben, denn sonst würde ein Teil der Ausgabe zwar im
Ausgabepuffer landen, aber niemals ausgegeben werden. Bitte
vergessen Sie das nicht, sonst fragen Sie sich später warum
ein Teil Ihrer Ausgabe verschwunden ist.
Als dritten Abschnitt im Quelltext haben wir
.bss
. Dieser Abschnitt wird nicht in unsere
ausführbare Datei eingebunden und kann daher nicht
initialisiert werden. Wir verwenden resb
anstelle von db
. Dieses reserviert einfach die
angeforderte Menge an uninitialisiertem Speicher zu unserer
Verwendung.
Wir nutzen, die Tatsache, dass das System die Register nicht
verändert: Wir benutzen Register, wo wir anderenfalls
globale Variablen im Abschnitt .data
verwenden müssten. Das ist auch der Grund, warum die
UNIX®-Konvention, Parameter auf dem Stack zu übergeben,
der von Microsoft, hierfür Register zu verwenden,
überlegen ist: Wir können Register für unsere
eigenen Zwecke verwenden.
Wir verwenden EDI
und
ESI
als Zeiger auf das
nächste zu lesende oder schreibende Byte. Wir verwenden
EBX
und ECX
, um die Anzahl der Bytes in den
beiden Puffern zu zählen, damit wir wissen, wann wir die
Ausgabe an das System übergeben, oder neue Eingabe vom
System entgegen nehmen müssen.
Lassen Sie uns sehen, wie es funktioniert:
%
nasm -f elf hex.asm
%
ld -s -o hex hex.o
%
./hex
Hello, World!
Here I come!
48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0A
48 65 72 65 20 49 20 63 6F 6D 65 21 0A
^D
%
Nicht was Sie erwartet haben? Das Programm hat die Ausgabe
nicht auf dem Bildschirm ausgegeben bis sie
^D
gedrückt haben. Das kann man
leicht zu beheben indem man drei Zeilen Code einfügt,
welche die Ausgabe jedesmal schreiben, wenn wir einen
Zeilenumbruch in 0A
umgewandelt haben. Ich
habe die betreffenden Zeilen mit > markiert (kopieren Sie die
> bitte nicht mit in Ihre
hex.asm
).
Lassen Sie uns jetzt einen Blick darauf werfen, wie es funktioniert.
%
nasm -f elf hex.asm
%
ld -s -o hex hex.o
%
./hex
Hello, World!
48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0A
Here I come!
48 65 72 65 20 49 20 63 6F 6D 65 21 0A
^D
%
Nicht schlecht für eine 644 Byte große Binärdatei, oder?
Dieser Ansatz für gepufferte Ein- und Ausgabe enthält eine Gefahr, auf die ich im Abschnitt Die dunkle Seite des Buffering eingehen werde.
Das ist vielleicht ein etwas fortgeschrittenes Thema, das vor allem für Programmierer interessant ist, die mit der Theorie von Compilern vertraut sind. Wenn Sie wollen, können Sie zum nächsten Abschnitt springen und das hier vielleicht später lesen.
Unser Beispielprogramm benötigt es zwar nicht, aber etwas anspruchsvollere Filter müssen häufig vorausschauen. Mit anderen Worten, sie müssen wissen was das nächste Zeichen ist (oder sogar mehrere Zeichen). Wenn das nächste Zeichen einen bestimmten Wert hat, ist es Teil des aktuellen Tokens, ansonsten nicht.
Zum Beispiel könnten Sie den Eingabestrom für eine Text-Zeichenfolge parsen (z.B. wenn Sie einen Compiler einer Sprache implementieren): Wenn einem Buchstaben ein anderer Buchstabe oder vielleicht eine Ziffer folgt, ist er ein Teil des Tokens, das Sie verarbeiten. Wenn ihm ein Leerzeichen folgt, oder ein anderer Wert, ist er nicht Teil des aktuellen Tokens.
Das führt uns zu einem interessanten Problem: Wie kann man ein Zeichen zurück in den Eingabestrom geben, damit es später noch einmal gelesen werden kann?
Eine mögliche Lösung ist, das Zeichen in einer
Variable zu speichern und ein Flag zu setzen. Wir können
getchar
so anpassen, dass es das Flag
überprüft und, wenn es gesetzt ist, das Byte aus der
Variable anstatt dem Eingabepuffer liest und das Flag
zurück setzt. Aber natürlich macht uns das
langsamer.
Die Sprache C hat eine Funktion
ungetc()
für genau diesen Zweck.
Gibt es einen schnellen Weg, diese in unserem Code zu
implementieren? Ich möchte Sie bitten nach oben zu
scrollen und sich die Prozedur getchar
anzusehen und zu versuchen eine schöne und schnelle
Lösung zu finden, bevor Sie den nächsten Absatz
lesen. Kommen Sie danach hierher zurück und schauen sich
meine Lösung an.
Der Schlüssel dazu ein Zeichen an den Eingabestrom zurückzugeben, liegt darin, wie wir das Zeichen bekommen:
Als erstes überprüfen wir, ob der Puffer leer
ist, indem wir den Wert von EBX
testen. Wenn er null ist, rufen
wir die Prozedur read
auf.
Wenn ein Zeichen bereit ist verwenden wir lodsb
, dann verringern wir den Wert
von EBX
. Die Anweisung
lodsb
ist letztendlich
identisch mit:
Das Byte, welches wir abgerufen haben, verbleibt im Puffer
bis read
zum nächsten Mal aufgerufen
wird. Wir wissen nicht wann das passiert, aber wir wissen,
dass es nicht vor dem nächsten Aufruf von
getchar
passiert. Daher ist alles was wir
tun müssen um das Byte in den Strom "zurückzugeben"
ist den Wert von ESI
zu
verringern und den von EBX
zu erhöhen:
Aber seien Sie vorsichtig! Wir sind auf der sicheren Seite,
solange wir immer nur ein Zeichen im Voraus lesen. Wenn wir
mehrere kommende Zeichen betrachten und
ungetc
mehrmals hintereinander aufrufen,
wird es meistens funktionieren, aber nicht immer (und es wird
ein schwieriger Debug). Warum?
Solange getchar
read
nicht aufrufen muss, befinden sich
alle im Voraus gelesenen Bytes noch im Puffer und
ungetc
arbeitet fehlerfrei. Aber sobald
getchar
read
aufruft
verändert sich der Inhalt des Puffers.
Wir können uns immer darauf verlassen, dass
ungetc
auf dem zuletzt mit
getchar
gelesenen Zeichen korrekt
arbeitet, aber nicht auf irgendetwas, das davor gelesen
wurde.
Wenn Ihr Programm mehr als ein Byte im Voraus lesen soll, haben Sie mindestens zwei Möglichkeiten:
Die einfachste Lösung ist, Ihr Programm so zu ändern, dass es immer nur ein Byte im Voraus liest, wenn das möglich ist.
Wenn Sie diese Möglichkeit nicht haben, bestimmen Sie
zuerst die maximale Anzahl an Zeichen, die Ihr Programm auf
einmal an den Eingabestrom zurückgeben muss. Erhöhen
Sie diesen Wert leicht, nur um sicherzugehen, vorzugsweise auf
ein Vielfaches von 16—damit er sich schön
ausrichtet. Dann passen Sie den .bss
Abschnitt Ihres Codes an und erzeugen einen kleinen
Reserver-Puffer, direkt vor ihrem Eingabepuffer, in etwa
so:
Außerdem müssen Sie ungetc
anpassen, sodass es den Wert des Bytes, das zurückgegeben
werden soll, in AL
übergibt:
Mit dieser Änderung können Sie sicher
ungetc
bis zu 17 Mal hintereinander
gqapaufrufen (der erste Aufruf erfolgt noch im Puffer, die
anderen 16 entweder im Puffer oder in der Reserve).
Wenn Sie Fragen zu FreeBSD haben, schicken Sie eine E-Mail an
<de-bsd-questions@de.FreeBSD.org>.
Wenn Sie Fragen zu dieser Dokumentation haben, schicken Sie eine E-Mail an
<de-bsd-translators@de.FreeBSD.org>.