Co dzieje się ze zmiennymi globalnymi i statycznymi w bibliotece współdzielonej, gdy jest ona połączona dynamicznie?

127

Próbuję zrozumieć, co się dzieje, gdy moduły ze zmiennymi globalnymi i statycznymi są dynamicznie połączone z aplikacją. Przez moduły rozumiem każdy projekt w rozwiązaniu (dużo pracuję z Visual Studio!). Te moduły są wbudowane w * .lib lub * .dll lub sam * .exe.

Rozumiem, że plik binarny aplikacji zawiera globalne i statyczne dane wszystkich indywidualnych jednostek tłumaczeniowych (plików obiektowych) w segmencie danych (i segment danych tylko do odczytu, jeśli jest const).

  • Co się dzieje, gdy ta aplikacja używa modułu A z dynamicznym łączeniem w czasie ładowania? Zakładam, że biblioteka DLL ma sekcję dotyczącą globalnych i statycznych. Czy system operacyjny je ładuje? Jeśli tak, gdzie są ładowane?

  • A co się dzieje, gdy aplikacja używa modułu B z dynamicznym łączeniem w czasie wykonywania?

  • Jeśli mam dwa moduły w mojej aplikacji, które używają A i B, czy kopie globali A i B są tworzone, jak wspomniano poniżej (jeśli są to różne procesy)?

  • Czy biblioteki DLL A i B uzyskują dostęp do globalnych aplikacji?

(Proszę również podać powody)

Cytowanie z MSDN :

Zmienne zadeklarowane jako globalne w pliku kodu źródłowego DLL są traktowane przez kompilator i konsolidator jako zmienne globalne, ale każdy proces ładujący daną bibliotekę DLL otrzymuje własną kopię zmiennych globalnych tej biblioteki DLL. Zakres zmiennych statycznych jest ograniczony do bloku, w którym zadeklarowane są zmienne statyczne. W rezultacie każdy proces ma domyślnie własne wystąpienie zmiennych globalnych i statycznych biblioteki DLL.

i stąd :

Podczas dynamicznego łączenia modułów może być niejasne, czy różne biblioteki mają własne instancje obiektów globalnych, czy też pliki globalne są współużytkowane.

Dzięki.

Radża
źródło
3
Przez moduły prawdopodobnie masz na myśli biblioteki libs . Istnieje propozycja dodania modułów do standardu C ++ z bardziej precyzyjną definicją tego, czym byłby moduł i inną semantyką niż zwykłe biblioteki w chwili obecnej.
David Rodríguez - dribeas
Ach, powinienem był to wyjaśnić. Traktuję różne projekty w rozwiązaniu (dużo pracuję z Visual Studio) jako moduły. Te moduły są wbudowane w pliki * .lib lub * .dll.
Raja
3
@ DavidRodríguez-dribeas Termin „moduł” jest poprawnym terminem technicznym dla samodzielnych (w pełni połączonych) plików wykonywalnych, w tym: programów wykonywalnych, bibliotek dołączanych dynamicznie (.dll) lub obiektów współdzielonych (.so). Jest to w pełni odpowiednie, a znaczenie jest poprawne i dobrze zrozumiane. Dopóki nie pojawi się standardowa funkcja o nazwie „moduły”, jej definicja pozostaje tradycyjna, jak wyjaśniłem.
Mikael Persson

Odpowiedzi:

176

Jest to dość znana różnica między systemami Windows a systemami podobnymi do Uniksa.

Nieważne co:

  • Każdy proces ma własną przestrzeń adresową, co oznacza, że ​​żadna pamięć nie jest współużytkowana między procesami (chyba że używasz jakiejś biblioteki lub rozszerzeń komunikacji między procesami).
  • Zasada jedna definicja (ODR) nadal obowiązuje, co oznacza, że można mieć tylko jedną definicję zmiennej globalnej widocznym na link-czasu (statyczne lub dynamiczne łączenie).

Tak więc kluczową kwestią jest tutaj naprawdę widoczność .

We wszystkich przypadkach staticzmienne globalne (lub funkcje) nigdy nie są widoczne spoza modułu (dll / so lub plik wykonywalny). Standard C ++ wymaga, aby miały one wewnętrzne powiązania, co oznacza, że ​​nie są widoczne poza jednostką tłumaczeniową (która staje się plikiem obiektowym), w której są zdefiniowane. Więc to rozwiązuje ten problem.

Sytuacja staje się skomplikowana, gdy masz externzmienne globalne. Tutaj systemy Windows i systemy typu Unix są zupełnie inne.

W przypadku systemu Windows (.exe i .dll) externzmienne globalne nie są częścią eksportowanych symboli. Innymi słowy, różne moduły w żaden sposób nie są świadome zmiennych globalnych zdefiniowanych w innych modułach. Oznacza to, że wystąpią błędy konsolidatora, jeśli spróbujesz na przykład utworzyć plik wykonywalny, który powinien używać externzmiennej zdefiniowanej w bibliotece DLL, ponieważ jest to niedozwolone. Będziesz musiał dostarczyć plik obiektowy (lub bibliotekę statyczną) z definicją tej zmiennej zewnętrznej i połączyć ją statycznie zarówno z plikiem wykonywalnym, jak i biblioteką DLL, w wyniku czego powstają dwie odrębne zmienne globalne (jedna należąca do pliku wykonywalnego i jedna należąca do biblioteki DLL ).

Aby faktycznie wyeksportować zmienną globalną w systemie Windows, musisz użyć składni podobnej do składni eksportu / importu funkcji, tj .:

#ifdef COMPILING_THE_DLL
#define MY_DLL_EXPORT extern "C" __declspec(dllexport)
#else
#define MY_DLL_EXPORT extern "C" __declspec(dllimport)
#endif

MY_DLL_EXPORT int my_global;

Gdy to zrobisz, zmienna globalna zostanie dodana do listy eksportowanych symboli i może być połączona jak wszystkie inne funkcje.

W przypadku środowisk uniksopodobnych (takich jak Linux) biblioteki dynamiczne, zwane „obiektami współdzielonymi” z rozszerzeniem, .soeksportują wszystkie externzmienne globalne (lub funkcje). W tym przypadku, jeśli wykonujesz łączenie w czasie ładowania z dowolnego miejsca do współdzielonego pliku obiektu, wtedy zmienne globalne są współdzielone, tj. Połączone razem jako jedna. Zasadniczo systemy uniksopodobne są zaprojektowane tak, aby praktycznie nie było różnicy między łączeniem z biblioteką statyczną lub dynamiczną. Ponownie ODR ma zastosowanie we externwszystkich modułach : zmienna globalna będzie współdzielona między modułami, co oznacza, że ​​powinna mieć tylko jedną definicję dla wszystkich załadowanych modułów.

Na koniec, w obu przypadkach dla Windows lub Unix-like systemów można zrobić run-time łączącego biblioteki dynamicznej, czyli przy użyciu albo LoadLibrary()/ GetProcAddress()/ FreeLibrary()lub dlopen()/ dlsym()/ dlclose(). W takim przypadku musisz ręcznie uzyskać wskaźnik do każdego z symboli, których chcesz użyć, w tym zmiennych globalnych, których chcesz użyć. W przypadku zmiennych globalnych można użyć funkcji GetProcAddress()lub dlsym()tak samo, jak w przypadku funkcji, pod warunkiem, że zmienne globalne są częścią listy eksportowanych symboli (zgodnie z zasadami z poprzednich akapitów).

I oczywiście, jako niezbędna uwaga końcowa: należy unikać zmiennych globalnych . Uważam, że cytowany tekst (o tym, że rzeczy są „niejasne”) odnosi się dokładnie do różnic specyficznych dla platformy, które właśnie wyjaśniłem (biblioteki dynamiczne nie są tak naprawdę zdefiniowane przez standard C ++, jest to obszar specyficzny dla platformy, co oznacza, że jest znacznie mniej niezawodny / przenośny).

Mikael Persson
źródło
5
Świetna odpowiedź, dziękuję! Mam dalszy ciąg: ponieważ biblioteka DLL jest samodzielnym fragmentem kodu i danych, czy zawiera sekcję segmentu danych podobną do plików wykonywalnych? Próbuję zrozumieć, gdzie i jak te dane są ładowane (do), gdy używana jest biblioteka współdzielona.
Raja
18
@Raja Tak, biblioteka DLL ma segment danych. W rzeczywistości, jeśli chodzi o same pliki, pliki wykonywalne i biblioteki DLL są praktycznie identyczne, jedyną prawdziwą różnicą jest flaga umieszczona w pliku wykonywalnym, aby powiedzieć, że zawiera on funkcję „główną”. Kiedy proces ładuje bibliotekę DLL, jego segment danych jest kopiowany gdzieś do przestrzeni adresowej procesu, a statyczny kod inicjujący (który inicjowałby nietrywialne zmienne globalne) jest również uruchamiany w przestrzeni adresowej procesu. Ładowanie przebiega tak samo, jak w przypadku pliku wykonywalnego, z tą różnicą, że przestrzeń adresowa procesu jest rozszerzana zamiast tworzyć nową.
Mikael Persson
4
A co ze zmiennymi statycznymi zdefiniowanymi w funkcji wbudowanej klasy? np. zdefiniuj "class A {void foo () {static int st_var = 0;}}" w pliku nagłówkowym i umieść go w module A i module B, czy A / B będzie współdzielił tę samą st_var, czy każdy będzie miał własną kopię?
camino
2
@camino Jeśli klasa jest eksportowana (tj. zdefiniowana za pomocą __attribute__((visibility("default")))), A / B będzie współużytkować tę samą st_var. Ale jeśli klasa jest zdefiniowana za pomocą __attribute__((visibility("hidden"))), to moduł A i moduł B będą miały własną kopię, a nie współdzielone.
Wei Guo
1
@camino __declspec (dllexport)
ruipacheco