在不使用/proc/self/exe的情况下找到当前可执行文件的路径

234

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

如果您曾经需要支持例如Mac OS X这样没有/proc/的系统,您会怎么做?使用#ifdef来隔离特定于平台的代码(例如NSBundle)吗?还是尝试从argv [0],$PATH等中推断出可执行文件的路径,冒着在边缘情况下发现错误的风险?


重复:https://dev59.com/KHNA5IYBdhLWcg3wgeId - Greg Hewgill
我谷歌搜索了一下:获取我的 ps -o comm。 让我来到这里的是:"/proc/pid/path/a.out"。 - basin
1
在我看来,prideout的回答应该排在首位,因为它正确地解决了“跨平台接口”的要求,并且非常容易集成。 - Stéphane Gourichon
1
@ALX23z std::filesystem::current_path() 不会返回当前可执行文件的路径,而是返回当前工作目录的路径。 - Ted Lyngmo
1
@ALX23z 不,当前目录永远不会与可执行文件相同。 - Ted Lyngmo
显示剩余2条评论
15个回答

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

还有第三方库可以用来获取这些信息,例如whereami(在Prideout的答案中提到),或者如果您正在使用Qt,则可以使用QCoreApplication :: applicationFilePath(),如评论中所述。

便携式(但不太可靠)的方法是使用 argv [0] 。尽管调用程序可以将其设置为任何内容,但通常将其设置为可执行文件的路径名或使用 $ PATH 找到的名称。

一些Shell(包括bash和ksh)设置环境变量“ _ ”为执行之前的完整路径。在这种情况下,您可以使用 getenv("_") 来获取它。但是,这是不可靠的,因为并非所有Shell都这样做,它可能被设置为任何内容,或者从未更改过父进程留下的内容,在执行程序之前没有更改它。


4
请注意,_NSGetExecutablePath() 不会遵循符号链接。 - naruse
2
NetBSD:读取链接/proc/curproc/exe DragonFly BSD:读取链接/proc/curproc/file - naruse
7
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的操作... - FrankH.
4
"QDesktopServices::storageLocation(QDesktopServices::DataLocation)" 的含义是获取当前用户数据应存储的目录路径,而非可执行文件的路径。请注意,这仅是涉及到数据存储的路径,不包括任何其他解释或内容。 - user862787
3
在2017年,OpenBSD是唯一一个你仍然不能使用的操作系统。你必须使用PATH和argv[0]的方式。 - Lothar
显示剩余7条评论

42
使用/proc/self/exe是不可移植和不可靠的。在我的Ubuntu 12.04系统上,您必须以root身份才能读取/遵循符号链接。这将使Boost示例和可能发布的whereami()解决方案失败。
这篇文章非常长,但讨论了实际问题,并呈现了实际有效的代码以及针对测试套件的验证。

找到你的程序的最好方法是重复系统使用的相同步骤。这可以通过使用argv[0]解析根文件系统、pwd、路径环境以及考虑符号链接和路径名规范化来完成。我记得以前我曾经成功地做过这个,并在各种不同的情况下进行了测试。它不能保证一定有效,但如果它无效,那么你可能有更大的问题,而且总体上比其他讨论过的任何方法更可靠。在Unix兼容系统中,有些情况下正确处理argv[0]可能无法帮助你找到你的程序,但那时你正在执行一个明显有问题的环境。它也相当适用于所有Unix衍生系统,自1970年以来甚至包括一些非Unix衍生系统,因为它基本上依赖于libc()标准功能和标准命令行功能。它应该在Linux(所有版本)、Android、Chrome OS、Minix、原始贝尔实验室Unix、FreeBSDNetBSDOpenBSDBSD x.x、SunOS、Solaris、SYSV、HP-UX、Concentrix、SCO、Darwin、AIX、OS X、NeXTSTEP等系统上工作。并且通过一些修改可能还适用于VMS、VM/CMS、DOS/Windows、ReactOSOS/2等系统。如果一个程序是直接从GUI环境启动的,它应该将argv[0]设置为绝对路径。

