Как запустить программу в Linux
Обновлено: 24.11.2024
Как вы программируете на C в Linux? Это действительно очень просто и состоит из трех простых шагов.
Шаг 1. Вы пишете свою программу и сохраняете файл с расширением .c. Например, my_program.c.
Шаг 2. Вы компилируете программу и создаете объектный файл с помощью компилятора gcc в таком терминале:
Шаг 3. Вы запускаете сгенерированный объектный файл для запуска вашей программы C в Linux:
Это был всего лишь краткий обзор того, как компилировать и запускать программу C в Linux. Если вы новичок в C или Linux, я подробно покажу эти шаги, чтобы вы чувствовали себя комфортно при написании программы C в среде Linux.
На самом деле, я расскажу, как запускать программы C в терминале Linux, а также в редакторе кода.
Метод 1. Как запускать программы C в терминале Linux
Чтобы запустить программу C в Linux, в вашей системе должен быть установлен компилятор C. Наиболее популярным компилятором является gcc (GNU Compiler Collection).
Вы можете установить gcc с помощью менеджера пакетов вашего дистрибутива. В дистрибутивах Linux на основе Debian и Ubuntu используйте команду apt:
Перейдите в каталог, в котором вы сохранили свою программу C (или укажите путь), а затем сгенерируйте объектный файл, скомпилировав программу:
Имейте в виду, что указывать выходной объектный файл (-o my_program) необязательно. Если вы этого не сделаете, будет автоматически сгенерирован объектный файл с именем a.out. Но это нехорошо, потому что он будет перезаписан для каждой программы на C, и вы не сможете узнать, какой программе принадлежит объектный файл a.out.
После создания объектного файла запустите его, чтобы запустить программу C. Он уже исполняемый. Просто используйте это так:
И он отобразит желаемый результат, если ваша программа верна. Как видите, это не сильно отличается от запуска программ C++ в Linux.
Каждый раз, когда вы вносите изменения в свою программу, вы должны сначала скомпилировать ее, а затем запустить сгенерированный объектный файл для запуска программы C.
Вы должны убедиться, что в вашей системе Linux установлен компилятор gcc.
Следующее, что вам нужно, это использовать расширение, позволяющее запускать код C. Microsoft может предложить вам установить собственное расширение для программы C/C++, но оно сложно в установке, поэтому я не буду его рекомендовать.
Вместо этого я предлагаю использовать расширение Code Runner. Это серьезное расширение, и вы можете легко запускать код C и C++ без дополнительной настройки.
Перейдите на вкладку "Расширения", найдите "Code Runner" и установите его.
Перезапустите Visual Studio Code. Теперь вы сможете запустить код C одним из следующих способов:
Когда вы запускаете программу, она автоматически компилируется, а затем запускается. Вы можете увидеть вывод в терминале, который открывается в нижней части редактора. Что может быть лучше этого?
Какой метод вы предпочитаете?
Выполнение нескольких программ на C в командной строке Linux — это нормально, но использование редактора кода намного проще и экономит время. Вы не согласны?
Это четвертая часть главы, описывающая системные вызовы в ядре Linux, и, как я писал в заключении предыдущей, эта часть будет последней в этой главе. В предыдущей части мы остановились на двух новых концепциях:
которые связаны и очень похожи по концепции системных вызовов.
Эта часть будет последней в этой главе, и, как вы можете понять из названия части, мы увидим, что происходит в ядре Linux, когда мы запускаем наши программы. Итак, приступим.
как мы запускаем наши программы?
Существует множество различных способов запуска приложения с точки зрения пользователя. Например, мы можем запустить программу из оболочки или дважды щелкнуть значок приложения. Неважно. Ядро Linux обрабатывает запуск приложения независимо от того, как мы запускаем это приложение.
В этой части мы рассмотрим способ, когда мы просто запускаем приложение из оболочки. Как вы знаете, стандартный способ запуска приложения из оболочки следующий: мы просто запускаем приложение эмулятора терминала и просто пишем название программы и передаем или не передаем аргументы нашей программе, например:
Давайте рассмотрим, что происходит, когда мы запускаем приложение из оболочки, что делает оболочка, когда мы пишем имя программы, что делает ядро 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 становится раем для разработчиков, поскольку это бесплатная операционная система с открытым исходным кодом. Компилятор Turbo C — это уже старый подход к компиляции программ, поэтому давайте, программисты, перейдем на Linux для новой среды программирования. В этой статье мы объясним, как написать, скомпилировать и запустить простую программу на C. Это послужит вам основой для перехода к более сложным и полезным программам на C, которые вы сможете писать и выполнять в Linux.
Мы выполнили шаги и команды, упомянутые в этой статье, в системе Ubuntu 20.04 LTS, но они будут работать и в других версиях, таких как Ubuntu 18.04, или дистрибутивах, таких как Debian 10, точно так же.
Мы будем использовать инструмент командной строки Linux, Терминал, для компиляции простой программы на C. Чтобы открыть Терминал, вы можете использовать Ubuntu Dash или сочетание клавиш Ctrl+Alt+T.
Шаг 1. Установите необходимые для сборки пакеты
Чтобы скомпилировать и выполнить программу C, в вашей системе должны быть установлены необходимые пакеты.Введите следующую команду от имени пользователя root в терминале Linux:
Вас попросят ввести пароль для root; после этого начнется процесс установки. Убедитесь, что вы подключены к Интернету.
Шаг 2. Напишите простую программу на C
После установки необходимых пакетов давайте напишем простую программу на C.
Откройте графический текстовый редактор Ubuntu и напишите или скопируйте в него следующий пример программы:
Затем сохраните файл с расширением .c. В этом примере я называю свою программу на C как sampleProgram.c
В качестве альтернативы вы можете написать программу C через Терминал в gedit следующим образом:
Это создаст файл .c, в котором вы сможете написать и сохранить программу.
Шаг 3. Скомпилируйте программу C с помощью компилятора gcc
В терминале введите следующую команду, чтобы сделать исполняемую версию написанной вами программы: Реклама
Убедитесь, что ваша программа находится в вашей домашней папке. В противном случае вам нужно будет указать соответствующие пути в этой команде.
Шаг 4. Запустите программу
Последний шаг — запустить скомпилированную программу на C. Для этого используйте следующий синтаксис:
Вы можете увидеть, как программа выполняется в приведенном выше примере, отображая текст, который мы написали для печати.
Из этой статьи вы узнали, как написать, скомпилировать и запустить простую программу на C в Linux. Все, что вам нужно, это необходимые пакеты и нужные навыки, чтобы стать гуру программирования в Linux!
Карим Буздар
Об авторе: Карим Буздар имеет степень инженера в области телекоммуникаций и несколько сертификатов системного администратора. Как ИТ-инженер и технический автор, он пишет для различных веб-сайтов. Вы можете связаться с Каримом в LinkedIn
Основные команды Unix (включая Cygwin)
Вот некоторые основные команды Unix для навигации и изучения данных. Поскольку Unix часто запускается в среде командной строки, вам придется вводить команды, чтобы просматривать, редактировать и изменять файлы и перемещаться между ними. Вот самые основные команды, которые вам нужно знать, чтобы уверенно запускать DEX.
Список файлов и папок, введите:
- ls — список файлов и папок в текущем каталоге.
- ls -lt — выводит дополнительную информацию обо всех файлах/папках в текущем каталоге, по одному файлу/папке в строке
Соответствие шаблону
- * – Пример: � ls time* � выведет список всех файлов, начинающихся со слова ”time”, в то время как команда
� ls time*.dat – будет соответствовать всему в папке, начинающемуся с "time" и заканчивающемуся на ".dat". Это очень мощная функция Unix, и ее следует использовать для уменьшения количества команд, необходимых для выполнения программы.
- Tab — кнопка «Tab» дополняет имя файла или папки, если оно уникально. Например, если есть только один файл, который начинается с «время-2», то простой ввод «ls time-2 (Tab)» завершит файл без необходимости вводить остальные буквы. Это экономит много времени, когда большинство файлов уникальны.
- Ctrl d — набирается, если вы закончили вводить часть имени файла и вам нужно выяснить, в имени каких файлов есть такая же начальная часть. Например, если ввести «ls time-2 (Ctrl d)», отобразятся все файлы, имя которых начинается с «time-2». После ввода этого вы можете увидеть, как называется точное имя файла, который вы хотите, и ввести остальную часть имени. Когда вы комбинируете «Ctrl d» с кнопкой Tab, вам обычно нужно ввести всего несколько букв для каждого имени, даже если оно длинное. Это значительно экономит время.
- Стрелка вверх. При вводе стрелки вверх в командной строке отображается последняя выполненная команда. Вы можете просто изменить этот файл или запустить его снова, чтобы увидеть результаты того же или нового файла.
Изменение папок/управление папками
- cd имя-папки — имя-папки — это имя папки, в которую вы хотите переместиться
- pwd – показывает, в какой папке вы сейчас находитесь.
- mkdir — создает папку (каталог)
- rmdir — удаляет папку (каталог), но она должна быть пустой
Изменение/перемещение файла/имен файлов
- cp old-filename new-filename — копирует старый файл в новое имя файла, не стирая старый файл.
- mv old-filename new-filename — изменяет старый файл на новое имя файла и стирает старый файл.
- rm имя файла – удаляет введенное вами имя файла.
Печать файла
- lpr �Pprinter-name — имя-принтера — это имя сетевого принтера Unix. Если у вас нет настройки принтера с помощью команд Unix, вам придется найти файл в оконной системе и распечатать его так, как вы обычно это делаете. Пользователи Cygwin: вы можете открывать обычные текстовые файлы в MS Word, сохранять и распечатывать их в обычном режиме.
Просмотр содержимого файла
- имя файла vi. vi – это визуальная программа для просмотра текста в файле, например результатов работы программы.
- head filename — показывает первые 10 строк файла
- tail filename — показывает последние 10 строк файла.
Запуск/остановка программ
- Чтобы запустить программу, вам нужно всего лишь ввести ее имя. Вам может потребоваться ввести �./� перед именем, если ваша система не проверяет наличие исполняемых файлов в этом файле.
- Ctrl c — эта команда отменит запущенную программу или не завершит ее автоматически. Это вернет вас в командную строку, чтобы вы могли запустить что-то еще.
После того, как вы введете приведенную выше команду, в окне отобразится текст. В отличие от обычной программы для работы с документами, vi не позволяет вам просто печатать с клавиатуры. Вместо этого у каждой клавиши есть функция, например, перемещение курсора влево или вниз. Вот основные команды, которые вы можете использовать для редактирования и сохранения команд. Вы можете использовать клавиши со стрелками, как обычно, и их необходимо использовать для перемещения курсора в то место, где вы хотите начать ввод/редактирование. Мышь не будет работать в стандартной программе «vi», используется только клавиатура. К этому нужно привыкнуть.
Читайте также: