Warum Argumente zerbrechen — Die Regeln der Windows-Kommandozeilenargumente

· Aktualisiert am: · · Windows, Windows-Entwicklung, C#, C++, Win32 API, .NET, Prozess

Änderungsverlauf (Erstfassung, veröffentlicht am 2. Sep 2026)
Erstveröffentlichung

„Im Test lief es, aber auf einem PC, dessen Pfad ein Leerzeichen enthält, startet das externe Werkzeug nicht.“ „Ich habe C:\data\ übergeben, und es wurde mit dem nächsten Argument zu einem einzigen zusammengezogen.“ „Ich habe JSON als Argument übergeben, die Anführungszeichen waren weg, und die Gegenseite konnte nicht parsen.“ Das sind Fehler, die in Code, der Kindprozesse startet, immer wiederkehren. Die meisten entstehen nicht durch Logik, sondern durch Code, der die Prämisse nicht kennt, dass Windows keinen Mechanismus hat, ein „Array von Argumenten“ zu übergeben.

Was CreateProcess, die Funktion, die unter Windows einen Prozess erzeugt, empfängt, ist eine einzige Zeichenfolge namens lpCommandLine. So sorgfältig der Aufrufer auch ein Array vorbereitet: an der Grenze des Betriebssystems wird es immer zu einer Zeichenfolge verkettet, und die Empfangsseite zerlegt sie erneut. Die Zerlegungsregeln bestimmt die Runtime der Empfangsseite, und die C-Laufzeit, CommandLineToArgvW, die .NET-Runtime und cmd.exe sind jeweils eigener Code. Argumente zu übergeben heißt, eine Zeichenfolge zu bauen, die der Parser der Gegenseite wieder in die ursprünglichen Stücke zerlegt.

Dieser Artikel nimmt den Standpunkt ein, Kindprozesse aus Win32- und .NET-Code zu starten, nicht aus PowerShell-Skripten, und legt dar, wo die Zeichenfolge verkettet wird, wo sie zerlegt wird und welche Regeln gelten. Die PowerShell-Seite (die Änderung der Argumentübergabe in 7.3, --%, $PSNativeCommandArgumentPassing) behandelt „Externe EXEs aus PowerShell korrekt aufrufen“, deshalb gräbt dieser Artikel die Schicht darunter aus.

Die Schicht, die dieser Artikel behandeltDie Argumentübergabe in PowerShell behandelt ein eigener Artikel; dieser Artikel behandelt die Schicht darunter, von Win32 CreateProcess und .NET ProcessStartInfo bis zum Parser des Ziel-ExeGeltungsbereich dieses ArtikelsPowerShell-Argumentübergabe (eigener Artikel).NET ProcessStartInfoWin32 CreateProcessWEine einzige KommandozeilenzeichenfolgeDer Parser des Ziel-Exe

Abbildung 1: Unter PowerShell liegen die Schichten .NET und Win32, und von welcher aus man auch startet, am Ende steht eine einzige Zeichenfolge. Dieser Artikel behandelt die Regeln dieser Schicht.

1. Zuerst das Fazit

  • Ein Windows-Prozess empfängt niemals ein Array von Argumenten. Die eine Zeichenfolge, die an CreateProcess übergeben wird, erreicht den neuen Prozess (nur beim führenden Namen der ausführbaren Datei kann das Betriebssystem den vollständigen Pfad ergänzen), und GetCommandLineW gibt sie zurück. argv erzeugt die Empfangsseite selbst.1 2
  • Der Kern der Zerlegungsregeln sind drei Punkte: an Leerzeichen und Tabulatoren trennen, innerhalb eines doppelten Anführungsbereichs nicht trennen, und ein Backslash ist nur dann besonders, wenn unmittelbar ein doppeltes Anführungszeichen folgt (2n Backslashes werden zu n plus das Anführungszeichen öffnet oder schließt die Quotierung; 2n+1 werden zu n plus einem literalen Anführungszeichen).3 4
  • Nur das führende Token (argv[0], der Name der ausführbaren Datei) folgt einer anderen Regel: es kann in Anführungszeichen gesetzt werden, aber Backslash-Escaping gilt nicht. Ist lpApplicationName NULL, wird die Deutung eines Pfads mit Leerzeichen mehrdeutig und C:\Program.exe wird zuerst versucht.1 4
  • Auf der Aufbauseite reicht eine Regel: „enthält das Argument ein Leerzeichen oder ein Anführungszeichen oder ist es leer, in Anführungszeichen setzen, die Backslashes unmittelbar vor einem Anführungszeichen und die abschließenden Backslashes verdoppeln, und Anführungszeichen als \" schreiben.“ ProcessStartInfo.ArgumentList in .NET Core 2.1 und später erledigt das für Sie.5 6
  • Erzeugen Sie nicht die Form, die innerhalb eines nichtleeren Arguments zwei benachbarte Anführungszeichen setzt (etwas wie "ab""c"), weil Empfänger sie unterschiedlich deuten. Das "", das ein leeres Argument darstellt, ist etwas anderes und korrekt. cmd.exe und Batch-Dateien stehen außerhalb dieser Regeln, deshalb leiten Sie nicht vertrauenswürdige Werte nicht durch sie.6 7
  • Die Grenzen sind 32.767 UTF-16-Codeeinheiten für lpCommandLine (einschließlich des abschließenden Nullzeichens; Zeichen, die Surrogatpaare sind, etwa Emoji, zählen als zwei) und 8.191 Zeichen für cmd.exe. Drohen Sie sie zu überschreiten, wechseln Sie zu einer Antwortdatei, aber nur wenn das Ziel eine mit einer Syntax wie @file lesen kann (oder so geändert werden kann, dass es das kann).1 8

In der Abbildung kennzeichnet eine durchgezogene Linie eine stets geltende Beziehung und eine gestrichelte Linie eine bedingte (die Bedingungen stehen bei jeder Beziehung auf der Detailseite). Die vollständige Liste der Beziehungen (28 insgesamt, mit Beleg und Sicherheitsgrad) und die Definitionen der wichtigsten Konzepte sind auf der Detailseite der Wissenskarte (auf Japanisch) zusammengestellt. Daten: JSON-LD / Turtle

2. Es gibt kein Argument-Array — CreateProcess und die eine Zeichenfolge

Der zweite Parameter von CreateProcessW, lpCommandLine, ist eine einzige nullterminierte Zeichenfolge, in der der Name der ausführbaren Datei und die Argumente durch Leerzeichen getrennt liegen. Die Längengrenze ist 32.767 UTF-16-Codeeinheiten einschließlich des abschließenden Nullzeichens (die Anzahl der wchar_t-Elemente; ein Surrogatpaar-Zeichen wie ein Emoji verbraucht zwei pro Zeichen, prüfen Sie also niemals vorab anhand der scheinbaren Zeichenanzahl), und weil die Unicode-Variante diese Zeichenfolge ändern kann, kann das Übergeben eines Zeichenfolgenliterals oder eines const-Puffers eine Zugriffsverletzung auslösen.1

Diese Zeichenfolge geht unverändert als Teil der Prozessparameter an den neuen Prozess, und der Kindprozess holt sie mit GetCommandLineW. Weil das Betriebssystem den führenden Namen der ausführbaren Datei durch den vollständigen Pfad ergänzen kann, muss die Zeichenfolge, die das Kind sieht, nicht genau mit der übereinstimmen, die der Elternprozess übergeben hat.2 Das lpCmdLine, das an WinMain einer GUI-Anwendung geht, ist diese Zeichenfolge ohne den Programmnamen.9

Der Weg, den Argumente nehmen, um den Kindprozess zu erreichenDas Argument-Array des Aufrufers wird in lpCommandLine von CreateProcess zu einer einzigen Zeichenfolge verkettet und an den neuen Prozess übergeben, und der Kindprozess zerlegt die mit GetCommandLineW geholte Zeichenfolge mit seinem eigenen Parser, um argv zu erzeugenDas Argument-Array des AufrufersZu einer Zeichenfolge verkettet (Verantwortung des Aufrufers)lpCommandLine von CreateProcessWDie Prozessparameter des neuen ProzessesDie Zeichenfolge, die GetCommandLineW zurückgibtDer Parser der Empfangsseite zerlegt sieDas Array argv / args

Abbildung 2: Das Array überschreitet die Grenze nicht. Verketten ist Aufgabe des Aufrufers, Zerlegen Aufgabe des Empfängers, und das ursprüngliche Array entsteht nur wieder, wenn die Regeln beider Seiten übereinstimmen.

Der Punkt, den man hier festhalten muss: Verketten und Zerlegen geschehen in unterschiedlichen Prozessen, in unterschiedlichem Code. Der Aufrufer kann nicht korrekt verketten, ohne zu wissen, womit die Gegenseite zerlegen wird, und der Empfänger hat keine Möglichkeit zu wissen, wie die Zeichenfolge verkettet wurde. Auf Unix-ähnlichen Betriebssystemen nimmt execve das Array unverändert entgegen, deshalb gibt es dieses Problem dort nicht. Es ist eine windows-spezifische Prämisse, die aber jedem Prozessstart folgt.

3. Wer zerlegt — drei Parser

Auf der Empfangsseite kommt der Code, der die Zeichenfolge in argv zerlegt, vor allem in drei Arten vor.

Empfangsseite Code, der zerlegt Wann er aufgerufen wird
main / wmain in C/C++ Der Startcode der MSVC-C-Laufzeit Erzeugt argc / argv automatisch beim Programmstart4
Code, der die Win32-API direkt verwendet CommandLineToArgvW Man übergibt den Rückgabewert von GetCommandLineW, um ihn in argv-Form zu wandeln3
.NETs Main(string[] args) / Environment.GetCommandLineArgs() (die übliche Konfiguration, gestartet über den apphost oder dotnet.exe) Der Startcode der C-Laufzeit des Hosts (apphost / dotnet.exe) Unter Windows ist der Host ein wmain-Programm; er nimmt das argv, das die C-Laufzeit gebaut hat, entfernt die eigenen Optionen und den App-Pfad und übergibt den Rest zusammen mit dem App-Pfad an die Runtime. Beim Start baut die Runtime ein Array, dessen erstes Element der Programmname ist (der vom Host übergebene Startname, oder der Assembly-Pfad, falls keiner), und behält es für GetCommandLineArgs(), während Main in args nur die Argumente ohne den Programmnamen erhält10 11 12
Eine Konfiguration, die die .NET-Runtime als gehostete Bibliothek lädt und keine Startargumente empfängt Der eigene Zerlegungscode der .NET-Runtime (SegmentCommandLine) Als Fallback zerlegt GetCommandLineArgs() den Rückgabewert von GetCommandLineW selbst. Er ist so implementiert, dass er den Regeln der C-Laufzeit entspricht, und verwendet CommandLineToArgvW nicht, weil das „sich geringfügig anders verhält“12

