Дополнительные статьи

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

Последнее обновление: 28.01.2024

Если у нас есть несколько файлов с исходным кодом, то мы можем их вместе скомпилировать в один бинарный файл. Например, у нас есть два файла с исходным кодом hello.c и app.c. Для компиляции обоих файлов в одно приложение мы можем все эти файла передать компилятору:

gcc app.c hello.c -o app

Либо мы можем по отдельности скомпилировать объектные файлы для обоих файлов и скомпоновать их в один исполняемый файл:

gcc -c hello.c -o hello.o
gcc -c app.c -o app.o
gcc app.o hello.o -o app

Для компиляции в объектный файл применяется флаг -c. Можно сократить, без явной компиляции основного файла в объектный файл:

gcc -c hello.c -o hello.o
gcc app.c hello.o -o app

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

Но на Linux компилятор GCC также позволяет использовать другой подход - dynamic linking (динамическое связывание или динамическая компоновка). При динамическом связывании код библиотек не включается в приложение, и библиотеки остаются отдельными файлами, а код приложения просто ссылается на них. Они объединяются только во время (или иногда после) запуска программы. Это более гибкий подход, поскольку если код находится в библиотеках, библиотеки можно обновлять отдельно от приложений. Поэтому, если в библиотеке возникла проблема с безопасностью, единственное, что нужно изменить, — это саму библиотеку. Это также экономит дисковое пространство, поскольку отдельные функции не копируются в каждую программу, а существуют только в одном месте файловой системы. Эти библиотеки еще называются shared libraries (общие или разделяемые библиотеки).

В системе Linux общие библиотеки также называются shared objects (общими объектами) и имеют расширение .so. Стоит отметить, что аналогичные библиотеки есть в других системах: на Windows это динамические библиотеки с расширением .dll, на Mac - библиотеки с расширением .dylib (хотя также используется расширение .so).

Стоит отметить, что по умолчанию вне внешние зависимости подключаются как разделяемые библиотеки динамически. Например, возьмем простейшую программу, которая представленная следующим файлом app.c:

#include <stdio.h>

int main(void) 
{
    printf("Hello World\n");
    return 0;
}

Здесь применяется библиотечная функция printf, которую не мы определяем и которая берется извне. Скомпилируем эту программу стандартным образом:

gcc app.c -o app

Скомпилированный файл имеет небольшой размер (около 16 кБ). Теперь посмотрим зависимости этого файла, запустив команду

ldd app

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

root@Eugene:~/asm# ldd app
        linux-vdso.so.1 (0x00007ffe643f7000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa9113e1000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fa911615000)
root@Eugene:~/asm#

Самая последняя запись в этом списке представляет загрузчик

/lib64/ld-linux-x86-64.so.2

Загрузчик — это программа, которая читает файл программы и загружает его в память, а также любые связанные библиотеки. Загрузчик обычно имеет имя ld.so. Это библиотека, которая фактически сама загружает. Фактически, это сам исполняемый файл. И мы даже можем вручную запустить файл /lib64/ld-linux-x86-64.so.2 как самостоятельную программу.

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

/lib64/ld-linux-x86-64.so.2 ./app

аналогично запустит и выполнит выше скомпилированное приложение.

Следующая запись:

libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa9113e1000)

Представляет библиотеку C версии 6. Стрелка (=>) после имени библиотеки указывает, где в системе можно найти библиотеку. Это позволяет исполняемому файлу узнать, на какую библиотеку ссылаться, а загрузчику динамической компоновки — где ее найти.

И сама первая запись:

linux-vdso.so.1 (0x00007ffe643f7000)

представляет библиотеку "linux-vdso.so.1". Это специальная библиотека, называемая библиотекой vDSO и предоставляемая самим ядром Linux. Эта библиотека позволяет быстро выполнять определенные функции ядра, такие как функции времени, для доступа к которым не требуется какой-либо особый уровень привилегий. Вызов этих функций позволяет получить общедоступную системную информацию без фактического вызова системного вызова.

Но мы можем поместить весь используемый приложением функционал непосредственно в исполняемый файл. Для этого при компоновке применяется флаг -static:

root@Eugene:~/asm# gcc -static app.c -o app
root@Eugene:~/asm# ldd app
        not a dynamic executable
root@Eugene:~/asm#

