Салимóненко Дмитрий Александрович

Операционные системы

Задание 6: Создание статических и динамических библиотек

(C, Linux)

Введение


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

Скрыть пояснение
Однако, как показывает практика, все-таки находятся студенты, которые, ну, УПОРНО то ли не могут, то ли не желают этого сделать. Поэтому я в итоге решился подробно изложить здесь правильные команды. Конечно, наверное, это – не совсем правильный подход, но, увы, надоело каждый год объяснять на занятиях одно и то же (а, бывает, уж и сам позабуду – так тогда еще и самому вспоминать приходится).

По способу компоновки библиотеки подразделяют на архивы (статические библиотеки, static libraries) и совместно используемые (динамические библиотеки, shared libraries). В Linux, кроме того, есть механизмы динамической подгрузки библиотек. Суть динамической подгрузки состоит в том, что запущенная программа может по собственному усмотрению подключить к себе какую-либо библиотеку. Об этом всем пойдет речь ниже.

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

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

В Linux статические библиотеки обычно имеют расширение .a (Archive), а совместно используемые библиотеки имеют расширение .so (Shared Object). Хранятся библиотеки, как правило, в каталогах /lib и /usr/lib.

Подключаемые библиотеки необходимо перечислять после имени ссылающегося на них файла.

В соответствии с соглашениями 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.

Исходные коды библиотек

Давайте создадим пару библиотек, которые будем потом подключать разными способами:

  1. /* function1.с */
  2. #include <stdio.h>
  3. void function1() {
  4. printf("\tLOADED Function 1 worked...\n");
  5. }

  1. /* function2.c */
  2. #include <stdio.h>
  3. void function2(char *string) {
  4. printf("%s\n", string);
  5. }

Т.е. это – обычные функции типа «Hello World».

Затем рассмотрим код главной программы main.c:

  1. /* main.c */
  2. #include <dlfcn.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>

  1. int main(int argc, char *argv[]) {
  2. printf("Testing Function1: \n");
  3. function1();
  4. printf("Testing Function2: \n");
  5. function2("\tLOADED Function 2 worked.");
  6. }

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

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

Для компиляции статической библиотеки лучше всего использовать Makefile, так как придется вводить несколько строчек в консоли:

# Makefile for static library

  1. binary: main.o libworld.a
  2. [TAB]gcc -o main main.o -L. -lmain -I.
  3. main.o: main.c
  4. [TAB]gcc -c main.c
  5. libmain.a: function1.o function2.o
  6. [TAB]ar cr libmain.a function1.o function2.o
  7. function1.o: function1.c
  8. [TAB]gcc -c function1.c
  9. function2.o: function2.c
  10. [TAB]gcc -c function2.c
  11. clean:
  12. [TAB]rm   -f   *.o  *.a main

Описание используемых опций приведено ниже. Запускаем программу:

./main

Все должно работать.

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

Чтобы избежать повторного включения одного и того же кода (одних и тех же библиотек) в разных модулях проекта, используются директивы #ifndef, #define, #endif.

1. Заголовочный файл

Заголовочный файл может иметь следующий вид:

  1. /* main.h */
  2. void function1 (void);
  3. void function2 (void);

Чтобы подключить его к программе main.c, в ней между строчками 4 и 5 следует написать:

#include "main.h"

Примечание. Системные библиотеки(заголовочные файлы) следует подключать, используя символы <   >. А вот свои, пользовательские – посредством символов  "   ", т.е. при помощи двойных кавычек.

2. Объявление библиотечных функций в теле программы main.c

Для этого строчки

  1. void function1 (void);
  2. void function2 (void);

следует указать в теле программы main.c между строчками 4 и 5.

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


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

Динамическая библиотека, в отличие от статической, не присоединяется к исполняемому файлу (программе). Использование динамических библиотек в Linux возможно разными способами. Общим среди них является то, что программа должна содержать в себе вызовы таких библиотек (точки входа). Рассмотрим здесь варианты.

Пути для поиска библиотек назначаются:

  1. Во-первых, при конфигурировании утилиты ld во время ее компиляции
  2. Во-вторых, они могут быть указаны через переменную окружения LD_LIBRARY_PATH
  3. В-третьих, они могут быть заданы в командной строке
  4. В-четвертых, путь можно указать и в самой программе (см. пример ниже)

Чаще всего системные библиотеки находятся в каталогах /lib и /usr/lib, поэтому два этих каталога просматриваются автоматически.

В книгах пишут, что, типа того, другие каталоги задаются одной или несколькими опциями -L в командной строке. Например, по следующей команде компоновщик будет просматривать текущий каталог и каталог с именем /home/Desktop/lib для поиска любой библиотеки, не обнаруженной в пути поиска по умолчанию:

gcc -L. -L/home/Desktop/lib hello.о

Однако, у меня в Ubuntu 16 такой способ именно для динамических библиотек не заработал, при запуске компилятор не может обнаружить динамическую библиотеку.

Также пишут, что компоновщик сначала проводит поиск разделяемых библиотек, а затем уже статических (если при сборке библиотека указывается как -lmain, а не как libmain.a или libmain.so).

Однако, я бы не рекомендовал использовать такой вот безразличный способ. Ибо, банально, легко запутаться (какая библиотека используется – статическая или динамическая), а потом можно удивляться – почему, мол, программа не работает. Поэтому, никаких безразличных компиляций, рекомендую везде (да, и ВООБЩЕ ВЕЗДЕ) придерживаться не безразличного, а строгого синтаксиса. И уж тем более не следует озадачиваться попытками экономии вводимых нескольких символов в командной строке, благо есть легкая возможность вызвать ранее введенные команды при помощи нажатия на клавиатуре стрелки «Вверх» или можно использовать Makefile. Безразличие и неуместная краткость – это сестры не таланта, а ошибок и (***)кода.

1. «Обычное» подключение динамических библиотек

Рассмотрим теперь Makefile для компиляции и создания динамической библиотеки:

# Makefile for dynamic library (1) project (usual use)

  1. main1: main1.o libmain1.so
  2. #[TAB]gcc -o main1 main1.o -L. -lmain1 -Wl,-rpath,.
  3. [TAB]gcc -o main1 main1.o  libmain1.so -Wl,-rpath,.
  4. main1.o: main1.c
  5. [TAB]gcc -c main1.c
  6. libmain1.so: function1.o function2.o
  7. [TAB]gcc -shared -o libmain1.so function1.o function2.o
  8. function1.o: function1.c
  9. [TAB]gcc -c -fPIC function1.c
  10. function2.o: function2.c
  11. [TAB]gcc -c -fPIC function2.c
  12. clean:
  13. [TAB]rm  -f  *.o  *.so main1

Внимание: между -Wl, и -rpath,. пробела НЕТ. Т.е. пишется слитно: «Wl,-rpath,.».

И еще раз: сборку динамической библиотеки с объектным файлом main1.o можно проводить разными способами, указывая либо полное имя библиотеки:

libmain1.so

либо сокращенное с опцией -l  (нежелательно, см. выше):

-lmain1

Если все вышесказанное было выполнено правильно, в результате запуска программы main1 в консоли получится что-то вроде:

  1. Testing Function1:
  2.   LOADED Function 1 worked...
  3. Testing Function2:
  4.   LOADED Function 2 worked.

2. Динамическая подгрузка динамических библиотек через системный вызов dlopen()

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

Как и выше, из двух процедур-функций function1.c и function2.c следует сделать объектные файлы (с расширением ), а затем скомпилировать их в динамическую библиотеку. Можно делать это по очереди (используя три последовательные команды, как в вышеприведенном Makefile), а можно и сразу одной командой:

gcc -fpic -shared function1.c function2.c -o libFunc1_Func2.so

Если компиляция пройдет успешно, в текущем каталоге появится файл libFunc1_Func2.so. Это и есть динамическая библиотека.

Далее рассмотрим главную функцию, которая, собственно, и будет подгружать эту динамическую библиотеку и вызывать имеющиеся в ней функции function1() и function2():

  1. /* main2.c */
  2. // gcc main2.c -ldl -o main2 -Wl,-rpath,.
  3. #include <dlfcn.h>
  4. #include <stdio.h>
  5. #include <stdlib.h>

  1. int main(int argc,char *argv[]) {
  2. void *LIB_so;
  3. char *error;
  4. void (*funct1)(void);
  5. void (*funct2)(char *);
  6. LIB_so = dlopen ("libFunc1_Func2.so", RTLD_LAZY);
  7. // LIB_so = dlopen ("./libFunc1_Func2.so", RTLD_LAZY); // Если не указывать опцию -Wl,-rpath,. и если libFunc1_Func2.so расположена в текущем каталоге
  8. if (error = dlerror()) {
  9. printf("dlopen: %s\n",error);
  10. exit(1);
  11. }
  12. funct1 = dlsym(LIB_so, "function1");
  13. if (error = dlerror()) {
  14. printf("dlsym1: %s\n",error);
  15. exit(1);
  16. }
  17. funct2 = dlsym(LIB_so, "function2");
  18. if (error = dlerror()) {
  19. printf("dlsym2: %s\n", error);
  20. exit(1);
  21. }

  1. printf("Testing Function1: \n");  
  2. funct1();
  3. printf("Testing Function2: \n");
  4. funct2("\tLOADED Function 2 worked.");
  5. dlclose(LIB_so);
  6. }

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