Der Zerlegungscode kommt in drei Linien vor, dem Startcode der C-Laufzeit, CommandLineToArgvW und dem eigenen Zerlegungscode der .NET-Runtime, und sie implementieren Regeln mit demselben Gerüst, sind aber nicht derselbe Code. Eine .NET-App, die über den apphost oder dotnet.exe startet, wird faktisch nach den Regeln der ersten Linie (Startcode der C-Laufzeit) zerlegt, weil der Host selbst ein mit der MSVC-C-Laufzeit gebautes wmain-Programm ist. Der Quellcode der .NET-Runtime trägt weiterhin einen Kommentar, dass CommandLineToArgvW nicht verwendet wird, weil es sich geringfügig anders verhält.12 Die Unterschiede treten an den Rändern auf, etwa bei der später beschriebenen Behandlung von "", und alltägliche Argumente treffen sie selten, aber die Annahme „die Regeln sind dieselben, also geht alles“ ist das, was an den Rändern zerbricht.

Die drei Parser auf der EmpfangsseiteDie eine Zeichenfolge, die GetCommandLineW zurückgibt, zerlegt für C/C++ der Startcode der C-Laufzeit, für direkte Win32-Nutzung CommandLineToArgvW und für .NET, das als gehostete Bibliothek geladen wird, der eigene Zerlegungscode der Runtime; jede folgt Regeln mit demselben Gerüst, ist aber eine eigene Implementierung. Eine normale .NET-App, die über den apphost oder dotnet.exe startet, empfängt das Array, das der Startcode der C-Laufzeit des Hosts zerlegt hatDie Zeichenfolge von GetCommandLineWStartcode der C-LaufzeitCommandLineToArgvWEigener Zerlegungscode von .NET (bei Host-Laden).NET über apphost / dotnet.exe ist dasselbeDasselbe Gerüst der Regeln, getrennte Implementierungen

Abbildung 3: Es gibt drei Linien von Zerlegungscode. Eine .NET-App, die über den apphost oder dotnet.exe startet, empfängt das Array, das der Startcode der C-Laufzeit des Hosts zerlegt hat, und der eigene Zerlegungscode der Runtime ist der Fallback für die Konfiguration als gehostete Bibliothek. Weil man von außen nicht sieht, auf welcher das Ziel-Exe läuft, ist die praktische Antwort, eine Zeichenfolge zu bauen, die auf allen dasselbe Ergebnis liefert.

Beachten Sie, dass args in .NETs Main(string[] args) den Programmnamen nicht enthält, das erste Element von Environment.GetCommandLineArgs() dagegen schon. Letzteres steht an derselben Stelle wie argv[0] in C/C++.13 Bei einem normalen Start wie dotnet app.dll x entfernt der Host die Host-Optionen und den App-Pfad (dotnet.exe und app.dll), und nur x erreicht args in Main.14 GetCommandLineArgs() dagegen gibt das Array zurück, dem die Runtime beim Start den Programmnamen vorangestellt hat (der Pfad von app.dll gefolgt von x).11 Der eigene Zerlegungscode der Runtime zerlegt GetCommandLineW nur in der Konfiguration als gehostete Bibliothek, die keine Startargumente empfängt; in einer Konfiguration, in der ein nativer Host eigenes argc/argv übergibt und Main aufruft, ist args in Main das, was der Host übergeben hat.

4. Die Zerlegungsregeln — Leerzeichen, Anführungszeichen und Backslashes

Hier sind die Regeln, die die drei Parser für argv[1] und folgende teilen.3 4

  1. Argumente werden durch Leerzeichen oder Tabulatoren getrennt.
  2. Ein Bereich in doppelten Anführungszeichen wird zu einem Argument, auch wenn er Leerzeichen enthält. Die Anführungszeichen selbst gehören nicht zum Argument. Ein Anführungszeichen darf mitten in einem Argument beginnen, und endet die Zeichenfolge ohne schließendes Anführungszeichen, wird alles bis zum Ende zum letzten Argument.
  3. Ein Backslash wird als gewöhnliches Zeichen behandelt. Nur wenn unmittelbar ein doppeltes Anführungszeichen folgt, gelten die nächsten Regeln.
  4. Stehen 2n Backslashes vor einem doppelten Anführungszeichen, werden n Backslashes ausgegeben, und das Anführungszeichen wirkt als „Beginn oder Ende der Quotierung“.
  5. Stehen 2n+1 Backslashes vor einem doppelten Anführungszeichen, werden n Backslashes und ein literales Anführungszeichen ausgegeben, und der Quotierungszustand ändert sich nicht.
  6. Das Caret (^) ist kein Escape-Zeichen (das ist eine Regel von cmd.exe, keine Parser-Regel).

Der Parser hält ein Bit Zustand, „bin ich in Anführungszeichen“, kippt es bei jedem Anführungszeichen und liest die Zeichenfolge von links nach rechts. Ob ein Leerzeichen Argumente trennt, entscheidet dieser Zustand.

Der Zerlegungsfluss, der zwischen innen und außen der Anführungszeichen wechseltAußerhalb von Anführungszeichen trennt der Parser Argumente an Leerzeichen; trifft er ein Anführungszeichen, geht er hinein und behandelt Leerzeichen als Teil des Arguments; trifft er ein weiteres Anführungszeichen, kehrt er nach außen zurück. Ein Backslash wird nur dann besonders behandelt, wenn unmittelbar ein Anführungszeichen folgtEin Anführungszeichen wird getroffenEin Anführungszeichen wird getroffenAuf einen Backslash folgt unmittelbar ein AnführungszeichenAuf einen Backslash folgt unmittelbar ein Anführungszeichen2n: n ausgeben und öffnen/schließen2n+1: n und ein literales Anführungszeichen ausgebenAußerhalb von Anführungszeichen: an Leerzeichen trennenInnerhalb von Anführungszeichen: Leerzeichen sind Teil des ArgumentsDie Backslash-Regel anwendenDen Quotierungszustand kippenDen Quotierungszustand beibehalten

Abbildung 4: Der Kern der Zerlegung entscheidet sich durch nur ein Bit, „innen oder außen der Anführungszeichen“, und die Anzahl der Backslashes unmittelbar vor einem Anführungszeichen.

Statt die Regeln als Prosa zu lernen, ist es zuverlässiger, die Zuordnung von Eingabe und Ausgabe zu betrachten.

Teil der Kommandozeile (Eingabe) Entstehende Argumente Wirksame Regel
a b c a, b, c An Leerzeichen trennen
"a b" c a b, c Ein quotierter Bereich wird nicht getrennt
C:\data\ next C:\data\, next Dem Backslash folgt kein Anführungszeichen, also ist er ein gewöhnliches Zeichen
"C:\data\\" next C:\data\, next Die zwei vor dem Anführungszeichen werden zu einem, und das Anführungszeichen schließt
"C:\data\" next C:\data" next Ein Backslash, also wird das Anführungszeichen zum literalen Anführungszeichen und die Quotierung schließt nie, das nächste Argument wird verschluckt
"say \"hi\"" say "hi" Ungerade Anzahl, also literale Anführungszeichen
"" Leere Zeichenfolge Der einzige Weg, ein leeres Argument zu übergeben
'a b' 'a, b' Einfache Anführungszeichen haben keine besondere Bedeutung15

Zeile 5 ist die wahre Identität von „ich habe C:\data\ übergeben, und es wurde mit dem nächsten Argument zu einem einzigen zusammengezogen“ aus der Einleitung. In dem Moment, in dem Sie einen Pfad mit abschließendem Backslash in Anführungszeichen setzen, wird das schließende Anführungszeichen zum Zeichen und die Quotierung schließt nie.

Wie ein abschließender Backslash das nächste Argument verschlucktWird ein Pfad mit abschließendem Backslash in Anführungszeichen gesetzt, sitzt das Anführungszeichen, das schließen sollte, unmittelbar nach einem einzelnen Backslash und wird als literales Anführungszeichen gedeutet, sodass die Quotierung nie schließt und alles bis zum nächsten Argument als ein Argument gelesen wirdDen Backslash verdoppelnEin quotierter Pfad, der mit einem Backslash endetEine ungerade Anzahl Backslashes steht vor dem schließenden AnführungszeichenDas Anführungszeichen wird als Zeichen ausgegeben und die Quotierung schließt nichtNachfolgende Leerzeichen trennen nicht mehrAlles bis zum nächsten Argument kommt als ein Argument anDie Quotierung schließt und die Argumente trennen sich

Abbildung 5: Warum „den abschließenden Backslash verdoppeln“ nötig ist. Quotierung, die ohne Kenntnis der Regeln geschrieben wird, zerbricht am Ende eines Pfads.

„Zwei aufeinanderfolgende Anführungszeichen innerhalb der Quotierung“, wo sich Implementierungen unterscheiden

Die Regeln der MSVC-C-Laufzeit haben noch einen Punkt: „zwei aufeinanderfolgende Anführungszeichen innerhalb einer quotierten Zeichenfolge werden als ein Anführungszeichen behandelt“ (eine Form wie "ab""c", die eine andere Sache ist als das "", das ein leeres Argument darstellt).4 Die offiziellen Regeln von CommandLineToArgvW haben diesen Punkt jedoch nicht, und der Aufbau-Code der .NET-Runtime vermeidet ausdrücklich, diese Form zu erzeugen, weil „ein Anführungszeichen nach einem schließenden Anführungszeichen von VC vor und nach 2008 unterschiedlich gedeutet wird“.6

Als Empfänger reicht es zu wissen, dass solche Eingabe ankommen kann. Als Aufbauender verwenden Sie, wenn Sie ein Anführungszeichen als Zeichen übergeben wollen, nur die Form \". Sie liefert auf jedem Parser dasselbe Ergebnis.

5. argv[0] folgt einer anderen Regel — lpApplicationName und das Program.exe-Problem

Das führende Token, also der Name der ausführbaren Datei, liegt außerhalb der bisherigen Regeln. Es gilt als Zeichenfolge, die als Dateisystempfad gültig ist, deshalb kann es in Anführungszeichen gesetzt werden, um Leerzeichen zu enthalten, aber die Backslash-Escape-Regeln gelten nicht. Es gibt auch keinen Weg, ein Anführungszeichen selbst in argv[0] unterzubringen.4 3 Der Aufbau-Code von .NET behandelt das erste Element ebenfalls gesondert: „bei Leerzeichen in Anführungszeichen setzen, und eine Ausnahme werfen, wenn es ein Anführungszeichen enthält“.6

