Зачем dlopen повторно использовать адрес ранее загруженного символа? - PullRequest
0 голосов
/ 30 сентября 2019

Я только что отладил странную проблему, когда у меня есть две библиотеки, назовем это libA.so и libB.so

Приложение dlopens libA.so (РЕДАКТИРОВАТЬ: это не так, этосвязан с опцией -l), которая является тонкой библиотекой, которая затем загружает libB.so, который является фактической реализацией.

dlopen вызывается с использованием опции RTLD_NOW, другие опции не передаются.

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

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

Если это важно, эта переменная определена глубоко в файле .cpp, я не уверен, что связь между C и C ++разные.

Чтение документации dlopen там написано:

RTLD_GLOBAL

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

RTLD_LOCAL

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

Таким образом, RTLD_LOCAL должен быть значением по умолчанию, то есть символы libA не должны использоваться при разрешении символов libB. Но это все еще происходит. Почему?

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

EDIT2:

Пример источника:

commonvar.h:

#pragma once

#include <iostream>

struct A
{
    A()
    {
        std::cout << "A inited. Address: " << this << "\n";
    }
    virtual ~A() {}
};

extern A object;

struct POD
{
    int x, y, z;
};

extern POD pod;

commonvar.cpp:

#include <string>
#include "commonvar.h"

A object;

POD pod = {1, 2, 3};

ах:

#pragma once

extern "C" void foo();

a.cpp:

#include <iostream>
#include "commonvar.h"

using FnFoo = void (*)();

extern "C" void foo()
{
    std::cout << "A called.\n";
    std::cout << "A: Address of foo  is: " << &object << "\n";
    std::cout << "A: Address of pod  is: " << &pod << "\n";
    std::cout << "A: {" << pod.x << ", " << pod.y << ", " << pod.z << "}\n";

    pod.x = 42;
}

b.cpp:

#include <iostream>
#include <string>
#include "commonvar.h"

extern "C" void foo()
{
    std::cout << "B called.\n";
    std::cout << "B: Address of foo  is: " << &object << "\n";
    std::cout << "B: Address of pod  is: " << &pod << "\n";
    std::cout << "B: {" << pod.x << ", " << pod.y << ", " << pod.z << "}\n";
}

main.cpp:

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

#include "a.h"

using FnFoo = void (*)();

int main()
{
    std::cout << "Start of program.\n";
    foo();

    std::cout << "Loading B\n";
    void *b = dlopen("libb.so", RTLD_NOW);
    assert(b);
    FnFoo fnB;
    fnB = FnFoo(dlsym(b, "foo"));
    assert(fnB);

    fnB();
}

Сценарий сборки:

#!/bin/bash

g++ -fPIC -c commonvar.cpp
ar rcs common.a commonvar.o
g++ -fPIC -shared a.cpp common.a -o liba.so
g++ -fPIC -shared b.cpp common.a -o libb.so
g++ main.cpp liba.so -ldl -o main

Динамические символы main:

                U __assert_fail
0000000000202010 B __bss_start
                 U __cxa_atexit
                 w __cxa_finalize
                 U dlopen
                 U dlsym
0000000000202010 D _edata
0000000000202138 B _end
0000000000000bc4 T _fini
                 U foo
                 w __gmon_start__
0000000000000860 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 U __libc_start_main
                 U _ZNSt8ios_base4InitC1Ev
                 U _ZNSt8ios_base4InitD1Ev
0000000000202020 B _ZSt4cout
                 U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc

Динамические символы liba.so:

0000000000202064 B __bss_start
                 U __cxa_atexit
                 w __cxa_finalize
0000000000202064 D _edata
0000000000202080 B _end
0000000000000e6c T _fini
0000000000000bba T foo
                 w __gmon_start__
0000000000000a30 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000202070 B object
0000000000202058 D pod
                 U _ZdlPvm
0000000000000dca W _ZN1AC1Ev
0000000000000dca W _ZN1AC2Ev
0000000000000e40 W _ZN1AD0Ev
0000000000000e22 W _ZN1AD1Ev
0000000000000e22 W _ZN1AD2Ev
                 U _ZNSolsEi
                 U _ZNSolsEPKv
                 U _ZNSt8ios_base4InitC1Ev
                 U _ZNSt8ios_base4InitD1Ev
                 U _ZSt4cout
                 U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
0000000000201dd0 V _ZTI1A
0000000000000ed5 V _ZTS1A
0000000000201db0 V _ZTV1A
                 U _ZTVN10__cxxabiv117__class_type_infoE

Динамические символы libb.so:

$ nm -D libb.so
0000000000202064 B __bss_start
                 U __cxa_atexit
                 w __cxa_finalize
0000000000202064 D _edata
0000000000202080 B _end
0000000000000e60 T _fini
0000000000000bba T foo
                 w __gmon_start__
0000000000000a30 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000202070 B object
0000000000202058 D pod
                 U _ZdlPvm