В этом случае исполняемый файл будет занимать гораздо больше места 800-900 кБ, но не будет иметь внешних зависимостей, на что укажется результат команды lld. Это именно то, что происходит, когда несколько файлов при компиляции определяются в один. Соответсвенно мы можем использовать статическую линковку, при которой файл не имеет зависимостей, но при этом имеет большой объекм. Или можем использовать динамическую линковку, когда файл имеет зависимости, которые мы должны учитывать, но его размер гораздо меньше. Если у нас есть приложений, которые используют одни и те же разделяемые библиотеки, то выгода в плане экономии памяти еще больше.

Определение своей библиотеки

Рассмотрим как создать свою разделяемую библиотеку. Определим файл hello.c, который будет представлять код разделяемой библиотеки

#include <stdio.h>

extern char* message;

void print_data(int value) 
{
    printf("value: %d\n", value );
    printf("message: %s\n", message);
}

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

Скомпилируем этот файл в разделяемую библиотеку. Для этого сначала выполним команду

gcc -c -fPIC  hello.c -o hello.o

Для создания приложения, в котором код не зависит от позиции (Position Independent Code) применяется флаг -fPIC. При работе с разделяемыми библиотеками загрузчик может загружать их в любое место вида памяти. Это одна из мер безопасности, которые обычно принимают дистрибутивы Linux, — рандомизация адресных пространств (ASLR или address space layout randomization), чтобы библиотеки и части приложения можно было загружать в любое место в памяти. Что уменьшает вероятность, что злонамеренный хакер узнает точное расположение частей приложения в памяти. Но это означает, что библиотеки и приложение должны быть написаны специально, чтобы загрузка их в память не приводила к ошибкам. Данный тип кода приложения известен как позиционно-независимый код или PIC (position-independent code). Поэтому для компиляции библиотек применяется флаг -fPIC.

В итоге будет сгенерирован объектный файл hello.o. Далее для создания собственно разделяемой библиотеки выполним команду:

root@Eugene:~/asm# gcc -shared hello.o -o hello.so

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

Для тестирования библиотеки определим главный файл приложения, который назовем app.c и который будет иметь следуюший код:

extern void print_data(int);

char* message = "Hello";

int main(void) 
{
    print_data(22);
    return 0;
}

Здесь объявляем внешнюю функцию print_data и определяем глобальную переменную message (которую будет использовать разделяемая библиотека. В функции main выполняем функцию print_data из библиотеки. Скомпилируем программу:

gcc -c app.c -o app.o
gcc app.o hello.so -o app

Вторая команда передает линковщику объектный файл app.o и файл библиотеки hello.so и создает исполняемый файл приложения - файл app. Запустим этот файл на выполнение:

root@Eugene:~/asm# ./app
./app: error while loading shared libraries: hello.so: cannot open shared object file: No such file or directory

Как видно мы сталкиваемся с ошибкой - загрузчик не может найти библиотеку "hello.so". Дело в том, что по умолчанию поиск библиотек выполняется в некоторых стандартных каталогах операционной системы, в частности, в каталоге /lib, либо в каталогах, которые установлены в переменной окружения LD_LIBRARY_PATH. Но наша библиотека располагается не в этих каталогах, а в текущем каталоге, где также находится исполняемый файл app. Поэтому передадим переменной LD_LIBRARY_PATH путь к текущему каталогу и повторно запустим приложение:

root@Eugene:~/asm#  export LD_LIBRARY_PATH=.
root@Eugene:~/asm# ./app
value: 22
message: Hello
root@Eugene:~/asm#

Обратите внимание, что поиск начинается с каталогов, определенных в LD_LIBRARY_PATH, и продолжается до стандартных каталогов.

Полный консольный вывод:

root@Eugene:~/asm# gcc -c -fPIC  hello.c -o hello.o
root@Eugene:~/asm# gcc -shared hello.o -o hello.so
root@Eugene:~/asm# gcc -c app.c -o app.o
root@Eugene:~/asm# gcc app.o hello.so -o app
root@Eugene:~/asm# ./app
./app: error while loading shared libraries: hello.so: cannot open shared object file: No such file or directory
root@Eugene:~/asm#  export LD_LIBRARY_PATH=.
root@Eugene:~/asm# ./app
value: 22
message: Hello
root@Eugene:~/asm#

И если мы посмотрим на применяемые приложением зависимости, то увидим в списке нашу библиотеку hello.so:

root@Eugene:~/asm# ldd app
        linux-vdso.so.1 (0x00007fff3193e000)
        hello.so => ./hello.so (0x00007fd953be2000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd9539b5000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fd953bee000)
root@Eugene:~/asm#
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850