Какие динамические библиотеки использует ваш исполняемый файл
Перейти к содержимому

Какие динамические библиотеки использует ваш исполняемый файл

  • автор:

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

В 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

Чем отличается динамическая библиотека от статической?

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

Если что я имею ввиду разницу принципиальную. То что динамические можно подгрузить в рантайме это и так понятно.

  • Вопрос задан более трёх лет назад
  • 26650 просмотров

8 комментариев

Простой 8 комментариев

Adamos

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

zkelo

beduin01 @beduin01 Автор вопроса
Александр Рябов, мляяя, ты сам по свое же ссылке пробовал проходить. ГДЕ там разница описана?

lazalu68

beduin01, например здесь — www.cyberforum.ru/cpp-beginners/thread855558.html
beduin01 @beduin01 Автор вопроса

Никита Полевой, где там написано чем они отличаются? Что мешает преобразовать статическую библиотеку в динамическую или выполнить импорт функций из динамической библиотеки?

zkelo

beduin01, боже, зачем быть таким токсичным. Если тебе скидывают информацию, а ты не можешь ее понять, то это твоя проблема, а не проблема того, кто скинул. Банально ты не понимаешь вообще в чем суть динамической и статической библиотеки. Да, и там и там код, одни и те же функции, да, но разница в том, что код статических библиотек вшиваются в исполняемый файл, что делает приложение тяжелым и еще трудно поддерживаемым, так как с обновлением библиотеки придется заново рекомпилить весь код. А динамические же библиотеки, еще называются shared libraries, что более интуитивно понятное название, потому что их могут использовать хоть 100 разных приложений не копируя код библиотеки в исполняемый файл, а просто ссылаясь в runtime на функции библиотеки. В итоге приложение будет меньше весить, поддержка лучше. А спрашивать а пачему нилезя из динамэчисуй сделать стаэтчискую банально некорректный вопрос, да у них есть общее слово — библиотека, еще и выполняют одни и те же функции, но они различаются. Это как спрашивать а почему можно хранить файлы двумя способами, локально и на облаке, почему нельзя из локальной памяти перенести на облако и обратно, в чем смысл? А потому что у этих двух способов хранения свои цели, свои недостатки и преимущества.

beduin01 @beduin01 Автор вопроса

hodini, ты походу реально не понимаешь даже сути вопроса, а пытаешься ответить.

Еще раз поясню: чем структура динамической библиотеки отличается от статической и почему нельзя статически слинковаться с динамической библиотекой.

Какие динамические библиотеки использует ваш исполняемый файл

Издано: 2003, BHV
Твердый переплет, 560 стр..

Динамические библиотеки для начинающих

(статья была опубликована в журнале «Программист»)

    1. как эффективно использовать чужие DLL?
    2. как создать свою собственную?
    3. какие способы загрузки DLL существуют, и чем они отличаются?
    4. как загружать ресурсы из DLL?

    Обо всем этом (и многом другом) рассказывает настоящая глава. Материал рассчитан на пользователей Microsoft Visual C++, а поклонникам других языков и компиляторов придется разбираться с ключами компиляции приведенных примеров самостоятельно.

    Создание собственной DLL

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

    Другое дело — если какие-то функции используются несколькими приложениями. Тогда, поместив их в одну DLL, мы избавимся от дублирования кода и сократим общий объем приложений — и на диске, и в оперативной памяти. Можно выносить в DLL и редко используемые функции отдельного приложения; например, немногие пользователи текстового редактора используют в документах формулы и диаграммы — так зачем же соответствующим функциям впустую «отъедать» память?

    Загрузившему DLL процессу доступны не все ее функции, а лишь явно предоставляемые самой DLL для «внешнего мира» — т. н. экспортируемые. Функции, предназначенные сугубо для «внутреннего» пользования, экспортировать бессмысленно (хотя и не запрещено). Чем больше функций экспортирует DLL — тем медленнее она загружается; поэтому к проектированию интерфейса (способа взаимодействия DLL с вызывающим кодом) следует отнестись повнимательнее. Хороший интерфейс интуитивно понятен программисту, немногословен и элегантен: как говорится, ни добавить, ни отнять. Строгих рекомендаций на этот счет дать невозможно — умение приходит с опытом

    Для экспортирования функции из DLL — перед ее описанием следует указать ключевое слово __declspec(dllexport), как показано в следующем примере:

    // myfirstdll.c #include // Ключевое слово __declspec(dllexport) // делает функцию экспортируемой __declspec(dllexport) void Demo(char *str) < // Выводим на экран переданную функции Demo строку printf(str); >

    Листинг 10 Демонстрация экспорта функции из DLL

    Для компиляции этого примера в режиме командной строки можно запустить компилятор Microsoft Visual Studio: «cl.exe myfirstdll.c /LD«. Ключ «/LD» указывает линкеру, что требуется получить именно DLL.

    Для сборки DLL из интегрированной оболочки Microsoft Visual Studio — при создании нового проекта нужно выбрать пункт «Win32 Dynamics Link Library«, затем «An Empty DLL project«; потом перейти к закладке «File View» окна «Workspace» — и, выбрав правой клавишей мыши папку «Source Files«, добавить в проект новый файл («Add Files to Folder«). Компиляция осуществляется как обычно («Build» ( «Build»).

    Если все прошло успешно — в текущей директории (или в директории Release\Debug при компиляции из оболочки) появится новый файл — «MyFirstDLL.dll». Давайте заглянем в него через «микроскоп» — утилиту dumpbin, входящую в штатную поставку SDK и Microsoft Visual Studio: «dumpbin /EXPORTS MyFirstDLL.dll«. Ответ программы в несколько сокращенно виде должен выглядеть так:

    Section contains the following exports for myfirst.dll 0 characteristics 0.00 version 1 ordinal base 1 number of functions 1 number of names 1 0 00001000 Demo 

    Получилось! Созданная нами DLL действительно экспортирует функцию «Demo» — остается только разобраться, как ее вызывать

    Вызов функций из DLL

    Существует два способа загрузки DLL: с явной и неявной компоновкой.

    1. все подключенные DLL загружаются всегда, даже если в течение всего сеанса работы программа ни разу не обратится ни к одной из них;
    2. если хотя бы одна из требуемых DLL отсутствует (или DLL не экспортирует хотя бы одной требуемой функции) — загрузка исполняемого файла прерывается сообщением «Dynamic link library could not be found» (или что-то в этом роде) — даже если отсутствие этой DLL некритично для исполнения программы. Например, текстовой редактор мог бы вполне работать и в минимальной комплектации — без модуля печати, вывода таблиц, графиков, формул и прочих второстепенных компонентов, но если эти DLL загружаются неявной компоновкой — хочешь не хочешь, придется «тянуть» их за собой.
    3. поиск DLL происходит в следующем порядке: в каталоге, содержащем вызывающий файл; в текущем каталоге процесса; в системном каталоге %Windows%System%; в основном каталоге %Windows%; в каталогах, указанных в переменной PATH. Задать другой путь поиска невозможно (вернее — возможно, но для этого потребуется вносить изменения в системный реестр, и эти изменения окажут влияние на все процессы, исполняющиеся в системе — что не есть хорошо).

    Явная компоновка устраняет все эти недостатки — ценой некоторого усложнения кода. Программисту самому придется позаботиться о загрузке DLL и подключении экспортируемых функций (не забывая при этом о контроле над ошибками, иначе в один прекрасный момент дело кончится зависанием системы). Зато явная компоновка позволяет подгружать DLL по мере необходимости и дает программисту возможность самостоятельно обрабатывать ситуации с отсутствием DLL. Можно пойти и дальше — не задавать имя DLL в программе явно, а сканировать такой-то каталог на предмет наличия динамических библиотек и подключать все найденные к приложению. Именно так работает механизм поддержки plug-in’ов в популярном файл-менеджере FAR (да и не только в нем).

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

    Загрузка DLL с неявной компоновкой

    Чтобы вызвать функцию из DLL, ее необходимо объявить в вызывающем коде — либо как external (т. е. как обычную внешнюю функцию), либо предварить ключевым словом __declspec(dllimport). Первый способ более популярен, но второй все же предпочтительнее — в этом случае компилятор, поняв, что функция вызывается именно из DLL, сможет соответствующим образом оптимизировать код. Например, функция «Demo» из созданной нами библиотеки — «MyFirstDll» вызывается так:

    // ImplictDll.c // Объявляем внешнюю функцию Demo __declspec(dllimport) void Demo(char *str); main() < // Вызываем функцию Demo из DLL Demo("Hello, World!\n"); >

    Листинг 11 Демонстрация вызова функции из DLL неявной компоновкой

    Из командной строки данный пример компилируется так: «cl.exe ImplictDll.c myfirstdll.lib«, где «myfirstdll.lib» — имя библиотеки, автоматически сформированной компоновщиком при создании нашей DLL.

    Разумеется, «чужие» DLL не всегда поставляются вместе с сопутствующими библиотеками, но их можно легко изготовить самостоятельно! На этот случай предусмотрена специальная утилита implib, поставляемая вместе с компилятором, и вызываемая так: «implib.exe Имя_файла _создаваемой_библиотеки Имя_DLL«.

    В нашем случае — не будь у нас файла «MyFirstDLL.lib«, его пришлось бы получить так: «implib.exe MyFirstDLL.lib MyFirstDLL.dll«. Со всеми стандартными DLL, входящими в состав Windows, эту операцию проделывать не нужно, т.к. необходимые библиотеки распространяются вместе с самим компилятором.

    Для подключения библиотеки в интегрированной среде Microsoft Visual Studio — в меню «Project» выберите пункт «Project Settings«, в открывшемся диалоговом окне перейдите к закладке «Link» и допишите имя библиотеки в конец строки «Object/Library Modules«, отделив ее от остальных символом пробела.

    Если все прошло успешно, появится новый файл «ImplictDll.exe«, который, будучи запущенным, горделиво выведет на экран «Hello, Word!«. Это означает, что наша DLL подключена и успешно работает.

    Заглянем внутрь: как это происходит? Запустим «dumpbin /IMPORTS ImplictDll.exe» и посмотрим, что нам сообщит программа:

    File Type: EXECUTABLE IMAGE Section contains the following imports: myfirstdll.dll 404090 Import Address Table 4044C8 Import Name Table 0 time date stamp 0 Index of first forwarder reference 0 Demo KERNEL32.dll 404000 Import Address Table 404438 Import Name Table 0 time date stamp 0 Index of first forwarder reference 19B HeapCreate 2BF VirtualFree CA GetCommandLineA 174 GetVersion 7D ExitProcess 29E TerminateProcess F7 GetCurrentProcess

    Вот она — «Myfirstdll.dll» (в тексте выделена жирным шрифтом), и вот функция «Demo«, а кроме нее — обнаруживает свое присутствие библиотека KERNEL32.DLL – она необходима RTL-коду (Run Time Library — библиотека времени исполнения), насильно помещенному компилятором в наше приложение. RTL-код обеспечивает работу с динамической памятью (heap), считывает аргументы командной строки, проверяет версию Windows и многое-многое другое! Отсюда и появляются в таблице импорта функции HeapCreate, GetCommandLine, GetVersion и т.д. Так что — не удивляйтесь, увидев «левый» импорт в своем приложении!

    Проследить, как именно происходит загрузка DLL, можно с помощью отладчика. Общепризнанный лидер — это, конечно, SoftIce от NuMega, но для наших экспериментов вполне сойдет и штатный отладчик Microsoft Visual Studio. Откомпилировав нашу вызывающую программу, нажмем для пошагового прогона приложения

    Оппаньки! Не успело еще выполниться ни строчки кода, как в окне «output» отладчика появились следующие строки, свидетельствующие о загрузке внешних DLL: NTDLL.DLL, MyFirstDll.dll и Kernel32.dll. Так и должно быть — при неявной компоновке динамические библиотеки подключаются сразу же при загрузке файла, задолго до выполнения функции main!

    Loaded 'C:\WINNT\System32\ntdll.dll', no matching symbolic information found. Loaded 'F:\ARTICLE\PRG\DLL.files\myfirstdll.dll', no matching symbolic information found. Loaded 'C:\WINNT\system32\kernel32.dll', no matching symbolic information found.
    Загрузка DLL с явной компоновкой

    Явную загрузку динамических библиотек осуществляет функция HINSTANCE LoadLibrary(LPCTSTR lpLibFileName) или ее расширенный аналог HINSTANCE LoadLibraryEx(LPCTSTR lpLibFileName, HANDLE hFile, DWORD dwFlags).

    Обе они экспортируются из KERNEL32.DLL, следовательно, каждое приложение требует неявной компоновки по крайней мере этой библиотеки. В случае успешной загрузки DLL возвращается линейный адрес библиотеки в памяти. Передав его функции FARPROC GetProcAddress(HMODULE hModule, LPCSTR lpProcName) — мы получим указатель на функцию lpProcName, экспортируемую данной DLL. При возникновении ошибки обе функции возвращают NULL. После завершения работы с динамической библиотекой ее следует освободить вызовом функции BOOL FreeLibrary(HMODULE hLibModule). Для пояснения приведем код примера с подробными комментариями:

    // DynCall.c #include #include main() < // Дескриптор загружаемой dll HINSTANCE h; // Объявление указателя на функцию, вызываемой из DLL // Обратите внимание – имена объявляемой функции и // функции, вызываемой из DLL, могут и не совпадать, // т.к. за выбор вызываемой функции отвечает // GetProcAddress void (*DllFunc) (char *str); // Загружаем MyFirstDLL h=LoadLibrary("MyFirstDLL.dll"); // Контроль ошибок – если загрузка прошла успешно, // функция вернет что-то отличное от нуля if (!h) < printf("Ошибка - не могу найти MyFirstDLL.dll\n"); return; >// Вызовом GetProcAddress получаем адрес функции Demo // и присваиваем его указателю DllFunc с явным // приведением типов. Это необходимо т.к. // GetProcAddress возвращает бестиповой far-указатель DllFunc=(void (*) (char *str)) GetProcAddress(h,"Demo"); // Контроль ошибок – если вызов функции GetProcAddress // завершился успешно, она вернет ненулевой указатель if (!DllFunc) < printf("Ошибка! В MyFirstDLL " "отсутствует ф-ция Demo\n"); return; >// Вызов функции Demo из DLL DllFunc("Test"); // Выгрузка динамической библиотеки из памяти FreeLibrary(h); >

    Листинг 12 Демонстрация вызова функции из DLL явной компоновкой

    Компилировать так: «cl DynCall.c» — никаких дополнительных библиотек указывать не нужно (необходимые kernel32.lib и LIBC.lib компоновщик подключит самостоятельно).

    В интегрированной среде Microsoft Visual Studio достаточно щелкнуть мышкой по иконке «Build» — и никаких дополнительных настроек!

    Для изучения секции импорта только что полученного файла запустим утилиту Dumpbin – обратите внимание, что здесь отсутствует всякое упоминание о MyFirstDLL.dll, но обнаруживаются две функции: LoadLibrary и GetProcAddress — которые и загружают нашу библиотеку. Это очень важное обстоятельство — изучение секции импорта исследуемого файла не всегда позволяет установить полный перечень функций и динамических библиотек, которые использует приложение. Наличие LoadLibrary и GetProcAddress красноречиво свидетельствует о том, что приложение подгружает какие-то модули во время работы самостоятельно.

    Чтобы выяснить какие — запустим его под отладчиком. Сразу же после загрузки исполняемого файла в окне Output появятся строки:

    Loaded 'C:\WINNT\System32\ntdll.dll', no matching symbolic information found. Loaded 'C:\WINNT\system32\kernel32.dll', no matching symbolic information found.

    Это опять грузятся обязательные KERNEL32.DLL и NTDLL.DLL (последнее — только под Windows NT/2000). Никакого упоминания о MyFirstDLL.dll еще нет. Пошагово исполняя программу («Debug» ( «Step Over«), дождемся выполнения функции LoadLibrary. Тут же в Output-окне появится следующая строка:

    Loaded 'F:\ARTICLE\PRG\DLL.files\myfirstdll.dll', no matching symbolic information found.

    Наша динамическая библиотека загрузилась; но не сразу после запуска файла (как это происходило при неявной компоновке), а только когда в ней возникла необходимость!

    Если же по каким-то причинам DLL не найдется, или окажется, что в ней отсутствует функция Demo — операционная система не станет «убивать» приложение с «некрологом» критической ошибки, а предоставит программисту возможность действовать самостоятельно. В качестве эксперимента попробуйте удалить (переименовать) MyFirstDLL.dll и посмотрите, что из этого получится.

    Выгрузка динамических библиотек из памяти

    Когда загруженная динамическая библиотека больше не нужна — ее можно освободить, вызвав функцию BOOL FreeLibrary(HMODULE hLibModule) и передав ей дескриптор библиотеки, ранее возвращенный функцией LoadLibrary. Обратите внимание — DLL можно именно освободить, но не выгрузить! Выгрузка DLL из памяти не гарантируется, даже если работу с ней завершили все ранее загрузившие ее процессы.

    Задержка выгрузки предусмотрена специально — на тот случай, если эта же DLL через некоторое время вновь понадобится какому-то процессу. Такой трюк оптимизирует работу часто используемых динамических библиотек, но плохо подходит для редко используемых DLL, загружаемых лишь однажды на короткое время. Никаких документированных способов насильно выгрузить динамическую библиотеку из памяти нет; а те, что есть — работают с ядром на низком уровне и не могут похвастаться переносимостью. Поэтому здесь мы их рассматривать не будем. К тому же — тактика освобождения и выгрузки DLL по-разному реализована в каждой версии Windows: Microsoft, стремясь подобрать наилучшую стратегию, непрерывно изменяет этот алгоритм; а потому и отказывается его документировать.

    Нельзя не обратить внимания на одно очень важное обстоятельство: динамическая библиотека не владеет никакими ресурсами — ими владеет, независимо от способа компоновки, загрузивший ее процесс. Динамическая библиотека может открывать файлы, выделять память и т. д., но память не будет автоматически освобождена после вызова FreeLibrary, а файлы не окажутся сами собой закрыты — все это произойдет лишь после завершения процесса, но не раньше! Естественно, если программист сам не освободит все ненужные ресурсы вручную, с помощью функций CloseHandle, FreeMemory и подобных им.

    Если функция FreeLibrary пропущена, DLL освобождается (но не факт, что выгружается!) только после завершения вызвавшего процесса. Могут возникнуть сомнения: раз FreeLibrary немедленно не выгружает динамическую библиотеку из памяти, так зачем она вообще нужна? Не лучше ли тогда все пустить на самотек — все равно ведь загруженные DLL будут гарантированно освобождены после завершения процесса? Что ж, доля правды тут есть, и автор сам порой так и поступает; но при недостатке памяти операционная система может беспрепятственно использовать место, занятое освобожденными динамическими библиотеками под что-то полезное — а если DLL еще не освобождены, их придется «скидывать» в файл подкачки, теряя драгоценное время. Поэтому лучше освобождайте DLL сразу же после их использования!

    ООП и DLL

    Динамические библиотеки ничего не знают ни о каком ООП! Они не имеют ни малейшего представления о существовании классов! Все, что умеют DLL — экспортировать одно или несколько имен функций (ресурсов, глобальных переменных), а уж как его использовать — решать программисту или компилятору.

    Что ж; испытаем компилятор на «сообразительность», включив в описание класса ключевое слово __declspec(dllexport) и посмотрим, что из этого выйдет:

    // DLLclass.cpp #include class __declspec(dllexport) MyDllClass< public: Demo(char *str); >; MyDllClass::Demo(char *str)

    Листинг 13 Демонстрация экспорта класса из DLL

    Откомпилируем этот код как обычную DLL и заглянем в таблицу импорта утилитой dumpbin:

    dumpbin /EXPORTS DLLclass.dll File Type: DLL Section contains the following exports for DLLclass.dll 0 characteristics 3B1B98E6 time date stamp Mon Jun 04 18:19:18 2001 0.00 version 1 ordinal base 2 number of functions 2 number of names ordinal hint RVA name 1 0 00001000 ??4MyDllClass@@QAEAAV0@ABV0@@Z 2 1 00001020 ?Demo@MyDllClass@@QAEHPAD@Z 

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

    Как же со всем этим работать? Попробуй-ка угадай — во что превратится то или иное имя после компиляции! Впрочем, при неявной компоновке ни о чем гадать не придется, т. к. обо всем позаботится сам компилятор, а от программиста потребуется лишь описать класс, предварив его ключевым словом __declspec(dllimport):

    // DLLClassCall.cpp #include class __declspec(dllimport) MyDllClass< public: Demo(char *str); >; main()

    Листинг 14 Демонстрация импорта класса из DLL неявной компоновкой

    Откомпилируйте пример как обычную программу с неявной компоновкой («cl DLLClassCall.cpp DLLClass.lib«) и попробуйте запустить полученный файл. Работает? Никакой разницы с «классическим» Си нет, не правда ли? Вот только как подключить DLL с явной компоновкой? Неужели нельзя запретить компилятору «калечить» имена функций?! Конечно же, можно

    Мангл и как его побороть или импорт классов из DLL явной компоновкой

    Искажение функций Cи ++ компилятором называется по-английски «mangle«, и среди русскоязычных программистов широко распространена его калька — «манглить». В принципе «манглеж» функций не препятствует их явному вызову посредством GetProcAddress — достаточно лишь знать, как точно называется та или иная функция, что нетрудно выяснить тем же dumpbin. Однако засорение программы подобными «заученными» именами выглядит не очень-то красиво; к тому же — всякий компилятор «манглит» имена по-своему, и непосредственное использование имен приводит к непереносимости программы.

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

    // DllClass.def: EXPORTS Demo

    Листинг 15 Отказ от «замангления» имен

    Сперва идет ключевое слово «EXPORTS», за которым следуют одно или несколько «неприкасаемых» имен. Каждое имя начинается с новой строки, и в его конце не указывается точка с запятой.

    Для подключения DEF-файла при компиляции из командной строки — используйте опцию «/link /DEF:имя_файла.def«, например так: «cl DLLclass.cpp /LD /link /DEF:DLLClass.def«.

    Для подключения DEF-файла в интегрированной среде Microsoft Visual Studio — перейдите к закладке «File View» окна «Workspace» и, щелкнув правой клавишей по папке «Source Files«, выберите в контекстом меню пункт «Add Files to Folder«, а затем укажите путь к DEF-файлу. Откомпилируйте проект как обычно: «Build» ( «Build».

    Заглянув в таблицу импорта полученного DLL-файла, мы, среди прочей информации, увидим следующее:

    1 0 00001000 ??4MyDllClass@@QAEAAV0@ABV0@@Z 2 1 00001020 Demo 

    Теперь имя функции Demo выглядит «как положено». А абракадабра, расположенная строчкой выше — это конструктор класса MyDllClass, который, хоть и не был специально объявлен, все равно экспортируется из динамической библиотеки.

    Однако, избавившись от одной проблемы, мы получаем другую — имя функции Demo потеряло всякое представление о классе, которому оно принадлежало; и теперь придется загружать его вручную, повторяя эту операцию для каждого элемента класса. Фактически — придется в вызываемой программе собирать «скелет» класса из «косточек» заново. Но иного способа явной загрузки класса из DLL не существует.

    Следующий пример демонстрирует вызов функции MyDllClass из динамической библиотеки с явной компоновкой. Обработка ошибок для упрощения опущена. Обратите внимание, как объявляется функция Demo — описания класса в DLL и в вызывающей программе существенно отличаются, поэтому с идеей поместить описания класса в общий для всех include-файл придется расстаться.

    // DeMangle.cpp #include class MyDllClass< public: void (*Demo) (char *str); >; main() < HINSTANCE h=LoadLibrary("DllClass.dll"); MyDllClass zzz; // Внимание! Выполнение конструктора / деструктора // класса при явной загрузке не происходит // автоматически и при необходимости эту операцию // следует выполнить вручную zzz.Demo=(void (*) (char *str)) GetProcAddress(h,"Demo"); zzz.Demo("Test"); >

    Листинг 16 Демонстрация вызова функции MyDllCLass::Demo явной компоновкой

    Загрузка ресурсов из DLL

    Помимо функций, динамические библиотеки могут содержать и ресурсы — строки, иконки, рисунки, диалоги и т. д. Хранение ресурсов в DLL очень удобно; в частности — при создании приложений с многоязычным интерфейсом: заменив одну DLL на другую, мы заменяем все надписи в программе, скажем, с английского на русский — и, заметьте, без всяких «хирургических вмешательств» в код приложения! Аналогично можно менять иконки, внешний вид диалогов и т. д.

    Создание DLL, содержащей ресурсы, ничем не отличается от создания исполняемого приложения с ресурсами; сначала необходимо создать сам файл ресурсов — например, так:

    // MyResDll.rc #pragma code_page(1251) STRINGTABLE DISCARDABLE BEGIN 1 "Hello, World!" END

    Листинг 17 Создание DLL, содержащей одни лишь ресурсы

    Файл ресурсов надо скомпилировать — «rc MyResDll.rc» — и преобразовать линкером в DLL, обязательно указав флаг «/NOENTRY«, т. к. эта динамическая библиотека содержит исключительно одни ресурсы и ни строки кода: «link MyRedDll.res /DLL /NOENTRY«.

    В Visual Studio это сделать еще проще — достаточно кликнуть по папке «Resourses» окна «File View» и добавить новый файл ресурса, который затем можно будет модифицировать визуальным редактором по своему усмотрению.

    Для загрузки ресурса из DLL — в принципе, можно воспользоваться уже знакомой нам функцией LoadLibray, и передавать возращенный ею дескриптор LoadString или другой функции, работающей с ресурсами. Однако загрузку динамической библиотеки можно значительно ускорить, если «объяснить» системе, что эта DLL не содержит ничего, кроме ресурсов, и нам достаточно лишь спроецировать ее на адресное пространство процесса, а обо всем остальном мы сумеем позаботиться и самостоятельно.

    Вот тут-то и пригодится функция LoadLibraryEx: ее первый аргумент, как и у коллеги LoadLibrary, задает имя динамической библиотеки для загрузки, второй — зарезервирован и должен быть равен нулю, а третий, будучи равным LOAD_LIBRARY_AS_DATAFILE, заставляет функцию делать именно то, что нам нужно — загружать DLL как базу данных (если динамическая библиотека содержит помимо ресурсов еще и код, то загрузка с этим ключом проходит все равно успешно, но функции загруженной DLL не будут доступны — только ресурсы):

    // DllLoadRes.c #include #include main()

    Листинг 18 Демонстрация оптимизированной загрузки DLL, не содержащей ничего кроме ресурсов

    Эта программа компилируются точно так же, как и предыдущие примеры явной компоновки — и после запуска победно выводит на экране «Hello, Word!«, подтверждая, что ресурс «строка» из динамической библиотеки был успешно загружен! Аналогичным способом можно загружать ресурсы из исполняемых файлов; с этой точки зрения они ничем не отличаются от динамических библиотек.

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

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

    В UNIX-системах библиотеки имеют расширение so (shared object), в Windows — расширение dll (dynamic link library).

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

    Динамические библиотеки могут использоваться двумя способами:

    • динамическая компоновка (dynamic linking)
    • динамическая загрузка (dynamic loading)

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

    API и ABI

    API: Application Program Interface

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

    В C и C++ API обычно поставляется в виде заголовочного файла (h) вместе с библиотекой.

    С API работают люди, когда пишут код.

    ABI: Application Binary Interface

    Детали реализации этого интерфейса. Определяет такие вещи, как

    • Способ передачи параметров в функции (регистры, стек).
    • Кто извлекает параметры из стека (вызывающий код или вызываемый, caller/callee).
    • Как происходит возврат значений из функции.
    • Как реализован механизм исключений.
    • Декорирование имён в C++ (mangling).

    ABI важно, когда приложение использует внешние библиотеки. Если при обновлении библиотеки ABI не меняется, то менять программу не надо. API может остаться тем же, но поменяется ABI. Две версии библиотеки, имеющие один ABI, называют binary compatible (бинарно совместимыми): старую версию библиотеки можно заменить на новую без проблем.

    Иногда без изменений ABI не обойтись. Тогда приходится перекомпилировать зависящие программы. Если ABI библиотеки меняется, а API нет, то версии называют source compatible.

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

    Linux

    Полностью статическая сборка

    Рассмотрим такой простейший код:

    #include int main() { puts("Hello, world!"); return 0; }

    Даже такая программа использует динамические библиотеки. Вы можете просмотреть библиотеки, используемые приложением, через команду ldd.

    $ gcc main.c $ ldd a.out linux-vdso.so.1 => (0x00007ffcecba0000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f324251d000) /lib64/ld-linux-x86-64.so.2 (0x00007f32428e7000) $ ldd a.out linux-vdso.so.1 => (0x00007ffc99b1b000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4bb85f5000) /lib64/ld-linux-x86-64.so.2 (0x00007f4bb89bf000)

    Обратите внимание, что адреса меняются — это ASLR.

    С помощью ключа -static компилятора gcc можно создать статически скомпонованный исполняемый файл.

    $ gcc -static main.c $ ldd a.out not a dynamic executable

    Динамический загрузчик ld-linux.so

    Когда операционная система загружает приложение, скомпонованное динамически, она должна найти и загрузить динамические библиотеки, необходимые для выполнения программы. В ОС Linux эту работу выполняет ld-linux.so.2.

    $ ldd /bin/ls linux-vdso.so.1 => (0x00007fff3f3a5000) libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f0418ac7000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f04186fd000) libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f041848d000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0418289000) /lib64/ld-linux-x86-64.so.2 (0x00007f0418ce9000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f041806c000)

    Когда запускается программа ls, ОС передаёт управление в ld-linux.so.2 вместо нормальной точки входа в приложение. В свою очередь ld-linux.so.2 ищет и загружает требуемые библиотеки, затем передаёт управление на точку старта приложения.

    Справочная страница (man) к ld-linux.so.2 даёт высокоуровневое описание работы динамического компоновщика. По сути это рантайм-компонент компоновщика (ld), который отыскивает и загружает в память динамические библиотеки, используемые приложением. Обычно динамический компоновщик неявно задаётся в процессе компоновки. Спецификация ELF предоставляет функциональность динамической компоновки. Компилятор GCC включает в исполняемые файлы специальный заголовок (program header) под названием INTERP, он указывает путь к динамическому компоновщику.

    $ readelf -l a.out Elf file type is EXEC (Executable file) Entry point 0x400430 There are 9 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x00000000000001f8 0x00000000000001f8 R E 8 INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

    Спецификация гласит, то если присутствует заголовок PT_INTERP, то ОС должна создать образ процесса интерпретатора вместо приложения. Управление передаётся интерпретатору, который отвечает за загрузку динамических библиотек. Спецификация закладывает достаточную гибкость.

    linux-vdso.so.1

    В те времена, когда процессоры с архитектурой x86 только появились, взаимодействие пользовательских приложений со службами операционной системы осуществлялось с помощью прерываний. По мере создания более мощных процессоров эта схема взаимодействия становилась узким местом системы. Во всех процессорах, начиная с Pentium® II, Intel® реализовала механизм быстрых системных вызовов (Fast System Call), в котором вместо прерываний используются инструкции SYSENTER и SYSEXIT, ускоряющие выполнение системных вызовов.

    Библиотека linux-vdso.so.1 является виртуальной библиотекой, или виртуальным динамически разделяемым объектом (VDSO), который размещается только в адресном пространстве отдельной программы. В более ранних системах эта библиотека называлась linux-gate.so.1. Эта виртуальная библиотека содержит всю необходимую логику, обеспечивающую для пользовательских приложений наиболее быстрый доступ к системным функциям в зависимости от архитектуры процессора – либо через прерывания, либо (для большинства современных процессоров) через механизм быстрых системных вызовов.

    Система нумерации версий

    Во всём UNIX-мире принята система нумерации вида major.minor.patchlevel:

    • Мажорная версия библиотеки изменяется всякий раз, когда у неё меняется ABI.
    • Минорная версия изменяется при добавлении в библиотеку новой функциональности без изменения ABI.
    • Patchlevel изменяется при исправлении ошибок без добавления новой функциональности.

    Смена мажорной версии библиотеки — это всегда событие, переход на неё — это всегда трудозатраты.

    Пример

    Пример динамической загрузки

    #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

    Переадресация (relocation)

    Разные программы имеют различные размеры и различный набор подгружаемых динамических библиотек, и если разделяемая библиотека отображается в адресное пространство различных программ, она будет иметь различные адреса. Это в свою очередь означает, что все функции и переменные в библиотеке будут на различных местах. Если все обращения к адресам относительные («значение +1020 байта отсюда») нежели абсолютные («значение в 0x102218BF»), то это не проблема, однако так бывает не всегда. В таких случаях всем абсолютным адресам необходимо прибавить подходящий офсет — это и есть relocation.

    Это практически всегда скрыто от C/C++ программиста — очень редко проблемы компоновки вызваны трудностями переадресации.

    Таблица перемещений (relocation table) — это список указателей, созданный транслятором (компилятором или ассемблером) и хранимый в объектном или исполняемом файле. Каждая запись в таблице, или «fixup», является указателем на абсолютный адрес в объектном коде, который должен быть изменен, когда загрузчик перемещает программу так, чтобы она ссылалась на правильное местоположение. Fixup’ы предназначены для поддержки переноса программы в виде цельной единицы.

    Решение проблемы перемещения

    int myglob = 42; int Foo(int a, int b) { myglob += a; return b + myglob; }

    Скомпилируем объектный файл:

    $ objdump -M intel foo.o -d foo.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 : 0: 55 push rbp 1: 48 89 e5 mov rbp,rsp 4: 89 7d fc mov DWORD PTR [rbp-0x4],edi 7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi a: 8b 15 00 00 00 00 mov edx,DWORD PTR [rip+0x0] # 10 10: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 13: 01 d0 add eax,edx 15: 89 05 00 00 00 00 mov DWORD PTR [rip+0x0],eax # 1b 1b: 8b 15 00 00 00 00 mov edx,DWORD PTR [rip+0x0] # 21 21: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 24: 01 d0 add eax,edx 26: 5d pop rbp 27: c3 ret

    RIP — регистр instruction pointer, указывает на следующую инструкцию.

    Адресация относительно RIP была введена в x86-64 в «длинном» режиме и используется по умолчанию. В старом x86 такая адресация применялась только для инструкций перехода call, jmp, . а теперь стала применяться в гораздо большем числе инструкций.

    Когда потом объектный файл статически линкуется в исполняемый файл, нули превращаются в ненули:

    0000000000400571 : 400571: 55 push rbp 400572: 48 89 e5 mov rbp,rsp 400575: 89 7d fc mov DWORD PTR [rbp-0x4],edi 400578: 89 75 f8 mov DWORD PTR [rbp-0x8],esi 40057b: 8b 15 b7 0a 20 00 mov edx,DWORD PTR [rip+0x200ab7] # 601038 400581: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 400584: 01 d0 add eax,edx 400586: 89 05 ac 0a 20 00 mov DWORD PTR [rip+0x200aac],eax # 601038 40058c: 8b 15 a6 0a 20 00 mov edx,DWORD PTR [rip+0x200aa6] # 601038 400592: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 400595: 01 d0 add eax,edx 400597: 5d pop rbp 400598: c3 ret 400599: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0]

    Но это статическая линковка, а с динамической сложнее.

    Есть два подхода:

    • Load-time relocation
    • Position independent code (PIC)
    Load-time relocation

    На x86-64 метод не применяется.

    • Замедление на стадии загрузки.
    • text-сегмент получается разным в разных копиях библиотеки, то есть не может разделяться между библиотеками, теряется преимущество экономии памяти.
    • text-сегмент доступен на запись (лишняя угроза безопасности)

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

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