Как получить путь к исполняемому файлу в C

Программа запускается, но не находит свои конфигурационные файлы, потому что текущий рабочий каталог не совпадает с каталогом, где лежит сам исполняемый файл — типичная ситуация, из-за которой в C требуется получить путь к исполняемому файлу. В зависимости от способа запуска (из терминала, по двойному клику, через планировщик задач или как служба) рабочий каталог может оказаться совершенно другим, и относительные пути перестают работать.

Универсального способа, зафиксированного в стандарте языка C, не существует: поведение зависит от операционной системы. В этой статье разберём рабочие варианты для Linux, Windows и macOS, ограничения argv[0] и типичные ошибки при обработке полученного пути.

Почему argv[0] — ненадёжный источник пути

Первое, что приходит в голову, — использовать argv[0], первый аргумент командной строки. Действительно, при запуске ./myapp или /home/user/bin/myapp этот параметр содержит строку, с которой программа была вызвана. Однако стандарт C не гарантирует, что argv[0] содержит полный путь или вообще осмысленное значение.

Проблемы проявляются в нескольких сценариях. Если программа найдена через переменную окружения PATH, в argv[0] окажется только имя файла без каталога. При запуске через exec() вызывающий процесс может передать в argv[0] произвольную строку, не имеющую отношения к реальному расположению файла. Символические ссылки тоже вносят путаницу: вы получите путь к ссылке, а не к целевому файлу.

  • 📂 Относительный путь — если программа запущена как ./app, вам придётся комбинировать значение с текущим каталогом.
  • 🔗 Символическая ссылка — argv[0] указывает на ссылку, а не на реальный бинарник.
  • 🛠 Подмена при exec — родительский процесс вправе записать в argv[0] что угодно.
  • ❓ Пустая строка — стандарт допускает argv[0] равным пустой строке или NULL.
⚠️ Внимание: не используйте argv[0] как единственный источник пути в программах, где от этого зависит загрузка ресурсов или безопасность. Это значение контролируется вызывающей стороной и может быть подделано.

Linux: чтение /proc/self/exe

В Linux самый надёжный способ — прочитать символическую ссылку /proc/self/exe, которую ядро создаёт для каждого процесса. Она всегда указывает на реальный исполняемый файл, независимо от того, как программа была запущена. Для чтения используется системный вызов readlink().

#include <stdio.h>

#include <unistd.h>

#include <limits.h>

int main(void) {

char path[PATH_MAX];

ssize_t len = readlink("/proc/self/exe", path, sizeof(path) - 1);

if (len == -1) {

perror("readlink");

return 1;

}

path[len] = '\0';

printf("Путь к исполняемому файлу: %s\n", path);

return 0;

}

Обратите внимание на важную деталь: readlink() не добавляет завершающий нулевой символ, поэтому буфер заполняется на один байт меньше, а '\0' ставится вручную. Размера PATH_MAX обычно достаточно, хотя теоретически пути в Linux могут быть длиннее — для строгого кода проверяйте, не равна ли длина размеру буфера минус один.

Если программа была удалена после запуска, ядро допишет к пути суффикс " (deleted)". В этом случае файл физически продолжает существовать, пока процесс держит его открытым, но по указанному пути его уже нет.

Windows: функция GetModuleFileName

В Windows аналогичную задачу решает функция WinAPI GetModuleFileName(). Передав NULL в качестве первого параметра, вы запросите путь к исполняемому файлу текущего процесса.

#include <windows.h>

#include <stdio.h>

int main(void) {

char path[MAX_PATH];

DWORD len = GetModuleFileNameA(NULL, path, MAX_PATH);

if (len == 0 || len == MAX_PATH) {

fprintf(stderr, "Не удалось получить путь\n");

return 1;

}

printf("Путь: %s\n", path);

return 0;

}

Функция возвращает полный абсолютный путь с буквой диска. Если возвращённая длина равна размеру буфера, путь мог быть усечён — это стоит проверять. Для программ, работающих с длинными путями или Unicode-именами, используйте GetModuleFileNameW() с буфером wchar_t и динамическим выделением памяти при необходимости.

⚠️ Внимание: в Windows разделителем каталогов является обратный слэш \, а в строковых литералах C его нужно экранировать. При обработке пути не забывайте, что последний разделитель ищется по символу '\\'.
📊 На какой платформе вы чаще всего разрабатываете на C?
Linux
Windows
macOS
Кроссплатформенно

macOS: функция _NSGetExecutablePath

В macOS применяется функция _NSGetExecutablePath() из заголовка mach-o/dyld.h. Она записывает путь в буфер, а если буфер слишком мал — возвращает -1 и записывает требуемый размер в параметр bufsize.

#include <stdio.h>

#include <stdlib.h>

#include <mach-o/dyld.h>

