11.9. Kommandozeilenparameter

Übersetzt von Fabian Ruch.

Unser hex-Programm wird nützlicher, wenn es die Dateinamen der Ein- und Ausgabedatei über die Kommandozeile einlesen kann, d.h., wenn es Kommandozeilenparameter verarbeiten kann. Aber... Wo sind die?

Bevor ein UNIX®-System ein Programm ausführt, legt es einige Daten auf dem Stack ab (push) und springt dann an das _start-Label des Programms. Ja, ich sagte springen, nicht aufrufen. Das bedeutet, dass auf die Daten zugegriffen werden kann, indem [esp+offset] ausgelesen wird oder die Daten einfach vom Stack genommen werden (pop).

Der Wert ganz oben auf dem Stack enthält die Zahl der Kommandozeilenparameter. Er wird traditionell argc wie "argument count" genannt.

Die Kommandozeilenparameter folgen einander, alle argc. Von diesen wird üblicherweise als argv wie "argument value(s)" gesprochen. So erhalten wir argv[0], argv[1], ... und argv[argc-1]. Dies sind nicht die eigentlichen Parameter, sondern Zeiger (Pointer) auf diese, d.h., Speicheradressen der tatsächlichen Parameter. Die Parameter selbst sind durch NULL beendete Zeichenketten.

Der argv-Liste folgt ein NULL-Zeiger, was einfach eine 0 ist. Es gibt noch mehr, aber dies ist erst einmal genug für unsere Zwecke.

Anmerkung: Falls Sie von der MS-DOS®-Programmierumgebung kommen, ist der größte Unterschied die Tatsache, dass jeder Parameter eine separate Zeichenkette ist. Der zweite Unterschied ist, dass es praktisch keine Grenze gibt, wie viele Parameter vorhanden sein können.

Ausgerüstet mit diesen Kenntnissen, sind wir beinahe bereit für eine weitere Version von hex.asm. Zuerst müssen wir jedoch noch ein paar Zeilen zu system.inc hinzufügen:

Erstens benötigen wir zwei neue Einträge in unserer Liste mit den Systemaufrufnummern:

%define        SYS_open        5
%define SYS_close       6

Zweitens fügen wir zwei neue Makros am Ende der Datei ein:

%macro sys.open        0
        system  SYS_open
%endmacro

%macro  sys.close       0
        system  SYS_close
%endmacro

Und hier ist schließlich unser veränderter Quelltext:

%include       'system.inc'

%define BUFSIZE 2048

section .data
fd.in   dd      stdin
fd.out  dd      stdout
hex     db      '0123456789ABCDEF'

section .bss
ibuffer resb    BUFSIZE
obuffer resb    BUFSIZE

section .text
align 4
err:
        push    dword 1         ; return failure
        sys.exit

align 4
global  _start
_start:
        add     esp, byte 8     ; discard argc and argv[0]

        pop     ecx
        jecxz   .init           ; no more arguments

        ; ECX contains the path to input file
        push    dword 0         ; O_RDONLY
        push    ecx
        sys.open
        jc      err             ; open failed

        add     esp, byte 8
        mov     [fd.in], eax

        pop     ecx
        jecxz   .init           ; no more arguments

        ; ECX contains the path to output file
        push    dword 420       ; file mode (644 octal)
        push    dword 0200h | 0400h | 01h
        ; O_CREAT | O_TRUNC | O_WRONLY
        push    ecx
        sys.open
        jc      err

        add     esp, byte 12
        mov     [fd.out], eax

.init:
        sub     eax, eax
        sub     ebx, ebx
        sub     ecx, ecx
        mov     edi, obuffer

.loop:
        ; read a byte from input file or 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, dl
        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 [fd.in]
        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

        ; close files
        push    dword [fd.in]
        sys.close

        push    dword [fd.out]
        sys.close

        ; return success
        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 [fd.out]
        sys.write
        add     esp, byte 12
        sub     eax, eax
        sub     ecx, ecx        ; buffer is empty now
        ret

In unserem .data-Abschnitt befinden sich nun die zwei neuen Variablen fd.in und fd.out. Hier legen wir die Dateideskriptoren der Ein- und Ausgabedatei ab.

Im .text-Abschnitt haben wir die Verweise auf stdin und stdout durch [fd.in] und [fd.out] ersetzt.

Der .text-Abschnitt beginnt nun mit einer einfachen Fehlerbehandlung, welche nur das Programm mit einem Rückgabewert von 1 beendet. Die Fehlerbehandlung befindet sich vor _start, sodass wir in geringer Entfernung von der Stelle sind, an der der Fehler auftritt.

Selbstverständlich beginnt die Programmausführung immer noch bei _start. Zuerst entfernen wir argc und argv[0] vom Stack: Sie sind für uns nicht von Interesse (sprich, in diesem Programm).

Wir nehmen argv[1] vom Stack und legen es in ECX ab. Dieses Register ist besonders für Zeiger geeignet, da wir mit jecxz NULL-Zeiger verarbeiten können. Falls argv[1] nicht NULL ist, versuchen wir, die Datei zu öffnen, die der erste Parameter festlegt. Andernfalls fahren wir mit dem Programm fort wie vorher: Lesen von stdin und Schreiben nach stdout. Falls wir die Eingabedatei nicht öffnen können (z.B. sie ist nicht vorhanden), springen wir zur Fehlerbehandlung und beenden das Programm.

Falls es keine Probleme gibt, sehen wir nun nach dem zweiten Parameter. Falls er vorhanden ist, öffnen wir die Ausgabedatei. Andernfalls schreiben wir die Ausgabe nach stdout. Falls wir die Ausgabedatei nicht öffnen können (z.B. sie ist zwar vorhanden, aber wir haben keine Schreibberechtigung), springen wir auch wieder in die Fehlerbehandlung.

Der Rest des Codes ist derselbe wie vorher, außer dem Schließen der Ein- und Ausgabedatei vor dem Verlassen des Programms und, wie bereits erwähnt, die Benutzung von [fd.in] und [fd.out].

Unsere Binärdatei ist nun kolossale 768 Bytes groß.

Können wir das Programm immer noch verbessern? Natürlich! Jedes Programm kann verbessert werden. Hier finden sich einige Ideen, was wir tun könnten:

Ich beabsichtige, diese Verbesserungen dem Leser als Übung zu hinterlassen: Sie wissen bereits alles, das Sie wissen müssen, um die Verbesserungen durchzuführen.

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