использование std :: thread в библиотеке, загруженной с помощью dlopen, приводит к сигсеву - PullRequest
0 голосов
/ 06 июля 2018

Недавно я обнаружил странное поведение, используя std::thread и dlopen.

В основном, когда я выполняю std::thread в библиотеке, которая загружается с помощью dlopen, я получаю сигсев.Сама библиотека связана с pthread, исполняемый файл, который вызывает dlopen, не является.

Как только я связываю исполняемый файл с pthread или с самой библиотекой, все работает нормально.Однако мы используем инфраструктуру на основе плагинов, где мы не знаем, связано ли само приложение с pthread или нет.Поэтому нельзя всегда связывать исполняемый файл с pthread.

Пожалуйста, найдите прикрепленный код, чтобы воспроизвести проблему.В настоящее время я не уверен, что является причиной проблемы.Это проблема gcc, glibc, libstdc ++ или ld.so?Есть ли удобный способ обойти это?Похоже, что ошибка glibc связана, но я использую glibc2.27 (тестирование Debian).

Кажется, что сам вызов pthread_create из библиотеки работает.

hello.cpp

#include <thread>
#include <iostream>

void thread()
{
    std::thread t ([](){std::cout << "hello world" << std::endl;});
    t.join();
}

extern "C" {
    void hello()
    {
        thread();
    }
}

example.cpp

#include <iostream>
#include <dlfcn.h>

/** code from https://www.tldp.org/HOWTO/html_single/C++-dlopen/
*/
int main() {

    std::cout << "C++ dlopen demo\n\n";

    // open the library
    std::cout << "Opening hello.so...\n";
    void* handle = dlopen("./libhello.so", RTLD_LAZY);

    if (!handle) {
        std::cerr << "Cannot open library: " << dlerror() << '\n';
        return 1;
    }

    // load the symbol
    std::cout << "Loading symbol hello...\n";
    typedef void (*hello_t)();

    // reset errors
    dlerror();
    hello_t hello = (hello_t) dlsym(handle, "hello");
    const char *dlsym_error = dlerror();
    if (dlsym_error) {
        std::cerr << "Cannot load symbol 'hello': " << dlsym_error <<
            '\n';
        dlclose(handle);
        return 1;
    }

    // use it to do the calculation
    std::cout << "Calling hello...\n";
    hello();

    // close the library
    std::cout << "Closing library...\n";
    dlclose(handle);
}

build.sh (сборка и выполнение верхнего примера. Сбой примера 1)

#!/bin/bash

echo "g++ -shared -fPIC -std=c++14 hello.cpp -o libhello.so -pthread"
g++ -shared -fPIC -std=c++14 hello.cpp -o libhello.so -pthread

echo "g++ example.cpp -o example1 -ldl"
g++ example.cpp -o example1 -ldl

echo "g++ example.cpp -o example2 -ldl -pthread"
g++ example.cpp -o example2 -ldl -pthread

echo "g++ example.cpp -o example3 -ldl -lhello -L ./"
g++ example.cpp -o example3 -ldl -lhello -L ./

export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:$(pwd)

echo "===== example1 ====="
./example1
echo "===== end      ====="

echo "===== example2 ====="
./example2
echo "===== end      ====="

echo "===== example3 ====="
./example3
echo "===== end      ====="

EDIT