Was auf der Aufrufseite zum Problem wird, ist das Verhalten, wenn lpApplicationName von CreateProcess NULL ist. In diesem Fall wird das auszuführende Modul aus dem führenden, an Leerzeichen getrennten Token von lpCommandLine abgeleitet. Enthält der Pfad Leerzeichen, entstehen mehrere Kandidaten, und das Betriebssystem versucht sie, beginnend beim kürzesten.1

Die Reihenfolge, in der die ausführbare Datei geraten wird, wenn lpApplicationName NULL istWird C:\Program Files\MyApp -L -S ohne Anführungszeichen übergeben, prüft CreateProcess der Reihe nach C:\Program.exe und dann C:\Program Files\MyApp.exe, sodass C:\Program.exe läuft, wenn es dort abgelegt wurdeExistiertExistiert nichtlpApplicationName übergeben oder das führende Token quotierenEinen unquotierten Pfad (mit Leerzeichen) in lpCommandLine übergebenKandidat 1: C:\Program.exe versuchenEine unbeabsichtigte ausführbare Datei startetKandidat 2: C:\Program Files\MyApp.exe versuchenDie beabsichtigte ausführbare Datei startet

Abbildung 6: Einen Pfad mit Leerzeichen ohne Anführungszeichen an den Anfang zu stellen, lässt das Betriebssystem Kandidaten vom kürzesten an versuchen. Die offizielle Dokumentation nennt das in klaren Worten „gefährlich“.

Die offizielle Dokumentation stellt fest, dass C:\Program.exe, wenn es dort liegt, statt der beabsichtigten App läuft, und verlangt, für lpApplicationName kein NULL zu übergeben und, falls doch, den führenden Pfad in Anführungszeichen zu setzen.1 In der Praxis tun Sie beides. Übergeben Sie in lpApplicationName den vollständigen Pfad der ausführbaren Datei, und stellen Sie denselben Pfad, in Anführungszeichen gesetzt, auch an den Anfang von lpCommandLine. Wenn beide übergeben werden, entscheidet lpApplicationName das Modul, das läuft, und argv[0] des Kindprozesses wird zum führenden Token von lpCommandLine. Wenn Sie die beiden nicht per Konvention konsistent halten, zerbricht Code, der den eigenen Pfad aus argv[0] ableitet. Der zuverlässige Weg, den eigenen Pfad zu holen, ist GetModuleFileNameW.4

Wie das ausgeführte Modul und argv[0] entschieden werdenWerden sowohl lpApplicationName als auch lpCommandLine übergeben, entscheidet lpApplicationName das Modul, das läuft, und argv[0] des Kindes ist das führende Token von lpCommandLine. Code, der den eigenen Pfad aus argv[0] ableitet, zerbricht, wenn die beiden auseinanderlaufen, deshalb den eigenen Pfad mit GetModuleFileNameW holenZerbricht, wenn sie auseinanderlaufenStattdessen verwendenlpApplicationNameDas Modul, das läuftDas führende Token von lpCommandLineargv[0] des KindesCode, der den eigenen Pfad aus argv[0] ableitetGetModuleFileNameW

Abbildung 7: „Was läuft“ und „was in argv[0] landet“ werden getrennt entschieden. Ein Entwurf, der den eigenen Pfad aus argv[0] ableitet, kann auf dieser Trennung nicht stehen.

Noch ein Punkt: wenn lpApplicationName NULL ist, ist der Teil mit dem Namen der ausführbaren Datei in lpCommandLine auf MAX_PATH begrenzt.1 Zur Behandlung langer Pfade siehe „MAX_PATH und die Fallstricke von Windows-Pfaden und Dateinamen“.

6. Die Regeln auf der Aufbauseite — eine Funktion reicht

Sobald Sie die Zerlegungsregeln kennen, können Sie „eine Zeichenfolge, die die Gegenseite wieder in das Ursprüngliche zerlegt“ einfach bauen, indem Sie sie rückwärts laufen lassen. Für jedes Argument ab argv[1] tun Sie Folgendes.6

  1. Ist es nicht leer und enthält weder Leerzeichen noch Anführungszeichen, stellen Sie es unverändert.
  2. Andernfalls setzen Sie das Ganze in Anführungszeichen. Innerhalb der Quotierung
    • machen Sie eine Folge von k Backslashes unmittelbar vor einem Anführungszeichen zu 2k+1 und setzen dann das Anführungszeichen (eine ungerade Anzahl macht daraus ein „literales Anführungszeichen“);
    • machen Sie eine Folge von k abschließenden Backslashes zu 2k (sie stehen vor dem schließenden Anführungszeichen, deshalb macht eine gerade Anzahl daraus das „Ende der Quotierung“);
    • lassen Sie jeden anderen Backslash unverändert.
  3. Eine leere Zeichenfolge stellen Sie als "".
Der Entscheidungsfluss zum Aufbau eines ArgumentsIst das Argument nicht leer und enthält weder Leerzeichen noch Anführungszeichen, wird es unverändert gestellt; andernfalls in Anführungszeichen setzen, Backslashes vor einem Anführungszeichen zu 2k+1 und abschließende Backslashes zu 2k machen, Anführungszeichen einen Backslash voranstellen und schließenNeinJaEin Argument entgegennehmenLeer, oder enthält ein Leerzeichen oder ein Anführungszeichen?Unverändert stellenÖffnendes AnführungszeichenVon links scannenk Backslashes vor einem Anführungszeichen → 2k+1k abschließende Backslashes → 2kAlles andere unverändertSchließendes Anführungszeichen

Abbildung 8: Aufbauen ist die Umkehrung der Zerlegungsregeln. Es gibt nur drei Zweige, und Sie passen die Backslash-Anzahl nur am Ende und unmittelbar vor einem Anführungszeichen an; damit rundet jede Zeichenfolge, sofern die Empfangsseite Wide-Zeichen mit denselben Zerlegungsregeln wie CommandLineToArgvW, die C-Laufzeit und .NET (Kapitel 4) zerlegt (ein Ziel, das die Roh-Kommandozeile mit eigener Grammatik deutet, oder ein Shell-Parser dazwischen, fällt heraus), die Platzhaltererweiterung wie wsetargv.obj nicht eingeschaltet hat, die Zeichenfolge keine NUL-Zeichen enthält und die gebaute Gesamtzeichenfolge in die Grenze von lpCommandLine passt (32.767 UTF-16-Codeeinheiten einschließlich des abschließenden Nullzeichens). (Die Kommandozeile ist eine nullterminierte Zeichenfolge, deshalb ist ein NUL-Zeichen das eine, das sich grundsätzlich nicht übergeben lässt. Bei einem Ziel mit eingeschalteter Platzhaltererweiterung wird ein Argument, das * oder ? enthält, durch Dateinamen ersetzt; siehe Kapitel 8. Eine Zeichenfolge über der Grenze lehnt CreateProcessW ab; siehe Kapitel 10.)

Diese Regel spiegelt die Asymmetrie „ein Backslash ist nur unmittelbar vor einem Anführungszeichen besonders“ so wider, wie sie ist. Es besteht keine Notwendigkeit, die Backslashes, die Pfadkomponenten trennen, mechanisch zu verdoppeln; der Punkt ist, dass Sie nur die unmittelbar vor einem Anführungszeichen und am Ende anfassen.

7. Implementierung in .NET — ArgumentList und Arguments

ProcessStartInfo in .NET Core 2.1 und später hat ArgumentList, das diesen Aufbau übernimmt. Ein Element ist ein Argument, die Zeichenfolgen, die Sie hinzufügen, brauchen kein Escape im Voraus, und bei Process.Start baut .NET sie intern zu einer Zeichenfolge und übergibt sie an das Betriebssystem.5

var psi = new ProcessStartInfo
{
    FileName = @"C:\Program Files\MyTool\convert.exe",
    UseShellExecute = false,
};
psi.ArgumentList.Add("--input");
psi.ArgumentList.Add(inputPath);      // darf Leerzeichen, abschließende Backslashes und Anführungszeichen enthalten
psi.ArgumentList.Add("--output");
psi.ArgumentList.Add(outputPath);
psi.ArgumentList.Add("--label");
psi.ArgumentList.Add("");             // ein leeres Argument wird korrekt als "" übergeben

using var proc = Process.Start(psi)
    ?? throw new InvalidOperationException("Process.Start hat null zurückgegeben");
proc.WaitForExit();
if (proc.ExitCode != 0)
    throw new InvalidOperationException($"convert.exe ist fehlgeschlagen (ExitCode={proc.ExitCode})");

Arguments ist eine Eigenschaft, die eine einzige, selbst gebaute Zeichenfolge unverändert übergibt. Die beiden sind unabhängig, und wenn Sie die eine verwenden, muss die andere leer sein.16 Die offizielle Dokumentation rät ebenfalls, ArgumentList zu wählen, wenn Sie bei der Quotierung unsicher sind.5

Wo ArgumentList und Arguments zu einer Zeichenfolge werdenBei ArgumentList escaped .NET jedes Element und baut eine einzige Zeichenfolge, bevor sie an CreateProcess geht; bei Arguments wird die vom Aufrufer gebaute Zeichenfolge unverändert übergeben. In beiden Fällen erreicht das Betriebssystem eine einzige ZeichenfolgeArgumentList (1 Element = 1 Argument).NET escaped jedes Element und verkettetArguments (eine selbst gebaute Zeichenfolge)UnverändertEine einzige KommandozeilenzeichenfolgeCreateProcess

Abbildung 9: Was Sie auch verwenden, das Betriebssystem erreicht eine einzige Zeichenfolge. Der einzige Unterschied ist, wer sie baut, und ArgumentList überlässt das der Seite, die die Regeln kennt.

Der Aufbau-Code hinter ArgumentList sind genau die Regeln aus Kapitel 6. Ist das Argument nicht leer und enthält weder Leerzeichen noch Anführungszeichen, wird es unverändert gestellt; andernfalls in Anführungszeichen gesetzt, Backslashes unmittelbar vor einem Anführungszeichen werden zu 2k+1, abschließende Backslashes zu 2k, und jedem Anführungszeichen wird immer ein Backslash vorangestellt. Die Form mit benachbarten Anführungszeichen innerhalb eines nichtleeren Arguments wird nie erzeugt. Nur ein leeres Argument wird als "" gestellt, und das ist die korrekte Form.6

Unter .NET Framework selbst bauen

ArgumentList ist eine mit .NET Core 2.1 eingeführte API und existiert auf ProcessStartInfo unter .NET Framework nicht.5 In einer App unter .NET Framework 4.8 oder einem darauf aufbauenden internen Werkzeug schreiben Sie die Regeln aus Kapitel 6 selbst und übergeben das Ergebnis an Arguments.

