Статические переменные имеют общее значение в разных модулях

Прежде всего контекст проблемы: Linux x64, gcc v4.8.5. Есть приложение, которое загружает две разделяемые библиотеки (пусть это module1.so и module2.so), эти модули имеют частично одинаковый код. Теперь немного кода:

//SomeClass.h
class SomeClass
{
public:
    static unsigned long& s_uObj2()
    {
        static unsigned long s_uObj2;
        return s_uObj2;
    };
    void Initialize();
};

//SomeClass.cpp
void SomeClass::Initialize()
{
     if (0 == s_uObj2())
     {
         //do init
     }
     s_uObj2()++; //acts as a counter
}

Этот код был написан давно, и его идея состоит в том, чтобы предотвратить двойную инициализацию SomeClass в каждом модуле. Проблема: эта реализация каким-то образом разделяет значение s_uObj2 между разными модулями (в одном приложении), что приводит к тому, что инициализируется только первый модуль.

Как это возможно? Я думал это должно быть изолированное адресное пространство между разными модулями?

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


person Community    schedule 03.03.2017    source источник
comment
Связанный? Ссылка.   -  person François Andrieux    schedule 03.03.2017
comment
@FrançoisAndrieux Я не нашел ничего полезного в этом посте - он говорит, что во всех случаях статические глобальные переменные (или функции) никогда не видны снаружи модуля (dll/so или исполняемого файла). И в моем случае у меня есть РАЗНЫЕ глобальные переменные в РАЗНЫХ модулях.   -  person    schedule 03.03.2017
comment
В вашем случае у вас нет статических глобальных переменных, у вас есть статическая local переменная, которая во многом похожа на нестатическую глобальную переменную. Прочтите часть о extern глобалах в Linux.   -  person François Andrieux    schedule 03.03.2017
comment
@FrançoisAndrieux, спасибо, конечно, но на самом деле вы совсем не помогаете, указывая на какой-то другой общий случай   -  person    schedule 03.03.2017
comment
Вы хотите сказать, что SomeClass существует в обеих библиотеках? Вопрос немного не ясен. А динамические библиотеки не загружаются в отдельные адресные пространства, они загружаются в разные части адресного пространства приложения.   -  person Mark Ransom    schedule 03.03.2017


Ответы (1)


Проблема: эта реализация каким-то образом разделяет значение s_uObj2 между разными модулями (в одном приложении), что приводит к тому, что инициализируется только первый модуль.

Как это возможно? Я думал это должно быть изолированное адресное пространство между разными модулями?

Это требование одного правила определения стандарта C++. Правило в основном гласит, что все глобальные объекты с одинаковым именем должны разрешаться в одно определение во всей программе. Например, все глобальные переменные (включая статику класса, как в вашем случае) должны разрешаться в один и тот же единственный объект.

Теперь GCC очень старается сохранить ODR во всех разделяемых библиотеках. Если вы создадите приведенный выше код и изучите его экспортированные символы, вы увидите, что он экспортирует SomeClass::s_uObj2:

$ g++ tmp.cpp -shared -fPIC && objdump -T a.out | c++filt
...
0000000000200970  w   DO .bss   0000000000000008  Base        SomeClass::s_uObj2()::s_uObj2

Это означает, что при запуске динамический компоновщик разрешает все повторяющиеся копии SomeClass::s_uObj2()::s_uObj2 в одиночный объект (который является копией в первой загруженной общей библиотеке).

Обычный способ решить эту проблему (если вы действительно готовы отказаться от ODR, что очень плохо) — избежать экспорта s_uObj2 из библиотеки, т. е. ограничить его видимость.

Есть много способов сделать это, я назову лишь некоторые:

  • скомпилировать с -fvisibility-inlines-hidden

  • прикрепить __attribute__((visibility("hidden"))) к определению s_uObj2

  • поместите объявление вашего класса внутри

    #pragma GCC visibility push(hidden)

    ...

    #pragma GCC visibility pop

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

person yugr    schedule 03.03.2017
comment
Похоже, gcc/Linux работает с этим тяжелее, чем Visual Studio/Windows. Несколько лет назад у меня была проблема с несколькими синглтонами, потому что у разных DLL была собственная копия локальной статической переменной. - person Mark Ransom; 03.03.2017
comment
Вот и все! Спасибо. Можете ли вы объяснить, почему это происходит только в Linux? Я тестировал этот код в Windows (Visual Studio 2010) и Android NDK — таких проблем не было. - person ; 03.03.2017
comment
И на самом деле кажется, что правильный аргумент GCC -fvisibility=hidden - person ; 03.03.2017
comment
@AlekDepler -fvisibility=hidden еще более опасен, чем -fvisibility-inlines-hidden, который я не рекомендовал. Я скрою все символы в вашей библиотеке (что сделает ее бесполезной, если только вы не аннотируете экспортированные функции). Перестройка вашего ПО со скрытой видимостью по умолчанию — это огромное изменение, и к нему нельзя относиться легкомысленно. - person yugr; 04.03.2017
comment
@MarkRansom Действительно, но за это приходится платить - GCC не разрешено оптимизировать функции в общих библиотеках так хорошо, как это может сделать Visual Studio. - person yugr; 04.03.2017