Shared library что это
Перейти к содержимому

Shared library что это

  • автор:

Shared library что это

Существует набор базовых действий, которые практически любая программа выполняет одинаково – открытие файла, чтение и запись данных и тому подобное. Разделяемые библиотеки предназначены для того, чтобы предоставить прикладным программам готовые интерфейсы функций для выполнения каких-либо более-менее стандартных действий. Разделяемая библиотека, как понятно из названия, может использоваться множеством программ. В настоящий момент стандартным форматом для разделяемых библиотек в Linux является ELF (Executable Linked Format).

Каждый файл ELF имеет заголовок, в котором описывается, какие секции содержит этот файл. Секции объединяют однотипные данные, и их детальное описание можно прочитать в справочном руководстве ( man elf ). Мы же выделим следующую информацию: каждая библиотека содержит список имен переменных и функций, которые она содержит и предоставляет другим (экспортирует) и список переменных и функций, которые необходимо взять в других библиотеках, а также секции инициализации и деинициализации. Экспортируемые и импортируемые объекты (переменные и функции) называют символами библиотеки.

Большинство исполняемых файлов программ также имеют формат ELF, и на самом деле отличаются от библиотек в основном тем, что не имеют экспортируемых функций. Загрузчик ELF (он же dl, dynamic linker и dynamic loader) умеет загружать в память код ELF-файла, анализировать его структуру для определения списков экспортируемых и импортируемых символов и загружать необходимые для работы программы библиотеки.

Когда пользователь пытается запустить какую–либо программу, первым начинает работу загрузчик ELF. Он загружает в память процесса бинарный файл и выделяет, какие символы и из каких библиотек необходимо догрузить в память. После дозагрузки каждой библиотеки загрузчик связывает символы (проставляет реальные адреса) из загруженной библиотеки и повторяет цикл анализа на предмет того, какую библиотеку нужно загрузить. Когда все нужные библиотеки загружены, загрузчик передает управление коду инициализации каждой из загруженных библиотек в порядке, обратном загрузке, после чего передает управление коду программы. По завершении программы загрузчик снова “проходится” по всем библиотекам и вызывает их функции деинициализации. Если на этапе загрузки какой – либо библиотеке возникает ошибка, загрузчик сообщит об этом пользователю. Наиболее типичные ошибки dl – это не найденный файл библиотеки или неразрешимый символ (символ не был найден в библиотеке, в которой ожидался).

Вполне естественно, что загрузчик ищет библиотеки не по всей файловой системе, а только в определенных каталогах. Это каталоги /lib , /usr/lib и те, которые были перечислены системным администратором в файле /etc/ld.so.conf . Уточним, что этот файл на самом деле используется только системной утилитой ldconfig , сам же загрузчик использует кэш-файл /etc/ld.so.cache . Обновить этот кэш-файл можно путем простого запуска ldconfig без параметров. Следствием этого является то, что если вы установили в систему новые библиотеки, не мешает вызвать ldconfig .

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