// Für .NET Framework. Baut die eine Zeichenfolge, die an ProcessStartInfo.Arguments geht.
// Die Regeln sind dieselben, die ProcessStartInfo.ArgumentList intern verwendet.
static string BuildArguments(IEnumerable<string> args)
{
    var sb = new StringBuilder();
    foreach (var arg in args)
    {
        if (sb.Length > 0) sb.Append(' ');
        AppendArgument(sb, arg);
    }
    return sb.ToString();
}

static void AppendArgument(StringBuilder sb, string arg)
{
    if (arg.IndexOf('\0') >= 0)
        throw new ArgumentException("Ein Argument darf kein NUL-Zeichen enthalten (die Kommandozeile ist eine nullterminierte Zeichenfolge und würde dort abgeschnitten)");

    bool needsQuote = arg.Length == 0 || arg.Any(c => char.IsWhiteSpace(c) || c == '"');
    if (!needsQuote)
    {
        sb.Append(arg);                       // unverändert
        return;
    }

    sb.Append('"');
    int i = 0;
    while (i < arg.Length)
    {
        int backslashes = 0;
        while (i < arg.Length && arg[i] == '\\') { i++; backslashes++; }

        if (i == arg.Length)
        {
            sb.Append('\\', backslashes * 2); // abschließend: verdoppelt, weil das schließende Anführungszeichen folgt
        }
        else if (arg[i] == '"')
        {
            sb.Append('\\', backslashes * 2 + 1).Append('"'); // vor einem Anführungszeichen: verdoppelt plus eins
            i++;
        }
        else
        {
            sb.Append('\\', backslashes).Append(arg[i]);      // alles andere: unverändert
            i++;
        }
    }
    sb.Append('"');
}

Hier stehen Eingaben und Ausgaben nebeneinander.

Zu übergebender Wert Zeichenfolge, die AppendArgument ausgibt
strict strict
Leere Zeichenfolge ""
C:\Program Files\input "C:\Program Files\input"
C:\Program Files\input\ "C:\Program Files\input\\"
say "hi" "say \"hi\""
a\"b "a\\\"b"
C:\data\ (keine Leerzeichen) C:\data\

Beachten Sie die letzte Zeile. Ein Wert, der weder Leerzeichen noch Anführungszeichen enthält, wird nicht umschlossen, deshalb kommt der abschließende Backslash unverändert heraus. Ohne Umschließen zünden die Regeln 4 und 5 nie, und C:\data\ kommt korrekt an.

Die Wahl der Aufbaumethode nach .NET-VersionUnter .NET Core 2.1 oder höher ArgumentList von ProcessStartInfo überlassen; unter .NET Framework die Arguments-Zeichenfolge mit einer eigenen Funktion nach denselben Regeln bauen. In keinem Fall Anführungszeichen per Zeichenfolgenverkettung von Hand schreibenCore 2.1 oder höherFrameworkWelche .NET-Version?Elementweise zu ArgumentList hinzufügenArguments mit einer eigenen Funktion bauenNiemals Anführungszeichen von Hand schreiben

Abbildung 10: Zwei Methoden, ein Prinzip. Bleiben Sie bei „niemals Anführungszeichen von Hand schreiben“, und der Bruch am Ende eines Pfads entsteht nie.

Beachten Sie, dass bei UseShellExecute = true der Start über ShellExecuteEx statt über CreateProcess geht und der Inhalt von ArgumentList zu den Parametern wird, die an die Shell gehen. Beim Öffnen eines Dokuments oder einer URL baut die Dateizuordnung die tatsächliche Kommandozeile des Handlers, deshalb erreicht die hier gebaute Zeichenfolge das Ziel nicht unbedingt unverändert. Für Verwendungen, bei denen Sie die Ausgabe umleiten oder den Exitcode zuverlässig brauchen, setzen Sie UseShellExecute = false und entwerfen den Code so, dass er Standardausgabe und Standardfehler gleichzeitig liest. Dieser Teil steht in „Eine Checkliste für den sicheren Umgang mit Kindprozessen in Windows-Apps“.

8. Implementierung in C++ / Win32

In C++ schreiben Sie beide Seiten selbst, Aufbau und Zerlegung. Für den Aufbau machen Sie die Regeln aus Kapitel 6 direkt zu einer Funktion.

#include <windows.h>
#include <string>
#include <stdexcept>
#include <string_view>
#include <vector>

// Hängt ein Argument für argv[1] und folgende an. Die Regeln sind die Umkehrung der Zerlegungsregeln von CommandLineToArgvW / CRT.
void AppendArgument(std::wstring& cmd, std::wstring_view arg)
{
    if (!cmd.empty()) cmd += L' ';
    if (arg.find(L'\0') != std::wstring_view::npos)
        throw std::invalid_argument("Ein Argument darf kein NUL-Zeichen enthalten (die Kommandozeile ist eine nullterminierte Zeichenfolge und würde dort abgeschnitten)");

    const bool needsQuote =
        arg.empty() || arg.find_first_of(L" \t\"") != std::wstring_view::npos;
    if (!needsQuote) { cmd += arg; return; }

    cmd += L'"';
    for (size_t i = 0; ; ) {
        size_t backslashes = 0;
        while (i < arg.size() && arg[i] == L'\\') { ++i; ++backslashes; }

        if (i == arg.size()) {
            cmd.append(backslashes * 2, L'\\');           // abschließend: verdoppelt
            break;
        }
        if (arg[i] == L'"') {
            cmd.append(backslashes * 2 + 1, L'\\');       // vor einem Anführungszeichen: verdoppelt plus eins
            cmd += L'"';
        } else {
            cmd.append(backslashes, L'\\');               // alles andere: unverändert
            cmd += arg[i];
        }
        ++i;
    }
    cmd += L'"';
}

// argv[0] (die ausführbare Datei) folgt einer anderen Regel: nur bei Leerzeichen in Anführungszeichen setzen. Ein Anführungszeichen darf nicht enthalten sein.
std::wstring QuoteArgv0(std::wstring_view exe)
{
    if (exe.find(L'\0') != std::wstring_view::npos)
        throw std::invalid_argument("Der Pfad der ausführbaren Datei darf kein NUL-Zeichen enthalten (sowohl lpApplicationName als auch die Kommandozeile würden dort abgeschnitten, und der Pfad bis dahin könnte gestartet werden)");
    if (exe.find(L'"') != std::wstring_view::npos)
        throw std::invalid_argument("Der Pfad der ausführbaren Datei darf kein Anführungszeichen enthalten");
    if (exe.empty() || exe.find_first_of(L" \t") != std::wstring_view::npos)
        return L'"' + std::wstring(exe) + L'"';
    return std::wstring(exe);
}

Im Aufruf übergeben Sie in lpApplicationName den vollständigen Pfad der ausführbaren Datei und in lpCommandLine einen schreibbaren Puffer.

const std::wstring exe = LR"(C:\Program Files\MyTool\convert.exe)";

std::wstring cmd = QuoteArgv0(exe);          // argv[0] mit der ausführbaren Datei konsistent halten
AppendArgument(cmd, L"--input");
AppendArgument(cmd, inputPath);
AppendArgument(cmd, L"--output");
AppendArgument(cmd, outputPath);

std::vector<wchar_t> buffer(cmd.begin(), cmd.end());
buffer.push_back(L'\0');                     // CreateProcessW darf die Zeichenfolge ändern

STARTUPINFOW si{}; si.cb = sizeof(si);
PROCESS_INFORMATION pi{};
if (!CreateProcessW(exe.c_str(),             // lpApplicationName: niemals NULL
                    buffer.data(),           // lpCommandLine: beginnt mit demselben Pfad, quotiert
                    nullptr, nullptr, FALSE, CREATE_UNICODE_ENVIRONMENT,
                    nullptr, nullptr, &si, &pi)) {
    const DWORD err = GetLastError();
    // err hier protokollieren und an den Aufrufer zurückgeben. Nicht verschlucken
    return;
}
CloseHandle(pi.hThread);                     // das Handle des Hauptthreads wird nicht gebraucht, deshalb zuerst schließen

switch (WaitForSingleObject(pi.hProcess, INFINITE)) {   // bei Bedarf eine Zeitüberschreitung hinzufügen
case WAIT_OBJECT_0: {                        // er ist beendet. Den Exitcode nur in diesem Zweig lesen
    DWORD exitCode = 0;
    if (!GetExitCodeProcess(pi.hProcess, &exitCode)) {
        const DWORD err = GetLastError();
        // Auch den Fehler beim Holen protokollieren und als Fehlschlag an den Aufrufer zurückgeben
    } else if (exitCode != 0) {
        // Das Ziel ist gestartet, aber seine Verarbeitung ist fehlgeschlagen. Nicht wie 0 behandeln;
        // den Exitcode protokollieren und an den Aufrufer zurückgeben (wie die ExitCode-Prüfung im C#-Beispiel)
    }
    break;
}
case WAIT_TIMEOUT:
    // Läuft noch. GetExitCodeProcess hier aufzurufen, liefert nur STILL_ACTIVE (259),
    // das ist kein Exitcode. Dieses Beispiel nimmt die Richtlinie „eine Zeitüberschreitung als Fehlschlag falten“:
    // nur wenn die Beendigungsanforderung durchgeht, sehen wir das Ende, dann weiter zu CloseHandle unten.
    // Ist die Richtlinie, weiter zu warten, hier nicht breaken und die Handles schließen (das würde
    // das Kind loslassen, während es noch läuft). Zum Warten zurückkehren
    if (!TerminateProcess(pi.hProcess, 1)) {
        const DWORD err = GetLastError();
        // Konnte nicht beendet werden (fehlende Rechte usw.). Hier mit INFINITE zu warten, würde die
        // Frist, die Überläufe verhindern soll, bedeutungslos machen. err protokollieren und ohne Warten
        // einen Fehlschlag an den Aufrufer zurückgeben (das Kind wird noch laufend losgelassen, das ebenfalls protokollieren)
        break;
    }
    WaitForSingleObject(pi.hProcess, INFINITE); // die Beendigungsanforderung ist durch, deshalb das Ende sehen, bevor geschlossen wird
    // Die Zeitüberschreitung als Fehlschlag an den Aufrufer zurückgeben
    break;
default: {                                   // WAIT_FAILED
    const DWORD err = GetLastError();
    // Auch den Fehlschlag des Wartens selbst protokollieren
    break;
}
}
CloseHandle(pi.hProcess);                    // das zu vergessen lässt bei jedem Start ein Handle auslaufen
Die Rollenteilung der zwei Argumente, die an CreateProcessW gehenlpApplicationName legt das auszuführende Modul fest, und lpCommandLine entscheidet die Zeichenfolge, die der Kindprozess über GetCommandLineW empfängt. lpCommandLine als schreibbaren Puffer übergeben und das führende argv[0] mit lpApplicationName konsistent haltenKonsistent haltenlpApplicationName: der vollständige Pfad der ausführbaren DateiDas auszuführende Modul ist festgelegtlpCommandLine: ein schreibbarer PufferDie Zeichenfolge, die das Kind über GetCommandLineW empfängtFührendes Token = argv[0]Der Rest = nach den Regeln von Kapitel 6 gebaute Argumente

