Serg Oskin (Serg.Oskin@f20.n5020.z2.fidonet.org) wrote:
: >>>>> "Slav" == Slav Matveev writes:
: Slav> Изначально я пытался понять что будет если у скрипта нет
: Slav> заголовка
: Slav> #! , мне почему-то всегда казалось что shell этот скрипт будет
: Slav> разбирать сам. Меня же попытались переубедить что будет сказано
: Slav> command not found, причем это аргументировалось всем чем только
: Slav> можно (в смысле очень умными словами). В том числе ссылкой на
: Slav> execv. Я привел кусок man'а по этой самой функции, в который ясно
: Slav> написано, что в случае ненахождения magic'a будет запущен
: Slav> shell. Мне только осталось не совсем понятно, какой конкретно
: Slav> шел. Тот из которого это команда выполнялась, тот, который для
: Slav> пользователя установлен, /usr/sh или что-то еще.
: Hесколько грубый пример, но всетаки:
: $ cat >tst
: if [ -f /etc/passwd ] ; then
: echo "Hello, world!"
: fi
: ^D
: $ file tst
: tst: Composer 669 Module sound data
: $
То есть, в данном случае Вы намекаете на то, что шеллом должна быть
некоторая программа, которая умеет Module sound data ? И что ядро будет
запускать эту программу ?
Гмм...
Давайте я еще раз попробую объяснить механизм запуска программы с помощью
exec ?
Только прошу, Slav Matveev и Valentin Nechayev, не подумайте чего плохого. У
меня и в мыслях нет делать "пальцы веером" и прочее. Ну зачем мне это ?
Просто есть некоторые заблуждения в вопросе exec, которые я пытаюсь
развеять. На пользу всем, а не для показухи - "какой я умный". Какая мне, в
конце концов разница, что про меня подумает человек, которого я никогда в
жизни не видел, скорее всего не увижу, и который никоим образом не может
повлиять на мою жизнь ? Равно как и я на его.
В общем, "не корысти для...", а в интересах эхи, темой которой, как ни
странно, является unix, а не мои амбиции.
Итак:
Выполнение функций и системных вызовов семейства exec. Текущее
состояние вопроса в BSD.
1. Системный вызов execve(2).
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
В BSD единственным способом запустить программу из именованного файла
является системный вызов execve(2).
Его синтакис:
int execve(const char *path, const char *argv[], const char *envp[])
Где path - абсолютный (от корня) или относительный (от текущего рабочего
каталога) путь к файлу, который следует исполнить. Путь может состоять и из
одного только имени файла. Тогда файл будет разыскиваться в текущем рабочем
каталоге.
Ядро, исполняя системный вызов execve(2), делает (схематично и несколько
сокращенно):
1. Проверяет на правильность параметры path, argv, envp.
2. Проверяет путь к файлу, на который указывает path.
3. Проверяет - можно ли запускать файл ? в соответствии с типом файла
(файл должен быть обычный, а не каталог, device или fifo) и правами доступа.
4. Создает или находит уже существующий vnode для файла.
5. Делает отображение в память первой страницы файла.
6. Изучает заголовок файла с помощью так называемого exec switch - массива
зарегистрированных в системе подготовщиков к запуску определенных типов
файлов. ВНИМАНИЕ: если ни один из зарегистрированных обработчиков не
обнаруживает, что файл можно запустить, ядро возвращается из execve(2) с
выдачей ошибки ENOEXEC !
7. Если же один из подготовщиков обнаружил, что файл можно исполнить, он
подготавливает необходимые данные (например, правильно размещает файл в
памяти) и ядро продолжает выполнение execve(2).
8. Если подготовщик сообщает ядру, что файл следует интерпретировать, то 2-6
повторяется уже для интерпретатора. То есть, реально запустится или не
запустится интерпретатор.
9. Вот теперь уже для подготовленного к запуску файла или интерпретатора
делаются suid/sgid изменения прав доступа. Из этого следует, что биты
suid/sgid, поставленные на интерпретируемый скрипт, для ядра не имеют
никакого значения.
10. Производится еще некоторая работа и execve(2) завершается хитрым
образом, возвращаясь на точку входа новой программы.
2. Что делает подготовщик к запуску, вызываемый из exec switch ?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1. Он проверяет передаваемую ему первую страницу образа в памяти
исполнимого файла на предмет распознования СВОЕГО типа файла.
Первичное распознование происходит обычно по некоторым MAGIC-признакам.
Например, подготовщик может проверить первые несколько байтов файла
на совпадение с некоторым значением - числом или строкой. Если эта
первичная проверка положительна, могут производиться дополнительные
проверки на, так сказать, вторичные признаки.
Работа, производимая ядром, не имеет никакого отношения к утилите file(1).
Если подготовщик не смог распознать файл (частный случай - MAGIC верен,
вторичные признаки не прошли), то в execve(2) возвращается ENOEXEC !
2. Если все проверки прошли, подготовщик пытается произвести собственно
работу по подготовке файла к запуску. На этом этапе тоже могут быть ошибки,
но они не будут ENOEXEC.
Иными словами, подготовщик может вернуть ENOEXEC только в том случае, если
он не может определить тип файла как свой. А execve(2) возвращает ENOEXEC
только в том случае, если все подготовщики из exec switch вернули ENOEXEC.
То есть, если exec switch вообще не смог определить тип файла. Тут есть
ТОЛЬКО две ситуации - тип файла определен и ядро пытается запустить его (в
этом случае НИКОГДА не будет ENOEXEC) или тип файла как-бы НЕ определен
вообще никак (он может быть определен, но, по некоторым причинам подготовщик
отказывается его выполнить наотрез, даже не пытаясь производить подготовку)
и ядро его не будет запускать (в этом, и только в этом случае execve(2)
вернет ENOEXEC).
3. Один из подготовщиков называется exec_shell. Он распознает тип файла по
MAGIC - двум первым байтам файла, которые ДОЛЖНЫ быть равны '#!' (число
0x2123 или 0x2321 в зависимости от byte ordering системы).
ЕСЛИ ПЕРВЫЕ ДВА БАЙТА ФАЙЛА НЕ РАВНЫ '#!', ЯДРО НЕ РАСПОЗНАЕТ ФАЙЛ КАК
ИНТЕРПРЕТИРУЕМЫЙ СКРИПТ. Стало быть, САМО ЯДРО НИКОГДА НЕ БУДЕТ ВЫПОЛНЯТЬ
СКРИПТ БЕЗ '#!'.
Если же проверка на MAGIC прошла, подготовщик делает дополнительные проверки
на предмет, не пытаются ли запустить в качестве shell скрипт (запретная
ситуация, ENOEXEC(!)) и правильна ли строка запуска shellа (тоже ENOEXEC).
После чего подправляются параметры запуска файла на предмет того, что для
execve(2) делается пометка, что файл является скриптом и делается так, чтобы
прочие подготовщики файлов к исполнению при желании смогли бы получить
параметры, передаваемые в командной строке скрипту, плюс имя файла со
скриптом, плюс параметры интерпретатора из строки '#!'.
Особо попрошу заметить, что, если в строке '#!' shell указан без пути и не
находится в текущем каталоге, то execve(2) завершится с ENOENT.
3. Знает ли ядро в контексте exec о /bin/sh ?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Нет, не знает. Если в скрипте в строке '#!' не указан этот shell, то /bin/sh
автоматом вызываться из ядра НЕ БУДЕТ.
4. Библиотечные libc функции семейства exec.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Перечень:
execl(3), execlp(3), execle(3), exect(3), execv(3), execvp(3)
Все они являются более или менее продвинутыми надстройками над execve(2).
Реальный запуск программы производится ТОЛЬКО execve(2).
execl(3), execle(3), exect(3) и execv(3) различаются только аргументами.
Внутренняя работа функций сводится к простому преобразованию своих
аргументов в аргументы execve(2). При этом, параметр path в этих функциях
идентичен параметру path в execve(2).
execlp(3) и execvp(3) более "интеллектуальны". Они имеют некоторый
shell-подобный сервис для поиска и запуска программ. А именно:
1. Вместо параметра path в этих функциях используется параметр file. Если
это абсолютный или относительный путь к исполняемому файлу, функция
оставляет его без изменений при передаче execve(2). Если же file - это
просто имя файла без указания пути, то execlp(3) и execvp(3) производят
поиск файла в соответствии с путями, указанными в переменной окружения PATH.
При этом, путь последовательно конструируется из очередного элемента PATH и
параметра file, вызывается execve(2) и, в случае возврата от execve(2)
ENOENT, процедура повторяется до полного перебора всех путей в PATH.
Таким образом, execlp(3) и execlp(3) имеют некоторое преимущество в поиске
исполняемого файла, поскольку могут искать файл в нескольких каталогах.
Однако, есть и недостатки: если в PATH не указать явно текущий каталог, в
нем поиск не будет производиться, в отличие от прочих функций.
2. Кроме того, execlp(3) и execvp(3) делают такое допущение:
Если execve(2) вернул ENOEXEC, эти функции считают, что исполнимый файл
является скриптом /bin/sh без строки '#!' и пытаются запустить /bin/sh с
передачей ему построенного с помощью PATH пути на исполнение.
Таким образом, о /bin/sh в контексте execlp(3) и execvp(3), и только их из
всего набора функций семейства exec, знает libc.
5. Где все это можно посмотреть ?
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
execve(2) :
man execve, /usr/src/sys/kern/kern_exec.c, /usr/src/sys/kern/imgact*.c
execl(3), execlp(3), execle(3), exect(3), execv(3), execvp(3):
man exec, /usr/src/lib/libc/gen/exec.c
6. Замечания.
^^^^^^^^^^^^^
Все вышеозначенное абсолютно точно относится к BSD-линии *nix. С большОй
вероятностью и к прочим *nix. Однако, в случае прочих *nix следует
ориентироваться на POSIX.
В принципе, стандарт POSIX не запрещает реализовывать функции семейства
exec в ядре. В таком случае, некоторые из POSIX-совместимых систем, в том
числе и некоторые *nix, могут иметь ядро, знающее в случае execlp и execvp
о /bin/sh.
Из области догадок: скорее всего таких *nix нет. И execlp, execvp
реализованы через libc. Особенно вероятно это для Linux, как
использующего gnulibc.
-- Anatoly A. Orehovsky. AO9-RIPE. AAO1-RIPN http://www.tekmetrics.com/transcript.shtml?pid=6064 -- end of forwarded message --
-- С тем, что не помешает никогда, Kittle