请注意,几乎所有曾经发布的基于Unix的操作系统上的几乎所有shell程序都以基本相同的方式查找程序并设置操作环境(带有一些可选项)。任何其他启动程序的程序都应该为该程序创建与从shell中运行时相同的环境(argv、环境字符串等),带有一些可选项。程序或用户可以为其启动的其他下属程序设置偏离此约定的环境,但如果这样做,就会出现错误,并且该程序无法合理地期望下属程序或其下属程序能够正确运行。 argv[0] 可能的值包括:
  • /path/to/executable — 绝对路径
  • ../bin/executable — 相对于pwd
  • bin/executable — 相对于pwd
  • ./foo — 相对于pwd
  • executable — 基本名称,在路径中查找
  • 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将解析多个符号链接并使名称规范化。 Chase可能会执行类似的操作,但未安装。 如果存在,则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:/// foo / bar / baz”。旧DEC VMS系统使用“ [”和“ ]”括住路径的目录部分,但如果您的程序在POSIX环境中编译,则可能已更改此设置。一些系统,如VMS,可能具有文件版本(在末尾用分号分隔)。一些系统使用两个连续斜杠,例如“//drive/path/to/file”或“user@host:/path/to/file”(scp命令)或“file://hostname/path/to/file”(URL)。在某些情况下(DOS和Windows),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

请注意,用户bill posted在HP上发布了一个处理argv[0]三种基本情况的程序的链接。不过它还需要一些更改:
  • 需要重写所有的strcat()strcpy(),使用strncat()strncpy()。即使变量声明为PATHMAX长度,长度为PATHMAX-1加上连接字符串的长度的输入值大于PATHMAX,长度为PATHMAX的输入值也将无法终止。
  • 它需要被重写为库函数,而不仅仅是打印结果。
  • 它未能将名称标准化(使用我在上面链接的realpath代码)
  • 它未能解析符号链接(使用realpath代码)

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


2
你在这方面付出了很多努力,干得好。不幸的是,在代码中既不安全使用strncpy()也不安全使用strncat(),尤其是strncat()strncpy()不能保证空终止;如果源字符串比目标空间长,则该字符串没有空终止符。strncat()非常难以使用;即使target最初是一个空字符串,strncat(target, source, sizeof(target))也是错误的,如果source比目标长。长度是可以安全附加到目标的字符数,不包括尾随的空值,因此sizeof(target)-1是最大值。 - Jonathan Leffler
9
strncpy代码是正确的,不像你暗示我应该使用的方法。我建议您仔细阅读该代码。它既不会溢出缓冲区也不会使其未终止。每次使用strncpy()/stncat()都是通过拷贝sizeof(buffer)来限制的,这是有效的,然后将缓冲区的最后一个字符填充为零,覆盖了缓冲区的最后一个字符。 然而,strncat()将size参数错误地用作计数器,可能会溢出,因为它是在缓冲区溢出攻击之前发布的。 - whitis
"sudo apt-get install libbsd0 libbsd-dev",然后将s/strncat/strlcat/ - whitis
2
不要使用PATH_MAX。这已经在30年前停止工作了,始终使用malloc。 - Lothar
如果你使用了init调用,请在init中完全解析exe的路径,而不是仅部分解析,并在后续调用中执行。如果在解析器中使用realpath,则无法使用惰性评估。与其他错误一起,这是我在stackoverflow上看过的最糟糕的代码。 - Lothar

28

whereami库由Gregory Pakosz实现,使用mark4o的文章中提到的API来支持各种平台。如果您“只是”需要适用于可移植项目的解决方案,并且不关心不同平台的特殊性,则这最为有趣。

截至撰写本文时,支持的平台有:

  • Windows
  • Linux
  • Mac
  • iOS
  • Android
  • QNX Neutrino
  • FreeBSD
  • NetBSD
  • DragonFly 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;
}

13

在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 上实际上并不是一个问题。


1
我喜欢这个因为它很简单,而且Gtk+已经包含了glibc(我正在使用)。 - Colin Keenan
如果我使用 ./build/foo 启动我的进程,你的解决方案会返回 ./build/foo,这是毫无用处的。(Debian Bullseye) - BitTickler
@BitTickler:是吗?我认为定位可执行文件最常见的用途是访问相对于添加的文件,使用相对路径没有问题。如果出于任何原因需要绝对路径,也不是不能将其转换为绝对路径。 - Dolda2000
我的使用案例是一个带有一些资源文件的gtk应用程序。在尝试通过gnome收藏夹启动它时(不知道它们的当前目录是什么...),我需要构建一个绝对路径到我的资源(它们与我的可执行meson构建目录上面的一个文件夹中...)。因此,如果我不知道.是什么,我实际上无法将路径绝对化...最终我使用了readlink解决方案,它返回一个绝对路径。 - BitTickler
@BitTickler:你总是知道是什么,只需调用getcwd()即可。即使你不知道,而且你只需要进入上一级目录,也总有.. - Dolda2000