Abbildung 11: „Was auszuführen ist“ und „was zu übergeben ist“ entscheiden unterschiedliche Argumente. Machen Sie beides ausdrücklich, und weder das Program.exe-Problem noch die Zugriffsverletzung durch einen nicht schreibbaren Puffer tritt auf.

Auf der Empfangsseite übergeben Sie den Rückgabewert von GetCommandLineW an CommandLineToArgvW, um ihn in argv-Form zu erhalten. Geben Sie den Rückgabewert mit einem einzigen LocalFree frei. Es gibt Randverhalten: ist lpCmdLine eine leere Zeichenfolge, wird der Pfad der aktuellen ausführbaren Datei zurückgegeben, und beginnt sie mit einem Leerzeichen, wird das erste Argument zu einer leeren Zeichenfolge.3

int argc = 0;
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
if (argv == nullptr) {
    const DWORD err = GetLastError();
    // Auch den Parse-Fehler protokollieren
    return 1;
}
for (int i = 0; i < argc; ++i) {
    // argv[0] ist der Name der ausführbaren Datei. Das Betriebssystem kann den vollständigen Pfad ergänzt haben
}
LocalFree(argv);

Wenn Sie main / wmain verwenden, erledigt die C-Laufzeit dasselbe beim Start für Sie. Beachten Sie jedoch, dass argv in main eine schmale Zeichenfolge ist, die in die aktuelle Codepage gewandelt wurde, deshalb gehen Zeichen, die die Codepage nicht darstellen kann (zum Beispiel ein japanischer Pfad auf einem PC außerhalb einer japanischen Umgebung), hier verloren. Die Aufbau-Funktion aus Kapitel 6 „rundet“ gegenüber Empfängern, die Wide-Zeichen unverändert zerlegen, etwa wmain, CommandLineToArgvW und .NET. Standardmäßig werden Platzhalter nicht expandiert, aber das Linken von setargv.obj (wsetargv.obj für wmain) lässt * und ? expandieren.4 Übergeben Sie einem Ziel mit dieser Einstellung ein Argument, das * in einem Dateinamen enthält, weichen die ankommenden Argumente von dem ab, was Sie beabsichtigt haben.

9. Wenn cmd.exe und Batch-Dateien dazwischenkommen

Die bisherigen Regeln gelten, wenn die Zeichenfolge direkt von CreateProcess zum Ziel-Exe geht. Sitzt cmd.exe dazwischen, kommt eine weitere Interpretationsstufe hinzu.

cmd.exe behandelt &, |, ( und ) als Syntax, und um sie als Argumente zu übergeben, müssen Sie sie mit ^ escapen oder in Anführungszeichen setzen. Die Behandlung von Anführungszeichen in der Zeichenfolge nach /c oder /k hat eigene Regeln, und ob „die äußeren Anführungszeichen entfernt werden“, ändert sich mit dem Vorhandensein von /s, der Anzahl der Anführungszeichen und dem Vorhandensein besonderer Zeichen.17 Außerdem empfängt eine Batch-Datei die Argumente nicht zerlegt, sondern als Roh-Kommandozeilenzeichenfolge. Die offizielle PowerShell-Dokumentation warnt klar davor, nicht vertrauenswürdige Eingaben an Batch-Dateien zu übergeben.7 Die Dokumentation von CreateProcess sagt, dass man zum Starten einer Batch-Datei cmd.exe in lpApplicationName angibt und /c plus den Batch-Namen übergibt, und merkt dann an, dass das Engineering-Team von MSRC das nicht empfiehlt, mit einem Link zur Darstellung von MS14-019.1 Was MS14-019 behoben hat, war das Problem, dass beim direkten Übergeben einer Batch-Datei an CreateProcess cmd.exe zuerst im aktuellen Verzeichnis gesucht und so entführt werden konnte, und die Empfehlung von MSRC lautet, „den vollständig qualifizierten Pfad von cmd.exe zu übergeben und die Batch-Datei zu ihrem Argument zu machen“.18 Mit anderen Worten: das Problem ist, eine Batch-Datei zu starten, ohne cmd.exe mit vollständigem Pfad zu nennen (lpApplicationName auf NULL zu setzen und den Batch-Namen den Start auslösen zu lassen), nicht der /c-Start selbst mit dem vollständigen Pfad von cmd.exe in lpApplicationName.

cmd.exe dazwischen fügt Interpretationsstufen hinzuDas Ziel-Exe direkt zu starten bedeutet, dass einmal zerlegt wird, nur im Parser des Ziels; über cmd.exe /c kommt die Syntaxdeutung von cmd.exe hinzu, und eine Batch-Datei darüber hinaus empfängt die Rohzeichenfolge, sodass die Quotierungsregeln auf jeder Stufe wechselnIhr Prozess → das Ziel-ExeEinmal zerlegen, nur durch den Parser des ZielsIhr Prozess → cmd.exe /c → das Ziel-ExeDie Syntaxdeutung von cmd.exe kommt hinzu (Ampersand, Pipe, Klammern, Caret)Zerlegen durch den Parser des ZielsIhr Prozess → cmd.exe /c → eine Batch-DateiDie Batch-Datei empfängt die RohzeichenfolgeNicht vertrauenswürdige Werte hindurchzuleiten wird zu Command Injection

Abbildung 12: Je mehr Stufen, desto mehr vermischen sich die Regeln. Starten Sie direkt, was sich direkt starten lässt, und übergeben Sie niemals Werte von außen an eine Batch-Datei.

Die praktische Entscheidung ist einfach. Ist das Ziel ein Exe, setzen Sie cmd.exe nicht dazwischen. Müssen Sie ein .bat aufrufen, ist das Prinzip, die Batch Werte von außen nicht deuten zu lassen. Schreiben Sie die Werte in eine Datei, lassen Sie die Batch nur den Pfad dieser Datei als feste Zeichenfolge an das nachgelagerte Exe übergeben, und lesen Sie den Dateiinhalt auf der Exe-Seite. Den Wert in einer Umgebungsvariable abzulegen ist keine Grenze, weil in dem Moment, in dem die Batch ihn als %VAR% expandiert, & und | von cmd.exe erneut gedeutet werden. Der Weg über eine Umgebungsvariable ist nur dann akzeptabel, wenn das nachgelagerte Exe die Variable direkt liest, ohne über die Batch zu gehen. Ist selbst das schwierig, verlagern Sie den Inhalt der Batch nach PowerShell oder in ein eigenes Exe („Sollte diese Batch-Datei zu PowerShell wechseln?“).

10. Längengrenzen

Die Grenzen unterscheiden sich auch nach dem Weg.

Weg Grenze Quelle
lpCommandLine von CreateProcess 32.767 UTF-16-Codeeinheiten (einschließlich des abschließenden Nullzeichens; ein Surrogatpaar zählt als zwei) 1
Der Teil mit dem Namen der ausführbaren Datei, wenn lpApplicationName NULL ist MAX_PATH 1
Die Kommandozeile von cmd.exe (einschließlich Zeilen in einer Batch-Datei) 8.191 Zeichen 8
ProcessStartInfo.Arguments von .NET Zeichenfolgenlänge (UTF-16-Codeeinheiten) unter 32.699 16

Ein Entwurf, der Werte variabler Länge wie eine Dateiliste als Argumente aufreiht, trifft die Grenze an dem Tag, an dem die Anzahl wächst. Für Verwendungen, die an die Grenze herankommen, wechseln Sie zur Methode „Antwortdatei“: schreiben Sie die Argumente in eine einzige Datei und übergeben Sie nur den Pfad dieser Datei. Der offizielle Workaround für die Grenze von cmd.exe ist dieselbe Methode.8 Weder CreateProcess noch cmd.exe expandieren die Datei jedoch für Sie. Diese Methode funktioniert nur, wenn das Zielprogramm eine Antwortdatei mit einer Syntax wie @file lesen kann oder Sie das Ziel so ändern können, dass es das kann. Ist das Ziel ein Fertig-Exe, das Sie nicht ändern können, bleibt nur, die Aufrufe so zu teilen, dass jeder in die Grenze passt.

Die Grenzen, Werte variabler Länge als Argumente zu übergeben, und der Weg darum herumWerte variabler Länge wie eine Dateiliste als Argumente aufzureihen, erreicht mit wachsender Anzahl die Grenze von cmd.exe von 8191 Zeichen oder die Grenze von CreateProcess von 32767 UTF-16-Codeeinheiten. Kann das Ziel eine Antwortdatei lesen (oder so geändert werden), zur Antwortdatei-Methode wechseln, die Werte in eine Datei schreibt und nur den Pfad übergibt; ist das Ziel ein Fertig-Exe, das das nicht kann, die Aufrufe teilenDas Ziel kann eine Antwortdatei lesenEin Fertig-Exe, das das nicht kannWerte variabler Länge (eine Dateiliste usw.) als Argumente aufreihenDie Zeichenfolge wächst mit der AnzahlDie Grenze ist erreicht (cmd.exe 8.191 / CreateProcess 32.767)Eines Tages schlägt der Start plötzlich fehlDie Werte in eine Datei schreiben und nur den Pfad übergeben (Antwortdatei)Die Aufrufe teilen

Abbildung 13: Die Grenze ist die Art Problem, die „heute noch in Ordnung“ ist. Bei Argumenten, die proportional zur Anzahl wachsen, machen Sie es von Anfang an so, wenn das Ziel eine Antwortdatei lesen kann (oder so geändert werden kann).

11. Prüfen, was tatsächlich angekommen ist

