Encontrar la ruta del ejecutable actual sin /proc/self/exe

Resuelto Uros Dimitrijevic asked hace 15 años • 0 respuestas

Me parece que Linux lo tiene fácil con /proc/self/exe. Pero me gustaría saber si existe una manera conveniente de encontrar el directorio de la aplicación actual en C/C++ con interfaces multiplataforma. He visto algunos proyectos jugando con argv[0], pero no parece del todo confiable.

Si alguna vez tuvieras que soportar, digamos, Mac OS X, que no tiene /proc/, ¿qué habrías hecho? ¿Usar #ifdefs para aislar el código específico de la plataforma (NSBundle, por ejemplo)? ¿O intentar deducir la ruta del ejecutable a partir de argv[0], $PATH y demás, arriesgándose a encontrar errores en casos extremos?

Uros Dimitrijevic avatar Jun 21 '09 13:06 Uros Dimitrijevic
Aceptado

Algunas interfaces específicas del sistema operativo:

  • Mac OS X: _NSGetExecutablePath()( hombre 3 dyld )
  • Linux:readlink /proc/self/exe
  • Solaris:getexecname()
  • FreeBSD:sysctl CTL_KERN KERN_PROC KERN_PROC_PATHNAME -1
  • FreeBSD si tiene procfs: readlink /proc/curproc/file(FreeBSD no tiene procfs por defecto)
  • NetBSD:readlink /proc/curproc/exe
  • Libélula BSD:readlink /proc/curproc/file
  • Windows: GetModuleFileName()con hModule=NULL

También hay bibliotecas de terceros que se pueden usar para obtener esta información, como Whereami como se menciona en la respuesta de Prideout , o si está usando Qt, QCoreApplication::applicationFilePath() como se menciona en los comentarios.

El método portátil (pero menos confiable) es utilizar argv[0]. Aunque el programa que lo llama puede establecerlo en cualquier cosa, por convención se establece en un nombre de ruta del ejecutable o en un nombre que se encontró usando $PATH.

Algunos shells, incluidos bash y ksh, configuran la variable de entorno " _" en la ruta completa del ejecutable antes de ejecutarlo. En ese caso puedes utilizar getenv("_")para conseguirlo. Sin embargo, esto no es confiable porque no todos los shells hacen esto y podría configurarse con cualquier valor o ser sobrante de un proceso principal que no lo cambió antes de ejecutar su programa.

mark4o avatar Jun 21 '2009 22:06 mark4o

El uso de /proc/self/exeno es portátil y no es confiable. En mi sistema Ubuntu 12.04, debes ser root para leer/seguir el enlace simbólico. Esto hará que el ejemplo de Boost y probablemente las whereami()soluciones publicadas fallen.

Esta publicación es muy larga pero analiza los problemas reales y presenta un código que realmente funciona junto con la validación frente a un conjunto de pruebas.

La mejor manera de encontrar su programa es volver sobre los mismos pasos que utiliza el sistema. Esto se hace utilizando argv[0]la solución resuelta contra la raíz del sistema de archivos, la contraseña, el entorno de ruta y considerando los enlaces simbólicos y la canonicalización del nombre de ruta. Esto es de memoria, pero lo he hecho con éxito en el pasado y lo he probado en una variedad de situaciones diferentes. No se garantiza que funcione, pero si no es así, probablemente tenga problemas mucho mayores y, en general, es más confiable que cualquiera de los otros métodos discutidos. Hay situaciones en un sistema compatible con Unix en las que el manejo adecuado argv[0]no le permitirá acceder a su programa, pero luego lo ejecutará en un entorno certificablemente defectuoso. También es bastante portátil para todos los sistemas derivados de Unix desde alrededor de 1970 e incluso para algunos sistemas no derivados de Unix, ya que básicamente se basa en la funcionalidad estándar libc() y la funcionalidad de línea de comando estándar. Debería funcionar en Linux (todas las versiones), Android, Chrome OS, Minix , Bell Labs Unix original, FreeBSD , NetBSD , OpenBSD , BSD xx, SunOS, Solaris , SYSV, HP-UX , Concentrix, SCO, Darwin, AIX , OS. X, NeXTSTEP , etc. Y con una pequeña modificación probablemente VMS , VM/CMS, DOS/Windows, ReactOS , OS/2 , etc. Si un programa se inició directamente desde un entorno GUI, debería haberse configurado argv[0]en una ruta absoluta.

