Мне кажется, что Linux легко справляется с /proc/self/exe. Но я хотел бы знать, есть ли удобный способ найти текущий каталог приложений в C/С++ с кросс-платформенными интерфейсами. Я видел некоторые проекты, сбрасывающие с argv [0], но это не кажется полностью надежным.
Если вам когда-либо приходилось поддерживать, скажем, Mac OS X, у которой нет /proc/, что бы вы сделали? Использовать #ifdefs для выделения кода для конкретной платформы (например, NSBundle)? Или попытайтесь вывести исполняемый путь из argv [0], $PATH и еще чего, рискуя найти ошибки в крайних случаях?
Ответ 2
Использование /proc/self/exe
является не переносным и ненадежным. В моей системе Ubuntu 12.04 вы должны быть root, чтобы читать/следовать символической ссылке. Это приведет к примеру Boost и, вероятно, решения, связанные с whereami()
не будут выполнены.
Этот пост очень длинный, но обсуждает актуальные проблемы и представляет код, который фактически работает вместе с проверкой на тестовый набор.
Лучший способ найти вашу программу - это повторить те же действия, которые использует система. Это делается с помощью argv[0]
разрешенного для корневой файловой системы, pwd, среды пути и рассмотрения символических ссылок и канонизации. Это из памяти, но я сделал это в прошлом успешно и протестировал его в самых разных ситуациях. Не гарантируется работа, но если это не так, у вас, вероятно, есть гораздо большие проблемы, и она более надежна в целом, чем любые другие обсуждаемые методы. В системе, совместимой с Unix, есть ситуации, когда правильная обработка argv[0]
не приведет вас к вашей программе, но затем вы выполняете ее в сертифицированной сломанной среде. Он также довольно портативен для всех производных систем Unix с 1970 года и даже некоторых не-Unix-производных систем, поскольку он в основном полагается на стандартные функции libc() и стандартную функциональность командной строки. Он должен работать на Linux (все версии), Android, Chrome OS, Minix, оригинальные Bell Labs Unix, FreeBSD, NetBSD, OpenBSD, BSD xx, SunOS, Solaris, SYSV, HPUX, Concentrix, SCO, Darwin, AIX, OS X, Nextstep и т.д. И с небольшой модификацией, вероятно, VMS, VM/CMS, DOS/Windows, ReactOS, OS/2 и т.д. Если программа запускалась непосредственно из среды GUI, она должна была установить argv[0]
на абсолютную дорожка.
Поймите, что почти каждая оболочка на каждой совместимой с Unix операционной системой, которая когда-либо была выпущена, в основном находит программы одинаково и настраивает рабочую среду почти одинаково (с некоторыми дополнительными дополнительными функциями). Ожидается, что любая другая программа, запускающая программу, создаст ту же среду (argv, строки среды и т.д.) Для этой программы, как если бы она была запущена из оболочки с некоторыми необязательными дополнительными функциями. Программа или пользователь могут настроить среду, которая отличается от этого соглашения для других подчиненных программ, которые он запускает, но если это так, это ошибка, и у программы нет разумного ожидания, что подчиненная программа или ее подчиненные будут работать правильно.
Возможные значения argv[0]
включают:
-
/path/to/executable
- абсолютный путь -
../bin/executable
- относительно pwd -
bin/executable
- относительно pwd -
./foo
- относительно pwd -
executable
- basename, найти в пути -
bin//executable
- относительно pwd, неканонический -
src/../bin/executable
- относительно pwd, неканоническое, обратное отслеживание -
bin/./echoargc
- относительно pwd, неканоническое
Значения, которые вы не должны видеть:
-
~/bin/executable
- перезаписано до запуска вашей программы. -
~user/bin/executable
- перезаписано до запуска вашей программы -
alias
- переписан до запуска вашей программы -
$shellvariable
- перезаписано до $shellvariable
вашей программы -
*foo*
- подстановочный знак, переписанный до запуска вашей программы, не очень полезный -
?foo?
- подстановочный знак, переписанный до запуска вашей программы, не очень полезный
Кроме того, они могут содержать неканонические имена путей и несколько уровней символических ссылок. В некоторых случаях может быть несколько жестких ссылок на одну и ту же программу. Например, /bin/ls
, /bin/ps
, /bin/chmod
, /bin/rm
и т.д. Могут быть жесткими ссылками на /bin/busybox
.
Чтобы найти себя, выполните следующие действия:
-
Сохраните pwd, PATH и argv [0] при входе в вашу программу (или инициализации вашей библиотеки), поскольку они могут измениться позже.
-
Необязательно: особенно для систем, отличных от Unix, отдельно, но не отбрасывайте часть префикса хоста/пользователя/привода pathname, если они есть; часть, которая часто предшествует двоеточию или следует за начальным "//".
-
Если argv[0]
является абсолютным путем, используйте это как отправную точку. Абсолютный путь, вероятно, начинается с "/", но в некоторых не-Unix-системах он может начинаться с "\" или префикса буквы диска или имени, за которым следует двоеточие.
-
Else, если argv[0]
- относительный путь (содержит "/" или "\", но не начинается с него, например "../../bin/foo", а затем объединить pwd+ "/" [ CN00] [0] (используйте текущую рабочую директорию с момента запуска программы, а не по текущему).
-
Else, если argv [0] - это простое базовое имя (без косых черт), затем по очереди объединяйте его с каждой записью в переменной среды PATH и попробуйте те и используйте первую, которая будет успешной.
-
Необязательно: просто попробуйте использовать определенную платформу /proc/self/exe
, /proc/curproc/file
(BSD) и (char *)getauxval(AT_EXECFN)
и dlgetname(...)
если они есть. Вы даже можете попробовать их до использования методов argv[0]
-based, если они доступны, и вы не сталкиваетесь с проблемами разрешения. В несколько маловероятном случае (когда вы рассматриваете все версии всех систем), что они присутствуют и не терпят неудачу, они могут быть более авторитетными.
-
Необязательно: проверьте имя пути, переданное с использованием параметра командной строки.
-
Необязательно: проверьте имя пути в среде, явно переданной вашим сценарием оболочки, если таковой имеется.
-
Необязательно: в качестве последней меры попробуйте переменную среды "_". Это может указывать на другую программу целиком, такую как оболочка пользователей.
-
Разрешить символические ссылки, может быть несколько уровней. Существует возможность бесконечных циклов, хотя, если они существуют, ваша программа, вероятно, не будет вызвана.
-
Канонизировать имя файла, разрешив подстроки типа "/foo/../bar/" на "/bar/". Обратите внимание, что это может потенциально изменить значение, если вы пересекаете точку подключения к сети, поэтому канонизация не всегда хороша. На сетевом сервере ".." в symlink может использоваться для перемещения по пути к другому файлу в контексте сервера, а не на клиенте. В этом случае вам, вероятно, нужен клиентский контекст, поэтому канонизация становится ок. Также преобразуйте шаблоны типа "/./" в "/" и "//" в "/". В оболочке readlink --canonicalize
разрешает множественные символические readlink --canonicalize
и имя canonicalize. Чейз может делать подобное, но не установлен. realpath()
или canonicalize_file_name()
, если они есть, могут помочь.
Если realpath()
не существует во время компиляции, вы можете заимствовать копию из лицензированного дистрибутива библиотеки и скомпилировать ее в себе, а не изобретать колесо. Исправьте переполнение потенциального буфера (перейдите в размерный буфер вывода, подумайте, что strncpy() vs strcpy()), если вы будете использовать буфер меньше PATH_MAX. Может быть проще просто использовать переименованную частную копию, а не тестировать ее, если она существует. Разрешительная копия лицензии с 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 файловая система, жесткие ссылки и т.д.) Возможно для команд оболочки для установки PATH, но не экспортировать их.
Вам не обязательно нужно кодировать системы, отличные от Unix, но было бы неплохо знать некоторые особенности, чтобы вы могли написать код таким образом, что кому-то не так сложно переносить позже, Имейте в виду, что некоторые системы (DEC VMS, DOS, URL и т.д.) Могут иметь имена дисков или другие префиксы, которые заканчиваются двоеточием, таким как "C: \", "sys $ drive: [foo] bar" и "file :///Foo/бар/Баз". В старых системах DEC VMS используются "[" и "]", чтобы заключить часть каталога в путь, хотя это может быть изменено, если ваша программа скомпилирована в среде POSIX. Некоторые системы, такие как VMS, могут иметь версию файла (разделенные точкой с запятой в конце). Некоторые системы используют две последовательные косые черты, как в "//drive/path/to/file" или "user @host: /path/to/file" (команда scp) или "file://hostname/path/to/file", (URL). В некоторых случаях (DOS, windoze) PATH может иметь разные разделительные символы - ";" vs ":" и "\" против "/" для разделителя путей. В csh/tsh есть "путь" (ограниченный пробелами) и "PATH", разделенный двоеточиями, но ваша программа должна получать PATH, поэтому вам не нужно беспокоиться о пути. DOS и некоторые другие системы могут иметь относительные пути, начинающиеся с префикса диска. C: foo.exe относится к foo.exe в текущем каталоге на диске C, поэтому вам нужно искать текущий каталог на 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
Обратите внимание, что пользовательский счет отправил ссылку выше на программу в HP, которая обрабатывает три основных случая argv[0]
. Однако для этого нужны некоторые изменения:
- Необходимо будет переписать все
strcat()
и strcpy()
для использования strncat()
и strncpy()
. Несмотря на то, что переменные объявлены длиной PATHMAX, входное значение длины PATHMAX-1 плюс длина конкатенированных строк> PATHMAX, а входное значение длины PATHMAX будет прекращено. - Его нужно переписать как библиотечную функцию, а не просто распечатывать результаты.
- Он не может канонизировать имена (используйте код realpath, с которым я связан выше)
- Он не может разрешить символические ссылки (используйте код realpath)
Итак, если вы объедините как код HP, так и код реального пути и исправьте оба, чтобы быть стойкими к переполнениям буфера, тогда у вас должно быть что-то, что может правильно интерпретировать argv[0]
.
Ниже показаны фактические значения argv[0]
для различных способов вызова одной и той же программы на Ubuntu 12.04. И да, программа была случайно названа echoargc вместо echoargv. Это было сделано с использованием сценария для чистого копирования, но его выполнение вручную в оболочке получает одинаковые результаты (кроме псевдонимов не работает в скрипте, если вы явно не разрешаете их).
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
Эти примеры иллюстрируют, что методы, описанные в этом сообщении, должны работать в самых разных условиях и почему некоторые из этапов необходимы.
EDIT: теперь программа, которая печатает 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-запуска также правильно найдут программу.
Существует одна потенциальная ловушка. Функция access()
отбрасывает разрешения, если программа установлена перед тестированием. Если есть ситуация, когда программа может быть найдена как пользователь с повышенными правами, но не как обычный пользователь, тогда может возникнуть ситуация, когда эти тесты потерпят неудачу, хотя маловероятно, что программа действительно может быть выполнена в этих обстоятельствах. Вместо этого можно использовать euidaccess(). Возможно, однако, что он может найти недоступную программу ранее на пути, чем может сделать пользователь.