Что означает запись bin bash в начале исполняемого файла Linux

Обновлено: 18.05.2024

В этом разделе представлены несколько примеров сценариев оболочки.

Пример 9. Привет, мир

Пример 10. Аргументы сценария оболочки

Сохраните этот файл как name.sh , установите разрешение на выполнение для этого файла, набрав chmod a+x name.sh , а затем запустите файл следующим образом: ./name.sh .

$ chmod a+x name.sh $ ./name.sh Hans-Wolfgang Loidl Мое имя Hans-Wolfgang Моя фамилия Loidl Всего аргументов 2

В первом примере просто подсчитывается количество строк во входном файле. Он делает это, перебирая все строки файла с помощью цикла while, выполняя операцию чтения в заголовке цикла. Пока есть строка для обработки, в этом случае будет выполняться тело цикла, просто увеличивая счетчик на ((counter++)) . Кроме того, текущая строка записывается в файл, имя которого задается переменной file, путем повторения значения строки переменной и перенаправления стандартного вывода переменной в $file. текущая строка в файл. Последнее, конечно, не нужно для подсчета строк, но демонстрирует, как проверить успешность операции: специальная переменная $? будет содержать код возврата из предыдущей команды (перенаправленное эхо). Согласно соглашению Unix, успех обозначается кодом возврата 0, все остальные значения представляют собой коды ошибок со значением, зависящим от приложения.

Еще один важный момент, который следует учитывать, заключается в том, что целочисленная переменная, над которой выполняется итерация, всегда должна иметь обратный отсчет, чтобы анализ мог найти границу. Это может потребовать некоторой реструктуризации кода, как в следующем примере, где для этой цели вводится явный счетчик z. После цикла количество строк и содержимое последней строки выводятся с помощью команды echo. Конечно, есть команда Linux, которая уже реализует функцию подсчета строк: wc (для подсчета слов) печатает при вызове с опцией -l количество строк в файле. Мы используем это, чтобы проверить правильность подсчета строк, демонстрируя по пути числовые операции.

Образец текстового файла

$ cp /home/msc/public/LinuxIntro/WaD.txt text_file.txt

В этом разделе разрабатывается несколько сценариев оболочки, каждый из которых подсчитывает общее количество строк в наборе файлов. В этих примерах раскрываются специфические особенности оболочки. Для подсчета количества строк в одном файле мы используем wc -l. В качестве простого упражнения вы можете заменить эту команду вызовом приведенного выше сценария подсчета строк.

Мы используем цикл for для перебора всех файлов, предоставленных скрипту в качестве аргументов. Мы можем получить доступ ко всем аргументам через переменную $*. Команда sed сопоставляет количество строк и заменяет всю строку только количеством строк, используя обратную ссылку на первую подстроку (\1). В цикле for переменная оболочки n – это счетчик количества файлов, а s – общее количество строк на данный момент.

В этой версии мы определяем функцию count_lines, которая подсчитывает количество строк в файле, указанном в качестве аргумента. Внутри функции значение аргумента извлекается путем обращения к переменной $1 .

Эта версия пытается использовать возвращаемое функцией значение для возврата количества строк. Однако это не удается для файлов с более чем 255 строками. Возвращаемое значение предназначено только для предоставления кода возврата, например. 0 в случае успеха, 1 в случае неудачи, но не для возврата правильных значений.

В приведенном ниже примере используются массивы оболочки для хранения всех имен файлов ( file ) и количества строк ( line ). Элементы в массиве упоминаются с использованием обычной нотации [ ], например. file[1] относится к первому элементу массива file. Обратите внимание, что bash поддерживает только одномерные массивы с целыми числами в виде indizes.

Последний пример поддерживает параметры, которые можно передать из командной строки, например. по ./loc7.sh -d 1 loc7.sh . Функция оболочки getopts используется для перебора всех опций (указанных в следующей строке) и присвоения текущей опции переменной name . Обычно он используется в цикле while для установки переменных оболочки, которые будут использоваться позже. Мы используем конвейер cat и awk для печати заголовка этого файла до первой пустой строки, если выбрана опция справки. Основная часть сценария — это цикл for для всех аргументов командной строки, не являющихся параметрами. На каждой итерации $f содержит имя обрабатываемого файла. Если параметры даты используются для сужения области обрабатываемых файлов, мы используем дату и оператор if, чтобы сравнить, находится ли время модификации файла в пределах указанного интервала. Только в этом случае мы считаем количество строк, как и раньше. После цикла мы печатаем общее количество строк и количество обработанных файлов.

Пример 11. Версия 7: количество строк в нескольких файлах

Расширить Версию 7 приведенного выше примера подсчета строк, чтобы также вычислить общее количество байтов и общее количество слов во входном файле.

Изучение методов ввода данных в скрипты и управления путями выполнения скриптов для лучшей автоматизации и управления скриптами.

Опубликовано: 19 мая 2021 г. | Дэвид Бот

