Основы компиляции программ в ОС Linux.

Основы использования утилиты MAKE.

Утилита ‘make’ автоматически определяет, какие части составной программы (программы, состоящей из нескольких файлов) необходимо перекомпилировать, и выполняет их команды для перекомпиляции.

Далее использование ‘make’ будет показано на С программах, так как они наиболее часто используются в Linux, но вы можете испльзовать ‘make’ с любым другим языком программирования, чей компилятор может работать с командами интерпретатора. Конечно ‘make’ неограничена программированием. Вы можете ее использовать для описания любой задачи, в которой некоторые файлы должны обновляться автоматически , всякий раз когда изменяютя файлы, от которых они зависят.

Для подготовки использования ‘make’, необходимо создать файл, называемый “makefile” или “Makefile” , который описывает зависимости среди файлов, составляющих вашу программу и задает команды для обновления каждого файла. При создании программы на любом языке программирования, обычно, исполнимый файл обновляется ( или создается) из объектных файлов, которые в свою очередь обновляются (создаются) посредством компиляции файлов исходных текстов программы.

Если существует подходящий makefile , при каждом изменении некоторых исходных файлов, составляющих программу, команда оболочки

Make

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

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

Когда ‘make’ перекомпилирует редактор, каждый измененный файл исходных текстов должен быть перекомпилирован. Если заголовочный файл был изменен, каждый файл исходных текстов, включающий этот зоголовочный файл должен быть перекомпилирован. Каждая процедура компиляции создает объектный файл соответствующий исходному. Наконец, если любой файл исходных текстов был перекомпилирован, все объектные файлы, и вновь созданные и осташиеся от предыдущей компиляции, должны быть собраны вместе линковщиком для создания нового исполнимого файла текстового редактора.

Простой makefile состоит из правил следующего вида:

ЦЕЛЬ ... : ЗАВИСИМОСТИ ...

КОМАНДЫ

...

...

ЦЕЛЬ – это обычно имя файла, который создается программой; примерами ЦЕЛИ являются исполнимые и объектные файлы. ЦЕЛЬ также может быть именем действия , которое необходимо выполнить, такое как ‘clean’ (очистка от ненужных файлов).

ЗАВИСИМОСТИ – это файлы, кторые используются как исходные для создания файла цели. ЦЕЛЬ обычно зависит от нескольких файлов.

КОМАНДЫ – это действия которые выполняет ‘make’. Правило может иметь более одной кманды, каждая из которых должна пиписаться с новой строки. Заметим, что вначале каждой строки команд необходимо использовать символ табуляции.

Обычно КОМАНДЫ используются в правиле с ЗАВИСИМОСТЯМИ и служат для создания файла ЦЕЛИ если изменится какая нибудь ЗАВИСИМОСТЬ. Однако есть правила, которые определяют КОМАНДЫ для ЦЕЛИ не имея ЗАВИСИМОСТЕЙ. Например, правило ,содержащее КОМАНДЫ удаления и ассоциированное с целью ‘clean’ не имеет ЗАВИСИМОСТЕЙ.

Принцип работы make следующий - после запуска, make начинает сначала просматривать содержимое файла makefile. Найдя первую ЦЕЛЬ, make смотрит по порядку написания ЗАВИСИМОСТИ. Если ЗАВИСИМОСТЬ является файлом, но не является ЦЕЛЬЮ какого нибудь другого правила, то сравниваются времена последней модификации этой ЗАВИСИМОСТИ и ЦЕЛИ. Если время последней модификации ЦЕЛИ более раннее, чем ЗАВИСИМОСТИ, то make отмечает,что необходимо выполнение КОМАНД данного правила и переходит к следующей ЗАВИСИМОСТИ. Если ЗАВИСИМОСТЬ является ЦЕЛЬЮ какого нибудь другого правила, то make вначале переходит к выполнению этого зависимого правила. Затем make возвращается к исходному правилу и смотрит времена последней модификации ЦЕЛИ и данной ЗАВИСИМОСТИ. После прохождения всех ЗАВИСИМОСТЕЙ make выполняет КОМАНДЫ. В случае, если после прохождения всех ЗАВИСИМОСТЕЙ правила выяснится, что времена последней модификации ЗАВИСИМОСТЕЙ и ЦЕЛИ совпадают, то make выполнять КОМАНДЫ не будет и прекратит работу. Замтим, что выполняется по умолчанию только одно первое встретившееся правило. Если есть правила, не являющееся ЗАВИСИМОСТЬЮ первого, то для его выполнения необходимо указать его имя в качестве аргумента make. Например ,если нам необходимо удалить ненужные файлы, и в makefile присутствует ЦЕЛЬ clean, то для ее выполнения необходимо ввести команду

