FAQ der Newsgroup de.comp.lang.assembler (d.c.l.a.)

Wie komme ich an die Kommandozeilenparameter?

Jedem Programm kann man beim Aufruf zusätzliche Informationen mitgeben, sogenannte Argumente oder Kommandozeilenparameter. An diese Informationen kommt man mit den Funktionen GetCommandLineA und GetCommandLineW. Die erste Funktion liefert einen Zeiger auf die Kommandozeile in ANSI-Form (ein Byte für jeden Buchstaben), die zweite in Unicode-Form (zwei Bytes für jeden Buchstaben) zurück. Diese Kommandozeile entspricht genau der Übergabe, also mit allen Leerzeichen, Tabulatoren, Anführungszeichen. Um die einzelnen Argumente "mundgerecht" aufzubearbeiten, kann man in WinNT (Windows NT, 2000, XP, aber nicht 95, 98, ME) die Funktion CommandLineToArgvW benutzen:

Nasm & GoLink
; Assemblieren: nasm.exe -fwin32 -o <Name>.obj <Name>.nasm
; Linken:       GoLink.exe /console /entry _main <Name>.obj kernel32.dll user32.dll shell32.dll msvcrt.dll
; Testen:       <Name>.exe Hier "stehe" ich und "kann nicht" anders

EXTERN GetCommandLineW, LocalFree  ; kernel32.dll
EXTERN CommandLineToArgvW          ; shell32.dll
EXTERN printf                      ; msvcrt.dll

%MACRO m_invoke 1-*     ; stdcall
    %if %0 > 1
        %rep %0-1
            %rotate -1
            push %1
        %endrep
        %rotate -1
    %endif
    call %1
%ENDMACRO

%MACRO m_ccall 1-*       ; c_decl
    %if %0 > 1
        %rep %0-1
            %rotate -1
            push %1
        %endrep
        %rotate -1
    %endif
    call %1
    %if %0 > 1
       add esp, ((%0-1)*4)
    %endif
%ENDMACRO

SECTION .data
    fmt         db "%ws",10,0

SECTION .bss
    szArglist   RESD 1
    nArgs       RESD 1

SECTION .text
GLOBAL _main
_main:
    m_invoke GetCommandLineW
    m_invoke CommandLineToArgvW, eax, nArgs
    mov [szArglist], eax
    mov esi, [eax]              ; esi: *szArglist
.Schleife:
    m_ccall printf, fmt, esi    ; eax = printf ("%ws\n", *szArglist)
    shl eax, 1                  ; Byteanzahl = 2mal Buchstabenanzahl wg. Unicode
    add esi, eax                ; *szArglist += eax
    sub dword [nArgs], 1        ; weitere Argumente in der Liste?
    jnz .Schleife               ; ja
Ende:
    m_invoke LocalFree, dword [szArglist]
    mov eax, 0                  ; Exitcode
    ret                         ; Ende

Bestimmte Zeichen haben eine Sonderfunktion. Leerzeichen (ASCII 32) und Tabulatoren (ASCII 9) trennen die Argumente voneinander. Sollen diese Zeichen nicht trennen, z.B. in  C:\Dokumente und Einstellungen, so ist dieser Begriff in Anführungszeichen einzuschließen:  "C:\Dokumente und Einstellungen". Will man das Anführungszeichen als Buchstaben behandeln, so muss das Zeichen "escaped" werden, indem man ihm einen umgekehrten Schrägstrich (Backslash) voranstellt:  "dir \"C:\Dokumente und Einstellungen\" \b". Nun gibt es Fälle, wo bei einer \"-Kombination der Backslash ein Buchstabe bleiben und das Anführungszeichen seine Sonderfunktion behalten soll. In diesem Fall wird auch der Backslash escaped:  "C:\Dokumente und Einstellungen\\". Und zu guter Letzt gibt es Fälle, wo ein Backslash als Buchstabe auf ein Anführungszeichen als Buchstabe trifft, also sowohl Backslash als auch Anführungszeichen keine Sonderfunktionen haben:  "dir \"C:\Dokumente und Einstellungen\\\" \b". Es wurde also sowohl der Backslash escaped wie auch das Anführungszeichen, was zu der seltsam anmutenden Folge von drei Backslashes und einem Anführungszeichen führt. Nun wird das Verhalten von CommandLineToArgvW etwas sonderbar: Eine Folge  \\\"  wird als  \"  interpretiert, eine Folge  \\\a  aber als \\\a . Ob zwei Backslashes hintereinander zu einem einzigen Backslash verkürzt werden oder nicht, hängt also davon ab, ob die Folge mit einem Anführungszeichen abgeschlossen wird oder nicht.

