Как операционная система определяет, что файл можно запускать

Когда вы нажимаете Enter после имени файла в терминале или дважды кликаете по иконке, операционная система за доли секунды принимает решение: запустить этот файл как программу или отказать с ошибкой. Решение основывается не на одном признаке, а на комбинации метаданных файловой системы, внутренней структуры файла и настроек среды. Понимание этого механизма помогает отлаживать ошибки «Permission denied», «Exec format error», создавать переносимые скрипты и настраивать политики безопасности.

Два фундаментальных подхода: права доступа и формат файла

Любая современная ОС разделяет проверку на два этапа. Первый — «может ли этот процесс запускать файлы вообще и этот файл в частности». Второй — «понимает ли ядро формат этого файла и как его загрузить». Если первый этап проходит, а второй проваливается, вы получаете ошибку формата. Если проваливается первый — ошибку прав доступа.

В семействе Unix (Linux, macOS, *BSD) решающую роль играют биты прав доступа в inode файла. В Windows — расширение имени файла и ассоциации в реестре. Но обе системы дополнительно проверяют внутреннюю структуру файла: «магические числа» в заголовке, которые сообщают загрузчику ядра, как именно интерпретировать байты на диске.

Unix-подход: биты исполнения и shebang

Биты прав: user, group, other

В файловой системе ext4, xfs, apfs и др. каждый файл хранит набор битов режима (mode). Три бита исполнения — x для владельца, группы и остальных — определяют, может ли процесс с соответствующими UID/GID вызвать execve() над этим файлом. Без хотя бы одного установленного бита x системный вызов вернёт EACCES (Permission denied), даже если файл содержит идеально валидный машинный код.

Практически: chmod +x script.sh устанавливает биты исполнения для всех трех классов. chmod u+x script.sh — только для владельца. На смонтированных файловых системах без поддержки прав (FAT32, exFAT, NTFS без правильного маппинга) биты могут игнорироваться или эмулироваться опциями монтирования umask, fmask, dmask.

Shebang: как ядро находит интерпретатор

Если файл имеет биты исполнения, но не является нативным бинарным форматом (ELF, Mach-O), ядро считывает первые байты. Если они начинаются с #! (shebang), ядро парсит остаток первой строки как путь к интерпретатору и необязательный аргумент. Затем ядро запускает интерпретатор, передавая ему путь к исходному скрипту как аргумент.

  • #!/bin/bash — запуск через bash.
  • #!/usr/bin/env python3 — поиск python3 в PATH (переносивее).
  • #!/bin/sh -e — sh с флагом -e (выход при ошибке).

