Работа с GCC: компиляция проекта и создание библиотек

Введение

    Прежде, чем мы займёмся рассмотрением примеров работы с GCC, мы разберём, что же за «зверь» такой GCC.

    GCC — это «GNU Compiler Collection» — набор компиляторов и утилит для проведения успешной компиляции исходников в итоговую программу или библиотеку. Все это распространяется по лицензии GPLv3 (4.2.1 версия, последняя под лицензией GPLv2).  GCC находится под крылом компании «Free Software Foundation».

    Изначально GCC расшифровывался как «GNU C Compiler». Из названия ясно, что он поддерживал лишь работу с языком C. Появился GCC в 1985 году. Автором являлся Ричард Столлман. GCC был написан на диалекте Pascal. Несколько позже он был переписан на языке C Леонардом Тауэром и Ричардом Столлманом.

    Позднее в GCC была добавлена поддержка других языков (Ada, C++, Fortran, Java и другие). После этого GCC перестал обозначать «GNU C Compiler» и стал обозначать «GNU Compiler Collection».

    В августе 2012 GCC был переписан на C++.

GCC используется для сборки различных OS. Например: Linux, BSD, ReactOS, Mac OS X, OpenSolaris, NeXTSTEP, BeOS и Haiku.

    Основные особенности GCC это поддержка множества архитектур процессоров и реализация его под разные Операционные Системы.

Основы

Пожалуй теперь мы можем перейти от введения к практике на GCC

   GCC использует расширение файла для определения компилятора, который нужно использовать (хотя можно принудительно заставить GCC использовать конкретный компилятор).Примеры расширений:*.с — исходный код для языка C*.cpp — исходный код для C++*.o — объектный файл

    Кстати, GCC является обладателем консольного интерфейса взаимодействия с пользователем. Мы должны вызвать утилиту gcc и сообщить ей набор аргументов (ключи и файл) для работы. В зависимости от набора аргументов меняется работа gcc.

    Чуть ближе к коду. Данный материал будет построен на примерах C кода. Но для остальных языков изменения незначительны (если таковы имеются).

    Нам потребуется некий исходный код, который мы будем компилировать. Я возьму что-то простое («Hello World»), а вы можете использовать всё что угодно.

/*hw.c*/
#include «stdio.h»
int main(void) {
   printf(«Hello World\n»);
   return 0;
}

    Откроем терминал в данной директории и выполним следующую команду:

$ gcc hw.c

    После успешного исполнения данной команды, в директории с файлом «hw.c» появится новый файл «a.out». Этот файл является исполняемым. Его можно выполнить в терминале. Результатом будут предсказуемые «Hello World».

Для запуска в терминале вы должны указать полный путь к исполняемому файлу. Если терминал запущен в конкретной директории (в которой и лежит исполняемый файл), то для исполнения файла нам понадобится ввести в терминал:$ ./a.outЕсли же мы введем:$ a.outто ничего не произойдёт, ибо поиск исполняемых файлов будет произведён по директориям, указанным в системной переменной «PATH».

Поглядеть их можно:$ echo $PATH
   GCC по умолчанию присваивает «a.out» исполняемым файлам. Это своеобразная отсылка к прошлому, когда это означало «assembler output».

    Если вам не нравится «a.out» как результирующее имя исполняемого файла, вы можете использовать ключ «-o ИМЯ_ФАЙЛА» для изменения результирующего имени.

Пример:

$ gcc -o HW hw.c

    После этой команды в директории появится файл «HW», который и будет исполняемым.

   Давайте вспомним ещё одну полезную при отладке вещь. В конце файла «hw.c» есть строка «return 0;», которая означает, что программа была завершена правильно.    В более сложных программах могут возникать разные ошибки, где мы можем сигнализировать о них через «return». После исполнения этот код присваивается системной переменной «?».Поглядеть её содержимое можно через:$ echo $?

