Выполнение сценария Bash с шебангом и без него в Linux и BSD

Как и кто определяет, что выполняется, когда Bash-подобный скрипт выполняется как двоичный файл без шебанга?

Я предполагаю, что запуск обычного скрипта с shebang обрабатывается с помощью binfmt_script Linux-модуль, который проверяет шебанг, анализирует командную строку и запускает назначенный интерпретатор сценариев.

Но что происходит, когда кто-то запускает скрипт без шебанга? Я протестировал прямой подход execv и обнаружил, что там нет магии ядра, то есть такой файл:

$ cat target-script
echo Hello
echo "bash: $BASH_VERSION"
echo "zsh: $ZSH_VERSION"

Запуск скомпилированной программы C, которая выполняет только вызов execv, дает:

$ cat test-runner.c
void main() {
        if (execv("./target-script", 0) == -1)
                perror();
}
$ ./test-runner
./target-script: Exec format error

Однако, если я сделаю то же самое из другого сценария оболочки, он запустит целевой сценарий, используя тот же интерпретатор оболочки, что и исходный:

$ cat test-runner.bash
#!/bin/bash
./target-script

$ ./test-runner.bash 
Hello
bash: 4.1.0(1)-release
zsh: 

Если я проделаю тот же трюк с другими оболочками (например, Debian по умолчанию sh - /bin/dash), он также сработает:

$ cat test-runner.dash   
#!/bin/dash
./target-script

$ ./test-runner.dash 
Hello
bash: 
zsh: 

Как ни странно, с zsh он работает не совсем так, как ожидалось, и не следует общей схеме. Похоже, zsh все-таки выполнил /bin/sh на таких файлах:

greycat@burrow-debian ~/z/test-runner $ cat test-runner.zsh 
#!/bin/zsh
echo ZSH_VERSION=$ZSH_VERSION
./target-script

greycat@burrow-debian ~/z/test-runner $ ./test-runner.zsh 
ZSH_VERSION=4.3.10
Hello
bash: 
zsh: 

Обратите внимание, что ZSH_VERSION в родительском скрипте работало, а ZSH_VERSION в дочернем - нет!

Как оболочка (Bash, dash) определяет, что будет выполняться, когда нет шебанга? Я пытался откопать это место в исходниках Bash/dash, но, увы, похоже, я там заблудился. Может ли кто-нибудь пролить свет на магию, которая определяет, должен ли целевой файл без shebang выполняться как скрипт или как двоичный файл в Bash/dash? Или, может быть, есть какое-то взаимодействие с ядром/libc, и тогда я бы приветствовал объяснения того, как это работает в ядрах/libcs ​​Linux и FreeBSD?


person GreyCat    schedule 01.09.2011    source источник
comment
Это хороший вопрос о внутреннем устройстве оболочки, и на него стоит найти ответ. Однако всем читателям я говорю: на практике не надо. Используйте шебанг.   -  person Tom Zych    schedule 01.09.2011
comment
Возможно, было бы проще просто запустить target-script с разными оболочками напрямую (например, bash target-script или dash target-script), а не создавать средство запуска тестов для каждой оболочки. Это должно дать такие же результаты.   -  person Piotr Dobrogost    schedule 09.10.2014
comment
Я протестировал подход с прямым выполнением execv и обнаружил, что в нем нет магии ядра. Что ж, у вашего target-script нет строки shebang, так какую магию ядра вы ожидаете здесь? Если вы хотите протестировать магию ядра, вы должны были поместить строку shebang в свой скрипт (только для этого теста).   -  person Piotr Dobrogost    schedule 09.10.2014
comment
я предпочитаю вопросы/ответы отсюда: stackoverflow.com/q/12296308/52074, потому что связанный вопрос имеет больше голосов за и вопрос / ответ, и письмо лучше для ИМО.   -  person Trevor Boyd Smith    schedule 18.01.2019


Ответы (2)


Так как это происходит в тире, а тире проще, я сначала посмотрел туда.

