找到当前可执行文件的路径而不使用 /proc/self/exe
Finding current executable's path without /proc/self/exe是一个常见的问题,它涉及到在不使用/proc/self/exe路径的情况下,获取当前可执行文件的路径。这个问题的出现是由于/proc/self/exe路径在某些平台上不可用,因此需要找到替代的解决方法。
解决这个问题的方法是使用Gregory Pakosz开发的whereami库。该库实现了在多个平台上获取当前可执行文件路径的功能,使用了在mark4o的帖子中提到的API。如果您只是需要一个适用于可移植项目的解决方案,并不关心各个平台的特殊情况,那么这个库是非常有用的。
目前,whereami库支持以下平台:
- Windows
- Linux
- Mac
- iOS
- Android
- QNX Neutrino
- FreeBSD
- NetBSD
- DragonFly BSD
- SunOS
该库由whereami.c和whereami.h两个文件组成,使用MIT和WTFPL2许可证进行授权。只需将这两个文件添加到您的项目中,包含头文件并使用它即可。
以下是使用whereami库获取当前可执行文件路径的示例代码:
#include "whereami.h" int main() { int length = wai_getExecutablePath(NULL, 0, NULL); char* path = (char*)malloc(length + 1); wai_getExecutablePath(path, length, NULL); path[length] = '\0'; printf("My path: %s", path); free(path); return 0; }
通过调用`wai_getExecutablePath`函数,可以获取当前可执行文件的路径。首先,调用一次该函数,传入NULL作为路径缓冲区和长度参数,以便获得所需的缓冲区大小。然后,根据返回的长度分配足够的内存,并再次调用该函数来获取路径。最后,将路径打印出来并释放内存。
使用whereami库,您可以在不依赖于/proc/self/exe路径的情况下,轻松地获取当前可执行文件的路径。这对于可移植项目非常有用,因为它提供了跨平台的解决方案。无论您在哪个平台上开发,都可以使用这个库来获取当前可执行文件的路径。
解决方法:寻找当前可执行文件的路径的最佳方法是通过使用argv[0]相对于文件系统根目录、pwd、path环境以及考虑符号链接和路径名规范化的步骤来重现系统使用的相同步骤。此方法在大多数Unix兼容系统上都有效,并且在各种不同情况下已成功测试过。它依赖于libc()标准功能和标准命令行功能,因此在所有Unix派生系统上都应该工作。如果程序是直接从GUI环境启动的,它应该将argv[0]设置为绝对路径。
原因:/proc/self/exe的使用是非便携和不可靠的。在Ubuntu 12.04系统上,您必须是root才能读取/跟随符号链接。这将导致Boost示例和可能的whereami()解决方案失败。
可能的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的硬链接。
解决方法如下:
1. 进入程序(或库)时保存pwd、PATH和argv[0],因为它们可能会在之后更改。
2. 可选:特别是对于非Unix系统,分离但不丢弃主机/用户/驱动器前缀部分(如果存在);通常在冒号之前或初始"//"之后。
3. 如果argv[0]是绝对路径,则使用它作为起点。绝对路径可能以"/"开头,但在某些非Unix系统上可能以""、驱动器号或名称前缀开头。
4. 否则,如果argv[0]是相对路径(包含"/"或""但不以它开头,例如"../../bin/foo"),则将pwd+"/"+argv[0](使用程序启动时的当前工作目录,而不是当前目录)合并。
5. 否则,如果argv[0]是纯粹的基本名称(没有斜杠),则将其与PATH环境变量中的每个条目依次组合,并尝试这些路径,使用第一个成功的路径。
6. 可选:否则,尝试特定于平台的/proc/self/exe、/proc/curproc/file(BSD)、(char*)getauxval(AT_EXECFN)和dlgetname(...)(如果存在)。如果可用且没有遇到权限问题,甚至可以在argv[0]基于方法之前尝试这些方法。在考虑到所有版本的所有系统时,如果它们存在且不失败,它们可能更可靠。
7. 可选:检查通过命令行参数传递的路径名。
8. 可选:检查由包装脚本明确传递的环境中的路径名(如果有)。
9. 可选:作为最后的手段,尝试环境变量"_"。它可能指向不同的程序,例如用户的shell。
10. 解析符号链接,可能存在多层。存在无限循环的可能性,但如果存在,您的程序可能不会被调用。
11. 通过解析"/foo/../bar/"之类的子字符串将文件名规范化为"/bar/"。请注意,这可能会改变含义,如果您跨越网络挂载点,则不一定是一件好事。在网络服务器上,符号链接中的".."可以用于在服务器上的上下文中遍历路径,而不是在客户端上。在这种情况下,您可能需要客户端上的上下文,因此规范化是可以的。还将"/./"转换为"/"以及"//"转换为"/"。在shell中,readlink --canonicalize将解析多个符号链接并规范化名称。Chase可能会执行类似的操作,但未安装。如果存在,realpath()或canonicalize_file_name()可能有所帮助。
12. 如果在编译时realpath()不存在,可以从具有宽松许可证的库分发中借用一份副本,并将其自己编译到代码中,而不是重新发明轮子。如果将使用小于PATH_MAX的缓冲区,则需要修复潜在的缓冲区溢出(将sizeof输出缓冲区作为参数传递,考虑strncpy()与strcpy()之间的区别)。也许只需使用重命名的私有副本而不是测试它是否存在,可能更容易。Android/darwin/bsd提供了宽松许可证副本:https://android.googlesource.com/platform/bionic/+/f077784/libc/upstream-freebsd/lib/libc/stdlib/realpath.c
13. 注意,多次尝试可能会成功或部分成功,它们可能不会指向相同的可执行文件,因此请考虑验证您的可执行文件;但是,如果您无法读取它,不要将其视为失败。或者验证与您的可执行文件相邻的某些内容,例如您要查找的"../lib/"目录。您可能有多个版本、打包和本地编译版本、本地和网络版本以及本地和USB驱动器便携版本等,有可能从不同的定位方法获取到两个不兼容的结果。并且"_"可能只是指向错误的程序。
14. 使用execve的程序可以故意将argv[0]设置为与用于加载程序的实际路径不兼容的值,并破坏PATH、"_"、pwd等。尽管通常没有太多理由这样做,但如果您的执行环境可以通过多种方式进行更改,包括但不限于此方式(chroot、fuse文件系统、硬链接等),则可能会有安全性问题。对于shell命令,可以设置PATH但未导出它。
15. 您不一定需要为非Unix系统编写代码,但最好了解一些特殊性,以便您可以以不那么难以移植的方式编写代码。请注意,某些系统(DEC VMS、DOS、URL等)可能具有以冒号结尾的驱动器名称或其他前缀,例如"C:"、"sys$drive:[foo]bar"和"file:///foo/bar/baz"。旧的DEC VMS系统使用"["和"]"来括起路径的目录部分,但如果您的程序在POSIX环境中编译,则可能已更改。某些系统(例如VMS)可以在路径末尾使用文件版本(由分号分隔)。某些系统使用两个连续的斜杠,例如"//drive/path/to/file"或"user:/path/to/file"(scp命令)或"file://hostname/path/to/file"(URL)。在某些情况下(DOS和Windows),PATH可能具有不同的分隔符字符——";"与":"以及""与"/"用作路径分隔符。在csh/tsh中,有"path"(使用空格分隔)和"PATH"(使用冒号分隔),但您的程序应该接收到PATH,因此无需担心路径。DOS和其他一些系统可以具有相对路径,以驱动器前缀开头。C:foo.exe是指当前目录中驱动器C中的foo.exe,因此您需要查找驱动器C上的当前目录并将其用于pwd。
16. 以上是一个示例,展示了在Ubuntu 12.04上以各种方式调用相同程序时的实际argv[0]值。并且是通过实际查找自己的程序进行了验证。
需要注意的是,代码中使用的strncpy()和strncat()是正确的,并且没有发生缓冲区溢出或未终止的情况。每个strncpy()/strncat()的使用都受到sizeof(buffer)的限制,这是有效的,然后将缓冲区的最后一个字符填充为零,覆盖了缓冲区的最后一个字符。然而,strncat()将大小参数错误地用作计数,并且可能会发生溢出,因为它是在缓冲区溢出攻击出现之前编写的。
在不使用/proc/self/exe的情况下找到当前可执行文件的路径是一个常见的问题。不同的操作系统有不同的接口可以解决这个问题。
在Mac OS X上,可以使用_NSGetExecutablePath()函数(man 3 dyld)来获取可执行文件的绝对路径。
在Linux上,可以使用readlink /proc/self/exe命令来获取可执行文件的路径。
在Solaris上,可以使用getexecname()函数来获取可执行文件的路径。
在FreeBSD上,可以使用sysctl CTL_KERN KERN_PROC KERN_PROC_PATHNAME -1命令来获取可执行文件的路径。如果系统上有procfs,还可以使用readlink /proc/curproc/file命令。
在NetBSD上,可以使用readlink /proc/curproc/exe命令来获取可执行文件的路径。
在DragonFly BSD上,可以使用readlink /proc/curproc/file命令来获取可执行文件的路径。
在Windows上,可以使用GetModuleFileName()函数并将hModule参数设置为NULL来获取可执行文件的路径。
此外,还有一些第三方库可以用于获取这些信息,例如whereami和Qt中的QCoreApplication::applicationFilePath()。
另一种可移植(但不太可靠)的方法是使用argv[0]。尽管调用程序可以将其设置为任何值,但按照惯例,它被设置为可执行文件的路径或使用$PATH找到的名称。
一些shell(包括bash和ksh)在执行可执行文件之前,会将环境变量"_"设置为可执行文件的完整路径。在这种情况下,可以使用getenv("_")来获取它。但是,这种方法不可靠,因为并不是所有的shell都会这样做,它可能被设置为任何值,或者在执行程序之前,父进程没有更改它。
需要注意的是,以上方法中的某些方法可能不适用于特定的操作系统版本或配置。
根据不同的操作系统,可以使用不同的接口或方法来获取当前可执行文件的路径。