Добавление аргументов и параметров в сценарии Bash

Одним из наиболее важных инструментов для большинства системных администраторов является автоматизация. Мы пишем и поддерживаем сценарии для автоматизации общих и частых задач, которые мы должны выполнять.

У меня есть десятки сценариев — коротких и длинных, — которые я написал и модифицировал на протяжении многих лет. Некоторые из моих наиболее полезных сценариев заключались в том, чтобы выполнять регулярное резервное копирование каждое утро рано утром, устанавливать обновленные программные пакеты с исправлениями и улучшениями и обновлять одну версию Fedora до другой. Несколько дней назад я обновил все свои персональные хосты и серверы до Fedora 34, используя довольно простой скрипт.

Две наиболее распространенные вещи, которые я делаю для всех своих скриптов, — это создание функции справки и функции, отображающей положение о лицензии GPL3. Мне нравится включать подробные или тестовые режимы, чтобы помочь в определении проблемы в моих сценариях. В некоторых сценариях я также передаю такие значения, как имя пользователя, версию Fedora для обновления, имена файлов и т. д.

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

Кубернеты и OpenShift

Позиционные параметры

Bash использует инструмент, называемый позиционными параметрами, для предоставления средств ввода данных в программу Bash, когда она вызывается из командной строки. Есть десять позиционных параметров, которые варьируются от 0 до 9 долларов, хотя есть способы обойти это ограничение.

Начнем с простого сценария, отображающего введенное имя на экране. Создайте файл script1.sh со следующим содержимым и сделайте его исполняемым.

Я поместил этот сценарий в свой каталог ~/bin , где должны храниться личные исполняемые файлы, такие как сценарии. Посмотрите на свою переменную $PATH, которая содержит /home/username/bin как один компонент. Если каталог ~/bin не существует, вы можете его создать. Или вы можете просто поместить этот файл в любое место и использовать его оттуда.

Затем запустите скрипт без параметров.

Выводом этого скрипта является имя скрипта. Параметр $0 зарезервирован и предопределен как имя запущенного скрипта и не может использоваться для каких-либо других целей. Это может быть удобно внутри скрипта, потому что вам не нужно передавать скрипту собственное имя, если оно требуется.

Поэтому измените скрипт так, чтобы он использовал $1 для позиционной переменной, и запустите его снова:

Запустите его еще раз, на этот раз с одним параметром:

Что произойдет, если параметр состоит из двух слов?

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

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

Измените программу так, чтобы она выглядела следующим образом:

И запустите его, используя указанные параметры:

Конечно, есть много способов использовать позиционные параметры после присвоения значений, но эта небольшая программа позволяет легко увидеть, что происходит. Это также позволяет безопасно экспериментировать.

Попробуйте указать параметры в другом порядке, чтобы увидеть, как это работает. Эти параметры являются позиционными, и это ключевое соображение. Вы должны учитывать, сколько параметров требуется, как пользователь их запоминает и в каком порядке их размещать.

Вам нужен способ сделать порядок параметров нерелевантным и по-прежнему нужен способ изменить путь выполнения.

Безопасность Linux

Параметры

Вы можете сделать эти две вещи, используя параметры командной строки.

Я считаю, что даже в простых программах Bash должны быть какие-то средства справки, даже если они довольно примитивны. Многие программы оболочки Bash, которые я пишу, используются достаточно редко, поэтому я могу забыть точный синтаксис команды, которую мне нужно выполнить. Некоторые из них настолько сложны, что мне нужно просмотреть необходимые параметры и аргументы, хотя я часто их использую.

Наличие встроенной функции справки позволяет вам просматривать эти вещи, не прибегая к проверке самого кода. Хорошая и полная справочная система также является частью документации программы.

О функциях

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

Функции вызываются в сценариях или из интерфейса командной строки по их именам, точно так же, как и для любой другой команды. В программе CLI или сценарии команды в функции выполняются при вызове. Затем последовательность выполнения программы возвращается к вызывающему объекту, и выполняется следующая серия операторов программы в этом объекте.

Синтаксис функции:

Создайте простую функцию в интерфейсе командной строки. Функция хранится в среде оболочки для экземпляра оболочки, в котором она создана. Вы собираетесь создать функцию с именем hw, что означает Hello world. Введите следующий код в CLI и нажмите Enter. Затем введите hw, как и любую другую команду оболочки.

Хорошо, я немного устал от стандартного "Hello world!" Я обычно начинаю с. Теперь перечислите все определенные в настоящее время функции. Их очень много, поэтому я показал только новую функцию hw. При вызове из командной строки или внутри программы функция выполняет запрограммированную задачу. Затем он завершает работу, возвращая управление вызывающему объекту, командной строке или следующему оператору программы Bash в сценарии после вызывающего оператора.

Теперь удалите эту функцию, потому что она вам больше не нужна. Вы можете сделать это с помощью команды unset, например:

Скрипт hello.sh