Сборка

    gcc — это программа которая автоматизирует процесс сборки. Сам же процесс сборки состоит из нескольких частей:

  • Препроцессор
  • Компиляция
  • Ассемблирование
  • Линковка

    Ключи gcc позволяют остановить процесс сборки на любом из этих шагов.

Сборка: Препроцессор

    Существует ключ «-E», который позволяет прервать процесс сборки сразу после выполнения обработки препроцессором:

$ gcc -E -o hw.i hw.c

    Откройте файл любым текстовым редактором. Вы увидите, каким большим становится ваш исходник после этого этапа. Я не буду прикладывать содержимое файла «hw.i», но я приведу скучные цифры: «hw.c» весит 86 байтов против 17.1 Кбайта у «hw.i».

    Это происходит потому, что мы подключали заголовочные файлы. Заголовочные файлы содержат в себе много всего (то, что нужно и даже то, что нам не нужно), плюс одни заголовочные файлы подключают множество других.     Запомните, файлы с расширением «*.i» воспринимаются GCC как файлы с исходным кодом на C/C++, которые не требуется обрабатывать препроцессором.

Сборка: Компиляция

    Для остановки сборки после этапа компиляции есть ключ «-S»:

$ gcc -S -o hw.s hw.i

    Я продолжил компиляцию с прерванного места (после обработки препроцессором).

    Как вы могли догадаться, файл «hw.s» — итоговый файл после компиляции. В файле находится код на ассемблере, который реализует вашу программу. Просмотреть его вы можете из любого текстового редактора.

Сборка: Ассемблирование

    Для остановки сборки после этапа ассемблирования есть ключ «-c»:

$ gcc -c -o hw.o hw.s

    После этого этапа у нас на выходе получится объектный файл. По сути машинный код. Но это всё ещё не исполняемый файл, ибо не все компоненты программы собранны в единое целое.

Сборка: Линковка

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

$ gcc -o hw hw.o

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

GCC ключи

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

-l ПутьУказывает путь к дополнительным заголовочным файлам. При наличии нескольких путей, загружается по приоритету: слева направо
-L ПутьУказывает дополнительную директорию для поиска библиотек.
WallВывод предупреждений (потенциальных ошибок в коде), которые выскакивают при компиляции программы НО не мешают ей.
-WerrorВсе предупреждения превращаются в ошибки.
-gВставка в объектный (исполняемый) файл информации, необходимой для работы отладчика gdb.
-O0 / -O1 / -O2 / -O3Уровни оптимизации кода (эффективность кода).
-OsОптимизация по размеру итогового файла.
-march=АрхитектураВыбор архитектуры для компиляции.
-xВыбрать конкретный язык (списка не будет).
fPICТребует от gcc создать PIC — Position Independent Code. Это позволяет подключать исполняемые модули к коду основной программы в момент её загрузки.
-sharedМетка для создания динамической библиотеки.
-Wl,Передать линковщику опцию. Это нужно делать через запятую.

Компиляция составной программы

    Вот мы ознакомились с основной частью по работе с GCC. Итак, теперь как нам быть, если программа состоит из нескольких исходных файлов?

    Для начала давайте разберёмся, какие из этого плюсы:

  • Удобство работы с набором маленьких файлов, в которых реализованы разные части программы.
  • Ускорение сборки проекта. Зачем нам собирать весь проект, если мы изменили лишь один файл?
  • Распределение разработки между несколькими программистами.
  • Реализация библиотек. Зачем нам изобретать велосипед?

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

/*main.c*/

#include «stdio.h»
#include «start.h»
#include «end.h»

int main(void) {
   start();
   printf(«I work\n»);
   end();
   return 0;
}
/*start.h*/

void start(void);
/*start.c*/

#include «stdio.h»
#include «start.h»

void start(void) {
   printf(«I start\n»);
}
/*end.h*/

void end(void);
/*end.c*/

#include «stdio.h»
#include «end.h»

