Как сделать привязку во время выполнения на основе возможностей процессора в Linux

Возможно ли, чтобы библиотека Linux (например, «libloader.so») загружала другую библиотеку для разрешения любых внешних символов?

У меня есть целая куча кода, который условно компилируется для поддержки уровня SIMD (SSE2, AVX, AVX2). Это прекрасно работает, если платформа сборки совпадает с платформой времени выполнения. Но это препятствует повторному использованию процессоров разных поколений.

Одна мысль состоит в том, чтобы иметь executable, который вызывает function ссылку на libloader.so, который не реализует напрямую function. Скорее, он разрешает (привязывает?) этот символ из другой загруженной библиотеки, например. libimpl_sse2.so, libimpl_avx2.so или так далее в зависимости от cpuflags.

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

Я дошел до создания исполняемого файла, который собирается и запускается с неразрешенными внешними символами (UES) с помощью флага ld --unresolved-symbols=ignore-all. Но последующая загрузка библиотеки impl не меняет значение функции UES с NULL.


person Mark Borgerding    schedule 08.04.2015    source источник
comment
Это может быть неприменимо к вашему случаю, но с компилятором Intel вы можете добиться этого, используя семейство флагов -ax* (т.е. если вы хотите, чтобы пути кода SSE и AVX были доступны - используйте -axAVX во время компиляции).   -  person zam    schedule 12.04.2015
comment
@zam, спасибо, но -ax, похоже, применяется только к автоматической (генерируемой компилятором) оптимизации для Intel. Мне нужно решение, позволяющее компилировать пути #ifdef отдельно. Он также должен работать с gcc.   -  person Mark Borgerding    schedule 12.04.2015


Ответы (1)


Редактировать: позже я узнал, что описанный ниже метод работает только в ограниченном числе случаев. В частности, ваши общие библиотеки должны содержать только функции без каких-либо глобальных переменных. Если внутри библиотек, к которым вы хотите выполнить диспетчеризацию, есть глобальные переменные, вы получите ошибку динамического компоновщика во время выполнения. Это происходит поскольку глобальные переменные перемещаются до вызова конструкторов разделяемой библиотеки< /а>. Таким образом, компоновщик должен разрешить эти ссылки заранее, прежде чем описанная здесь схема диспетчеризации сможет запуститься.


Один из способов добиться того, чего вы хотите, - это (ab) использовать поле DT_SONAME в заголовке ELF вашей общей библиотеки. Это можно использовать для изменения имени файла, который динамический загрузчик (ld-linux-so*) загружает во время выполнения, чтобы устранить зависимость от общей библиотеки. Это лучше всего объяснить на примере. Скажем, я компилирую разделяемую библиотеку libtest.so с помощью следующей командной строки:

g++ test.cc -shared -o libtest.so -Wl,-soname,libtest_dispatch.so

Это создаст разделяемую библиотеку с именем файла libtest.so, но в поле DT_SONAME установлено значение libtest_dispatch.so. Давайте посмотрим, что произойдет, когда мы свяжем с ним программу:

g++ testprog.cc -o test -ltest

Давайте рассмотрим зависимости библиотеки времени выполнения для получившегося двоичного файла приложения test:

> ldd test
linux-vdso.so.1 =>  (0x00007fffcc5fe000)
libtest_dispatch.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd1e4a55000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd1e4e4f000)

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

  • Создавайте различные версии вашей общей библиотеки. Я предполагаю, что существует какая-то «общая» версия, которую всегда можно использовать, а другие оптимизированные версии используются во время выполнения по мере необходимости. Я бы назвал универсальную версию «простым» именем библиотеки libtest.so, а остальные назвал бы так, как вы выберете (например, libtest_sse2.so, libtest_avx.so и т. д.).

  • При связывании универсальной версии библиотеки замените ее DT_SONAME на что-то другое, например libtest_dispatch.so.

  • Создайте библиотеку диспетчера с именем libtest_dispatch.so. Когда диспетчер загружается при запуске приложения, он отвечает за загрузку соответствующей реализации библиотеки. Вот псевдокод того, как может выглядеть реализация libtest_dispatch.so:

    #include <dlfcn.h>
    #include <stdlib.h>
    
    // the __attribute__ ensures that this function is called when the library is loaded
    __attribute__((constructor)) void init()
    {
        // manually load the appropriate shared library based upon what the CPU supports
        // at runtime
        if (avx_is_available) dlopen("libtest_avx.so", RTLD_NOW | RTLD_GLOBAL);
        else if (sse2_is_available) dlopen("libtest_sse2.so", RTLD_NOW | RTLD_GLOBAL);
        else dlopen("libtest.so", RTLD_NOW | RTLD_GLOBAL);
        // NOTE: this is just an example; you should check the return values from 
        // dlopen() above and handle errors accordingly
    }
    
  • Связывая приложение с вашей библиотекой, свяжите его с «ванильным» libtest.so, у которого DT_SONAME переопределено, чтобы указать на библиотеку диспетчера. Это делает диспетчеризацию практически прозрачной для любых авторов приложений, использующих вашу библиотеку.

Это должно работать, как описано выше, в Linux. В Mac OS общие библиотеки имеют «установочное имя», аналогичное DT_SONAME, используемому в общих библиотеках ELF, поэтому вместо этого можно использовать процесс, очень похожий на описанный выше. Я не уверен, можно ли использовать что-то подобное в Windows.

Примечание. Выше было сделано одно важное допущение: совместимость ABI между различными реализациями библиотеки. То есть ваша библиотека должна быть спроектирована таким образом, чтобы было безопасно связываться с наиболее общей версией во время компоновки, используя оптимизированную версию (например, libtest_avx.so) во время выполнения.

person Jason R    schedule 10.04.2015
comment
Отличный ответ. Ваш дар решать проблемы сравним только с вашей способностью объяснять. - person Mark Borgerding; 10.04.2015
comment
Одно замечание: в Mac OS, когда вы связываете свое приложение с libtest.so, вам необходимо указать флаг компоновщика -flat_namespace. Поведение по умолчанию требует, чтобы символы поступали из библиотеки, из которой они должны быть получены. В данном случае это означает, что libtest_dispatch.so будет отвечать за предоставление всех символов. Если вы включите плоские пространства имен, тогда он привяжет символы к другой библиотеке, которую загружает диспетчер, что, как указано на странице руководства ld, больше похоже на поведение в других операционных системах (например, Linux). - person Jason R; 10.04.2015