Parameter per Kommandozeile
« | 02 Apr 2023 | »Wenn Programme andere Programme starten sollen, stellt sich die Frage:
Wie?
Die C Standard Bibliothek
beantwortet das leider sehr unzufriedenstellend, doch trotzdem basieren
viele Lösungen auf dem alten
system("myprog arg1 arg2")
Schema.
Und da kann man leider auch viel falsch machen.
Windows vs. Unix vs. C
Das aus CP/M und
DOS geborene
Windows verfolgt einen vom
C-Standard abweichenden Weg mit “Befehlszeilenargumenten” umzugehen.
Denn während Unix Programme für
erfahrene Profis gemacht wurden, die alles über eine Vielzahl von Parameter
konfigurieren wollten, waren DOS und Windows Programme für Anfänger
in Büros gedacht.
Ein DOS/Windows Programm braucht keine Argumente, sondern startet ein UI,
in welchem der Benutzer seine Daten über eine “schöne” Maske findet.
Die “Profis” unter diesen Anfängern lernten dann, dass man den Start und
das Laden eines Programmes vereinen konnte, in dem man nach der Programmdatei
noch den Pfad zur Datendatei angab, wie:
edit c:\my_diary.txt
Hier unterscheiden sich DOS/Windows und Unix gewaltig, denn Windows leitet
diesen String einfach an das Programm weiter, während das Unix Programm von
seinem Elternprozess ein Array von Strings für jeden Parameter erhält.
Man könnte auch sagen:
Windows ist auf genau einen Parameter optimiert bzw. eingeschränkt.
Bei DOS/Windows Programmen müssen also die Parameter im Prozess selbst auseinanderdividiert werden, während auf Unix-Systemen bereits der Elternprozess festlegt, wie viele Parameter übergeben werden.
Problematisch wird das dann bei Leerzeichen, die es in DOS ursprünglich in
Dateinamen nicht gab, womit man den einen Befehlszeilen-String einfach
durch seine Leerzeichen aufspalten konnte.
Im Nachhinein wurde dann mit
CommandLineToArgv()
eine API eingeführt, damit dieses Parsen auch unter Windows einheitlich
wurde und C-Runtime darauf aufsetzen konnte.
Hier werden nun nämlich Anführungszeichen (double quotes) benutzt, um einen
zusammengehörenden Parameter zu markieren.
z.B.: my_app first_arg "seconds arg" "and a third arg"
Einfaches und doppeltes Anführungszeichen
Die C Funktion system(command_string)
ruft die OS-Shell mit dem übergebenen Kommando
auf, was unter Windows zu einen CreateProcess()
mit cmd.exe /c
+ command_string
wird.
Die Linux C-Runtime nutzt ein fork()
/ exec()
Paar mit dem Array
[ "/bin/sh", "-c", command_string ]
.
Das bedeutet also, wenn man Leerzeichen in Argumenten eines Zielprogramms
braucht (z.B.: für das Auflisten des Ordners my folder
), dann muss der
Aufruf den Leerzeichen-Parameter auch unter Anführungszeichen setzen.
1system("dir \"my folder\"");
wird am Ende zu:
- Windows:
CreateProcess("cmd.exe /c dir \"my folder\"")
- Linux:
exec([ "/bin/sh", "-c" "dir \"my folder\"" ] )
Man könnte glauben, doppelte Anführungszeichen lösen somit alle Probleme und sind plattformunabhängig.
Leider falsch!
Jede Shell hat die Eigenschaft, dass sie Umgebungsvariablen expandieren und Escapezeichen umwanden kann. Das trifft sowohl unter Windows wie auch unter Linux zu (wenn auch ganz unterschiedlich).
Linux führte daher die einfachen Anführungszeichen ein (Hochkomma, single quotes), mit denen die Variablen und Zeichenauswertung verhindert wird.
dir "my folder"
und dir 'my folder'
liefern beide unter Linux das
gleiche Ergebnis, doch unter Windows wird der zweite Aufruf fehlschlagen,
da die Hochkommata nicht interpretiert werden können.
echo $HOME
und echo "$HOME"
liefern unter Linux beide Male das
tatsächliche Verzeichnis, das in der HOME
Variable gespeichert wird.
Doch der Aufruf echo '$HOME'
führt zur Ausgabe $HOME
, da der
Variablenverweis nicht durch den Inhalt ersetzt wird.
An dieser Stelle unterscheidet sich also zusätzlich das Verhalten der Shells
von Windows und Linux deutlich, denn um zu verhindern, dass in manche
Argumente versehentlich Umgebungsvariablen eingeschleust werden, wird das
Hochkomma '
in vielen Anwendungen bevorzugt.
Beispielprogramm system_cmd
Ich habe im BLOG Classroom das Beispielprogramm system_cmd
hinzugefügt, mit
dem system()
Aufrufe per Kommandozeile ausgeführt werden können.
Hier kann man mitverfolgen, welche Kommandos wie im Prozess ankommen,
und was der Prozess selbst wieder an die Shell weitergibt, und was diese
am Ende daraus macht.
Hintergrund dieser Analyse war ein Bug in einer Software, wo es um die Weitergabe von Passwort-Token ging. Hier ist die Abschaltung der Variablen- und Sonderzeichenauflösung sehr sinnvoll, denn ein einzelnes Dollarzeichen kann schnell zu falschen Daten führen.
Ich sehe im Besonderen die Notwendigkeit, dass die Parameterverwaltung für Windows und Linux unterschiedlich durchgeführt werden muss und Sonderzeichen manuell so umgestaltet werden müssen, dass die Shell daraus nichts Ungewolltes macht.
Der Aufruf von Programmen, die sowohl unter Windows also auch unter Linux
verfügbar sind (z.B.: curl
), liefern beim Start durch system()
nicht
immer die gleichen Ergebnisse.
Fazit
Oh Mann! Lieber nicht
system()
benutzen!
Es gab schon früher einige Meldungen, dass system()
zum Hacken missbraucht
werden konnte. Hat man die Möglichkeit Teile des Strings zu manipulieren,
kann man Schlimmste Sachen damit anstellen.
Dass aber die Art der Anführungszeichen zu unterschiedlichem Handling in der Shell führen können, hatte ich so deutlich bisher aber auch nicht auf dem Zeiger.
Generell nutze ich selbst wenn möglich direkt die APIs des OS,
also exec()
oder CreateProcess()
und nie system()
.
Damit bleiben mir “Interpretationsspielräume” der Shell erspart.
Leider machen es sich viele Projekte leicht und bauen “mal schnell”
einen String zusammen. Der Grund liegt in der Einfachheit des Aufrufs …
sicher ist das aber nicht.
Das Gleiche gilt natürlich auch für popen().
Nun geht für mich die Analyse weiter, in welchen anderen Projekten ich noch
solche system()
Zeitbomben finde.