неопределенное поведение в разделяемой lib с использованием libpthread, но не в ELF в качестве зависимости - PullRequest
0 голосов
/ 08 июня 2018

При связывании «должным образом» (объяснено далее), оба вызова функции ниже блокируются на неопределенный срок при вызовах pthread, реализующих cv.notify_one и cv.wait_for:

// let's call it odr.cpp, which forms libodr.so

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void Notify() {
  std::chrono::milliseconds(100);
  std::unique_lock<std::mutex> lock(mtx);
  ready = true;
  cv.notify_one();
}

void Get() {
  std::unique_lock<std::mutex> lock(mtx);
  cv.wait_for(lock, std::chrono::milliseconds(300));
}

, когда совместно используемая библиотека используется в следующем приложении:

// let's call it test.cpp, which forms a.out

int main() {
  std::thread thr([&]() {
    std::cout << "Notify\n";
    Notify();
  });

  std::cout << "Before Get\n";
  Get();
  std::cout << "After Get\n";

  thr.join();
}

Проблема воспроизводится только при связывании libodr.so:

  • с g ++
  • с золотым компоновщиком
  • с предоставлением -lpthread в качестве зависимости

со следующими версиями соответствующих инструментов:

  • Linux Mint 18.3 Sylvia
  • binutils 2.26.1-1ubuntu1~16.04.6
  • g++ 4:5.3.1-1ubuntu1
  • libc6:amd64 2.23-0ubuntu10

, поэтому мы получим:

  • __pthread_key_create, определенный как символ WEAK в PLT
  • нет libpthread.so какзависимость в ELF

, как показано здесь:

$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
    10: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __pthread_key_create

С другой стороны, при любом из следующих событий мы не обнаруживаем ошибки:

  • clang ++
  • bfd linker
  • без явного -lpthread
  • -lpthread, но с -Wl,--no-as-needed

примечание: на этот раз мы имеем:

  • NOTYPE и нет libpthread.soЗависимость
  • WEAK и libpthread.so Зависимость

, как показано здесь:

$ clang++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out 
 0x0000000000000001 (NEEDED)             Shared library: [libpthread.so.0]
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
    24: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __pthread_key_create@GLIBC_2.2.5 (7)

$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=bfd -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out 
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
    14: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __pthread_key_create

$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out  0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
    18: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __pthread_key_create

$ g++ -fPIC -shared -o build/libodr.so build/odr.cpp.o -fuse-ld=gold -Wl,--no-as-needed -lpthread && readelf -d build/libodr.so | grep Shared && readelf -Ws build/libodr.so | grep -m1 __pthread_key_create && ./a.out 
 0x0000000000000001 (NEEDED)             Shared library: [libpthread.so.0]
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
    10: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __pthread_key_create@GLIBC_2.2.5 (4)

Полный пример компиляции / запуска можно найти здесь: https://github.com/aurzenligl/study/tree/master/cpp-pthread

Что ломает shlib с помощью pthread, когда __pthread_key_create равен WEAK и нет зависимости libpthread.so в ELF?Принимает ли динамический компоновщик символы pthread из libc.so (заглушки) вместо libpthread.so?

1 Ответ

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

Здесь много чего происходит: различия между gcc и clang, различия между gnu ld и gold, флаг компоновщика --as-needed, два разных режима сбоя и, возможно, даже некоторые проблемы с синхронизацией.

Давайте начнем скак связать программу, используя потоки POSIX.

Флаг -pthread компилятора - все, что вам нужно.Это флаг компилятора, поэтому вы должны использовать его как при компиляции кода, использующего потоки, так и при компоновке окончательного исполняемого файла.Когда вы используете -pthread на этапе компоновки, компилятор автоматически предоставит флаг -lpthread в нужном месте в строке компоновки.

Как правило, вы будете использовать его только при компоновке конечного исполняемого файла., а не при связывании общей библиотеки.Если вы просто хотите сделать поток вашей библиотеки безопасным, но не хотите принудительно заставлять каждую программу, которая использует вашу библиотеку, связываться с pthreads, вам нужно использовать проверку во время выполнения, чтобы проверить, загружена ли библиотека pthreads, и вызватьPthread API, только если это так.В Linux это обычно делается путем проверки «канарейки» - например, сделать слабую ссылку на произвольный символ, такой как __pthread_key_create, который будет определен только при загрузке библиотеки и будет иметь значение 0, еслиПрограмма была связана без нее.

В вашем случае, однако, ваша библиотека libodr.so в значительной степени зависит от потоков, поэтому разумно связать ее с флагом -pthread.

