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.
%include 'system.inc' %define BUFSIZE 2048 section .data hex db '0123456789ABCDEF' section .bss ibuffer resb BUFSIZE obuffer resb BUFSIZE section .text global _start _start: sub eax, eax sub ebx, ebx sub ecx, ecx mov edi, obuffer .loop: ; read a byte from stdin call getchar ; convert it to hex mov dl, al shr al, 4 mov al, [hex+eax] call putchar mov al, dl and al, 0Fh mov al, [hex+eax] call putchar mov al, ' ' cmp dl, 0Ah jne .put mov al, dl .put: call putchar jmp short .loop align 4 getchar: or ebx, ebx jne .fetch call read .fetch: lodsb dec ebx ret read: push dword BUFSIZE mov esi, ibuffer push esi push dword stdin sys.read add esp, byte 12 mov ebx, eax or eax, eax je .done sub eax, eax ret align 4 .done: call write ; flush output buffer push dword 0 sys.exit align 4 putchar: stosb inc ecx cmp ecx, BUFSIZE je write ret align 4 write: sub edi, ecx ; start of buffer push ecx push edi push dword stdout sys.write add esp, byte 12 sub eax, eax sub ecx, ecx ; buffer is empty now ret
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).
%include 'system.inc' %define BUFSIZE 2048 section .data hex db '0123456789ABCDEF' section .bss ibuffer resb BUFSIZE obuffer resb BUFSIZE section .text global _start _start: sub eax, eax sub ebx, ebx sub ecx, ecx mov edi, obuffer .loop: ; read a byte from stdin call getchar ; convert it to hex mov dl, al shr al, 4 mov al, [hex+eax] call putchar mov al, dl and al, 0Fh mov al, [hex+eax] call putchar mov al, ' ' cmp dl, 0Ah jne .put mov al, dl .put: call putchar > cmp al, 0Ah > jne .loop > call write jmp short .loop align 4 getchar: or ebx, ebx jne .fetch call read .fetch: lodsb dec ebx ret read: push dword BUFSIZE mov esi, ibuffer push esi push dword stdin sys.read add esp, byte 12 mov ebx, eax or eax, eax je .done sub eax, eax ret align 4 .done: call write ; flush output buffer push dword 0 sys.exit align 4 putchar: stosb inc ecx cmp ecx, BUFSIZE je write ret align 4 write: sub edi, ecx ; start of buffer push ecx push edi push dword stdout sys.write add esp, byte 12 sub eax, eax sub ecx, ecx ; buffer is empty now ret
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?
Anmerkung: Dieser Ansatz für gepufferte Ein- und Ausgabe enthält eine Gefahr, auf die ich im Abschnitt Die dunkle Seite des Buffering eingehen werde.
Warnung: 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:
mov al, [esi] inc esi
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:
ungetc: dec esi inc ebx ret
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:
section .bss resb 16 ; or whatever the value you came up with ibuffer resb BUFSIZE obuffer resb BUFSIZE
Außerdem müssen Sie ungetc 	anpassen, sodass es den
Wert des Bytes, das zurückgegeben 	werden soll, in AL
	übergibt:
ungetc: dec esi inc ebx mov [esi], al ret
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).
| Zurück | Zum Anfang | Weiter | 
| UNIX®-Filter schreiben | Nach oben | Kommandozeilenparameter | 
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>.