Компилировать следует так:

gcc main2.c -ldl -o main2 -Wl,-rpath,.

Примечание: «ldl» означает «ЭлДэЭл» латинскими буквами.

Таким образом, состав файлов проекта:

  • function1.c
  • function2.c
  • libFunc1_Func2.so
  • main2.c
  • main2

Запускаем - как обычно:

./main2

Здесь использован вызов dlopen(), который загружает в память динамическую (разделяемую) библиотеку (если она еще не загружена) и возвращает идентификатор, используемый для адресации к ее функциям. Вызов dlsym() возвращает адреса функций, которые потом могут вызываться, как будто бы они находятся в главной программе main.

Функция dlclose() отсоединяет (detache) главную (текущую) программу от загруженной разделяемой библиотеки (что ОЧЕНЬ удобно, если необходимо минимизировать объем используемой оперативной памяти). Отметим, что если к динамической библиотеке не присоединено больше ни одной программы, то она выгружается из памяти.

Функция dlerror() возвращает строку описания ошибки, произошедшей при последнем вызове одной из функций dlopen(), dlclose(), dlsym(). При отсутствии ошибок dlerror() возвращает значение NULL.

Второй аргумент вызова функции dlopen() — флаг способа динамической загрузки библиотеки. Он может иметь следующие значения. При значении RTLD_NOW все функции библиотеки сразу загружаются в память и после этого становятся доступными для вызова. При значении флага RTLD_LAZY загрузка каждой функции задерживается до тех пор, пока ее имя не будет передано функции dlsym(). Каждое из двух этих значений флага может быть соединено с помощью ключевого слова OR со значением RTLD_GLOBAL. При этом все внешние вызовы загружаемой динамической библиотеки разрешаются вызовом функций из других динамических библиотек. Последние при этом также загружаются в (оперативную) память компьютера.

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

libFunc1_Func2_X.so

Попытавшись запустить программу main, видим, что библиотека libFunc1_Func2.so не найдена, в консоли будет что-то типа:

dlopen: libFunc1_Func2.so: cannot open shared object file: No such file or directory

Это и понятно – ведь мы изменили имя исходной динамической библиотеки. Как поправить ситуацию? Легко. Надо всего-навсего изменить имя этой библиотеки в программе main2.c (см. строчку 11) и заново ее откомпилировать. Итак:

11. LIB_so = dlopen ("libFunc1_Func2_X.so", RTLD_LAZY);

Компилируем и получаем то же, что и раньше:

  1. Testing Function1:
  2.   LOADED Function 1 worked...
  3. Testing Function2:
  4.   LOADED Function 2 worked.

Понятно, что вместо libFunc1_Func2_X.so вполне можно было указать имя какой-нибудь другой динамической библиотеки или даже использовать параметр типа argv[], в который будет поступать имя библиотеки, вводимое в консоли при запуске программы. Таким образом, данный способ (динамической подгрузки) дает очень даже удобные и легкие возможности по использованию библиотек в собственных программах: достаточно лишь указать их имена и затем подгрузить при помощи функции dlopen(). После чего останется лишь вызывать содержащиеся в библиотеках функции.

Обратите внимание: в отличие от первого варианта, здесь, при динамической подгрузке, НЕТ НЕОБХОДИМОСТИ собирать динамическую библиотеку вместе с объектным файлом программы main2.o – в отличие от первого варианта («обычной» компиляции динамической библиотеки).

Вот видите, как все просто и удобно.

Скрыть пояснение
Почти как в языках высокого уровня, типа РНР, java или Питон. Которые, кстати, примерно также динамически погружают код тех или иных функций (библиотек), по мере выполнения основной программы. Ну, насчет последних двух я, конечно, не ручаюсь, а вот в PHP дела обстоят практически именно таким образом. И разве же можно после этого говорить, что С – это сложный для применения язык?... Если, конечно, не вдаваться в особенности управления памятью и «не вспоминать» о ядре Linux…

Минимизируем объем библиотеки утилитой strip

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

strip libmain1.so -o libmain1_MIN.so

Данная команда создаст библиотеку libmain1_MIN.so. Ее объем у меня составил, к примеру, 6152 байтов, тогда как объем исходной библиотеки (libmain1.so) составляет 8192 байтов. Как видим, разница – существенная. Однако, это целесообразно делать только тогда, когда программа уже полностью отлажена и компилируется в чистовую.

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

strip libmain1.so

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

Вывод списка зависимостей, связанных с динамическими библиотеками при помощи утилиты ldd

1. Попробуйте выполнить (для статической библиотеки):

ldd main