Создайте новый сценарий оболочки Bash ~/bin/hello.sh и сделайте его исполняемым. Добавьте следующий контент, оставив его базовым для начала:

Запустите его, чтобы убедиться, что он печатает "hello world!".

Я знаю, я ничего не могу с собой поделать, поэтому я вернулся к "привет, мир!".

Создание функции справки

Добавьте функцию справки, показанную ниже, в код программы hello. Поместите функцию справки между двумя операторами, которые у вас уже есть. Эта справочная функция отображает краткое описание программы, синтаксическую диаграмму и краткое описание каждой доступной опции. Вы также добавляете вызов функции справки для ее проверки и несколько строк комментариев, обеспечивающих визуальное разграничение между функциями и основной частью программы.

Теперь программа выглядит так.

Параметры, описанные в этой справочной функции, могут быть типичными для программ, которые я пишу, хотя в коде их пока нет. Запустите программу, чтобы проверить ее.

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

Параметры обработки

Возможность скрипта Bash обрабатывать такие параметры командной строки, как -h для отображения справки, дает вам некоторые мощные возможности для управления программой и изменения того, что она делает. В случае вашей опции -h вы хотите, чтобы программа распечатала текст справки в сеансе терминала, а затем вышла, не запуская остальную часть программы. Возможность обработки параметров, введенных в командной строке, можно добавить в сценарий Bash с помощью команды while в сочетании с командами getops и case.

Команда getops считывает все без исключения параметры, указанные в командной строке, и создает список этих параметров. Команда while перебирает список опций, устанавливая переменную $options для каждой в приведенном ниже коде. Оператор case используется для поочередной оценки каждого параметра и выполнения операторов в соответствующем разделе. Оператор while будет продолжать оценивать список параметров до тех пор, пока все они не будут обработаны или пока не встретится оператор выхода, завершающий программу.

Обязательно удалите вызов функции справки непосредственно перед эхом "Hello world!" оператор, так что основная часть программы теперь выглядит так.

Обратите внимание на двойную точку с запятой в конце оператора выхода в опции case для -h . Это необходимо для каждого варианта. Добавьте к этому оператору case, чтобы обозначить конец каждой опции.

Теперь тестирование стало немного сложнее. Вам нужно протестировать вашу программу с несколькими различными опциями — и без опций — чтобы увидеть, как она реагирует. Во-первых, убедитесь, что без опций он печатает «Hello world!» как следует.

Это работает, так что теперь проверьте логику, которая отображает текст справки.

Это работает, как и ожидалось, поэтому попробуйте проверить, что произойдет, если вы введете неожиданные параметры.

Обработка недопустимых параметров

Программа просто игнорирует параметры, для которых вы не создали конкретные ответы, без каких-либо ошибок. Хотя в последней записи с параметрами -lkjsahdf из-за того, что в списке есть "h", программа распознала его и напечатала текст справки. Тестирование показало, что отсутствует одна вещь — способность обрабатывать неправильный ввод и завершать программу, если таковой обнаружен.

К оператору case можно добавить еще одну строфу case, которая будет соответствовать любому параметру, для которого нет явного соответствия. Этот общий случай будет соответствовать всему, для чего вы не указали конкретное совпадение. Оператор case теперь выглядит так.

Кубернеты и OpenShift

Этот фрагмент кода заслуживает объяснения того, как он работает. Это кажется сложным, но довольно легко понять. Структура while-done определяет цикл, который выполняется один раз для каждой опции в структуре getopts-option. Строка «:h» — которая требует кавычек — перечисляет возможные параметры ввода, которые будут оцениваться по структуре case — esac. Каждая указанная опция должна иметь соответствующую строфу в операторе case. В данном случае их два. Одной из них является h) строфа, вызывающая процедуру Help. После завершения процедуры справки выполнение возвращается к следующему оператору программы, exit;; который выходит из программы без выполнения какого-либо кода, даже если он существует. Цикл обработки опций также завершается, поэтому никакие дополнительные опции не проверяются.

Обратите внимание на универсальное совпадение \? как последняя строфа в заявлении случая. Если вводятся какие-либо параметры, которые не распознаются, в этом разделе печатается короткое сообщение об ошибке и происходит выход из программы.

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

Последний оператор каждой строфы в конструкции case должен заканчиваться двойной точкой с запятой ( ;; ), которая используется для явного обозначения конца каждой строфы. Это позволяет тем программистам, которым нравится использовать явные точки с запятой в конце каждого оператора вместо неявных, продолжать делать это для каждого оператора в каждой строфе case.

Протестируйте программу еще раз, используя те же параметры, что и раньше, и посмотрите, как она теперь работает.

Сценарий Bash теперь выглядит так.

Не забудьте тщательно протестировать эту версию вашей программы. Используйте случайный ввод и посмотрите, что произойдет. Вам также следует попробовать проверить допустимые и недопустимые параметры, не используя тире (-) впереди.

Использование параметров для ввода данных

Сначала добавьте переменную и инициализируйте ее. Добавьте две строки, выделенные жирным шрифтом, в сегмент программы, показанный ниже. Это инициализирует переменную $Name со значением «мир» по умолчанию.

Замените последнюю строку программы, команду echo, на эту.

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

$OPTARG всегда является именем переменной, используемой для каждого аргумента новой опции, независимо от их количества. Вы должны присвоить значение в $OPTARG имени переменной, которое будет использоваться в остальной части программы. В этой новой строфе нет оператора выхода. Это изменяет поток программы таким образом, что после обработки всех допустимых параметров в операторе case выполнение переходит к следующему оператору после конструкции case.

Протестируйте измененную программу.

Завершенная программа выглядит следующим образом.

Обязательно протестируйте справку и то, как программа реагирует на недопустимый ввод, чтобы убедиться, что ее способность обрабатывать их не нарушена. Если все это работает как надо, значит, вы успешно научились использовать параметры и аргументы параметров.

Подведение итогов

В этой статье вы использовали позиционные параметры для ввода данных в программу Bash во время вызова из командной строки и использовали параметры для управления потоком программы, а также для ввода данных в программу. Вы добавили функцию справки и возможность обрабатывать параметры командной строки для выборочного отображения справки. И вы добавили необязательный аргумент, который позволяет вводить имя в командной строке.

Эта небольшая тестовая программа разработана так, чтобы быть простой, поэтому вы можете легко поэкспериментировать с ней самостоятельно, чтобы протестировать этот метод ввода самостоятельно. В качестве упражнения повторите программу, чтобы взять имя и фамилию. Попробуйте ввести параметры имени и фамилии в обратном порядке, чтобы посмотреть, что произойдет.

Это четвертая часть главы, описывающая системные вызовы в ядре Linux, и, как я писал в заключении предыдущей, эта часть будет последней в этой главе. В предыдущей части мы остановились на двух новых концепциях:

которые связаны и очень похожи по концепции системных вызовов.

Эта часть будет последней в этой главе, и, как вы можете понять из названия части, мы увидим, что происходит в ядре Linux, когда мы запускаем наши программы. Итак, приступим.

как мы запускаем наши программы?

Существует множество различных способов запуска приложения с точки зрения пользователя. Например, мы можем запустить программу из оболочки или дважды щелкнуть значок приложения. Неважно.Ядро Linux обрабатывает запуск приложения независимо от того, как мы запускаем это приложение.

В этой части мы рассмотрим способ, когда мы просто запускаем приложение из оболочки. Как вы знаете, стандартный способ запуска приложения из оболочки следующий: мы просто запускаем приложение эмулятора терминала и просто пишем название программы и передаем или не передаем аргументы нашей программе, например:

ls shell

Давайте рассмотрим, что происходит, когда мы запускаем приложение из оболочки, что делает оболочка, когда мы пишем имя программы, что делает ядро ​​Linux и т. д. Но прежде чем мы приступим к рассмотрению этих интересных вещей, хочу предупредить, что эта книга о ядре Linux. Вот почему в этой части мы увидим внутренности ядра Linux. Мы не будем подробно рассматривать, что делает оболочка, не будем рассматривать сложные случаи, например подоболочки и т.д.

Моей оболочкой по умолчанию является bash, поэтому я рассмотрю, как оболочка bash запускает программу. Итак, начнем. Оболочка bash, как и любая программа, написанная на языке программирования C, запускается из функции main. Если вы посмотрите на исходный код оболочки bash, вы найдете основную функцию в файле исходного кода shell.c. Эта функция делает много разных вещей до того, как основной цикл bash начал работать. Например, эта функция:

  • проверяет и пытается открыть /dev/tty ;
  • проверьте, что оболочка работает в режиме отладки;
  • анализирует аргументы командной строки;
  • считывает среду оболочки;
  • загружает .bashrc , .profile и другие файлы конфигурации;
  • и многое другое.

После всех этих операций мы видим вызов функции reader_loop. Эта функция определена в файле исходного кода eval.c и представляет собой цикл основного потока или, другими словами, она читает и выполняет команды. Поскольку функция reader_loop выполнила все проверки и прочитала заданное имя программы и аргументы, она вызывает функцию execute_command из файла исходного кода execute_cmd.c. Функция execute_command через цепочку вызовов функций:

делает различные проверки, например, нужно ли нам запускать subshell , была ли это встроенная функция bash или нет и т. д. Как я уже писал выше, мы не будем рассматривать все подробности о вещах, не связанных с ядром Linux. В конце этого процесса функция shell_execve вызывает системный вызов execve:

и выполняет программу с заданным именем файла, с заданными аргументами и переменными среды. Этот системный вызов в нашем случае первый и единственный, например:

Итак, пользовательское приложение (в нашем случае bash) вызывает системный вызов, и, как мы уже знаем, следующим шагом является ядро ​​Linux.

выполнить системный вызов

