L'utilisation de /proc/self/exe
n'est pas portable et peu fiable. Sur mon système Ubuntu 12.04, vous devez être root pour lire / suivre le lien symbolique. Cela fera whereami()
échouer l'exemple Boost et probablement les solutions publiées.
Cet article est très long mais traite des problèmes réels et présente du code qui fonctionne réellement avec la validation par rapport à une suite de tests.
La meilleure façon de trouver votre programme est de retracer les mêmes étapes que le système utilise. Ceci est fait en utilisant argv[0]
résolu contre la racine du système de fichiers, pwd, l'environnement de chemin et en tenant compte des liens symboliques et de la canonisation des noms de chemin. C'est de mémoire, mais je l'ai fait dans le passé avec succès et l'ai testé dans une variété de situations différentes. Il n'est pas garanti que cela fonctionne, mais si ce n'est pas le cas, vous avez probablement des problèmes beaucoup plus importants et il est globalement plus fiable que toutes les autres méthodes discutées. Il existe des situations sur un système compatible Unix dans lesquelles une bonne gestion desargv[0]
ne vous amènera pas à votre programme, mais vous exécutez alors dans un environnement cassé de manière certifiée. Il est également assez portable pour tous les systèmes dérivés d'Unix depuis environ 1970 et même certains systèmes non dérivés d'Unix car il repose essentiellement sur la fonctionnalité standard de libc () et la fonctionnalité de ligne de commande standard. Il devrait fonctionner sur Linux (toutes versions), Android, Chrome OS, Minix, Bell Labs Unix, FreeBSD, NetBSD, OpenBSD, BSD xx, SunOS, Solaris, SYSV, HPUX, Concentrix, SCO, Darwin, AIX, OS X, Nextstep, etc. Et avec une petite modification probablement VMS, VM / CMS, DOS / Windows, ReactOS, OS / 2, etc. Si un programme a été lancé directement depuis un environnement GUI, il aurait dû être défini argv[0]
sur un chemin absolu.
Comprenez que presque tous les shell sur tous les systèmes d'exploitation compatibles Unix qui ont jamais été publiés trouvent fondamentalement les programmes de la même manière et configure l'environnement d'exploitation presque de la même manière (avec quelques extras optionnels). Et tout autre programme qui lance un programme est censé créer le même environnement (argv, chaînes d'environnement, etc.) pour ce programme comme s'il était exécuté à partir d'un shell, avec quelques extras optionnels. Un programme ou un utilisateur peut configurer un environnement qui s'écarte de cette convention pour d'autres programmes subordonnés qu'il lance mais si c'est le cas, il s'agit d'un bogue et le programme n'a aucune attente raisonnable que le programme subordonné ou ses subordonnés fonctionnent correctement.
Les valeurs possibles de argv[0]
incluent:
/path/to/executable
- chemin absolu
../bin/executable
- par rapport à pwd
bin/executable
- par rapport à pwd
./foo
- par rapport à pwd
executable
- nom de base, trouver dans le chemin
bin//executable
- par rapport à pwd, non canonique
src/../bin/executable
- par rapport à pwd, non canonique, retour arrière
bin/./echoargc
- par rapport à pwd, non canonique
Valeurs que vous ne devriez pas voir:
~/bin/executable
- réécrit avant l'exécution de votre programme.
~user/bin/executable
- réécrit avant l'exécution de votre programme
alias
- réécrit avant l'exécution de votre programme
$shellvariable
- réécrit avant l'exécution de votre programme
*foo*
- wildcard, réécrit avant l'exécution de votre programme, pas très utile
?foo?
- wildcard, réécrit avant l'exécution de votre programme, pas très utile
En outre, ceux-ci peuvent contenir des noms de chemin non canoniques et plusieurs couches de liens symboliques. Dans certains cas, il peut y avoir plusieurs liens physiques vers le même programme. Par exemple, /bin/ls
, /bin/ps
, /bin/chmod
, /bin/rm
, etc. , peuvent être des liens durs vers /bin/busybox
.
Pour vous retrouver, suivez les étapes ci-dessous:
Enregistrez pwd, PATH et argv [0] à l'entrée de votre programme (ou à l'initialisation de votre bibliothèque) car ils peuvent changer plus tard.
Facultatif: en particulier pour les systèmes non Unix, séparez mais ne supprimez pas la partie du préfixe hôte / utilisateur / lecteur du chemin, si elle est présente; la partie qui précède souvent un deux-points ou suit un "//" initial.
Si argv[0]
est un chemin absolu, utilisez-le comme point de départ. Un chemin absolu commence probablement par "/" mais sur certains systèmes non Unix, il peut commencer par "\" ou une lettre de lecteur ou un préfixe de nom suivi de deux points.
Sinon, si argv[0]
est un chemin relatif (contient "/" ou "\" mais ne commence pas par lui, comme "../../bin/foo", alors combinez pwd + "/" + argv [0] (utilisez répertoire de travail actuel à partir du démarrage du programme, pas en cours).
Sinon, si argv [0] est un nom de base simple (sans barres obliques), combinez-le avec chaque entrée de la variable d'environnement PATH à tour de rôle et essayez-les et utilisez la première qui réussit.
Facultatif: Sinon, essayez le très spécifique à la plate-forme /proc/self/exe
, /proc/curproc/file
(BSD), et (char *)getauxval(AT_EXECFN)
, et le dlgetname(...)
cas échéant. Vous pouvez même essayer ces argv[0]
méthodes antérieures , si elles sont disponibles et si vous ne rencontrez pas de problèmes d'autorisation. Dans le cas quelque peu improbable (lorsque vous considérez toutes les versions de tous les systèmes) où elles sont présentes et n'échouent pas, elles peuvent faire plus autorité.
Facultatif: recherchez un nom de chemin transmis à l'aide d'un paramètre de ligne de commande.
Facultatif: recherchez un chemin d'accès dans l'environnement explicitement transmis par votre script wrapper, le cas échéant.
Facultatif: en dernier recours, essayez la variable d'environnement "_". Il peut pointer vers un programme complètement différent, tel que le shell des utilisateurs.
Résolvez les liens symboliques, il peut y avoir plusieurs couches. Il y a la possibilité de boucles infinies, mais si elles existent, votre programme ne sera probablement pas appelé.
Canonicalisez le nom de fichier en résolvant les sous-chaînes telles que "/foo/../bar/" en "/ bar /". Notez que cela peut potentiellement changer la signification si vous traversez un point de montage réseau, donc la canonisation n'est pas toujours une bonne chose. Sur un serveur réseau, ".." dans le lien symbolique peut être utilisé pour parcourir un chemin vers un autre fichier dans le contexte du serveur plutôt que sur le client. Dans ce cas, vous voulez probablement le contexte client, donc la canonisation est correcte. Convertissez également des motifs tels que "/./" en "/" et "//" en "/". Dans le shell, readlink --canonicalize
résoudra plusieurs liens symboliques et canonisera le nom. Chase peut faire la même chose mais n'est pas installé. realpath()
ou canonicalize_file_name()
, si présent, peut aider.
S'il realpath()
n'existe pas au moment de la compilation, vous pouvez emprunter une copie d'une distribution de bibliothèque sous licence permissive et la compiler vous-même plutôt que de réinventer la roue. Corrigez le débordement potentiel de la mémoire tampon (passez à la taille du tampon de sortie, pensez à strncpy () vs strcpy ()) si vous utilisez un tampon inférieur à PATH_MAX. Il peut être plus facile d'utiliser simplement une copie privée renommée plutôt que de tester si elle existe. Copie de licence permissive depuis android / darwin / bsd:
https://android.googlesource.com/platform/bionic/+/f077784/libc/upstream-freebsd/lib/libc/stdlib/realpath.c
Sachez que plusieurs tentatives peuvent être réussies ou partiellement réussies et qu'elles peuvent ne pas toutes pointer vers le même exécutable, pensez donc à vérifier votre exécutable; cependant, vous n'avez peut-être pas l'autorisation de lecture - si vous ne pouvez pas le lire, ne le considérez pas comme un échec. Ou vérifiez quelque chose à proximité de votre exécutable tel que le répertoire "../lib/" que vous essayez de trouver. Vous pouvez avoir plusieurs versions, des versions packagées et compilées localement, des versions locales et réseau, et des versions portables locales et USB, etc. et il y a une petite possibilité que vous obteniez deux résultats incompatibles à partir de différentes méthodes de localisation. Et "_" peut simplement pointer vers le mauvais programme.
Un programme utilisant execve
peut délibérément être défini argv[0]
pour être incompatible avec le chemin d'accès réel utilisé pour charger le programme et corrompre PATH, "_", pwd, etc. bien qu'il n'y ait généralement pas beaucoup de raisons de le faire; mais cela pourrait avoir des implications sur la sécurité si vous avez un code vulnérable qui ignore le fait que votre environnement d'exécution peut être modifié de diverses manières, y compris, mais sans s'y limiter, celle-ci (chroot, système de fichiers fusionné, liens physiques, etc.). pour que les commandes shell définissent PATH mais ne parviennent pas à l'exporter.
Vous n'avez pas nécessairement besoin de coder pour les systèmes non-Unix, mais ce serait une bonne idée d'être conscient de certaines des particularités afin que vous puissiez écrire le code de telle manière qu'il ne soit pas aussi difficile pour quelqu'un de porter plus tard . Sachez que certains systèmes (DEC VMS, DOS, URL, etc.) peuvent avoir des noms de lecteur ou d'autres préfixes qui se terminent par un signe deux-points tels que "C: \", "sys $ lecteur: [foo] bar", et "fichier : /// foo / bar / baz ". Les anciens systèmes DEC VMS utilisent "[" et "]" pour entourer la partie répertoire du chemin, bien que cela puisse avoir changé si votre programme est compilé dans un environnement POSIX. Certains systèmes, tels que VMS, peuvent avoir une version de fichier (séparée par un point-virgule à la fin). Certains systèmes utilisent deux barres obliques consécutives comme dans "// lecteur / chemin / vers / fichier" ou "utilisateur @ hôte: / chemin / vers / fichier" (commande scp) ou "fichier: (délimité par des espaces) et "PATH" délimité par des deux-points mais votre programme devrait recevoir PATH afin que vous n'ayez pas à vous soucier du chemin. DOS et certains autres systèmes peuvent avoir des chemins relatifs commençant par un préfixe de lecteur. C: foo.exe fait référence à foo.exe dans le répertoire actuel sur le lecteur C, vous devez donc rechercher le répertoire actuel sur C: et l'utiliser pour pwd. (délimité par des espaces) et "PATH" délimité par des deux-points mais votre programme devrait recevoir PATH afin que vous n'ayez pas à vous soucier du chemin. DOS et certains autres systèmes peuvent avoir des chemins relatifs commençant par un préfixe de lecteur. C: foo.exe fait référence à foo.exe dans le répertoire actuel sur le lecteur C, vous devez donc rechercher le répertoire actuel sur C: et l'utiliser pour pwd.
Un exemple de liens symboliques et de wrappers sur mon système:
/usr/bin/google-chrome is symlink to
/etc/alternatives/google-chrome which is symlink to
/usr/bin/google-chrome-stable which is symlink to
/opt/google/chrome/google-chrome which is a bash script which runs
/opt/google/chome/chrome
Notez que la facture d' utilisateur a publié un lien ci-dessus vers un programme chez HP qui gère les trois cas de base de argv[0]
. Il a cependant besoin de quelques changements:
- Il faudra réécrire tous les
strcat()
et strcpy()
utiliser strncat()
et strncpy()
. Même si les variables sont déclarées de longueur PATHMAX, une valeur d'entrée de longueur PATHMAX-1 plus la longueur des chaînes concaténées est> PATHMAX et une valeur d'entrée de longueur PATHMAX ne serait pas terminée.
- Il doit être réécrit comme une fonction de bibliothèque, plutôt que simplement pour imprimer les résultats.
- Il ne parvient pas à canoniser les noms (utilisez le code realpath auquel j'ai lié ci-dessus)
- Il ne parvient pas à résoudre les liens symboliques (utilisez le code realpath)
Donc, si vous combinez à la fois le code HP et le code realpath et que vous corrigez les deux pour résister aux débordements de tampon, vous devriez avoir quelque chose qui puisse interpréter correctement argv[0]
.
Ce qui suit illustre les valeurs réelles de argv[0]
différentes manières d'appeler le même programme sur Ubuntu 12.04. Et oui, le programme a été accidentellement nommé echoargc au lieu de echoargv. Cela a été fait en utilisant un script pour une copie propre, mais le faire manuellement dans le shell donne les mêmes résultats (sauf que les alias ne fonctionnent pas dans le script à moins que vous ne les activiez explicitement).
cat ~/src/echoargc.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
main(int argc, char **argv)
{
printf(" argv[0]=\"%s\"\n", argv[0]);
sleep(1); /* in case run from desktop */
}
tcc -o ~/bin/echoargc ~/src/echoargc.c
cd ~
/home/whitis/bin/echoargc
argv[0]="/home/whitis/bin/echoargc"
echoargc
argv[0]="echoargc"
bin/echoargc
argv[0]="bin/echoargc"
bin//echoargc
argv[0]="bin//echoargc"
bin/./echoargc
argv[0]="bin/./echoargc"
src/../bin/echoargc
argv[0]="src/../bin/echoargc"
cd ~/bin
*echo*
argv[0]="echoargc"
e?hoargc
argv[0]="echoargc"
./echoargc
argv[0]="./echoargc"
cd ~/src
../bin/echoargc
argv[0]="../bin/echoargc"
cd ~/junk
~/bin/echoargc
argv[0]="/home/whitis/bin/echoargc"
~whitis/bin/echoargc
argv[0]="/home/whitis/bin/echoargc"
alias echoit=~/bin/echoargc
echoit
argv[0]="/home/whitis/bin/echoargc"
echoarg=~/bin/echoargc
$echoarg
argv[0]="/home/whitis/bin/echoargc"
ln -s ~/bin/echoargc junk1
./junk1
argv[0]="./junk1"
ln -s /home/whitis/bin/echoargc junk2
./junk2
argv[0]="./junk2"
ln -s junk1 junk3
./junk3
argv[0]="./junk3"
gnome-desktop-item-edit --create-new ~/Desktop
# interactive, create desktop link, then click on it
argv[0]="/home/whitis/bin/echoargc"
# interactive, right click on gnome application menu, pick edit menus
# add menu item for echoargc, then run it from gnome menu
argv[0]="/home/whitis/bin/echoargc"
cat ./testargcscript 2>&1 | sed -e 's/^/ /g'
#!/bin/bash
# echoargc is in ~/bin/echoargc
# bin is in path
shopt -s expand_aliases
set -v
cat ~/src/echoargc.c
tcc -o ~/bin/echoargc ~/src/echoargc.c
cd ~
/home/whitis/bin/echoargc
echoargc
bin/echoargc
bin//echoargc
bin/./echoargc
src/../bin/echoargc
cd ~/bin
*echo*
e?hoargc
./echoargc
cd ~/src
../bin/echoargc
cd ~/junk
~/bin/echoargc
~whitis/bin/echoargc
alias echoit=~/bin/echoargc
echoit
echoarg=~/bin/echoargc
$echoarg
ln -s ~/bin/echoargc junk1
./junk1
ln -s /home/whitis/bin/echoargc junk2
./junk2
ln -s junk1 junk3
./junk3
Ces exemples illustrent que les techniques décrites dans cet article devraient fonctionner dans un large éventail de circonstances et pourquoi certaines étapes sont nécessaires.
EDIT: Maintenant, le programme qui imprime argv [0] a été mis à jour pour se trouver réellement.
// Copyright 2015 by Mark Whitis. License=MIT style
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <assert.h>
#include <string.h>
#include <errno.h>
// "look deep into yourself, Clarice" -- Hanibal Lector
char findyourself_save_pwd[PATH_MAX];
char findyourself_save_argv0[PATH_MAX];
char findyourself_save_path[PATH_MAX];
char findyourself_path_separator='/';
char findyourself_path_separator_as_string[2]="/";
char findyourself_path_list_separator[8]=":"; // could be ":; "
char findyourself_debug=0;
int findyourself_initialized=0;
void findyourself_init(char *argv0)
{
getcwd(findyourself_save_pwd, sizeof(findyourself_save_pwd));
strncpy(findyourself_save_argv0, argv0, sizeof(findyourself_save_argv0));
findyourself_save_argv0[sizeof(findyourself_save_argv0)-1]=0;
strncpy(findyourself_save_path, getenv("PATH"), sizeof(findyourself_save_path));
findyourself_save_path[sizeof(findyourself_save_path)-1]=0;
findyourself_initialized=1;
}
int find_yourself(char *result, size_t size_of_result)
{
char newpath[PATH_MAX+256];
char newpath2[PATH_MAX+256];
assert(findyourself_initialized);
result[0]=0;
if(findyourself_save_argv0[0]==findyourself_path_separator) {
if(findyourself_debug) printf(" absolute path\n");
realpath(findyourself_save_argv0, newpath);
if(findyourself_debug) printf(" newpath=\"%s\"\n", newpath);
if(!access(newpath, F_OK)) {
strncpy(result, newpath, size_of_result);
result[size_of_result-1]=0;
return(0);
} else {
perror("access failed 1");
}
} else if( strchr(findyourself_save_argv0, findyourself_path_separator )) {
if(findyourself_debug) printf(" relative path to pwd\n");
strncpy(newpath2, findyourself_save_pwd, sizeof(newpath2));
newpath2[sizeof(newpath2)-1]=0;
strncat(newpath2, findyourself_path_separator_as_string, sizeof(newpath2));
newpath2[sizeof(newpath2)-1]=0;
strncat(newpath2, findyourself_save_argv0, sizeof(newpath2));
newpath2[sizeof(newpath2)-1]=0;
realpath(newpath2, newpath);
if(findyourself_debug) printf(" newpath=\"%s\"\n", newpath);
if(!access(newpath, F_OK)) {
strncpy(result, newpath, size_of_result);
result[size_of_result-1]=0;
return(0);
} else {
perror("access failed 2");
}
} else {
if(findyourself_debug) printf(" searching $PATH\n");
char *saveptr;
char *pathitem;
for(pathitem=strtok_r(findyourself_save_path, findyourself_path_list_separator, &saveptr); pathitem; pathitem=strtok_r(NULL, findyourself_path_list_separator, &saveptr) ) {
if(findyourself_debug>=2) printf("pathitem=\"%s\"\n", pathitem);
strncpy(newpath2, pathitem, sizeof(newpath2));
newpath2[sizeof(newpath2)-1]=0;
strncat(newpath2, findyourself_path_separator_as_string, sizeof(newpath2));
newpath2[sizeof(newpath2)-1]=0;
strncat(newpath2, findyourself_save_argv0, sizeof(newpath2));
newpath2[sizeof(newpath2)-1]=0;
realpath(newpath2, newpath);
if(findyourself_debug) printf(" newpath=\"%s\"\n", newpath);
if(!access(newpath, F_OK)) {
strncpy(result, newpath, size_of_result);
result[size_of_result-1]=0;
return(0);
}
} // end for
perror("access failed 3");
} // end else
// if we get here, we have tried all three methods on argv[0] and still haven't succeeded. Include fallback methods here.
return(1);
}
main(int argc, char **argv)
{
findyourself_init(argv[0]);
char newpath[PATH_MAX];
printf(" argv[0]=\"%s\"\n", argv[0]);
realpath(argv[0], newpath);
if(strcmp(argv[0],newpath)) { printf(" realpath=\"%s\"\n", newpath); }
find_yourself(newpath, sizeof(newpath));
if(1 || strcmp(argv[0],newpath)) { printf(" findyourself=\"%s\"\n", newpath); }
sleep(1); /* in case run from desktop */
}
Et voici le résultat qui démontre que dans chacun des tests précédents, il s'est réellement retrouvé.
tcc -o ~/bin/echoargc ~/src/echoargc.c
cd ~
/home/whitis/bin/echoargc
argv[0]="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
echoargc
argv[0]="echoargc"
realpath="/home/whitis/echoargc"
findyourself="/home/whitis/bin/echoargc"
bin/echoargc
argv[0]="bin/echoargc"
realpath="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
bin//echoargc
argv[0]="bin//echoargc"
realpath="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
bin/./echoargc
argv[0]="bin/./echoargc"
realpath="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
src/../bin/echoargc
argv[0]="src/../bin/echoargc"
realpath="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
cd ~/bin
*echo*
argv[0]="echoargc"
realpath="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
e?hoargc
argv[0]="echoargc"
realpath="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
./echoargc
argv[0]="./echoargc"
realpath="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
cd ~/src
../bin/echoargc
argv[0]="../bin/echoargc"
realpath="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
cd ~/junk
~/bin/echoargc
argv[0]="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
~whitis/bin/echoargc
argv[0]="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
alias echoit=~/bin/echoargc
echoit
argv[0]="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
echoarg=~/bin/echoargc
$echoarg
argv[0]="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
rm junk1 junk2 junk3
ln -s ~/bin/echoargc junk1
./junk1
argv[0]="./junk1"
realpath="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
ln -s /home/whitis/bin/echoargc junk2
./junk2
argv[0]="./junk2"
realpath="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
ln -s junk1 junk3
./junk3
argv[0]="./junk3"
realpath="/home/whitis/bin/echoargc"
findyourself="/home/whitis/bin/echoargc"
Les deux lancements GUI décrits ci-dessus trouvent également correctement le programme.
Il y a un écueil potentiel. La access()
fonction supprime les autorisations si le programme est défini avant le test. S'il y a une situation où le programme peut être trouvé en tant qu'utilisateur élevé mais pas en tant qu'utilisateur régulier, alors il peut y avoir une situation où ces tests échoueraient, bien qu'il soit peu probable que le programme puisse réellement être exécuté dans ces circonstances. On pourrait utiliser euidaccess () à la place. Il est possible, cependant, qu'il puisse trouver un programme inaccessible plus tôt sur le chemin que l'utilisateur réel ne le pourrait.