int main(void) {

uint32_t size = 0;

_NSGetExecutablePath(NULL, &size);

char *path = malloc(size);

if (_NSGetExecutablePath(path, &size) == 0)

printf("Путь: %s\n", path);

free(path);

return 0;

}

Двухпроходный вызов (сначала узнать размер, потом выделить буфер) делает код устойчивым к путям любой длины. Учтите, что функция может вернуть путь, содержащий символические ссылки и компоненты ..; для канонизации применяйте realpath().

Сравнение методов по платформам

ПлатформаМеханизмЗаголовокОсобенности
Linuxreadlink("/proc/self/exe")unistd.hНе добавляет '\0'; суффикс " (deleted)" при удалённом файле
WindowsGetModuleFileName()windows.hАбсолютный путь; проверка на усечение буфера
macOS_NSGetExecutablePath()mach-o/dyld.hМожет содержать ссылки и ".."; нужен realpath
Любаяargv[0]Ненадёжен, контролируется вызывающей стороной

Для кроссплатформенного проекта удобно обернуть все три варианта в одну функцию с условной компиляцией через #ifdef _WIN32, #elif __APPLE__ и #elif __linux__. Так вы получите единый интерфейс вроде get_executable_path(char *buf, size_t size), а платформенные детали останутся скрыты внутри реализации.

Получение каталога программы из полного пути

Чаще всего нужен не сам файл, а каталог, в котором он лежит, — например, чтобы искать рядом ресурсы. Для этого найдите последний разделитель пути и обрежьте строку. Вот кроссплатформенный приём:

char *sep = strrchr(path, '/');

#ifdef _WIN32

char *bsep = strrchr(path, '\\');

if (bsep && (!sep || bsep > sep)) sep = bsep;

#endif

if (sep) *sep = '\0'; // теперь path — каталог программы

В Linux также доступна функция dirname() из libgen.h, но она может изменять переданную строку и в некоторых реализациях возвращает указатель на статический буфер — копируйте путь перед вызовом.

☑️ Проверка корректности полученного пути

Выполнено: 0 / 5

Типичные ошибки и их диагностика

На практике проблемы возникают не с получением пути, а с его дальнейшей обработкой. Разберём частые случаи.

  • 🐛 Переполнение буфера — всегда передавайте реальный размер буфера и проверяйте возвращаемое значение.
  • 🧵 Отсутствие '\0' после readlink — строка без терминатора приводит к чтению мусора и неопределённому поведению.
  • 📁 Путаница с рабочим каталогом — getcwd() возвращает текущий каталог, который не обязан совпадать с каталогом программы; не смешивайте эти понятия.
  • 🔗 Неразрешённые ссылки — если пользователь запускает программу через симлинк из /usr/local/bin, решите, что вам нужно: путь к ссылке или к реальному файлу. В Linux /proc/self/exe всегда указывает на реальный файл, а argv[0] — на ссылку.

Если программа «не видит» свои файлы после того, как вы начали строить пути от каталога исполняемого файла, выведите полученный путь в лог или на экран и сверьте с фактическим расположением. Это простое действие сразу показывает, где логика расходится с реальностью.

Почему не стоит хранить ресурсы рядом с бинарником в Linux

В Linux принято устанавливать ресурсы в /usr/share/имя_программы согласно FHS, а не рядом с исполняемым файлом. Путь относительно бинарника удобен для портативных сборок, но для системных пакетов корректнее использовать префикс установки, заданный на этапе компиляции.

FAQ: частые вопросы

Можно ли получить путь к исполняемому файлу стандартными средствами C?

Нет. Стандарт языка C (C11, C17) не предоставляет такой функции. Необходимо использовать API конкретной операционной системы: readlink("/proc/self/exe") в Linux, GetModuleFileName() в Windows, _NSGetExecutablePath() в macOS.

Чем отличается путь к исполняемому файлу от текущего рабочего каталога?

Рабочий каталог — это каталог, относительно которого разрешаются относительные пути процесса; его возвращает getcwd(), и он может меняться вызовом chdir(). Путь к исполняемому файлу фиксирован с момента запуска и указывает на сам бинарник. Эти два значения совпадают лишь в частном случае.

Что произойдёт, если исполняемый файл удалить во время работы программы?

Процесс продолжит работу: ядро удерживает файл, пока он используется. В Linux /proc/self/exe при этом покажет путь с суффиксом " (deleted)". В Windows удалить запущенный exe-файл обычно не даёт система.

Как получить путь к DLL или .so, а не к главному exe?

В Windows передайте в GetModuleFileName() дескриптор нужного модуля вместо NULL — его можно получить через GetModuleHandle(). В Linux используйте dladdr() с адресом любой функции из библиотеки.

Безопасно ли доверять argv[0] для построения путей?

Нет. Значение argv[0] задаёт вызывающая сторона и может быть произвольным. Для загрузки ресурсов или проверок безопасности опирайтесь на системные механизмы получения пути, а argv[0] используйте разве что как запасной вариант с проверкой существования файла.