Comprenda que casi todos los shells de todos los sistemas operativos compatibles con Unix que se hayan lanzado básicamente encuentran programas de la misma manera y configuran el entorno operativo casi de la misma manera (con algunos extras opcionales). Y se espera que cualquier otro programa que inicie un programa cree el mismo entorno (argv, cadenas de entorno, etc.) para ese programa como si se ejecutara desde un shell, con algunos extras opcionales. Un programa o usuario puede configurar un entorno que se desvíe de esta convención para otros programas subordinados que inicie, pero si lo hace, se trata de un error y el programa no tiene expectativas razonables de que el programa subordinado o sus subordinados funcionen correctamente.

Los valores posibles de argv[0]incluyen:

  • /path/to/executable- camino absoluto
  • ../bin/executable— relativo a las personas con discapacidad
  • bin/executable— relativo a las personas con discapacidad
  • ./foo— relativo a las personas con discapacidad
  • executable— nombre base, buscar en la ruta
  • bin//executable— relativo a pwd, no canónico
  • src/../bin/executable— relativo a pwd, no canónico, retroceso
  • bin/./echoargc— relativo a pwd, no canónico

Valores que no deberías ver:

  • ~/bin/executable- reescrito antes de que se ejecute su programa.
  • ~user/bin/executable- reescrito antes de que se ejecute su programa
  • alias- reescrito antes de que se ejecute su programa
  • $shellvariable- reescrito antes de que se ejecute su programa
  • *foo*— comodín, reescrito antes de que se ejecute el programa, no muy útil
  • ?foo?— comodín, reescrito antes de que se ejecute el programa, no muy útil

Además, pueden contener nombres de rutas no canónicas y múltiples capas de enlaces simbólicos. En algunos casos, puede haber varios enlaces físicos al mismo programa. Por ejemplo, /bin/ls, /bin/ps, /bin/chmod, /bin/rm, etc. pueden ser enlaces físicos a /bin/busybox.

