Ä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.
flowchart TB
accTitle: Die Schicht, die dieser Artikel behandelt
accDescr: Die 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-Exe
ps["PowerShell-Argumentübergabe (eigener Artikel)"] --> net[".NET ProcessStartInfo"]
net --> win["Win32 CreateProcessW"]
win --> str["Eine einzige Kommandozeilenzeichenfolge"]
str --> parser["Der Parser des Ziel-Exe"]
net -.->|"Geltungsbereich dieses Artikels"| parser
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), undGetCommandLineWgibt sie zurück.argverzeugt 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. IstlpApplicationNameNULL, wird die Deutung eines Pfads mit Leerzeichen mehrdeutig undC:\Program.exewird 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.ArgumentListin .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@filelesen 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
flowchart TB
accTitle: Der Weg, den Argumente nehmen, um den Kindprozess zu erreichen
accDescr: Das 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 erzeugen
arr["Das Argument-Array des Aufrufers"] --> join["Zu einer Zeichenfolge verkettet (Verantwortung des Aufrufers)"]
join --> cp["lpCommandLine von CreateProcessW"]
cp --> peb["Die Prozessparameter des neuen Prozesses"]
peb --> gcl["Die Zeichenfolge, die GetCommandLineW zurückgibt"]
gcl --> parse["Der Parser der Empfangsseite zerlegt sie"]
parse --> argv["Das 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.
flowchart TB
accTitle: Die drei Parser auf der Empfangsseite
accDescr: Die 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 hat
s["Die Zeichenfolge von GetCommandLineW"] --> crt["Startcode der C-Laufzeit"]
s --> api["CommandLineToArgvW"]
s --> net["Eigener Zerlegungscode von .NET (bei Host-Laden)"]
crt --> app[".NET über apphost / dotnet.exe ist dasselbe"]
crt --> same["Dasselbe Gerüst der Regeln, getrennte Implementierungen"]
api --> same
net --> same
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
- Argumente werden durch Leerzeichen oder Tabulatoren getrennt.
- 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.
- Ein Backslash wird als gewöhnliches Zeichen behandelt. Nur wenn unmittelbar ein doppeltes Anführungszeichen folgt, gelten die nächsten Regeln.
- Stehen 2n Backslashes vor einem doppelten Anführungszeichen, werden n Backslashes ausgegeben, und das Anführungszeichen wirkt als „Beginn oder Ende der Quotierung“.
- Stehen 2n+1 Backslashes vor einem doppelten Anführungszeichen, werden n Backslashes und ein literales Anführungszeichen ausgegeben, und der Quotierungszustand ändert sich nicht.
- 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.
flowchart TB
accTitle: Der Zerlegungsfluss, der zwischen innen und außen der Anführungszeichen wechselt
accDescr: Auß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 folgt
out["Außerhalb von Anführungszeichen: an Leerzeichen trennen"] -->|"Ein Anführungszeichen wird getroffen"| inq["Innerhalb von Anführungszeichen: Leerzeichen sind Teil des Arguments"]
inq -->|"Ein Anführungszeichen wird getroffen"| out
out -->|"Auf einen Backslash folgt unmittelbar ein Anführungszeichen"| bs["Die Backslash-Regel anwenden"]
inq -->|"Auf einen Backslash folgt unmittelbar ein Anführungszeichen"| bs
bs -->|"2n: n ausgeben und öffnen/schließen"| toggle["Den Quotierungszustand kippen"]
bs -->|"2n+1: n und ein literales Anführungszeichen ausgeben"| lit["Den 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.
flowchart TB
accTitle: Wie ein abschließender Backslash das nächste Argument verschluckt
accDescr: Wird 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 wird
a["Ein quotierter Pfad, der mit einem Backslash endet"] --> b["Eine ungerade Anzahl Backslashes steht vor dem schließenden Anführungszeichen"]
b --> c["Das Anführungszeichen wird als Zeichen ausgegeben und die Quotierung schließt nicht"]
c --> d["Nachfolgende Leerzeichen trennen nicht mehr"]
d --> e["Alles bis zum nächsten Argument kommt als ein Argument an"]
a -.->|"Den Backslash verdoppeln"| ok["Die 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
flowchart TB
accTitle: Die Reihenfolge, in der die ausführbare Datei geraten wird, wenn lpApplicationName NULL ist
accDescr: Wird 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 wurde
in["Einen unquotierten Pfad (mit Leerzeichen) in lpCommandLine übergeben"] --> t1["Kandidat 1: C:\Program.exe versuchen"]
t1 -->|"Existiert"| bad["Eine unbeabsichtigte ausführbare Datei startet"]
t1 -->|"Existiert nicht"| t2["Kandidat 2: C:\Program Files\MyApp.exe versuchen"]
t2 --> ok["Die beabsichtigte ausführbare Datei startet"]
in -.->|"lpApplicationName übergeben oder das führende Token quotieren"| ok
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
flowchart TB
accTitle: Wie das ausgeführte Modul und argv[0] entschieden werden
accDescr: Werden 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 holen
app["lpApplicationName"] --> run["Das Modul, das läuft"]
cl["Das führende Token von lpCommandLine"] --> a0["argv[0] des Kindes"]
a0 -.->|"Zerbricht, wenn sie auseinanderlaufen"| self["Code, der den eigenen Pfad aus argv[0] ableitet"]
self -.->|"Stattdessen verwenden"| gmf["GetModuleFileNameW"]
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
- Ist es nicht leer und enthält weder Leerzeichen noch Anführungszeichen, stellen Sie es unverändert.
- 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.
- Eine leere Zeichenfolge stellen Sie als
"".
flowchart TB
accTitle: Der Entscheidungsfluss zum Aufbau eines Arguments
accDescr: Ist 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ßen
s["Ein Argument entgegennehmen"] --> q{"Leer, oder enthält ein Leerzeichen oder ein Anführungszeichen?"}
q -->|"Nein"| raw["Unverändert stellen"]
q -->|"Ja"| open["Öffnendes Anführungszeichen"]
open --> scan["Von links scannen"]
scan --> bq["k Backslashes vor einem Anführungszeichen → 2k+1"]
scan --> be["k abschließende Backslashes → 2k"]
scan --> other["Alles andere unverändert"]
bq --> close["Schließendes Anführungszeichen"]
be --> close
other --> close
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
flowchart TB
accTitle: Wo ArgumentList und Arguments zu einer Zeichenfolge werden
accDescr: Bei 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 Zeichenfolge
al["ArgumentList (1 Element = 1 Argument)"] --> esc[".NET escaped jedes Element und verkettet"]
ar["Arguments (eine selbst gebaute Zeichenfolge)"] --> pass["Unverändert"]
esc --> cmd["Eine einzige Kommandozeilenzeichenfolge"]
pass --> cmd
cmd --> cp["CreateProcess"]
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.
flowchart TB
accTitle: Die Wahl der Aufbaumethode nach .NET-Version
accDescr: Unter .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 schreiben
v{"Welche .NET-Version?"}
v -->|"Core 2.1 oder höher"| al["Elementweise zu ArgumentList hinzufügen"]
v -->|"Framework"| own["Arguments mit einer eigenen Funktion bauen"]
al --> no["Niemals Anführungszeichen von Hand schreiben"]
own --> no
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
flowchart TB
accTitle: Die Rollenteilung der zwei Argumente, die an CreateProcessW gehen
accDescr: lpApplicationName 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 halten
app["lpApplicationName: der vollständige Pfad der ausführbaren Datei"] --> mod["Das auszuführende Modul ist festgelegt"]
cl["lpCommandLine: ein schreibbarer Puffer"] --> child["Die Zeichenfolge, die das Kind über GetCommandLineW empfängt"]
child --> a0["Führendes Token = argv[0]"]
a0 -.->|"Konsistent halten"| app
child --> rest["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.
flowchart TB
accTitle: cmd.exe dazwischen fügt Interpretationsstufen hinzu
accDescr: Das 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 wechseln
direct["Ihr Prozess → das Ziel-Exe"] --> p1["Einmal zerlegen, nur durch den Parser des Ziels"]
p1 ~~~ via
via["Ihr Prozess → cmd.exe /c → das Ziel-Exe"] --> p2["Die Syntaxdeutung von cmd.exe kommt hinzu (Ampersand, Pipe, Klammern, Caret)"]
p2 --> p3["Zerlegen durch den Parser des Ziels"]
p3 ~~~ bat
bat["Ihr Prozess → cmd.exe /c → eine Batch-Datei"] --> p4["Die Batch-Datei empfängt die Rohzeichenfolge"]
p4 --> danger["Nicht 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.
flowchart TB
accTitle: Die Grenzen, Werte variabler Länge als Argumente zu übergeben, und der Weg darum herum
accDescr: Werte 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 teilen
list["Werte variabler Länge (eine Dateiliste usw.) als Argumente aufreihen"] --> grow["Die Zeichenfolge wächst mit der Anzahl"]
grow --> lim["Die Grenze ist erreicht (cmd.exe 8.191 / CreateProcess 32.767)"]
lim --> fail["Eines Tages schlägt der Start plötzlich fehl"]
fail -.->|"Das Ziel kann eine Antwortdatei lesen"| resp["Die Werte in eine Datei schreiben und nur den Pfad übergeben (Antwortdatei)"]
fail -.->|"Ein Fertig-Exe, das das nicht kann"| split["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.
- Protokollieren Sie auf der Aufrufseite die Zeichenfolge, die Sie gebaut haben. Das ist das
lpCommandLineunmittelbar, bevor es anCreateProcessgeht. Dieser Vergleich setzt einen Start mitUseShellExecute = falseoder einen direkten Aufruf vonCreateProcessvoraus. Wenn Sie ein Dokument oder eine URL mitUseShellExecute = trueöffnen, baut die Dateizuordnung die tatsächliche Kommandozeile überShellExecuteEx(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 .NETsArgumentListverwenden, 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 wieBuildArgumentsin Kapitel 7 eine einzige Zeichenfolge und zeichnen die auf (sie liefert dasselbe Ergebnis wie die Formatierung, dieArgumentListintern 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). - Bereiten Sie ein Exe vor, das nur seine Argumente anzeigt. Starten Sie es anstelle des Ziel-Exe und lassen Sie es die empfangenen
argszeilenweise 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++ mitwmain, 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 undargvbeim 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
- 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“.
- 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
GetCommandLineWnicht 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.
flowchart TB
accTitle: Die Reihenfolge, um ein Argumentproblem einzugrenzen
accDescr: Zuerst 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ängers
s["Die Argumente sind falsch"] --> caller["Die auf der Aufrufseite gebaute Zeichenfolge betrachten (Protokoll des Aufrufers)"]
caller --> target["Die Zeichenfolge des Ziels betrachten (Process Explorer / Startprotokoll des Ziels)"]
target --> same{"Stimmen sie abgesehen vom führenden Namen der ausführbaren Datei überein?"}
same -->|"Nein"| mid["Eine Stufe dazwischen hat sie verformt (siehe Kapitel 9 und 7)"]
same -->|"Ja"| arr["Das Array nach der Zerlegung betrachten (ein Anzeige-Exe auf derselben Runtime wie das Ziel)"]
arr --> cmp{"Entsprechen Zeichenfolge und Array den Regeln?"}
cmp -->|"Ja: zerlegt, aber nicht das gewünschte Array"| build["Ein Problem der Aufbauseite: durch die Funktion aus Kapitel 6 ersetzen"]
cmp -->|"Nein: nicht nach den Regeln zerlegt"| recv["Ein 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.
flowchart TB
accTitle: Fünf Versprechen, die Argumentfehler verhindern
accDescr: Den 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 Backslashes
r0["Den vollständigen Pfad in lpApplicationName übergeben und auch das führende Token quotieren"]
r1["Die Quotierung einer regelkonformen Funktion oder ArgumentList überlassen"]
r2["Niemals benachbarte Anführungszeichen innerhalb der Quotierung erzeugen"]
r3["Niemals Werte von außen durch cmd.exe oder eine Batch-Datei leiten"]
r4["Für wachsende Argumente eine Antwortdatei verwenden (wenn das Ziel eine lesen kann)"]
goal["Keine Fehler durch Leerzeichen oder abschließende Backslashes"]
r0 ~~~ r1 ~~~ r2 ~~~ r3 ~~~ r4
r0 --> goal
r1 --> goal
r2 --> goal
r3 --> goal
r4 --> goal
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
- Externe EXEs aus PowerShell korrekt aufrufen — Die Fallstricke bei Argument-Quoting, Exitcodes und Zeichensalat
- Eine Checkliste für den sicheren Umgang mit Kindprozessen in Windows-Apps — Best Practices für Job Objects, Exit-Weitergabe, Standardein-/-ausgabe und Watchdogs
- Was nach dem Tod des Elternprozesses bleibt — Kindprozesse in einem Job Object halten
- MAX_PATH und die Fallstricke von Windows-Pfaden und Dateinamen — das 260-Zeichen-Limit, reservierte Namen, abschließende Punkte und Groß-/Kleinschreibung
- Windows-Shell-Integration heute — Kontextmenüs, Dateizuordnungen und was sich unter Windows 11 geändert hat
- Mehrfachstarts einer Windows-App verhindern — Benannte Mutexe und das Aktivieren des vorhandenen Fensters beim zweiten Start
- Win32-APIs sicher aus C# aufrufen — Ein praxistauglicher P/Invoke-Leitfaden (DllImport / LibraryImport / CsWin32)
- Sollte diese Batch-Datei zu PowerShell wechseln? — Bestandsaufnahme von cmd/bat-Assets und die Migrationsentscheidung
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“.
- Windows-App-Entwicklung
- Fehleruntersuchung und Ursachenanalyse
- Nutzung und Migration bestehender Assets
- Technische Beratung und Design-Review
- Kontakt
Referenzlinks
-
Microsoft Learn, CreateProcessW function (processthreadsapi.h). Dazu, dass
lpCommandLineeine 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, wennlpApplicationNameNULList, wobei ein Pfad mit Leerzeichen beginnend beic:\program.exegedeutet wird, zur Gefahr, dass eine andere ausführbare Datei läuft, wennProgram.exedort liegt, und zur Notwendigkeit,NULLzu vermeiden oder den Pfad in Anführungszeichen zu setzen, dazu, dassargv[0]bei Angabe beider nicht mit dem Modulnamen übereinstimmen kann, dazu, dass der Modulname-Teil beiNULLaufMAX_PATHbegrenzt 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 -
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
CommandLineToArgvWin argv-Form gewandelt werden kann, und dass er nicht mit der Zeichenfolge übereinstimmen muss, die der Elternprozess anCreateProcessübergeben hat, weil das Betriebssystem den vollständigen Pfad des Namens der ausführbaren Datei ergänzt. ↩ ↩2 -
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
lpCmdLinemit 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 einzigenLocalFreefreigegeben wird. ↩ ↩2 ↩3 ↩4 ↩5 -
Microsoft Learn,
mainfunction 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 undargv, zur Platzhaltererweiterung mitsetargv.obj, und dazu, dassargv[0]nicht der Name der ausführbaren Datei sein muss, wenn sowohllpApplicationNameals auchlpCommandLineangegeben sind, sodass er mitGetModuleFileNamegeholt werden sollte. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, ProcessStartInfo.ArgumentList Property. Dazu, dass die hinzugefügten Zeichenfolgen kein Escape im Voraus brauchen,
ArgumentListundArgumentsunabhängig sind und nicht gleichzeitig verwendet werden können,ArgumentListdie Argumente escaped und intern eine einzige Zeichenfolge baut, die beiProcess.Startan das Betriebssystem geht,ArgumentListdie 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 -
dotnet/runtime (GitHub), PasteArguments.cs und PasteArguments.Windows.cs. Der Aufbau-Code, der innerhalb von
ArgumentListverwendet 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ürargv[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 -
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
-
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
-
Microsoft Learn, WinMain function (winbase.h). Dazu, dass
lpCmdLinedie Kommandozeile ohne den Programmnamen ist, die gesamte Kommandozeile mitGetCommandLinegeholt wird, undwWinMainals Unicode-Einstiegspunkt existiert. ↩ -
dotnet/runtime (GitHub), apphost.c und dotnet.cpp. Dazu, dass die Einstiegspunkte des apphost und von
dotnet.exeunter Windowswmain(int argc, wchar_t* argv[])sind und das von der C-Laufzeit gebauteargvdirekt an die Startverarbeitung des Hosts übergeben. ↩ -
dotnet/runtime (GitHub), corhost.cpp. Dazu, dass
ExecuteAssemblydas Array vonEnvironment.GetCommandLineArgs()mitSetCommandLineArgs(pwzAssemblyPath, argc, argv)baut, das erste Element der vom Host übergebene Startname ist (oder der Assembly-Pfad, falls keiner) gefolgt vonargv, und nur diesesargvanMaingeht. ↩ ↩2 -
dotnet/runtime (GitHub), Environment.cs und Environment.Windows.cs. Dazu, dass
GetCommandLineArgsdas beim Start initialisierte Array (s_commandLineArgs) zurückgibt, eine gehostete Bibliothek ohne es als Fallback den Rückgabewert vonGetCommandLineWmit der eigenenSegmentCommandLineder Runtime zerlegt, diese Regeln der Dokumentation dermain-Funktion von MSVC folgen, undCommandLineToArgvWnicht verwendet wird, weil sein Verhalten geringfügig abweicht. ↩ ↩2 ↩3 -
Microsoft Learn, Main() and command-line arguments. Dazu, dass
argsinMainniemals null ist und, anders als in C/C++, der Programmname nicht am Anfang vonargssteht, sondern das erste Element vonGetCommandLineArgs()ist. ↩ -
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. ↩ -
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. ↩
-
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 -
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/coder/kerhalten 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. ↩ -
Microsoft Security Response Center, MS14-019 – Fixing a binary hijacking via .cmd or .bat file und Microsoft Security Bulletin MS14-019. Dazu, dass
CreateProcesscmd.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. ↩
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Time Travel Debugging — Langlaufende Fehler, die sich nicht reproduzieren, aufzeichnen und zurückspulen
Ein Fehler, der nur einmal im Monat auftritt, hinterlässt im Absturz-Dump nur das Ergebnis. Mit Time Travel Debugging (TTD) in WinDbg zei...
Ende der Wartung von Windows-Druckertreibern — Wie Geschäftsanwendungen Bericht- und Etikettendruck vorbereiten
Microsoft stellt v3/v4-Druckertreiber schrittweise ein. Was Windows protected print mode entfernt und wie Geschäftsanwendungen Bericht- u...
Was nach dem Tod des Elternprozesses übrig bleibt — Kindprozesse in einem Job Object halten
Warum SDK-Helfer eine beendete UI überleben und Kamera oder COM-Port behalten. Kindprozesslebensdauer mit Job Object, KillOnJobClose und ...
Named Pipes in der Praxis — Windows-Standard-IPC von der Auslegung bis zur Sicherheit
Ein Praxisleitfaden zu Named Pipes, der Standard-Prozesskommunikation unter Windows. Der Artikel ordnet anhand von Primärquellen die Wahl...
Scheinwecken — Warum Bedingungsvariablen „ohne Benachrichtigung“ aufwachen und wie man unter Windows richtig wartet
Das wait einer Bedingungsvariable kann auch ohne Benachrichtigung zurückkehren (Scheinwecken). Der Artikel erklärt anhand der Windows-Ums...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
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.