Примерно вот что получится:

  1. linux-vdso.so.1 =>  (0x00007ffc9bb33000)
  2. libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5f14039000)
  3. /lib64/ld-linux-x86-64.so.2 (0x00007f5f14403000)

Т.е. есть лишь зависимости, связанные с системными библиотеками Linux. Самой же статической библиотеки, естественно, нет, так как она вошла в состав исполняемого файла. Кстати, с целью убедиться в этом (косвенно), неплохо бы сравнить объемы файлов. Для чего посмотрите, сколько байтов занимают объектные файлы function1.o, function2.o, main.o, libmain.a, просуммируйте и сравните с объемом исполняемого файла main. У меня получилось примерно следующее.

Сумма байтов в указанных четырех файлах:

1528 + 1392 + 3140 + 1792 = 7852 байта.

Объем исполняемого файла:

8744 байт.

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

2. Теперь выполняем (для динамической библиотеки, скомпилированной по первому варианту):

ldd main1

Получится что-то вроде:

  1.   linux-vdso.so.1 =>  (0x00007ffca1599000)
  2.   libmain1.so => ./libmain1.so (0x00007fca4fb7d000)
  3.   libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fca4f7b3000)
  4.   /lib64/ld-linux-x86-64.so.2 (0x00007fca4fd7f000)

Как видим, во второй строчке идет наша библиотека libmain1.so, указан ее начальный виртуальный адрес. Т.е., по сути, нет ничего необычного.

3. А затем попробуйте выполнить

ldd main2

В результате получим:

  1. linux-vdso.so.1 =>  (0x00007ffce59fb000)
  2. libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fadc5e65000)
  3. libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fadc5a9b000)
  4. /lib64/ld-linux-x86-64.so.2 (0x00007fadc6069000)

Любопытно, что при динамической подгрузке в перечне зависимостей нет библиотеки libFunc1_Func2.so, упомянутой в исходном коде main2.c. А почему? См. выше. Потому, что мы эту библиотеку не собирали вместе главной функцией, а лишь сделали ее динамическую подгрузку. Поэтому компилятор и не включил эту библиотеку в перечень зависимостей. Вместо нее фигурирует системная библиотека libdl.so.2. Для подключения этой библиотеки и используется опция -ldl.

Итак, еще раз: второй вариант использует динамическую подгрузку динамической библиотеки. Тогда как первый вариант можно было бы условно назвать «статическим» подключением динамической библиотеки.



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

Команда аr создает статическую библиотеку (архив). В данном случае два объектных файла объединяются в один файл libmain.a.

Примечание: это, на самом деле, не опция, а полноценная команда.

Опция -I, переданная компилятору, обрабатывается и посылается линковщику для того, чтобы тот подключил к бинарному файлу библиотеку.

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

Опция -l используется для реализации ссылок на библиотеки, которые существуют в каталогах, указанных в параметрах -L. Эти каталоги также появляются в LD_LIBRARY_PATH. Таким образом, получается преобразование имен библиотек:

  1. libmain.a -> -lmain
  2. libmain.so -> -lmain

Опция -static указывает линковщику использовать только статические версии всех необходимых приложению библиотек:  

gcc -static -o main main.o -L. -lmain

Опция -shared вызывает линковщик, только не для сборки исполняемого файла, а для создания динамической библиотеки.

Опция -fPIC генерирует позиционно-независимый код для динамических библиотек (если используется опция -shared).

Опция -fpic создает более эффективный код (если поддерживается платформой компилятора).

Опция -Wl, используется для передачи линковщику (сборщику) каких-либо своих (дополнительных) опций. Например, так можно передать путь к библиотеке.

Опция -rpath,. необходима для указания линковщику пути к библиотеке (в данном случае – текущий каталог). Передается при помощи опции -Wl,

Опция -fPIC (-fpic) при компиляции function1.c и function2.c сообщает компилятору, что объектные файлы, полученные в результате компиляции должны содержать позиционно-независимый код (PIC - Position Independent Code), который используется в динамических библиотеках. В таком коде используются не абсолютные (фиксированные) виртуальные позиции (адреса), а относительные (плавающие), что позволяет корректно использовать динамическую библиотеку, размещенную практически с любого виртуального адреса.

Опция -ldl означает компоновку с библиотекой libdl.

Опция -f (для команды rm) означает, что не будет запрашиваться подтверждений, будет удаляться все, что возможно.

Опция -o создает выходной исполняемый файл (т.е. создает и объектные файлы и их сборку в бинарный файл).

Опция -c создает объектные файлы, не собирая их в исполняемый файл (т.е. только компиляция, без линкования).

Опции -Wall -Wextra позволяет компилятору выводить все предупреждения. Эту опцию целесообразно использовать всегда, чтобы генерировать наиболее безошибочный код.


С уважением, Салимоненко Д.А.