Похоже, что exec.c - это то место, где нужно искать, и соответствующие функции - это tryexec, который вызывается из shellexec, который вызывается всякий раз, когда оболочка требует выполнения команды. И (упрощенная версия) функция tryexec выглядит следующим образом:

STATIC void
tryexec(char *cmd, char **argv, char **envp)
{
        char *const path_bshell = _PATH_BSHELL;

repeat:

        execve(cmd, argv, envp);

        if (cmd != path_bshell && errno == ENOEXEC) {
                *argv-- = cmd;
                *argv = cmd = path_bshell;
                goto repeat;
        }
}

Таким образом, он просто всегда заменяет команду для выполнения на путь к себе (_PATH_BSHELL по умолчанию равен "/bin/sh"), если происходит ENOEXEC. Здесь действительно нет никакой магии.

Я обнаружил, что FreeBSD демонстрирует одинаковое поведение в bash и в своей собственной sh.

То, как bash справляется с этим, похоже, но намного сложнее. Если вы хотите изучить это подробнее, я рекомендую прочитать bash execute_command.c и конкретно посмотреть на execute_shell_script, а затем на shell_execve. Комментарии достаточно описательные.

person sorpigal    schedule 01.09.2011
comment
Спасибо! Должен отметить, что комментарии в execute_shell_script несколько вводят в заблуждение: реальной битовой проверки в исполнительном режиме нет, все зависит от возвращаемого значения execve() и, что еще сложнее, bash также пытается сам эмулировать синтаксический анализ shebang, т.е. он не просто запускается что бы он ни получал как скрипт, но пытается найти и запустить интерпретатор, как указано в shebang. Эта функция несколько бесполезна в Linux, так как binfmt_script уже обрабатывает ее на уровне ядра, но, возможно, она полезна для какой-то другой ОС?.. - person GreyCat; 01.09.2011
comment
@GreyCat: По крайней мере, я ожидаю, что это будет полезно в порте Windows, так что да. - person sorpigal; 02.09.2011
comment
@GreyCat настоящей проверки битов в исполнительном режиме нет, все зависит от execve() возвращаемого значения Согласно справочной странице execve(3), функции exec завершатся ошибкой EACCESS, если отказано в разрешении на поиск для каталога, указанного в префиксе пути к новому файлу образа процесса, или новый файл образа процесса отказывает в разрешении на выполнение, или новый файл образа процесса не является обычным файлом, и реализация не поддерживает выполнение файлов его типа. Поскольку разрешение на выполнение зависит от установленного бита выполнения, можно сказать, что этот бит проверяется, хотя и не напрямую :) - person Piotr Dobrogost; 09.10.2014

(Похоже, что Sorpigal это уже описал, но я уже напечатал это, и это может быть интересно.)

Согласно разделу 3.16 часто задаваемых вопросов по Unix, оболочка сначала смотрит на магическое число (первые два байта файла). Некоторые числа указывают на двоичный исполняемый файл; #! указывает, что остальную часть строки следует интерпретировать как шебанг. В противном случае оболочка пытается запустить его как сценарий оболочки.

Кроме того, кажется, что csh просматривает первый байт, и если это #, он попытается запустить его как скрипт csh.

person Tom Zych    schedule 01.09.2011
comment
оболочка сначала просматривает магическое число (первые два байта файла) Нет, это не соответствует записи часто задаваемых вопросов, на которую вы дали ссылку. Сначала оболочка пытается выполнить скрипт как двоичный файл, а ядро ​​смотрит на магическое число. Только после того, как сценарий не удалось выполнить таким образом, оболочка пытается запустить его как сценарий. Если execl() успешно запускает программу, код за пределами execl() никогда не выполняется. - person Piotr Dobrogost; 09.10.2014
comment
@PiotrDobrogost: Вы правы. Я был немного неосторожен там. Но остальная часть записи показывает, что все еще сложнее; csh просматривает первый байт (а не два байта). Также стоит отметить, что эта запись примерно такая же старая, как и первое ядро ​​Linux, поэтому все могло измениться. - person Tom Zych; 13.10.2014