Мы видели подготовку перед системным вызовом, вызванным пользовательским приложением, и после того, как обработчик системного вызова завершил свою работу во второй части этой главы. Мы остановились на вызове системного вызова execve в предыдущем пункте. Этот системный вызов определен в файле исходного кода fs/exec.c и, как мы уже знаем, принимает три аргумента:

Реализация execve здесь довольно проста, поскольку мы видим, что она просто возвращает результат функции do_execve. Функция do_execve, определенная в том же файле исходного кода, выполняет следующие действия:

  • Инициализировать два указателя на данные пользовательского пространства с заданными аргументами и переменными среды;
  • вернуть результат do_execveat_common .

Мы видим его реализацию:

Функция do_execveat_common выполняет основную работу — запускает новую программу. Эта функция принимает аналогичный набор аргументов, но, как видите, она принимает пять аргументов вместо трех. Первый аргумент — это дескриптор файла, представляющий каталог с нашим приложением, в нашем случае AT_FDCWD означает, что данный путь интерпретируется относительно текущего рабочего каталога вызывающего процесса. Пятый аргумент — флаги. В нашем случае мы передали 0 в do_execveat_common. Мы проверим на следующем шаге, так что увидим позже.

Прежде всего функция do_execveat_common проверяет указатель имени файла и возвращает значение NULL . После этого проверяем флаги текущего процесса, чтобы не превышался лимит запущенных процессов:

Если эти две проверки прошли успешно, мы сбрасываем флаг PF_NPROC_EXCEEDED в флагах текущего процесса, чтобы предотвратить сбой execve. Вы можете видеть, что на следующем шаге мы вызываем функцию unshare_files, определенную в файле kernel/fork.c, и удаляем файлы текущей задачи и проверяем результат этой функции:

Нам нужно вызвать эту функцию, чтобы устранить возможную утечку файлового дескриптора исполняемого двоичного файла. На следующем шаге мы начинаем подготовку bprm, представленного структурой struct linux_binprm (определенной в файле include/linux/binfmts.h заголовочный файл). Структура linux_binprm используется для хранения аргументов, используемых при загрузке двоичных файлов. Например, он содержит поле vma, которое имеет тип vm_area_struct и представляет собой одну область памяти на непрерывном интервале в заданном адресном пространстве, где будет загружено наше приложение, поле mm, которое является дескриптором памяти двоичного файла, указатель на верхнюю часть памяти и многое другое. разные поля.

Прежде всего мы выделяем память для этой структуры с помощью функции kzalloc и проверяем результат выделения:

После этого мы начинаем готовить учетные данные binprm с помощью вызова функции prepare_bprm_creds:

Иными словами, инициализация учетных данных binprm — это инициализация структуры cred, хранящейся внутри структуры linux_binprm. Структура cred содержит контекст безопасности задачи, например, реальный uid задачи, реальный guid задачи, uid и guid для операций с виртуальной файловой системой и т. д. На следующем шаге, выполняя подготовку учетных данных bprm, мы проверяем, что теперь мы можем безопасно выполнить программу вызовом функции check_unsafe_exec и установить текущий процесс в состояние in_execve.

После всех этих операций мы вызываем функцию do_open_execat, которая проверяет флаги, которые мы передали функции do_execveat_common (помните, что в флагах у нас стоит 0), и ищет и открывает исполняемый файл на диске, проверяет, что наш файл будет загружен. двоичный файл из точек монтирования noexec (нам нужно избегать запуска двоичного файла из файловых систем, которые не содержат исполняемых двоичных файлов, таких как proc или sysfs), инициализирует файловую структуру и возвращает указатель на эту структуру. Далее мы можем увидеть вызов sched_exec после этого:

Функция sched_exec используется для определения наименее загруженного процессора, который может выполнить новую программу, и для переноса на него текущего процесса.

После этого нам нужно проверить файловый дескриптор данного исполняемого файла. Мы пытаемся проверить, начинается ли имя нашего бинарного файла с символа / или интерпретируется ли путь данного исполняемого бинарника относительно текущего рабочего каталога вызывающего процесса или, другими словами, дескриптор файла AT_FDCWD (читайте выше о это).

Если одна из этих проверок прошла успешно, мы устанавливаем имя файла бинарного параметра:

В противном случае, если имя файла пусто, мы устанавливаем имя файла двоичного параметра в /dev/fd/%d или /dev/fd/%d/%s в зависимости от имени файла данного исполняемого двоичного файла, что означает, что мы будем выполнять файл, на который ссылается файловый дескриптор:

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

Функция bprm_mm_init определена в том же файле исходного кода, и, как мы можем понять из названия функции, она выполняет инициализацию дескриптора памяти или, другими словами, функция bprm_mm_init инициализирует структуру mm_struct. Эта структура определена в заголовочном файле include/linux/mm_types.h и представляет собой адресное пространство процесса. Мы не будем рассматривать реализацию функции bprm_mm_init, потому что мы не знаем многих важных вещей, связанных с диспетчером памяти ядра Linux, но нам просто нужно знать, что эта функция инициализирует mm_struct и заполняет его временным стеком vm_area_struct .

После этого мы вычисляем количество аргументов командной строки, которые были переданы нашему исполняемому двоичному файлу, количество переменных среды и устанавливаем их в bprm->argc и bprm->envc соответственно:

Как видите, мы выполняем эти операции с помощью функции count, которая определена в том же файле исходного кода и вычисляет количество строк в массиве argv. Макрос MAX_ARG_STRINGS определен в заголовочном файле include/uapi/linux/binfmts.h и, как мы можем понять из названия макроса, представляет собой максимальное количество строк, которые были переданы системному вызову execve. Значение MAX_ARG_STRINGS:

После того, как мы подсчитали количество аргументов командной строки и переменных среды, мы вызываем функцию prepare_binprm. Мы уже вызывали функцию с похожим именем до этого момента. Эта функция называется prepare_binprm_cred, и мы помним, что эта функция инициализирует структуру cred в linux_bprm. Теперь функция prepare_binprm:

заполняет структуру linux_binprm идентификатором пользователя из inode и считывает 128 байтов из исполняемого двоичного файла. Мы читаем только первые 128 из исполняемого файла, потому что нам нужно проверить тип нашего исполняемого файла. Мы прочитаем остальную часть исполняемого файла на следующем шаге.После подготовки структуры linux_bprm копируем имя исполняемого бинарного файла, аргументы командной строки и переменные окружения в linux_bprm вызовом функции copy_strings_kernel:

И установите указатель на вершину стека новой программы, которую мы установили в функции bprm_mm_init:

Вершина стека будет содержать имя файла программы, и мы сохраняем это имя файла в поле exec структуры linux_bprm.

Теперь мы заполнили структуру linux_bprm, вызываем функцию exec_binprm:

Прежде всего мы сохраняем pid и pid, видимые из пространства имен текущей задачи в exec_binprm :

функция. Эта функция просматривает список обработчиков, содержащих различные двоичные форматы. В настоящее время ядро ​​Linux поддерживает следующие двоичные форматы:

Итак, search_binary_handler пытается вызвать функцию load_binary и передать ей linux_binprm. Если двоичный обработчик поддерживает заданный формат исполняемого файла, он начинает подготовку исполняемого двоичного файла к выполнению:

Где load_binary например для эльфа проверяет магическое число (каждый бинарный файл эльфа содержит магическое число в заголовке) в буфере linux_bprm (помните, что мы читаем первые 128 байт из исполняемого бинарного файла): и выходим, если оно не является бинарным эльфом:

Если данный исполняемый файл имеет формат elf, load_elf_binary продолжает выполняться. load_elf_binary делает много разных вещей для подготовки исполняемого файла к исполнению. Например, он проверяет архитектуру и тип исполняемого файла:

и выйти, если есть неправильная архитектура и исполняемый файл, неисполняемый, необщий. Пытается загрузить таблицу заголовков программы:

который описывает сегменты. Считайте с диска интерпретатор программы и библиотеки, связанные с нашим исполняемым бинарным файлом, и загрузите его в память. Интерпретатор программы указан в секции .interp исполняемого файла и, как вы можете прочитать в части, описывающей линкеры, это - /lib64/ld-linux-x86-64.so.2 для x86_64. Он устанавливает стек и сопоставляет двоичный файл elf в правильном месте в памяти. Он сопоставляет разделы bss и brk и выполняет множество других действий для подготовки исполняемого файла к выполнению.

В конце выполнения load_elf_binary мы вызываем функцию start_thread и передаем ей три аргумента:

  • Набор регистров для новой задачи;
  • Адрес точки входа новой задачи;
  • Адрес вершины стека для новой задачи.

Как мы поняли из названия функции, она запускает новый поток, но это не так. Функция start_thread просто подготавливает регистры новой задачи к запуску. Давайте посмотрим на реализацию этой функции:

Как мы видим, функция start_thread просто вызывает функцию start_thread_common, которая сделает все за нас:

Функция start_thread_common заполняет регистр сегмента fs нулем, а es и ds значением регистра сегмента данных. После этого мы присваиваем новые значения указателю инструкции, сегментам cs и т. д. В конце функции start_thread_common мы видим макрос force_iret, который вызывает возврат системного вызова через инструкцию iret. Хорошо, мы подготовили новый поток для запуска в пользовательском пространстве, и теперь мы можем вернуться из exec_binprm, и теперь мы снова в do_execveat_common. После того, как exec_binprm завершит свое выполнение, мы освобождаем память для структур, которые были выделены ранее, и возвращаемся.

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

Это все. С этого момента наша программа будет выполняться.

Заключение

Это конец четвертой части концепции системных вызовов в ядре Linux. В этих четырех частях мы увидели почти все, что связано с концепцией системных вызовов. Мы начали с понимания концепции системного вызова, мы узнали, что это такое и зачем пользовательские приложения нуждаются в этой концепции. Затем мы увидели, как Linux обрабатывает системный вызов из пользовательского приложения. Мы познакомились с двумя концепциями, схожими с концепцией системных вызовов, это vsyscall и vDSO, и, наконец, мы увидели, как ядро ​​Linux запускает пользовательскую программу.

Если у вас есть вопросы или предложения, не стесняйтесь пинговать меня в твиттере 0xAX, напишите мне по электронной почте или просто создайте проблему.

Обратите внимание, что английский не является моим родным языком, и я приношу извинения за причиненные неудобства. Если вы нашли какие-либо ошибки, пришлите мне PR в linux-insides.


Что происходит, когда файл запускается в Linux? Что означает, что файл является исполняемым? Можем ли мы выполнять только скомпилированные двоичные файлы? Как насчет сценариев оболочки? Если я могу выполнять сценарии оболочки, что еще я могу выполнять? В этой статье мы постараемся ответить на эти вопросы.

Что включает в себя выполнение файла

Начиная с основ, давайте попробуем понять, что происходит, когда мы вводим в терминале следующее

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

Системным вызовом, отвечающим за выполнение файлов, является системный вызов execve(). Когда мы пишем код, мы обычно обращаемся к нему через семейство функций exec, присутствующих в стандартной библиотеке, или даже чаще через абстракции более высокого уровня, такие как popen() или system().

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

Это отличается от того, как мы привыкли запускать исполняемые файлы из терминала. Когда мы набираем в терминале sleep 30, мы получаем дочерний процесс bash, и последний не исчезает.

Здесь в игру вступает еще один системный вызов, системный вызов fork(). bash сначала создаст свою копию в другом дочернем процессе, а затем этот дочерний процесс вызовет execve(), чтобы перевести себя в спящий режим. Таким образом, bash не исчезнет и будет там, чтобы взять на себя управление, когда sleep отключится через 30 секунд.

Мы можем пропустить шаг разветвления с помощью exec bash bultin

В другом терминале мы видим, что спящий режим берет на себя PID

И, конечно же, через 30 секунд после выхода из режима sleep сеанса bash не будет, поэтому окно терминала закроется.

Основная часть вещей

До сих пор мы видели взаимодействие пользовательского пространства через системные вызовы, давайте посмотрим, что происходит с другой стороны.

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

Это точка входа системного вызова по адресу fs/exec.c

.

С этого момента ядро ​​сначала выполняет все необходимые приготовления, чтобы начать выполнение двоичного файла, например, настройку виртуальной памяти для процесса

и установка имени файла, аргументов командной строки и унаследованной среды. Да, переменные окружения также являются неотъемлемой частью ядра Linux.

Наконец файл «будет выполнен».

Двоичные файлы ELF

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

Код ядра, отвечающий за синтаксический анализ формата ELF, находится в файле fs/binfmt_elf.c. Здесь заголовки ELF считываются и анализируются

, а разделы PT_LOAD загружаются в виртуальную память

Это немного сложнее для динамически связанных программ. Ядро распознает динамически компонуемую программу по наличию заголовка PT_INTERP.

Этот заголовок жестко запрограммирован во время компиляции с путем компоновщика среды выполнения ld-linux-x86-64.so, который необходимо использовать для его запуска. Компоновщик среды выполнения найдет в файловой системе библиотеки .so, необходимые для выполнения двоичного файла, и загрузит их в память.

В этом простом случае требуется динамическое связывание только со стандартной библиотекой C, поскольку vDSO — это специальная виртуальная библиотека, предназначенная для эффективного выполнения системных вызовов только для чтения.

Ядро распознает заголовок PT_INTERP, а также загрузит компоновщик времени выполнения (также известный как интерпретатор ELF) ld.so и выполнит его.

Затем ld.so найдет и загрузит динамические библиотеки .so или потерпит неудачу, если какой-либо неопределенный символ останется неразрешенным, и, наконец, перейдет к началу выполнения (AT_ENTRY ) исходного двоичного кода.

Мы понимаем, что ядро ​​проверяет двоичный файл и обрабатывает его, но на самом деле мы выполняем не наш двоичный файл, а интерпретатор ELF ld.so. Технически это машинный код нашего двоичного файла, который все еще выполняется, но мы сталкиваемся с концепцией, интерпретатор, который на самом деле является исполняемым, и именно он фактически «интерпретирует» наш файл. .

В конце концов, мы делаем это

А как насчет скриптов?

Я изо всех сил старался избегать слова двоичный, потому что на самом деле вы можете "запускать" файлы, которые не являются двоичными. Технически эти файлы выполняются не сами по себе, поскольку они не обязательно содержат машинный код, а исполняются интерпретатором, как мы только что видели.

