Наши партнеры

UnixForum






Книги по Linux (с отзывами читателей)

Библиотека сайта rus-linux.net

На главную -> MyLDP -> Тематический каталог -> Программирование и алгоритмические языки в Linux

Как происходит компиляция. Часть 3

Оригинал: Examining the compilation process. Part 3.
Автор: Mike Diehl
Дата: 10 ноября 2008
Перевод: Александр Тарасов aka oioki
Дата перевода: 8 декабря 2008

В предыдущих двух статьях, написанных мной для Linuxjournal.com, рассказывалось о том, что какие шаги проделывает компилятор GCC в процессе компиляции программы. Они основаны на знаниях, полученных на курсе по разработке ПО, законченного мной несколько лет назад. Я не планировал, что это будет серия из трех статей. Просто в какое-то время я понял, что не рассказал об утилите make. Это абсолютно невежественно, рассказывать о разработке ПО и не упомянуть make. Поэтому я решил расширить серию еще одной статьей.

Если ваш проект прост и насчитывает скажем, не более 5 исходных файлов, вполне вероятно, что утилита make вам не понадобится. Компилировать проект можно и обычным shell-скриптом, например таким, какой я использовал для простой программы с трехмерной графикой (это часть другой статьи для Linux Journal). Взгляните на этот скрипт:

#!/bin/bash
g++ ./game.cpp -lIrrlicht -lGL -lXxf86vm -lXext -lX11 -ljpeg -lpng -o game

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

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

a = a+b;

на

a = a-b;

Изменение этого одного символа означает, что вам придется перекомпилировать весь проект! Это весьма неэффективное использование вашего времени как разработчика ПО.

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

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

main: main.o f.o
	gcc main.o f.o -o main

main.o: main.c
	gcc -c main.c -o main.o

f.o: f.c
	gcc -c f.c -o f.o

Общий формат Makefile таков: сначала идет цель, затем список зависимостей этой цели, и команда для создания цели при условии, что любой файл из списка зависимостей был модифицирован.

Из этого Makefile можно извлечь ряд фактов. К примеру, исполняемый файл main зависит от объектных файлов main.o и f.o. Когда нам нужно пересобрать файл main (если изменился файл main.o либо f.o), будет использоваться команда "gcc main.o f.o -o main". Далее, файл main.o зависит от main.c. Когда изменяется файл main.c, тогда файл main.o должен быть перекомпилирован, и Makefile указывает нам, каким образом это нужно проделать. Получается, что при изменении main.c понадобится перекомпилировать main.o, и затем пересобрать исполняемый файл. Утилита make берет на себя разрешение рекурсивных зависимостей.

Цель f.o обрабатывается аналогично.

После создания этого Makefile и введя впервые make, будет произведена компиляция всего проекта с нуля:

# make
gcc -c main.c -o main.o
gcc -c f.c -o f.o
gcc main.o f.o -o main

Получим исполняемый файл main. Теперь допустим, что мы внесли изменения в файл f.c. Вновь запустив утилиту make, мы увидим:

# make
gcc -c f.c -o f.o
gcc main.o f.o -o main

Здесь видно, что файл main.c не перекомпилируется, ведь с момента последней компиляции он не менялся. Файл f.c вновь компилируется в f.o и связывается с существующим объектным файлом main.o. В результате получаем обновленный исполняемый файл main.

Если по какой-либо причине нам потребовалось перекомпилировать main.c, мы можем попросить сделать это утилиту make, следующим образом:

make main.o

В этом случае make просмотрит Makefile в текущем каталоге и определит, что должно быть сделано для достижения цели main.o.

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

Утилита make обладает еще рядом интересных функций. К примеру, make позволяет определять переменные и использовать их внутри нашего Makefile. К примеру, взгляните на отрывок Makefile одного моего проекта:

OBJ = network.o config.o protocol.o parsers.o events.o users.o
CPPFLAGS = -DTEXT_CLIENT
CXXFLAGS = -O3 -ffast-math -Wall
LDFLAGS = -lenet

game: $(OBJ) main.cpp
	g++ $(CPPFLAGS) $(CXXFLAGS) \
	main.cpp $(OBJ) $(LDFLAGS) -o game

Здесь можно увидеть несколько интересных вещей. Во-первых, мы определяем несколько переменных: OBJ, CPPFLAGS, CXXFLAGS, LDFLAGS. Эти переменные используются в дальнейшем, где мы описываем, каким образом должна собираться цель "game".

Для ясности понимания давайте посмотрим, как именно изменяется команда, указанная в цели "game". Начнем с этого:

g++ $(CPPFLAGS) $(CXXFLAGS) \
main.cpp $(OBJ) $(LDFLAGS) -o game

Теперь соответствующие ссылки на переменные заменяются их значениями, определенными ранее. В итоге получается следующая строка:

g++ -DTEXT_CLIENT -O3 -ffast-math -Wall \
main.cpp network.o config.o protocol.o parsers.o events.o users.o -lenet -o game

Думаю, вы поняли, как использовать переменные в Makefile. В реальной работе переменные можно использовать в разных частях Makefile. Они экономят время разработчика и устраняют шанс совершить опечатку.

Команды в Makefile проще читать, если разделять их символом '\', как в нашем примере. Существует простой факт: код, который легко читается, меньше подвержен ошибкам, и наш Makefile в этом случае не исключение.

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

Распространенная ошибка, которую допускают пользователи make - это неуказанные зависимости. К примеру, у вас есть файл foo, который зависит от файла bar.o, однако вы забыли указать его в списке зависимостей foo.

Теперь, если файл bar.o не существует, вы будете просто получать ошибки связывания. В этом простом случае вам будет очевидно, что именно не так - отсутствует bar.o.

Однако, представьте, что ваш проект долгое время компилировался вручную, и настолько вырос, что вы решили перейти на make. Пусть, к примеру, bar.o уже существует, но его нет в списке зависимостей для foo. В этом случае все будет хорошо... до тех пор, пока вы не найдете ошибку в bar.o. Вы ищете файлы, из которых получается bar.o, исправляете ошибку и перекомпилируете проект. После запуска программы будут обнаружены проявления той же ошибки. Вам начинает казаться, что вы забыли сохранить изменения и пробуете проделать все еще раз. То же самое. На третий раз вы вставляете в код несколько отладочных сообщений и снова перекомпилируете проект. Та же ошибка и никаких сообщений! Надеюсь, вы поняли в чем суть - мы забыли указать bar.o в списке зависимостей foo, и при очередной перекомпиляции проекта foo брался старый объектный файл bar.o. Теперь вы знаете о подобной ошибке, и поведение программы, аналогичное описанному выше, сразу натолкнет вас на мысль, что где-то есть непрописанные зависимости.

Заголовочные файлы, или файлы .h, которые используются в языке C, могут приводить к некоторым проблемам в make. В заголовочных файлах принято записывать прототипы всех внешних функций и пользовательские типы данных. Часто программисты ленятся и помещают в один заголовочный файл прототипы ВСЕХ функций и подключают его во всех исходных файлах. Таким образом получается, что все исходные файлы проекта оказываются зависимым от заголовочного файла, что логически неверно. В данном случае программист поступает неверно. Хорошим решением проблемы будет разделение заголовочного файла на отдельные файлы, каждый для своего программного модуля.

Частой практикой является использование make в качестве средства чистки проекта - удаления всех объектных и исполняемых файлов, таким образом вызывая необходимость полностью перекомпилировать проект. Типичная цель для очистки проекта выглядит так:

clean:
	rm *.o main

Программист просто вводит "make clean" и получает полностью чистый проект. Аналогичным образом, создают цель "install". Пользователю остается ввести "make install" и таким простым способом установить программу себе на компьютер. Такой подход чрезвычайно популярен.

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