Ограничения: длина строки shebang ограничена (обычно 128 байт, включая #! и перевод строки). Путь к интерпретатору должен быть абсолютным (кроме варианта с env). Ядро не раскрывает переменные окружения, тильду или подстановочные символы в shebang.

ELF: нативный формат Linux и *BSD

Если первых четыре байта файла — 0x7F ‘E’ ‘L’ ‘F’ (магическое число \x7fELF), ядро распознаёт формат ELF (Executable and Linkable Format). Далее ядро проверяет класс (32/64 бит), эндианность, версию ABI, тип файла (ET_EXEC — исполняемый, ET_DYN — общая библиотека/PIE), архитектуру машины (x86_64, aarch64, riscv64 и др.). Несоответствие архитектуры даст «Exec format error» даже при наличии бит x.

Современные Linux-дистрибутивы используют PIE (Position Independent Executable) — тип ET_DYN с точкой входа, который ядро загружает по случайному адресу (ASLR). Статические бинарники (без динамических зависимостей) запускаются напрямую. Динамические требуют наличия интерпретатора (динамического линкера) — обычно /lib64/ld-linux-x86-64.so.2 — путь к которому записан в секции .interp ELF-файла. Если линкер отсутствует или несовместим, запуск упадёт до передачи управления программе.

Windows: расширения, ассоциации и PE

Расширения и PATHEXT

Windows не использует биты исполнения в файловой системе NTFS для решения «запускать или нет». Вместо этого оболочка (Explorer, cmd.exe, PowerShell) смотрит на расширение имени файла. Список «исполняемых» расширений хранится в переменной окружения PATHEXT (по умолчанию: .COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC и др.). Если расширение есть в PATHEXT, оболочка пытается запустить файл через CreateProcess.

Для скриптов (.bat, .cmd, .ps1, .py) запуск делегируется зарегистрированному обработчику через ассоциации файлов в реестре (HKEY_CLASSES_ROOT\.ext → HKEY_CLASSES_ROOT\progid\shell\open\command). Это почему script.py запускается через python.exe, если ассоциация настроена правильно.

PE/COFF: нативный формат Windows

Нативные исполняемые файлы Windows имеют формат PE (Portable Executable) на базе COFF. Магическое число — MZ (DOS-заголовок), за которым по смещению 0x3C следует сигнатура PE\0\0. Загрузчик ядра (ntoskrnl.exe) проверяет заголовок PE: подсистему (GUI, CUI, POSIX), архитектуру (x86, x64, ARM64), наличие манифеста, цифровую подпись (если включена политика Driver Signature Enforcement или AppLocker).

64-битный Windows не запускает 16-битные PE (NE/LE) и DOS MZ-исполняемые напрямую — для этого нужен эмулятор (NTVDM, отключен по умолчанию в x64). 32-битные PE запускаются под WoW64.

macOS: Mach-O, кодовая подпись и Gatekeeper

macOS использует формат Mach-O. Магическое число: 0xFEEDFACE (32-бит) или 0xFEEDFACF (64-бит) для little-endian; 0xCAFEBABE — для универсальных (fat) бинарников, содержащих несколько архитектур. Ядро (XNU) проверяет load commands: LC_MAIN (точка входа), LC_LOAD_DYLIB (зависимости), LC_CODE_SIGNATURE (смещение подписи).

Ключевое отличие: обязательная кодовая подпись (code signing) для запуска на Apple Silicon (arm64) и для любых приложений, прошедших нотаризацию. Gatekeeper проверяет подпись и нотаризацию при первом запуске скачанного файла (атрибут com.apple.quarantine). Неподписанный бинарник запустится только если пользователь явно разрешит в «System Settings → Privacy & Security» или через xattr -d com.apple.quarantine file и codesign —force —sign — file (ad-hoc подпись).

Shebang в macOS работает так же, как в Linux, но системные интерпретаторы (/bin/bash, /usr/bin/python3) могут быть старыми версиями; рекомендуется /usr/bin/env или Homebrew-пути.

Файловые системы без прав исполнения: FAT32, exFAT, сетевые шары

На файловых системах без поддержки битов режима (FAT32, exFAT, SMB/CIFS, NFS с certain exports) ОС эмулирует права через опции монтирования:

  • Linux: mount -o uid=1000,gid=1000,fmask=0133,dmask=0022 — файлы получают биты x в зависимости от fmask (обычно файлы не получают x по умолчанию для безопасности).
  • macOS: аналогично через mount_msdos или опции SMB.
  • Windows: NTFS на Linux через ntfs-3g или kernel ntfs3 использует маппинг Windows ACL → POSIX mode; по умолчанию файлы могут получать x если у них нет ACL «deny execute».

Практический вывод: если скрипт с флешки (exFAT) не запускается — проверьте ls -l и опции монтирования. Часто помогает chmod +x (если ФС позволяет) или копирование на нативную ФС.

Механизм загрузки: от системного вызова до точки входа

Обобщённый алгоритм (упрощённый) для execve(path, argv, envp):

  1. Проверка прав доступа к inode: есть ли бит x для effective UID/GID процесса (Unix) или разрешена ли политика AppLocker/WDAC (Windows).
  2. Чтение первых байт файла (обычно 128–4096 байт) для определения формата.
  3. Распознавание формата:
    • #! → парсинг интерпретатора, рекурсивный execve интерпретатора с аргументом скрипта.
    • \x7fELF → парсинг ELF-заголовка, проверка архитектуры, загрузка сегментов PT_LOAD, настройка стека, передача управления динамическому линкеру (если есть .interp) или точке входа.
    • MZ + PE → загрузка PE, проверка подписи, подсистемы, загрузка DLL-зависимостей, вызов EntryPoint.
    • FEEDFACE/FACF → парсинг Mach-O, проверка подписи/нотаризации, загрузка dyld, передача управления.
    • Если формат не распознан — возврат ENOEXEC (Exec format error).
    • Инициализация пространства адресов, загрузка библиотек, запуск runtime (CRT, dyld, ld.so), вызов main/WinMain/_start.

    Сравнение критериев «исполнимости» по ОС

    Критерий Linux / *BSD macOS Windows
    Первичный признак в ФС Бит x в mode (inode) Бит x + расширенные атрибуты (quarantine) Расширение имени файла (PATHEXT)
    Нативный формат ELF Mach-O (fat/universal) PE/COFF
    Магическое число \x7fELF FEEDFACE / CAFEBABE MZ + PE\0\0
    Скрипты Shebang (#!) Shebang (#!) Ассоциации в реестре (ftype/assoc)
    Проверка подписи Опционально (IMA, kernel lockdown) Обязательно для arm64 / нотаризованных Политика (WDAC, AppLocker, SmartScreen)
    Загрузчик пользовательского пространства ld-linux.so* (динамический линкер) dyld NTDLL + LDR (внутри процесса)

    Типичные ошибки и их причины

    • Permission denied (EACCES) — нет бита x для вашего UID/GID, или файловая система смонтирована с noexec, или ACL/SELinux/AppArmor блокирует execve. Проверка: ls -l, mount | grep noexec, getfattr -m security.* file.
    • Exec format error (ENOEXEC) — бит x есть, но ядро не узнало формат. Причины: неверная архитектура (arm64 бинарник на x86_64 без эмуляции), повреждённый заголовок, отсутствие shebang в скрипте, попытка запустить текстовый файл без shebang как бинарник. Проверка: file ./program, head -c 4 ./program | xxd.
    • No such file or directory (ENOENT) при запуске существующего файла — частая ловушка: shebang указывает на несуществующий интерпретатор (например, #!/bin/bash^M с Windows CRLF), или динамический линкер ELF (.interp) отсутствует в системе. Проверка: readelf -l ./program | grep interpreter, cat -A script.sh (покажет ^M).
    • Operation not permitted (EPERM) на macOS — Gatekeeper блокирует неподписанный/не нотаризованный файл с атрибутом карантина. Решение: xattr -d com.apple.quarantine file и ad-hoc подпись codesign —force -s — file.
    • Запуск скрипта без shebang — в Linux текущая оболочка (bash/zsh) может попытаться интерпретировать файл сама, если execve вернул ENOEXEC. Но это поведение оболочки, не ядра; в других контекстах (systemd, cron, ssh forced-command) запуск упадёт. Всегда пишите shebang.

    Практические проверки: что сделать перед запуском

    1. ls -l файл — есть ли бит x у нужного класса (user/group/other).
    2. file файл — покажет формат (ELF 64-bit LSB executable, ASCII text, script, Mach-O, PE32+).
    3. head -c 8 файл | xxd — магические байты вручную.
    4. Для ELF: readelf -h файл (класс, машина, тип, точка входа), readelf -l файл | grep INTERP (путь к линкеру).
    5. Для скриптов: head -1 файл | cat -A — shebang без скрытых CR.
    6. На macOS: codesign -dv файл — статус подписи; xattr -l файл — quarantine.
    7. На Windows: dumpbin /headers файл.exe (Visual Studio) или sigcheck -i файл.exe (Sysinternals) — подпись, подсистема, архитектура.

    Сценарии выбора: что делать в типичных ситуациях

    Ситуация Действие
    Скрипт не запускается с «Permission denied» chmod +x script; если на exFAT/FAT — скопируйте на ext4/APFS/NTFS или смонтируйте с fmask=0111.
    Бинарник даёт «Exec format error» Проверьте file и архитектуру (uname -m). Для arm64 на x86 — установите qemu-user-static и binfmt_misc.
    Скрипт запускается в терминале, но не из cron/systemd Добавьте shebang; используйте абсолютные пути; проверьте переменные окружения (PATH).
    macOS блокирует приложение «cannot be opened because the developer cannot be verified» ПКМ → Open (разовый), или xattr -d com.apple.quarantine + codesign —force -s -.
    Windows не запускает .py/.jar без консоли Проверьте ассоциации: assoc .py → ftype Python.File; исправьте через «Open with → Choose another app → Always use this app».
    Нужен запуск ELF без бита x (например, из памяти) Используйте memfd_create + execveat(AT_EMPTY_PATH) (Linux 3.17+) — требует CAP_DAC_OVERRIDE или root; пользовательский ld.so не загрузится без прав.

    Безопасность: как ОС защищает от случайного запуска

    • noexec на точке монтирования — ядро отказывает в execve для любых файлов на этой ФС (полезно для /tmp, /home, съёмных носителей).
    • W^X (Write XOR Execute) — страницы памяти не могут быть одновременно записываемыми и исполняемыми. JIT-компиляторы (V8, JVM, PyPy) используют mprotect для переключения.
    • SELinux / AppArmor — метки типов (type enforcement) ограничивают, какие домены могут исполнять какие типы файлов (например, httpd_t не может запускать user_home_t).
    • Kernel lockdown / IMA (Integrity Measurement Architecture) — проверка подписи ELF перед загрузкой в режиме lockdown.
    • WDAC / AppLocker (Windows) — политики разрешают запуск только подписанным/установленным в доверенных путях файлам.
    • Gatekeeper / Notarization (macOS) — проверка нотаризации Apple при первом запуске скачанного кода.

    Понимание этих слоёв помогает не только чинить «не запускается», но и проектировать безопасные среды: контейнеры с —security-opt no-new-privileges,readonly rootfs, drop-caps; CI-безопасность через подпись артефактов; политику «только подписанное в проде».

    Кросс-платформенные исполняемые файлы: миф или реальность?

    Нативно единого бинарника для Linux, Windows и macOS не существует — форматы (ELF, PE, Mach-O), системные вызовы и ABI принципиально различаются. Существующие подходы:

    • Интерпретируемые языки (Python, Node.js, Java, Go с компиляцией под цель) — исходник/байт-код переносим, рантайм нужен на каждой ОС.
    • Fat/Universal бинарники — только внутри одной ОС (macOS: x86_64 + arm64 в одном Mach-O; Linux: multiarch ELF через binfmt_misc + QEMU — не нативно).
    • WebAssembly (Wasm) — переносимый байт-код, запускается в браузере или через Wasmtime/Wasmer/WasmEdge на любой ОС с POSIX-интерфейсом (WASI). Перспективно для плагинов и изолированных модулей.
    • Cosmopolitan Libc / APE (Actually Portable Executable) — хак: один файл, который одновременно валиден как PE (Windows), ELF (Linux), Mach-O (macOS) и shell-скрипт, за счёт перекрытия заголовков и polyglot-техник. Работает для утилит без сложных зависимостей, но не стандарт.

    Для распространения CLI-инструментов сегодня практичнее: собирать отдельные артефакты под каждую ОС/архитектуру (GitHub Actions, GoReleaser, cargo-dist) и распространять через пакетные менеджеры (Homebrew, Scoop, apt, dnf, pkg) или подписанные архивы с checksums.

    Чек-лист: файл не запускается — что проверить за 2 минуты

    1. ls -l файл — есть ли x? Нет → chmod +x файл.
    2. file файл — что за формат? ASCII text без shebang → добавьте #!/usr/bin/env bash.
    3. head -1 файл — shebang указывает на существующий интерпретатор? Нет → исправьте путь или установите интерпретатор.
    4. uname -m vs архитектура бинарника (file покажет) — совпадает? Нет → нужен эмулятор или нативная сборка.
    5. macOS: xattr -l файл — есть com.apple.quarantine? Да → снимите или подпишите.
    6. Windows: расширение в PATHEXT? Ассоциация в реестре верная? SmartScreen не блокирует?
    7. Монтировано ли с noexec? mount | grep noexec.
    8. SELinux/AppArmor: dmesg -T | grep -i denied или audit.log.

    Резюме: главное, что нужно помнить

    Операционная система не «угадывает», исполняемый файл или нет — она последовательно проверяет жёсткие критерии. В Unix-мире это бит x + распознавание формата ядром (ELF, shebang, binfmt_misc). В Windows — расширение + реестр ассоциаций + валидный PE-заголовок. В macOS — бит x + Mach-O + обязательная подпись для современных архитектур.

    Если файл не запускается, порядок диагностики всегда одинаков: права ФС → магическое число/формат → зависимость от интерпретатора или линкера → политики безопасности (noexec, MAC, Gatekeeper, WDAC). Понимание этой цепочки экономит часы отладки и позволяет строить предсказуемые конвейеры сборки, развёртывания и исполнения кода на любой платформе.

    Материал носит справочный характер. Конкретные флаги монтирования, пути к динамическим линкерам, реестровые ключи и политики безопасности могут отличаться между версиями ОС, дистрибутивами и корпоративными настройками. Перед изменением прав, подписанием или отключением защит на продакшн-системах согласуйте действия с администраторами безопасности и проверьте актуальную документацию вашей ОС.

    PEFile.ru