Это означает, что для выполнения требуется нечто большее, чем просто ELF: Linux поддерживает множество бинарных форматов, и ELF — лишь один из них. Внутри ядра каждый двоичный формат управляется обработчиком, который знает, как работать с указанным файлом. Некоторые обработчики поставляются со стандартным ядром, но некоторые другие можно добавить с помощью загружаемых модулей.

Всякий раз, когда файл должен быть выполнен с помощью execve(), его первые 128 байтов считываются и передаются каждому обработчику. Это происходит в файле fs/exec.c

Затем каждый обработчик может принять его или проигнорировать, обычно в зависимости от некоторой магии в первых байтах двоичного файла. Таким образом, соответствующий обработчик позаботится о выполнении этого двоичного файла или передаст шанс сделать это другому обработчику.

В случае формата ELF волшебством является 0x7F ‘ELF’ в поле e_ident

Это проверяется обработчиком ELF в binfmt_elf.c, чтобы принять двоичный файл.

Что происходит со сценариями? оказывается, для этого в ядре есть обработчик, который можно найти в binfmt_script.c.

Все обработчики двоичного формата предлагают интерфейс для execve(), например, это интерфейс для формата ELF

Основным хуком здесь является функция load_binary(), которая будет отличаться для каждого обработчика.

В случае обработчика скрипта его хук load_binary() находится в файле binfmt_script.c и начинается так.

Еще раз: мы на самом деле не выполняем наш скрипт, а делаем что-то вроде

Мы начинаем понимать, что название двоичный формат немного вводит в заблуждение, поскольку мы можем выполнять недвоичные файлы. Существуют и другие обработчики экзотических или старых двоичных форматов, таких как плоский формат или старый формат a.out, и, в частности, очень мощный и универсальный обработчик. , обработчик binfmt_misc

Выполнить что-либо с помощью bitfmt_misc

Теперь мы знаем, что такое бинарный обработчик, и можем понять binfmt_misc. Это гибкий обработчик формата, который позволяет нам указать, какой интерпретатор пользовательской среды должен запускаться для определенного типа файла. Он не просто просматривает захардкоженную магию в начале файла, но также поддерживает определение двоичного файла по расширению, используя маски, и предлагает системному администратору интерфейс /proc. Помните, что все это происходит в пространстве ядра. Загрузчик для этого обработчика — load_misc_binary() в fs/binfmt_misc.c.

Если интерфейс /proc еще не смонтирован для нас, мы можем сделать это с помощью

Давайте посмотрим

Мы видим, что он уже заполнен некоторыми элементами Python и другими элементами.

Мы можем удалить, включить или отключить эти записи.

  • выведите 1, чтобы включить запись
  • выведите 0, чтобы отключить запись
  • echo -1 для удаления записи

Что действительно здорово, так это то, что мы можем легко добавлять собственные записи через binfmt_misc. Чтобы добавить запись, вы выводите строку формата для register. Подробности о том, как настроить все эти флаги, маски и магические значения, можно найти здесь.

В качестве примера давайте создадим обработчик для формата изображения JPG, который будет открываться средством просмотра изображений feh. В этом случае мы сопоставляем по расширению (поэтому E )

Обработчик зарегистрирован. Можем проверить

Теперь давайте сделаем снимок, создадим исполняемый файл, и вуаля.


Теперь создадим исполняемый список TODO на основе обнаружения магического числа ( M ).

Файлы PDF начинаются с текста %PDF , как показано в спецификации.

Другой пример, файлы Libreoffice по расширению


У нас есть все новые записи в proc

С помощью этой техники мы можем прозрачно запускать Java-приложения (на основе магии 0xCAFEBABE)

Для этого требуется обертка, которую вы можете получить на Arch Wiki.

Это также работает для Mono и даже для DOS!Чтобы прозрачно запустить старую добрую Civilization, установите dosbox, настройте binfmt_misc


Некоторые подходят для двоичных файлов, эмулированных Windows


Проблема в том, что все двоичные файлы DOS, Windows и Mono используют одну и ту же магию MZ, поэтому для их объединения нам потребуется использовать специальную оболочку, способную обнаруживать различия глубже в файле, например start .exe.

Если мы хотим сделать эти настройки постоянными, мы можем настроить эту конфигурацию во время загрузки в /etc/binfmt.d. Например, если мы хотим настроить обработчик PDF во время загрузки, мы просто добавляем новый файл в папку

Мы видим, что этот подход очень гибкий и мощный. Одна из проблем заключается в том, что немного необычно иметь «обычные файлы», отличные от скриптов, в качестве исполняемых файлов. Хорошо то, что это действительно общесистемная настройка, поэтому, как только вы настроите ее в ядре, она будет работать из всех ваших оболочек, файловых менеджеров и любого другого пользователя execve() системный вызов.

В следующем посте мы увидим еще несколько полезных вещей, которые можно сделать с помощью binfmt_misc.

Ссылки

Эта статья предназначена для краткого обзора binfmt_misc и процесса выполнения с некоторыми практическими примерами. Если вам нужны кровавые подробности, прочтите следующие ссылки.

Читайте также: