15. Инструменты разработчика
Операционные системы Linux и Unix очень популярны среди программистов не только благодаря впечатляющему набору инструментов и доступных сред, но также потому, что система является исключительно хорошо документированной и прозрачной. При работе на компьютере с Linux вам не обязательно быть программистом, чтобы использовать преимущества инструментов разработки, однако вам следует знать об инструментах программирования, поскольку они играют более важную роль в управлении системами Unix, если сравнивать с другими операционными системами. В конечном итоге вы должны уметь идентифицировать утилиты для разработки, а также иметь некоторое представление о том, как их запускать.
В данной главе большой объем информации собран в небольшом пространстве, но вам не обязательно осваивать все, что здесь упоминается. Можно поверхностно ознакомиться с ним и вернуться к нему чуть позже. Вопрос, касающийся совместно используемых библиотек, вероятно, самое важное из того, что вам необходимо знать. Однако, чтобы понять, откуда берутся эти библиотеки, сначала следует получить основные сведения о том, как создаются программы.
15.1. Компилятор C
Знание того, как запустить компилятор языка программирования C, поможет вам получить серьезное представление о происхождении тех команд, которые вы встречаете в Linux. Исходный программный код большинства утилит Linux, а также многих приложений для этой системы написан на языке C или C++. В основном мы будем использовать в этой главе примеры на языке C, но вы сможете применить эту информацию и для языка C++.
Программы на языке C создаются традиционным для разработки способом: вы пишете программу, компилируете ее, а затем запускаете. То есть, когда вы пишете программу на языке C и желаете ее запустить, вы должны скомпилировать исходный код, превратив его в низкоуровневое двоичное представление, которое понимает компьютер. Можете сопоставить это с языками сценариев (о них мы поговорим чуть позже), где вам не придется ничего компилировать.
примечание
По умолчанию в большинстве версий системы нет инструментов, необходимых для компилирования кода на языке C, поскольку такие инструменты занимают довольно много пространства. Если вам не удастся обнаружить некоторые из описанных здесь инструментов, можно попробовать установить необходимый для конкретного релиза Debian/Ubuntu пакет или применить групповую установку для Fedora/CentOS с помощью менеджера yum. Если это не завершится успехом, попробуйте поискать пакет по запросу «C compiler».
Исполняемый файл компилятора C в большинстве версий систем Unix является компилятором GNU C, gcc, хотя новый компилятор clang, разработанный группой LLVM, набирает популярность. Файлы программного кода на языке C имеют расширение.c. Взгляните на одиночный модульный файл hello.c с кодом на языке C, который можно найти в книге Брайана У. Кернигана (Brian W. Kernighan) и Денниса М. Ритчи (Dennis M. Ritchie) The C Programming Language («Язык программирования C»), 2-е издание (Prentice Hall, 1988):
#include
main() {
printf("Hello, World.\n");
}
Поместите этот код в файл с названием hello.c, а затем запустите такую команду:
$ cc hello.c
В результате появится исполняемый файл с именем a.out, который можно запустить подобно любому другому исполняемому файлу системы. Однако следует присвоить этому исполняемому файлу другое имя (например, hello). Чтобы это сделать, используйте параметр компилятора — o:
$ cc — o hello hello.c
Для небольших программ компилировать больше нечего. Может понадобиться добавить каталог включаемых файлов или библиотеку (см. подразделы 15.1.2 и 15.1.3), но прежде, чем переходить к этим темам, посмотрим на программы, которые немного больше по объему.
15.1.1. Исходный код в виде нескольких файлов
Большинство программ на языке C слишком велики, чтобы уместиться в пределах единственного файла с исходным кодом. Исполинские файлы становятся неуправляемыми для программиста, а компиляторы иногда даже испытывают сложности при синтаксическом анализе больших файлов. По этой причине разработчики группируют компоненты исходного кода вместе, предоставляя каждому фрагменту отдельный файл.
При компиляции большинства файлов. c исполняемый файл создается не сразу. Вместо этого сначала используется параметр компилятора — c для каждого файла, чтобы создать объектные файлы. Чтобы понять, как это устроено, предположим, что у вас есть два файла, main.c и aux.c. Следующие две команды для компилятора выполняют основную часть работы по созданию программы:
$ cc — c main.c
$ cc — c aux.c
Эти две команды компилируют два файла источника в два объектных файла: main.o и aux.o.
Объектный файл является двоичным файлом, который процессор уже почти готов понять, если учесть еще несколько моментов. Во-первых, операционная система не знает, как запускать объектные файлы, а во-вторых, вам, вероятно, потребуется скомбинировать несколько объектных файлов и системных библиотек, чтобы создать завершенную программу.
Чтобы создать полностью функционирующий исполняемый файл из одного или нескольких объектных файлов, следует запустить компоновщик, команду ld в Unix. Программисты редко используют эту команду в командной строке, поскольку компилятор C знает, как запускать компоновщик. Для создания исполняемого файла с названием myprog из двух приведенных выше объектных файлов запустите такую команду:
$ cc — o myprog main.o aux.o
Хотя и возможно скомпилировать несколько исходных файлов вручную, как показано в этом примере, трудно отслеживать их во время компиляции, если число таких файлов велико. Утилита make, описанная в разделе 15.2, является стандартом Unix для управления компиляцией. Эта утилита особенно важна при управлении файлами, описанными в следующих двух разделах.
15.1.2. Заголовочные файлы (Include) и каталоги
Заголовочные файлы C являются дополнительными файлами с исходным кодом, который обычно содержит объявления типов и библиотечных функций. Например, файл stdio.h является заголовочным (см. простую программу в разделе 15.1).
К сожалению, с заголовочными файлами связано большое число проблем при компиляции. Большинство глюков возникает, когда компилятор не может отыскать заголовочные файлы и библиотеки. Бывают даже случаи, когда программист забывает подключить необходимый заголовочный файл, это приводит к тому, что исходный код не компилируется.
Исправление проблем, вызванных включаемыми файлами
Отследить правильный включаемый файл не всегда легко. Иногда несколько включаемых файлов с одинаковыми именами расположены в разных каталогах и неясно, какой из них правильный. Когда компилятор не может обнаружить включаемый файл, сообщение об ошибке выглядит так:
badinclude.c:1:22: fatal error: notfound.h: No such file or directory
Это сообщение говорит о том, что компилятор не может найти заголовочный файл notfound.h, на который ссылается файл badinclude.c. Эта ошибка является прямым следствием такой директивы в первой строке файла badinclude.c:
#include
По умолчанию в Unix каталогом для включаемых файлов является /usr/include; компилятор всегда просматривает его, если вы явно не укажете ему не выполнять этого. Тем не менее можно настроить компилятор так, чтобы он просматривал другие каталоги (большинство каталогов с заголовочными файлами содержит слово include где-либо в своем имени).
примечание
Из главы 16 вы узнаете о том, как отыскать отсутствующие включаемые файлы.
Предположим, вы обнаружили файл notfound.h в каталоге /usr/junk/include. Можно сделать так, чтобы компилятор видел этот каталог с помощью параметра — I:
$ cc — c -I/usr/junk/include badinclude.c
Теперь компилятор не должен спотыкаться на строке кода в файле badinclude.c, которая ссылается на заголовочный файл.
Следует также опасаться включаемых файлов, использующих двойные кавычки (" ") вместо угловых скобок (< >), например так:
#include "myheader.h"
Двойные кавычки означают, что заголовочный файл не располагается в системном каталоге для включаемых файлов и компилятору следует поискать его путь. Часто это говорит о том, что включаемый файл находится в том же каталоге, что и файл с исходным кодом. Если вам встретится проблема с двойными кавычками, то, вероятно, вы пытаетесь скомпилировать неполный исходный код.
Что такое препроцессор C (cpp)?
Оказывается, компилятор C не выполняет работу по отыскиванию всех этих включаемых файлов. Эта задача приходится на долю препроцессора C — команды, которую компилятор применяет к исходному коду, прежде чем выполнить синтаксический анализ реальной программы. Препроцессор перезаписывает исходный код в такой форме, которую способен понять компилятор; это инструмент, делающий исходный код более легким для чтения (и снабжающий его обходными маневрами).
Команды препроцессора в исходном коде называются директивами, они начинаются с символа #. Существуют три основных типа директив.
• Включаемые файлы. Директива #include дает препроцессору указание о том, чтобы он включил весь файл. Обратите внимание на то, что флаг компилятора — I является в действительности параметром, который вынуждает препроцессор искать включаемые файлы в указанном каталоге, как вы видели в предыдущем разделе.
Макроопределения. Строка, подобная #define BLAH something, говорит препроцессору о том, чтобы он выполнил замену всех вхождений элемента BLAH на элемент something в исходном коде. По соглашению названия макроопределений даются прописными буквами, но не следует удивляться тому, что программисты иногда используют макроопределения, имена которых похожи на функции и переменные. Сплошь и рядом это причиняет массу неприятностей. Многие программисты превращают в спорт неправильное использование препроцессора.
примечание
Вместо того чтобы приводить макроопределения в исходном коде, можно также передавать параметры в компилятор: команда — DBLAH=something будет работать так же, как приведенная выше директива.
Условные операторы. Можно пометить отдельные фрагменты кода с помощью слов #ifdef, #if и #endif. Директива #ifdef MACRO проверяет, определено ли макроопределение MACRO для препроцессора, а директива #if condition проверяет, является ли результат условия condition ненулевым. Для обеих директив в том случае, когда условие, следующее за директивой if, является ложным, препроцессор не передает компилятору текст программы, который расположен между директивами #if и #endif. Следует привыкнуть к этому, если вы собираетесь исследовать какой-либо код на языке C.
Приведу далее пример условной директивы. Когда препроцессор встречает такой код, он проверяет, есть ли макроопределение DEBUG, и если оно определено, передает компилятору строку, содержащую команду fprintf(). В противном случае препроцессор пропускает эту строку и продолжает обработку файла после директивы #endif:
#ifdef DEBUG
fprintf(stderr, "This is a debugging message.\n");
#endif
примечание
Препроцессор C ничего не знает о синтаксисе языка C, переменных, функциях и других элементах. Он понимает только свои собственные макроопределения и директивы.
В Unix препроцессор C называется cpp, но можно также запускать его с помощью команды gcc — E. Однако вам нечасто понадобится запускать препроцессор как таковой.
15.1.3. Связывание с библиотеками
Компилятор C знает о вашей системе недостаточно для того, чтобы самостоятельно создать пригодную программу. Для построения завершенных программ вам необходимы библиотеки. Библиотека C является набором распространенных, заранее скомпилированных функций, которые можно встраивать в программу. Например, многие исполняемые файлы используют библиотеку math, поскольку она обеспечивает работу с тригонометрическими и другими функциями.
Библиотеки вступают в игру главным образом во время компоновки, когда программа-компоновщик создает исполняемый файл из объектных файлов. Например, если у вас есть программа, которая использует библиотеку gobject, но вы забыли указать компилятору о связывании с этой библиотекой, то появятся ошибки компоновщика, подобные этой:
badobject.o(.text+0x28): undefined reference to 'g_object_new'
Наиболее важные части этого сообщения выделены жирным шрифтом. Когда компоновщик проверял объектный файл badobject.o, ему не удалось найти функцию, которая выделена жирным шрифтом, и, как следствие, не удалось создать исполняемый файл. В данном частном случае можно предположить, что вы забыли о библиотеке gobject, поскольку отсутствующая функция называется g_object_new().
примечание
Неопределенные ссылки не всегда означают, что вы упустили библиотеку. Один из объектных файлов команды может отсутствовать в команде компоновки. Но обычно достаточно легко понять, что отсутствует — библиотечные функции или объектные файлы.
Чтобы исправить эту ошибку, сначала вы должны отыскать библиотеку gobject, а затем использовать параметр компилятора — l, чтобы установить связь с библиотекой. Так же как и включаемые файлы, библиотеки разбросаны по всей системе (по умолчанию используется каталог /usr/lib), хотя большинство из них расположено в подкаталоге lib. В предыдущем примере основным файлом библиотеки gobject является libgobject.a, поэтому имя библиотеки — gobject. Объединяя все это, можно выполнить компоновку команды следующим образом:
$ cc — o badobject badobject.o — lgobject
Вы должны сообщать компоновщику о нестандартном расположении библиотеки; для этого применяется параметр — L. Допустим, что команде badobject необходим файл libcrud.a в каталоге /usr/junk/lib. Чтобы выполнить компиляцию и создать исполняемый файл, используйте команду, подобную этой:
$ cc — o badobject badobject.o — lgobject — L/usr/junk/lib — lcrud
примечание
Если вам необходимо отыскать в библиотеке некоторую функцию, применяйте команду nm. Будьте готовы к обширному отчету. Попробуйте, например, такую команду: nm libgobject.a. Вам может понадобиться команда locate, чтобы отыскать файл libgobject.a; многие версии системы теперь помещают библиотеки в подкаталоги, зависящие от архитектуры, внутри каталога /usr/lib.
15.1.4. Совместно используемые библиотеки
Библиотека, имя файла которой оканчивается на. a (например, libgobject.a), называется статической библиотекой. Когда вы связываете команду со статической библиотекой, компоновщик копирует машинный код из библиотеки в исполняемый файл. Следовательно, для работы окончательного исполняемого файла не требуется наличие исходного файла библиотеки и, более того, поведение исполняемого файла никогда не меняется.
Однако размеры библиотек постоянно возрастают, равно как и количество используемых библиотек; это делает статические библиотеки неэкономными с точки зрения использования дискового пространства и памяти. Кроме того, если впоследствии обнаружится, что статическая библиотека реализована неадекватно или является незащищенной, то нет никакого способа исправить связанный с ней исполняемый файл, кроме повторной компиляции.
Совместно используемые библиотеки избавляют от таких проблем. Когда вы запускаете команду, связанную с такой библиотекой, система загружает код библиотеки в область памяти процесса только тогда, когда это требуется. Несколько процессов могут совместно использовать один и тот же код библиотеки в памяти. Если вам потребуется немного изменить код библиотеки, то обычно это можно выполнить, не компилируя заново другие команды.
У совместно используемых библиотек есть свои издержки: трудность управления и довольно сложная процедура связывания. Тем не менее можно взять такие библиотеки под контроль, если вы будет знать четыре вещи.
• Как получить список совместно используемых библиотек, необходимых исполняемому файлу.
• Как исполняемый файл отыскивает совместно используемые библиотеки.
• Как связать команду с совместно используемой библиотекой.
• Подводные камни при использовании таких библиотек.
В следующих разделах рассказано о том, как применять и обслуживать совместно используемые библиотеки в вашей системе. Если вам интересно, как устроены эти библиотеки, или вы желаете узнать о компоновщиках в целом, обратитесь к книге Джона Р. Ливайна (John R. Levine) Linkers and Loaders («Компоновщики и загрузчики», Morgan Kaufmann, 1999) или к статье Дэвида М. Бизли (David M. Beazley), Брайана Д. Уарда (Brian D. Ward) и Йена Р. Кука (Ian R. Cooke) The Inside Story on Shared Libraries and Dynamic Loading («Внутренняя история совместно используемых библиотек и динамической загрузки», журнал Computing in Science & Engineering, сентябрь/октябрь 2001), а также к таким онлайн-ресурсам, как Program Library HOWTO (http://dwheeler.com/program-library/). Стоит также прочитать страницу руководства ld.so(8).
Вывод зависимостей совместно используемой библиотеки
Файлы совместно используемой библиотеки обычно размещаются там же, где и статические библиотеки. Двумя стандартными каталогами для библиотек в системе Linux являются /lib и /usr/lib. Каталог /lib не должен содержать статических библиотек.
Имя файла совместно используемой библиотеки содержит суффикс. so (shared object — «совместно используемый объект»), как, например, у файлов libc-2.15.so и libc.so.6. Чтобы увидеть, какие совместно используемые библиотеки применяет команда, запустите команду ldd prog (параметр prog — это имя исполняемого файла). Вот пример для команды оболочки:
$ ldd /bin/bash
linux-gate.so.1 => (0xb7799000)
libtinfo.so.5 => /lib/i386-linux-gnu/libtinfo.so.5 (0xb7765000)
libdl.so.2 => /lib/i386-linux-gnu/libdl.so.2 (0xb7760000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75b5000)
/lib/ld-linux.so.2 (0xb779a000)
В интересах оптимального быстродействия и гибкости исполняемые файлы обычно не знают о расположении своих совместно используемых библиотек; они знают лишь их названия и, возможно, немного о том, где их искать. Небольшая команда ld.so (динамический компоновщик/загрузчик времени исполнения) отыскивает и загружает совместно используемые библиотеки для команды во время исполнения. В приведенном выше отчете команды ldd имена библиотек показаны слева — именно они известны исполняемому файлу. Правая часть показывает, где команда ld.so ищет данную библиотеку.
Последняя строка приведенного отчета дает актуальное местоположение команды ld.so: ld-linux.so.2.
Как команда ld.so отыскивает совместно используемые библиотеки
Одной распространенной проблемой совместно используемых библиотек является то, что динамический компоновщик не может отыскать библиотеку. Первое местоположение, в котором компоновщику обычно следует искать совместно используемые библиотеки, — это заранее сконфигурированный путь поиска библиотеки времени исполнения (rpath) для исполняемого файла, если такой путь существует. О том, как его определить, вы вскоре узнаете.
Далее динамический компоновщик смотрит в системный кэш /etc/ld.so.cache, чтобы понять, находится ли библиотека в стандартном месте расположения. Это быстрый кэш имен файлов библиотек, найденных в каталогах, которые перечислены в файле конфигурации /etc/ld.so.conf.
примечание
Типичным для файла ld.so.conf, как и для многих файлов конфигурации Linux, которые вы уже видели, является то, что он может включать некоторые файлы из такого каталога, как /etc/ld.so.conf.d.
Каждая строка файла ld.so.conf является каталогом, который вы можете включить в кэш. Перечень каталогов обычно короткий и содержит нечто вроде этого:
/lib/i686-linux-gnu
/usr/lib/i686-linux-gnu
Каталоги стандартных библиотек /lib и /usr/lib являются неявными, это означает, что их не нужно включать в файл /etc/ld.so.conf.
Если изменить файл ld.so.conf или сделать изменения в одном из каталогов совместно используемых библиотек, необходимо перестроить файл /etc/ld.so.cache вручную с помощью следующей команды:
# ldconfig — v
Параметр — v сообщает детальную информацию о библиотеках (которую команда ldconfig добавляет в кэш), а также информацию о любых обнаруженных изменениях.
Есть еще одно место, где команда ld.so ищет совместно используемые библиотеки: переменная окружения LD_LIBRARY_PATH. Вскоре мы поговорим о ней.
Не добавляйте что-либо в файл /etc/ld.so.conf. Вы должны знать, какие совместно используемые библиотеки есть в системном кэше, а если помещать в кэш каждую непонятную маленькую библиотеку, могут возникнуть конфликты и система станет крайне неорганизованной. Когда выполняется компиляция программы, которой необходим нестандартный путь к библиотеке, передайте исполняемому файлу встроенный путь поиска библиотеки времени исполнения. Посмотрим, как это делается.
Связывание программ с совместно используемыми библиотеками
Допустим, у вас есть совместно используемая библиотека с именем libweird.so.1 в каталоге /opt/obscure/lib, с которой вам необходимо связать программу myprog. Выполните это следующим образом:
$ cc — o myprog myprog.o — Wl, — rpath=/opt/obscure/lib — L/opt/obscure/lib — lweird
Параметр — Wl, — rpath сообщает компоновщику о том, чтобы он включил следующий каталог в путь поиска библиотеки времени исполнения для исполняемого файла. Тем не менее, даже если вы используете параметр — Wl, — rpath, флаг — L по-прежнему необходим.
Если у вас уже есть готовый двоичный файл, можно применять команду patchelf, чтобы вставить в него другой путь поиска библиотеки времени исполнения, но обычно лучше делать это во время компиляции.
Проблемы, вызванные совместно используемыми библиотеками
Совместно используемые библиотеки обеспечивают замечательную гибкость, не говоря уже о некоторых действительно превосходных решениях, но в то же время их легко применить неправильно до такой степени, что ваша система превращается в полную кашу. Могут возникнуть три чрезвычайно неприятные ситуации:
• отсутствие библиотек;
• ужасное быстродействие;
• несоответствие библиотек.
Первой причиной всех проблем с совместно используемыми библиотеками является переменная окружения LD_LIBRARY_PATH. Если в этой переменной указать перечень имен каталогов, разделенных с помощью двоеточия, то тогда команда ld.so выполнит поиск в указанных каталогах прежде поиска совместно используемых библиотек где-либо еще. Это легкий способ заставить ваши программы работать, если вы переместили библиотеку и у вас нет исходного кода программы или же вы не можете использовать команду patchelf, а возможно, просто ленитесь заново компилировать исполняемые файлы. К сожалению, вы получаете то, за что платите.
Никогда не определяйте переменную LD_LIBRARY_PATH в файлах запуска оболочки или при компиляции программ. Когда динамический компоновщик встречает эту переменную, ему зачастую приходится просматривать содержимое каждого указанного каталога большее число раз, чем вы могли себе представить. Это сильно сказывается на быстродействии, но, что более важно, могут возникнуть конфликты и несоответствия библиотек, поскольку компоновщик времени исполнения просматривает эти каталоги для каждой программы.
Если вы должны использовать переменную LD_LIBRARY_PATH, чтобы запустить какую-либо программу, для которой у вас нет исходного кода (или приложение, которое вы предпочли бы не компилировать, вроде Mozilla или каких-либо других), применяйте сценарий обертки. Допустим, исполняемому файлу /opt/crummy/bin/crummy.bin необходимы совместно используемые библиотеки из каталога /opt/crummy/lib. Напишите сценарий обертки с именем crummy следующим образом:
#!/bin/sh
LD_LIBRARY_PATH=/opt/crummy/lib
export LD_LIBRARY_PATH
exec /opt/crummy/bin/crummy.bin $@
Если избегать переменной LD_LIBRARY_PATH, то можно предотвратить большинство проблем с совместно используемыми библиотеками. Иногда возникает еще одна существенная проблема для разработчиков: интерфейс прикладного программирования (API) для какой-либо библиотеки может немного измениться при переходе от одной младшей версии к другой, это нарушит работу установленных программ. Лучшие решения проблемы являются профилактическими: либо пользуйтесь последовательной методологией, чтобы установить совместно используемые библиотеки с помощью команды — Wl, — rpath для создания ссылки на путь времени исполнения, либо просто применяйте статические версии непонятных библиотек.
15.2. Утилита make
Программа, у которой есть несколько файлов исходного кода или для которой необходимы необычные параметры компиляции, слишком неудобна для компиляции вручную. Эта проблема возникает на протяжении многих лет, и традиционным средством Unix для управления компиляцией является утилита make. Вам следует узнать немного об этой утилите, если вы работаете в системе Unix, поскольку системные утилиты иногда опираются на нее в своей работе. Однако данная глава является лишь верхушкой айсберга. Утилите make посвящены целые книги, например Managing Projects with GNU Make («Управление проектами с помощью утилиты GNU Make») Роберта Мекленбурга (Robert Mecklenburg) (O’Reilly, 2004). Кроме того, большинство пакетов Linux собрано с использованием дополнительного уровня, основанного на утилите make или подобном средстве. Есть множество систем для сборки; одну из них с названием Autotools мы рассмотрим в главе 16. Утилита make является большой системой, но получить представление о том, как она работает, совсем не трудно. Когда вы увидите файл с именем Makefile или makefile, знайте, что вы имеете дело с утилитой make. Попробуйте запустить команду make, чтобы понять, можно ли что-нибудь собрать.
Главной идеей утилиты make является цель — то, чего вы желаете достичь. Целью может быть файл (файл. o, исполняемый файл и т. д.) или ярлык. Кроме того, некоторые цели зависят от других целей; например, вам необходимо создать полный набор файлов. o, прежде чем вы сможете скомпоновать исполняемый файл. Эти требования называются зависимостями.
Чтобы собрать цель, утилита make следует какому-либо правилу, например, определяющему, как перейти от исходного файла. c к объектному файлу.o. Утилите make уже известны некоторые правила, но вы можете изменить их, а также создать собственные.
15.2.1. Пример файл Makefile
Следующий очень простой файл Makefile собирает программу myprog из файлов aux.c и main.c:
# object files
OBJS=aux.o main.o
all: myprog
myprog: $(OBJS)
$(CC) — o myprog $(OBJS)
Символ # в первой строке этого файла означает комментарий.
Следующая строка является всего лишь макроопределением; она задает для переменной OBJS два имени объектных файлов. Это будет важно в дальнейшем. Сейчас обратите внимание на то, как записывается макроопределение и как на него ссылаются далее (($(OBJS)).
Следующий элемент файла Makefile содержит первую цель, all. Первая цель всегда является целью по умолчанию, утилита make будет собирать ее, если вы запустите команду make в командной строке саму по себе.
Правило сборки цели следует после двоеточия. Для цели all в этом файле Makefile сказано, что вам необходимо удовлетворить чему-то по имени myprog. Это первая зависимость в данном файле; цель all зависит от myprog. Заметьте, что myprog может быть реальным файлом или целью другого правила. В данном случае оно является и тем и другим (правилом для цели all и целью для OBJS).
Чтобы собрать программу myprog, этот файл Makefile использует макроопределение $(OBJS) в зависимостях. Макроопределение развертывается в имена aux.o и main.o, поэтому программа myprog зависит от этих двух файлов (они должны быть реальными файлами, поскольку нигде в файле Makefile нет целей с такими именами).
Данный файл Makefile предполагает, что у вас есть два файла с исходным кодом на языке C: aux.c и main.c в одном каталоге. Если запустить утилиту make для файла Makefile, то в результате будут показаны команды, выполняемые утилитой:
$ make
cc — c -o aux.o aux.c
cc — c -o main.o main.c
cc — o myprog aux.o main.o
Схема зависимостей приведена на рис. 15.1.
Рис. 15.1. Зависимости в файле Makefile
15.2.2. Встроенные правила
Каким же образом утилита make узнает о том, как перейти от файла aux.c к файлу aux.o? Ведь файла aux.c нет внутри файла Makefile. Ответ такой: утилита make следует встроенным правилам. Она знает о том, что следует искать файл. c, если вам необходим файл. o, и, более того, она знает, как запустить команду cc — c для этого файла. c, чтобы добиться цели — создать файл.o.
15.2.3. Окончательная сборка программы
Последний шаг при получении программы myprog довольно хитрый, но идея достаточно ясная. Когда у вас появятся два объектных файла в макроопределении $(OBJS), можно запустить компилятор C в соответствии со следующей строкой (здесь переменная $(CC) развертывается в имя компилятора):
$(CC) — o myprog $(OBJS)
Отступ перед переменной $(CC) является табуляцией. Вы обязаны вставлять табуляцию перед каждой реальной командой в той строке, где она находится.
Остерегайтесь следующего сообщения:
Makefile:7: *** missing separator. Stop.
Подобная ошибка означает, что файл Makefile не в порядке. Табуляция является разделителем, и, если она отсутствует или есть какая-либо другая помеха, вы увидите такую ошибку.
15.2.4. Поддержание актуальных версий файлов
Одним из последних фундаментальных свойств утилиты make является то, что ее цели должны соответствовать по времени зависимостям. Если вы дважды вызовете утилиту make из предыдущего примера, первая из них соберет программу myprog, но вторая выдаст такое сообщение:
make: Nothing to be done for 'all'.
Во второй раз утилита make посмотрела свои правила и обнаружила, что программа myprog уже существует, поэтому она не стала повторно собирать ее, поскольку ни одна из зависимостей не изменилась с тех пор, как была собрана программа. Чтобы поэкспериментировать, выполните следующее.
1. Запустите команду touch aux.c.
2. Запустите утилиту make еще раз. На этот раз она определит, что файл aux.c является более «свежим», чем файл aux.o, уже находящийся в каталоге, поэтому она скомпилирует файл aux.o заново.
3. Программа myprog зависит от файла aux.o, и теперь файл aux.o является более новым, чем уже существующая программа myprog, поэтому утилита make должна создать программу myprog заново.
«Цепные реакции» такого типа весьма характерны.
15.2.5. Аргументы и параметры командной строки
Вы можете извлечь существенную пользу от утилиты make, если будете знать, как работают ее аргументы и параметры командной строки.
Один из самых полезных параметров позволяет указать единичную цель в командной строке. Для предыдущего файла Makefile можно запустить команду make aux.o, если вам необходим только файл aux.o.
Можно также создать макроопределение в командной строке. Чтобы, например, использовать компилятор clang, попробуйте такую команду:
$ make CC=clang
Здесь утилита make использует ваше определение переменной CC вместо собственного компилятора по умолчанию cc. Макроопределения из командной строки становятся полезны при тестировании определений и библиотек препроцессора, в особенности с макроопределениями CFLAGS и LDFLAGS, о которых мы вкратце поговорим.
На самом деле вам даже не требуется файл Makefile для запуска утилиты make. Если встроенные в утилиту правила соответствуют цели, можно просто попросить утилиту создать цель. Например, если у вас есть исходный код для очень простой программы blah.c, попробуйте запустить команду make blah. Работа утилиты будет выглядеть так:
$ make blah
cc blah.o — o blah
Подобный вариант использования утилиты подходит только для очень простых программ. Если же вашей программе необходимы библиотека или специальный каталог для включаемых файлов, то, вероятно, потребуется написать файл Makefile. Запуск утилиты без файла Makefile в действительности наиболее полезен в том случае, когда вы имеете дело с чем-либо вроде языка Fortran, Lex или Yacc и не знаете, как работает компилятор или утилита. Почему бы не дать возможность утилите make выяснить это за вас? Даже если ей не удастся создать цель, она, вероятно, снабдит вас очень хорошей подсказкой о том, как применять данный инструмент.
Среди ряда параметров утилиты make особо выделяются следующие:
• — n — выводит список команд, которые необходимы для сборки, но удерживает утилиту make от запуска каких-либо команд;
• — f file — дает утилите make указание на чтение из файла file вместо файлов Makefile или makefile.
15.2.6. Стандартные макроопределения и переменные
У команды make есть много специальных макроопределений и переменных. Сложно уловить различия между макроопределением и переменной, поэтому мы будем использовать термин «макроопределение» для обозначения чего-либо, что обычно не изменяется, когда утилита приступает к сборке целей.
Как вы видели ранее, можно назначить макроопределения в начале файла Makefile. Вот самые распространенные из них.
• CFLAGS. Параметры компилятора C. При создании объектного кода из файла. c утилита make передает этот параметр компилятору в качестве аргумента.
• LDFLAGS. Подобны параметрам CFLAGS, но они предназначены для компоновщика при создании исполняемого файла из объектного кода.
• LDLIBS. Если вы используете параметры LDFLAGS, но не желаете комбинировать параметры имени библиотеки с путем поиска, поместите параметры имени библиотеки в этот файл.
• CC. Компилятор C. По умолчанию это команда cc.
• CPPFLAGS. Параметры препроцессора C. Когда утилита make каким-либо образом запускает препроцессор, она передает ему в качестве аргумента это макроопределение.
• CXXFLAGS. Утилита GNU make использует эти параметры в качестве флагов для компилятора C++.
Переменная утилиты make изменяется, когда происходит сборка целей. Поскольку вы никогда не определяете переменные утилиты make вручную, следующий перечень содержит символ $:
• $@ — внутри правила эта переменная развертывается в имя текущей цели;
• $* — развертывается в базовое имя текущей цели. Например, если вы собираете файл blah.o, эта переменная будет развернута в blah.
Наиболее полный перечень переменных Linux можно найти в info-руководстве к утилите make.
примечание
Помните о том, что утилита GNU make обладает множеством таких расширений, встроенных правил и функций, каких нет у других вариантов. Это замечательно, пока вы работаете в Linux, но, если вы окажетесь за компьютером с Solaris или BSD и будете рассчитывать, что все станет работать так же, вас может поджидать сюрприз. Однако с этой проблемой можно разобраться, если применить такую многоплатформенную систему сборки, как GNU autotools.
15.2.7. Обычные цели
Большинство файлов Makefiles содержит некоторые стандартные цели, которые выполняют вспомогательные задачи, относящиеся к компиляции.
• clean. Цель clean является повсеместной; команда make clean обычно дает утилите make указание удалить все объектные и исполняемые файлы, чтобы можно было начать все заново или подготовить пакет ПО. Вот пример правила для файла Makefile программы myprog:
clean:
rm — f $(OBJS) myprog
• distclean. В файле Makefile, созданном с помощью системы GNU autotools всегда есть цель distclean, чтобы удалить все, что не является частью исходного дистрибутива, включая и сам файл Makefile. Подробнее об этом вы узнаете из главы 16. В очень редких случаях вы обнаружите, что разработчик предпочитает удалять исполняемые файлы не с помощью этой цели, а чем-либо вроде цели realclean.
• install. Копирует файлы и скомпилированные программы в местоположение системы, которое файл Makefile считает надлежащим. Это может быть опасным, поэтому всегда сначала запустите команду make — n install, чтобы увидеть, что произойдет, не выполняя в действительности никаких команд.
• test или check. Некоторые разработчики предусматривают цели test или check, чтобы убедиться в том, что все работает после выполнения сборки.
• depend. Создает зависимости, вызывая компилятор с параметром — M для проверки исходного кода. Эта цель выглядит необычно, поскольку часто она изменяет сам файл Makefile. Это больше не является общепринятой практикой, но, если вам встретятся инструкции, в которых говорится об использовании этого правила, следуйте им.
• all. Часто является первой целью в файле Makefile. Вам много раз будут попадаться ссылки на эту цель, а не на исполняемый файл.
15.2.8. Устройство файла Makefile
Несмотря на то что существуют различные стили файла Makefile, большинство программистов придерживается некоторых основных принципов его написания. Для начала в первой части файла Makefile (внутри макроопределений) следует указать библиотеки и включаемые файлы, сгруппированные в соответствии с пакетами:
MYPACKAGE_INCLUDES=-I/usr/local/include/mypackage
MYPACKAGE_LIB=-L/usr/local/lib/mypackage — lmypackage
PNG_INCLUDES=-I/usr/local/include
PNG_LIB=-L/usr/local/lib — lpng
Каждый тип флагов компилятора и компоновщика часто оформляется в виде таких макроопределений:
CFLAGS=$(CFLAGS) $(MYPACKAGE_INCLUDES) $(PNG_INCLUDES)
LDFLAGS=$(LDFLAGS) $(MYPACKAGE_LIB) $(PNG_LIB)
Объектные файлы обычно группируются в соответствии с исполняемыми файлами. Допустим, например, что у вас есть пакет, который создает исполняемые файлы boring и trite. У каждого из них есть файл. c с исходным кодом и необходимый код в файле util.c. Вы можете увидеть нечто вроде следующего:
UTIL_OBJS=util.o
BORING_OBJS=$(UTIL_OBJS) boring.o
TRITE_OBJS=$(UTIL_OBJS) trite.o
PROGS=boring trite
Остальная часть файла Makefile могла бы выглядеть так:
all: $(PROGS)
boring: $(BORING_OBJS)
$(CC) — o $@ $(BORING_OBJS) $(LDFLAGS)
trite: $(TRITE_OBJS)
$(CC) — o $@ $(TRITE_OBJS) $(LDFLAGS)
Вы могли бы скомбинировать две цели для исполняемых файлов в виде одного правила, но обычно так поступать не следует, поскольку вам было бы непросто перенести правило в другой файл Makefile и удалить исполняемый файл или группу исполняемых файлов в отдельности. Более того, зависимости стали бы некорректными: если бы у вас было лишь одно правило для файлов boring и trite, файл trite зависел бы от файла boring.c, файл boring — от файла trite.c, и утилита make всегда пыталась бы собрать заново обе программы, если вы изменили один из файлов с исходным кодом.
примечание
Если вам необходимо определить специальное правило для объектного файла, поместите такое правило непосредственно над правилом, которое задает сборку исполняемого файла. Если несколько исполняемых файлов используют один и тот же объектный файл, поместите правило для объектного файла над всеми правилами для исполняемых файлов.
15.3. Отладчики
Стандартным отладчиком в системах Linux является gdb; доступны также системы с дружественным к пользователю интерфейсом, например Eclipse IDE и Emacs. Чтобы включить полную отладку ваших программ, запустите компилятор с параметром — g для записи таблицы имен и другой отладочной информации в исполняемый файл. Чтобы запустить отладчик gdb для исполняемого файла program, выполните такую команду:
$ gdb program
Вы должны получить приглашение (gdb). Чтобы запустить программу program с параметром командной строки options, введите следующую команду после приглашения отладчика:
(gdb) run options
Если программа в порядке, она должна запускаться, работать и завершать выполнение нормально. Однако, если возникает проблема, отладчик gdb останавливается, выводит ошибочный исходный код и возвращает вас в строку приглашения (gdb). Поскольку фрагмент исходного кода часто содержит подсказку о причине проблемы, вам может потребоваться вывести значение какой-либо переменной, с которой связана ошибка. Команда print работает также для массивов и структур языка C.
(gdb) print variable
Чтобы отладчик остановил программу в указанном месте исходного кода, используйте контрольные точки. В следующей команде файл file является файлом с исходным кодом, а параметр line_num — это номер строки этого файла, в которой отладчик должен остановиться:
(gdb) break file: line_num
Для продолжения отладки выполните такую команду:
(gdb) continue
Чтобы удалить контрольную точку, введите команду:
(gdb) clear file: line_num
Этот раздел содержит только краткое введение в отладчик gdb в надежде на то, что вы изучите более полное руководство, онлайн или в печатном виде, например 10-е издание книги Ричарда М. Столлмана (Richard M. Stallman) и др. Debugging with GDB («Отладка с помощью GDB», GNU Press, 2011). Еще одним руководством по отладке является книга Нормана Матлофа (Norman Matloff) и Питера Джея Зальцмана (Peter Jay Salzman) The Art of Debugging («Искусство отладки», No Starch Press, 2008).
примечание
Если вам интересно выявление проблем в памяти и запуск профильных тестов, посетите сайт проекта Valgrind (http://valgrind.org/).
15.4. Инструменты Lex и Yacc
Инструменты Lex и Yacc могли встретиться вам при компиляции программ, которые читают файлы конфигурации или команды. Эти инструменты являются строительными блоками для языков программирования.
• Lex — это разметчик (tokenizer), который переводит текст в пронумерованные теги с ярлыками. Версия GNU/Linux для этого инструмента называется flex. Для его совместной работы с компоновщиком могут потребоваться флаги — ll или — lfl.
• Yacc — это синтаксический анализатор, который пытается считывать метки в соответствии с грамматикой. Анализатор GNU называется bison; для его совместимости с Yacc запустите команду bison — y. Может потребоваться флаг компоновщика — ly.
15.5. Языки сценариев
В давние времена обычному системному администратору Unix не приходилось особенно беспокоиться насчет других языков сценариев, кроме Bourne shell и awk. Сценарии оболочки (рассмотренные в главе 11) по-прежнему остаются важной частью системы Unix, но язык awk понемногу сходит со сценарной арены. В то же время появились его мощные наследники, и теперь многие системные команды созданы не на языке C, а на языках сценариев (например, практичная версия команды whois). Рассмотрим некоторые основы сценариев.
Для начала вам необходимо знать о любом языке сценариев следующее: первая строка сценария выглядит так же, как и в сценарии оболочки Bourne shell. Например, сценарий на языке Python начинается так:
#!/usr/bin/python
Или так:
#!/usr/bin/env python
В Linux любой исполняемый текстовый файл, начинающийся символами #! является сценарием. Путь, который следует за этим префиксом, представляет исполняемый файл интерпретатора языка сценариев. Когда Unix пытается запустить исполняемый файл, который начинается с символов #! она выполняет следующую за ним команду, используя оставшуюся часть файла как стандартный ввод. Следовательно, даже такой код является сценарием:
#!/usr/bin/tail -2
This program won't print this line,
but it will print this line…
and this line, too.
Первая строка сценария оболочки часто содержит одну из самых распространенных проблем со сценариями: неверный путь к интерпретатору языка сценариев. Допустим, вы назвали предыдущий сценарий myscript. Что будет, если команда tail на самом деле находится в вашей системе в каталоге /bin вместо /usr/bin? В этом случае запуск сценария myscript вызвал бы такую ошибку:
bash:./myscript: /usr/bin/tail: bad interpreter: No such file or directory
Не рассчитывайте на то, что в первой строке сценария будет работать несколько аргументов. То есть аргумент -2 в приведенном примере мог бы сработать, но если вы добавите еще один аргумент, то система могла бы расценивать -2 и этот новый аргумент как один большой аргумент с пробелами, и все. Это может быть различным для разных систем, поэтому не испытывайте свое терпение на таких малозначащих вещах, как эта.
Теперь рассмотрим некоторые языки.
15.5.1. Python
Python — это язык сценариев с хорошим сопровождением и набором мощных функций, таких как обработка текста, доступ к базам данных, работа с сетью и многопоточный режим. Он обладает производительным интерактивным режимом и хорошо организованной объектной моделью.
Его исполняемый файл называется python и обычно помещается в каталоге /usr/bin. Тем не менее язык Python применяется не только для создания сценариев командной строки. Его можно встретить также в качестве инструмента для создания сайтов. Замечательным справочником, который содержит в начале небольшое руководство, может стать 4-е издание книги Дэвида М. Бизли (David M. Beazley) Python Essential Reference («Основной справочник по языку Python», Addison-Wesley, 2009).
15.5.2. Perl
Одним из старейших языков сценариев Unix, разработанным независимо, является Perl. Это действительно «швейцарский армейский нож» среди инструментов программирования. Хотя язык Perl в последние годы уступил часть позиций языку Python, он превосходен, в частности, при обработке текста, конвертации файлов и работе с ними. Вы можете встретить много инструментов, созданных с его помощью. Введением, изложенным в стиле учебника, может послужить 6-е издание книги Рэндала Л. Шварца (Randal L. Schwartz), Брайана Д. Фойя (Brian D. Foy) и Тома Феникса (Tom Phoenix) Learning Perl («Осваиваем язык Perl», O’Reilly, 2011). Более полным справочником является книга Modern Perl («Современный язык Perl») группы Chromatic (Onyx Neon Press, 2014).
15.5.3. Другие языки сценариев
Вы можете встретить также следующие языки сценариев.
• PHP. Этот язык обработки гипертекста часто можно увидеть в динамических веб-сценариях. Некоторые пользователя применяют язык PHP для автономных сценариев. Сайт проекта PHP — http://www.php.net/.
Ruby. Приверженцы объектно-ориентированного подхода и веб-разработчики обожают программировать на этом языке (http://www.ruby-lang.org/).
JavaScript. Этот язык применяется в браузерах в основном для работы с динамическим содержимым. Наиболее искушенные программисты остерегаются использовать его как средство для создания автономных сценариев из-за его многочисленных недостатков, но этого практически невозможно избежать в веб-программировании. В своей системе вы сможете отыскать его реализацию с именем Node.js и исполняемым файлом node.
Emacs Lisp. Разновидность языка программирования Lisp, которая используется в текстовом редакторе Emacs.
Matlab, Octave. Matlab является платным языком программирования и библиотекой для матричных и математических вычислений. Существует очень похожий на него бесплатный проект Octave.
R. Популярный бесплатный язык для статистического анализа. Дополнительную информацию можно получить на сайте http://www.r-project.org/ и в книге Нормана Матлофа (Norman Matloff) The Art of R Programming («Искусство программирования на языке R», No Starch Press, 2011).
Mathematica. Еще один коммерческий математический язык программирования с библиотеками.
m4. Это язык обработки макроопределений, который обычно находится только в утилите GNU autotools.
Tcl (Tool command language, инструментальный командный язык). Это простой язык сценариев, обычно ассоциируемый с инструментарием графического интерфейса пользователя Tk и утилитой автоматизации Expect. Хотя язык Tcl уже не столь широко распространен, как раньше, не пренебрегайте его возможностями. Многие бывалые разработчики предпочитают использовать язык Tk, в особенности за его внедренные возможности. Подробности можно узнать на сайте http://www.tcl.tk/.
15.6. Java
Язык Java является транслируемым, подобно языку C, с более простым синтаксисом и мощной поддержкой объектно-ориентированного программирования. В системах Unix у него несколько специальных применений. Так, он часто используется как среда для веб-приложений, а также популярен для специальных программ. Например, приложения для платформы Android обычно написаны на языке Java. Хотя он и не часто встречается в типичном рабочем столе Linux, вы должны знать, как он работает, по меньшей мере в автономных приложениях.
Есть два вида компиляторов Java: собственные компиляторы, создающие машинный код для вашей системы (подобно компилятору С), и компиляторы байт-кода для его использования в интерпретаторе байт-кода (иногда называемом виртуальной машиной, которая отличается от виртуальной машины, предлагаемой гипервизором, как описано в главе 17). В Linux вы практически всегда будете встречать байт-код.
Имена файлов байт-кода Java заканчиваются на. class. Среда времени исполнения Java (JRE, Java runtime environment) содержит все команды, которые необходимы вам для запуска байт-кода. Чтобы выполнить байт-код, используйте такую команду:
$ java file.class
Можно также встретить файлы байт-кода, которые оканчиваются на. jar; это подборки заархивированных файлов. class. Чтобы запустить файл. jar, используйте следующий синтаксис:
$ java — jar file.jar
Иногда вам потребуется указать в переменной окружения JAVA_HOME путь к среде Java. Если вам сильно не повезет, то придется использовать переменную CLASSPATH, чтобы включить все каталоги, которые содержат ожидаемые командой классы. Этот список каталогов приводится с разделителем-двоеточием, подобно обычной переменной PATH для исполняемых файлов.
Если вам необходимо скомпилировать файл. java в байт-код, потребуется набор для Java-разработки (JDK, Java Development Kit). Запустить компилятор javac из набора JDK для создания файлов. class можно так:
$ javac file.java
В набор JDK входит также команда jar, которая позволяет собирать файлы. jar. Она работает подобно команде tar.
15.7. Заглядывая вперед: компиляция программных пакетов
Мир компиляторов и языков сценариев велик и постоянно расширяется. Пока пишутся эти строки, набирают популярность новые транслируемые языки, такие как Go (golang) и Swift.
Инфраструктура компиляции LLVM (http://llvm.org/) существенно облегчила разработку компиляторов. Если вам интересно узнать о разработке и реализации компиляторов, в этом вам помогут две хорошие книги: Compilers: Principles, Techniques and Tools («Компиляторы: принципы, методы и инструменты») Альфреда В. Эхоу (Alfred V. Aho) (2-е издание, Addison-Wesley, 2006) и Modern Compiler Design («Разработка современного компилятора») Дика Грюна (Dick Grune) и др. (2-е издание, Springer, 2012). О разработке с помощью языков сценариев лучше узнавать из онлайн-ресурсов, поскольку реализации весьма различаются.
Теперь, когда вы знакомы с основами инструментов программирования в системе, вы готовы узнать, что они могут делать. Следующая глава полностью посвящена тому, как в Linux создавать пакеты программного обеспечения на основе исходного кода.