CommandLineToArgvW gibt es nicht als ANSI-Version und auch nicht für Win9x (Windows 95, 98, ME). Nachfolgend wird diese Funktion für ANSI-Zeichen nachgebildet. Allerdings werden zwei Backslashes in Folge immer als ein einziges Backslash interpretiert.

Nasm & GoLink
; Assemblieren: nasm.exe -fwin32 -o <Name>.obj <Name>.nasm
; Linken:       GoLink.exe /console /entry _main <Name>.obj kernel32.dll msvcrt.dll
; Testen:       <Name>.exe Hier "stehe" ich \\\\ "und \"kann\" nicht" anders

; kernel32.dll
EXTERN GetCommandLineA, RtlMoveMemory, LocalAlloc, LocalReAlloc, LocalFree
EXTERN GetLastError, SetLastError
; msvcrt.dll
EXTERN printf

%MACRO m_invoke 1-*     ; stdcall
    %if %0 > 1
        %rep %0-1
            %rotate -1
            push %1
        %endrep
        %rotate -1
    %endif
    call %1
%ENDMACRO

%MACRO m_ccall 1-*       ; c_decl
    %if %0 > 1
        %rep %0-1
            %rotate -1
            push %1
        %endrep
        %rotate -1
    %endif
    call %1
    %if %0 > 1
       add esp, ((%0-1)*4)
    %endif
%ENDMACRO

SECTION .data
    fmt         db "%s",10,0
    err         db "Fehler",0

SECTION .bss
    szArglist   RESD 1
    nArgs       RESD 1

SECTION .text
GLOBAL _main
_main:

    m_invoke GetCommandLineA
    test eax, eax
    jz .ErrExit
    m_invoke CommandLineToArgvA, eax, nArgs
    test eax, eax
    jz .ErrExit
    mov [szArglist], eax

    xor ebx, ebx
    mov edi, dword [szArglist]
.A: ; for (ebx = 0; ebx < nArgs; ebx++)
    mov esi, [edi + ebx * 4]
    m_ccall printf, fmt, esi    ; eax = printf ("%s\n", szArglist[ebx])
    add ebx, 1                  ; szArglist++
    cmp ebx, dword [nArgs]      ; weitere Argumente in der Liste?
    jnz .A                      ; ja

    m_invoke LocalFree, dword [szArglist]
    mov eax, 0                  ; Exitcode
    ret                         ; Ende
.ErrExit:
    m_ccall printf, fmt, err    ; eax = printf ("%s\n", err)
    mov eax, 1                  ; Exitcode
    ret                         ; Ende

CommandLineToArgvA:
    %define Arg_nArgs dword [ebp+12]
    %define Arg_lpCmdLine dword [ebp+8]
    %define hMemLocalAlloc dword [ebp-4]
    %define countCommands dword [ebp-8]
    %define lenCommands dword [ebp-12]
    %define lenPointers dword [ebp-16]

    push ebp
    mov ebp, esp
    sub esp, 16

    ; Länge der CommandLine mit Abschlussnull bestimmen
    ; Neuen Speicher in dieser Größe anfordern
    xor al, al
    mov ecx, -1
    mov edi, Arg_lpCmdLine  ; Arg: lpCmdLine
    repnz scasb
    sub edi, Arg_lpCmdLine
    m_invoke LocalAlloc, 0x0040, edi    ; LocalAlloc (uFlags=LPTR, uBytes=edi)
    mov hMemLocalAlloc, eax
    test eax, eax
    jz .Err

    ; CommandLine nach LocalAlloc übertragen
    mov edi, eax
    mov esi, Arg_lpCmdLine  ; Arg: lpCmdLine
    xor eax, eax
    xor edx, edx                ; Anzahl der Kommandozeilenargumente
    mov ecx, 1b                 ; Bit0=0/1: double quote/kein double quote (toggelt später)