Я забыл упомянуть: если я запускаю ошибочный пример (например, пример 1) с использованием LD_DEBUG=all, программа вылетает при поиске pthread_create.Еще интереснее то, что прежний поиск pthread_create завершается успешно:

  8111:     symbol=_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_;  lookup in file=/usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]
  8111:     binding file ./libhello.so [0] to /usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]: normal symbol `_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_' [GLIBCXX_3.4]
  8111:     symbol=pthread_create;  lookup in file=./example1 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libdl.so.2 [0]
  8111:     symbol=pthread_create;  lookup in file=/usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libm.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libgcc_s.so.1 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib64/ld-linux-x86-64.so.2 [0]
  8111:     symbol=pthread_create;  lookup in file=./libhello.so [0]
  8111:     symbol=pthread_create;  lookup in file=/usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libm.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libgcc_s.so.1 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libpthread.so.0 [0]
  8111:     binding file ./libhello.so [0] to /lib/x86_64-linux-gnu/libpthread.so.0 [0]: normal symbol `pthread_create' [GLIBC_2.2.5]
  8111:     symbol=_ZTVNSt6thread6_StateE;  lookup in file=./example1 [0]
  8111:     symbol=_ZTVNSt6thread6_StateE;  lookup in file=/lib/x86_64-linux-gnu/libdl.so.2 [0]
  8111:     symbol=_ZTVNSt6thread6_StateE;  lookup in file=/usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]
  8111:     binding file ./libhello.so [0] to /usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]: normal symbol `_ZTVNSt6thread6_StateE' [GLIBCXX_3.4.22]
  ...
  8111:     binding file ./libhello.so [0] to ./libhello.so [0]: normal symbol `_ZNSt10_Head_baseILm0EPNSt6thread6_StateELb0EE7_M_headERS3_'
  8111:     symbol=_ZNSt6thread15_M_start_threadESt10unique_ptrINS_6_StateESt14default_deleteIS1_EEPFvvE;  lookup in file=./example1 [0]
  8111:     symbol=_ZNSt6thread15_M_start_threadESt10unique_ptrINS_6_StateESt14default_deleteIS1_EEPFvvE;  lookup in file=/lib/x86_64-linux-gnu/libdl.so.2 [0]
  8111:     symbol=_ZNSt6thread15_M_start_threadESt10unique_ptrINS_6_StateESt14default_deleteIS1_EEPFvvE;  lookup in file=/usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]
  8111:     binding file ./libhello.so [0] to /usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]: normal symbol `_ZNSt6thread15_M_start_threadESt10unique_ptrINS_6_StateESt14default_deleteIS1_EEPFvvE' [GLIBCXX_3.4.22]
  8111:     symbol=pthread_create;  lookup in file=./example1 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libdl.so.2 [0]
  8111:     symbol=pthread_create;  lookup in file=/usr/lib/x86_64-linux-gnu/libstdc++.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libm.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libgcc_s.so.1 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
  8111:     symbol=pthread_create;  lookup in file=/lib64/ld-linux-x86-64.so.2 [0]
  ./build.sh: line 18:  8111 Segmentation fault      (core dumped) LD_DEBUG=all ./example1
  ===== end      =====

Ответы [ 3 ]

0 голосов
/ 09 июля 2018

Я могу предложить некоторую предысторию относительно того, почему существует segfault, но, к сожалению, нет решения.

Кажется, что это проблема с libstdc++: Технически эта огромная монолитная библиотека зависит от libpthread,но по веским причинам они не ссылаются на libpthread.Теперь, чтобы иметь возможность загружать libstdc++ из программ, которые вообще не используют потоки, пропущенные символы (например, pthread_create) должны где-то появляться.Поэтому libstdc++ определяет их как слабые символы.

Эти слабые символы также используются для определения во время выполнения, действительно ли загружен libpthread.Для старого ABI была даже проверка в _M_start_thread, которая вызвала значимое исключение, если pthread не был загружен вместо вызова слабо определенного nullptr - что-то, чего я не желал бы при моей худшей энсемии.

К сожалению, проверка во время выполнения была потеряна для нового ABI.Вместо этого существует проверка времени соединения для pthread_create путем создания зависимости при компиляции кода, вызывающего _M_start_thread, и передачи указателя на pthread_create в эту функцию.К сожалению, этот указатель отбрасывается и используется все еще слабый указатель nullptr.

Теперь что-то во время связывания / загрузки приводит к тому, что слабо определенный pthread_create не будет переопределен в вашем проблемном случае.Я не уверен относительно точных правил разрешения, которые применяются там - я предполагаю, что это связано с тем, что libstdc++ уже полностью загружен, когда загружается libpthread.Я был бы рад, если бы любой дополнительный ответ прояснил это.К сожалению, кажется, что вообще нет жизнеспособного варианта исправить это, кроме связывания основного приложения с -lpthread или LD_PRELOAD=libpthread.so (что я бы не рекомендовал).

0 голосов
/ 09 июля 2018

Проблема заключается в libstdc ++.

  • С программами на Си этого не происходит.
  • С программами на C ++, созданными на libc ++, этого тоже не происходит.
  • С программами на C ++, созданными с использованием libstdc ++ статически , этого также не происходит.
  • При сборке библиотек с использованием libc ++ этого не происходит, даже если вызывающая программа динамически создается с использованием libstdc ++.
  • Когда программа дублирует библиотеку с помощью RTLD_GLOBAL, этого также не происходит.

Так что одним из решений было бы переключиться на libc ++. Очевидно, это работает только в том случае, если вы никогда не экспортируете какой-либо интерфейс, который использует какой-либо тип std::. В частности, библиотека, которая экспортирует только C-совместимые интерфейсы, должна быть в порядке.

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

Параллельно нужно сообщить об ошибке в libstdc ++ и дождаться исправления. Нет никаких причин, почему это должно быть сломано таким образом.

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

Наконец, всегда есть отвратительный обходной путь предварительной загрузки libpthread в вызывающей программе.

0 голосов
/ 08 июля 2018

Как только я связываю исполняемый файл с pthread или самой библиотекой, все работает нормально. Однако мы используем инфраструктуру на основе плагинов, где мы не знаем, связано ли само приложение с pthread или нет. Поэтому нельзя всегда связывать исполняемый файл с pthread.

Наоборот : очень немногие системы поддерживают приложение, которое становится «внезапно многопоточным» (ваша система, очевидно, этого не делает).

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

Это проблема gcc, glibc, libstdc ++ или ld.so

Это проблема с libstdc++ - GLIBC поддерживает «неожиданно многопоточное» выполнение, GCC вообще не является частью среды выполнения, а ld.so является частью GLIBC.

...