Make clean

Main.o : main.c defs.h

Cc -c main.c

Cc -c kbd.c

Cc -c command.c

Cc -c display.c

Cc -c insert.c

Cc -c search.c

Cc -c files.c

Utils.o : utils.c defs.h

Cc -c utils.c

clean :

rm edit main.o kbd.o command.o display.o \

Main.o : main.c defs.h

Cc -c main.c

то make временно приостанавливает выполнение предыдущего и переходит к выполнению этого правила. main.o зависит от main.c defs.h (т.е для полученияобъектого файла main.o необходимо откомпилировать файл исходных текстов main.c в который включен заголовочный файл defs.h ). Далее make опять просматривает ЗАВИСИМОСТИ на предмет их совпадения с акой либо целью другого правила. Т.к в нашем примере совпадения нет, то сравниваются времена последней модификации файлов main.o и main.c defs.h .Если время последней модификации хотябы одного из файлов main.o и main.c более позднее, чем main.o , то make выполняет КОМАНДЫ cc -c main.c (т.е осуществляет компиляцию изменивщегося файла ЗАВИСИМОСТИ) и переходит к следующей ЗАВИСИМОСТИ предыдущей цели. При этом время последней модификации файла main.o изменится ( это время будет необходимо в предыдущей цели). Если эти времена совпадают, то КОМАНДЫ не выполняются , а происходит возврат к предыдущей цели. По приведенной аналогии make просматривает все ЗАВИСИМОСТИ цели edit. После этого сравниваютя времена последней модификации файлов ЦЕЛИ edit и ЗАВИСИМОСТЕЙ main.o kbd.o command.o display.o insert.o search.o files.o utils.o . Если время последней модификации хотябы одного из файлов ЗАВИСИМОСТИ будет более поздним чем у ЦЕЛИ , то make ваполнит КОМАНДЫ cc -o edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o для получения нового исполнимого файла редактора.

Заметим, т.к цель clean не является ЗАВИСИМОСТЬЮ ни в первой ни в подчиненных целях, то это правило простым введением команды ‘make’ никогда не выполнится. Для его выолнения необходимо, как было сказано выше ввести команду ‘make clean’. Т.к. ЦЕЛЬ clean не имеет ЗАВИСИМОСТЕЙ, то выполнится только она, и никакие другие правила выполнены не будут.

Основы компиляции программ в ОС Linux.

Компилятор превращает код программы на "человеческом" языке в объектный код понятный компьютеру. Компиляторов под Linux существует много, практически для каждого распространенного языка. Большинство самых востребованных компиляторов входит в набор GNU Compiler Collection, известных под названием GCC.

Изначально аббревиатура GCC имела смысл GNU C Compiler, но в апреле 1999 года сообщество GNU решило взять на себя более сложную миссию и начать создание компиляторов для новых языков с новыми методами оптимизации, поддержкой новых платформ, улучшенных runtime-библиотек и других изменений. Поэтому сегодня коллекция содержит в себе компиляторы для языков C, C++, Objective C, Chill, Fortran, Ada и Java, как библиотеки для этих языков (libstdc++, libgcj, ...).

Компиляция программ производится командой:

gcc <имя_файла>

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

./a.out

Для примера давайте напишем маленькую простейшую программку:

#include <stdio.h> int main(){ printf("[http://linux.firststeps.ru]\n"); printf("Our first program for Linux.\n"); return 0;};

И запустим ее:

Основы компиляции программ в ОС Linux. - student2.ru

Любой компилятор по умолчанию снабжает объектный файл отладочной информацией. Компилятор gcc также снабжает файл такой информацией и на результат вы можете посмотреть сами. При компиляции проекта из предыдущего шага у нас появился файл a.out размером 11817 байт (возможно у вас он может быть другого размера).

Вся эта отладочная информация предназначается для отладки программы отладчиком GNU Debugger. Запустить его вы можете командой:

gdb a.out

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

Компилятор gcc может создавать отладочную информацию в различных объемах и форматах, контролировать которые можно специальными ключами. Посмотреть их подробное описание можно командой man gcc:

Debugging Options -a -dletters -fpretend-float -g -glevel -gcoff -gxcoff -gxcoff+ -gdwarf -gdwarf+ -gstabs -gstabs+ -ggdb -p -pg -save-temps -print-file-name=library -print-libgcc-file-name -print-prog-name=program

Ключ -g создает отладочню информацию в родном для операционной системы виде, он выбирает между несколькими форматами: stabs, COFF, XCOFF или DWARF. На многих системах данный ключ позволяет использовать специальную информацию, которую умеет использовать только отладчик gdb. Другие ключи позволяют более тонко контролировать процесс встраивания отладочной информации.

