Использование GCC

Введение

Один из самых распространённых компиляторов с языков C и C++ на многих современных Unix-подобных системах, таких как GNU/Linux, члены семейства систем BSD и Mac OS X, это GCC. Первоначально эта аббревиатура расшифровывалась как GNU C Compiler (компилятор языка C от организации GNU), в свою очередь GNU является «рекурсивным акронимом», то есть расшифровывается через самое себя: GNU=GNU is Not UNIX, имея ввиду свою открытость в отличие от оригинальных систем UNIX (таких как UNIX System V от компании AT&T и BSD UNIX, разрабатывавшемся в Университете Беркли, Калифорния). После его появления в 1987 году к GCC был дописан ряд фронт-ендов (front-ends, трансляторов исходных кодов во внутреннее представление GCC, рассчитанное на дальнейшую трансляцию в исполняемый двоичный код бек-ендом, back-end) для других языков программирования:

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

Простейший способ задействовать средства GCC — выполнить в командной строке команду:

$ gcc имя_файла
($ здесь и далее означает приглашение командной строки, а курсивом выделены строки, которые должны быть чем-то замещены, причем сама строка указывает на то, чем). Файл с указанным именем должен содержать исходный код на одном из языков, поддерживаемых GCC. Расширение файла может подсказать, какой именно язык используется. В частности, для C++ можно использовать такие расширения:
‘.C’, ‘.cc’, ‘.cpp’, ‘.CPP’, ‘.c++’, ‘.cp’ или ‘.cxx’;
для заголовочных файлов распознаются расширения:
‘.hh’, ‘.hpp’, ‘.H’, или (иногда для разделяемого шаблонного кода) ‘.tcc’
Тип обрабатываемого файла можно указать и явно:
$ gcc имя_файла -x тип
Тип может быть одним из следующих (с группировкой по языкам): (Заголовочные файлы C++, как известно, не компилируются в исполняемый код, но GCC поддерживает возможность предкомпиляции заголовочных файлов, которая может повысить скорость сборки всей программы на C++ в целом, однако мы не рассматриваем здесь эту возможность.) В примерах мы будем в основном использовать язык C++ (делая иногда оговорки для случая C), файлы с расширениями ‘.cpp’ и ‘.hpp’, а также GCC версии 4.3 (посмотреть версию можно с помощью команды:
$ gcc -v
). Итак, создадим в отдельном каталоге файл с простой программой на C++ и попробуем скомпилировать её из командной строки:
// first.cpp
#include <iostream>

int main() {
    std::cout << "Здравствуй, Мир!" << std::endl;
}
$ gcc ./first.cpp
/tmp/cc6dArpT.o: In function `main':
first.cpp:(.text+0x1c): undefined reference to `std::cout'
first.cpp:(.text+0x21): undefined reference to `std::basic_ostream >& std::operator<<  >(std::basic_ostream >&, char const*)'
first.cpp:(.text+0x29): undefined reference to `std::basic_ostream >& std::endl >(std::basic_ostream >&)'
first.cpp:(.text+0x31): undefined reference to `std::basic_ostream >::operator<<(std::basic_ostream >& (*)(std::basic_ostream >&))'
/tmp/cc6dArpT.o: In function `__static_initialization_and_destruction_0(int, int)':
first.cpp:(.text+0x60): undefined reference to `std::ios_base::Init::Init()'
first.cpp:(.text+0x65): undefined reference to `std::ios_base::Init::~Init()'
/tmp/cc6dArpT.o:(.eh_frame+0x11): undefined reference to `__gxx_personality_v0'
collect2: выполнение ld завершилось с кодом возврата 1
Не сложно догадаться, что наша первая попытка не увенчалась успехом (это можно проверить выполнив:
$ ls
first.cpp
— никаких новых файлов в каталоге для экспериментов не появилось, мы же хотели бы увидеть исполняемый файл). Чтобы понять, в чём проблема, нужно вспомнить, что создание исполняемого файла проходит в два этапа: (1) компиляция исходного текста в объектный файл, из которого затем (2) строится исполняемый файл путем линковки (‘разрешения связей’) всех необходимых средств, используемых, но не определённых в данном объектном файле. В нашем файле используются средства стандартной библиотеки (например, std::cout), которые определены в реализации GCC стандартной библиотеки C++: libstdc++.a. Хотя команда gcc самостоятельно определяет используемый язык программирования, она не считает нужным в данном случае по умолчанию линковать наш файл со стандартной библиотекой C++. Можно явно попросить её сделать это:
$ gcc ./first.cpp -lstdc++
$ ls
a.out  first.cpp
Как видим, в каталоге появился новый файл. Попробовав его запустить, получим:
$ ./a.out 
Здравствуй, Мир!
Обратите внимание на ключ компиляции -l, сопровождаемый именем библиотеки без префикса ‘lib’ и расширения ‘.a’: мы ещё вернёмся к этому механизму и рассмотрим его более подробно. Сейчас же стоит отметить, что такое поведение gcc по умолчанию стоит признать не слишком логичным, и указать на другую возможность компиляции программ на C++:
$ rm ./a.out 
$ g++ ./first.cpp 
$ ls
a.out  first.cpp
Команда g++ явно указывает на то, что мы используем язык C++ и по умолчанию линкует стандартную библиотеку этого языка.

Если имя ‘a.out’ кажется вам не очень выразительным, то вы наверняка захотите указать другое, делается это так:

$ g++ ./first.cpp -o helloworld
$ ls
first.cpp  helloworld

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

Языковые стандарты

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

$ gcc ./first.cpp -lstdc++ -std=c++98
скомпилирует нашу программу в режиме соответствия стандарту C++ 1998 года. Поскольку нас интересуют два языка, C и C++, посмотрим, какие из стандартов поддерживаются для них.

Язык C

GCC поддерживает три версии стандарта C, хотя последняя версия поддерживается лишь частично.

Первый стандарт C появился в 1989 как документ американской стандартизирующей организации ANSI и стал международным стандартом в следующем году, после ратицификации его международной организацией по стандартам ISO. Потребовать, чтобы GCC руководствовался именно им, можно, добавив один из трёх ключей компиляции: -ansi, -std=c89 или -std=iso9899:1990.

В 1995 в язык была внесена первая «поправка» (amendment), которая добавила диграфы (комбинации символов, замещавших одиночные символы, которых могло не быть на некоторых клавиатурах), макрос __STDC_VERSION__ для того, чтобы в исходном коде можно было проверить версию стандарта, которую поддерживает компилятор, обрабатывающий этот код, и внёсшая изменения в стандартную библиотеку. Поддержка этой редакции включается ключом -std=iso9899:199409.

Наконец, новая версия стандарта C была опубликована в 1999 и внесла довольно серьезные изменения в язык. Её GCC поддерживает пока не полностью, но эта поддержка включается ключом -std=iso9899:199409.

По умолчанию GCC использует некоторые расширения языка C, которые довольно редко конфликтуют со стандартом. Использование ключей -std со значениями, перечисленными выше, заблокирует эти расширения. Потребовать явного использования расширений можно, указав ключи -std=gnu89 или -std=gnu99 для стандартов 1989-го и 1999-го годов, соответственно. По умолчанию компиляция программ C происходит в режиме -std=gnu89, однажды он изменится на -std=gnu99, когда поддержка стандарта 1999-го года станет полной. Кстати, некоторые расширения GCC для стандарта 1989-го года стали частью стандарта 1999.

Язык C++

GCC поддерживает стандарт ISO C++ (1998 год) и предоставляет экспериментальную поддержку грядущего нового стандарта ISO, который обычно обозначают как C++0x (имея ввиду предполагаемые сроки выхода: 200x). В стандарт 1998-го года вносилась поправка в 2003-м году. GCC реализует большую часть стандарта 1998 (ключевое слово export для раздельной трансляции шаблонов C++ является важным исключением) и почти целиком поправки 2003: -std=c++98. Некоторые уже окончательно утверждённые средства C++0x можно использовать при использовании ключа -std=c++0x.

Как и в случае с языком C, по умолчанию GCC использует некоторые расширения языка C++, которые блокируются при указании одной из опций, описанных выше. Явное использование расширений можно затребовать, указав ключ -std=gnu++98 или -std=gnu++0x, первый используется по умолчанию при компиляции программ на C++.

Предупреждения компилятора

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

Примерами таких потенциально опасных мест могут служить:

Предупреждения компилятора это очень полезный механизм, позволивший программистам избежать многих незаметных на первый взгляд ошибок, которые могли проявиться только после запуска уже скомпилированной программы (возможно, много лет спустя после первого запуска). Его, безусловно, нужно использовать, и относиться к предупреждениям следует максимально внимательно. Многие авторитетные книги по C++, написанные в жанре сборников советов (Скотта Мейерса, Герба Саттера, Андрея Александреску, Стивена Дьюхерста), содержат совет компилировать программу на максимально строгом уровне предупреждений и добиваться того, чтобы компилятору не к чему было придраться.

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

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

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

Отладочная информация

Сложно представить себе программу, которая выполняет что-то осмысленное и которая была написана с первого раза без единой ошибки. Хуже того, иногда может оказаться, что давно и успешно работающая программа также содержит изъяны. Установить источник проблем помогает отладочная информация, которую компилятор может добавить в каждый объектный файл. Она включает в себя указание на типы данных, использованные в программе, и соответствие объектного кода строкам текста исходного файла. Отладочная информация, сгенерированная GCC, предполагается для использования в первую очередь отладчиком GDB (GNU debugger, отладчик GNU), но не только им.

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

Каждый из перечисленных форматов может быть использован, только если он поддерживается текущей платформой. Указать формат можно с помощью ключа -gимя_формата_строчными_буквами. Для форматов stabs и COFF можно добавить к ключу символ ‘+’ справа без пробела, это включит дополнительную информацию, специфичную для GDB и может помешать работе других отладчиков с этим файлом.

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

К каждому из упомянутых ключей можно дописать справа цифру от 0 до 3, она будет обозначать уровень отладочной информации, иначе говоря — её объем:

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

Во-вторых, отладочная информация все же занимает дополнительное пространство в памяти (темпы удешевления которой, впрочем, настолько высоки, что вряд ли можно оправдать пренебрежительное отношение к генерации отладочной информации), которое вы можете захотеть сэкономить. Для этого существует ключ -s (от strip — удалить). Размер файлов с отладочной информацией и без неё может отличаться довольно сильно, особенно на маленьких программах. Сравним размеры исполняемых файлов нашей первой программы в трёх случаях:

  1. C ключом -s.
  2. Без каких-либо ключей, касающихся отладки.
  3. С ключом -ggddb3, который должен поместить довольно много отладочной информации в исполняемый файл.
$ g++ -s ./first.cpp -o hw-1-s
$ g++ ./first.cpp -o hw-2
$ g++ -ggdb3 ./first.cpp -o hw-3-gddb3
$ ls -l | grep 'hw*' | awk '{printf "%-10s: %5s B\n", $8, $5}'
hw-1-s    :  5736 B
hw-2      :  9747 B
hw-3-gddb3: 89827 B

Оптимизация

Если с предупреждениями компилятора всё ясно (действует принцип «чем больше, тем лучше»), количество отладочной информации также, в основном, контролируется программистом в соответствии с его целями (доведение программы до работоспособного состояния или изготовление окончательной версии программы для передачи заказчику), то с оптимизацией дело обстоит намного сложней. Оптимизация это процесс улучшения производительности программы, в том числе, увеличение скорости её работы и снижение объёма занимаемой памяти (эти две цели, между прочим, иногда противоречат друг другу). GCC поддерживает большой набор ключей оптимизации и часть из них объединены в группы (как и в случае с предупреждениями), что позволяет активировать их также по групповому принципу.

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

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

Если компилятор не получает каких-либо флагов оптимизации, его основная цель — снизить время компиляции (которое при сборках больших программ на C или C++ может достигать внушительных показателей и, тем самым, сильно тормозить процесс разработки) и обеспечить предсказуемость и корректность процедуры отладки (если она будет производиться в будущем). Групповые флаги оптимизации имеют вид: -Oуровень, где уровень может изменяться от 0 до 3 (каждый следующий включает в себя все оптимизации предыдущего).

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

Раздельная трансляция

Реальные программы сильно превышают размеры нашего первого примера ‘first.cpp’ по количеству строк кода. Когда оно увеличивается достаточно сильно, появляются проблемы:

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

Начнём с простого случая, когда программа представляет из себя набор функций, и мы хотим вынести их часть в отдельный ‘.cpp’-файл. Возьмём уже использовавшийся пример с программой ‘first.cpp’ и добавим в неё вызов функции, определение которой помещается в файле ‘second.cpp‘:

// first.cpp
#include <iostream>

using std::endl;

int f();

int main() {
    std::cout << "Здравствуй, Мир!" << endl
        << f() << endl;
}
// second.cpp
int f() {return 42;}
Обратите внимание на объявление int f(); в ‘first.cpp’: правила языка C++ требуют объявить каждую функцию перед первым использованием. Обычно вместо того, чтобы вручную писать объявления всех функций, определённых в другой единице трансляции (‘.cpp’-файле), используют заголовочные файлы, содержащие все необходимые объявления, текст которых целиком включается в нужный ‘.cpp’-файл директивой препроцессора C #include. Обычно для каждого ‘.cpp’-файла, который не содержит точки входа в программу (функции main), а значит, содержимое которого будет использовано другими частями программы, создаётся заголовочный файл с таким же основным именем, но другим расширением. В нашем примере можно добавить заголовочный файл ‘second.hpp’ и получить эквивалентный приведённому выше, но в перспективе более удобный в использовании код:
// first.cpp
#include <iostream>
#include "second.hpp"

using std::endl;

int main() {
    std::cout << "Здравствуй, Мир!" << endl
        << f() << endl;
}
// second.hpp
#ifndef SECOND_HPP
#define SECOND_HPP
int f();
#endif
// second.cpp
int f() {return 42;}
Директивы препроцессора C (строки, начинающиеся #) в заголовочном файле — «стражи включения», — как обычно, добавляются для того, чтобы данный файл не был по случайности включён в один ‘.cpp’-файл несколько раз (символ SECOND_HPP должен быть своим у каждого заголовочного файла, потому разумно связывать его с именем этого файла).

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

$ g++ ./first.cpp ./second.cpp -o hw42
$ ./hw42
Здравствуй, Мир!
42
Стоит отметить удобную возможность использования шаблонов имён (wildcards), чтобы избежать полного перечисления файлов:
$ g++ ./*.cpp -o hw42
даст аналогичный приведённому выше результат.

Итак, мы сумели разнести код в несколько файлов, но пока еще не добились того, чтобы изменения в одном файле не приводили к перекомпиляции всех. Здесь снова придется вспомнить о том, что используемые до этого момента вызовы gcc (g++) приводят не только к компиляции, но и к линковке полученных после компиляции объектных фалов в один исполняемый файл. Нам потребуется явно разбить создание исполняемого файла на две стадии, используя ключ gcc , который просит оставить получившиеся объектные файлы на диске и не линковать их:

$ g++ ./*.cpp -c
$ ls | grep '\.o$'
first.o
second.o
Как видно, на диске появились два объектных файла (расширение ‘.o’), которые, очевидно, соответствуют исходным ‘.cpp’. Слинковать и получить исполняемый файл можно обычным образом:
$ g++ ./*.o -o hw-from-o
$ ./hw-from-o 
Здравствуй, Мир!
42
(GCC определяет тип файлов и не пытается компилировать их как исходные тексты, а сразу линкует). Теперь, внеся изменение в файл ‘second.cpp’, можно перекомпилировать только его и заново слинковать все объектные файлы программы:
$ g++ ./second.cpp -c
$ g++ ./*.o -o hw42-v.2
Не поленитесь исправить в тексте ‘second.cpp’ 42 на ваше любимое число и убедиться, что эта схема работает.

Библиотеки

Раздельная трансляция файлов делает доступным систематическое повторное использование кода: набор объектных файлов, содержащих функции, которые могут быть использованы в разных программах, собирается в библиотеку (программного кода), представляющую собой единый файл, который содержит весь код объектных с некоторой дополнительной информацией (наподобие индекса в настоящих библиотеках). Последнее может ускорять процесс линковки при использовании библиотеки по сравнению с обычной сборкой программы из большого числа объектных файлов. Информация об исходных объектных файлах также сохраняется.

Библиотеки могут существовать в двух различных вариантах:

Бесспорное преимущество динамических библиотек состоит в том, что если несколько программ используют одну библиотеку, то она загружается в память только один раз. Иными словами, сразу несколько программ могут (и будут) использовать один загруженный экземпляр библиотеки «одновременно». В то же время использование статической библиотеки заставит добавлять части её кода в каждый исполняемый файл по отдельности. Обновление динамической библиотеки потребует перезапуска, использующих её программ, статической — их перелинковки (что обычно занимает немало времени).

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

Рассмотрим сначала более простой случай статической библиотеки. Для создания библиотеки такого типа из набора объектных файлов используется утилита ar. Добавим к двум имеющимся ‘.cpp’-файлам третий, ‘third.cpp’ (с произвольным содержимым), скомпилируем его в объектный файл и сделаем из двух объектных (‘second.o’, ‘third.o’) статическую библиотеку:

$ g++ -c ./third.cpp 
$ ar crs libfirst.a ./second.o ./third.o
$ ls | grep '\.a'
libfirst.a
Опции ar означают следующее: Важным является формат имени файла библиотеки:

Для указания GCC на библиотеки, которые нужно использовать при сборке, понадобятся ещё два ключа:

Компиляция нашей программы с использованием созданной библиотеки может выглядеть так:
$ g++ ./first.cpp -L. -lfirst
С помощью -L. текущий каталог добавляется в список каталогов для поиска библиотек (чтобы добавить несколько каталогов, нужно использовать ключ -L несколько раз). Обратите внимание, что именем библиотеки считается подстрока имени ‘.a’-файла между ‘lib’ слева и ‘.a’ справа. Здесь, в отличие от рассмотренных ранее случаев, порядок аргументов g++ важен, поскольку -L и -l это опции линковщика, который вступает в работу после компилятора, обрабатывающего первый аргумент (имя файла).

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

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

$ g++ -fPIC -c ./second.cpp
(мы не упоминаем файл ‘third.cpp’, чтобы не загромождать год: создадим библиотеку из одного файла). Ключ -fPIC позволяет создавать независимый от размещения в оперативной памяти код (position independent code), что позволяет использовать один библиотечный код из разных программ (находящихся, очевидно, в разных местах оперативной памяти) во время их выполнения. Будем для краткости называть результирующий файл этой команды так: объектный pic-файл. Можно вместо ключа -fPIC использовать -fpic аналогичного назначения, который может создать файл меньшего размера, но работает не на всех платформах. Если на вашей платформе он не поддерживается, GCC сообщит вам об этом.

Файлы динамических библиотек должны, как и раньше, иметь префикс ‘lib’ и расширение ‘.so’ (shared object). Сложность состоит в том, что для одной библиотеки надо иметь в виду четыре имени, причем три из них соответствуют объектам файловой системы:

  1. «Реальное имя» (real name): имя файла на диске, содержащего код библиотеки. Должно иметь форму:
    libимя.so.n.m.k
    гдеНомер релиза с предшествующей точкой можно опускать, что мы и будем делать в дальнейшем.
  2. «so-имя» (soname) получается из реального имени удалением минорной версии и релиза (если он указан):
    libимя.so.n
    В одном каталоге с файлом библиотеки (для которого используется «реальное имя») должна существовать мягкая ссылка (soft link) на него, имеющая so-имя этой библиотеки. Эти мягкие ссылки создаются для всех библиотек в указанном каталоге с помощью запуска утилиты ldconfig либо вручную для конкретной библиотеки с помощью утилиты ln:
  3. Имя, используемое линковщиком при создании исполняемого файла. Не содержит информации о версиях:
    libимя.so
    Обычно в каталоге с файлом библиотеки создаётся мягкая ссылка с этим именем на so-имя (ссылка с so-именем должна уже существовать) либо на реальное имя (первый вариант часто удобней):
    $ ln -s /путь/к/каталогу/libимя.so.n /путь/к/каталогу/libимя.so
  4. Имя библиотеки для передачи при вызове компилятора (линковщика) с ключом -l. Аналогично статическим библиотекам, это подстрока между ‘lib’ и ‘.so’.

Рассмотрим команду получения файла библиотеки (который должен носить «реальное имя», п. 1 списка выше) из объектного pic-файла:

$ g++ -shared -Wl,-soname,libfirst.so.1 -o libfirst.so.1.0 ./second.o
Ключ -shared имеет вполне понятное значение: мы сигнализируем GCC о том, что хотим получить динамическую библиотеку («разделяемый объект»). После ключа -o идёт имя выходного файла («реальное имя» файла нашей библиотеки), а затем список файлов, которые должны в эту библиотеку войти: в нашем случае это всего один объектный файл ‘second.cpp’.

Теперь попробуем разобраться с тем, что написано между ключами -shared и -o. Ключ -Wl говорит о том, что далее пойдут опции, которые предназначаются непосредственно линковщику. В соответствии с синтаксическими правилами, в списке этих опций не должно присутствовать пробелов: мы передаём GCC что-то, на что он должен «закрыть глаза» (опции предназначены не ему, а линковщику) и в этом случае мы договариваемся о том, что он откроет глаза только когда встретит первый пробел. В качестве символа-разделителя в списке этих опций вместо пробела выступает запятая. Теперь должно быть ясно, что между ключами -shared и -o стоит опция, передаваемая линковщику, которая указывает на «so-имя» (п. 2 списка выше) создаваемой библиотеки.

Такое усложнение возникает из основного принципа работы GCC (как и большинства Unix-программ): решаемая задача неявно разбивается на подзадачи, которые выполняют отдельные утилиты. Это создаёт некоторый уровень абстракции: для большинства операций мы просто вызываем команду g++ и не задумываемся, какие именно программы трудятся над обработкой наших файлов, а зачастую их (программ) бывает немало. В случае с -Wl мы натолкнулись на явление, которое Джоэль Спольски назвал «протекающими абстракциями» (его статью «Закон дырявых абстракций» на эту тему можно и сейчас найти в интернете). Более прозрачное решение текущей задачи (получение библиотечного файла из объектного(ых) pic-файла(ов)) состоит в прямом вызове линковщика, минуя g++:

$ ld -shared -soname libfirst.so.1 -o libfirst.so.1.0 ./second.o
Результат в данном случае будет аналогичным.

Обсудим теперь процесс загрузки библиотек в память. Он, как было сказано, происходит при запуске первой программы, которая использует данную динамическую библиотеку. Загрузка необходимых библиотек при запуске программы осуществляется специальной программой, являющейся частью операционной системы — динамическим линковщиком. Он ищет файлы библиотек в некоторых заранее определённых каталогах, список которых на многих Unix-подобных операционных системах хранится в файле /etc/ld.so.conf. Почти наверняка в этом списке имеются каталоги /lib и /usr/lib. Возможно, сюда входит и /usr/local/lib. Как это часто бывает, разные авторитетные организации и специалисты советуют использовать для динамических библиотек разные папки. Мы остановим свой выбор на /usr/lib. Создание необходимой инфраструктуры библиотечных файлов: одного реального, ссылки с so-именем и ссылки с именем для линковщика — в каком-то из системных каталогов для хранения библиотек часто называют установкой библиотеки.

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

$ g++ -fPIC -c ./second.cpp
$ ld -shared -soname libfirst.so.1 -o libfirst.so.1.0 ./second.o
$ sudo cp ./libfirst.so.1.0 /usr/lib/libfirst.so.1.0
$ sudo ldconfig -n /usr/lib/
$ sudo ln -s /usr/lib/libfirst.so.1 /usr/lib/libfirst.so
$ g++ ./first.cpp -lfirst -o hw-dinamic
$ ./hw-dinamic 
Здравствуй, Мир!
42
Приятной неожиданностью можно считать то, что сборка основной программы (из файла ‘first.cpp’) осталась практически без изменений по сравнению со случаем статической библиотеки (за исключением того, что исчезла необходимость в указании пути для поиска файла библиотеки с ключом -L: это и понятно, все библиотечный файлы находятся теперь в системных каталогах). Выполняя эту операцию, мы использовали четвёртое имя библиотеки (п. 4 списка выше).

Обращает на себя внимание необходимость наличия администраторских прав (вызов sudo) для установки библиотеки. Во время тестовых запусков программы можно обойтись без копирования в системный каталог /usr/lib/, если использовать переменную окружения LD_LIBRARY_PATH, которая содержит пути для поиска библиотек. В этом случае перед запуском программы её нужно модифицировать, а также снова (как в случае со статической библиотекой) указать ключ -L:

$ g++ -fPIC -c ./second.cpp
$ ld -shared -soname libfirst.so.1 -o libfirst.so.1.0 ./second.o
$ ldconfig -n .
$ ln -s ./libfirst.so.1 ./libfirst.so
$ g++ ./first.cpp -L. -lfirst -o hw-dinamic
$ LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./hw-dinamic 
Здравствуй, Мир!
42