FreeBSD Developers' Handbook
Zurück | Kapitel 11. x86-Assembler-Programmierung | Weiter

# 11.8. Gepufferte Eingabe und Ausgabe

Übersetzt von Hagen Kühl.

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.

## 11.8.1. Ein Zeichen ungelesen machen

> 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>.