【问题标题】:Finding current executable's path without /proc/self/exe在没有 /proc/self/exe 的情况下查找当前可执行文件的路径
【发布时间】:2010-11-04 15:07:48
【问题描述】:

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

如果您必须支持没有 /proc/ 的 Mac OS X,您会怎么做?使用#ifdefs 隔离特定于平台的代码(例如NSBundle)?或者尝试从 argv[0]、$PATH 等推断可执行文件的路径,冒着在极端情况下发现错误的风险?

【问题讨论】:

  • 我用谷歌搜索:得到我的ps -o comm。把我带到这里的是:“/proc/pid/path/a.out”
  • 恕我直言prideout's answer 应该是最重要的,因为它正确地解决了“跨平台接口”的要求并且很容易集成。
  • 在 C++17 中你可以使用std::current_path()。我没有看到可移植的 C 风格方法,因为不同的操作系统使用不同的字符串格式,例如 wchar_t、UTF-8 等。
  • @ALX23z std::filesystem::current_path() 没有给出当前可执行文件的路径。它会给你当前的工作目录。

标签: c++ c linux macos executable


【解决方案1】:

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

还有第三方库可用于获取此信息,例如 whereami as mentioned in prideout's answer,或者如果您使用的是 Qt,请使用 cmets 中提到的 QCoreApplication::applicationFilePath()

可移植(但不太可靠)的方法是使用argv[0]。尽管调用程序可以将其设置为任何内容,但按照惯例,它设置为可执行文件的路径名或使用$PATH 找到的名称。

一些 shell,包括 bash 和 ksh,set the environment variable "_" 到可执行文件执行前的完整路径。在这种情况下,您可以使用getenv("_") 来获取它。然而,这是不可靠的,因为并非所有的 shell 都这样做,并且它可以设置为任何值,或者是在执行程序之前没有更改它的父进程遗留下来的。

【讨论】:

  • 还要注意 _NSGetExecutablePath() 不遵循符号链接。
  • NetBSD: readlink /proc/curproc/exe DragonFly BSD: readlink /proc/curproc/file
  • Solaris: char exepath[MAXPATHLEN]; sprintf(exepath, "/proc/%d/path/a.out", getpid()); readlink(exepath, exepath, sizeof(exepath));;这与 getexecname() 不同 - 相当于 pargs -x <PID> | grep AT_SUN_EXECNAME ...
  • "QDesktopServices::storageLocation(QDesktopServices::DataLocation)" 这不是可执行文件的路径,而是应该存储数据的每个用户目录的路径名。
  • OpenBSD 是唯一一个你在 2017 年仍然不能的。你必须使用 PATH 和 argv[0] 方式
【解决方案2】:

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

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

找到您的程序的最佳方法是追溯系统使用的相同步骤。这是通过使用argv[0] 解决文件系统根目录、密码、路径环境并考虑符号链接和路径名规范化来完成的。这是凭记忆,但我过去成功地做到了这一点,并在各种不同的情况下对其进行了测试。它不能保证有效,但如果不能,您可能会遇到更大的问题,并且总体上它比讨论的任何其他方法都更可靠。在 Unix 兼容系统上存在这样的情况,在这种情况下,正确处理 argv[0] 不会让您进入您的程序,但是您正在一个可证明损坏的环境中执行。它也相当可移植到自 1970 年左右以来的所有 Unix 派生系统,甚至一些非 Unix 派生系统,因为它基本上依赖于 libc() 标准功能和标准命令行功能。它应该适用于 Linux(所有版本)、Android、Chrome OS、Minix、原始贝尔实验室 Unix、FreeBSDNetBSDOpenBSDBSDxx、SunOS、Solaris、SYSV、@ 987654327@、Concentrix、SCO、Darwin、AIX、OS X、NeXTSTEP 等。稍加修改可能是VMS、VM/CMS、DOS/Windows、ReactOSOS/2 等。如果程序是直接从 GUI 环境启动的,它应该将argv[0] 设置为绝对路径。

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

argv[0] 的可能值包括:

  • /path/to/executable — 绝对路径
  • ../bin/executable — 相对于密码
  • bin/executable — 相对于密码
  • ./foo — 相对于密码
  • executable — 基本名称,在路径中查找
  • bin//executable — 相对于密码,非规范
  • src/../bin/executable — 相对于密码、非规范、回溯
  • bin/./echoargc — 相对于密码,非规范