include ld.so.conf.d/*.conf

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

$ cat /etc/ld.so.conf include ld.so.conf.d/*.conf /usr/lib/mysql /usr/X11R6/lib /usr/lib/qt-3.3/lib $ ls /etc/ld.so.conf.d/ oracle $ cat /etc/ld.so.conf.d/oracle /opt/oracle/9i/lib

Нередко возникает ситуация, когда пользователю необходимо запустить какую-либо программу, которая не находится в каталогах, описанных в /etc/ld.so.conf . В таких ситуациях можно воспользоваться специальным “люком”, оставленным разработчиками dl специально для таких случаев: дело в том, что кроме загрузки библиотек с использованием данных из ld.so.cache загрузчик проверяет факт наличия библиотеки с указанным именем в каталогах, перечисленных в переменной среды LD_LIBRARY_PATH .

Разработчики часто используют еще одну возможность ld: если файл некоторой разделяемой библиотеки указан в переменной LD_PRELOAD , эта библиотека принудительно загружается и ее символы считаются более “приоритетными” и перекрывают одноименные символы, если таковые существуют в других библиотеках, загружаемых ld при запуске на выполнение бинарного файла ELF.

Попробуем рассмотреть примеры использования указанных возможностей dl: пусть есть некоторый программный продукт, в состав которого кроме собственно исполняемых программ входят разделяемые библиотеки (например, таковы практически все продукты, разработанные с помощью Borland Kylix). Если мы установим такой пакет, например, в /opt/program , его исполняемые файлы в /opt/program/bin а разделяемые библиотеки в /opt/program/lib , то программа, скорее всего, не будет запускаться, поскольку не сможет загрузить необходимых библиотек. Для того, чтобы программы пакета начали запускаться, мы должны “объяснить” ld где именно искать библиотеки. Рассмотрим возможные способы, которыми мы можем воздействовать на ld чтобы добиться нужного нам результата.

Первый способ – указать каталог с библиотеками перед запуском программы и уже затем запустить программу (ld воспользуется значением переменной для того, чтобы попытаться найти библиотеки по указанному пути):

$ export LD_LIBRARY_PATH=/opt/program/lib $ /opt/program/bin/filename

Второй способ – добавить каталог /opt/program/lib в файл /etc/ld.so.conf и запустить ldconfig , решив проблему с невозможностью нахождения этих библиотек для всех программ сразу:

$ su - # echo /opt/program/lib >>/etc/ld.so.conf # ldconfig # exit $ /opr/program/bin/filename

Можно также воспользоваться возможностью принудительной загрузки тех библиотек, которые необходимы программе для запуска:

$ export LD_PRELOAD=/opt/program/lib/*$ /opr/program/bin/filename

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

В начало → Linux не для идиотов → LD, Shared Library, SO и много страшных слов

Unix2019b/Библиотеки

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

Одним из возможных решений было бы использование одних и тех же объектных файлов, однако было бы гораздо удобнее держать всю коллекцию объектных файлов в одном легко доступном месте: библиотеке.

  • 1 Статические библиотеки
    • 1.1 Литература
    • 2.1 Position-Independent Code
    • 2.2 GOT и PLT
    • 2.3 Литература
    • 5.1 fakeroot
    • 5.2 Литература
    • 6.1 Преимущества
    • 6.2 Недостатки

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

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

    В системах UNIX командой для сборки статичной библиотеки обычно является ar, и библиотечный файл, который при этом получается, имеет расширение *.a. Также эти файлы обычно имеют префикс «lib» в своём названии и они передаются компоновщику с опцией «-l» с последующим именем библиотеки без префикса и расширения (т.е. «-lfred» подхватит файл «libfred.a»).

    Изготовить свою статическую библиотеку можно так:

    $ gcc -c a.c $ gcc -c b.c $ ar rcs libfoobar.a a.o b.o $ gcc main.c -L. -lfoobar $ ./a.out

    Литература

    • Library order in static linking
    • Why does the order in which libraries are linked sometimes cause errors in GCC?

    Разделяемые библиотеки

    Часто возникает желание переиспользовать один и тот же код в разных программах. Для этого его можно выносить в разделяемые библиотеки.

    В UNIX-системах библиотеки имеют расширение so (shared object).

    Position-Independent Code

    Возникает две проблемы.

    • Размер библиотек заранее неизвестен. Точные адреса, по которым символы из библиотек будут загружены в адресное пространство программы, становятся доступны только после загрузки библиотеки. Одна и та же библиотека в разных программах может быть загружена по разным виртуальным адресам.
    • Код библиотек должен быть немодифицируемым. Если в коде библиотек оставлять дырки, как в случае статической компоновки, и заполнять их в момент загрузки библиотеки, то придётся патчить код библиотеки, то есть модифицировать память некоторым образом, специфичным для данной программы. Это сделает невозможным переиспользование этой библиотеки в разных программах, к тому же медленно.

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

    gcc генерирует подобный код, если указать специальную опцию -fPIC.

    GOT и PLT

    int a; int GetA() { return a; } void SetA(int a_) { a = a_; }
    $ gcc -fPIC -fno-asynchronous-unwind-tables -O2 -S a.c

    Видим, что обращение к переменной уже требует двух инструкций mov.

    Литература

    • Position Independent Code (PIC) in shared libraries
    • Position Independent Code (PIC) in shared libraries on x64

    Пример: чтение zip-архива

    Динамическая загрузка

    Ранее мы указывали разделяемые библиотеки на этапе компоновки. Программа запоминала, какие разделяемые библиотеки ей нужны. Это можно было увидеть в выводе команды ldd.

    Но можно в рантайме загрузить произвольную библиотеку по полному пути к so-файлу и получить доступ к произвольному символу по имени.

    #include #include #include int main(int argc, char **argv) { void *handle; double (*cosine)(double); char *error; handle = dlopen ("/lib/x86_64-linux-gnu/libm.so.6", RTLD_LAZY); if (!handle) { fputs (dlerror(), stderr); exit(1); } cosine = dlsym(handle, "cos"); if ((error = dlerror()) != NULL) { fputs(error, stderr); exit(1); } printf ("%f\n", (*cosine)(2.0)); dlclose(handle); }

    Сборка выполняется так:

    gcc cos.c -ldl

    LD_PRELOAD

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

    Механизм позволяет переопределять функции, подменять реализации.

    Например, сделаем свой rand:

    int rand() { return 4; // chosen by fair dice roll, guaranteed to be random }
    LD_PRELOAD=/path/to/rand.so ./a.out

    Конечно, можно вызвать и оригинальную версию функции, чтобы наша библиотека работала как прокси.

    fakeroot

    Утилита создаёт фиктивное окружение суперпользователя для выполнения операций с файлами. [1]

    Работает через LD_PRELOAD специальной библиотеки libfakeroot.so, которая переопределяет системные вызовы getuid, chown, chmod, mknod, stat, .

    $ fakeroot # echo "Wow I have root access" > root.tst # ls -l root.tst -rw-rw-r-- 1 root root 23 Oct 25 12:13 root.tst # ls -l /root ls: cannot open directory /root: Permission denied # exit $ ls -l root.tst -rw-rw-r-- 1 ubuntu ubuntu 23 Oct 25 12:13 root.tst

    Часто применяется при сборке пакетов. Файлы, которыми якобы владеет root, попадают в архив tar или в deb-пакет с сохранением информации о владении. Распаковка этого архива будет делаться от имени настоящего root, и файлы займут свои места с правильными правами.

    Чем отличается fakeroot от sudo? В первом случае происходит «симуляция root-окружения», во втором — «переключение в root-окружение».

    $ sudo whoami root $ fakeroot whoami root

    Вроде одинаково? А вот и нет.

    В первом случае whoami делает запрос на id пользователя, ядро ему отвечает 0, whoami говорит вам, что вы root.

    Во втором случае whoami делает запрос на id пользователя, запрос перехватывается обёрткой fakeroot, обёртка отвечает 0, whoami говорит вам, что вы root. Но для ядра ваш id как был 1000 (допустим) так и остался.

    К чему это приводит:

    $ sudo touch hello_sudo $ fakeroot touch hello_fakeroot $ ls -l hello_sudo hello_fakeroot -rw-r--r-- 1 user users 0 мая 4 11:36 hello_fakeroot -rw-r--r-- 1 root root 0 мая 4 11:36 hello_sudo

    Литература

    • What is the LD_PRELOAD trick?
    • A Simple LD_PRELOAD Tutorial, Part Two

    Выводы

    Проанализируем плюсы и минусы динамических библиотек по сравнению со статическими.

    Преимущества

    • Обновления и исправления ошибок. Если нужно обновить код, который вынесен в динамическую библиотеку, то достаточно обновить только библиотеку, и все программы, что её используют, получат новую версию, их не надо пересобирать.
    • Экономия памяти. Если одну и ту же библиотеку использует несколько приложений, в оперативной памяти может храниться только один ее экземпляр, доступный этим приложениям. Пример — библиотека C/C++. Ею пользуются многие приложения. Если всех их скомпоновать со статически подключаемой версией этой библиотеки, то код таких функций, как sprintf, strcpy, malloc и др., будет многократно дублироваться в памяти. Но если они компонуются с динамической версией библиотеки C/C++, в памяти будет присутствовать лишь одна копия кода этих функций, что позволит гораздо эффективнее использовать оперативную память.
    • Экономия места на диске. Аналогично.
    • Общие данные. Библиотеки могут содержать такие ресурсы, как строки, значки и растровые изображения. Эти ресурсы доступны любым программам.
    • Расширение функциональности приложения. Библиотеки можно загружать в адресное пространство процесса динамически, что позволяет приложению, определив, какие действия от него требуются, подгружать нужный код. Поэтому одна компания, создав какое-то приложение, может предусмотреть расширение его функциональности за счет библиотек от других компаний.
    • Возможность использования разных языков программирования. У вас есть выбор, на каком языке писать ту или иную часть приложения. Так, пользовательский интерфейс — на одном, прикладную логику — на другом.
    • Реализация специфических возможностей. Определенная функциональность доступна только при использовании динамических библиотек. Если говорить о UNIX, то примером специфической для динамических библиотек является возможность LD_PRELOAD.
    • Ускорение процесса сборки. Каждый раз при сборке анализируются изменения только в коде приложения, выполняется более лёгкая компоновка.

    Недостатки

    • Необходимость сохранения API и ABI. Затрудняется внесение изменений в код, нужно постоянно думать об обратной совместимости и не ломать интерфейсы.
    • Неудобства при сборке. Например, нужно собрать код программы с каким-то флагом компилятора, при этом приходится следить, чтобы все нужные библиотеки были также пересобраны с флагом и подхватились при запуске.
    • Неудобство развёртывания. Статически скомпонованная программа — один самодостаточный файл.
    • Dependency hell. Многочисленные проблемы разного вида. Например, две версии одной библиотеки, половина программ требуют первую, другая половина программ — вторую.
    • Потери производительности. Вызов функции из динамической библиотеки получается медленнее.

    Параметр при сборки библиотеки «shared lib» — что это значит?

    Подскажите пожалуйста, что значит собрать библиотеку с параметром «shared lib» ? То есть «разделяемая библиотека» — что и с кем она разделяет ? Это значит, что библиотека будет динамически подключаемая ? Типа .dll для windows ?

    Отслеживать
    задан 26 ноя 2021 в 10:07
    1,045 6 6 серебряных знаков 16 16 бронзовых знаков
    да, это будет dll или so (для линукса) или dlyn (вроде так) для мака
    26 ноя 2021 в 10:28

    Это библиотека разделяется между процессами. т.е. исполняемый код будет присутствовать в оперативной памяти в одном экземпляре.

    26 ноя 2021 в 10:34

    1 ответ 1

    Сортировка: Сброс на вариант по умолчанию

    TL;DR

    Динамическая библиотека — скомпилированный бинарь, который сам по себе не является программой, но предоставляет какие-то функции, которые могут импортировать другие программы во время своей работы. Пример — sprintf() из libc в *NIX и ntdll.dll в Windows.

    Про этапы компиляции

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

    Трансляция

    В это время транслятор (часть компилятора, которая отвечает за перевод человекочитемого исходника в бинарный вид) парсит даннный на вход файл, проверяет базовый синтаксис и пытается оттранслировать его в бинарный вид. На выходе мы получаем *.o или *.obj файл, в котором содержится бинарный вид компилируемого исходника. В этот момент мы еще не можем его запустить — в таком виде любой прыжок не в программе не определен. А прыжков, даже в линейной программе, наподобие «Hello world!», хватает — мы так или иначе подключаем библиотеки, чтобы самим с нуля не писать функции для вывода на экран (да и, спойлер, без дергания ядра ОС это все равно невозможно, а чтобы работать с ядром все равно нужны библиотеки).

    Для примера разберем этап трансляции того же Hello world’a: Если мы хотм, чтобы компилятор не зашел дальше трансляции, то у GCC существует замечательный ключ -c :

    #include int main(void)
    $ gcc -c main.o main.c 

    И посмотрим содержимое обьектного файла с помощью objdump :

     main.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 : 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # b b: 48 89 c7 mov %rax,%rdi e: e8 00 00 00 00 call 13 13: b8 00 00 00 00 mov $0x0,%eax 18: 5d pop %rbp 19: c3 ret 

    Как видно, здесь просто наша функция main, которая оттранслирована в асм. Причем call идет на 00 00 00 00 — то есть по нулевому смещению, о чем нам objdump сообщает, показывая, что прыжок будет на , то есть следующую строчку. По понятным причинам, такой файл не является исполняемым.

    Линковка

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

    Продолжим мучить Hello world. На прошлом этапе мы получили обьектный файл, в котором не посчитаны адреса. Что ж, теперь посчитаем адреса и получим исполняемый файл:

    $ gcc -o main main.o 

    (Я не буду прикладывать весь вывод objdump ‘a, потому что он реально огромен, а просто приложу еще раз функцию main). Остальные функции являются служебными и служат для запуска программы/использования функций из динамических библиотек

    0000000000001139 : 1139: 55 push %rbp 113a: 48 89 e5 mov %rsp,%rbp 113d: 48 8d 05 c0 0e 00 00 lea 0xec0(%rip),%rax # 2004 1144: 48 89 c7 mov %rax,%rdi 1147: e8 e4 fe ff ff call 1030 114c: b8 00 00 00 00 mov $0x0,%eax 1151: 5d pop %rbp 1152: c3 ret 1153: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) 115a: 00 00 00 115d: 0f 1f 00 nopl (%rax) 

    Как мы видим, здесь у call ‘a появился адрес, теперь мы прыгаем не «вникуда», а куда-то, где определена функция puts (на самом деле, это тоже не совсем так, но об этом опять-таки ниже).

    Немного про библиотеки в целом

    Для начала немного о терминологии. Очень много раз видел, как хедеры ( *.h файлы) называют библиотеками. Это не совсем корректно. По-хорошему говоря, в хедере (если он написан правильно) нет определений функций — только их обьявления. А вот сами функции уже лежат в библиотеке, хедер нужен исключительно для того, чтобы корректно вызывать функции в программе.

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

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

    Рассмотрим пример (опять будут *NIX): У нас есть два хедера с соответствующими *.c файлами и main.c , который эти хедеры использует. Соберем из всего этого добра статическую библиотеку и попытаемся получить полноценную программу:

    #ifndef BYE_H #define BYE_H void print_bye(void); #endif 
    #include "bye.h" #include void print_bye()
    #ifndef PRINT_H #define PRINT_H void print_hello(void); #endif 
    #include "hello.h" #include void print_hello(void)
    #include "hello.h" #include "bye.h" int main(void)

    Собираем хедеры, как мы уже умеем:

    $ gcc -c hello.c bye.c 

    Получаем два обьектных файла. Теперь нужно их собрать в архив с помощью ar . Тут небольшое отступление про нейминг — есть договоренность, что имена библиотек должны начинаться с lib . И линковщик при указании бибилотеки, как мы увидим далее, пытается найти файл начинающийся именно с lib . Поэтому создадим библиотеку print , с названием файла libprint.a :

    $ ar rc libprint.a hello.o bye.o 

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

    $ ranlib libprint.a 

    После чего мы получили статическую библиотеку, которая при линковке окажется жестко зашита в бинарник. Посмотрим, что это означает. Скомпилируем с этой библиотекой и откроем через objdump (обратите внимание, что название библиотеки мы указываем без префикса lib , линковщик сам его добавит:

    $ gcc main.c -L. -lprint -o main $ objdump -d main 
    0000000000001139 : 1139: 55 push rbp 113a: 48 89 e5 mov rbp,rsp 113d: e8 22 00 00 00 call 1164 1142: e8 07 00 00 00 call 114e 1147: b8 00 00 00 00 mov eax,0x0 114c: 5d pop rbp 114d: c3 ret 000000000000114e : 114e: 55 push rbp 114f: 48 89 e5 mov rbp,rsp 1152: 48 8d 05 ab 0e 00 00 lea rax,[rip+0xeab] # 2004 1159: 48 89 c7 mov rdi,rax 115c: e8 cf fe ff ff call 1030 1161: 90 nop 1162: 5d pop rbp 1163: c3 ret 0000000000001164 : 1164: 55 push rbp 1165: 48 89 e5 mov rbp,rsp 1168: 48 8d 05 a0 0e 00 00 lea rax,[rip+0xea0] # 200f 116f: 48 89 c7 mov rdi,rax 1172: e8 b9 fe ff ff call 1030 1177: 90 nop 1178: 5d pop rbp 1179: c3 ret 117a: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0] 

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

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

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

    Лирическое отступление. Динамические библиотеки называются иногда разделяемыми как раз благодаря вот этому механизму. Потмоу что, по сути, файл физически на диске один. Отсюда и название в Linux/UNIX Shared Object ( *.so ). Но по-русски проще и понятнее для окружающих говорить «динамические библиотеки». Тем более, что в Windows и OS X они называются Dynamic Linked Library ( *.dll ) и Dynamic Library ( *.dylib ) соответственно.

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

    $ gcc -c -fPIC bye.c hello.c 

    После этого мы получаем два обьектных файла, которые могут быть собраны в *.so . Самое время приступить:

    $ gcc -shared -o libprint.so bye.o hello.o 

    На выходе мы получаем динамическую библиотеку libprint.so , которая, на самом деле, тоже использует динамическую библиотеку! Ведь функция puts() , на самом деле, предоставляется библиотекой libc.so . Соберем бинарник:

    $ gcc main.c -L. -lprint -o main 
    ./main 
    ./main: error while loading shared libraries: libprint.so: cannot open shared object file: No such file or directory 

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

    LD_LIBRARY_PATH=. ./main 

    В этот раз все хорошо, библиотека подгрузилась и мы увидели две заветные строчки вывода нашей программы. Если мы посмотрим на бинарник в objdump , то увидим, что функции print_hello и print_bye находтся в секции plt , которая описывает, как вызвать функции из динамических библиотек. Если вам интересна эта тема и как работают таблицы PLT , GOT и вообще динамическая загрузка — советую погуглить на эту тему, материала много и разобраться достаточно легко.

    UPD. Чуть не забыл — существует второй способ работы с динамическими библиотеками, чтобы каждый раз не указывать LD_LIBRARY_PATH . Можно жестко вшить путь библиотеки в сам бинарь, но это чревато последствиями. Делается это с помощью rpath :

    $ gcc -L -l -rpath= -o

    UPD2. В некоторых ситуациях возникает необходимость загрузить свою библиотеку вместо системной — к примеру, своя реализация функции rand() . Это делается с помощью LD_PRELOAD . Возьмем, для примера, следующий код:

    #ifndef RAND_H #define RAND_H int rand(void); #endif 
    #include "rand.h" int rand(void)
    #include #include #include int main() < srand (time(NULL)); for(int i = 0; i < 5; i++) < printf ("%d\n", rand() % 100); >return 0; > 

    Если собрать отдельно динамическую библиотеку и отдельно бинарь, то все будет как обычно — даже если указать библиотеку в LD_LIBRARY_PATH :

    $ ./main 74 59 1 47 11 
    $ LD_LIBRARY_PATH=. ./main 88 21 88 79 9 

    Но стоит только определить LD_PRELOAD как происходит магия и мы получаем ответ на главный вопрос вселенной:

    $ LD_PRELOAD=./ld_rand.so ./main 42 42 42 42 42 

    Если хотите немного про это почитать, то вот неплохая точка входа

    Разработка и тестирование Jenkins Shared Library

    В компаниях с большим количеством проектов часто возникает ситуация, когда при разработке пайплайнов мы начинаем повторять себя, добавляя в разные сервисы одинаковые конструкции. Это противоречит основному принципу программирования DRY (Don’t Repeat Yourself), а ещё усложняет внесение изменений в код. Справиться с описанными проблемами помогает Jenkins Shared Library.

    Мы пообщались с Кириллом Борисовым, Infrastructure Engineer технологического центра Deutsche Bank, и узнали, какие задачи решает Jenkins Shared Library и что её внедрение даёт компании. А ещё рассмотрели кейс разработки и тестирования с примерами кода.

    Что такое Jenkins Shared Library

    Jenkins Shared Library — это библиотека для многократного использования кода, которая позволяет описать методы один раз и затем применять их во всех пайплайнах. Она решает проблемы дублирования кода и унификации. Благодаря ней нам не нужно держать в каждом пайплайне огромное количество кода и постоянно повторять его. Мы можем написать код в одном месте, а потом инклюдить и вызывать его в рамках самого пайплайна.

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

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

    Польза Jenkins Shared Library для компании

    Jenkins Shared Library уменьшает количество кода в пайплайне и повышает его читаемость. Для бизнеса это не столь важно, его больше заботит скорость доставки новых версий продукта до конечных пользователей. А здесь многое зависит от скорости разработки. Если каждая команда компании перестанет придумывать своё и начнёт использовать наработки других, вероятнее всего, разработка пайплайнов пойдёт быстрее. Соответственно, процесс доставки обновлений тоже ускорится.

    С точки зрения самой команды, Jenkins Shared Library одновременно и усложняет, и упрощает работу. Например, проводить онбординг оказывается проще — вы показываете всё в одном месте. В то же время командам становится сложнее взаимодействовать. Сложность заключается в необходимости постоянной коммуникации. Когда решаем изменить что-то у себя в библиотеке, мы сначала должны синхронизироваться с другой командой, чтобы убедиться, что у неё ничего не сломается. Но обычно это решается регулярными синками, ревью кода и пул-реквестами.

    Как команда понимает, что пора внедрять Jenkins Shared Library

    На прошлом проекте у нас было 20 микросервисов, очень похожих между собой. Они были написаны по одному скелету на Java, а ещё в каждом из них лежал Jenkins file, где был описан пайплайн сборки проектов. Всякий раз, когда нужно было что-то поменять в пайплайне (а делать это приходилось часто, так как проект динамичный и быстро развивался), мы проходились по 20 репозиториям и поправляли всё вручную. Это довольно геморройный процесс, и мы подумали: «А почему бы нам не сделать что-то общее?».

    Так мы перешли на Jenkins Shared Library. У нас появился базовый пайплайн в библиотеке, и, когда требовалось что-то изменить, мы работали с ним. Добавляли какие-то правки, и они автоматически имплементировались в каждый микросервис.

    Ограничения Jenkins Shared Library

    Любой инструмент призван решать какую-то проблему. Jenkins Shared Library решает проблему дублирования кода и унификации. Если вам эта унификация не нужна, смысла тащить библиотеку в проект нет.

    Если, скажем, у вас небольшой проект, где всего 5 микросервисов, писать для него библиотеку с нуля не стоит. Единственный вариант — переиспользовать, если она уже написана. Jenkins Shared Library — всё-таки решение для более крупных проектов с большим количеством микросервисов.

    Кейс: разработка и тестирование Jenkins Shared Library

    Для разработки и тестирования Jenkins Shared Library нам необходимо установить на свой компьютер gradle. Вот инструкция по установке — https://gradle.org/install/.

    Далее в рабочем каталоге мы выполняем команду инициализации «gradle init», говорим установщику, что хотим настроить base каталог с groovy-файлами, и получаем готовый проект.

    Следующий шаг — создание каталогов для JSL:

    Директория src используется для Groovy классов, которые добавляются в classpath.

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

    Директория test содержит тесты для наших скриптом.

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

    Затем заполянем build.gradle, добавляя в него описание наших каталогов:

    –-- sourceSets < main < groovy < srcDirs = ['src','vars'] >resources < srcDirs = ['resources'] >> test < groovy < srcDirs = ['test'] >> > ---

    И добавляем зависимости, которые понадобятся нам в работе. Полный файл можно посмотреть в репозитории: ссылка на репозиторий.

    Пишем простой первый класс, который увеличивает возраст на указанное значение 🙂

    package com.example class SampleClass < String name Integer age def increaseAge(Integer years) < this.age += years >>

    Не забываем про тесты — создаем в директории test файл с именем SampleClassTest.groovy и содержимым.

    В секции Before описываем действия, которые необходимо выполнить перед тестом — в нашем случае это объявление класса. Далее в секции Test описываем сам тест. Объявляем age = 7 и выполняем функцию increaseAge с параметром 3. В случае правильного выполнения ожидаем получить 10.

    class SampleClassTest < def sampleClass @Before void setUp() < sampleClass = new SampleClass() >@Test void testIncrease() < sampleClass.age = 7 def expect = 10 assertEquals expect, sampleClass.increaseAge(3) >>

    Тест готов, запускаем командой gradle test. Результат будет такой:

    Но это синтетические примеры, давайте рассмотрим реальный

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

    Последуем главному принципу программирования DRY и подготовим класс, который позволит нам вызывать get и post запросы в пайплайне.

    Создаем в директории src/com/example HttpsRequest.groovy, в нём создаем два метода get и post. В параметрах методов передаём URL-запроса и в случае POST еще и body-запроса.

    Корневым методом нашего класса будет метод httpInternal. В нём закладываем логику, добавляем параметры к HttpsURLConnection, отлавливаем ошибку и в результате возвращаем тело ответа и ошибку:

    package com.example import groovy.json.JsonSlurper import javax.net.ssl.HttpsURLConnection import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager import java.security.cert.CertificateException import java.security.cert.X509Certificate class HttpsRequest < def get(String uri) < httpInternal(uri, null, false) >def post(String uri, String body) < httpInternal(uri, body, true) >def httpInternal(String uri, String body, boolean isPost) < def response = [:] def error try < def http = new URL(uri).openConnection() as HttpsURLConnection if (isPost) < http.setRequestMethod('POST') http.setDoOutput(true) if (body) < http.outputStream.write(body.getBytes("UTF-8")) >> http.setRequestProperty("Accept", 'application/json') http.setRequestProperty("Content-Type", 'application/json') http.connect() if (http.responseCode == 200) < response = new JsonSlurper().parseText(http.inputStream.getText('UTF-8')) >else < response = -1 >> catch (Exception e) < println(e) error = e >return [response, error] > >

    Покрываем тестами и создаем файл в директории test/HttpsRequestTest.groovy.

    Для примера делаем get запрос, получаем ответ и проверяем, что он соответствует нашим ожиданиям:

     void testGet() < def expect = json.parseText('') def (result, error) = http.get("https://run.mocky.io/v3/0bd64f74-1861-4833-ad9d-80110c9b5f25") if (error != null) < println(error) >assertEquals "result:", expect, result >

    Осталось дело за малым — подключить библиотеку в Jenkins.

    Для этого в Jenkins нужно перейти: Manage Jenkins → Configure System (Настроить Jenkins → Конфигурирование системы). В блоке Global Pipeline Libraries, добавить наш репозиторий:

    Последний шаг — создаем наш пайплайн:

    @Library('jenkins-shared-library') _ import com.example.HttpsRequest pipeline < agent any stages < stage('Demo') < steps < script < def http = new HttpsRequest() def (result, error) = http.get("https://run.mocky.io/v3/0bd64f74-1861-4833-ad9d-80110c9b5f25") if (error != null) < println(error) >else < println result >> > > > >

    Весь код можно найти в репозитории: ссылка на репозиторий.

    Вместо заключения: о чём нужно помнить при работе с Jenkins Shared Library

    Jenkins Shared Library — больше инструмент разработки в том плане, что нужно программировать. Чаще всего когда вы первый раз работаете с ней, если вы не разработчик, а администратор или DevOps, вы делаете это по наитию с точки зрения администратора. Это рабочий подход, но у него есть свои недостатки. Например, расширять проект дальше сложнее.

    Лучше сразу начинать мыслить как разработчик: выделять классы и методы, правильно их имплементировать в пайплайн и т.д. Если вы не из мира разработки, это осознание приходит с запозданием. И чем позже оно придёт, тем больше времени и сил вы потратите на то, чтобы переписать то, что уже сделали.

    Для тех, кто хочет углубиться в тонкости работы с Jenkins Shared Library и получить скидку 10% на обучение

    6 сентября у нас стартует курс по Jenkins, автором которого выступил Кирилл Борисов, Infrastructure Engineer технологического центра Deutsche Bank. В курсе будет много кейсов и примеров из практики спикера.

    • автоматизировать процесс интеграции и поставки;
    • ускорять цикл разработки и внедрять полезные инструменты;
    • настраивать плагины и создавать пайплайны Jenkins as a code;
    • работать с Jenkins Shared Library.

    Промокод «READER» даёт скидку 10% при покупке курса.

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

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