void end(void) {
   printf(«I end\n»);
}

    Вот у нас 5 файлов. Собрать их можно очень просто. Для этого нам понадобится привычный gcc:

$ gcc main.c start.c end.c

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

$ gcc -c main.c$ gcc -c start.c$ gcc -c end.c$ gcc -o main main.o start.o end.o

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

Библиотеки

    Библиотека содержит в себе объектный код, который может быть вызван в использующей библиотеку программе. Это соединение происходит на этапе линковки.

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

    Существуют два типа библиотек:

  • Статичная, которая полностью включается в состав исполняемого файла на этапе линковки.
  • Динамическая, которая находится в директориях операционной системы и подключается к программе средствами операционной системы.

    Перейдём к практике. Сейчас мы создадим два типа библиотек.

    Статичная библиотека будет первой, которую мы реализуем. Статичная библиотека должна иметь расширение «*.a» и желательно обладать префиксом «lib» (это необязательно и относится как к статичным, так и к динамическим библиотекам).

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

/*main.c*/

#include «stdio.h»
#include «StartEnd.h»

int main(void) {
   start();
   printf(«I work\n»);
   end();
   return 0;
}
/*StartEnd.h*/

void start(void);
void end(void);
/*start.c*/

#include «stdio.h»
#include «StartEnd.h»

void start(void) {
   printf(«I start\n»);
}
/*end.c*/

#include «stdio.h»
#include «StartEnd.h»

void end(void) {
   printf(«I end\n»);
}

    Займёмся теперь непосредственно библиотекой. Для начала мы должны создать объектные файлы для «start.c» и «end.c»:

$ gcc -c start.c$ gcc -c end.c

    Теперь мы скомпонуем объектные файлы в библиотеку. Для этого мы воспользуемся утилитой «ar» (это простой архиватор):

$ ar crs libStartEnd.a start.o end.o

    После успешного создания библиотеки, давайте мы её используем:

$ gcc -o main main.c -L. -lStartEnd

    Кстати. Если вы хотите, то можете добавить пути где линковщик будет искать файлы библиотек. Эти пути указаны в переменной окружения (LD_LIBRARY_PATH или LIBRARY_PATH) где пути разделены знаком «:»

    Теперь создадим динамическую библиотеку. Для этого введём команду в консоль:

$ gcc -fPIC -c start.c$ gcc -fPIC -c end.c$ gcc -shared -o libStartEnd.so.2.0.0.1 -Wl,-soname,libStartEnd.so.2 start.o end.o

    В результате мы получим файл «libStartEnd.so.2.2.0.4» который и будет динамической библиотекой.

    Давайте остановимся на моменте с « -Wl,-soname,libStartEnd.so.2» из команды. Остальные части команды есть выше в описании некоторых команд.

    Soname библиотеки указывает совместимость версий. Этот идентификатор помещается в начале нашей библиотеки  и служит для указания — совместима ли программа с библиотекой или нет. Если ваша библиотека после изменений перестала быть совместимой с ранее выпущенными программами (вы удалили некоторые функции или изменили их сигнатуры и прочее) то вы должны создать новый soname идентификатор.

    Наша библиотека готова, осталось лишь научиться ей пользоваться.

    Как вы понимаете, просто скомпилировать наш код не получится. Мы должны:

  • Создать символическую ссылку на «libStartEnd.so» (для успешной компиляции)
  • Создать ссылку на библиотеку «libStartEnd.so.2» (для запуска)
  • Скомпилировать нашу программу

Приступим:

$ ln -s libStartEnd.so.2.0.0.1 libStartEnd.so
$ ln -s libStartEnd.so.2.0.0.1 libStartEnd.so.2
$ gcc -o main main.c -L. -lStartEnd -Wl,-rpath,.

    «-Wl,-rpath,.» сообщает линковщику дополнительные пути к библиотекам.

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

Работа с GCC: компиляция проекта и создание библиотек

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *