Linux. Глубокое понимание работы системы. Часть 3 — Сборка программного обеспечения

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

Предисловие

В предыдущей статье, мы рассмотрели средства анализа сетевого взаимодействия и кое- что знаем о взаимодействии программ, работающих в user-space с ядром или сервисами, работающими в kernel-space.

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

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

Работа пакетного менеджера

В любой системе ( мы говорим по прежнему о Linux системах) есть пакетный менеджер. В нашем случае (debian-based дистрибутивы) это dpkg. А большая часть ПО поставляется в пакетах, устанавливаемых пакетными менеджерами. Вот в частности список всех пакетов ( на самом деле он намного длинней), установленных в моей системе:

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

 

Помимо этого есть еще менеджер зависимостей, который работает с таким понятием как репозиторий, это набор пакетов и зависимостей между ними. Репозиторий обычно находится где-то в сети на выделенных серверах.
У нас менеджером зависимостей является apt-get. Существует более продвинутые аналоги (надстройки) — например обладающая как командным, так и псевдо графическим интерфейсом aptitude и графическая утилита synaptic.

Но это не столь важно. Важно понимать что из себя представляет пакет ПО. Т.к. в мире Gnu\Linux ПО свободно распространяется, мы всегда можем получить доступ к исходным текстам любой программы. Если мы попросим ( об этом чуть позже) пакетный менеджер предоставить нам исходные ресурсы их которых была собрана та или иная программы, мы получим 3 объекта

  • архив с именем и версией данного ПО, содержащий его исходные тексты
  • файл — description (.dsc), содержащий описание ПО и список зависимостей
  • и архив специализированных патчей от лица, сопровождающего его в данном дистрибутиве. Набор каких то исправлений, принятых в  дистрибутиве. Они могут накладывать какие-то исправления безопасности или другие исправления, которые дистрибьютор посчитал нужным применять. например перенести конфигурационный файл в другой каталог.

Но нас все же будет интересовать сборка ПО своими руками. Поэтому мы с вами познакомимся с двумя вещами:

  • что из себя представляет бинарный пакет. И нам его придется откуда то взять.Например из интернета — archive.ubuntu.com

Подключаемся клиентом lftp и используя для перемещения синтаксис команд, аналогичный фс, переходим в ubuntu-pool-main.
Выводим список всех пакетов дистрибутива. Видим что пакеты отсортированы по каталогам, согласно литере, с которой начинается их название. нам нужен каталог z — видим там каталог интересующей нас программы.

А дальше нас будет интересовать какая нибудь версия этого архиватора zip.

Почему же здесь столько пакетов и что тут вообще находится в репозитарии то? Тут находятся:

  • .deb — пакеты бинарные, т.е. готовые к использованию для различных архитектур — например 32 битные и 64
  • .tar.gz — тут находятся исходные тексты ПО
  • .dsc — файлы с описанием
  • .debia.tar.gz — различные добавки к этому ПО

Кроме того видно, что тут присутствуют различные сборки (2,4,7 и 8) версии 3.0 утилиты-архиватора zip. Потому что собирается всегда некий оригинальный, написанный своей командой разработчиков пакет ПО ( в нашем случае сходным является просто zip_3.0.orig…), а дальше, различные его версии с различными добавками для дистрибутива претерпели некоторое количество сборок (2,4,7 и 8 — где 1,5 и 6 -вопрос к сборщику). Итак, это готовые конечные сборки. Для какого-то дистрибутива последняя сборка 2, для какого-то 4 и т.д.

Нас будет интересовать самая последняя сборка 8. Скачаем пакет для 32 битной системы.

Тут хочу отметить что при попытке скачивания get zip_3.0-8_i386.deb
я получил ошибку 550, пришлось пере подключиться к серверу по http ( да, lftp умеет и так- он парсит html странички и достает ссылки, позволяя так же удобно перемещаться и работать).

Итак, мы скачали этот пакет программного обеспечения ( убедимся в этом командой ls), после чего, уже известной нам утилитой file узнаем, что же это мы такое скачали:

Видим что это якобы бинарный пакет Debian, однако на самом деле ( что не очень очевидно), это архив, который сжат архиватором ar. Древним архиватором ar-archiver. Кстати в Linux системах важно не путать архиваторы и компрессоры, потому что zip,gzip — компрессоры а ar, tar — архиваторы. Архиватор это процесс сборки большого числа файлов в один а компрессия — процесс сжатия файла за счет за кодирования повторяющихся участков, замены их например на ссылки с числом этих участков( на саом деле существует много достаточно сложных и разнообразных алгоритмов сжатия).
Если мы посмотрим на содержимое файла:

hexdump -C zip_3.0-8_i386.deb |less

то увидим строчку arch.

Но, воспользовавшись ar ( древнейшая утилита архивации) , попробовать заглянуть внутрь, с помощью параметра t попросив заглянуть внутрь и пролистать содержимое архива ( не сжатого)

То есть это просто «склеенные» три файла:

  • debian-binary — метаданные, заголовок
  • control — управляющие инструкции по сборке и/или инсталляции ( в данном случае все же инсталляции — ПО уже собрано)
  • data — собственно сами бинарники, которые нужно в систему установить.

Таким образом, когда мы даем команду менеджеру пакетов инсталлировать вот этот пакет:

dpkg -i  zip_3.0-8_i386.deb

на самом деле он делает следующие вещи:

  • распаковывает архив на 3 файла
  • разжимает и распаковывает из tar.gz файлы
  • раскладывает их куда нужно согласно инструкциям из файла control

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

Тут видно, что архив подготовлен уже другим архиватором — tar, да еще и с компрессией, с использованием gzip (gz) — .tar.gz

Извлечем с декомпрессией и увидим, что получилось. Мы видим что был извлечен каталог usr, значит программное обеспечение ставится в каталог usr. Можем в него зайти и посмотреть содержимое (cd и ls либо find п относительному пути)

видим структуру каталогов и файлов, которые при установке будут разложены по соответствующим местам. Тут все без корня /, т.к. все пути относительные внутри пакета, а абсолютными они станут при установке.
Что же мы видим относительно содержимого?

  • 4 бинарника (в папке bin/)
  • некоторое количество документации (doc/)
  • и документация, оформленная в виде man страниц по каждой утилите

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

Распакуем содержимое сжатого архива control. На выходе получим файл control и md5sum. Оба представляют из себя текстовые ascii файлы как видим, причем второй- содержит контрольные суммы устанавливаемых компонентов. А что же нам говорит сам файл control?

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

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

Перейдем теперь в каталог /var/lib/dpkg/info/, где находится информация по всем инсталлированным в систему пакетам — сохраненные на будущее списки инсталлированных файлов ( суффикс .list) из пакетов, списки контрольных сумм из пакета (суффикс .md5sums) и инструкции по пред удалению и пост инсталляции (.postinst, .prerm)

Если мы спросим про последние- что же это такое, то узнаем что это обычные текстовые сценарии на языке командного интерпретатора.

 

 

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

Сборка ПО. Простой пример

Итак, у нас бинарный пакет. А теперь мы попробуем достать те исходники, из которых он получался. Но больше не используя ftp. За нас всю грязную работу пусть делает специализированное ПО:

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

apt-get source сам сходил за нас на ftp сервер, нашел архив исходных текстов нашего пакета и выкачал нам их. Видим что он сказал:

  • dsc- описание
  • tar- исходные текст
  • diff — «разница», накладываемая при компиляции под конкретный дистрибутив

Дальше он ругнулся что у нас не установлен специальный пакет dpkg-dev для сборки ПО — мол я вам все достал, только собирать вы не сможете. Он даже попробовал подготовить (unpack command .. failed) для сборки все.. и не мог из за отсутствия инструмента.
Но мы можем сделать это руками.

Нам для иллюстрации процесса достаточно из полученного архива изъять тексты и не накладывая «разницу» проиллюстрировать процесс сборки.

Видим что у нас есть некое число файлов с исходными тестами на языке С. А дальше процесс сборки начинается… с чтения файлов описывающих сборку, т.е. README и в файле INSTALL — как его устанавливать. Смотрим читаем на предмет тонкостей и нюансов.

В итоге, все равно одним из основных шагов сборочного процесса является запуск команды make.

С момента своего создания, make обросла различными фронтэндами ( automake, cmake) и даже различными графическими средствами/интегрированными средствами разработки, которые в конце концов генерируют специальный сборочный файл makefile, вручаемый команде make.