Ключ -ggdb включает в исполняемый файл отладочную информацию в родном для ОС виде и дополняет ее специализированной информацией для отладчика gdb.

Ключ -gstabs создает отладочную информацию в формате stabs без дополнительных расширений gdb. Данный формат используется отладчиком DBX на большинстве BSD систем. Ключ -gstabs+ дополняет отладочную информацию расширенниями понятными отладчику gdb.

Ключ -gcoff создает отладочную информацию в формате COFF, которая используется отладчиком SDB на большинстве систем System V до версии System V R4.

Ключ -gxcoff снабжает файл информацией в формате XCOFF, который используется отладчиком DBX на системах IBM RS/6000. Использование -gxcoff+ влкючает использование дополнительной информации для gdb.

Ключ -gdwarf добавляет инфомацию в формате DWARF приняотм в системе System V Release 4. Соответственно ключ -gdwarf+ прибавляет возможностей отладчику gdb.

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

gcc -g3 ...

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

Отладочная информация может значительно увеличить объем вашего файла (в три-четыре раза). Для создания программ "релизов" существует отдельная программа, позволяющая удалить отладочную информацию из запускаемого файла. Называется эта программа strip. Для того, чтобы полностью очистить файл от отладочной информации, требуется вызвать ее с ключом -s:

strip -s a.out

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

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

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

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

Это далеко не все пробемы, которые могут возникнуть при наличии программы "монстра". Поэтому при разработке программ рекомендуется их разбивать на куски, которые функционально ограничены и закончены. В этом значительно помогает сам язык C++, предоставляя свой богатый синтаксис.

Для того, чтобы вынести функцию или переменную в отдельный файл надо перед ней поставить зарезервированное слово extern. Давайте для примера создадим программу из нескольких файлов. Сначала создадим главную программу, в которой будут две внешние процедуры. Назовем этот файл main.c:

#include <stdio.h> // описываем функцию f1() как внешнююextern int f1(); // описываем функцию f2() как внешнююextern int f2(); int main(){ int n1, n2; n1 = f1(); n2 = f2(); printf("f1() = %d\n",n1); printf("f2() = %d\n",n2); return 0;}

Теперь создаем два файла, каждый из которых будет содержать полное определение внешней функции из главной программы. Файлы назовем f1.c и f2.c:

// файл f1.cint f1(){ return 2;} // файл f2.cint f2(){ return 10;}

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

gcc -c main.c f1.c f2.c

Или каждый файл в отдельности:

gcc -c f1.cgcc -c f2.cgcc -c main.c

В результате работы компилятора мы получим три отдельных объектных файла:

main.of1.of2.o

Чтобы их собрать в один файл с помощью gcc надо использовать ключ -o, при этом линкер соберет все файлы в один:

gcc main.o f1.o f2.o -o rezult

В результате вызова полученной программы rezult командой:

./rezult

На экране появится результат работы:

dron:~# ./rezultf1() = 2f2() = 10dron:~#

Теперь, если мы изменим какую-то из процедур, например f1():

int f1(){ return 25;}

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

dron:~# gcc -c f1.cdron:~# gcc main.o f1.o f2.o -o rezult2dron:~# ./rezult2f1() = 25f2() = 10dron:~#

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

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

Библиотека объектных файлов - это файл содержащий несколько объектных файлов, которые будут использоваться вместе в стадии линковки программы. Нормальная библиотека содержит символьный индекс, который состоит из названий функций и переменных и т.д., которые содержатся в библиотеке. Это позволяет ускорить процесс линковки программы, так как поиск функций и переменных в объектных файлах библиотеки происходит намного быстрее, чем поиск в наборе указанных объектных файлов. Поэтому использование библиотеки позволяет компактно хранить все требуемые объектные файлы в одном месте, и при этом значительно повысить скорость компиляции.

Объектные библиотеки по способу использования разделяются на два вида:

· Статические библиотеки

· Динамические библиотеки

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

Динамическая библиотека - это созданная специальным образом библиотека, которая присоединяется к результирующей программе в два этапа. Первый этап, это естественно этап компиляции. На этом этапе линковщик встраивает в программу описания требуемых функций и переменных, которые присутствуют в библиотеке. Сами объектные файлы из библиотеки не присоединяются к программе. Присоединение этих объектных файлов (кодов функций) осуществляет системный динамический загрузчик во время запуска программы. Загрузчик проверяет все библиотеки прилинкованные к программе на наличие требуемых объектных файлов, затем загружает их в память и присоединяет их в копии запущенной программы, находящейся в памяти.

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

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