Bevor Sie Quotierung nach Vermutung hinzufügen, ist der kürzeste Weg, die Argumente zu betrachten, die das Ziel erreicht haben. Es gibt drei Dinge zu betrachten, „die auf der Aufrufseite gebaute Zeichenfolge“, „die Zeichenfolge, die die Zielseite erreicht hat“, und „das Array nach der Zerlegung“, und vier Mittel dafür. Zuvor ein Versprechen. Welches Mittel Sie auch verwenden: machen Sie Geheimnisse unkenntlich, bevor Sie eine Kommandozeile in ein Protokoll schreiben. Legt der Entwurf Passwörter, API-Schlüssel oder Token in die Argumente, lässt sie unverändert zu schreiben die Geheimnisse im Protokoll, ob es das Protokoll des Aufrufers oder das Startprotokoll des Ziels ist. Protokolle werden länger gehalten als der Prozess und von mehr Leuten gesehen. Überhaupt kann eine Kommandozeile von anderen Prozessen auf derselben Maschine gelesen werden, wie später bei Process Explorer, deshalb ist die grundlegende Gegenmaßnahme ein Entwurf, der Passwörter und Token nicht als Argumente, sondern über einen anderen Weg wie Standardeingabe oder einen geschützten Konfigurationsspeicher übergibt; Unkenntlichmachen in Protokollen ist eine Sicherung darüber. Entweder die zerlegten Argumente (auf der Aufrufseite die Elemente vor dem Aufbau) deuten und die Werte von Optionen, die geheim sein können, vor dem Aufzeichnen unkenntlich machen, oder das Aufzeichnen der Rohzeichenfolge nur in einem eingeschränkten Diagnosemodus einschalten.

  1. Protokollieren Sie auf der Aufrufseite die Zeichenfolge, die Sie gebaut haben. Das ist das lpCommandLine unmittelbar, bevor es an CreateProcess geht. Dieser Vergleich setzt einen Start mit UseShellExecute = false oder einen direkten Aufruf von CreateProcess voraus. Wenn Sie ein Dokument oder eine URL mit UseShellExecute = true öffnen, baut die Dateizuordnung die tatsächliche Kommandozeile über ShellExecuteEx (Kapitel 7), deshalb unterscheiden sich die Zeichenfolge des Aufrufers und die des Ziels auch ohne cmd.exe oder eine Batch-Datei, und das ist nicht das Problem aus Kapitel 9. Wenn Sie .NETs ArgumentList verwenden, lässt sich die Liste der Elemente unverändert nicht für den Vergleich verwenden. Die Elemente sind die Werte vor der Quotierung und vor dem Verdoppeln abschließender Backslashes, und was das Betriebssystem erreicht, ist die Zeichenfolge, die .NET daraus formatiert hat. Entweder rekonstruieren Sie aus den Elementen mit denselben Regeln wie BuildArguments in Kapitel 7 eine einzige Zeichenfolge und zeichnen die auf (sie liefert dasselbe Ergebnis wie die Formatierung, die ArgumentList intern vornimmt), oder vergleichen Sie die Liste der Elemente direkt mit dem Array nach der Zerlegung. Das ist das einzige Mittel, „den ursprünglichen Puffer des Aufrufers“ zu sehen; Process Explorer und das Protokoll des Ziels, weiter unten, zeigen nur die Zeichenfolge, die eine dazwischenliegende Stufe von cmd.exe oder einer Batch neu gebaut hat. Beim Aufzeichnen halten Sie das Versprechen vom Anfang und machen die Werte von Elementen unkenntlich, die geheim sein können (ein unkenntlich gemachtes Element stimmt nicht mehr mit der Zeichenfolge des Ziels überein, deshalb schließen Sie dieses Element vom Vergleich aus).
  2. Bereiten Sie ein Exe vor, das nur seine Argumente anzeigt. Starten Sie es anstelle des Ziel-Exe und lassen Sie es die empfangenen args zeilenweise ausgeben. Schreiben Sie die Werte unverändert, kann ein Argument mit Zeilenumbrüchen oder Steuerzeichen als mehrere Zeilen erscheinen oder benachbarte Zeilen überschreiben, und Sie zählen falsch, deshalb geben Sie jeden Wert als JSON-Zeichenfolge escaped zusammen mit seiner Länge aus (das Escaping ist umkehrbar, deshalb lässt sich der ursprüngliche Wert wiederherstellen). Denken Sie jedoch daran, dass es, wie Kapitel 3 erklärt, drei Parser-Linien gibt und sie Randformen wie zwei aufeinanderfolgende Anführungszeichen innerhalb der Quotierung unterschiedlich deuten. Verwenden Sie ein Anzeige-Exe, das mit derselben Runtime wie das Ziel gebaut ist (C++ mit wmain, wenn das Ziel MSVC C/C++ ist, .NET, wenn es .NET ist). Ist das Ziel Ihr eigenes Programm, ist der zuverlässigste Ansatz, das Anzeige-Exe zu überspringen und argv beim eigenen Start des Ziels zu protokollieren (unter der Unkenntlichmachungsregel im nächsten Punkt). Für .NET reichen die folgenden wenigen Zeilen.
using System.Text.Encodings.Web;
using System.Text.Json;

// Zeilenumbrüche, Steuerzeichen, Anführungszeichen und Backslashes escapen; japanischen Text unverändert ausgeben
var json = new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

Console.WriteLine("CommandLine: " + JsonSerializer.Serialize(Environment.CommandLine, json)); // die eine Zeichenfolge
for (int i = 0; i < args.Length; i++)
    Console.WriteLine($"[{i}] len={args[i].Length} {JsonSerializer.Serialize(args[i], json)}");
    // Nach der Zerlegung. Jeder Eintrag passt immer in eine Zeile, und eine leere Zeichenfolge erscheint als len=0 und "". len ist in UTF-16-Codeeinheiten
  1. Betrachten Sie die Kommandozeile des Kindprozesses in Process Explorer. Die Prozesseigenschaften zeigen die Kommandozeilenzeichenfolge, die der Kindprozess hält. Das ist ein Mittel, „die Zeichenfolge, die die Zielseite erreicht hat“ zu prüfen; es sagt Ihnen nicht „das Array nach der Zerlegung“. Gezeigt wird die Zeichenfolge, die auf der Seite des Kindprozesses gehalten wird, deshalb kann, wie in Kapitel 2 berührt, das Betriebssystem den führenden Namen der ausführbaren Datei durch den vollständigen Pfad ergänzt haben, und sitzt cmd.exe oder eine Batch-Datei dazwischen, sehen Sie die Zeichenfolge, die cmd.exe neu gebaut hat. Die Punkte sind, sich nicht allein über einen Unterschied im führenden Token zu beunruhigen, und dass die ursprüngliche Zeichenfolge des Aufrufers nur aus dem Protokoll in Punkt 1 bekannt sein kann. Die Verwendung steht in „Process Explorer / Handle / VMMap in der Praxis“.
  2. Protokollieren Sie beim Start Ihrer eigenen App die Kommandozeile, die sie empfangen hat. Wenn jemand vor Ort sagt „es startet nicht“, lässt sich mit einer Aufzeichnung der Zeichenfolge, mit der gestartet wurde, zuerst eingrenzen, ob es ein Argumentproblem ist. Auch hier speichern Sie den Rückgabewert von GetCommandLineW nicht unverändert. Halten Sie das Versprechen vom Anfang: entweder die zerlegten Argumente deuten und Werte, die geheim sein können, vor dem Aufzeichnen unkenntlich machen, oder das Aufzeichnen der Rohzeichenfolge nur in einem eingeschränkten Diagnosemodus einschalten.

Die Vergleichsreihenfolge ist wie folgt. Vergleichen Sie zuerst die Zeichenfolge des Aufrufers (Punkt 1) mit der Zeichenfolge des Ziels (Punkt 3 oder 4). Stimmen sie abgesehen vom führenden Namen der ausführbaren Datei nicht überein, hat eine Stufe dazwischen sie verformt: cmd.exe oder eine Batch-Datei bei einem direkten Start (Kapitel 9), oder die Dateizuordnung der Shell bei UseShellExecute = true (Kapitel 7). Den Code durch die Funktion aus Kapitel 6 zu ersetzen, behebt das nicht. Stimmen sie überein, vergleichen Sie diese Zeichenfolge mit dem Array nach der Zerlegung (Punkt 2). Ist sie nach den Regeln zerlegt, aber nicht das gewünschte Array, liegt das Problem auf der Aufbauseite; ist sie nicht nach den Regeln zerlegt, liegt das Problem beim Parser des Empfängers.

Die Reihenfolge, um ein Argumentproblem einzugrenzenZuerst das Protokoll der auf der Aufrufseite gebauten Zeichenfolge mit der Zeichenfolge des Ziels vergleichen, wie sie in Process Explorer oder im Startprotokoll des Ziels erscheint. Stimmen sie abgesehen vom führenden Namen der ausführbaren Datei nicht überein, hat eine Stufe dazwischen (cmd.exe oder eine Batch-Datei bei einem direkten Start, die Dateizuordnung der Shell bei UseShellExecute=true) sie verformt. Stimmen sie überein, mit dem Array nach der Zerlegung vergleichen; ist es nach den Regeln zerlegt, aber nicht das gewünschte Array, liegt das Problem auf der Aufbauseite, und ist es nicht nach den Regeln zerlegt, liegt das Problem beim Parser des EmpfängersNeinJaJa: zerlegt, aber nicht das gewünschte ArrayNein: nicht nach den Regeln zerlegtDie Argumente sind falschDie auf der Aufrufseite gebaute Zeichenfolge betrachten (Protokoll des Aufrufers)Die Zeichenfolge des Ziels betrachten (Process Explorer / Startprotokoll des Ziels)Stimmen sie abgesehen vom führenden Namen der ausführbaren Datei überein?Eine Stufe dazwischen hat sie verformt (siehe Kapitel 9 und 7)Das Array nach der Zerlegung betrachten (ein Anzeige-Exe auf derselben Runtime wie das Ziel)Entsprechen Zeichenfolge und Array den Regeln?Ein Problem der Aufbauseite: durch die Funktion aus Kapitel 6 ersetzenEin Problem im Parser des Empfängers

Abbildung 14: Vergleichen Sie der Reihe nach die drei Dinge „die Zeichenfolge des Aufrufers“, „die Zeichenfolge des Ziels“ und „das Array“, und es entscheidet sich mechanisch, ob die Verantwortung bei einer Stufe dazwischen, der Aufbauseite oder der Empfangsseite liegt. Escapes nach Vermutung hinzuzufügen, kann bis nach dieser Prüfung warten.

12. Eine grobe Orientierung (Entscheidungstabelle)

