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

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

История

Динамическая загрузка была распространенной техникой для операционных систем IBM для System / 360, таких как OS / 360 , особенно для подпрограмм ввода-вывода , а также для библиотек времени выполнения COBOL и PL / I , и продолжает использоваться в операционных системах IBM для z / Architecture. , например z / OS . Что касается прикладного программиста, загрузка в значительной степени прозрачна, поскольку она в основном обрабатывается операционной системой (или ее подсистемой ввода-вывода). Основные преимущества:

  • Исправления ( патчи ) подсистем фиксировали сразу все программы, без необходимости повторно связывать их
  • Библиотеки могут быть защищены от несанкционированного изменения

Система обработки стратегических транзакций IBM , CICS (1970-е гг.), Широко использует динамическую загрузку как для ядра, так и для обычной загрузки прикладных программ . Исправления в прикладных программах можно было делать в автономном режиме, а новые копии измененных программ загружались динамически без необходимости перезапуска CICS (который может работать и часто работает круглосуточно , 7 дней в неделю ).

Общие библиотеки были добавлены в Unix в 1980-х годах, но изначально не позволяли программе загружать дополнительные библиотеки после запуска.

Использует

Динамическая загрузка наиболее часто используется при реализации программных плагинов . Например, файлы подключаемых модулей «динамических общих объектов» веб-сервера Apache *.dso представляют собой библиотеки, которые загружаются во время выполнения с динамической загрузкой. Динамическая загрузка также используется при реализации компьютерных программ, где несколько разных библиотек могут предоставлять необходимые функции и где пользователь имеет возможность выбрать, какую библиотеку или библиотеки предоставить.

В C / C ++

Не все системы поддерживают динамическую загрузку. UNIX-подобные операционные системы, такие как macOS , Linux и Solaris, обеспечивают динамическую загрузку с помощью библиотеки языка программирования C «dl». Для Windows операционная система обеспечивает динамическую загрузку через Windows API , .

Резюме

Имя Стандартный POSIX / UNIX API Microsoft Windows API
Включение файла заголовка #include <dlfcn.h> #include <windows.h>
Определения заголовка dl

( libdl.so, libdl.dylibи т. д. в зависимости от ОС )

kernel32.dll
Загрузка библиотеки dlopen LoadLibrary
LoadLibraryEx
Извлечение содержимого dlsym GetProcAddress
Выгрузка библиотеки dlclose FreeLibrary

Загрузка библиотеки

Загрузка библиотеки выполняется в WindowsLoadLibrary или LoadLibraryExв Windows, а также dlopenв UNIX-подобных операционных системах . Ниже приведены примеры:

Большинство UNIX-подобных операционных систем (Solaris, Linux, * BSD и т. Д.)

void* sdl_library = dlopen("libSDL.so", RTLD_LAZY);
if (sdl_library == NULL) {
   // report error ...
} else {
   // use the result in a call to dlsym
}

macOS

В качестве библиотеки UNIX :

void* sdl_library = dlopen("libSDL.dylib", RTLD_LAZY);
if (sdl_library == NULL) {
   // report error ...
} else {
   // use the result in a call to dlsym
}

В качестве фреймворка macOS :

void* sdl_library = dlopen("/Library/Frameworks/SDL.framework/SDL", RTLD_LAZY);
if (sdl_library == NULL) {
   // report error ...
} else {
   // use the result in a call to dlsym
}

Или, если фреймворк или пакет содержит код Objective-C:

NSBundle *bundle = [NSBundle bundleWithPath:@"/Library/Plugins/Plugin.bundle"];
NSError *err = nil;
if ([bundle loadAndReturnError:&err])
{
    // Use the classes and functions in the bundle.
}
else
{
    // Handle error.
}

Окна

HMODULE sdl_library = LoadLibrary(TEXT("SDL.dll"));
if (sdl_library == NULL) {
   // report error ...
} else {
   // use the result in a call to GetProcAddress
}

Извлечение содержимого библиотеки

Извлечение содержимого динамически загружаемой библиотеки достигается GetProcAddressна Windows , и dlsymна UNIX - подобных операционных систем .

UNIX-подобные операционные системы (Solaris, Linux, * BSD, macOS и т. Д.)

void* initializer = dlsym(sdl_library,"SDL_Init");
if (initializer == NULL) {
   // report error ...
} else {
   // cast initializer to its proper type and use
}

В macOS при использовании пакетов Objective-C также можно:

Class rootClass = [bundle principalClass]; // Alternatively, NSClassFromString() can be used to obtain a class by name.
if (rootClass)
{
    id object = [[rootClass alloc] init]; // Use the object.
}
else
{
    // Report error.
}

Окна

FARPROC initializer = GetProcAddress(sdl_library,"SDL_Init");
if (initializer == NULL) {
   // report error ...
} else {
   // cast initializer to its proper type and use
}

Преобразование указателя библиотечной функции

Результат dlsym()или GetProcAddress()должен быть преобразован в указатель соответствующего типа, прежде чем его можно будет использовать.

Окна

В Windows преобразование выполняется просто, поскольку FARPROC по сути уже является указателем на функцию :

typedef INT_PTR (*FARPROC)(void);

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

typedef void (*sdl_init_function_type)(void);
sdl_init_function_type init_func = (sdl_init_function_type) initializer;

UNIX (POSIX)

Согласно спецификации POSIX результатом dlsym()является voidуказатель. Однако не требуется, чтобы указатель функции имел даже тот же размер, что и указатель объекта данных, и поэтому допустимое преобразование между типом void*и указателем на функцию может быть нелегко реализовать на всех платформах.

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