你不应该看到的值:

  • ~/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 将解析多个符号链接并规范化名称。 Chase 可能会做类似的事情,但没有安装。 realpath()canonicalize_file_name(),如果存在,可能会有所帮助。

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

请注意,多次尝试可能会成功或部分成功,而且它们可能并不都指向同一个可执行文件,因此请考虑验证您的可执行文件;但是,您可能没有阅读权限——如果您无法阅读,请不要将其视为失败。或者验证您的可执行文件附近的某些内容,例如您尝试查找的“../lib/”目录。您可能有多个版本,打包和本地编译的版本,本地和网络版本,以及本地和 U 盘便携式版本等,并且您可能会从不同的定位方法得到两个不兼容的结果。而“_”可能只是指向错误的程序。

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

您不一定需要为非 Unix 系统编写代码,但最好了解其中的一些特性,这样您就可以以一种对他人来说不那么难的方式编写代码稍后移植。请注意,某些系统(DEC VMS、DOS、URL 等)可能具有以冒号结尾的驱动器名称或其他前缀,例如“C:”​​、“sys$drive:[foo]bar”和“file: ///foo/bar/baz”。旧的 DEC VMS 系统使用“[”和“]”来包含路径的目录部分,尽管如果您的程序是在 POSIX 环境中编译的,这可能会有所改变。某些系统,例如 VMS,可能有一个文件版本(最后用分号分隔)。某些系统使用两个连续的斜杠,如“//drive/path/to/file”或“user@host:/path/to/file”(scp 命令)或“file://hostname/path/to/file” (网址)。在某些情况下(DOS 和 Windows),PATH 可能有不同的分隔符——“;” vs ":" 和 "" vs "/" 用于路径分隔符。在 csh/tsh 中有“路径”(用空格分隔)和“路径”用冒号分隔,但您的程序应该接收 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

请注意,用户 billposted 是一个链接,指向 HP 的一个程序,该程序处理 argv[0] 的三种基本情况。不过,它需要一些更改:

  • 有必要重写所有strcat()strcpy() 以使用strncat()strncpy()。即使变量声明的长度为 PATHMAX,长度为 PATHMAX-1 的输入值加上连接字符串的长度是 > PATHMAX,并且长度为 PATHMAX 的输入值将是未终止的。
  • 需要将其重写为库函数,而不仅仅是打印结果。
  • 无法规范化名称(使用我上面链接到的真实路径代码)
  • 无法解析符号链接(使用真实路径代码)

因此,如果您将 HP 代码和 realpath 代码结合起来并修复两者以防止缓冲区溢出,那么您应该有一些可以正确解释 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 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 */
}

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

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() 代替。但是,它可能会比实际用户更早地在路径上找到无法访问的程序。

【讨论】:

  • 你为此付出了很多努力——做得很好。不幸的是,strncpy() 和(尤其是)strncat() 都没有在代码中安全使用。 strncpy() 不保证空终止;如果源字符串比目标空间长,则该字符串不是以空值结尾的。 strncat() 很难用;如果source 比目标长,strncat(target, source, sizeof(target)) 是错误的(即使 target 是一个空字符串开头)。长度是可以安全地附加到目标的字符数,不包括尾随的 null,因此 sizeof(target)-1 是最大值。
  • strncpy 代码是正确的,不像你暗示我应该使用的方法。我建议您更仔细地阅读代码。它既不会溢出缓冲区,也不会使它们未终止。每次使用 strncpy()/stncat() 都会受到限制,复制 sizeof(buffer) 是有效的,然后缓冲区的最后一个字符用 0 填充,覆盖缓冲区的最后一个字符。然而,strncat() 错误地使用 size 参数作为计数,并且由于它早于缓冲区溢出攻击而可能会溢出。
  • "sudo apt-get install libbsd0 libbsd-dev",然后是 s/strncat/strlcat/
  • 不要使用 PATH_MAX。这在 30 年前停止工作,始终使用 malloc。
  • 如果你使用初始化调用。在 init 上完全解析 exe 的路径,而不仅仅是一部分,然后在调用时执行。如果您在解析器中使用 realpath,则此处无法进行延迟评估。连同其他错误只是我在stackoverflow上看到的最糟糕的代码。
【解决方案3】:

whereami library by Gregory Pakosz 使用mark4o's post 中提到的 API 为各种平台实现此功能。如果您“只是”需要一个适用于可移植项目的解决方案,并且对各种平台的特性不感兴趣,这将是最有趣的。

在撰写本文时,支持的平台是:

  • 窗户
  • Linux
  • 苹果机
  • iOS
  • 安卓
  • QNX 中微子
  • FreeBSD
  • NetBSD
  • 蜻蜓BSD
  • SunOS

该库由 whereami.cwhereami.h 组成,并获得 MIT 和 WTFPL2 许可。将文件拖放到您的项目中,包含标题并使用它:

#include "whereami.h"

int main() {
  int length = wai_getExecutablePath(NULL, 0, NULL);
  char* path = (char*)malloc(length + 1);
  wai_getExecutablePath(path, length, &dirname_length);
  path[length] = '\0';

  printf("My path: %s", path);

  free(path);
  return 0;
}

【讨论】:

    【解决方案4】:

    在 Linux 上使用 /proc/self/exeargv[0] 的替代方法是使用 ELF 解释器传递的信息,由 glibc 提供:

    #include <stdio.h>
    #include <sys/auxv.h>
    
    int main(int argc, char **argv)
    {
        printf("%s\n", (char *)getauxval(AT_EXECFN));
        return(0);
    }
    

    注意getauxval 是一个 glibc 扩展,为了更健壮你应该检查它不会返回 NULL(表明 ELF 解释器没有提供 AT_EXECFN 参数),但我没有'认为这在 Linux 上实际上是一个问题。

    【讨论】:

    • 我喜欢这个,因为它很简单,而且 Gtk+ 中包含 glibc(我正在使用)。
    • 如果我以./build/foo 开始我的流程,您的解决方案将返回:./build/foo,这完全没用。 (debian 靶心)。
    • @BitTickler:是吗?我想说定位可执行文件的最常见用途是访问相对于添加的文件,使用相对路径没有问题。而且,如果您出于任何原因需要绝对路径,也不是不能绝对化它。
    • 我的用例是一个带有一些资源文件的 gtk 应用程序。在尝试通过 gnome 收藏夹启动它时(不知道他们当前的目录是什么......)我需要为我的资源构建一个绝对路径(这是我的介子构建目录的一个文件夹,其中包含可执行文件......)。所以,如果我不知道 . 是什么,我实际上无法绝对化路径......我最终使用了返回绝对路径的 readlink 解决方案。
    • @BitTickler:您总是知道. 是什么,只需致电getcwd()。然而,即使你没有,而且你只需要上一级目录,总有..
    【解决方案5】:

    要跨平台可靠地进行这项工作需要使用 #ifdef 语句。

    以下代码在 Windows、Linux、MacOS、Solaris 或 FreeBSD 中查找可执行文件的路径(尽管 FreeBSD 未经测试)。它用 Boost 1.55.0(或更高版本)以简化代码,但如果您愿意,它很容易删除。只需按照操作系统和编译器的要求使用像 _MSC_VER 和 __linux 这样的定义。

    #include <string>
    #include <boost/predef/os.h>
    
    #if (BOOST_OS_WINDOWS)
    #  include <stdlib.h>
    #elif (BOOST_OS_SOLARIS)
    #  include <stdlib.h>
    #  include <limits.h>
    #elif (BOOST_OS_LINUX)
    #  include <unistd.h>
    #  include <limits.h>
    #elif (BOOST_OS_MACOS)
    #  include <mach-o/dyld.h>
    #elif (BOOST_OS_BSD_FREE)
    #  include <sys/types.h>
    #  include <sys/sysctl.h>
    #endif
    
    /*
     * Returns the full path to the currently running executable,
     * or an empty string in case of failure.
     */
    std::string getExecutablePath() {
        #if (BOOST_OS_WINDOWS)
            char *exePath;
            if (_get_pgmptr(&exePath) != 0)
                exePath = "";
        #elif (BOOST_OS_SOLARIS)
            char exePath[PATH_MAX];
            if (realpath(getexecname(), exePath) == NULL)
                exePath[0] = '\0';
        #elif (BOOST_OS_LINUX)
            char exePath[PATH_MAX];
            ssize_t len = ::readlink("/proc/self/exe", exePath, sizeof(exePath));
            if (len == -1 || len == sizeof(exePath))
                len = 0;
            exePath[len] = '\0';
        #elif (BOOST_OS_MACOS)
            char exePath[PATH_MAX];
            uint32_t len = sizeof(exePath);
            if (_NSGetExecutablePath(exePath, &len) != 0) {
                exePath[0] = '\0'; // buffer too small (!)
            } else {
                // resolve symlinks, ., .. if possible
                char *canonicalPath = realpath(exePath, NULL);
                if (canonicalPath != NULL) {
                    strncpy(exePath,canonicalPath,len);
                    free(canonicalPath);
                }
            }
        #elif (BOOST_OS_BSD_FREE)
            char exePath[2048];
            int mib[4];  mib[0] = CTL_KERN;  mib[1] = KERN_PROC;  mib[2] = KERN_PROC_PATHNAME;  mib[3] = -1;
            size_t len = sizeof(exePath);
            if (sysctl(mib, 4, exePath, &len, NULL, 0) != 0)
                exePath[0] = '\0';
        #endif
            return std::string(exePath);
    }
    

    以上版本返回包含可执行文件名的完整路径。相反,如果您想要没有可执行文件名称的路径,#include boost/filesystem.hpp&gt; 并将返回语句更改为:

    return strlen(exePath)>0 ? boost::filesystem::path(exePath).remove_filename().make_preferred().string() : std::string();
    

    【讨论】:

    • @Frank,不知道你为什么这么说。为我工作。我看到另一个回复声称您需要 root 才能访问 /proc/self/exe,但在我尝试过的任何 Linux 系统(CentOS 或 Mint)上我都没有发现。
    【解决方案6】:

    如果你曾经必须支持,比如说,Mac OS X,没有/proc/,什么 你会做吗?使用#ifdefs 隔离特定于平台的代码 (例如 NSBundle)?

    是的,使用#ifdefs 隔离特定于平台的代码是执行此操作的常规方式。

    另一种方法是有一个干净的 #ifdef-less 标头,其中包含函数声明并将实现放在特定于平台的源文件中。

    例如,看看 POCO(便携式组件)C++ 库如何为他们的 Environment 类做类似的事情。

    【讨论】:

      【解决方案7】:

      根据 QNX Neutrino 的版本,有不同的方法可以找到用于启动运行进程的可执行文件的完整路径和名称。我将进程标识符表示为&lt;PID&gt;。请尝试以下操作:

      1. 如果文件/proc/self/exefile存在,那么它的内容就是请求的信息。
      2. 如果文件/proc/&lt;PID&gt;/exefile存在,那么它的内容就是请求的信息。
      3. 如果文件/proc/self/as存在,则:
        1. open() 文件。
        2. 至少分配sizeof(procfs_debuginfo) + _POSIX_PATH_MAX 的缓冲区。
        3. 将该缓冲区作为输入提供给devctl(fd, DCMD_PROC_MAPDEBUG_BASE,...
        4. 将缓冲区转换为procfs_debuginfo*
        5. 请求的信息位于procfs_debuginfo 结构的path 字段中。 警告:出于某种原因,QNX 有时会省略文件路径的第一个斜杠/。需要时添加 /
        6. 清理(关闭文件、释放缓冲区等)。
      4. 使用文件/proc/&lt;PID&gt;/as 尝试3. 中的过程。
      5. 尝试dladdr(dlsym(RTLD_DEFAULT, "main"), &amp;dlinfo),其中dlinfoDl_info 结构,其dli_fname 可能包含请求的信息。

      我希望这会有所帮助。

      【讨论】:

      • 伟大的评论,但遗憾的是根本不起作用(QNX 6.5.0),使用'__progname'作为外部
      • 此过程适用于 ARMv7-A 上的 QNX 6.5 和 6.6。你可能错过了什么。
      【解决方案8】:

      除了mark4o's answerFreeBSD还有

      const char* getprogname(void)
      

      它也应该在 macOS 中可用。它可以通过 libbsd 在 GNU/Linux 中使用。

      【讨论】:

      • 它可用,但它不返回完整路径,甚至不返回来自 cwd 的相对路径。
      【解决方案9】:

      AFAIK,没有这样的方式。还有一个模棱两可的问题:如果同一个可执行文件有多个“指向”它的硬链接,你希望得到什么答案? (硬链接实际上并不“指向”,它们同一个文件,只是位于文件系统层次结构中的另一个位置。)

      一旦execve() 成功执行新的二进制文件,有关原始程序参数的所有信息都会丢失。

      【讨论】:

      • “一旦 execve() 成功执行一个新的二进制文件,所有关于它的参数的信息都会丢失。”实际上,argp 和 envp 参数并没有丢失,它们作为 argv[] 和环境传递,在某些 UN*Xes 中,路径名参数或由它构造的东西要么与 argp 和 envp 一起传递(OS X /iOS,Solaris)或通过mark4o的答案中列出的机制之一提供。但是,是的,如果有多个硬链接,那只会给你一个硬链接。
      【解决方案10】:

      当然,这并不适用于所有项目。 尽管如此,QCoreApplication::applicationFilePath() 在 6 年的 C++/Qt 开发中从未让我失望过。

      当然,在尝试使用之前应该仔细阅读文档:

      警告:在 Linux 上,此函数将尝试从 /proc 文件系统。如果失败,则假定 argv[0] 包含 可执行文件的绝对文件名。该函数还假设 应用程序未更改当前目录。

      说实话,我认为#ifdef 和其他类似的解决方案根本不应该在现代代码中使用。

      我确信还存在较小的跨平台库。让他们将所有平台特定的东西封装在里面。

      【讨论】:

      • 我用的是GTK,没有GTK跨平台解决方案吗?
      【解决方案11】:

      如果您正在编写 GPL 代码并使用 GNU 自动工具,那么在许多操作系统(包括 Windows 和 macOS)上处理细节的可移植方式是 gnulib 的 relocatable-prog 模块。

      【讨论】:

      • 模块文档说:“在每个程序中,添加到 main 作为第一条语句(甚至在设置语言环境或做与libintl 相关的任何事情之前):set_program_name (argv[0]); 的原型这个函数在progname.h。”。这意味着需要程序员干预。目前尚不清楚这是什么意图。
      • @JonathanLeffler 你必须打电话给set_program_name,是的。不知道您所说的“不清楚这就是预期的”是什么意思。
      【解决方案12】:

      您可以使用 argv[0] 并分析 PATH 环境变量。 看:A sample of a program that can find itself

      【讨论】:

      • 这实际上并不可靠(尽管它通常可以与通常的 shell 启动的程序一起使用),因为 execv 和 kin 分别使用 argv 的可执行文件路径
      • 这是一个错误的答案。它可能会告诉您在哪里可以找到具有相同名称的 a 程序。但它并没有告诉您当前运行的可执行文件的实际位置。
      【解决方案13】:

      只有我的两分钱。使用此代码,您可以在跨平台接口的 C/C++ 中找到当前应用程序的目录。

      void getExecutablePath(char ** path, unsigned int * pathLength)
      {
          // Early exit when invalid out-parameters are passed
          if (!checkStringOutParameter(path, pathLength))
          {
              return;
          }
      
      #if defined SYSTEM_LINUX
      
          // Preallocate PATH_MAX (e.g., 4096) characters and hope the executable path isn't longer (including null byte)
          char exePath[PATH_MAX];
      
          // Return written bytes, indicating if memory was sufficient
          int len = readlink("/proc/self/exe", exePath, PATH_MAX);
      
          if (len <= 0 || len == PATH_MAX) // memory not sufficient or general error occured
          {
              invalidateStringOutParameter(path, pathLength);
              return;
          }
      
          // Copy contents to caller, create caller ownership
          copyToStringOutParameter(exePath, len, path, pathLength);
      
      #elif defined SYSTEM_WINDOWS
      
          // Preallocate MAX_PATH (e.g., 4095) characters and hope the executable path isn't longer (including null byte)
          char exePath[MAX_PATH];
      
          // Return written bytes, indicating if memory was sufficient
          unsigned int len = GetModuleFileNameA(GetModuleHandleA(0x0), exePath, MAX_PATH);
          if (len == 0) // memory not sufficient or general error occured
          {
              invalidateStringOutParameter(path, pathLength);
              return;
          }
      
          // Copy contents to caller, create caller ownership
          copyToStringOutParameter(exePath, len, path, pathLength);
      
      #elif defined SYSTEM_SOLARIS
      
          // Preallocate PATH_MAX (e.g., 4096) characters and hope the executable path isn't longer (including null byte)
          char exePath[PATH_MAX];
      
          // Convert executable path to canonical path, return null pointer on error
          if (realpath(getexecname(), exePath) == 0x0)
          {
              invalidateStringOutParameter(path, pathLength);
              return;
          }
      
          // Copy contents to caller, create caller ownership
          unsigned int len = strlen(exePath);
          copyToStringOutParameter(exePath, len, path, pathLength);
      
      #elif defined SYSTEM_DARWIN
      
          // Preallocate PATH_MAX (e.g., 4096) characters and hope the executable path isn't longer (including null byte)
          char exePath[PATH_MAX];
      
          unsigned int len = (unsigned int)PATH_MAX;
      
          // Obtain executable path to canonical path, return zero on success
          if (_NSGetExecutablePath(exePath, &len) == 0)
          {
              // Convert executable path to canonical path, return null pointer on error
              char * realPath = realpath(exePath, 0x0);
      
              if (realPath == 0x0)
              {
                  invalidateStringOutParameter(path, pathLength);
                  return;
              }
      
              // Copy contents to caller, create caller ownership
              unsigned int len = strlen(realPath);
              copyToStringOutParameter(realPath, len, path, pathLength);
      
              free(realPath);
          }
          else // len is initialized with the required number of bytes (including zero byte)
          {
              char * intermediatePath = (char *)malloc(sizeof(char) * len);
      
              // Convert executable path to canonical path, return null pointer on error
              if (_NSGetExecutablePath(intermediatePath, &len) != 0)
              {
                  free(intermediatePath);
                  invalidateStringOutParameter(path, pathLength);
                  return;
              }
      
              char * realPath = realpath(intermediatePath, 0x0);
      
              free(intermediatePath);
      
              // Check if conversion to canonical path succeeded
              if (realPath == 0x0)
              {
                  invalidateStringOutParameter(path, pathLength);
                  return;
              }
      
              // Copy contents to caller, create caller ownership
              unsigned int len = strlen(realPath);
              copyToStringOutParameter(realPath, len, path, pathLength);
      
              free(realPath);
          }
      
      #elif defined SYSTEM_FREEBSD
      
          // Preallocate characters and hope the executable path isn't longer (including null byte)
          char exePath[2048];
      
          unsigned int len = 2048;
      
          int mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 };
      
          // Obtain executable path by syscall
          if (sysctl(mib, 4, exePath, &len, 0x0, 0) != 0)
          {
              invalidateStringOutParameter(path, pathLength);
              return;
          }
      
          // Copy contents to caller, create caller ownership
          copyToStringOutParameter(exePath, len, path, pathLength);
      
      #else
      
          // If no OS could be detected ... degrade gracefully
          invalidateStringOutParameter(path, pathLength);
      
      #endif
      }
      

      你可以详细看看here

      【讨论】:

      • 至于代码的 FREEBSD 部分:我认为 getprogname() 是更惯用的方式。 (#include &lt;stdlib.h&gt;,它是 libc 的一部分)。
      【解决方案14】:

      但我想知道是否有一种方便的方法可以通过跨平台接口在 C/C++ 中找到当前应用程序的目录。

      你不能这样做(至少在 Linux 上)

      由于一个可执行文件可以在运行它的process 执行期间,rename(2) 将其文件路径指向不同的目录(同一文件系统的)。另见syscalls(2)inode(7)

      在 Linux 上,可执行文件甚至(原则上)可以通过调用 unlink(2) 来调用 remove(3) 本身。然后Linux kernel 应该保留分配的文件,直到没有进程再引用它。使用proc(5),您可以做一些奇怪的事情(例如rename(2) 那个/proc/self/exe 文件等...)

      换句话说,在 Linux 上,“当前应用程序的目录”的概念没有任何意义。

      另请阅读Advanced Linux ProgrammingOperating Systems: Three Easy Pieces 了解更多信息。

      还可以在 OSDEV 上查看多个开源操作系统(包括 FreeBSDGNU Hurd)。其中一些提供了接近 POSIX 的接口 (API)。

      考虑使用(经许可)跨平台 C++ 框架,如 QtPOCO,或许可以通过将它们移植到您最喜欢的操作系统来为它们做出贡献。

      【讨论】:

      • 所以如果你有与你的可执行文件相关的资源文件,你在你的代码中硬编码资源的路径? :) 即使存在一些(相当模糊的)用例,其中一个进程运行的东西不是来自任何路径上的任何文件,通常的用例是您想要可执行文件的绝对路径以查找其他东西,相对于它。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多