8

要可靠地在各个平台上运行,需要使用 #ifdef 语句。

以下代码可以在 Windows、Linux、MacOS、Solaris 或 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>并将返回语句更改为:
return strlen(exePath)>0 ? boost::filesystem::path(exePath).remove_filename().make_preferred().string() : std::string();

@Frank,不确定你为什么这么说。对我来说是有效的。我看到另一个回答声称你需要root才能访问/proc/self/exe,但我在我尝试过的任何Linux系统(CentOS或Mint)上都没有发现这个问题。 - jtbr
在我的Linux(Ubuntu 20.04)和macOS 12.6(arm)上运行良好。非常感谢! - Fabian

6
如果你需要支持没有/proc/的Mac OS X,你会怎么做?使用#ifdef来隔离特定于平台的代码(例如NSBundle)是常规方法。
另一种方法是拥有一个干净的没有#ifdef的头文件,其中包含函数声明,并将实现放在特定于平台的源文件中。
例如,看看POCO(可移植组件)C ++库如何为其Environment类执行类似操作。

3
根据的版本不同,查找用于启动运行进程的可执行文件的完整路径和名称有不同的方法。我将进程标识符表示为。请尝试以下操作:
  1. 如果文件/proc/self/exefile存在,则其内容是所请求的信息。
  2. 如果文件/proc/PID/exefile存在,则其内容是所请求的信息。
  3. 如果文件/proc/self/as存在,则:
    1. 打开()该文件。
    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/PID/as执行步骤3的过程。
  5. 尝试dladdr(dlsym(RTLD_DEFAULT,“main”)&dlinfo)其中dlinfo是一个Dl_info结构,其dli_fname可能包含所请求的信息。
我希望这可以帮助您。

很好的评论,但遗憾的是根本不起作用(QNX 6.5.0),请使用'__progname'作为外部变量。 - Alexander G.
这个程序适用于ARMv7-A上的QNX 6.5和6.6。你可能漏掉了什么。 - Dr. Koutheir Attouchi

3

除了 mark4o的答案FreeBSD 也有

const char* getprogname(void)

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

1
它是可用的,但它不会返回完整路径,甚至不会返回相对于当前工作目录的路径。 - vy32

2
据我所知,没有这样的方法。而且存在一种歧义:如果同一个可执行文件具有多个硬链接“指向”它,您希望得到什么样的答案?(硬链接实际上不是“指向”,它们是相同的文件,只是在文件系统层次结构中的另一个位置。)一旦execve()成功执行新的二进制文件,关于原始程序的参数的所有信息都会丢失。

2
一旦execve()成功执行一个新二进制文件,关于其参数的所有信息都将丢失。实际上,argp和envp参数并没有丢失,它们以argv[]、环境变量的形式传递,并且在某些UNIX系统中,路径名参数或由路径名参数构造的内容会与argp和envp一起传递(例如OS X/iOS、Solaris),或通过mark4o答案中列出的机制之一提供。但是,是的,如果存在多个硬链接,则这只会给您一个。 - user862787

1
如果你正在编写使用GNU自动工具的GPL代码,并且想要在许多操作系统(包括Windows和macOS)中处理细节的可移植方法,那么gnulib的relocatable-prog模块是一种不错的选择。

模块文档中提到:“在每个程序中,将以下语句作为第一条语句添加到主函数中(甚至在设置区域设置或执行与 libintl 相关的任何操作之前):set_program_name (argv[0]); 此函数的原型在 progname.h 中。”这意味着需要程序员干预。不清楚这是否是预期的。 - Jonathan Leffler
@JonathanLeffler 你需要调用 set_program_name,是的。不确定你所说的“不清楚这是否是预期的意图”的意思。 - Reuben Thomas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接