.A: ; Führende space und tab überspringen
    lodsb
    cmp al, 32                  ; space
    je .A
    cmp al, 9                   ; tab
    je .A
.B: ; Schleife: Kommando in Puffer ablegen
    test al, al                 ; ASCIIZ-Null?
    jz .B3                      ; ja: Schleifenausstieg
    ; Escape-Zeichen \ bearbeiten
    cmp al, 92                  ; backslash
    jnz .B0
    lodsb                       ; nächstes Zeichen laden
    cmp al, 92                  ; zweiter Backslash?
    je .B2                      ; ja: ein einzelnes Backslash übertragen
    cmp al , 34                 ; backslash + double quote
    je .B2                      ; ja: Anführungszeichen übertragen
    sub esi, 2                  ; Alten Zustand wiederherstellen
    lodsb
.B0:; Anführungszeichen bearbeiten
    cmp al, 34                  ; double quotes
    jne .B1
    btc cx, 0                   ; Toggle Bit0
    lodsb
    jmp .B                      ; Double quote nicht abspeichern
.B1:; Leerzeichen und Tabulator
    test cl, 01b
    jz .B2                      ; Innerhalb double quotes nicht auf space oder tab prüfen
    cmp al, 32                  ; space
    je .B3
    cmp al, 9                   ; tab
    je .B3
.B2:; Zeichen übertragen, nächstes Zeichen holen, Loop
    stosb
    lodsb
    jmp .B                      ; Loop
.B3:
    shl eax, 8                  ; ah=al, al=0
    stosb
    add edx, 1
    test ah, ah                 ; Ende der Kommandozeile erreicht (ah = Null)?
    jnz .A                      ; nein
    ; An dieser Stelle stehen die Argumente nullterminiert ab LocalAlloc, edx: Anzahl

    ; Neue Größe für Realloc berechnen: Zeiger + Argumente
    ; Bisherige Argumente nach hinten verschieben
    mov countCommands, edx      ; Anzahl Kommandos
    sub edi, hMemLocalAlloc
    mov lenCommands, edi        ; Anzahl Bytes Kommandoblock
    shl edx, 2                  ; Anzahl Bytes Zeigerblock
    mov lenPointers, edx
    add edi, edx
    m_invoke LocalReAlloc, hMemLocalAlloc, edi, 0x0040    ; LocalReAlloc (hMem, , uBytes=edi, uFlags=LPTR)
    test eax, eax
    jz .Err
    mov hMemLocalAlloc, eax
    mov edi, eax
    add edi, lenPointers
    m_invoke RtlMoveMemory , edi, eax, lenCommands

.D: ; Zeiger für die Argumente ermitteln und an den Anfang stellen
    mov esi, hMemLocalAlloc
    mov edx, countCommands
    mov ecx, lenCommands
    xor eax, eax
.D1:
    mov [esi], edi
    sub edx, 1
    jz .Z
    add esi, 4
    repnz scasb
    jmp .D1
.Dz:

.Z: ; Abschlussarbeiten und Rückkehr
    mov edi, Arg_nArgs
    mov edx, countCommands
    mov [edi], edx
    mov eax, hMemLocalAlloc
    leave
    ret 8                       ; stdcall

.Err: ; Fehler bei LocalAlloc oder LocalRealloc
    m_invoke GetLastError
    push eax                    ; Errorcode sichern
    m_invoke LocalFree, hMemLocalAlloc
    call SetLastError           ; Errorcode bereits gepusht
    xor eax, eax
    leave
    ret 8                       ; stdcall

Ralph 'rkhb' Bauer Feb 2009