typedef void (*sdl_init_function_type)(void);
sdl_init_function_type init_func = (sdl_init_function_type)initializer;

Выше фрагмент кода будет выдавать предупреждение о некоторых компиляторах: warning: dereferencing type-punned pointer will break strict-aliasing rules. Другой обходной путь:

typedef void (*sdl_init_function_type)(void);
union { sdl_init_function_type func; void * obj; } alias;
alias.obj = initializer;
sdl_init_function_type init_func = alias.func;

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

Решение проблемы указателя на функцию в системах POSIX

Факт остается фактом: любое преобразование между указателями на функции и объекты данных следует рассматривать как расширение реализации (по своей сути непереносимое), и что «правильного» способа прямого преобразования не существует, поскольку в этом отношении стандарты POSIX и ISO противоречат друг с другом.

Из-за этой проблемы в документации POSIX по dlsym()устаревшей проблеме 6 говорится, что «будущая версия может либо добавить новую функцию для возврата указателей функций, либо текущий интерфейс может быть устаревшим в пользу двух новых функций: одна, которая возвращает указатели данных. а другой возвращает указатели на функции ".

В следующей версии стандарта (выпуск 7, 2008 г.) проблема обсуждалась, и был сделан вывод о том, что указатели функций должны быть преобразованы в void*соответствие с POSIX. Это требует от производителей компилятора реализации рабочего приведения для этого случая.

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

Выгрузка библиотеки

Загрузка библиотеки приводит к выделению памяти; библиотека должна быть освобождена, чтобы избежать утечки памяти . Кроме того, невозможность выгрузки библиотеки может помешать операциям файловой системы с файлом, который содержит библиотеку. Выгрузка библиотеки выполняется как FreeLibraryв Windows, так и dlcloseв UNIX-подобных операционных системах . Однако выгрузка DLL может привести к сбою программы, если объекты в основном приложении ссылаются на память, выделенную в DLL. Например, если DLL представляет новый класс, а DLL закрывается, дальнейшие операции с экземплярами этого класса из основного приложения, скорее всего, вызовут нарушение доступа к памяти. Аналогичным образом, если DLL представляет фабричную функцию для создания экземпляров динамически загружаемых классов, вызов или разыменование этой функции после закрытия DLL приводит к неопределенному поведению.

UNIX-подобные операционные системы (Solaris, Linux, * BSD, macOS и т. Д.)

dlclose(sdl_library);

Окна

FreeLibrary(sdl_library);

Специальная библиотека

Реализации динамической загрузки в UNIX-подобных операционных системах и Windows позволяют программистам извлекать символы из текущего выполняемого процесса.

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

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

UNIX-подобные операционные системы (Solaris, Linux, * BSD, macOS и т. Д.)

void* this_process = dlopen(NULL,0);

Окна

HMODULE this_process = GetModuleHandle(NULL);

HMODULE this_process_again;
GetModuleHandleEx(0,0,&this_process_again);

В Java

В языке программирования Java , классы могут быть загружены динамически с помощью ClassLoaderобъекта. Например:

Class type = ClassLoader.getSystemClassLoader().loadClass(name);
Object obj = type.newInstance();

Механизм отражения также предоставляет средства для загрузки класса, если он еще не загружен. Он использует загрузчик классов текущего класса:

Class type = Class.forName(name);
Object obj = type.newInstance();

Однако нет простого способа выгрузить класс контролируемым образом. Загруженные классы могут быть выгружены только контролируемым образом, т. Е. Когда программист хочет, чтобы это произошло, если загрузчик классов, используемый для загрузки класса, не является загрузчиком системного класса, а сам выгружается. При этом необходимо учитывать различные детали, чтобы гарантировать, что класс действительно выгружен. Это делает выгрузку классов утомительной.

Неявная выгрузка классов, т.е. неконтролируемая сборщиком мусора, несколько раз менялась в Java. До Java 1.2. сборщик мусора мог выгружать класс всякий раз, когда чувствовал, что ему нужно пространство, независимо от того, какой загрузчик классов использовался для загрузки класса. Начиная с Java 1.2 классы, загруженные через системный загрузчик классов, никогда не выгружались, а классы загружались через другие загрузчики классов только тогда, когда этот другой загрузчик классов был выгружен. Начиная с Java 6 классы могут содержать внутренний маркер, указывающий сборщику мусора, что они могут быть выгружены, если сборщик мусора пожелает это сделать, независимо от загрузчика классов, используемого для загрузки класса. Сборщик мусора может проигнорировать эту подсказку.

Точно так же библиотеки, реализующие собственные методы, динамически загружаются с использованием System.loadLibraryметода. Нет никакого System.unloadLibraryметода.

Платформы без динамической загрузки

Несмотря на его распространение в 1980-х годах через UNIX и Windows, некоторые системы все же предпочли не добавлять или даже не удалять динамическую загрузку. Например, Plan 9 от Bell Labs и его преемник 9front намеренно избегают динамического связывания, поскольку считают его «вредным». Язык программирования Go , разработанный некоторыми из тех же разработчиков, что и Plan 9, также не поддерживает динамическое связывание, но загрузка плагинов доступна с Go 1.8 (февраль 2017 г.). Среда выполнения Go и любые библиотечные функции статически связаны в скомпилированный двоичный файл.

Смотрите также

использованная литература

дальнейшее чтение

  • Зильбершац, Авраам; Гэлвин, Питер Баер; Гань, Грег (2005). «Глава 8.1.4« Динамическая загрузка »и Глава 8.1.5« Динамическое связывание и разделяемые библиотеки » ». Понятия операционной системы . J. Wiley & Sons . ISBN 978-0-471-69466-3.

внешние ссылки