Когда вы нажимаете Enter после имени файла в терминале или дважды кликаете по иконке, операционная система за доли секунды принимает решение: запустить этот файл как программу или отказать с ошибкой. Решение основывается не на одном признаке, а на комбинации метаданных файловой системы, внутренней структуры файла и настроек среды. Понимание этого механизма помогает отлаживать ошибки «Permission denied», «Exec format error», создавать переносимые скрипты и настраивать политики безопасности.
- Два фундаментальных подхода: права доступа и формат файла
- Unix-подход: биты исполнения и shebang
- Биты прав: user, group, other
- Shebang: как ядро находит интерпретатор
- ELF: нативный формат Linux и *BSD
- Windows: расширения, ассоциации и PE
- Расширения и PATHEXT
- PE/COFF: нативный формат Windows
- macOS: Mach-O, кодовая подпись и Gatekeeper
- Файловые системы без прав исполнения: FAT32, exFAT, сетевые шары
- Механизм загрузки: от системного вызова до точки входа
- Сравнение критериев «исполнимости» по ОС
- Типичные ошибки и их причины
- Практические проверки: что сделать перед запуском
- Сценарии выбора: что делать в типичных ситуациях
- Безопасность: как ОС защищает от случайного запуска
- Кросс-платформенные исполняемые файлы: миф или реальность?
- Чек-лист: файл не запускается — что проверить за 2 минуты
- Резюме: главное, что нужно помнить
Два фундаментальных подхода: права доступа и формат файла
Любая современная ОС разделяет проверку на два этапа. Первый — «может ли этот процесс запускать файлы вообще и этот файл в частности». Второй — «понимает ли ядро формат этого файла и как его загрузить». Если первый этап проходит, а второй проваливается, вы получаете ошибку формата. Если проваливается первый — ошибку прав доступа.
В семействе 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):
- Проверка прав доступа к inode: есть ли бит x для effective UID/GID процесса (Unix) или разрешена ли политика AppLocker/WDAC (Windows).
- Чтение первых байт файла (обычно 128–4096 байт) для определения формата.
- Распознавание формата:
- #! → парсинг интерпретатора, рекурсивный 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.
Практические проверки: что сделать перед запуском
- ls -l файл — есть ли бит x у нужного класса (user/group/other).
- file файл — покажет формат (ELF 64-bit LSB executable, ASCII text, script, Mach-O, PE32+).
- head -c 8 файл | xxd — магические байты вручную.
- Для ELF: readelf -h файл (класс, машина, тип, точка входа), readelf -l файл | grep INTERP (путь к линкеру).
- Для скриптов: head -1 файл | cat -A — shebang без скрытых CR.
- На macOS: codesign -dv файл — статус подписи; xattr -l файл — quarantine.
- На 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 минуты
- ls -l файл — есть ли x? Нет → chmod +x файл.
- file файл — что за формат? ASCII text без shebang → добавьте #!/usr/bin/env bash.
- head -1 файл — shebang указывает на существующий интерпретатор? Нет → исправьте путь или установите интерпретатор.
- uname -m vs архитектура бинарника (file покажет) — совпадает? Нет → нужен эмулятор или нативная сборка.
- macOS: xattr -l файл — есть com.apple.quarantine? Да → снимите или подпишите.
- Windows: расширение в PATHEXT? Ассоциация в реестре верная? SmartScreen не блокирует?
- Монтировано ли с noexec? mount | grep noexec.
- 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). Понимание этой цепочки экономит часы отладки и позволяет строить предсказуемые конвейеры сборки, развёртывания и исполнения кода на любой платформе.
Материал носит справочный характер. Конкретные флаги монтирования, пути к динамическим линкерам, реестровые ключи и политики безопасности могут отличаться между версиями ОС, дистрибутивами и корпоративными настройками. Перед изменением прав, подписанием или отключением защит на продакшн-системах согласуйте действия с администраторами безопасности и проверьте актуальную документацию вашей ОС.
