Динамические библиотеки и зависимости
В Linux исполняемые файлы можно условно разделить на две группы – те, которые содержат в себе весь код, необходимые для работы, и те, которым необходимы разделяемые библиотеки. Первые называют статически собранными бинарными файлами, вторые называют динамически собранными исполняемыми файлами.
Статически собранные программы характеризуются тем, что могут корректно функционировать в любых условиях, и не зависят от наличия или отсутствия разделяемых библиотек, что может оказаться полезным в ситуациях, когда возникают конфликты версий разделяемых библиотек, или когда системные библиотеки повреждены или недоступны (например во время восстановления операционной системы после серьезного сбоя). К недостаткам таких исполняемых файлов следует отнести то, что они имеют значительный размер и для обновления программы необходимо полностью заменить ее исполняемый файл – например, если несколько статически собранных программ, которые работают с архивами ZIP, содержат ошибку, то для исправления ошибки необходимо заменить все эти программы, что может быть затруднено (например, будет трудно точно установить, какие именно программы содержат ошибочный код и нуждаются в обновлении). Кроме того, статически собранные программы не умеют совместно использовать совпадающие участки кода, что ведет к излишнему расходу системных ресурсов.
Динамически собранные исполняемые файлы для корректной работы требуют наличия файлов разделяемых библиотек, и соответственно при их отсутствии/повреждении не могут корректно функционировать, но зато для обновления программы и исправления ошибки часто оказывается достаточным просто заменить соответствующую разделяемую библиотеку, после чего ошибка исчезает во всех программах, которые эту библиотеку используют динамически. Динамически связанные программы также значительно меньше по объему, чем статически связанные, и код разделяемых библиотек может использоваться одновременно многими программами – что позволяет экономить системные ресурсы.
Подавляющее большинство программ в современных дистрибутивах Linux являются динамически собранными. Определить тип исполняемого файла (статический ли он либо с динамическим связыванием) можно, например, с помощью команды ldd:
# ldd /bin/su
linux-gate.so.1 => (0xb77d0000)
libpam.so.0 => /lib/libpam.so.0 (0xb77be000)
libpam_misc.so.0 => /lib/libpam_misc.so.0 (0xb77bb000)
libc.so.6 => /lib/i686/cmov/libc.so.6 (0xb765f000)
libdl.so.2 => /lib/i686/cmov/libdl.so.2 (0xb765b000)
/lib/ld-linux.so.2 (0xb77d1000)
Но здесь нас подстерегает несколько НО:
1) Существует способ загрузки библиотек в обход ldd, функцией dlopen(). Такие библиотеки в выводе ldd не отображены.
2) Принцип работы ldd состоит в следующем: она устанавливает переменную окружения LD_TRACE_LOADED_OBJECTS, а затем запускает программу на исполнение. Входящий в libc загрузчик динамических библиотек (для GNU glibc это ld-linux.so) проверяет наличие этой переменной. Если она установлена, то вместо нормального запуска программы он выводит список используемых ею динамически подгружаемых библиотек и завершает работу.
То есть, мало того, что не все библиотеки (в идеале) мы сможем увидеть, так ещё и запускаем на исполнение файл! А если это руткит? Что же делать? Выход есть, правда не совсем удобный.
Выход первый (для пункта 2): использовать вместо ldd -> lddsafe (https://github.com/rg3/lddsafe) Скрипт использует objdump и безопасен, так как не запускает на исполнение проверяемую программу.
Выход второй (для пунктов 1 и 2): использовать команду strace. Правда вывод не такой удобный будет, как в случае с ldd.
Так же можно эту информацию получить и таким образом:
# readelf -d /sbin/fsck / grep ‘NEEDED’
0x00000001 (NEEDED) Shared library: [libblkid.so.1]
0x00000001 (NEEDED) Shared library: [libc.so.6]
Для более детальной вычинки бинарника можно использовать утилиту objdump и strace.
Что же делать, если вы получаете сообщения такого плана:
error while loading shared libraries: xxxx.so.0
cannot open shared object file no such file or directory
Это значит, что нет подходящих библиотек. Но что делать, если они есть, а система их не находит? На помощь приходит утилита ldconfig, которая управляет динамическими библиотеками.
С помощью этой программы вы можете добавить свои библиотеки, просматривать кеш библиотек. Список подключаемых библиотек находится в файле /etc/ld.so.conf, а кеш (собственно откуда берут информацию /lib/ld-linux.so) – /etc/ld.so.cache.
– Посмотреть список библиотек
# ldconfig -p | grep mysql
libmysqlclient_r.so.15 (libc6) => /usr/lib/libmysqlclient_r.so.15
libmysqlclient.so.15 (libc6) => /usr/lib/libmysqlclient.so.15
Подробнее можно узнать в справочном руководстве.
Так же можно использовать переменную окружения LD_LIBRARY_PATH.
Примечание.
В Solaris это можно сделать командой dtrace. Кстати, есть хорошая новость – её пытаются портировать на Linux.
Во FreeBSD используется команда truss
Библиотеки
Библиотеки позволяют использовать разработанный ранее программный код в различных программах. Таким образом, программист может не разрабатывать часть кода для своей программы, а воспользоваться тем, что входит в состав библиотек.
В языке программирования C код библиотек представляет собой функции, размещенные в файлах, которые скомпилированы в объектные файлы, а те, в свою очередь, объединены в библиотеки. В одной библиотеке объединяются функции, решающие определенный тип задач. Например, существует библиотека математических функций.
У каждой библиотеки должен быть свой заголовочный файл, в котором должны быть описаны прототипы (объявления) всех функций, содержащихся в этой библиотеке. С помощью заголовочных файлов вы «сообщаете» вашему программному коду, какие библиотечные функции есть и как их использовать.
При компиляции программы библиотеки подключаются линковщиком, который вызывается gcc. Если программе требуются только стандартные библиотеки, то дополнительных параметров линковщику передавать не надо (есть исключения). Он «знает», где стандартные библиотеки находятся, и подключит их автоматически. Во всех остальных случаях при компиляции программы требуется указать имя библиотеки и ее местоположение.
Библиотеки бывают двух видов — статические и динамические. Код первых при компиляции полностью входит в состав исполняемого файла, что делает программу легко переносимой. Код динамических библиотек не входит в исполняемый файл, последний содержит лишь ссылку на библиотеку. Если динамическая библиотека будет удалена или перемещена в другое место, то программа работать не будет. С другой стороны, использование динамических библиотек позволяет сократить размер исполняемого файла. Также если в памяти находится две программы, использующие одну и туже динамическую библиотеку, то последняя будет загружена в память лишь единожды.
Далее будет описан пример, в котором создается библиотека, после чего используется при создании программы.
Пример создания библиотеки
Допустим, мы хотим создать код, который в дальнейшем планируем использовать в нескольких проектах. Следовательно, нам требуется создать библиотеку. Исходный код для библиотеки было решено разместить в двух файлах исходного кода.
Также на данный момент у нас есть план первого проекта, использующего эту библиотеку. Сам проект также будет включать два файла.
В итоге, когда все будет сделано, схема каталогов и файлов будет выглядеть так:
Пусть каталоги library и project находятся в одном общем каталоге, например, домашнем каталоге пользователя. Каталог library содержит каталог source с файлами исходных кодов библиотеки. Также в library будут находиться заголовочный файл (содержащий описания функций библиотеки), статическая (libmy1.a) и динамическая (libmy2.so) библиотеки. Каталог project будет содержать файлы исходных кодов проекта и заголовочный файл с описанием функций проекта. Также после компиляции с подключением библиотеки здесь будет располагаться исполняемый файл проекта.
В операционных системах GNU/Linux имена файлов библиотек должны иметь префикс «lib», статические библиотеки — расширение *.a, динамические — *.so.
Для компиляции проекта достаточно иметь только одну библиотеку: статическую или динамическую. В образовательных целях мы получим обе и сначала скомпилируем проект со статической библиотекой, потом — с динамической. Статическая и динамическая «разновидности» одной библиотеки по-идее должны называться одинаково (различаются только расширения). Поскольку у нас обе библиотеки будут находиться в одном каталоге, то чтобы быть уверенными, что при компиляции проекта мы используем ту, которую хотим, их названия различны (libmy1 и libmy2).
Исходный код библиотеки
void rect(char sign, int w, int h) putchar('\n'); } for (i=0; i w; i++) putchar(sign); putchar('\n'); } void diagonals(char sign, int w) putchar('\n'); } }
В файле figure.c содержатся две функции — rect() и diagonals() . Первая принимает в качестве аргументов символ и два числа и «рисует» на экране с помощью указанного символа прямоугольник заданной ширины и высоты. Вторая функция выводит на экране две диагонали квадрата («рисует» крестик).
void text(char *ch) { while (*ch++ != '\0') putchar('*'); putchar('\n'); }
В файле text.c определена единственная функция, принимающая указатель на символ строки. Функция выводит на экране звездочки в количестве, соответствующем длине указанной строки.
void rect(char sign, int width, int height); void diagonals(char sign, int width); void text(char *ch);
Заголовочный файл можно создать в каталоге source, но мы лучше сохраним его там, где будут библиотеки. В данном случае это на уровень выше (каталог library). Тем самым как бы подчеркивается, что файлы исходных кодов после создания из них библиотеки вообще не нужны пользователям библиотек, они нужны лишь разработчику библиотеки. А вот заголовочный файл библиотеки требуется для ее правильного использования.
Создание статической библиотеки
Статическую библиотеку создать проще, поэтому начнем с нее. Она создается из обычных объектных файлов путем их архивации с помощью утилиты ar.
Все действия, которые описаны ниже выполняются в каталоге library (т.е. туда надо перейти командой cd). Просмотр содержимого каталога выполняется с помощью команды ls или ls -l.
Получаем объектные файлы:
gcc -c ./source/*.c
В итоге в каталоге library должно наблюдаться следующее:
figures.o mylib.h source text.o
Далее используем утилиту ar для создания статической библиотеки:
ar r libmy1.a *.o
Параметр r позволяет вставить файлы в архив, если архива нет, то он создается. Далее указывается имя архива, после чего перечисляются файлы, из которых архив создается.
Объектные файлы нам не нужны, поэтому их можно удалить:
rm *.o
В итоге содержимое каталога library должно выглядеть так:
libmy1.a mylib.h source
, где libmy1.a — это статическая библиотека.
Создание динамической библиотеки
Объектные файлы для динамической библиотеки компилируются особым образом. Они должны содержать так называемый позиционно-независимый код (position independent code). Наличие такого кода позволяет библиотеке подключаться к программе, когда последняя загружается в память. Это связано с тем, что библиотека и программа не являются единой программой, а значит как угодно могут располагаться в памяти относительно друг друга. Компиляция объектных файлов для динамической библиотеки должна выполняться с опцией -fPIC компилятора gcc:
gcc -c -fPIC source/*.c
В отличие от статической библиотеки динамическую создают при помощи gcc указав опцию -shared:
gcc -shared -o libmy2.so *.o
Использованные объектные файлы можно удалить:
rm *.o
В итоге содержимое каталога library:
libmy1.a libmy2.so mylib.h source
Использование библиотеки в программе
Исходный код программы
Теперь в каталоге project (который у нас находится на одном уровне файловой иерархии с library) создадим файлы проекта, который будет использовать созданную библиотеку. Поскольку сама программа будет состоять не из одного файла, то придется здесь также создать заголовочный файл.
#include #include "../library/mylib.h" void data (void) { char strs[3][30]; char *prompts[3] = { "Ваше имя: ", "Местонахождение: ", "Пунк прибытия: "}; int i; for (i=0; i3; i++) { printf("%s", prompts[i]); gets(strs[i]); } diagonals('~', 7); for (i=0; i3; i++) { printf("%s", prompts[i]); text(strs[i]); } }
Функция data() запрашивает у пользователя данные, помещая их в массив strs. Далее вызывает библиотечную функцию diagonals() , которая выводит на экране «крестик». После этого на каждой итерации цикла вызывается библиотечная функция text() , которой передается очередной элемент массива; функция text() выводит на экране звездочки в количестве равному длине переданной через указатель строки.
Обратите внимание на то, как подключается заголовочный файл библиотеки: через относительный адрес. Две точки обозначают переход в каталог на уровень выше, т.е. родительский по отношению к project, после чего путь продолжается во вложенный в родительский каталог library. Можно было бы указать абсолютный путь, например, «/home/pl/c/les22/library/mylib.h». Однако при перемещении каталогов библиотеки и программы на другой компьютер или в другой каталог адрес был бы уже не верным. В случае с относительным адресом требуется лишь сохранять расположение каталогов project и library относительно друг друга.
#include #include "../library/mylib.h" #include "project.h" int main() { rect('-',75,4); data(); rect('+',75,3); }
Здесь два раза вызывается библиотечная функция rect() и один раз функция data() из другого файла проекта. Чтобы сообщить функции main() прототип data() также подключается заголовочный файл проекта.
Файл project.h содержит всего одну строчку:
void data(void);
Из обоих файлов проекта с исходным кодом надо получить объектные файлы для объединения их потом с файлом библиотеки. Сначала мы получим исполняемый файл, содержащий статическую библиотеку, потом — связанный с динамической библиотекой. Однако с какой бы библиотекой мы не компоновали объектные файлы проекта, компилируются они как для статической, так и динамической библиотеки одинаково:
gcc -c *.c
При этом не забудьте сделать каталог project текущим!
Компиляция проекта со статической библиотекой
Теперь в каталоге project есть два объектных файла: main.o и data.o. Их надо скомпилировать в исполняемый файл project, объединив со статической библиотекой libmy1.a. Делается это с помощью такой команды:
gcc -o project *.o -L../library -lmy1
Начало команды должно быть понятно: опция -o указывает на то, что компилируется исполняемый файл project из объектных файлов.
Помимо объектных файлов проекта в компиляции участвует и библиотека. Об этом свидетельствует вторая часть команды: -L../library -lmy1. Здесь опция -L указывает на адрес каталога, где находится библиотека, он и следует сразу за ней. После опции -l записывается имя библиотеки, при этом префикс lib и суффикс (неважно .a или .so) усекаются. Обратите внимание, что после данных опций пробел не ставится.
Опцию -L можно не указывать, если библиотека располагается в стандартных для данной системы каталогах для библиотек. Например, в GNU/Linux это /lib/, /urs/lib/ и др.
Запустив исполняемый файл project и выполнив программу, мы увидим на экране примерно следующее:
Посмотрим размер файла project:
pl@desk:~/c/project$ ls -l project -rwxr-xr-x 1 pl pl 8648 ноя 19 07:46 project
Его размер равен 8698 байт.
Компиляция проекта с динамической библиотекой
Теперь удалим исполняемый файл и получим его уже связанным с динамической библиотекой. Команда компиляции с динамической библиотекой выглядит так (одна команда разбита на две строки с помощью обратного слэша и перехода на новую строку):
gcc -o project *.o \ > -L../library -lmy2 -Wl,-rpath. /library/
Здесь в отличии от команды компиляции со статической библиотеки добавлены опции для линковщика: -Wl,-rpath. /library/. -Wl — это обращение к линковщику, -rpath — опция линковщика, ../library/ — значение опции. Получается, что в команде мы два раза указываем местоположение библиотеки: один раз с опцией -L, а второй раз с опцией -rpath. Видимо для того, чтобы понять, почему так следует делать, потребуется более основательно изучить процесс компиляции и компоновки программ на языке C.
Следует заметить, что если вы скомпилируете программу, используя приведенную команду, то исполняемый файл будет запускаться из командной строки только в том случае, если текущий каталог project. Стоит сменить каталог, будет возникать ошибка из-за того, что динамическая библиотека не будет найдена. Но если скомпилировать программу так:
gcc -o project *.o -L../library -lmy2 \ > -Wl,-rpath,/home/pl/c/library
, т.е. указать для линковщика абсолютный адрес, то программа в данной системе будет запускаться из любого каталога.
Размер исполняемого файла проекта, связанного с динамической библиотекой, получился равным 8544 байта. Это немного меньше, чем при компиляции проекта со статической библиотекой. Если посмотреть на размеры библиотек:
pl@desk:~/c/library$ ls -l libmy* -rw-r--r-- 1 pl pl 3712 ноя 19 07:35 libmy1.a -rwxr-xr-x 1 pl pl 7896 ноя 19 07:36 libmy2.so
, то видно, что динамическая больше статической, хотя исполняемый файл проекта со статической библиотекой больше. Это доказывает, что в исполняемом файле, связанном с динамической библиотекой, присутствует лишь ссылка на нее.
Курс с решением части задач:
pdf-версия
Динамические и статические библиотеки.
Начнём с повторения того, что мы уже знаем. Каждый исходный файл транслируется в объектный файл, после чего все объектные файлы линкуются в программу. Но иногда бывает кусок кода, который хочется переиспользовать. Мы могли бы оттранслировать объектные файлы этого куска один раз, после чего сразу с ними компилировать. Но так оказалось, что уже существует механизм сгруппировать объектные файлы вместе, после чего отдать их линковщику. Называется этот механизм.
Статические библиотеки.
// sum.cpp int sum(int a, int b)
// four.cpp #include int sum(int a, int b); int main()
#include int sum(int a, int b); int main()
И мы зачем-то пытаемся вычленить sum.cpp как библиотеку. Тогда сделать надо вот что: Компилируем:
g++ -c sum.cpp -o sum.o ar rcs libsum.a sum.o
Что тут происходит?
- ar — сокращение от «archive».
- rcs — это некоторая магия (читайте man).
- libsum.a — название библиотеки.
Чтобы скомпилировать каждый из файлов выше с этой библиотекой, делаем так:
g++ four.cpp -lsum -L. -o four g++ five.cpp -lsum -L. -o five
А что происходит тут?
- -L говорит, в каком каталоге искать библиотеку (в нашем случае в каталоге . — в текущем).
- -lsum говорит, что нам нужна библиотека, которая называется libsum.a (т.е. к тому, что идёт после -l спереди приписывается lib», а сзади — «.a»).
Тут больше совершенно ничего интересного, потому что статическая библиотека — набор объектных файлов, а про объектные файлы мы всё знаем. То ли дело.
Динамические библиотеки.
Пусть у вас есть библиотека, которая используется везде вообще. Например, libc. Если она статическая, то код библиотеки есть в каждой из программ. А значит в каждой программе они занимают место и на диске, и в памяти. Чтобы этого избежать, применяют динамические библиотеки.
Идея динамических библиотек в том, что мы ссылаемся как-то на внешнюю библиотечку, а потом на этапе исполнения грузим по надобности её части. Тогда она на диске лежит всего одна, и в память мы можем загрузить её один раз.
Давайте в примере выше сделаем статическую библиотеку динамической:
g++ -fpic -c sum.cpp -o sum.o g++ -shared sum.o -o libsum.so g++ four.cpp -lsum -L. -o four g++ five.cpp -lsum -L. -o five
Что значат все консольные опции тут, уже пояснить намного сложнее, и мы поясним их в следующем параграфе.
А пока обратим внимание на то, что когда мы запустим four или five, нам на этапе исполнения скажут, что библиотека libsum.so не найдена. Хотя, казалось бы, вот она рядом лежит. Дело в том, что по умолчанию Linux ищет библиотеки только по системным путям. (Windows ищет и в текущей директории.) Чтобы проверить, от каких библиотек зависит ваша программа, запустите ldd ./four, и вам скажут, что нужна libsum.so, но её нет.
Есть два способа поправить сию оказию:
Первый — явно при запуске прописывать путь до библиотек.
Для этого существует переменная окружения LD_LIBRARY_PATH , если присвоить ей точку, всё сработает
LD_LIBRARY_PATH=. ./four
Если вам нужно несколько путей, разделяйте их двоеточиями.
Второй — записать в саму программу, где искать библиотеки.
Это можно посмотреть при помощи objdump в секции Dynamic Section , где есть RUNPATH . Чтобы записать туда что надо, делается вот что:
g++ four.cpp -lsum -L. -Wl,-rpath= -o four
-Wl говорит, что опцию после него (т.е. -rpath) надо передать линковщику. Линковщику эта опция говорит, что в тот самый RUNPATH надо записать тот путь, который вы попросили.
А какой надо просить? Не «.» ведь, потому что это путь, из которого вы запускаете программу, а не то место, где сама программа.
И тут вам на помощи приходит псевдо-путь $ORIGIN , который и ссылается на место программы. Используя его Вы можете свободно написать что-нибудь по типу -rpath=’$ORIGIN/../lib/’.
Впрочем, есть ещё и третий путь — использовать CMake, который будет делать всю грязную работу за вас, если написать ему команду add_library .
Кстати, в Windows это работает иначе. В-нулевых, динамическая библиотека там называется не shared object, а dynamic load library. Во-первых, DLL-ки сразу же ищутся в текущем каталоге. Во-вторых, чтобы понять, что вы ссылаетесь на динамическую библиотеку, в Linux вы пишете -L. -lsum, а в Windows компиляция DLL создаёт вам специальный .lib-файл, который называется import-библиотекой, и с которым вы компилируете вашу программу, чтобы она сама поняла, откуда какие функции брать.
Причины нестандартной компиляции динамических библиотек.
g++ -fpic -c sum.cpp -o sum.o g++ -shared sum.o -o libsum.so
Нас интересуют магические слова -fpic и -shared. Зачем на как-то особенно компилировать динамические библиотеки?
А дело вот в чём — при запуске программы, она первая загружается в адресное пространство, и она сама может выбрать, куда ей хочется. Динамические библиотеки такого же по понятным причинам позволить себе не могут. Возникает вопрос — и что теперь? А то, что при наличии глобальных переменных, мы не можем впаять им фиксированные адреса.
Есть путь, которым пошли разработчики архитектуры PowerPC: адреса динамической библиотеки считаются относительно некоторого регистра. И тут жить можно, разве что вам нужно правильно задавать этот регистр, когда обращаетесь к разным библиотекам, и не менять его, если обращаетесь к библиотечной функции из другой функции той же библиотеки. Сложно, но жить можно.
Самый простой способ жить с динамическими библиотеками был у Microsoft на 32-битной Windows. У каждой библиотеки был base-address — то куда библиотеке хочется загрузиться. Если там свободно — туда она и загружается, а если нет, то библиотеку загружают туда, где есть место, а в специальной отдельной секции (.reloc) хранится список адресов, которые надо исправить. Разумеется, в случае релокаций умирает переиспользование библиотеки, но Windows вам же полностью предоставляют, там можно расположить системные библиотеки так, как хочется, поэтому в проприетарных системах всё будет хорошо.
В Linux же это реализовано следующим образом. Смотрите как можем:
call next next: pop ABX lea EAX, [EBX + (myvar - next)]
Тут идея в том, что мы записываем адрес текущей команды на стек, потом берём его со стека, а дальше вместо того, чтобы писать абсолютный адрес переменной myvar , пишем относительный.
Относительный адрес позволяет нам обращаться к ней, если мы не знаем, в какое место памяти будет загружена библиотека, что нам и надо.
Вообще мы не очень хорошо написали (процессор не любит непарные call и pop ), поэтому обычно это выглядит так:
get_pc: mov EBX, [ESP] ret get_variable: call get_pc next: lea EAX, [EBX + (myvar - next)]
Этот код называется position-independent code, и ключ -fpic именно генерацией такого кода и занимается. Вопрос — почему для этого не сделали специальную инструкцию? А вот сделали, но в 64-битном режиме. Всё что с квадратными скобками стало уметь обращаться в память начиная со смещения текущей инструкции. И называется это RIP-relative positioning.
GOT/IAT. PLT.
В итоге мы имеем кусок данных, который можно загрузить и исполнять. Но на самом деле библиотека — это же не сферический код в вакууме, она хочет вызывать какие-то функции.
Например, библиотека для работы с JSON хочет делать fopen . То есть нужно подружить библиотеки друг с другом. Самый простой вариант — когда мы делаем call , в файл мы кладём нулевой адрес, а в секцию релокаций записываем, что вместо него нужно положить fopen , после чего при запуске динамический загрузчик всё разложит по местам. То есть то же самое, что с линковщиком. Почему так не делают? Потому что мы от’ mmap ‘или нашу библиотеку, а в ней дырки. И во все места дырок нужно что-то подставить. И опять вы не можете записать библиотеку в память один раз, что вам очень хочется.
Поэтому вместо этого просто заводят табличку со смещениями, и теперь все call ‘ы обращаются туда, и туда же динамический загрузчик подставляет истинные адреса функций. Эта таблица в Linux называется global offset table, а в Windows — import address table.
Но на самом деле есть ещё проблема. Давайте посмотрим, что происходит, когда мы делаем
void foo(); void bar()
Как мы обсуждали, тут будет call и пустой адрес (до линковки пустой). А что будет, если foo — это внешняя функция из какой-то библиотеки? Тогда надо бы вместо простого call ‘а сделать call qword [got_foo] . Но есть проблема — мы узнаём, откуда эта функция, только на этапе линковки, а компилировать надо раньше. Поэтому компилятор call foo , а потом, если это было неправильно, просто создаёт свою функцию foo , которая является прослойкой для jmp qword [got_foo] . Такие заглушки, которые просто совершают безусловный переход по глобальной таблице смещений имеют название. В Linux их называют PLT (procedure linkage table), а в Windows как-то по-другому.
Но в Linux PLT используется ещё для одной цели. Рассмотрим, скажем, LibreOffice, в котором сотни динамических библиотек с тысячами функций в каждой. Поэтому заполнение GOT — это долго. И нам не хочется смотреть, где лежит каждая функция, после чего записывать её в таблицу. Поэтому эту операцию сделали ленивой:
GOT заполняется специальными заглушками, которые динамически ищут в хэш-таблице настоящий адрес функции, после чего записывают его в GOT вместо себя, и вызывают эту функцию, чтобы она отработала. В Microsoft по-умолчанию отложенная загрузка не используется, но его можно включить (delayed DLL loading или как-то так называется). Это фича загрузчика, а не самой Windows, и делает эта фича примерно то же самое. Однако есть разница. В Linux отсутствие библиотеки не позволяет запустить программу. В Windows же библиотека подгружается при первом вызове функции оттуда, что, по их словам, сделано чтобы вы могли за’ if ‘ать ситуацию, когда библиотеки нет.
Офф-топ на тему «Как страшно жить».
Поговорим про изменение so-файлов. Давайте возьмём и во время работы программы поменяем библиотечку на диске, втупую вписав туда другой текст. Результат поразителен — работа программы также изменится. Почему? Мы же, вроде как, исполняем библиотеку из оперативки, а не с диска. А дело в том, как работает copy-on-write в операционных системах. Когда вы пишете в некоторую страницу, вам копируют её. Но когда кто-то извне пишет в страницу, вам не дают копию старых данных. С исполняемым файлом такое не прокатывает, кстати. Это потому, что вашу программу загружает ядро, и оно может запретить изменять бинарники, а библиотеку загружает ваша программа, которая такого механизма не имеет.
Кстати, изменение и перекомпиляция — разные вещи. И если вы во время работы программы перекомпилируете библиотеку, она не обновится. Связано это с тем, что перекомпилированная библиотека — это новый файл, а не старый. По сути вы удалили старую библиотеку и создали новую, вместо того, чтобы библиотеку изменить. А в Linux пока кто-то имеет доступ к файлу, файл не удаляется до конца. И поэтому в вашей программе всё ещё есть та самая библиотека, которую вы загружали (а не новая).
Детали работы с динамическими библиотеками в Windows.
Никто не удивиться, что набор
call foo@PLT foo@PLT: jmp qword [got_foo]
не очень эффективен (три обращения в память вместо одного). Поэтому в Windows есть спецификатор __declspec(dllimport) , который сразу вместо call 000000 и замены нулей на foo@plt вставляет call qword [got_foo] .
Ещё в Windows есть такая штука как .def-файл — линковщик экспортирует из вашей DLL-ки только то, что нужно, и в .def-файле указывается, что именно. Это хорошо работает в C, где имена символов и имена функций совпадают, но не очень хорошо в C++, где вам придётся писать сложные декорируемые имена. Поэтому есть второй вариант — написать на самой функции __declspec(dllexport) .
И вроде бы всё хорошо, вы метите функции, которые экспортируете как __declspec(dllexport) , которые импортируете — как __declspec(dllimport) и всё классно работает. Но есть проблема: вы и в библиотеке, и в коде, который её использует, подключаете один заголовочный файл, где объявлена функция. И непонятно, что там писать: __declspec(dllexport) или __declspec(dllimport) . Для этого заводится специальный макрос под каждую библиотеку, которым отличают, саму DLL вы компилируете или кого-то с её использованием.
Есть ещё одна проблема. Непонятно, что делать с глобальными переменными. Там проблема ещё более страшная: вы сначала читаете адрес переменной из GOT (извините, IAT), а потом по полученному адресу обращаетесь. Тут уже никакую функцию-прослойку не написать, увы. Поэтому если вы не пометите глобальную переменную как __declspec(dllimport) , тот тут вы уже точно совсем проиграете, у вас линковка не получится.
А ещё реализация DLL в Windows нарушает правила языка: если вы напишете inline -функцию в заголовочном файле. Она просто откопируется в каждую библиотеку, где вы этот заголовок подключился. С этим вы ничего не сделаете, тут вы просто проиграли.
Детали работы с динамическими библиотеками в Linux.
Если вы думаете, что в Windows проблемы с динамическими библиотеками, потому что Windows — какашка, то сильно заблуждаетесь, потому что в Linux нюансов тоже выше крыши.
Итак, мем первый и основной — interposition. Есть такая переменная окружения, как LD_PRELOAD . Она завставляет динамический загрузчик сначала обшарить в поисках динамических библиотек то, что вы в LD_PRELOAD написали, а уже потом смотреть какие-нибудь RUNPATH ‘ы и всё остальное. В частности, так можно подменить аллокатор (и мы так и делали, когда экспериментировали с mmap ‘ом и munmap ‘ом). Такая подмена и называется interposition. Теперь что же у него, собственно, за нюансы есть.
int sum(int a, int b) < return a + b; >int test(int x, int y)
Тут при обычной компиляции вторая функция просто вернёт свой второй аргумент. А при компиляции с -fpic, вы сможете подменить sum , а значит оптимизации не будет. Чтобы это пофиксить, можно пометить sum как static (тогда эта функция будет у вас только внутри файла, а значит его не поменять извне) или как inline (потому что inline полагается на ODR, а значит функция должна быть везде одинаковой). Но есть ещё способ.
Linux по-умолчанию считает, что все функции торчат наружу (т.е. как __declspec(dllexport) в Windows). А можно их пометить, как не торчащие наружу, а нужные только для текущей компилируемой программы/библиотеки: __attribute__((visibility(«hidden»))) .
На самом деле атрибут visibility может принимать несколько различных значений ( «default» , «hidden» , «internal» , «protected» ), где пользоваться сто́ит только вторым, потому что первый и так по-умолчанию, третий заставляет ехать все адреса, а четвёртый добавляет дополнительные аллокации.
При этом также есть различные ключи компиляции (типа -B symbolic), которые тем или иным образом немного меняют поведение, и пояснить разницу между ними всеми вам могут только избранные. И каждый из них может поменять вам поведение так, что вы легко выстрелите себе в ногу. То есть глобально в Linux поведение по умолчанию делаем вам хорошо, но, возможно, немного неоптимизированно, а когда вы начинаете использовать опции, вы погружаетесь в такую бездну, что ускорение заставляет вас очень много думать. Причём замедление от динамических библиотек может быть достаточно сильным: если взять компилятор clang-LLVM и компилировать при помощи его ядро Linux’а, то в зависимости от того, сложен ли clang-LLVM в один большой файл или разбит по библиотечкам, время компиляции отличается на треть. Поэтому ключи использовать придётся.
Один из самых безопасных из них — -fno-semantic-interposition. Это не то же самое, что и -fno-interposition потому, что бинарнику всё равно можно дать LD_PRELOAD , однако в нашем случае функция test будет оптимизирована.
Ещё один полезный ключ — -fno-plt. Он по сути вешает оптимизацию такую же, как __declspec(dllimport) , но на весь файл, поэтому функции, написанные в нём же, замедляются. Чтобы не замедлялись — visibility(«hidden») . Вообще всё это детально и подробно рассказано не будет, если вам интересно, гуглите и читайте/смотрите по теме.
Впрочем, всякие -fno-plt и прочие штуки нужны нам тогда и только тогда, когда мы не включили linking-time оптимизации. В GCC все наборы ключей нафиг не нужны, если включить -flto. Так что в перспективе -flto и -fno-semantic-interposition — это единственное, что вам может быть нужно. Но только в перспективе.
Статические и динамические библиотеки
Библиотека — это «сборник» кода, который можно многократно использовать в самых разных программах. Как правило, библиотека в языке C++ состоит из двух частей:
Заголовочный файл, который объявляет функционал библиотеки.
Предварительно скомпилированный бинарный файл, содержащий реализацию функционала библиотеки.
Некоторые библиотеки могут быть разбиты на несколько файлов и/или иметь несколько заголовочных файлов.
Оглавление:
- Типы библиотек
- Установка библиотек
- Использование библиотек
- Заключение
Типы библиотек
Библиотеки предварительно компилируют по нескольким причинам. Во-первых, их код редко меняется. Было бы напрасной тратой времени повторно компилировать библиотеку каждый раз при её использовании в новой программе. Во-вторых, поскольку весь код предварительно скомпилирован в машинный язык, то это предотвращает получение доступа к исходному коду (и его изменение) сторонними лицами. Этот пункт важен для предприятий/людей, которые не хотят, чтобы результат их труда (исходный код) был доступен всем.
Есть 2 типа библиотек: статические и динамические.
Статическая библиотека (или «архив») состоит из подпрограмм, которые непосредственно компилируются и линкуются с вашей программой. При компиляции программы, которая использует статическую библиотеку, весь функционал статической библиотеки (тот, что использует ваша программа) становится частью вашего исполняемого файла. В Windows статические библиотеки имеют расширение .lib (сокр. от «library»), тогда как в Linux статические библиотеки имеют расширение .a (сокр. от «archive»).
Одним из преимуществ статических библиотек является то, что вам нужно распространить всего лишь 1 (исполняемый) файл, чтобы пользователи могли запустить и использовать вашу программу. Поскольку статические библиотеки становятся частью вашей программы, то вы можете использовать их подобно функционалу своей собственной программы. С другой стороны, поскольку копия библиотеки становится частью каждого вашего исполняемого файла, то это может привести к увеличению размера файла. Также, если вам нужно будет обновить статическую библиотеку, вам придется перекомпилировать каждый исполняемый файл, который её использует.
Динамическая библиотека (или «общая библиотека») состоит из подпрограмм, которые подгружаются в вашу программу во время её выполнения. При компиляции программы, которая использует динамическую библиотеку, эта библиотека не становится частью вашего исполняемого файла — она так и остается отдельным модулем. В Windows динамические библиотеки имеют расширение .dll (сокр. от «dynamic link library»), тогда как в Linux динамические библиотеки имеют расширение .so (сокр. от «shared object»). Одним из преимуществ динамических библиотек является то, что разные программы могут совместно использовать одну копию динамической библиотеки, что значительно экономит используемое пространство. Еще одним преимуществом динамической библиотеки является то, что её можно обновить до более новой версии без необходимости перекомпиляции всех исполняемых файлов, которые её используют.
Поскольку динамические библиотеки не линкуются непосредственно с вашей программой, то ваши программы, использующие динамические библиотеки, должны явно подключать и взаимодействовать с динамической библиотекой. Этот механизм не всегда может быть понятен для новичков, что может затруднить взаимодействие с динамической библиотекой. Для упрощения этого процесса используют библиотеки импорта.
Библиотека импорта (англ. «import library») — это библиотека, которая автоматизирует процесс подключения и использования динамической библиотеки. В Windows это обычно делается через небольшую статическую библиотеку (.lib) с тем же именем, что и у динамической библиотеки (.dll). Статическая библиотека линкуется с вашей программой во время компиляции, и тогда функционал динамической библиотеки может эффективно использоваться в вашей программе, как если бы это была обычная статическая библиотека. В Linux общий объектный файл (с расширением .so) дублируется сразу как динамическая библиотека и библиотека импорта. Большинство линкеров при создании динамической библиотеки автоматически создают к ней библиотеку импорта.
Установка библиотек
Теперь, когда мы уже разобрались с типами библиотек, давайте поговорим о том, как их использовать в наших программах. Установка библиотеки в языке C++ состоит из четырех последовательных шагов:
Шаг №1: Получите библиотеку. Наилучшим вариантом является найти уже предварительно скомпилированный код (если он вообще существует) под вашу операционную систему, чтобы вам не пришлось компилировать библиотеку самостоятельно. В Windows библиотеки обычно распространяются в виде архивов (файлов .zip), а в Linux это пакеты кода (например, пакеты .rpm).
Шаг №2: Установите библиотеку. В Linux это делается путем вызова менеджера пакетов, а дальше он всё делает сам. В Windows вам придется разархивировать библиотеку самостоятельно в любую выбранную вами папку. Рекомендуется хранить все используемые библиотеки в одном месте для быстрого доступа к ним. Например, создайте папку Libs ( C:\Libs ) и выделяйте для каждой (используемой вами) библиотеки свою отдельную подпапку.
Шаг №3: Убедитесь, что компилятор знает, где искать заголовочные файлы библиотеки. В Windows это обычно подпапка include внутри основной папки библиотеки (например, если вы установили библиотеку в C:\Libs\SDL-1.2.11 , то заголовочные файлы находятся в C:\Libs\SDL-1.2.11\include ). В Linux библиотеки обычно устанавливаются в /usr/include . Однако, если файлы находятся в другом месте, вам нужно будет сообщить компилятору, где именно.
Шаг №4: Сообщите линкеру, где искать файлы с реализацией функционала библиотеки. Аналогично с предыдущим шагом, вам нужно указать линкеру место, где находятся файлы с реализацией библиотеки. В Windows это обычно подпапка \lib внутри основной папки библиотеки ( C:\Libs\SDL-1.2.11\lib ), а в Linux это обычно /usr/lib .
Использование библиотек
Как только библиотека установлена, и ваша IDE знает, где искать её файлы, то для того, чтобы вы могли использовать эту библиотеку в ваших проектах, вам необходимо выполнить следующие 3 шага:
Шаг №5: Если вы используете статические библиотеки или библиотеки импорта, сообщите линкеру, какие файлы библиотеки нужно связать с вашей программой.
Шаг №6: Подключите заголовочные файлы библиотеки к вашей программе.
Шаг №7: Если вы используете динамические библиотеки, то убедитесь, что исполняемые файлы будут иметь доступ к файлам библиотеки. Самый простой способ использовать .dll — это скопировать .dll в папку с исполняемым файлом. Поскольку .dll-ка обычно распространяется вместе с исполняемым файлом, то это не составит труда.
Заключение
Шаги №3-№5 включают настройку вашей IDE. К счастью, почти все IDE работают одинаково, когда дело доходит до выполнения подобных задач. На следующем уроке мы рассмотрим, как выполнить данные шаги в Visual Studio.