Это приноситПерейдем к первому режиму сбоя: если вы используете g ++ и gold для обоих шагов ссылки, программа выдает std::system_error и говорит, что вам нужно включить многопоточность.Это связано с флагом --as-needed.GCC передает компоновщику --as-needed по умолчанию, а clang (по-видимому) - нет.При --as-needed компоновщик будет записывать только зависимости библиотеки, которые разрешают строгую ссылку.Поскольку все ссылки на API-интерфейсы pthread являются слабыми, ни одной из них недостаточно, чтобы сообщить компоновщику, что libpthread.so следует добавить в список зависимостей (через запись DT_NEEDED в динамической таблице).Изменение clang или добавление флага -Wl,--no-as-needed решает эту проблему, и программа загрузит библиотеку pthread.

Но, подождите, почему вам не нужно делать это при использовании компоновщика Gnu?Он использует то же правило: только сильная ссылка приводит к тому, что библиотека записывается как зависимость.Разница в том, что Gnu ld также рассматривает ссылки из других общих библиотек, в то время как gold рассматривает ссылки только из обычных объектных файлов.Оказывается, что библиотека pthread предоставляет переопределенные определения нескольких символов libc, и существуют строгие ссылки от libstdc++.so на некоторые из этих символов (например, write).Этих сильных ссылок достаточно, чтобы Gnu ld записал libpthread.so в качестве зависимости.Это скорее случайность, чем дизайн;Я не думаю, что изменение золота для учета ссылок из других общих библиотек было бы надежным решением.Я думаю, что правильное решение для GCC - поставить --no-as-needed перед флагом -lpthread, когда вы используете -pthread.

. Возникает вопрос, почему эта проблема не возникает постояннопри использовании потоков POSIX и золотого компоновщика.Но это небольшая тестовая программа;большая программа почти наверняка будет содержать сильные ссылки на некоторые из тех символов libc, которые переопределяются libpthread.so.

Теперь давайте рассмотрим второй режим сбоя, когда и Notify(), и Get() блокируются на неопределенный срок, если выссылка libodr.so с g ++, gold и -lpthread.

В Notify() вы удерживаете блокировку до конца функции, пока вызываете cv.notify_one().Вам действительно нужно только удерживать блокировку, чтобы установить флаг готовности;если мы изменим его так, чтобы мы сняли блокировку до этого, то поток, вызывающий Get(), истечет через 300 мс и не будет блокироваться.Так что это действительно вызов notify_one(), который блокирует, и программа взаимоблокируется, потому что Get() ожидает этой же блокировки.

Так почему же он блокируется, только если __pthread_key_create равно FUNC вместо NOTYPE?Я думаю, что тип символа - красная сельдь, и что настоящая проблема вызвана тем фактом, что золото не записывает версии символов для ссылок, разрешенных библиотекой, которая не добавлена ​​в качестве необходимой библиотеки.Реализация wait_for вызывает pthread_cond_timedwait, которая имеет две версии: libpthread и libc.Возможно, загрузчик привязывает ссылку к неверной версии, что приводит к тупику из-за невозможности разблокировать мьютекс.Я сделал временный патч к золоту для записи этих версий, и это заставило программу работать.К сожалению, это не решение, так как этот патч может привести к сбою ld.so при других обстоятельствах.

Я попытался изменить cv.wait_for(...) на cv.wait(lock, []{ return ready; }), и программа отлично работает во всех сценариях, что также предполагаетпроблема в pthread_cond_timedwait.

Суть в том, что добавление флага --no-as-needed решит проблему в этом очень маленьком тестовом примере.Все, что больше, вероятно, будет работать без дополнительного флага, так как вы увеличите шансы сделать сильную ссылку на символ в libpthread.(Например, добавление вызова к std::this_thread::sleep_for в любом месте в odr.cpp добавляет сильную ссылку на nanosleep, что ставит libpthread в нужный список.)

Обновление: Я убедился, что сбойная программа ссылается на неправильную версию pthread_cond_timedwait.Для glibc 2.3.2 тип pthread_cond_t был изменен, а старые версии API, использующие этот тип, были изменены для динамического выделения новой (большей) структуры и сохранения указателя на нее в исходном типе.Так что теперь, если поток потребления достигает cv.wait_for до того, как поток производства достигает cv.notify_one, реализация cv.wait_for вызывает старую версию pthread_cond_timedwait, которая инициализирует то, что она считает старой pthread_cond_t в cvс указателем на новый pthread_cond_t.После этого, когда другой поток достигает cv.notify_one, его реализация предполагает, что cv содержит pthread_cond_t нового стиля, а не указатель на один, поэтому он вызывает pthread_mutex_lock с указателем на новый pthread_cond_t вместоуказателя на мьютекс.Он блокирует этот потенциальный мьютекс, но он никогда не разблокируется, потому что другой поток разблокирует настоящий мьютекс.

...