Situation Was zu tun ist
Ein Exe aus .NET Core 2.1 oder höher / .NET 5 oder höher starten Elementweise zu ProcessStartInfo.ArgumentList hinzufügen
Ein Exe aus .NET Framework starten Arguments mit einer Funktion nach den Regeln aus Kapitel 6 bauen. Niemals Anführungszeichen von Hand schreiben
Aus C++ starten lpApplicationName übergeben und lpCommandLine nach den Regeln in einem schreibbaren Puffer bauen
Ein Anführungszeichen im Argumentwert Nur die Form \" verwenden. Niemals benachbarte Anführungszeichen innerhalb eines nichtleeren Arguments setzen
Der Pfad endet mit einem Backslash Wenn Sie umschließen, den abschließenden Backslash verdoppeln. Gibt es keine Leerzeichen, nicht umschließen
Ein leeres Argument übergeben "" setzen. Lassen Sie es weg, verschwindet das ganze Argument
Der Pfad der ausführbaren Datei enthält ein Leerzeichen lpApplicationName übergeben und auch das führende Token in Anführungszeichen setzen
Sie müssen ein .bat aufrufen Die Batch Werte von außen nicht deuten lassen. Sie in eine Datei schreiben und das nachgelagerte Exe sie lesen lassen (eine in der Batch als %VAR% expandierte Umgebungsvariable ist keine Grenze)
Die Argumente werden lang Kann das Ziel eine Antwortdatei lesen (oder so geändert werden), zu einer Antwortdatei wechseln. Bei einem Fertig-Exe die Aufrufe teilen
Sie wissen nicht, was ankommt Die drei der Reihe nach vergleichen: das Protokoll des Aufrufers, die Zeichenfolge des Ziels (Process Explorer / Startprotokoll) und das Array nach der Zerlegung (ein Anzeige-Exe auf derselben Runtime wie das Ziel)

13. Zusammenfassung

Windows-Kommandozeilenargumente überschreiten die Grenze nicht als Array, sondern als eine einzige Zeichenfolge. Der Aufrufer verkettet, der Empfänger zerlegt, und die Zerlegungsregeln verdichten sich auf drei: „an Leerzeichen trennen“, „in Anführungszeichen setzen“ und „nur Backslashes unmittelbar vor einem Anführungszeichen sind besonders“. Nur der führende Name der ausführbaren Datei folgt einer anderen Regel, und das Weglassen von lpApplicationName macht die Deutung eines Pfads mit Leerzeichen mehrdeutig.

Was die Aufbauseite tun muss, passt in eine Funktion, und unter .NET Core 2.1 oder höher übernimmt ArgumentList das. Für die ausführbare Datei den vollständigen Pfad in lpApplicationName übergeben und denselben Pfad, in Anführungszeichen gesetzt, auch an den Anfang von lpCommandLine stellen (in .NET FileName überlassen). Niemals die Form mit benachbarten Anführungszeichen innerhalb eines nichtleeren Arguments erzeugen (das "", das ein leeres Argument darstellt, ist etwas anderes), niemals Werte von außen durch cmd.exe oder eine Batch-Datei leiten, und bei Argumenten, die proportional zur Anzahl wachsen, eine Antwortdatei nur verwenden, wenn das Ziel eine lesen kann (oder so geändert werden kann), andernfalls die Aufrufe teilen. Halten Sie diese fünf Punkte, und die Fehler „es startet nur auf einem PC mit einem Leerzeichen im Pfad nicht“ und „das nächste Argument verschwindet wegen eines abschließenden Backslashs“ entstehen nie.

Fünf Versprechen, die Argumentfehler verhindernDen vollständigen Pfad der ausführbaren Datei in lpApplicationName übergeben und auch das führende Token in Anführungszeichen setzen, die Quotierung einer regelkonformen Funktion oder ArgumentList überlassen, niemals die Form mit benachbarten Anführungszeichen innerhalb eines nichtleeren Arguments erzeugen, niemals Werte von außen durch cmd.exe oder eine Batch-Datei leiten, und für Argumente, die mit der Anzahl wachsen, eine Antwortdatei nur verwenden, wenn das Ziel eine lesen kann. Unter der Annahme, dass das Ziel nach den veröffentlichten Zerlegungsregeln deutet und die Platzhaltererweiterung nicht eingeschaltet hat, verhindern diese fünf Punkte die Fehler durch Pfade mit Leerzeichen und abschließende BackslashesDen vollständigen Pfad in lpApplicationName übergeben und auch das führende Token quotierenDie Quotierung einer regelkonformen Funktion oder ArgumentList überlassenNiemals benachbarte Anführungszeichen innerhalb der Quotierung erzeugenNiemals Werte von außen durch cmd.exe oder eine Batch-Datei leitenFür wachsende Argumente eine Antwortdatei verwenden (wenn das Ziel eine lesen kann)Keine Fehler durch Leerzeichen oder abschließende Backslashes

Abbildung 15: Jedes der fünf Versprechen ist eine Umformulierung von „das auszuführende Modul festlegen und nur Zeichenfolgen übergeben, die der Parser des Ziels zerlegen kann“. Die Prämisse ist, dass das Ziel nach den veröffentlichten Zerlegungsregeln deutet und die Platzhaltererweiterung nicht eingeschaltet hat (Kapitel 6 und 8); darauf aufbauend verhindern diese fünf Punkte die Fehler durch Leerzeichen und abschließende Backslashes.

Wenn es nicht funktioniert, betrachten Sie, bevor Sie Escapes nach Vermutung hinzufügen, die drei Dinge: die auf der Aufrufseite gebaute Zeichenfolge, die Zeichenfolge, die die Zielseite erreicht hat, und das Array nach der Zerlegung. Unterscheiden sich die Zeichenfolgen von Aufrufer und Ziel, ist eine Stufe dazwischen verantwortlich (cmd.exe oder eine Batch-Datei, oder die Dateizuordnung der Shell bei UseShellExecute = true); sind sie dieselben, entscheidet die Zuordnung zwischen Zeichenfolge und Array, ob es die Aufbauseite oder die Empfangsseite ist.

Verwandte Artikel

Verwandte Beratungsleistungen