Para encontrarte a ti mismo, sigue los pasos a continuación:

  • Guarde pwd, PATH y argv[0] al ingresar a su programa (o al inicializar su biblioteca), ya que pueden cambiar más adelante.

  • Opcional: particularmente para sistemas que no son Unix, separe pero no descarte la parte del prefijo del nombre de ruta del host/usuario/unidad, si está presente; la parte que a menudo precede a dos puntos o sigue a un "//" inicial.

  • Si argv[0]es una ruta absoluta, utilícela como punto de partida. Una ruta absoluta probablemente comience con "/", pero en algunos sistemas que no son Unix puede comenzar con "" o una letra de unidad o un prefijo de nombre seguido de dos puntos.

  • De lo contrario, si argv[0]es una ruta relativa (contiene "/" o "" pero no comienza con ella, como "../../bin/foo", luego combine pwd+"/"+argv[0] (use presente directorio de trabajo desde que se inició el programa, no el actual).

  • De lo contrario, si argv[0] es un nombre base simple (sin barras), combínelo con cada entrada en la variable de entorno PATH por turno, pruébelos y use el primero que tenga éxito.

  • Opcional: de lo contrario, pruebe con la plataforma específica /proc/self/exe, /proc/curproc/file(BSD) y (char *)getauxval(AT_EXECFN), y dlgetname(...)si está presente. Incluso podrías probar estos argv[0]métodos basados ​​en antes, si están disponibles y no encuentras problemas de permisos. En el caso algo improbable (cuando se consideran todas las versiones de todos los sistemas) de que estén presentes y no fallen, podrían tener más autoridad.

  • Opcional: busque un nombre de ruta pasado usando un parámetro de línea de comando.

  • Opcional: busque un nombre de ruta en el entorno que su secuencia de comandos contenedora haya pasado explícitamente, si corresponde.

  • Opcional: como último recurso, pruebe con la variable de entorno "_". Podría apuntar a un programa completamente diferente, como el shell del usuario.

  • Resuelva los enlaces simbólicos, puede haber varias capas. Existe la posibilidad de que se produzcan bucles infinitos, aunque si existen, es probable que su programa no sea invocado.

  • Canonicalice el nombre del archivo resolviendo subcadenas como "/foo/../bar/" en "/bar/". Tenga en cuenta que esto puede cambiar potencialmente el significado si cruza un punto de montaje de la red, por lo que la canonización no siempre es algo bueno. En un servidor de red, ".." en el enlace simbólico se puede utilizar para recorrer una ruta a otro archivo en el contexto del servidor en lugar de en el cliente. En este caso, probablemente desee el contexto del cliente para que la canonicalización esté bien. También convierta patrones como "/./" a "/" y "//" a "/". En Shell, readlink --canonicalizeresolverá múltiples enlaces simbólicos y canonicalizará el nombre. Chase puede hacer algo similar pero no está instalado. realpath()o canonicalize_file_name(), si está presente, puede ayudar.

If realpath() doesn't exist at compile time, you might borrow a copy from a permissively licensed library distribution, and compile it in yourself rather than reinventing the wheel. Fix the potential buffer overflow (pass in sizeof output buffer, think strncpy() vs strcpy()) if you will be using a buffer less than PATH_MAX. It may be easier just to use a renamed private copy rather than testing if it exists. Permissive license copy from android/darwin/bsd: https://android.googlesource.com/platform/bionic/+/f077784/libc/upstream-freebsd/lib/libc/stdlib/realpath.c

Be aware that multiple attempts may be successful or partially successful and they might not all point to the same executable, so consider verifying your executable; however, you may not have read permission — if you can't read it, don't treat that as a failure. Or verify something in proximity to your executable such as the "../lib/" directory you are trying to find. You may have multiple versions, packaged and locally compiled versions, local and network versions, and local and USB-drive portable versions, etc. and there is a small possibility that you might get two incompatible results from different methods of locating. And "_" may simply point to the wrong program.

A program using execve can deliberately set argv[0] to be incompatible with the actual path used to load the program and corrupt PATH, "_", pwd, etc. though there isn't generally much reason to do so; but this could have security implications if you have vulnerable code that ignores the fact that your execution environment can be changed in variety of ways including, but not limited, to this one (chroot, fuse filesystem, hard links, etc.) It is possible for shell commands to set PATH but fail to export it.

You don't necessarily need to code for non-Unix systems but it would be a good idea to be aware of some of the peculiarities so you can write the code in such a way that it isn't as hard for someone to port later. Be aware that some systems (DEC VMS, DOS, URLs, etc.) might have drive names or other prefixes which end with a colon such as "C:", "sys$drive:[foo]bar", and "file:///foo/bar/baz". Old DEC VMS systems use "[" and "]" to enclose the directory portion of the path though this may have changed if your program is compiled in a POSIX environment. Some systems, such as VMS, may have a file version (separated by a semicolon at the end). Some systems use two consecutive slashes as in "//drive/path/to/file" or "user@host:/path/to/file" (scp command) or "file://hostname/path/to/file" (URL). In some cases (DOS and Windows), PATH might have different separator characters — ";" vs ":" and "" vs "/" for a path separator. In csh/tsh there is "path" (delimited with spaces) and "PATH" delimited with colons but your program should receive PATH so you don't need to worry about path. DOS and some other systems can have relative paths that start with a drive prefix. C:foo.exe refers to foo.exe in the current directory on drive C, so you do need to lookup current directory on C: and use that for pwd.

An example of symlinks and wrappers on my system:

/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

Note that user bill posted a link above to a program at HP that handles the three basic cases of argv[0]. It needs some changes, though:

  • It will be necessary to rewrite all the strcat() and strcpy() to use strncat() and strncpy(). Even though the variables are declared of length PATHMAX, an input value of length PATHMAX-1 plus the length of concatenated strings is > PATHMAX and an input value of length PATHMAX would be unterminated.
  • It needs to be rewritten as a library function, rather than just to print out results.
  • It fails to canonicalize names (use the realpath code I linked to above)
  • It fails to resolve symbolic links (use the realpath code)

So, if you combine both the HP code and the realpath code and fix both to be resistant to buffer overflows, then you should have something which can properly interpret argv[0].

The following illustrates actual values of argv[0] for various ways of invoking the same program on Ubuntu 12.04. And yes, the program was accidentally named echoargc instead of echoargv. This was done using a script for clean copying but doing it manually in shell gets same results (except aliases don't work in script unless you explicitly enable them).

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

These examples illustrate that the techniques described in this post should work in a wide range of circumstances and why some of the steps are necessary.

EDIT: Now, the program that prints argv[0] has been updated to actually find itself.

// 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 */
}

And here is the output which demonstrates that in every one of the previous tests it actually did find itself.

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"

The two GUI launches described above also correctly find the program.

There is one potential pitfall. The access() function drops permissions if the program is setuid before testing. If there is a situation where the program can be found as an elevated user but not as a regular user, then there might be a situation where these tests would fail, although it is unlikely the program could actually be executed under those circumstances. One could use euidaccess() instead. It is possible, however, that it might find an inaccessable program earlier on path than the actual user could.

whitis avatar Dec 14 '2015 16:12 whitis