Linux. Глубокое понимание работы системы. Часть 1 — трассировка библиотечных и системных вызовов.

UPD: Запись перенесена из старого блога, опубликована в 2015 году.

Предисловие

Системному администратору зачастую очень важно понимать как устроена система, как настроить/отладить те ли иные вещи и как искать ошибки в работе. Если устройству и настройке можно достаточно легко научиться из различных руководств и справочных страниц, где написано что система сделана вот так вот, а настраивается она вот этими кнопочками и командами, то вот с отладкой и поиском проблем зачастую возникают проблемы. Для многих, к сожалению, система представляет собой черный ящик, с входом и выходом и магией внутри. Я покажу как немного пролить свет на эту темноту внутри ящика.

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

Поехали…

Представим что нам просто выдали сервер с предустановленой ОС. Мы знаем лишь то, что «под капотом» стоит некий Linux. Необходимо провести разведку

uname -r

uname -a

мы узнаем версию ядра ОС и полную краткую информацию — версия ядра, то, что это ядро PAE (позволяет на 32 битной архитектуре видеть более 4 гб памяти. Видим что это Ubuntu ( в основе)

Выясним что же это за дистрибутив

lsb_release -a

LSB — Linux Standart Base. Он показывет нам что это дистрибутив Elenemtary OS, релиз 0.2 с кодовым именем Luna.

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

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

Трассировщики и доп. утилиты

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

Покажу Вам парочку стандартных инструмнетов:

1. strace или System trace — простой трассировщик системных вызовов.

2. ltrace — library tracer — трассировщик библиотек. Строит трассы вызова системных библиотек в ходе запуска программы.

3. file — позволяет заглядывать внутрь указанного файла и сигнатурным способом строит предположения о том, что же это за файл. В unix системах у файлов нет суфикса- расширения. Поэтому работа ведется по внутреннему содержимому, хотя ни одна файловая система не содержит никаких признаков.

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

4. which — команда,  позволяющая узнать где находится исполняемый файл той или иной программы.

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

5. «type -a» — команда, позволяющая узнать, чем для нас является та или иная команда- встроенной командой интерпретатора или вызовом внешней программы.

В зависимости от ее вывода, мы можем понять- что чем является. То, что лежит в:

  • /bin — является неотъемлемыми системными компонентами, исполняемыми бинарными файлами, либо
  • /usr/bin — прикладными, которые можно изъять из операционной системы без потери ее функциональности

либо является встроенной командой.

Теперь, раз мы уже так много знаем и умеем, давайте разберемся в самих файлах и программах чуть глубже- спросим у утилиты file про нее же саму и посмотрим, что она нам скажет?

нам скажут что это:

  • исполняемый файл (ищем  в выводе команды слово «… executable …»)
  • нам скажут что он формата ELF — т.е. это не jpeg, не mpeg — т.е. не картинка, не видео (явно),
  • это 32 битный,  исполняемый код («… 32 bit LSB executable…»), который упакован в формат ELF (executable and linking format)
  • версия формата ( «…Intel 80386, version 1 …»)
  • динамически слинкован с библиотеками ( «… dynamicly linked…» )
  • использует разделяемые библиотеки
  • создан дли ядра Linux, начиная с версии 2.6.24
  • и некоторая контрольная сумма исполняемого файла («…[sha1]=…»)
  • и что из него удалена отладочная информация (stripped), которая могла бы быть использована отладчиком.

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

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

Продолжим исследование программы file

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

  • linux-gate.so.1 — библиотека системных вызовов ядра ОС
  • libmagic.so.1 — ее собственная библиотека, содержащая сигнатуры для определения типов исследуемых файлов.
  • libc.so.6  — библиотека языка С нужна ей, т.к. будучи написанной на С она использует ее функции для банального открытия файлов
  • libz.so.1 — нужна для работы с сжатыми файлами (архивами)
  • /lib/ld-linux.so.2 — библиотека линковщик- умеет/позволяет «прицеплять» другие библиотеки. Это и есть часть того Loader-а или загрузчика, который строит окружение программы-процесса, помещая в память необходимые компоненты. Саму ОС мало интересует от чего зависит программа- она просто выделяет память, создает контекст процесса и передает управление этой библиотеке, которая и делает за нее грязную работу.

После этого встает вопрос- а как же библиотека загружает другие библиотеки если ее тоже кто-то должен загрузить и загрузить ее библиотеки?

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

Посмотрим, что из себя представляет наша «магическая» библиотека — оказывается это символическая ссылка на библиотеку определенной версии в системе. Тогда мы даем команду file с ключом -L — сходи по ссылке и выясни таки, что там валяется.

Он показывает нам, что это тоже бинарный файл, формата ELF и т.д. и т.п. Только в отличие от программы ( смотрим пример для самой программы file), библиотека является не executable объектом, а shared object (выделил на скриншоте). Что это означает? — Запустить программу можно, а вот библиотеку — нет. В ней нет точки входа/старта. И если мы посмотрим остальные библиотеки, увидим такую же картину.

И у всех у них в свою очередь тоже есть зависимости, потому что между программой и библиотекой от которой она зависит, нет почти никакой разницы. Посмотрим ldd для библиотеки С — она в свою очередь зависит от другой библиотеки, а вот у нее уже уже нет никаких зависимостей, т.к. она статически слинкована — т.е. все что необходимо для нее, уже входит в ее состав. Т.е. она не зависит ни от каких других библиотек и может быть использована напрямую.

Вот вам ответ как она загружается- она не зависит от других библиотек и загружается сама сразу. Один ньюанс — раз команда file говорит что /lib/ld-linux.so.2 динамически слинкована, а ldd говорит что статически, значит кто-то из них врет. Скорее всего врет file, т.к. она строит предположения на основе сигнатур, а ldd четко проверяет зависимости.

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

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

И увидим что он так же имеет зависимости. Но как же так? Ведь мы не сможем без переинсталяции починить систему в случае такого краха. Нет, сможем. Потому что в каталоге /bin лежит версия интерпретатора sh, статически скомпилированная. Она называется static-sh. На самом деле это символическая ссылка на другой инструмент — busybox — эдакий швейцарский нож. В нем реализовано много других программ, необходимых для починки ОС.

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

Пример использования 1. окружение

Теперь давайте попробуем применить полученные знания на практике. Мы уже начали исследовать саму программу file — с ней и продолжим. Итак, вспомним вновь основные приемы- смотрим что это, где лежит и от чего зависит

Теперь немного поработаем ей- попробуем определить типы различных файлов

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

Мы уже видели, что она зависит от библиотеки libmagic. Логично было бы убедиться в том, что она использует этот сигнатурный способ на практике, а не основываясь на моих словах. Сделать мы это можем путем предположения, что именно эта библиотека и отвечает за анализ сигнатур ( ведь остальные являются стандартными системными библиотеками, а эта как то выделяется). Аналогичным способом мы могли бы выяснить что Libz отвечает за работу с архивами. Как нам это сделать?

Еще в самом начале мы выяснили, что у нас система основана на ubuntu, а значит, поковырявшись в справочной информации мы узнаем что это ветка развития Debian а значит использует его менеджер пакетов dpkg ( как раз та самая дистрибутив-специфичная информация).

Так давайте спросим- из какого пакета была инсталлирована та или иная библиотека.

$ dpkg -S /usr/lib/i386-linux-gnu/libmagic.so.1

Он скажет что она была инсталирована из пакета libmagic1  для платформы intel 386. Спросим теперь что это за зверь?

dpkg -s libmagic1

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

Все становится более-менее понятно. И видим что программа file не может жить без библиотеки, а значит пакет с программой зависит от пакета с библиотекой. Вот вам живая иллюстрация зависимостей програмного обеспечения.

Что мы видим? что в этом пакете не только библиотека но еще и пара файлов ( и их контрольные суммы):

Conffiles:
/etc/magic 272913026300e7ae9b5e2d51f138e674
/etc/magic.mime 272913026300e7ae9b5e2d51f138e674

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

dpkg -L libmagic1

Тут сама библиотека, каталоги, отладочная информация и справочные руководства man! ( Страница 5: /usr/share/man/man5/magic.5.gz) — значит мы можем почитать описание этой библиотеки:

man 5 magic

 Тут мы можем подробно почитать о том что это и зачем нужно. Из чего он состоит и как утилита file его использует. Но нам же интересно и на сам файл с сигнатурами посмотреть?

file -L /usr/share/misc/magic.mgc

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

А мы можем его посмотреть? Попробуем, воспользовавшись утилитой less для просмотра текстовых файлов

К сожалению, она нам говорит что не уверена что он текстовый и даже если мы прикажем его просмотреть- не сможет это сделать. А может утилита cat нам поможет? Нет, увы- она замусорит нам консоль.

То есть очевидно что там все же не совсем текстовое содержимое, а скорее сигнатуры и их описание. Отлично- вот как много мы уже выяснили, зная лишь про одну утилиту file и исследуя ее окружение.

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

Пример использования 2. Трассировка

Итак, посмотрим за поведением утилиты file во время ее работы, для этого используем упомянутые выше утилиты strace и ltrace и построим трассы вызовов.

strace file /usr/bin/file

Получим большой-большой вывод. Ознакомимся с ним — что мы видим:

  • выполняется системный вызов  execve, запускающий программу «/usr/bin/file», с аргументом «/usr/bin/file»
  • системный вызов  access, котоый узнает- если ли такой файл ( искомый программой)
  • системный вызов open — попытка открыть найденный файл
  • и т.д.

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

Т.е. мы уже видим типичный запуск любой программы ( а если видите в первый раз- запоминайте) — поиск и открытие всех библиотек и др составных частей рабочей программы, т.е. формирование среды ее работы.

Далее, программа открывает свой конфигурационный файл и файл, который как мы предполагали, содержит сигнатуры

А дальше она пытается открыть и обработать тот файл, который мы попросили проанализировать:

Разбирает список аргументов

открывает файл

читает его заголовок

находит в заголовке какие-то только ей понятные искомые структуры данных

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

Дополнительные детали работы программы можно увидеть, построив трассу вызова библиотек — например, какие библиотечные функции использует наша программа:

ltrace file /usr/bin/file

  • Устанавливает текущие настройки локали
  • Ищет первое вхождение символа / в строку аргументов, чтобы установить точку отсчета пути
  • дальше, загрузив библиотеку magic, с помощью вызовов open и load загружает информацию из сигнатурного файла и с помощью своих функций начинает обработку
  • найдя необходимую информацию в файле сигнатур и проанализировав обрабатываемый файл, она вызовом magic_file «выковыривает»  описание и вызовом puts выдает нам на экран результат

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

  • загрузиться в память и загрузить все библиотеки
  • получить список аргументов и разобрать его
  • найти анализируемый файл
  • открыть и прочитать свои magic файлы
  • обратиться  к своей библиотеке
  • проанализировать информацию
  • напечатать результат

А мы все увидели это с помощью трассировщиков.

Работа со справочной информацией

Но откуда нам знать что делают те или иные функции? Очень просто- почитать встроенную справку. Например мы видели функцию putc, почитаем справку про нее:

man 3 putc

Откуда я узнал, что нужно открыть именно 3 раздел руководства? прочитал справку о справке — man man, где описана структура справочного руководства.

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

Почему так произошло? Да очень просто- установив пакет с библиотекой, мы установили минимально необходимый для запуска программы набор компонентов, оставив в стороне набор компонентов разработчика, такой как заголовочные файлы и страницы справочного руководства. Их так же можно доустановить из пакетов так же, как мы ставим все прочее программное обеспечение.

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

Среди списка рекомендуемых пакетов в описании увы нет ничего похожего ( например одноименные библиотеки с суфиксами doc или dev), поэтому проведем поиск по имени пакета среди доступных:

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

sudo aptitude install libmagic-dev

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

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

П.С. Данная статья ( и последующие аналогичные) подготовлена на основе конспекта лекций Кетова Дмитрия Владимировича. От себя я добавил редактуру, несколько измененный стиль изложения, свои правки и комментарии.