Die KomuraSoft LLC übernimmt den Entwurf von Windows-Apps, die externe Werkzeuge und interne EXEs kombinieren, die Ursachenuntersuchung von Kindprozess-Starts, die „in manchen Umgebungen starten und in anderen nicht“, und die Überprüfung von Prozessstart-Code im Rahmen der Migration von .NET Framework nach .NET. Melden Sie sich gern auch wegen eines einzelnen Falls „die Argumente werden entstellt“.

  1. Microsoft Learn, CreateProcessW function (processthreadsapi.h). Dazu, dass lpCommandLine eine einzige Zeichenfolge von höchstens 32.767 Zeichen ist (einschließlich des abschließenden Nullzeichens; UTF-16-Codeeinheiten, weil es eine Wide-Zeichenfolge ist), dass die Unicode-Variante den Inhalt ändern kann, sodass schreibgeschützter Speicher nicht übergeben werden darf, dass das führende, an Leerzeichen getrennte Token zum Modulnamen wird, wenn lpApplicationName NULL ist, wobei ein Pfad mit Leerzeichen beginnend bei c:\program.exe gedeutet wird, zur Gefahr, dass eine andere ausführbare Datei läuft, wenn Program.exe dort liegt, und zur Notwendigkeit, NULL zu vermeiden oder den Pfad in Anführungszeichen zu setzen, dazu, dass argv[0] bei Angabe beider nicht mit dem Modulnamen übereinstimmen kann, dazu, dass der Modulname-Teil bei NULL auf MAX_PATH begrenzt ist, und dazu, dass zum Starten einer Batch-Datei cmd.exe /c nötig ist. Siehe auch die Anmerkung in CreateProcessA function, dass das Engineering-Team von MSRC diese Methode nicht empfiehlt (mit einem Link zur Darstellung von MS14-019).  2 3 4 5 6 7 8 9 10

  2. Microsoft Learn, GetCommandLineW function (processenv.h). Dazu, dass sie die Kommandozeilenzeichenfolge des aktuellen Prozesses zurückgibt, der Rückgabewert nicht freigegeben oder geändert werden darf, dass er über CommandLineToArgvW in argv-Form gewandelt werden kann, und dass er nicht mit der Zeichenfolge übereinstimmen muss, die der Elternprozess an CreateProcess übergeben hat, weil das Betriebssystem den vollständigen Pfad des Namens der ausführbaren Datei ergänzt.  2

  3. Microsoft Learn, CommandLineToArgvW function (shellapi.h). Zur besonderen Behandlung von Backslashes unmittelbar vor einem doppelten Anführungszeichen (2n ergibt n plus Öffnen oder Schließen der Quotierung, 2n+1 ergibt n plus ein literales Anführungszeichen, und sie bleiben unverändert, wenn kein Anführungszeichen folgt), dazu, dass Leerzeichen im Modus „in Anführungszeichen“ Teil des Arguments werden, dass der führende Programmname mit oder ohne Anführungszeichen erlaubt ist, dass das erste Argument zu einer leeren Zeichenfolge wird, wenn lpCmdLine mit einem Leerzeichen beginnt, dass der Pfad der aktuellen ausführbaren Datei zurückgegeben wird, wenn eine leere Zeichenfolge übergeben wird, und dass der Rückgabewert mit einem einzigen LocalFree freigegeben wird.  2 3 4 5

  4. Microsoft Learn, main function and command-line arguments. Zu den Regeln, nach denen der Microsoft-C/C++-Startcode die Kommandozeile deutet (Trennung durch Leerzeichen und Tabulatoren, argv[0] quotierbar, aber nicht den folgenden Regeln unterworfen, eine quotierte Zeichenfolge ein Argument, das Caret kein Escape-Zeichen, zwei aufeinanderfolgende Anführungszeichen innerhalb von Anführungszeichen ein Anführungszeichen, alles bis zum Ende das letzte Argument, wenn es kein schließendes Anführungszeichen gibt, und die Behandlung gerader und ungerader Anzahlen von Backslashes), zur Tabelle von Eingaben und argv, zur Platzhaltererweiterung mit setargv.obj, und dazu, dass argv[0] nicht der Name der ausführbaren Datei sein muss, wenn sowohl lpApplicationName als auch lpCommandLine angegeben sind, sodass er mit GetModuleFileName geholt werden sollte.  2 3 4 5 6 7 8

  5. Microsoft Learn, ProcessStartInfo.ArgumentList Property. Dazu, dass die hinzugefügten Zeichenfolgen kein Escape im Voraus brauchen, ArgumentList und Arguments unabhängig sind und nicht gleichzeitig verwendet werden können, ArgumentList die Argumente escaped und intern eine einzige Zeichenfolge baut, die bei Process.Start an das Betriebssystem geht, ArgumentList die Wahl ist, wenn Sie bei der Quotierung unsicher sind, zur Gefahr der Kombination mit nicht vertrauenswürdigen Daten, und dazu, dass es für .NET Core 2.1 und später gilt.  2 3 4

  6. dotnet/runtime (GitHub), PasteArguments.cs und PasteArguments.Windows.cs. Der Aufbau-Code, der innerhalb von ArgumentList verwendet wird. Dazu, ein nichtleeres Argument ohne Leerzeichen und Anführungszeichen unverändert zu stellen, andernfalls in Anführungszeichen zu setzen, abschließende Backslashes zu verdoppeln, Backslashes vor einem Anführungszeichen zu verdoppeln plus eins, Anführungszeichen immer einen Backslash voranzustellen, die Form eines Anführungszeichens nach einem schließenden Anführungszeichen nicht zu erzeugen, weil VC vor und nach 2008 sie unterschiedlich deutet, und für argv[0] nur bei Leerzeichen in Anführungszeichen zu setzen und eine Ausnahme zu werfen, wenn es ein Anführungszeichen enthält.  2 3 4 5 6

  7. Microsoft Learn, about_Parsing. Dazu, dass Argumente an eine Batch-Datei als Roh-Kommandozeilenzeichenfolge an cmd.exe gehen, und zur Warnung, nicht vertrauenswürdige Eingaben nicht zu übergeben.  2

  8. Microsoft Learn, Command prompt (Cmd.exe) command-line string limitation. Dazu, dass die maximale Länge einer am Eingabeaufforderung verwendbaren Zeichenfolge 8.191 Zeichen beträgt, das auch für Kommandozeilen innerhalb von Batch-Dateien gilt, und zum Workaround, die Argumente in eine Datei zu schreiben und diesen Dateinamen zu übergeben.  2 3

  9. Microsoft Learn, WinMain function (winbase.h). Dazu, dass lpCmdLine die Kommandozeile ohne den Programmnamen ist, die gesamte Kommandozeile mit GetCommandLine geholt wird, und wWinMain als Unicode-Einstiegspunkt existiert. 

  10. dotnet/runtime (GitHub), apphost.c und dotnet.cpp. Dazu, dass die Einstiegspunkte des apphost und von dotnet.exe unter Windows wmain(int argc, wchar_t* argv[]) sind und das von der C-Laufzeit gebaute argv direkt an die Startverarbeitung des Hosts übergeben. 

  11. dotnet/runtime (GitHub), corhost.cpp. Dazu, dass ExecuteAssembly das Array von Environment.GetCommandLineArgs() mit SetCommandLineArgs(pwzAssemblyPath, argc, argv) baut, das erste Element der vom Host übergebene Startname ist (oder der Assembly-Pfad, falls keiner) gefolgt von argv, und nur dieses argv an Main geht.  2

  12. dotnet/runtime (GitHub), Environment.cs und Environment.Windows.cs. Dazu, dass GetCommandLineArgs das beim Start initialisierte Array (s_commandLineArgs) zurückgibt, eine gehostete Bibliothek ohne es als Fallback den Rückgabewert von GetCommandLineW mit der eigenen SegmentCommandLine der Runtime zerlegt, diese Regeln der Dokumentation der main-Funktion von MSVC folgen, und CommandLineToArgvW nicht verwendet wird, weil sein Verhalten geringfügig abweicht.  2 3

  13. Microsoft Learn, Main() and command-line arguments. Dazu, dass args in Main niemals null ist und, anders als in C/C++, der Programmname nicht am Anfang von args steht, sondern das erste Element von GetCommandLineArgs() ist. 

  14. Microsoft Learn, dotnet command. Dazu, dass das Ausführen einer App die Form dotnet [runtime options] <app path> [arguments] hat, wobei alles nach dem App-Pfad die Argumente sind, die an die App gehen. 

  15. Microsoft Learn, Environment.GetCommandLineArgs Method. Dazu, dass das erste Element der Name der ausführbaren Datei ist, Argumente durch Leerzeichen getrennt werden und doppelte Anführungszeichen Leerzeichen darin erlauben, einfache Anführungszeichen diese Funktion nicht haben, zu den Regeln für gerade und ungerade Anzahlen von Backslashes und Anführungszeichen, und zur Tabelle von Eingaben und Ergebnissen. 

  16. Microsoft Learn, ProcessStartInfo.Arguments Property. Dazu, dass die Zeichenfolgenlänge unter 32.699 liegt, die Argumente von der Zielanwendung gedeutet werden und deshalb deren Erwartungen entsprechen müssen, die Anführungszeichen selbst nicht an das Ziel gehen, wenn ein Argument mit Leerzeichen quotiert wird, und zur Unabhängigkeit von ArgumentList 2

  17. Microsoft Learn, cmd. Dazu, dass &, | und ( ) besondere Zeichen sind, die ^ oder Anführungszeichen brauchen, zur Liste besonderer Zeichen, die in Anführungszeichen gesetzt werden sollten, zu den Bedingungen, unter denen Anführungszeichen mit /c oder /k erhalten bleiben (kein /s, genau ein Paar Anführungszeichen, keine besonderen Zeichen, Leerzeichen enthaltend und ein Name einer ausführbaren Datei), und dazu, wie das führende Anführungszeichen entfernt wird, wenn die Bedingungen nicht erfüllt sind. 

  18. Microsoft Security Response Center, MS14-019 – Fixing a binary hijacking via .cmd or .bat file und Microsoft Security Bulletin MS14-019. Dazu, dass CreateProcess cmd.exe zuerst im aktuellen Verzeichnis gesucht hat, wenn ein .cmd / .bat direkt übergeben wurde, was Entführung erlaubte, die Korrektur immer das cmd.exe des Systems verwendet, und die Empfehlung, dass Anwendungen den vollständig qualifizierten Pfad von cmd.exe mit der Batch-Datei als Argument übergeben. 

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Gibt es unter Windows keine API, die ein Array von Argumenten übergibt?
Nein. Was CreateProcess empfängt, ist eine einzige Zeichenfolge namens lpCommandLine, und genau diese Zeichenfolge erreicht den neuen Prozess (nur beim führenden Namen der ausführbaren Datei kann das Betriebssystem den vollständigen Pfad ergänzen). Was wie ein argv-Array aussieht, entsteht im empfangenden Prozess, indem der Startcode der C-Laufzeit, CommandLineToArgvW oder die .NET-Runtime die Zeichenfolge zerlegt. Argumente zu übergeben heißt deshalb dasselbe wie eine Zeichenfolge zu bauen, die der Parser der Gegenseite wieder in die ursprünglichen Stücke zerlegt.
Wann wird ein Backslash zum Escape-Zeichen?
Nur wenn unmittelbar ein doppeltes Anführungszeichen folgt. Ein Backslash, dem kein doppeltes Anführungszeichen folgt, bleibt unverändert, egal wie viele in Folge stehen. Stehen 2n Backslashes vor einem doppelten Anführungszeichen, werden daraus n Backslashes und das Anführungszeichen öffnet oder schließt die Quotierung; stehen 2n+1 davor, werden daraus n Backslashes und ein literales Anführungszeichen. Wegen dieser Asymmetrie müssen Sie einen abschließenden Backslash in einem Pfad nur dann verdoppeln, wenn Sie den Pfad in Anführungszeichen setzen.
Was soll ich verwenden, ProcessStartInfo.ArgumentList oder Arguments?
Wenn die Werte aus Variablen kommen, ArgumentList. Ein Element wird zu einem Argument, .NET setzt die nötigen Anführungszeichen und Escapes, und intern entsteht eine einzige Zeichenfolge, bevor sie an das Betriebssystem geht. Arguments ist eine Eigenschaft, die eine selbst gebaute Zeichenfolge unverändert übergibt; die beiden sind unabhängig und lassen sich nicht gleichzeitig verwenden. Beachten Sie, dass ArgumentList eine API ist, die mit .NET Core 2.1 eingeführt wurde und unter .NET Framework nicht existiert. Unter .NET Framework bauen Sie Arguments mit der Aufbau-Funktion in diesem Artikel.
Darf ich innerhalb eines quotierten Arguments zwei benachbarte Anführungszeichen schreiben?
Erzeugen Sie das auf der Aufbauseite nicht, weil Empfänger es unterschiedlich deuten. Gemeint ist hier, ein nichtleeres Argument in Anführungszeichen zu setzen und darin zwei benachbarte Anführungszeichen zu platzieren. Das "" , das ein leeres Argument darstellt (nur zwei Anführungszeichen), ist etwas anderes und der korrekte Weg, eine leere Zeichenfolge zu übergeben. Nach den Regeln der MSVC-C-Laufzeit werden zwei aufeinanderfolgende Anführungszeichen innerhalb einer quotierten Zeichenfolge als eines behandelt, aber die offiziellen Regeln von CommandLineToArgvW beschreiben diese Behandlung nicht, und der Quellcode der .NET-Runtime sagt ausdrücklich, dass diese Form nicht erzeugt wird, weil VC vor und nach 2008 sie unterschiedlich deuten. Wenn Sie ein Anführungszeichen als Zeichen übergeben wollen, setzen Sie einen Backslash davor, und jeder Parser liefert dasselbe Ergebnis.
Wenn der Pfad der ausführbaren Datei ein Leerzeichen enthält, was soll ich CreateProcess übergeben, damit es sicher ist?
Der zuverlässige Weg ist, in lpApplicationName den vollständigen Pfad der ausführbaren Datei zu übergeben und denselben Pfad, in Anführungszeichen gesetzt, auch an den Anfang von lpCommandLine zu stellen. Ist lpApplicationName NULL, rät CreateProcess den Namen der ausführbaren Datei vom Anfang von lpCommandLine, getrennt an Leerzeichen. Bei der Zeichenfolge C:\Program Files\MyApp -L -S prüft es zuerst, ob C:\Program.exe existiert; liegt dort eine bösartige Datei, läuft diese. Die offizielle Dokumentation benennt diese Gefahr ausdrücklich und verlangt, NULL zu vermeiden oder den Pfad in Anführungszeichen zu setzen.
Gelten dieselben Regeln, wenn man Argumente an eine Batch-Datei übergibt?
Nein. Eine Batch-Datei wird von cmd.exe interpretiert, und cmd.exe behandelt die Kommandozeile als Rohzeichenfolge, ohne sie in Argumente zu zerlegen. Symbole wie &, |, Klammern und ^ wirken als Syntax von cmd.exe, deshalb macht Quotierung nach den Regeln von CommandLineToArgvW sie nicht sicher. Die offizielle Dokumentation warnt davor, nicht vertrauenswürdige Eingaben an Batch-Dateien zu übergeben. Schreiben Sie die Werte in eine Datei und lassen Sie das nachgelagerte Exe, nicht die Batch, sie lesen, oder verlagern Sie den Inhalt der Batch nach PowerShell oder in ein eigenes Exe. Auch das Ablegen des Werts in einer Umgebungsvariable ist keine Grenze, weil cmd.exe die Symbole erneut deutet, sobald die Batch ihn als %VAR% expandiert.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog