blog · git · desktop · images · contact
2017-10-17
Jemand hat mich kürzlich gefragt:
Ich habe da ein Skript in
~/projects/work/foo/bar
. Dieses Verzeichnis habe ich in meinen$PATH
mit aufgenommen. Mein Skript muss jetzt noch ein paar andere Dateien einlesen, die im selben Verzeichnis liegen. Wie finde ich dieses Verzeichnis?
Das ist eine ziemlich interessante Frage. Im ersten Moment denkt man, dass die Antwort doch leicht zu finden sein müsse – ist sie aber nicht.
Wie immer geht dieser Blogpost davon aus, dass ein UNIX-artiges Betriebssystem benutzt wird.
– Update, 2018-03-24: Teil 2
Die Antwort auf die Frage ist, dass es keine Antwort gibt.
Führt man ein Programm aus, dann gibt es keine Garantie, dass dieses Programm tatsächlich auf der Festplatte liegt. Das kann man leicht zeigen: Man lösche das Programm unmittelbar nach dem Start.
#include <stdio.h>
#include <unistd.h>
int
main()
{
sleep(5);
printf("I'm still alive.\n");
return 0;
}
Man öffne zwei Terminals, starte das Programm im ersten und lösche das Programm im zweiten. Es wird sauber zu Ende laufen.
Anderes Szenario: Was, wenn das Programm über zwei Hardlinks verfügbar ist? Wie ist dann der Pfad „des“ Programms?
Es gibt also keine endgültige Antwort.
$foo
geht das aber!“Es gibt Programme, die mit dieser Situation umzugehen versuchen. Den
meisten Leuten wird Java ein Begriff sein und sie werden von dort auch
kennen, dass das Problem lösbar ist. Man kann ein JRE nämlich irgendwo
auf der Platte ablegen, dieses Verzeichnis in den $PATH
mit aufnehmen
und dann funktioniert das. Es ist auch nicht so, dass Java einfach eine
gigantische statisch gelinkte Binary wäre – das muss schon seine JARs
und andere Dateien noch lesen. Und, nein, man muss auch nicht
$JAVA_HOME
setzen, damit es funktioniert.
Wie also geht das?
argv[0]
?Es gibt so eine lose Konvention, die besagt, dass argv[0]
den Namen
enthalten soll, unter dem ein Programm aufgerufen wurde. Das bringt uns
der Antwort ein bisschen näher und „löst“ (eher: „umgeht“) das Problem
mit den mehreren Hardlinks. Der Problemfall „Programm gelöscht“ bleibt
aber.
Viel wichtiger ist aber, dass es keine Garantie gibt, dass argv[0]
tatsächlich den Pfad des Programms enthält:
#include <unistd.h>
int
main()
{
execl("/bin/ps", "foobar", "-f", NULL);
return 1;
}
Die Ausgabe sieht dann so aus:
UID PID PPID C STIME TTY TIME CMD
void 3465 7122 0 08:24 pts/13 00:00:00 -/bin/bash
void 3544 3465 0 08:24 pts/13 00:00:00 vim -p bla.c
void 30336 1 0 08:42 pts/13 00:00:00 xclip
void 30337 1 0 08:42 pts/13 00:00:00 xclip -selection clipboard -f
void 30688 3544 0 08:42 pts/13 00:00:00 foobar -f
Man beachte hier auch -/bin/bash
: Dieser zusätzliche Bindestrich am
Anfang, der eindeutig nicht Teil des Programmpfads ist, wird von
login(1)
(und anderen) hinzugefügt, um der Shell mitzuteilen, dass sie
doch bitte eine Loginshell sein möge. Das ist also ein Anwendungsfall
eines modifizierten argv[0]
.
$PATH
Man kann jetzt argumentieren, dass argv[0]
in aller Regel schon den
richtigen Pfad enthalten wird. Dass dort etwas Seltsames drinsteht, ist
schon eher selten. Man könnte das also ignorieren. Naja, fast.
Wenn man ein Verzeichnis zu $PATH
hinzufügt, dann kann man
beispielsweise ls
statt /bin/ls
eingeben. Und genau das ist das
Problem: argv[0]
wird weiterhin den Namen enthalten, unter dem das
Programm aufgerufen wurde, und das ist jetzt eben ls
.
procfs
auf LinuxIn Linux gibt es ein sehr mächtiges Interface namens procfs
. Das ist
ein Pseudo-Dateisystem, das in der Regel unter /proc
gemountet ist. Es
enthält allerlei Informationen über laufende Prozess. Jeder Prozess wird
über seine PID identifiziert und ist unter /proc/$pid
verfügbar. Es
gibt auch noch einen besonderen Symlink: /proc/self
. Der zeigt immer
auf das Verzeichnis desjenigen Prozesses, der den Link ausliest.
Zu guter Letzt gibt es in /proc/$pid
einen weiteren Symlink namens
exe
. Und der zeigt endlich auf den Pfad, über den der Prozess
gestartet wurde.
Man schaue:
#include <stdio.h>
#include <unistd.h>
int
main()
{
char buf[4096] = "";
ssize_t len;
if ((len = readlink("/proc/self/exe", buf, (sizeof buf) - 1)) != -1)
{
buf[len] = 0;
printf("[%s]\n", buf);
return 0;
}
else
return 1;
}
So findet auch Java unter Linux seine Dateien. Das kann man ganz am
Anfang sehen, wenn man es mit strace
aufruft:
07:52:14.046676 execve("/opt/jdk-9/bin/java", ["java", "Test"], 0x7ffda7b0fa28 /* 10 vars */) = 0
07:52:14.047496 brk(NULL) = 0x1114000
07:52:14.047592 readlink("/proc/self/exe", "/opt/jdk-9/bin/java", 4096) = 19
$PATH
abklappernprocfs
ist nicht auf jedem Betriebssystem verfügbar. Man kann aber das
tun, was die Shell (oder der Kernel) gemacht hat, um das Programm zu
finden. Warum sollte das auch nicht gehen: Irgendwas hat /bin/ls
gefunden, nachdem nur ls
eingegeben wurde, also warum sollte unser
Programm das nicht auch können?
Java scheint das auf OpenBSD so zu machen:
93056 java CALL stat(0x7f7ffffc73d0,0x7f7ffffc7350)
93056 java NAMI "/sbin/java"
93056 java RET stat -1 errno 2 No such file or directory
93056 java CALL stat(0x7f7ffffc73d0,0x7f7ffffc7350)
93056 java NAMI "/usr/sbin/java"
93056 java RET stat -1 errno 2 No such file or directory
93056 java CALL stat(0x7f7ffffc73d0,0x7f7ffffc7350)
93056 java NAMI "/bin/java"
93056 java RET stat -1 errno 2 No such file or directory
93056 java CALL stat(0x7f7ffffc73d0,0x7f7ffffc7350)
93056 java NAMI "/usr/bin/java"
93056 java RET stat -1 errno 2 No such file or directory
93056 java CALL stat(0x7f7ffffc73d0,0x7f7ffffc7350)
93056 java NAMI "/usr/X11R6/bin/java"
93056 java RET stat -1 errno 2 No such file or directory
93056 java CALL stat(0x7f7ffffc73d0,0x7f7ffffc7350)
93056 java NAMI "/usr/local/sbin/java"
93056 java RET stat -1 errno 2 No such file or directory
93056 java CALL stat(0x7f7ffffc73d0,0x7f7ffffc7350)
93056 java NAMI "/usr/local/bin/java"
93056 java RET stat -1 errno 2 No such file or directory
93056 java CALL stat(0x7f7ffffc73d0,0x7f7ffffc7350)
93056 java NAMI "/usr/local/foobar/bin/java"
93056 java STRU struct stat { dev=1027, ino=155914, mode=-rwxr-xr-x , nlink=1, uid=0<"root">, gid=7<"bin">, rdev=623880, atime=1508220651<"Oct 17 08:10:51 2017">.082047559, mtime=1506962794<"Oct 2 18:46:34 2017">, ctime=1508219904<"Oct 17 07:58:24 2017">.120012249, size=63657, blocks=128, blksize=16384, flags=0x0, gen=0x2049be9e }
93056 java RET stat 0
(Hier und auch oben bei Javas Benutzung von /proc/self/exe
ist ein
bisschen Vorsicht geboten. Ich habe den Quellcode von Java nicht
gelesen, weil der so unglaublich kompliziert ist, beide Ausgaben sind
aber starke Indizien dafür, dass Java wirklich so arbeitet. Es geht mir
aber auch viel mehr darum, das Durchlaufen von $PATH
als möglichen
Workaround darzustellen.)
Von den Race-Conditions hier fange ich gar nicht erst an.
Natürlich funktioniert das nur, solange das Programm noch Zugriff auf
den originalen Wert von $PATH
hat. Man kann das auch mutwillig
kaputtmachen.
executor.c
:
#include <unistd.h>
int
main()
{
char *env[] = { "PATH=broken", NULL };
execle("my-sub-program", "foo", NULL, env);
return 1;
}
my-sub-program.c
:
#include <stdio.h>
#include <stdlib.h>
int
main()
{
printf("PATH=%s\n", getenv("PATH"));
return 0;
}
Ausführung:
$ export PATH=$PATH:.
$ executor
PATH=broken
Die Tatsache, dass in der Ausgabe PATH=broken
erscheint, heißt, dass
das Unterprogramm aufgerufen wurde – es kann aber den originalen $PATH
nicht mehr sehen.
Das machen dann letztendlich viele Programm. Häufig wird Software im Quellcode weitergegeben und es gibt dann einen Build-Prozess. Während des Builds kann man leicht einen fest vorgegebenen String in die fertige Binary „injizieren“. Ruft man das Programm dann auf, muss gar nichts mehr „aufgelöst“ werden, sondern es weiß schon, wo es im Dateisystem nachschauen muss.
ratterplatter
macht das so:
(Update 2023-08-13: Das ist nicht mehr so, das Programm erwartet jetzt
zwingend den Pfad in seinem argv
. In diesem Fall ist das eine
akzeptable Lösung.)
Oder irssi:
Selbstverständlich gibt es wieder Szenarien, in denen auch dieser Ansatz nicht gangbar ist. Zum Beispiel dann, wenn es nur um ein kleines Skript gibt, das gar keinen Build-Prozess hat – so wie im ursprünglichen Fall desjenigen Menschen, der die Frage stellte.
Sieht nicht so gut aus. Was auch immer man tut, es ist ein bisschen zerbrechlich.
Es sei denn: Vielleicht übersehe ich hier etwas. Gibt es einen besseren Weg? Ich bin ganz Ohr. :-)