0000000000000dbe W _ZN1AC1Ev
0000000000000dbe W _ZN1AC2Ev
0000000000000e34 W _ZN1AD0Ev
0000000000000e16 W _ZN1AD1Ev
0000000000000e16 W _ZN1AD2Ev
                 U _ZNSolsEi
                 U _ZNSolsEPKv
                 U _ZNSt8ios_base4InitC1Ev
                 U _ZNSt8ios_base4InitD1Ev
                 U _ZSt4cout
                 U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
0000000000201dd0 V _ZTI1A
0000000000000ec9 V _ZTS1A
0000000000201db0 V _ZTV1A
                 U _ZTVN10__cxxabiv117__class_type_infoE

Вывод:

A inited. Address: 0x7efd6cf97070
Start of program.
A called.
A: Address of foo  is: 0x7efd6cf97070
A: Address of pod  is: 0x7efd6cf97058
A: {1, 2, 3}
Loading B
A inited. Address: 0x7efd6cf97070
B called.
B: Address of foo  is: 0x7efd6cf97070
B: Address of pod  is: 0x7efd6cf97058
B: {42, 2, 3}

Как видно, адреса переменных конфликтуют, а адрес функции - нет.

Более того, инициализация C ++ своеобразна: агрегаты переменной pod инициализируются только после того, как выможно увидеть, что вызов foo () изменяет его, но когда загружается B, он не инициализирует его заново, а вызывает конструктор для полного объекта при загрузке libb.so.

Ответы [ 2 ]

1 голос
/ 01 октября 2019

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

nm -D a.out | grep ' mangled_name_of_the_symbol'

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

Демонстрация:

// main.c
#include <assert.h>
#include <dlfcn.h>
#include <stdio.h>

int foo = 12;

int main()
{
  printf("main: &foo = %p, foo = %d\n", &foo, foo);
  void *h = dlopen("./foo.so", RTLD_NOW);
  assert (h != NULL);
  void (*fn)(void) = (void (*)()) dlsym(h, "fn");
  fn();

  return 0;
}
// foo.c
#include <assert.h>
#include <dlfcn.h>
#include <stdio.h>

int foo = 42;

void fn()
{
  printf("foo:  &foo = %p, foo = %d\n", &foo, foo);
  void *h = dlopen("./bar.so", RTLD_NOW);
  assert (h != NULL);

  void (*fn)(void) = (void (*)()) dlsym(h, "fn");
  fn();
}
// bar.c
#include <stdio.h>

int foo = 24;

void fn()
{
  printf("bar:  &foo = %p, foo = %d\n", &foo, foo);
}

Создайте это с помощью:

gcc -fPIC -shared -o foo.so foo.c && gcc -fPIC -shared -o bar.so bar.c &&
gcc main.c -ldl && ./a.out

Вывод:

main: &foo = 0x5618f1d61048, foo = 12
foo:  &foo = 0x7faad6955040, foo = 42
bar:  &foo = 0x7faad6950028, foo = 24

Теперь перестройте только основной двоичный файл с помощью -rdynamic (что приводит к экспорту foo из него): gcc main.c -ldl -rdynamic. Выходные данные изменятся на:

main: &foo = 0x55ced88f1048, foo = 12
foo:  &foo = 0x55ced88f1048, foo = 12
bar:  &foo = 0x55ced88f1048, foo = 12

PS. Вы можете получить представление о поведении динамического компоновщика, выполнив:

LD_DEBUG=symbols,bindings ./a.out

Обновление:

Оказывается, я задал неправильный вопрос ... Добавлен пример исходного кода.

Если вы посмотрите на вывод LD_DEBUG, вы увидите:

    165089: symbol=object;  lookup in file=./main [0]
    165089: symbol=object;  lookup in file=./liba.so [0]
    165089: binding file ./liba.so [0] to ./liba.so [0]: normal symbol `object'
    165089: symbol=object;  lookup in file=./main [0]
    165089: symbol=object;  lookup in file=./liba.so [0]
    165089: binding file ./libb.so [0] to ./liba.so [0]: normal symbol `object'

Что это означает: liba.so находится в глобальном списке поиска (в силу того, что он был напрямую связан с main). Это примерно эквивалентно выполнению dlopen("./liba.so", RTLD_GLOBAL).

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

0 голосов
/ 02 октября 2019

Возможное решение этой проблемы - использование флага dlopen RTLD_DEEPBIND (однако, это специфично для Linux, а не стандарта POSIX), что заставит загруженную библиотеку пытаться разрешить символы против себя (и своих собственных зависимостей) перед тем, как перейтичерез те, в глобальном масштабе.

Чтобы это работало должным образом, исполняемый файл должен быть собран с -fPIE, в противном случае некоторые нарушенные предположения ODR, сделанные libstdc ++, вероятно, вызовут segfault (альтернативно, если iostream будет заменено на cstdio,работает без -fPIE).

...