/proc/self/exe 的使用是不可移植且不可靠的。在我的 Ubuntu 12.04 系统上,您必须是 root 才能阅读/遵循符号链接。这将使 Boost 示例和发布的 whereami() 解决方案失败。
这篇文章很长,但讨论了实际问题并提供了与测试套件验证一起实际工作的代码。
找到您的程序的最佳方法是追溯系统使用的相同步骤。这是通过使用argv[0] 解决文件系统根目录、密码、路径环境并考虑符号链接和路径名规范化来完成的。这是凭记忆,但我过去成功地做到了这一点,并在各种不同的情况下对其进行了测试。它不能保证有效,但如果不能,您可能会遇到更大的问题,并且总体上它比讨论的任何其他方法都更可靠。在 Unix 兼容系统上存在这样的情况,在这种情况下,正确处理 argv[0] 不会让您进入您的程序,但是您正在一个可证明损坏的环境中执行。它也相当可移植到自 1970 年左右以来的所有 Unix 派生系统,甚至一些非 Unix 派生系统,因为它基本上依赖于 libc() 标准功能和标准命令行功能。它应该适用于 Linux(所有版本)、Android、Chrome OS、Minix、原始贝尔实验室 Unix、FreeBSD、NetBSD、OpenBSD、BSDxx、SunOS、Solaris、SYSV、@ 987654327@、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 — 相对于密码
-
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() 代替。但是,它可能会比实际用户更早地在路径上找到无法访问的程序。