Что мы видим в справочном файлу INSTALL ( скриншот выше) — вы мол почитайте, а потом надо запустить команду make, указав ключом -f ей на файл unix/makefile и целью сборки необходимо указать систему — generic, minix, xenix, либо что нибудь еще.  Варианта Linux мы тут не видим, поэтому читаем дальше и видим, что если в системе присутствует компилятор *сс, то выбираем вариант generic. Если есть gcc то generic_gcc.

Мы можем в итоге просто взять представленную команду и запустить процесс инсталяции. Я выбрал вариант generic.

Согласно выбранному варианту проводятся различные проверки (check if, check for) потом запускается компиляция, при компиляции компилятор предупреждает что у нас есть некие странности с его точки зрения (warning), только это не у нас а у них ( у разработчиков) и потихоньку собираются файлы. в результаты мы получаем некоторое количество бинарников ( обозначены зеленым цветом — раскраска в выводе происходит согласно настройкам окружения, основываясь на типе файла и правах доступа)

Так, вот — видим что у нас появился искомый файл zip. Запускаем его, указывая явно: ./zip, иначе система пойдет искать его  среди указанных в переменной окружения PATH местах и найдет стандартный системный а не наш zip.

Соответсвенно видим, что это версия от 5 июля 2008 года

А если запустим системный? Увидим то же самое. Почему? Потому что у нас установлен последний, находящийся в репозитарии пакет и исходники мы взяли оттуда же.

Теперь давайте посмотрим, что из себя представляет Makefile. Заглянем в каталог /unix и посмотрим на содержимое Makefile

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

  • что мы собираем ( левая часть и двоеточие:)
  • из чего мы собираем ( правя часть)

Например:

Модуль Match.o — объектный файл, получающийся в процессе компиляции, зависит от match.s — программы на языке ассемблера. А дальше, отступая табуляцией написаны на языке командного интерпретатора сборочные инструкции: нужно запустить препроцессор языка С, записанный в переменной СРР, вручить ему файл Match.S а результат перенаправить в _match.s. После препроцессирования запустить ассемблер, указанный в параметре и вручить ему этот результат. Используем mv (всегда фиксированную ибо она одна везде в unix системах) для переименовывания и удаляем промежуточные результаты.

Зачем такие хитрости? Нельзя написать просто скрипт? Ну сборщик с его встроенным интеллектом для того и нужен чтобы согласно этим целеуказаниям в дальнейшем перепроверять- если модуль match.o изменился то его надо пересобрать указанным образом, а если нет то не надо=)

Вот смотрите, мы один раз уже дали команду утилите make — зайди в каталог Unix, возьми makefile и собери пакет под generic.

makefile -f unix/Makefile generic

И что то происходило (см скриншоты выше). Попробуем еще раз — больше ничего не произойдет.

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

Видим, что происходит только конечное действие — возьмутся все объектные модели и скомпонуются в программу zip. А если мы удалим zipnote и снова запустим zip?

Он соберет только его. Откуда он знает? Это написано в сборочном в файле в виде простого формализма- дерева сборочных зависимостей.

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

mcedit zip.c

 

 

Кстати, почему именно — ? Обратимся к истории. Первые версии UNIX создавались в конце 60-х, начале 70-х на огромных 8 битных шкафах, жравших кучу электричество. Единственным средством взаимодействия с ними был т.н. телетайп — электромеханическое устройство, на котором клавиши нажимались со значительным усилием, а вывод информации осуществлялся на бумагу. Причем печатала эта машина даже не матричным способом, а как печатная машинка- с грохотом пробивая символы через копировальную ленту. Именно по этому, чтобы беречь уши разработчиков и администраторов ( в то время почти одно и то же), команды в Unix при успешном выполнении не выводили ничего ( как и сейчас команды в Linux), а печатали только краткие сообщения в случае ошибки. И чтобы беречь пальцы- ввод ключа начинался с -, потому что — можно ввести одной клавишей, а вот тот же + уже двумя (шифт).
Именно по этому во многих командах, являющихся наследниками тех времен включить что-то ( добавить в опцию команды) это -x ( где х — произвольный флаг), а вот выключить это +х. Потому что выключаем/исключаем мы что-то реже и для того чтобы лишний раз подумать. Все логично, если знать историю.

Вернемся к нашим баранам- запускаем Make и смотрим, что произошло.
Смотрите, он сам определил, что изменился только zip.c, сам его перекомпилирует (строка cc -c -I …) и сам потом пересобирает ( cc-o zip) все остальные модули+ измененный в выходной файл zip.

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

 

Сборка ПО.Сложный, интересный пример

Давайте попытаемся разобратсья с программой date. Сначала с помощью which узнаем где она лежит, далее с помощью dpkg узнаем к какому пакету она относится, а теперь, зная что делает dpkg (как  управляет по), а дальше все очень просто — выкачиваем исходники этого пакета (coreutils)

В файле dsc соответственно содержится описание данного пакета

и в нем указанны сборочные зависимости — компоненты которые нам необходимы для успешной компиляции этого добра

вот поэтому, когда мы команде apt-get source говорим скачать исходные тексты пакета, она старается подготовить все это по файлу описания, но выдает предупреждение что это сделать невозможно т.к. отсутствует dpkg-dev. А подготовка включает в себя инсталяцию сборочных инструментов. Т.е. стараются автоматизировать даже это.

В результату у нас есть пакет и мы постараемся руками собрать из него небольшой кусочек- одну утилитку date. Для этого вначале раскроем архив исходных текстов с помощью архиватора tar:   tar xvzf coreutils…..

Теперь зайдем в получившийся каталог coreutils и осмотримся. Тут все уже намного сложнее.

Итак, тут нет makefile, зато есть заготовки — makefile.in (input) и makefil.am (automake). Тут как раз makefile создается в результате работы инструментов обвязок (autotools). Так же, мы видим скрипт configure который ( а это уже маленькая Unix традиция, о которой может даже ничего быть не сказано в README) необходимо предварительно запустить. Он посмотрит, что у нас есть в системе, всего ли хватает (компилятор, сборочные инструменты, библиотеки), если чего то нет- выругается. Из альтернативных вариантов библиотек он выберет какую то согласно своей логике, хотя у него есть куча параметров, позволяющих переопределить поведение будущего процесса сборки ( ./configure —help)

Но нас пока устроит его запуск без параметров. Мы запускаем его и наблюдаем за процессом проверки и подготовки. В любой момент мы можем его приостановить с помощью комбинации Ctrl+S и посмотреть, что сейчас происходит. Вот видим, что он например проверяет наличие компилятора

Ошибки каких-то проверок будут критичными — скрипт вывалится с ошибкой, которую нужно будет устранить и запустить заново. Какие то не критичные- предупредит, изменит пару параметров сборки и пойдет дальше. Например при сборке веб-сервера apache, не найдет библиотеку openssl — выключит поддержку https протокола.
В конце подведет итог перед сборкой ( а может и не подвести). Понятно что все эти проверки каждый раз не пишутся руками. Они единожды написаны и оттестированы программистами, после упакованы в одну базу и эти скрипты в итоге автоматически создаются ( зачем и нужды всякие автотулы) но в любом случае, в итоге на выходе работы этого скрипта получится полностью параметризованный makefile, а дальше останется только запустить утилиту make и скормить ей этот  мейкфайл.

Дальше просто даем команду make. Начинается сборка. Кстати если у нас многоядерная система, можем запустить сборку в несколько потоков, указав флаг -j. Например make -j 2 — сборка в два потока. Но нам нет смысла собирать весь пакет coreutils — нам достаточно показать сборку нашей утилиты date. Делаем это так

make -j 2 src/date

Почему так странно описываем? Потому что по умолчанию make не найдет просто утилиту date, а если внимательно посмотрим по окружающим нас каталогам, то увидим каталог src а в нем файл для date.

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

В итоге получаем в каталоге src собранную утилиту date. Ее можно запустить прямо из этого же каталога и сравнить ее работу с системной.
А взяв лежащий в каталоге src/ модуль date.c можно так же что-нибудь подправить. Но это уже выходит за рамки данной статьи и превращается в работу программистов.

Итого, краткое резюме. Как происходит подготовка, сборка и установка ПО в *nix, если речь идет о ПО из исходных текстов?

  • Скачивается архив
  • распаковывается
  • Если нет mekafile но есть configure — запускается он, убеждаемся что отработал и создал makefile
  • заускаем make с параметрами, добиваемся того чтобы он все собрал и скомпилировал
  • и в конце запускаем установку с помощью make install.

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

На этом третья часть закончена. Успехов в Ваших изысканиях!

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