Пошаговый запуск программы в Linux x86, или как добраться до main()?

Статья предназначена для тех, кто хочет понять процесс загрузки программ в Linux. В частности, здесь пойдет речь о динамической загрузке файлов ELF x86. На основе изложенной информации вы сможете лучше понять, как устранять проблемы, возникающие в программе еще до запуска main .
Весь этот материал является актуальным, но кое-какие моменты в нем были опущены, так как к основной цели отношения не имеют. Кроме того, если вы выполняете линковку статически, то некоторые нюансы будут отличаться. Все эти детали я разбирать не стану, но к завершению вы будете знать достаточно, чтобы разобраться самостоятельно.
Вот предстоящий нам маршрут:
Схема получена с помощью dot-фильтра, используемого для рисования направленных графов
К концу статьи вам все это станет понятно.
Как мы попадаем в main?
Мы соберем простейшую программу Си с пустой функцией main , а затем рассмотрим ее дизассемблированную версию, чтобы понять весь путь запуска. В ходе этого процесса вы увидите, что первым делом выполняется линкуемая с каждой программой функция _start , которая в конечном итоге приводит к выполнению main .
Если хотите, можете сохранить копию этой программы как prog1.c и повторять за мной. Первым делом я выполню ее сборку:
Прежде, чем переходить к отладке последующей версии этой программы ( prog2 ) в gdb , мы ее дизассемблируем и узнаем некоторые детали о процессе запуска. Я покажу вам вывод objdump -d prog1 , но не в порядке фактического вывода, а в порядке его выполнения. (В идеале вам следует сделать это самим. Например, сохранить копию с помощью objdump -d prog1 >prog1.dump , чтобы потом просмотреть ее в привычном редакторе).
Для начала разберемся, как мы попадаем в _start
При запуске программы оболочка или GUI вызывают execve() , которая выполняет системный вызов execve() . Если вы хотите побольше узнать об этом системном вызове, то просто введите в оболочке man execve . Он находится в разделе 2 мануала вместе со всеми остальными системными вызовами. Если кратко, то он настраивает стек и передает в него argc , argv и envp .
Дескрипторы файлов 0 , 1 и 2 ( stdin , stdout , stderr ) остаются со значениями, установленными для них оболочкой. Загрузчик проделывает много работы, настраивая релокации и, как мы увидим позднее, вызывая пре-инициализаторы. Когда все готово, управление передается программе через вызов _start() .
Вот соответствующий раздел objdump -d prog1 :
Операция XOR элемента с самим собой устанавливает этот элемент на нуль. Поэтому xor %ebp,%ebp устанавливает %ebp на нуль. Это предполагается спецификацией ABI (Application Binary Interface) для обозначения внешнего фрейма.
Далее мы извлекаем верхний элемент стека. Здесь у нас на входе argc , argv и envp , значит операция извлечения отправляет argc в %esi . Мы планируем просто сохранить его и вскоре вернуть обратно в стек. Так как argc мы извлекли, %esp теперь указывает на argv . Операция mov помещает argv в %ecx , не перемещая указатель стека.
Теперь мы выполняем для указателя стека операцию and с маской, которая обнуляет нижние четыре бита. В зависимости от того, где находился указатель, он переместится ниже на величину от 0 до 15 байт, что приведет к выравниванию кратно 16 байтам. За счет подобного выравнивания элементов стека повышается эффективность обработки памяти и кэша. В частности, это необходимо для SSE (Streaming SIMD Extensions), инструкций, способных одновременно обрабатывать вектора с плавающей точкой одинарной точности.
В конкретно этом случае %esp на входе в _start имел значение 0xbffff770 . После того, как мы извлекли argc , %esp стал 0xbffff774 , то есть сместился на более высокий адрес (добавление элементов в стек ведет к перемещению по памяти вниз, а их извлечение – вверх). После выполнения and значение указателя стека вновь стало 0xbffff770 .
Далее устанавливаем значения для вызова __libc_start_main
Теперь мы начинаем передавать в стек аргументы для _libc_start_main . Первый, %eax , является мусором, который передается только потому, что в стек мы собираемся поместить 7 элементов, а для 16-байтового выравнивания требуется 8-й. Использоваться он не будет. Сама функция _libc_start_main линкуется из glibc . В дереве исходного кода glibc она находится в файле csu/libc-start.c . Определяется _libc_start_main так:
Итак, мы ожидаем, что _start передаст обозначенные аргументы в стек в обратном порядке до вызова _libc_start_main .

Содержимое стека перед вызовом __libc_start_main
__libc_csu_fini линкуется в наш код из glibc и находится в файле csu/elf-init.c дерева исходного кода. Это деструктор нашей программы на уровне Си, и чуть позже я разберу его подробно.
Так, а где переменные среды?
Вы заметили, что мы не получили из стека envp , указатель на наши переменные среды? Среди аргументов _libc_start_main его тоже нет. Но мы знаем, что main называется int main(int argc, char** argv, char** envp) , так в чем же дело?
Что ж, _libc_start_main вызывает _libc_init_first , которая с помощью секретной внутренней информации находит переменные среды сразу после завершающего нуля вектора аргументов, после чего устанавливает глобальную переменную _environ , которую _libc_start_main при необходимости использует впоследствии, в том числе в вызовах main .
После установки envp функция _libc_start_main использует тот же трюк и…вуаля! Сразу за завершающим нулем в конце массива envp находится очередной вектор, а именно вспомогательный вектор ELF, который загрузчик использует для передачи процессу определенной информации. Для просмотра его содержимого достаточно просто установить перед запуском программы переменную среды LD_SHOW_AUXV=1 . Вот результат для нашей prog1 :
Разве не интересно? Тут полно всяческой информации.
- Здесь мы видим AT_ENTRY , представляющую адрес _start , где находится наш userid , действующий userid и groupid .
- Очевидно, что используется платформа 686 , а частота times() равна 100 тактов/с.
- AT_PHDR указывает расположение ELF-заголовка программы, в котором хранится информация о нахождении всех сегментов этой программы в памяти, а также о записях релокаций и всем остальном, что нужно знать загрузчику.
- AT_PHENT – это просто количество байт в записи заголовка.
__libc_start_main в общем
На этом я закончу разбор деталей _libc_start_main и лишь добавлю, что в общем он:
- Реализует функционал безопасности с помощью вызовов setuid и stgid ;
- Запускает потоковую обработку;
- Регистрирует аргументы fini (для нашей программы) и аргументы rtld_fini (для загрузчика среды выполнения), которые запустит at_exit для выполнения процедур очистки программы и загрузчика.
- Вызывает аргумент init ;
- Вызывает main с передаваемыми ей аргументами argc и argv , а также аргументом global_environ , о чем я писал выше;
- Вызывает exit с возвращаемым main значением.
Вызов аргумента init
Аргумент init для _libc_start_main устанавливается на _libc_csu_init , который также линкуется в наш код. Он компилируется из программы Си, расположенной в файле csu/elf-init.c дерева исходного кода glibc , и линкуется в нашу программу. Его код Си похож на (но содержит намного больше #ifdef )…
Конструктор нашей программы
_libc_csu_init для нашей программы очень важен, так как конструирует ее исполняемый файл. Я уже слышу, как вы говорите: «Это же не C++!». Все верно, но принцип конструкторов и деструкторов не принадлежит к C++ и предшествовал этому языку.
Наш исполняемый файл и любой другой его аналог получает на уровне Си конструктор _libc_csu_init и деструктор _libc_csu_fini . Внутри конструктора, как вы увидите далее, исполняемый файл ищет глобальные конструкторы уровня Си и вызывает любой, который найдет. В программе Си они также могут присутствовать, и в ходе статьи я это продемонстрирую. Хотя, если для вас будет удобнее, можете называть их инициализаторы и финализаторы. Вот код ассемблера, сгенерированный для _libc_csu_init :
Что такое thunk?
Говорить здесь особо не о чем, но я подумал, что вы захотите это увидеть. Функция get_pc_thunk весьма интересна. Она вызывается для настройки позиционно-независимого кода. Чтобы все сработало, указатель базы должен иметь адрес GLOBAL_OFFSET_TABLE . Соответствующий код выглядел так:
Посмотрим на происходящее подробнее. Вызов _get_pc_thunk_bx , как и любой другой, помещает в стек адрес следующей функции, чтобы при возвращении выполнение продолжилось с очередной инструкции. В данном случае нам нужен тот самый адрес. Значит, в _get_pc_thunk_bx мы копируем адрес возврата из стека в %ebx . Когда происходит возврат, очередная инструкция прибавляет к нему _GLOBAL_OFFSET_TABLE_ , разрешаясь в разницу между текущим адресом и глобальной таблицей смещений, используемую позиционно-независимым кодом.
В этой таблице хранится набор указателей на данные, к которым мы хотим обратиться, и нам лишь нужно знать их смещения. При этом загрузчик сам фиксирует для нас нужный адрес. Для обращения к процедурам существует аналогичная таблица. Было бы поистине утомительно программировать подобным образом в ассемблере, но можно просто написать нужный код на Си/С++ и передать аргумент -pic компилятору, который сделает это автомагически.
Встречая данный код в ассемблере, вы можете сделать вывод, что исходник был скомпилирован с флагом -pic .
Но что это за цикл?
Цикл из _libc_csu_init мы рассмотрим сразу после вызова init() , который фактически вызывает _init . Пока же просто имейте ввиду, что он вызывает для нашей программы любые инициализаторы уровня Си.
Вызов _init
Хорошо. Загрузчик передал управление _start , которая вызвала _libc_start_main , которая вызвала _libc_csu_init , которая теперь вызывает _init :
Начинается она с регулярного соглашения о вызовах Си
Если вы хотите побольше узнать об этом соглашении, почитайте Basic Assembler Debugging with GDB. Если коротко, то мы сохраняем указатель базы вызывающего компонента в стеке и направляем указатель базы на верхушку стека, после чего резервируем место для своего рода 4-байтовой локальной переменной.
Интересен здесь первый вызов. Его задача во многом аналогична вызову get_pc_thunk , который мы видели ранее. Если посмотреть внимательно, то он направлен к следующему по порядку адресу. Это переносит нас к очередному адресу, как если бы мы просто продолжили, но при этом в качестве побочного эффекта данный адрес оказывается в стеке. Он помещается в %ebx , а затем используется для установки доступа к глобальной таблице доступа.
Покажи мне свой профиль
Далее мы захватываем адрес gmon_start . Если он равен нулю, то мы его просто проскакиваем. В противном случае он вызывается для запуска профилирования. В этом случае происходит запуск процедуры для начала профилирования и вызов at_exit , чтобы по завершению сработала другая процедура и записала gmon.out .
Вызов frame_dummy
В любом из случаев дальше мы вызываем frame_dummy . Вообще нам нужно вызвать _register_frame_info , а frame_dummy просто устанавливает для этой функции аргументы. Конечная цель – настроить разворачивание стековых фреймов для обработки исключений. Это интересно, но к нашему разбору не относится, и в данном случае все равно не используется.
Переходим к конструкторам!
В завершении мы вызываем _do_global_ctors_aux . Если у вас сложности с программой, которые возникают до запуска main , то искать, возможно, нужно именно здесь. Конечно, сюда помещаются конструкторы для глобальных объектов С++, но кроме них тут могут находиться и другие компоненты.
Создадим пример
Теперь давайте изменим prog1 , создав prog2 . Самая интересная часть – это __attribute__ ((constructor)) , который сообщает gcc , что компоновщик должен поместить соответствующий указатель в таблицу, используемую _do_global_ctors_aux . Как видите, наш фиктивный конструктор выполняется. (компилятор заполняет _FUNCTION_ именем функции. Это магия gcc ).
_init в prog2 практически не изменяется
Чуть позже мы подключим к процессу gdb и разберем эту программу. Ну а пока же рассмотрим ее _init .
Как видите, адреса немного отличаются от prog1 . Похоже, дополнительный элемент данных сместил все на 28 байт. Итак, здесь у нас имена двух функций, a_constructor (14 байт с завершающим нулем) и main (5 байт с завершающим нулем), а также две форматирующих строки %s\n (2*4 байта с символом переноса строки и завершающим нулем).
Итого получается 14+5+4+4 = 27. Хмм…одного не хватает. Хотя это просто предположение, проверять я не стал. Позже мы все равно сделаем остановку на вызове _do_global_ctors_aux , сделаем один шаг и проанализируем происходящее.
А вот и код, который будет вызван
Чисто в качестве подсказки приведу код для _do_global_ctors_aux , взятый из файла gcc/crtstuff.c исходного кода gcc .
Как видите, он инициализирует p из глобальной переменной _CTOR_END_ и вычитает из нее 1 . Напомню, что это арифметика указателей, а указатель указывает на функцию, значит в данном случае -1 приводит к смещению на один указатель функции назад или на 4 байта. Мы также увидим это в ассемблере.
Несмотря на то, что указатель не имеет значения -1 (приведение к указателю), мы вызовем функцию, на которую указываем, после чего снова переведем указатель функции назад. Очевидно, что таблица начинается с -1 , после чего идет некоторое количество (возможно даже 0) указателей функции.
То же самое в ассемблере
Вот код ассемблера, соответствующий полученному из objdump -d результату. Прежде, чем переходить к трассировке с помощью отладчика, мы внимательно по нему пройдемся, чтобы вам было понятнее.
Сначала пролог
Здесь у нас типичный пролог с добавлением резервирования %ebx , так как мы собираемся использовать его в функции. Помимо этого, мы резервируем место для указателя p . Вы заметите, что несмотря на резервирование под него места в стеке, хранить мы его там не будем. Вместо этого p будет размещаться в %ebx , а *p в %eax .
Далее подготовка к циклу
Похоже, произошла оптимизация. Вместо загрузки _CTOR_END_ с последующим вычитанием из него 1 и разыменовыванием мы переходим далее и загружаем *(__CTOR_END__ — 1) , который представляет непосредственное значение 0x8049f14 . Его значение мы помещаем в %eax (помните, что инструкция $0x8049f14 означала бы помещение этого значения, а ее вариант без $ — помещение содержимого этого адреса).
Следом мы сравниваем это первое значение с -1 , и если они равны, то заканчиваем и переходим к адресу 0x8049f14 , где очищаем стек, извлекая все сохраненные в нем элементы и делая возврат.
Предполагая, что в таблице функций есть хотя бы один элемент, мы также перемещаем непосредственное значение $8049f14 в %ebx , который является нашим указателем функции f , после чего выполняем xchg %ax,%ax .
Что это вообще такое? Эта команда используется в качестве NOP (No OPeration) в 16- и 32-битных x86. По факту она ничего не делает. В нашем случае ее задача в том, чтобы цикл (верхняя его часть – это вычитание на следующей строке) начинался не с 8048466 , а с 8048468 . Смысл здесь в выравнивании начала цикла с 4-байтовой границей, в результате чего весь цикл с большей вероятностью впишется в одну строку кэша, не разбиваясь на две. Это все ускорит.
И вот мы у вершины цикла
Далее мы вычитаем 4 из %ebx , подготавливаясь к очередному циклу, вызываем функцию, адрес которой получили в %eax , перемещаем следующий указатель функции в %eax и сравниваем его с -1 . Если они не равны, возвращаемся к операции вычитания и повторяем цикл.
Эпилог
В противном случае мы достигаем эпилога функции и возвращаемся к _init , которая сразу достигает своего эпилога и возвращается к _libc_csu_init_ , о котором вы уже наверняка забыли. Здесь все еще остается один цикл для завершения, но сначала…
Как я и обещал, мы займемся отладкой prog2 .
Напомню, что gdb всегда показывает очередную строку или инструкцию, которая будет выполнена.
Мы запустили программу в отладчике, включили disassemble-next-line , чтобы он всегда показывал очередную строку в дизассемблированном виде, и установили точку останова на строку в _init , где будем вызывать _do_global_ctors_aux .
Здесь я ввел r , чтобы запустить программу и достичь точки останова. Очередной командой для gdb стала si , инструкция шага, указывающая отладчику шагнуть на одну инструкцию вперед.
Теперь мы вошли в _do_global_ctors_aux . Далее вы заметите моменты, когда я будто бы не ввожу команды для gdb , хотя это не так. Дело в том, что при нажатии Ввода отладчик повторяет последнюю инструкцию. То есть, если я нажму Ввод сейчас, то еще раз выполню si .
Хорошо, с прологом мы закончили, пришло время реального кода.
После загрузки указателя мне стало любопытно, и я ввел p/x $eax , то есть попросил gdb вывести hex-содержимое регистра %eax . Это не -1 , значит можно предположить, что цикл мы проходим.
Теперь, поскольку последней командой был вывод, я не могу повторить si нажатием Ввода, и мне придется ее ввести.
Вот здесь очень интересно. Мы шагнули в вызов и теперь находимся в функции a_constructor . Поскольку у gdb есть для нее исходный код, он показывает исходник Си для следующей строки. А так как я включил disassemble-next-line , он также покажет нам соответствующий код ассемблера.
В данном случае это пролог функции, и мы получаем все его три строки. Разве не интересно? Далее я переключусь на команду n (next), потому что скоро покажется printf . Первая n пропустит пролог, вторая printf , а третья эпилог. Если вас когда-нибудь интересовало, почему при пошаговом продвижении с помощью gdb нужно делать дополнительный шаг в начале и конце функции, то теперь вы знаете почему.
Мы переместили адрес строки a_constructor в стек в качестве аргумента для printf , но он вызывает puts, поскольку компилятор догадался, что нас интересует только puts .
Раз мы трассируем программу, то она, естественно, выполняется, в связи с чем выше мы видим вывод a_constructor . Закрывающая скобка > соответствует эпилогу, поэтому он выводится сейчас. К слову отмечу, если вам не знакома инструкция leave , то выполняет она то же, что и:
Очередной шаг выводит нас из функции с возвращением ее результата. Здесь мне потребуется снова переключиться на si .
Мне снова стало интересно, и я решил еще раз проверить значение указателя функции. На этот раз он равен -1 , значит из цикла мы выходим.
Заметьте, что мы снова вернулись в _init .
Обратите внимание, что мы перепрыгнули обратно к _libc_csu_init , и здесь я ввел q для выхода из gdb . Вот и вся отладка, которую я обещал.
Теперь, когда мы вернулись в _libc_csu_init_ , нужно разобраться еще с одним циклом, через который я уже не буду шагать, а просто его проговорю.
Возвращаемся в __libc_csu_init__
Поскольку мы итак провели немало времени за работой с циклом в ассемблере, а ассемблерный код для этого конструктора еще более утомителен, то я оставлю эту задачу для тех, кому она будет интересна. Просто напомню, как он выглядит в Си:
Еще один цикл вызова функции
Что такое массив _init_ ? Я уж думал, вы и не спросите. На этом этапе вы также можете выполнять код. Поскольку идет он сразу после возвращения из _init , которая запускала наши конструкторы, то содержимое этого массива будет выполняться после завершения конструкторов. Вы можете сообщить компилятору, что хотите выполнить на этом этапе функцию, которая в результате получит те же аргументы, что и main .
Мы пока этого делать не будем, потому что есть и другие подобные моменты. Давайте просто вернем результат из _lib_csu_init . Помните, куда это нас приведет?
Мы вернемся аж к __libc_start_main__
Теперь он вызывает наш main , а результат передает в exit() .
exit() выполняет функции, зарегистрированные с помощью at_exit в порядке их добавления. Затем она выполняет очередной цикл функций, на этот раз из массива fini . Далее она выполняет еще один цикл функций, теперь уже деструкторов. (В реальности она находится во вложенном цикле и работает с массивом списков функций, но поверьте мне, завершаются они именно в этом порядке). Вот смотрите.
Эта программа, hooks.c, связывает все воедино
Если собрать и выполнить эту программу (я зову ее hook.c ), то выводом будет:
Конец
Еще раз продемонстрирую вам весь путь, который мы прошли, только теперь он должен быть вам уже более понятен.
Как запускаются исполняемые файлы в Linux
Доброго времени, читатели моих постов о Linux!
В сегодняшней статье расскажу о том, как работают исполняемые файлы. Из моей прошлой статьи о атрибутах доступа к файлам в Linux думаю Вам будет известно, что такое полномочия выполнения (исполнения). Данное право можно установить для любого файла. Исходя из этого, можно задать вопрос: неужели любой файл можно сделать программой? Да, так и есть. В Linux является ли файл исполняемым или нет, определяется не по его расширению, как в Windows (понятие расширение файла отсутствует в файловой системе Linux), а по правам доступа. Если у файла установлено право x (выполнения), его можно запустить на выполнение.
Что происходит, когда мы пытаемся выполнить файл ? Мы пытаемся набрать имя и, может быть, путь к файлу, который пытаемся запустить в командной строке и нажимаем Enter. (если файл расположен в текущем каталоге, то необходимо набирать ./ program). В первую очередь, оболочка проверяет, а имеет ли пользователь права на исполнение этого файла? Если имеет, тогда система смотрит, а это исполняемый бинарный файл? В Linux все исполняемые бинарные файлы в начале файла имеют заголовок .ELF (Executable and Linkable Format) (напомню, что в Windows в исполняемых файлах заголовок — MZ). Если это исполняемый бинарный файл, тогда, согласно его заголовку, происходит распределение оперативной памяти, и управление передается программе.
Если файл не бинарный, тогда считается, что это текстовый файл — скрипт или сценарий. В первых двух байтах сценария обнаруживается последовательность символов #!. Если символы «#!» присутствуют, тогда всю первую строку сценария, начиная с третьего байта, ядро воспримет как команду обработки. Исполнение сценария, содержащего указанную последовательность приведет к запуску указанной после » #!» команды, последним параметром которой будет имя самого файла сценария. Например, для файлов, написанных на языке shell script, первая строка будет выглядеть так:
#! /bin/sh
Для программ, написанных на perl, так:
#! /bin/perl
Таким образом, можно написать сценарий для любой программы, пример:
Во всех интерпретируемых языках программирования # — это символ комментария. То есть первая строка считается комментарием и программой не выполняется. При указании интерпретатора можно писать аргументы командной строки. Например:
#! /bin/sed -f command
Если в файле в первой строке нет этих символов, тогда все зависит о программы оболочки, в которой запускается программа. Если используется bash, то он считает, что файл содержит программу, написанную на языке shell script, запускает копию себя любимого и передает этой копии файл на интерпретацию. Если в файле действительно находится программа, то он ее выполняет. Если в файле находится «Война и мир» графа Льва Николаевича Толстого, то на экране появляются сообщения об ошибках shell script: «Я не знаю оператор Пьер Безухов. Наташа Ростова — это оператор или функция?»
Если Вы желаете выполнить exe-файл, который запускали в Windows, необходимо воспользоваться таким пакетом, как Wine. Но это уже совсем другая тема.
k1r8r0wn / linux_tips.md
Прогр1 | Прогр2 | . | ПрогрN — передать stdout Прогр1 в качестве stdin для Прогр2, далее stdout Прогр2 в качестве stdin для Прогр3 и т.д.
5. Скачивание файлов из интернета
wget ссылка — скачать файл по ссылке и сохранить в текущей директории
wget -P путь_до_директории ссылка — скачать файл по ссылке и сохранить в директории заданной путем
wget -O путь_до_файла ссылка — скачать файл по ссылке и сохранить под указанным именем
wget -c ссылка — докачать файл по ссылке в случае обрыва связи
wget —spider ссылка — проверить доступность файла по ссылке
wget -i текстовый_файл — скачать несколько файлов по ссылкам из текстового файла
wget -r -l глубина ссылка — рекурсивное скачивание файлов по ссылке на указанную глубину (по умолчанию глубина 5)
wget -r -A тип,тип. тип ссылка — рекурсивное скачивание файлов только определенного типа (типов)
6. Работа с архивами
unzip архив.zip — распаковать содержимое архива.zip
gunzip архив.gz — распаковать содержимое архива.gz, файл архив.gz удалить
tar -xvf архив.tar — распаковать архив.tar
tar -xzvf архив.tar.gz — распаковать архив.tar.gz (с использованием gunzip)
zip архив.zip файл1 файл2 . — запаковать перечисленные файлы и/или папки в архив.zip
gzip файл — запаковать файл в файл.gz, исходный файл удалить
tar -cvf архив.tar файл1 файл2 . — запаковать перечисленные файлы и/или папки в архив.tar (без сжатия)
gzip архив.tar — запаковать архив.tar в архив.tar.gz, исходный архив.tar удалить
tar -zcvf архив.tar.gz файл1 файл2 . — запаковать перечисленные файлы и/или папки в архив.tar.gz (c сжатием при помощи gzip)
Еще один архиватор:
tar -cjvf архив.tar.bz2 файл1 файл2 .
tar -xjvf архив.tar.bz2
Сжатие/распаковка без удаления:
gzip -c файл > файл.gz
gunzip -c файл.gz > файл
bzip2 -c файл > файл.bz2
bunzip2 -c файл.bz2 > файл
7. Поиск файлов и слов в файлах
find <папка> -name “<имя файла>” — найти указанный файл в папке
/ -name “file.txt” — найти file.txt в домашней директории
/ -name “*.txt” — найти все текстовые файлы в домашней директории
grep “<строка>” <файл> — найти строку в файле
grep -с “<строка>” <файл> — посчитать количество вхождений строки
grep -r “<строка>” <папка> — найти строку во всех файлах в папке
grep “hello” file.txt — найти “hello” в файле file.txt
grep -с “123” file.txt — вывести количество раз, которое “123” встречается в file.txt
/ — найти “world” во всех файлах в домашней директории
7*. Продвинутый поиск и редактирование
Поиск:
find -iname “<имя файла>” — не учитывать регистр
find -path “<путь>” — найти указанный путь
find -size <размер> — выводить файлы указанного размера
find -maxdepth <число> — искать не больше чем на заданное число уровней вниз
find -mindepth <число> — искать начиная с заданного числа уровней вниз
grep -l “<строка>” <файл> — список файлов с этой строкой
grep -L “<строка>” <файл> — список файлов, где этой строки нет
grep -n “<строка>” <файл> — выводить номер строки в файле
grep -m <число> “<строка>” <файл> — не искать дальше после заданного числа вхождений
grep -A <число> “<строка>” <файл> — выводить это число строк после вхождения
grep -B <число> “<строка>” <файл> — выводить это число строк до вхождения
grep -C <число> “<строка>” <файл> — выводить это число строк вокруг вхождения
grep -E “<шаблон>” <файл> — найти указанный шаблон в файле
grep -E “^go” <файл> — найти строки, начинающиеся с “go”
grep -E “go$” <файл> — найти строки, оканчивающиеся на “go”
grep -E “c[au]t” <файл> — найти все слова, содержащие cut и cat
grep -E “ [a-z]ight ” <файл> — слова из 5 букв, кончающиеся на “ight”
grep -E “ [a-z]*ight ” <файл> — слова из 4 и более букв, кончающиеся на “ight”
grep -E “ [a-z]+ight ” <файл> — слова из 5 и более букв, заканчивающиеся на “ight”
grep -E “ [a-z]?ight ” <файл> — слова из 4-5 букв, заканчивающиеся на “ight”
grep -E “ [a-zA-Z]*ight ” <файл> — слова, заканчивающиеся на “ight” (разрешены большие буквы)
cat <файл> | sed ‘инструкция’ sed ‘инструкция’ <файл> — потоковый редактор: читает строчки из stdin (или из файла), обрабатывает их по инструкции и пишет в stdout
Если хотим писать в файл:
> <файл> — обычное перенаправление
-i, —in-place — перезаписать входной файл
Замена:
sed ‘s/John/Nick/g’ old.txt > new.txt — заменить все John на Nick
sed -r ‘s/J[a-z]*n/Nick/g’ old.txt > new.txt — заменить все слова, которые начинаются на J и заканчиваются на n на Nick
sed -n ‘2,4p’ file.txt — вывести строки с 2 по 4
sed ‘2,4d’ file.txt — вывести все строки кроме 2-4
sed -n ‘/[0-9]\<2\>/p’ file.txt — вывести строки с 2 цифрами подряд
sed ‘2,/[Rr]ight/d’ file.txt — вывести все строки кроме со 2 до строки содержащей “right” (с большой или маленькой буквы)
Посчитать что-то в файле:
wc [что-считаем] <путь> wc -l file.txt
Сравнить файлы/директории:
diff [-q -r] <путь1> <путь2> diff file1.txt file2.txt | less diff -qr dir1/ dir2/
Узнать сколько места занимаем на диске:
du [—max-depth <глубина> -h] <путь> du -h
du –-max-depth 1 -h .
df [-h] — узнать сколько места занято/свободно во всей системе
Вывести часть файла:
head [-n <количество строк>] <путь> tail [-n <количество строк>] <путь>
head -n 10 file.txt
tail -n 50 file.txt | less
Работа с файлами/директориями:
Вывод с сортировкой:
ls —sort=[вид сортировки] -l <путь> ls –-sort=size -l
Перенаправление в один файл:
Перенаправление одного потока в другой:
2>&1 — stderr в stdout
1>&2 — stdout в stderr
Перенаправление в никуда и из ниоткуда:
cat /dev/null > file.txt
Команда входа: ssh логин@адрес_сервера -p порт
Создание ключа: ssh-keygen
Сообщить системе о ключе: ssh-add
Просмотр публичного ключа: cat
Редактирование авторизованных ключей (на сервере): nano
10. Обмен файлами
Копирование файлов:
scp -P порт логин@адрес_сервера:путь1 путь2 — с сервера (путь1) на клиента (путь2)
scp -P порт путь1 логин@адрес_сервера:путь2 — с клиента (путь1) на сервер (путь2)
11. Установка и обновление программ
Установка программ через терминал: sudo apt-get install программа
Удаление программ через терминал: sudo apt-get remove программа
Обновление ссылок на пакеты: sudo apt-get update
Обновление установленных пакетов: sudo apt-get upgrade
Обновление отдельной программы: sudo apt-get install —only-upgrade программа
12. Запус приложений, контроль запускаемых программ
Ctrl + C — прервать выполнение
<Ctrl + Z> — приостановить выполнение:
fg — продолжить (foreground)
bg — продолжить в фоновом режиме (background)
jobs — посмотреть запущенные программы
fg %<номер> — продолжить программу с этим номером
bg %<номер> — продолжить программу с этим номером в фоновом режиме
ps — посмотреть ваши процессы
top — отслеживать процессы в реальном времени
top -u <имя пользователя> — отслеживать процессы этого пользователя
kill <номер процесса> — завершить процесс с этим номером
kill -9 <номер процесса> — “убить” процесс с этим номером
13. Многопоточные приложения
free -g — информация об оперативной памяти
nproc — количество ядер процессора
lscpu — детальная информация о процессоре
14. Менеджер терминалов tmux
<Ctrl + Shift + T> — открыть новую вкладку в терминале
Alt + <цифра> — перейти в указанную вкладку
<Ctrl + Shift + W> — закрыть текущую вкладку
tmux — запустить tmux
<Ctrl + B> — перейти в режим команд
<Ctrl + B> и C (зажать <Ctrl+B>, отпустить, затем нажать С) — создать новую вкладку
<Ctrl + B> и <цифра> — перейти в указанную вкладку
<Ctrl + B> и N / <Ctrl + B> и P — перейти в следующую / предыдущую вкладку
<Ctrl + B> и X (или exit) — закрыть вкладку
<Ctrl + B> и D — временно выйти из tmux
tmux attach / tmux a — вернуться в tmux
tmux list-sessions — посмотреть список запущенных tmux’ов
Как запустить программу на Linux
По сути операционная система состоит из ядра и огромного набора программ, которые предназначены для выполнения различных задач, обслуживания системы и удовлетворения потребностей пользователя. Почти все взаимодействие пользователя и операционной системы выполняется с помощью программ. Поэтому новичкам важно понять как запустить программу на Linux, что происходит во время запуска и какие есть способы запуска.
Дальше мы рассмотрим виды программ, их запуск программ на Linux различными способами и другие полезные для новичков вещи, опытным пользователям это все и так уже известно.
Виды программ в Linux
Перед тем, как мы перейдем к запуску программ, нужно сначала понять что представляет из себя программа. В Linux программы отличаются от других файлов только тем, что для них установлен флаг исполняемости. Я уже подробно писал об этом в статье что такое исполняемость поэтому не буду повторяться.
Все программы можно поделить на несколько типов:
- Бинарные программы — содержат инструкции процессору уже готовые к выполнению, большинство программ находятся в таком формате, они быстрые и выполняются сразу же системой;
- Программы на байт-коде — это уже не процессорные инструкции, а инструкции определенной виртуальной машины, которая может их выполнять, без виртуальной машины такие команды не могут быть выполнены. Такие программы потребляют больше ресурсов, но тоже достаточно быстрые, их преимущество в том, что они могут выполняться без изменения везде где может работать виртуальная машина. К таким программам можно отнести программы на Java.
- Скриптовые программы — эти программы состоят из набора команд в виде обычного текста, которые выполняет специальный интерпретатор. Такие программы более медленные, но зато они проще в разработке и их код можно легко и быстро изменить.
А теперь перейдем к запуску программ.
Запуск программ в терминале
Изначально в операционных системах Unix и Linux не было графического интерфейса, поэтому программы запускались командами из терминала. Сейчас это тоже возможно и достаточно активно используется опытными пользователями. Синтаксис запуска программы выглядит таким образом:
/путь/к/файлу/программы параметры
Параметры указываются только, когда они нужны, но всегда оболочка должна знать полный путь к программе. Все что после имени программы и пробела — это параметры. Вы, наверное, уже заметили, что обычно мы не указываем полный путь при выполнении программ. Это было бы очень долго и неудобно.
Разработчики придумали обходной путь. Была создана переменная PATH, в которой хранятся все пути к папкам где обычно находятся программы — /bin, /sbin, /usr/bin, /usr/sbin и так далее. Вы можете посмотреть ее содержимое командой:

Когда вы набираете имя программы система ищет исполняемый файл с таким именем по всем папкам из PATH и если находит — то выполняет. Если же такого файла нет, то выдается сообщение — command not found. Таким образом, чтобы запустить одну из системных программ достаточно набрать имя ее исполняемого файла, например:

И можно передать параметры после пробела:

Когда программа находится не в этих каталогах, нужно указать к ней полный путь:

Если же вы хотите запустить программу через терминал ubuntu, которая находится в текущей папке, то ситуация будет немного другой. Система выполняет только поиск по папкам из переменной PATH, в текущей директории она не ищет. Поэтому, если вы наберете имя исполняемого файла, то получите ошибку. Нужно указывать полный путь, как вы помните путь к текущей папке будет ./:
Иногда возникает необходимость передать программе, какие-либо особые переменные окружения. Например, переменная EDITOR указывает какой текстовый редактор нужно использовать по умолчанию. Вы можете указать имя переменной и ее значение перед именем команды используя синтаксис:
имя_переменной = значение команда

По умолчанию эта команда открывает настройки утилиты sudo в редакторе Vim, но с этой переменной окружения настройки откроются в редакторе nano.
Запуск программ от имени другого пользователя
Вы уже знаете как запустить программу в терминале linux, а что насчет других пользователей? В Windows достаточно часто используется запуск программ от имени администратора чтобы программа могла получить больше прав доступа в системе. В Linux для этого используется утилита sudo. Ее имя можно расшифровать как switchuserdo — изменить пользователя и выполнить. По умолчанию утилита выполняет команду от имени суперпользователя root:
sudo команда
sudo whoami

Но с помощью опции -u можно выполнить программу от имени любого пользователя, зарегистрированного в системе:
sudo -u имя_пользователя команда
sudo -u postgres whoami

Команда whoami (кто я) выводит имя текущего пользователя.
Как запустить программу в фоне
Иногда возникает необходимость запустить долго выполняющуюся программу в терминале так, чтобы она не мешала дальше работать. Для этого можно использовать запуск программы в фоновом режиме linux:
dd if=/dev/zero of=

Система выведет PID, уникальный идентификатор программы, который вы потом можете использовать чтобы закрыть ее командой kill:

Как запустить скрипт в Linux
Мы уже говорили, что программы делятся на бинарные и интерпретируемые. Раньше мы говорили только про бинарные программы. Для запуска интерпретируемых нужен непосредственно интерпретатор, к таким программам относятся написанные на таких языках, как Java, Python, Perl, Ruby, PHP, NodeJS и многих других. Синтаксис запуска такой программы отличается:
интерпретатор /путь/к/файлу/программы параметры
Разные интерпретаторы ведут себя по разному, поэтому лучше сразу указывать полный путь к программе. Python обычно подхватывает скрипты из текущей папки без указания полного пути:
А Java программы нужно запускать так:
java -jar program.jar
Для файлов интерпретируемых программ флаг исполняемости необязательный, поскольку они передаются в виде параметра основной программе. Только Bash скрипты составляют исключение. Вы можете запустить скрипт интерпретатором:
Или же просто набрать путь к скрипту:
Оболочка сама определяет свои скрипты по флагу исполняемости и выполняет их. Если флаг исполняемости не установлен, то его стоит добавить:
sudo chmod u+x ./script.sh
Поэтому то и для большинства интерпретируемых программ созданы простые sh скрипты которыми их можно быстро запустить.
Запуск программ Linux в графическом интерфейсе
Намного удобнее запускать программы через графический интерфейс. Если консольные программы так запускать невозможно, то для всех графических утилит существуют ярлыки, которые вы можете найти в главном меню системы:

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


Точно так же работает запуск скриптов в графическом интерфейсе. Вы можете найти все ярлыки из меню в каталоге /usr/share/applications/. Любую программу можно запустить двойным щелчком отсюда. Но давайте посмотрим что находится внутри ярлыка, для этого откройте его в текстовом редакторе:

Кроме всего прочего, в строке Exec указана команда, которая выполняет запуск программы linux, когда вы делаете двойной клик на ярлыке. Вы можете взять один из существующих ярлыков и сделать на его основе свой. Здесь указано просто имя программы. Но важно заметить, что лучше указывать полный путь в таких местах, как ярлыки, скрипты, cron и так далее это уменьшит количество ошибок, поскольку вы не можете знать проверяет ли система в этом случае PATH или ищет программу только в текущем каталоге. Теперь вы знаете все о том как запустить программу на linux.
Выводы
В этой статье мы рассмотрели как запустить программу через терминал ubuntu или в других дистрибутивах Linux. Несмотря на то, что это кажется очень простой темой, тут есть свои интересные моменты, которые могут быть полезны. Но вы о них уже знаете. Если у вас остались вопросы, спрашивайте в комментариях!
Обнаружили ошибку в тексте? Сообщите мне об этом. Выделите текст с ошибкой и нажмите Ctrl+Enter.