在没有/ proc / self / exe的情况下查找当前可执行文件的路径

在我看来,Linux使用/ proc / self / exe很容易。 但是我想知道是否有一种方便的方式来用C / C ++在跨平台接口中查找当前应用程序的目录。 我看到一些项目使用argv [0],但它看起来并不完全可靠。

如果您曾经需要支持Mac OS X(没有/ proc /),您会做什么? 使用#ifdefs隔离平台特定的代码(例如NSBundle)? 或者尝试从argv [0],$ PATH以及其他任何方式推断可执行文件的路径,从而在边缘情况下发现错误的风险?


一些操作系统特定的接口:

  • Mac OS X: _NSGetExecutablePath() (man 3 dyld)
  • Linux: readlink /proc/self/exe
  • Solaris: getexecname()
  • FreeBSD: sysctl CTL_KERN KERN_PROC KERN_PROC_PATHNAME -1
  • FreeBSD如果有procfs: readlink /proc/curproc/file (FreeBSD默认没有procfs)
  • NetBSD: readlink /proc/curproc/exe
  • DragonFly BSD: readlink /proc/curproc/file
  • Windows: GetModuleFileName()hModule = NULL
  • 便携式(但不太可靠)的方法是使用argv[0] 。 虽然它可以被调用程序设置为任何东西,但通过约定,它可以设置为可执行文件的路径名或使用$PATH找到的名称。

    一些shell(包括bash和ksh)在执行前将环境变量“ _ ”设置为可执行文件的完整路径。 在这种情况下,您可以使用getenv("_")来获取它。 然而,这是不可靠的,因为并不是所有的shell都这样做,并且它可以被设置为任何东西,或者在执行程序之前不会改变它的父进程中被遗留下来。


    /proc/self/exe的使用不可移植且不可靠。 在我的Ubuntu 12.04系统上,您必须是root用户才能读取/遵循符号链接。 这会使Boost示例和可能的whereami()解决方案失败。

    这篇文章非常长,但讨论了实际问题,并提供了与测试套件一起验证的代码。

    找到您的程序的最佳方式是回溯系统使用的相同步骤。 这是通过使用argv[0]解析文件系统根,pwd,路径环境并考虑符号链接和路径名规范化来完成的。 这是来自记忆,但我已经成功地完成了这一点,并在各种不同的情况下对其进行测试。 它不能保证工作,但如果不是,你可能会遇到更大的问题,并且比其他任何讨论的方法更可靠。 在Unix兼容的系统中,有些情况下, argv[0]适当处理不会让你进入你的程序,而是你在一个可以认证的破坏环境中执行。 自从1970年左右以来,它对所有Unix派生系统都是相当便携的,甚至还有一些非Unix派生系统,因为它基本上依赖于libc()标准功能和标准命令行功能。 它应该在Linux(所有版本),Android,Chrome OS,Minix,原始贝尔实验室Unix,FreeBSD,NetBSD,OpenBSD,BSD xx,SunOS,Solaris,SYSV,HPUX,Concentrix,SCO,Darwin,AIX,OS X, Nextstep等等,并且可能对VMS,VM / CMS,DOS / Windows,ReactOS,OS / 2等进行一些修改。如果一个程序直接从GUI环境启动,它应该将argv[0]设置为绝对路径。

    了解几乎每个发布的Unix兼容操作系统上的每个shell基本上都会找到相同的程序,并以几乎相同的方式设置操作环境(带有一些可选附加组件)。 而且任何其他启动程序的程序都应该为该程序创建相同的环境(argv,环境字符串等),就好像它是从shell运行的一样,并带有一些可选的附加组件。 程序或用户可以设置一个偏离此约定的环境,用于其启动的其他下属程序,但如果这样做,这是一个错误,程序没有合理的期望,下属程序或其下属将正常运行。

    argv[0]可能值包括:

  • /path/to/executable - 绝对路径
  • ../bin/executable - 相对于pwd
  • bin/executable - 相对于pwd
  • ./foo - 相对于pwd
  • executable - basename,在路径中查找
  • bin//executable - 相对于pwd,非规范
  • src/../bin/executable - 相对于pwd,非规范,回溯
  • bin/./echoargc - 相对于pwd,非规范
  • 您不应该看到的值:

  • ~/bin/executable - 在程序运行之前重写。
  • ~user/bin/executable - 在程序运行之前重写
  • alias - 在程序运行之前重写
  • $shellvariable - 在程序运行之前重写
  • *foo* - 通配符,在您的程序运行之前重写,不是很有用
  • ?foo? - 通配符,在程序运行之前重写,不是很有用
  • 另外,这些可能包含非规范路径名称和多层符号链接。 在某些情况下,可能会有多个到同一程序的硬链接。 例如, /bin/ls/bin/ps/bin/chmod/bin/rm等可能是硬链接到/bin/busybox

    要找到自己,请按照以下步骤操作:

  • 将pwd,PATH和argv [0]保存到您的程序中(或初始化您的库),因为它们可能稍后会更改。

  • 可选:特别是对于非Unix系统,分离出来但不丢弃路径名主机/用户/驱动器前缀部分(如果存在); 经常在冒号前面或跟在最初的“//”之后的部分。

  • 如果argv[0]是绝对路径,则以此为起点。 绝对路径可能以“/”开头,但在某些非Unix系统中,它可能以“”开头或驱动器号或名称前缀后跟一个冒号。

  • 否则,如果argv[0]是一个相对路径(包含“/”或“”但不以它开头,如“../../bin/foo”,则将pwd +“/”+ argv [ 0](从程序启动时使用当前工作目录,不是最新的)。

  • 否则,如果argv [0]是一个普通的基本名称(无斜杠),然后将其与PATH环境变量中的每个条目依次组合,并尝试使用第一个成功的条目。

  • 可选:否则请尝试使用平台特定的/proc/self/exe/proc/curproc/file (BSD)和(char *)getauxval(AT_EXECFN)dlgetname(...)如果存在)。 您甚至可以在基于argv[0]的方法之前尝试这些方法,如果这些方法可用并且您没有遇到权限问题。 在某种程度上不太可能的情况下(当你考虑所有系统的所有版本时)它们存在并且不会失败,它们可能更具权威性。

  • 可选:使用命令行参数检查传入的路径名称。

  • 可选:检查包装脚本显式传入的环境中的路径名(如果有的话)。

  • 可选:作为最后的手段尝试环境变量“_”。 它可能完全指向一个不同的程序,例如用户shell。

  • 解析符号链接,可能会有多个图层。 有无限循环的可能性,但是如果它们存在,你的程序可能不会被调用。

  • 通过将“/foo/../bar/”等子字符串解析为“/ bar /”来标准化文件名。 请注意,如果您穿过网络安装点,这可能会改变含义,因此封装并不总是好事。 在网络服务器上,符号链接中的“..”可用于遍历服务器上下文中的另一个文件的路径,而不是客户端上的路径。 在这种情况下,您可能需要客户端上下文以使规范化成功。 还可以将“/./”等模式转换为“/”和“//”至“/”。 在shell中, readlink --canonicalize将解析多个符号链接并规范化名称。 大通可能会做类似的事情,但没有安装。 realpath()canonicalize_file_name() (如果存在)可能会有所帮助。

  • 如果realpath()在编译时不存在,您可以从获得许可的库发行版中借用一份副本,并自行编译它,而不是重新发明轮子。 修复潜在的缓冲区溢出(如果要使用小于PATH_MAX的缓冲区,则传入sizeof输出缓冲区,请考虑strncpy()vs strcpy())。 仅仅使用重命名的私人副本而不是测试它是否存在可能更容易。 从android / darwin / bsd许可许可副本:https://android.googlesource.com/platform/bionic/+/f077784/libc/upstream-freebsd/lib/libc/stdlib/realpath.c

    请注意,多次尝试可能会成功或部分成功,并且它们可能并不都指向同一个可执行文件,因此请考虑验证您的可执行文件; 但是,您可能没有读取权限 - 如果您无法读取它,请不要将其视为失败。 或者验证与您的可执行文件相近的内容,例如您正在尝试查找的“../lib/”目录。 您可能有多个版本,打包和本地编译版本,本地和网络版本,以及本地和USB驱动器可移植版本等,您可能会收到来自不同定位方法的两个不兼容结果。 而“_”可能只是指向错误的程序。

    使用execve的程序可以故意将argv[0]设置为与用于加载程序的实际路径不兼容,并且损坏PATH,“_”,pwd等,尽管通常没有太多理由这样做。 但是如果你有易受攻击的代码忽略了你的执行环境可以用多种方式改变的事实,包括但不限于这个(chroot,fuse文件系统,硬链接等),这可能会带来安全隐患。用于shell命令来设置PATH但无法导出它。

    你不一定需要为非Unix系统编写代码,但是了解一些特性是个好主意,这样你就可以编写代码,以便稍后人们可以很难再编写代码。 请注意,某些系统(DEC VMS,DOS,URL等)可能具有驱动器名称或以冒号(例如“C:”,“sys $ drive:[foo] bar”和“file :///富/酒吧/巴兹”。 旧的DEC VMS系统使用“[”和“]”来包围路径的目录部分,尽管如果你的程序是在POSIX环境中编译的话,这可能已经改变了。 某些系统(如VMS)可能具有文件版本(最后以分号分隔)。 有些系统在“// drive / path / to / file”或“user @ host:/ path / to / file”(scp命令)或“file:// hostname / path / to / file”中使用了两个连续的斜线。 (URL)。 在某些情况下(DOS,windoze),PATH可能有不同的分隔符 - “;” vs“:”和“”vs“/”作为路径分隔符。 在csh / tsh中有“path”(用空格分隔)和“PATH”用冒号分隔,但是你的程序应该接收PATH,所以你不需要担心路径。 DOS和其他一些系统可以具有以驱动器前缀开头的相对路径。 C:foo.exe是指驱动器C上当前目录中的foo.exe,因此您需要在C:上查找当前目录并将其用于pwd。

    我的系统上的符号链接和包装器的示例:

    /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
    

    请注意,用户帐单在HP上发布了一个链接,用于处理argv[0]的三种基本情况。 但它需要一些更改:

  • 有必要重写所有的strcat()strcpy()以使用strncat()strncpy() 。 即使变量声明为PATHMAX长度,但长度为PATHMAX-1的输入值加上连接字符串的长度为> PATHMAX,并且长度为PATHMAX的输入值将未终止。
  • 它需要被重写为一个库函数,而不仅仅是打印出结果。
  • 它无法规范化名称(使用上面链接的实际路径代码)
  • 它无法解析符号链接(使用真实路径代码)
  • 因此,如果将HP代码和实际路径代码结合起来并修复两者以抵抗缓冲区溢出,那么您应该有一些可以正确解释argv[0]

    以下说明了在Ubuntu 12.04上调用相同程序的各种方式的argv[0]实际值。 是的,该程序被无意中命名为echoargc而不是echoargv。 这是通过使用脚本进行整洁复制完成的,但是在shell中手动执行它会得到相同的结果(除非您明确启用它们,否则别名在脚本中不起作用)。

    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
    

    这些例子说明了这篇文章中描述的技术应该适用于各种各样的环境,以及为什么有些步骤是必要的。

    编辑:现在,打印argv [0]的程序已更新为实际查找本身。

    // 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 pathn");
         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 pwdn");
        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 $PATHn");
        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 */
    }
    

    这里的输出结果表明,在以前的每一个测试中,它确实找到了自己。

    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"
    

    上述两个GUI启动也正确地找到程序。

    有一个潜在的缺陷。 在测试之前,如果程序设置为setuid,则access()函数会丢弃权限。 如果程序可以作为高级用户而不是普通用户,那么可能会出现这些测试失败的情况,尽管在这种情况下程序实际上不可能被执行。 可以使用euidaccess()来代替。 但是,它可能会在实际用户的路径上找到一个不可访问的程序。


    查看Gregory Pakosz(它只有一个C文件)中的whereami库; 它允许您在各种平台上获取当前可执行文件的完整路径。 目前,它在github上作为回购协议提供。

    链接地址: http://www.djcxy.com/p/54543.html

    上一篇: Finding current executable's path without /proc/self/exe

    下一篇: Destructor vs IDisposable?