Для начала стоит сказать, что объектный файл создаваемый нашим проверенным способом вовсе не подходит для динамических библиотек. Связано это с тем, что все объектные файлы создаваемые обычным образом не имеют представления о том в какие адреса памяти будет загружена использующая их программа. Несколько различных программ могут использовать одну библиотеку, и каждая из них располагается в различном адресном пространстве. Поэтому требуется, чтобы переходы в функциях библиотеки (операции goto на ассемблере) использовали не абсолютную адресацию, а относительную. То есть генерируемый компилятором код должен быть независимым от адресов, такая технология получила название PIC - Position Independent Code. В компиляторе gcc данная возможность включается ключом -fPIC.

Теперь компилирование наших файлов будет иметь вид:

dron:~# gcc -fPIC -c f1.cdron:~# gcc -fPIC -c f2.c

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

dron:~# gcc -shared -o libfsdyn.so f1.o f2.o

В результате получим динамическую библиотеку libfsdyn.so: Теперь, чтобы компилировать результирующий файл с использованием динамической библиотеки нам надо собрать файл командой:

dron:~# gcc -с main.сdron:~# gcc main.o -L. -lfsdyn -o rezultdyn

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

Если Вы сейчас попробуете запустить файл rezultdyn, то получите ошибку:

dron:~# ./rezultdyn./rezultdyn: error in loading shared libraries: libfsdyn.so: cannot open shared object file: No such file or directorydron:~#

Это сообщение выдает загрузчик динамических библиотек (динамический линковщик - dynamic linker), который в нашем случае не может обнаружить библиотеку libfsdyn.so. Для настройки динамического линковщика существует ряд программ.

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

dron:~# ldd rezultdyn libfsdyn.so => not found libc.so.6 => /lib/libc.so.6 (0x40016000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)dron:~#

Как видите все правильно. Программа использует три библиотеки:

· libc.so.6 - стандартную библиотеку функций языка C++.

· ld-linux.so.2 - библиотеку динамической линковки программ ELF формата.

· libfsdyn.so - нашу динамическую библиотеку функций.

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

Для того, чтобы добавить нашу директорию с библиотекой в список известных директорий надо подредактировать файл /etc/ld.so.conf. Например, пусть этот файл состоит из таких строк:

dron:~# cat /etc/ld.so.conf/usr/X11R6/lib/usr/i386-slackware-linux/lib/usr/i386-slackware-linux-gnulibc1/lib/usr/i386-slackware-linux-gnuaout/libdron:~#

Во всех этих директории хранятся всеми используемые библиотеки. В этом списке нет лишь одной директории - /lib, которая сама по себе не нуждается в описании, так как она является главной. Получается, что наша библиотека станет "заметной", если поместить ее в один их этих каталогов, либо отдельно описать в отдельном каталоге. Давайте для теста опишем, добавим строку в конец файла ld.so.conf:

/root

Для примера, этот файл находится в домашнем каталога пользователя root, у Вас он может быть в другом месте. Теперь после этого динамический линковщик будет знать где можно найти наш файл, но после изменения конфигурационного файла ld.so.conf необходимо, чтобы система перечитала настройки заново. Это делает программа ldconfig. Пробуем запустить нашу программу:

dron:~# ldconfigdron:~# ./rezultdynf1() = 25f2() = 10dron:~#

Как видите все заработало :) Если теперь Вы удалите добавленную нами строку и снова запустите ldconfig, то данные о расположении нашей библиотеки исчезнут и будет появляться таже самая ошибка.

Но описанный метод влияет на всю систему в целом и требует доступа администратора системы, т.е. root. А если Вы простой пользователь без сверх возможностей ?!

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

dron:~# echo $LD_LIBRARY_PATH

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

dron:~# LD_LIBRARY_PATH=/rootdron:~# export LD_LIBRARY_PATH

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

dron:~# LD_LIBRARY_PATH=/root:${LD_LIBRARY_PATH}dron:~# export LD_LIBRARY_PATH

Если Вы обнулите эту переменную, то снова библиотека перестанет работать:

dron:~# LD_LIBRARY_PATH=""dron:~# export LD_LIBRARY_PATHdron:~# ./rezultdyn./rezultdyn: error in loading shared libraries: libfsdyn.so: cannot open shared object file: No such file or directorydron:~#

Вы также параллельно можете зайти в систему под другим пользователем или даже тем же самым, но если Вы захотите просмотреть значение LD_LIBRARY_PATH, то увидите ее прежнее значение. Это означает, что два разных пользователя Linux не могут влиять на работу друг друга, а это и есть самое главное хорошее отличие систем Unix от большинства других систем.

Приложение2.

Наши рекомендации