以编程方式获取OS X命令行应用程序的绝对路径

29

在Linux上,应用程序可以通过查询/proc/self/exe轻松获取其绝对路径。 在FreeBSD上,这更加复杂,因为您必须构建一个sysctl调用:

int mib[4];
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = KERN_PROC_PATHNAME;
mib[3] = -1;
char buf[1024];
size_t cb = sizeof(buf);
sysctl(mib, 4, buf, &cb, NULL, 0);

不过这个完全可以做到。但是我无法找到一种方法在OS X上确定命令行应用程序的路径。如果您是从应用程序包内运行,可以通过运行[[NSBundle mainBundle] bundlePath]来确定它,但是因为命令行应用程序不在包中,这并没有帮助。

(注意:查询argv[0]并不是一个合理的答案,因为如果从符号链接启动,则argv[0]将是该符号链接,而不是调用可执行文件的最终路径。如果愚蠢的应用程序使用exec()调用并忘记正确初始化argv,argv[0]也可能会误导,我曾经遇到过这种情况。)


1
读取argv [0]是解决方案,这个主题中还没有任何东西能说服我。 - bortzmeyer
13
考虑一下 execl("/home/hacker/.hidden/malicious", "/bin/ls", "-s", (char *)0); - 'argv[0]' 的值是 "/bin/ls",但这与可执行文件的名称无关。 - Jonathan Leffler
7个回答

63
函数_NSGetExecutablePath将返回可执行文件(GUI或非GUI)的完整路径。该路径可能包含符号链接,".."等内容,但如果需要,可以使用realpath函数来清理它们。有关更多信息,请参见man 3 dyld
char path[1024];
uint32_t size = sizeof(path);
if (_NSGetExecutablePath(path, &size) == 0)
    printf("executable path is %s\n", path);
else
    printf("buffer too small; need size %u\n", size);
这个函数的秘密在于,当Darwin内核创建进程时,会将可执行文件路径放在envp数组之后立即放入进程堆栈中。动态链接器dyld在初始化时会抓取这个指针并保留它。该函数利用该指针。

最后一行不是有点儿问题吗? - ioquatix
如果成功,它返回0;如果缓冲区不够大,则返回-1,并将“size”填入所需的大小。在这种情况下,您可以使用“malloc()”分配一个缓冲区。 - mark4o
所以,这相当于声明一个带有四个参数的主函数 - 第四个参数将包含安全的argv [0]。 - Nicholas Wilson
2
返回的路径是否以空字符结尾? - mic_e
1
@mic_e:是的,如果返回值为0,则路径将以 null 结尾。如果缓冲区太小,则返回-1且路径缓冲区不会被修改。 - mark4o
2
#include <sys/syslimits.h> char path[PATH_MAX+1]; // 最好 - Devon

36

我认为有更加优雅的解决方案,可以适用于任何PID,并且直接返回绝对路径:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <libproc.h>

int main (int argc, char* argv[])
{
    int ret;
    pid_t pid; 
    char pathbuf[PROC_PIDPATHINFO_MAXSIZE];

    pid = getpid();
    ret = proc_pidpath (pid, pathbuf, sizeof(pathbuf));
    if ( ret <= 0 ) {
        fprintf(stderr, "PID %d: proc_pidpath ();\n", pid);
        fprintf(stderr, "    %s\n", strerror(errno));
    } else {
        printf("proc %d: %s\n", pid, pathbuf);
    }

    return 0;
}

4
谢谢!不过有一件事,使用OS X 10.8.5,如果没有加上 #include <unistd.h>,这个方法对我来说是无效的。 - original_username
我能得到的最好解决方案。非常干净的工作! - Pal
这意味着 _NSGetExecutablePath 并不适用于所有的 PIDs - 你能详细说明一下吗?我在其他地方找不到任何关于这个的参考资料。 - hpm
@hyperum,_NSGetExecutablePath对PID也适用吗?如果是的话,您能否提供一个示例来进一步改进已接受的答案? - tresf
@hpm 或许 Alen 只是在指出 proc_pidpath 可以被调用从任意进程检索关于一个任意进程的信息(因为它有一个 pid 参数),而 _NSGetExecutablePath 只提供有关当前进程的信息。 - Burcea Bogdan Madalin

4

看起来答案是你不能这样做:

我想实现类似lsof的功能,并收集有关运行进程的大量统计信息和信息。如果lsof不那么慢,我会很高兴坚持使用它。
如果重新实现lsof,您会发现它很慢,因为它要做很多工作。
我猜这不是因为lsof是用户模式,而是因为它必须扫描任务的地址空间,寻找由外部页面支持的内容。当我在内核中时,有没有更快的方法来做到这一点?
不。 lsof并不愚蠢;它正在做它必须做的事情。如果您只想要其功能的子集,则可以考虑从可用的lsof源代码开始,并将其裁剪以满足您的要求。
出于好奇, p_textvp 是否被使用?看起来它在中设置为父级的 p_textvp (然后被释放??),但在任何例程中都没有被触及。 p_textvp 未被使用。在Darwin中,proc不是地址空间的根源;任务是。对于任务的地址空间,没有“vnode”的概念,因为它不一定是通过映射一个vnode来最初填充的。
如果exec要填充p_textvp,它将迎合所有进程都支持vnode的假设。然后,程序员会假定可以获取到vnode的路径,从那里跳转到当前路径是启动它的路径的假设,并且对字符串进行文本处理可能会导致应用程序包名称...所有这些都无法保证而需要付出巨大的代价。 - Mike Smith,Darwin Drivers邮件列表

我非常非常讨厌接受那些回答:“你不能这样做”,但是那句话确实让我的问题痛苦地被否定了。 - Benjamin Pollack
1
是的,我也不喜欢给答案。我花了一段时间在一场徒劳的追逐中,试图看看是否可以找出如何从p_textvp中获取信息,直到我发现这个。 - Brian Campbell

3
这可能有点晚了,但是[[NSBundle mainBundle] executablePath]对于非捆绑的命令行程序也可以很好地工作。

2

我认为没有一种保证的方法。 如果 argv[0] 是一个符号链接,那么可以使用 readlink()。 如果通过 $PATH 执行命令,则可以尝试以下方法之一:search(getenv("PATH"))、getenv("_")、dladdr()


这将涵盖许多情况,但仍然无法处理由应用程序启动的情况,该应用程序未正确初始化argv [0] - 从个人经验来看,这适用于相当多的应用程序。 - Benjamin Pollack
你能举个这样的应用程序的例子吗?不是应用程序初始化argv,而是libc,应用程序需要做一些非常特殊的事情才能混淆argv [0]。 - bortzmeyer
3
我无法举出一个应用程序的例子,该应用程序在调用exec*函数时忘记正确设置argv[0]就可能出现问题。只有当通过system()调用应用程序时,libc才会介入。请注意,我没有改变原文的意思。 - Benjamin Pollack

0

为什么不直接使用realpath(argv[0], actualpath);呢?确实,realpath有一些限制(在手册页面中有记录),但它可以很好地处理符号链接。在FreeBSD和Linux上测试过了。

    % ls -l foobar 
    lrwxr-xr-x  1 bortzmeyer  bortzmeyer  22 Apr 29 07:39 foobar -> /tmp/get-real-name-exe

    % ./foobar 
    My real path: /tmp/get-real-name-exe
#include <limits.h>
#include <stdlib.h>
#include <stdio.h>
#include <libgen.h>
#include <string.h>
#include <sys/stat.h>

int
main(argc, argv)
    int             argc;
    char          **argv;
{
    char            actualpath[PATH_MAX + 1];

    if (argc > 1) {
        fprintf(stderr, "Usage: %s\n", argv[0]);
        exit(1);
    }
    realpath(argv[0], actualpath);
    fprintf(stdout, "My real path: %s\n", actualpath);
    exit(0);
}

如果程序是通过 PATH 启动的,请参考 pixelbeat 的解决方案。


当一个愚蠢的程序通过exec*调用并不正确地初始化argv结构,以至于argv[0]只是可执行文件名(即不是完整路径)或者根本就是错误的(缺失、空字符串等),这种情况就会失败。 - Benjamin Pollack
1
如果argv [0]不是完整路径,那没问题,realpath()会处理它。如果它为空或为NULL,则这是调用者的错误,而不是我的程序的错误 :-) - bortzmeyer
@BenjaminPollack:如果可执行文件在$PATH中,前者绝对没有任何不当之处! - SamB

0

只要您的应用程序是通过Finder启动并链接到Carbon的GUI应用程序,它就可以工作。在这种情况下,[[NSBundle mainBundle] bundlePath]也可以工作,并避免创建FSRef和查找进程序列号。 - Benjamin Pollack
@BenjaminPollack [[NSBundle mainBundle] bundlePath] 对于那些只是单个二进制文件但链接到 Foundation 的非 UI 应用程序也适用。如果您的应用程序仅链接到 CoreFoundation,则可以使用 CFBundle。所有 bundle 方法也适用于根本不是 bundle 的纯二进制文件,尽管它们可能不总是返回有用的信息,但它们可用于获取二进制文件的可执行路径。 - Mecki

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