О GCC, компиляции и библиотеках часть 2
Библиотеки
Библиотека - в языке C, файл содержащий объектный код, который может быть присоединен к использующей библиотеку программе на этапе линковки. Фактически библиотека это набор особым образом скомпонованных объектных файлов.
Назначение библиотек - предоставить программисту стандартный механизм повторного использования кода, причем механизм простой и надёжный.
С точки зрения операционной системы и прикладного программного обеспечения библиотеки бывают статическими и разделяемыми (динамическими).
Код статических библиотек включается в состав исполняемого файла в ходе линковки последнего. Библиотека оказывается "зашитой" в файл, код библиотеки "сливается" с остальным кодом файла. Программа использующая статические библиотеки становиться автономной и может быть запущена практически на любом компьютере с подходящей архитектурой и операционной системой.
Код разделяемой библиотеки загружается и подключается к коду программы операционной системой, по запросу программы в ходе её исполнения. В состав исполняемого файла программы код динамической библиотеки не входит, в исполняемый файл включается только ссылка на библиотеку. В результате, программа использующая разделяемые библиотеки перестает быть автономной и может быть успешно запущена только в системе где задействованные библиотеки установлены.
Парадигма разделяемых библиотек предоставляет три существенных преимущества:
1. Многократно сокращается размер исполняемого файла. В системе, включающей множество бинарных файлов, использующих один и тот же код, отпадает необходимость хранить копию этого кода для каждого исполняемого файла.
2. Код разделяемой библиотеки используемый несколькими приложениями храниться в оперативной памяти в одном экземпляре (на самом деле не всё так просто...), в результате сокращается потребность системы в доступной оперативной памяти.
3. Отпадает необходимость пересобирать каждый исполняемый файл в случае внесения изменений в код общей для них библиотеки. Изменения и исправления кода динамической библиотеки автоматически отразятся на каждой из использующих её программ.
Без парадигмы разделяемых библиотек не существовало бы прекомпиллированных (бинарных) дистрибутивов Linux (да ни каких бы не существовало). Представьте размеры дистрибутива, в каждый бинарный файл которого, был бы помещен код стандартной библиотеки C (и всех других подключаемых библиотек). Так же представьте что пришлось бы делать для того, что бы обновить систему, после устранения критической уязвимости в одной из широко задействованных библиотек...
Теперь немного практики.
Для иллюстрации воспользуемся набором исходных файлов из предыдущего примера. В нашу самодельную библиотеку поместим код (реализацию) функций first() и second().
В Linux принята следующая схема именования файлов библиотек (хотя соблюдается она не всегда) - имя файла библиотеки начинается с префикса lib, за ним следует собственно имя библиотеки, в конце расширение .a (archive) - для статической библиотеки, .so (shared object) - для разделяемой (динамической), после расширения через точку перечисляются цифры номера версии (только для динамической библиотеки). Имя, соответствующего библиотеке заголовочного файла (опять же как правило), состоит из имени библиотеки (без префикса и версии) и расширения .h. Например: libogg.a, libogg.so.0.7.0, ogg.h.
В начале создадим и используем статическую библиотеку.
Функции first() и second() составят содержимое нашей библиотеки libhello. Имя файла библиотеки, соответственно, будет libhello.a. Библиотеке сопоставим заголовочный файл hello.h.
/* hello.h */ void first(void); void second(void); Разумеется, строки: #include "first.h" #include "second.h" в файлах main.c, first.c и second.c необходимо заменить на: #include "hello.h" Ну а теперь, введем следующую последовательность команд: $ gcc -Wall -c first.c $ gcc -Wall -c second.c $ ar crs libhello.a first.o second.o $ file libhello.a libhello.a: current ar archive
Как уже было сказано - библиотека это набор объектных файлов. Первыми двумя командами мы и создали эти объектные файлы.
Далее необходимо объектные файлы скомпоновать в набор. Для этого используется архиватор ar - утилита "склеивает" несколько файлов в один, в полученный архив включает информацию требуемую для восстановления (извлечения) каждого индивидуального файла (включая его атрибуты принадлежности, доступа, времени). Какого-либо "сжатия" содержимого архива или иного преобразования хранимых данных при этом не производится.
Опция c arname - создать архив, если архив с именем arname не существует он будет создан, в противном случае файлы будут добавлены к имеющемуся архиву.
Опция r - задает режим обновления архива, если в архиве файл с указанным именем уже существует, он будет удален, а новый файл дописан в конец архива.
Опция s - добавляет (обновляет) индекс архива. В данном случае индекс архива это таблица, в которой для каждого определенного в архивируемых файлах символического имени (имени функции или блока данных) сопоставлено соответствующее ему имя объектного файла. Индекс архива необходим для ускорения работы с библиотекой - для того чтобы найти нужное определение, отпадает необходимость просматривать таблицы символов всех файлов архива, можно сразу перейти к файлу, содержащему искомое имя. Просмотреть индекс архива можно с помощью уже знакомой утилиты nm воспользовавшись её опцией -s (так же будут показаны таблицы символов всех объектных файлов архива):
$ nm -s libhello.a Archive index: first in first.o second in second.o first.o: 00000000 T first U puts second.o: U puts 00000000 T second
Для создания индекса архива существует специальная утилита ranlib. Библиотеку libhello.a можно было сотворить и так:
$ ar cr libhello.a first.o second.o
$ ranlib libhello.a
Впрочем библиотека будет прекрасно работать и без индекса архива.
Теперь воспользуемся нашей библиотекой:
$ gcc -Wall -c main.c
$ gcc -o main main.o -L. -lhello
$ ./main
First function...
Second function...
Main function...
Работает...
Ну теперь комментарии... Появились две новые опции gcc:
Опция -lname - передаётся линковщику, указывает на необходимость подключить к исполняемому файлу библиотеку libname. Подключить значит указать, что такие-то и такие-то функции (внешние переменные) определены в такой-то библиотеке. В нашем примере библиотека статическая, все символьные имена будут ссылаться на код находящийся непосредственно в исполняемом файле. Обратите внимание в опции -l имя библиотеки задается как name без приставки lib.
Опция -L/путь/к/каталогу/с/библиотеками - передаётся линковщику, указывает путь к каталогу содержащему подключаемые библиотеки. В нашем случае задана точка ., линковщик сначала будет искать библиотеки в текущем каталоге, затем в каталогах определённых в системе.
Здесь необходимо сделать небольшое замечание. Дело в том, что для ряда опций gcc важен порядок их следования в командной строке. Так линковщик ищет код, соответствующий указанным в таблице символов файла именам в библиотеках, перечисленных в командной строке после имени этого файла. Содержимое библиотек перечисленных до имени файла линковщик игнорирует:
$ gcc -Wall -c main.c
$ gcc -o main -L. -lhello main.o
main.o: In function `main':
main.c:(.text+0xa): undefined reference to `first'
main.c:(.text+0xf): undefined reference to `second'
collect2: ld returned 1 exit status
$ gcc -o main main.o -L. -lhello
$ ./main
First function...
Second function...
Main function...
Такая особенность поведения gcc обусловлена желанием разработчиков предоставить пользователю возможность по разному комбинировать файлы с библиотеками, использовать пересекающие имена... На мой взгляд, если возможно, лучше этим не заморачиваться. В общем подключаемые библиотеки необходимо перечислять после имени ссылающегося на них файла.
Существует альтернативный способ указания местоположения библиотек в системе. В зависимости от дистрибутива, переменная окружения LD_LIBRARY_PATH или LIBRARY_PATH может хранить список разделенных знаком двоеточия каталогов, в которых линковщик должен искать библиотеки. Как правило, по умолчанию эта переменная вообще не определена, но ни чего не мешает её создать:
$ echo $LD_LIBRARY_PATH
$ gcc -o main main.o -lhello
/usr/lib/gcc/i686-pc-linux-gnu/4.4.3/../../../../i686-pc-linux-gnu/bin/ld: cannot find -lhello
collect2: выполнение ld завершилось с кодом возврата 1
$ export LIBRARY_PATH=.
$ gcc -o main main.o -lhello
$ ./main
First function...
Second function...
Main function...
Манипуляции с переменными окружения полезны при создании и отладке собственных библиотек, а так же если возникает необходимость подключить к приложению какую-нибудь нестандартную (устаревшую, обновленную, изменённую - в общем отличную от включенной в дистрибутив) разделяемую библиотеку.
Теперь создадим и используем библиотеку динамическую.
Набор исходных файлов остается без изменения. Вводим команды, смотрим что получилось, читаем комментарии:
$ gcc -Wall -fPIC -c first.c
$ gcc -Wall -fPIC -c second.c
$ gcc -shared -o libhello.so.2.4.0.5 -Wl,-soname,libhello.so.2 first.o second.o
Что получили в результате?
$ file libhello.so.2.4.0.5
libhello.so.2.4.0.5: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, not stripped
Файл libhello.so.2.4.0.5, это и есть наша разделяемая библиотека. Как её использовать поговорим чуть ниже.
Теперь комментарии:
Опция -fPIC - требует от компилятора, при создании объектных файлов, порождать позиционно-независимый код (PIC - Position Independent Code), его основное отличие в способе представления адресов. Вместо указания фиксированных (статических) позиций, все адреса вычисляются исходя из смещений заданных в глобальной таблицы смещений (global offset table - GOT). Формат позиционно-независимого кода позволяет подключать исполняемые модули к коду основной программы в момент её загрузки. Соответственно, основное назначение позиционно-независимого кода - создание динамических (разделяемых) библиотек.
Опция -shared - указывает gcc, что в результате должен быть собран не исполняемый файл, а разделяемый объект - динамическая библиотека.
Опция -Wl,-soname,libhello.so.2 - задает soname библиотеки. О soname подробно поговорим в следующем абзаце. Сейчас обсудим формат опции. Сея странная, на первый взгляд, конструкция с запятыми предназначена для непосредственного взаимодействия пользователя с линковщиком. По ходу компиляции gcc вызывает линковщик автоматически, автоматически же, по собственному усмотрению, gcc передает ему необходимые для успешного завершения задания опции. Если у пользователя возникает потребность самому вмешаться в процесс линковки он может воспользоваться специальной опцией gcc -Wl,-option,value1,value2.... Что означает передать линковщику (-Wl) опцию -option с аргументами value1, value2 и так далее. В нашем случае линковщику была передана опция -soname с аргументом libhello.so.2.
Теперь о soname. При создании и распространении библиотек встает проблема совместимости и контроля версий. Для того чтобы система, конкретно загрузчик динамических библиотек, имели представление о том библиотека какой версии была использована при компиляции приложения и, соответственно, необходима для его успешного функционирования, был предусмотрен специальный идентификатор - soname, помещаемый как в файл самой библиотеки, так и в исполняемый файл приложения. Идентификатор soname это строка, включающая имя библиотеки с префиксом lib, точку, расширение so, снова точку и оду или две (разделенные точкой) цифры версии библиотеки - libname.so.x.y. То есть soname совпадает с именем файла библиотеки вплоть до первой или второй цифры номера версии. Пусть имя исполняемого файла нашей библиотеки libhello.so.2.4.0.5, тогда soname библиотеки может быть libhello.so.2. При изменении интерфейса библиотеки её soname необходимо изменять! Любая модификация кода, приводящая к несовместимости с предыдущими релизами должна сопровождаться появлением нового soname.
Как же это все работает? Пусть для успешного исполнения некоторого приложения необходима библиотека с именем hello, пусть в системе таковая имеется, при этом имя файла библиотеки libhello.so.2.4.0.5, а прописанное в нем soname библиотеки libhello.so.2. На этапе компиляции приложения, линковщик, в соответствии с опцией -lhello, будет искать в системе файл с именем libhello.so. В реальной системе libhello.so это символическая ссылка на файл libhello.so.2.4.0.5. Получив доступ к файлу библиотеки, линковщик считает прописанное в нем значение soname и наряду с прочим поместит его в исполняемый файл приложения. Когда приложение будет запущено, загрузчик динамических библиотек получит запрос на подключение библиотеки с soname, считанным из исполняемого файла, и попытается найти в системе библиотеку, имя файла которой совпадает с soname. То есть загрузчик попытается отыскать файл libhello.so.2. Если система настроена корректно, в ней должна присутствовать символическая ссылка libhello.so.2 на файл libhello.so.2.4.0.5, загрузчик получит доступ к требуемой библиотеки и далее не задумываясь (и ни чего более не проверяя) подключит её к приложению. Теперь представим, что мы перенесли откомпилированное таким образом приложение в другую систему, где развернута только предыдущая версия библиотеки с soname libhello.so.1. Попытка запустить программу приведет к ошибке, так как в этой системе файла с именем libhello.so.2 нет.
Таким образом, на этапе компиляции линковщику необходимо предоставить файл библиотеки (или символическую ссылку на файл библиотеки) с именем libname.so, на этапе исполнения загрузчику потребуется файл (или символическая ссылка) с именем libname.so.x.y. При чем имя libname.so.x.y должно совпадать со строкой soname использованной библиотеки.
В бинарных дистрибутивах, как правило, файл библиотеки libhello.so.2.4.0.5 и ссылка на него libhello.so.2 будут помещены в пакет libhello, а необходимая только для компиляции ссылка libhello.so, вместе с заголовочным файлом библиотеки hello.h будет упакована в пакет libhello-devel (в devel пакете окажется и файл статической версии библиотеки libhello.a, статическая библиотека может быть использована, также только на этапе компиляции). При распаковке пакета все перечисленные файлы и ссылки (кроме hello.h) окажутся в одном каталоге.

Пример именования библиотек в Linux. Ubuntu
Убедимся, что заданная строка soname действительно прописана в файле нашей библиотеки. Воспользуемся мега утилитой objdump с опцией -p:
$ objdump -p libhello.so.2.4.0.5 | grep SONAME SONAME libhello.so.2
Утилита objdump - мощный инструмент, позволяющий получить исчерпывающую информацию о внутреннем содержании (и устройстве) объектного или исполняемого файла. В man странице утилиты сказано, что objdump прежде всего будет полезен программистам, создающими средства отладки и компиляции, а не просто пишущих какие-нибудь прикладные программы :) В частности с опцией -d это дизассемблер. Мы воспользовались опцией -p - вывести различную метаинформацию о объектном файле.
В приведенном примере создания библиотеки мы неотступно следовали принципам раздельной компиляции. Разумеется скомпилировать библиотеку можно было бы и вот так, одним вызовом gcc:
$ gcc -shared -Wall -fPIC -o libhello.so.2.4.0.5 -Wl,-soname,libhello.so.2 first.c second.c
Теперь попытаемся воспользоваться получившейся библиотекой:
$ gcc -Wall -c main.c
$ gcc -o main main.o -L. -lhello -Wl,-rpath,.
/usr/bin/ld: cannot find -lhello
collect2: ld returned 1 exit status
Линковщик ругается. Вспоминаем, что было сказано выше о символических ссылках. Создаем libhello.so и повторяем попытку:
$ ln -s libhello.so.2.4.0.5 libhello.so
$ gcc -o main main.o -L. -lhello -Wl,-rpath,.
Теперь все довольны. Запускаем созданный бинарник:
$ ./main
./main: error while loading shared libraries: libhello.so.2: cannot open shared object file: No such file or directory
Ошибка... Ругается загрузчик, не может найти библиотеку libhello.so.2. Убедимся, что в исполняемом файле действительно прописана ссылка на libhello.so.2:
$ objdump -p main | grep NEEDED NEEDED libhello.so.2 NEEDED libc.so.6
Создаем соответствующую ссылку и повторно запускаем приложение:
$ ln -s libhello.so.2.4.0.5 libhello.so.2
$ ./main
First function...
Second function...
Main function...
Заработало... Теперь комментарии по новым опциям gcc.
Опция -Wl,-rpath,. - уже знакомая конструкция, передать линковщику опцию -rpath с аргументом .. С помощью -rpath в исполняемый файл программы можно прописать дополнительные пути по которым загрузчик разделяемых библиотек будет производить поиск библиотечных файлов. В нашем случае прописан путь . - поиск файлов библиотек будет начинаться с текущего каталога.
$ objdump -p main | grep RPATH RPATH .
Благодаря указанной опции, при запуске программы отпала необходимость изменять переменные окружения. Понятно, что если перенести программу в другой каталог и попытаться запустить, файл библиотеки будет не найден и загрузчик выдаст сообщение об ошибке:
$ mv main ..
$ ../main
First function...
Second function...
Main function...
$ cd ..
$ ./main
./main: error while loading shared libraries: libhello.so.2: cannot open shared object file: No such file or directory
Узнать какие разделяемые библиотеки необходимы приложению можно и с помощью утилиты ldd:
$ ldd main
linux-vdso.so.1 => (0x00007fffaddff000)
libhello.so.2 => ./libhello.so.2 (0x00007f9689001000)
libc.so.6 => /lib/libc.so.6 (0x00007f9688c62000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9689205000)
В выводе ldd для каждой требуемой библиотеки указывается её soname и полный путь к файлу библиотеки, определённый в соответствии с настройками системы.
Сейчас самое время поговорить о том где в системе положено размещать файлы библиотек, где загрузчик пытается их найти и как этим процессом управлять.
В соответствии с соглашениями FHS (Filesystem Hierarchy Standard) в системе должны быть два (как минимум) каталога для хранения файлов библиотек:
/lib - здесь собраны основные библиотеки дистрибутива, необходимые для работы программ из /bin и /sbin;
/usr/lib - здесь хранятся библиотеки необходимые прикладным программам из /usr/bin и /usr/sbin;
Соответствующие библиотекам заголовочные файлы должны находиться в каталоге /usr/include.
Загрузчик по умолчанию будет искать файлы библиотек в этих каталогах.
Кроме перечисленных выше, в системе должен присутствовать каталог /usr/local/lib - здесь должны находиться библиотеки, развернутые пользователем самостоятельно, минуя систему управления пакетами (не входящие в состав дистрибутива). Например в этом каталоге по умолчанию окажутся библиотеки скомпилированные из исходников (программы установленные из исходников будут размещены в /usr/local/bin и /usr/local/sbin, разумеется речь идет о бинарных дистрибутивах). Заголовочные файлы библиотек в этом случае будут помещены в /usr/local/include.
В ряде дистрибутивов (в Ubuntu) загрузчик не настроен просматривать каталог /usr/local/lib, соответственно, если пользователь установит библиотеку из исходников, система её не увидит. Сиё авторами дистрибутива сделано специально, что бы приучить пользователя устанавливать программное обеспечение только через систему управления пакетами. Как поступить в данном случае будет рассказано ниже.
В действительности, для упрощения и ускорения процесса поиска файлов библиотек, загрузчик не просматривает при каждом обращении указанные выше каталоги, а пользуется базой данных, хранящейся в файле /etc/ld.so.cache (кэшем библиотек). Здесь собрана информация о том, где в системе находится соответствующий данному soname файл библиотеки. Загрузчик, получив список необходимых конкретному приложению библиотек (список soname библиотек, заданных в исполняемом файле программы), посредством /etc/ld.so.cache определяет путь к файлу каждой требуемой библиотеки и загружает её в память. Дополнительно, загрузчик может просмотреть каталоги перечисленные в системных переменных LD_LIBRARY_PATH, LIBRARY_PATH и в поле RPATH исполняемого файла (смотри выше).
Для управления и поддержания в актуальном состоянии кэша библиотек используется утилита ldconfig. Если запустить ldconfig без каких-либо опций, программа просмотрит каталоги заданные в командной строке, доверенные каталоги /lib и /usr/lib, каталоги перечисленные в файле /etc/ld.so.conf. Для каждого файла библиотеки, оказавшегося в указанных каталогах, будет считано soname, создана основанная на soname символическая ссылка, обновлена информация в /etc/ld.so.cache.
Убедимся в сказанном:
$ ls
hello.h libhello.so libhello.so.2.4.0.5 main.c
$ gcc -Wall -o main main.c -L. -lhello
$ ./main
./main: error while loading shared libraries: libhello.so.2: cannot open shared object file: No such file or directory
$ sudo ldconfig /полный/путь/к/катаогу/c/примером
$ ls
hello.h libhello.so libhello.so.2 libhello.so.2.4.0.5 main main.c
$ ./main
First function...
Second function...
Main function...
$ sudo ldconfig
$ ./main
./main: error while loading shared libraries: libhello.so.2: cannot open shared object file: No such file or directory
Первым вызовом ldconfig мы внесли в кэш нашу библиотеку, вторым вызовом исключили. Обратите внимание, что при компиляции main была опущена опция -Wl,-rpath,., в результате загрузчик проводил поиск требуемых библиотек только в кэше.
Теперь должно быть понятно как поступить если после установки библиотеки из исходников система её не видит. Прежде всего необходимо внести в файл /etc/ld.so.conf полный путь к каталогу с файлами библиотеки (по умолчанию /usr/local/lib). Формат /etc/ld.so.conf - файл содержит список разделённых двоеточием, пробелом, табуляцией или символом новой строки, каталогов, в которых производится поиск библиотек. После чего вызвать ldconfig без каких-либо опций, но с правами суперпользователя. Всё должно заработать.
Ну и в конце поговорим о том как уживаются вместе статические и динамические версии библиотек. В чем собственно вопрос? Выше, когда обсуждались принятые имена и расположение файлов библиотек было сказано, что файлы статической и динамической версий библиотеки хранятся в одном и том же каталоге. Как же gcc узнает какой тип библиотеки мы хотим использовать? По умолчанию предпочтение отдается динамической библиотеки. Если линковщик находит файл динамической библиотеки, он не задумываясь цепляет его к исполняемому файлу программы:
$ ls
hello.h libhello.a libhello.so libhello.so.2 libhello.so.2.4.0.5 main.c
$ gcc -Wall -c main.c
$ gcc -o main main.o -L. -lhello -Wl,-rpath,.
$ ldd main
linux-vdso.so.1 => (0x00007fffe1bb0000)
libhello.so.2 => ./libhello.so.2 (0x00007fd50370b000)
libc.so.6 => /lib/libc.so.6 (0x00007fd50336c000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd50390f000)
$ du -h main
12K main
Обратите внимание на размер исполняемого файла программы. Он минимально возможный. Все используемые библиотеки линкуются динамически.
Существует опция gcc -static - указание линковщику использовать только статические версии всех необходимых приложению библиотек:
$ gcc -static -o main main.o -L. -lhello
$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.15, not stripped
$ ldd main
не является динамическим исполняемым файлом
$ du -h main
728K main
Размер исполняемого файла в 60 раз больше, чем в предыдущем примере - в файл включены стандартные библиотеки языка C. Теперь наше приложение можно смело переносить из каталога в каталог и даже на другие машины, код библиотеки hello внутри файла, программа полностью автономна.
Как же быть если необходимо осуществить статическую линковку только части использованных библиотек? Возможный вариант решения - сделать имя статической версии библиотеки отличным от имени разделяемой, а при компиляции приложения указывать какую версию мы хотим использовать на этот раз:
$ mv libhello.a libhello_s.a
$ gcc -o main main.o -L. -lhello_s
$ ldd main
linux-vdso.so.1 => (0x00007fff021f5000)
libc.so.6 => /lib/libc.so.6 (0x00007fd0d0803000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd0d0ba4000)
$ du -h main
12K main
Так как размер кода библиотеки libhello ничтожен,
$ du -h libhello_s.a
4,0K libhello.a
Убедимся, что заданная строка soname действительно
размер получившегося исполняемого файла практически не отличается от размера файла созданного с использованием динамической линковки.
http://pyviy.blogspot.ru/2010/12/gcc.html