Qu'arrive-t-il aux variables globales et statiques dans une bibliothèque partagée lorsqu'elle est liée dynamiquement?

127

J'essaie de comprendre ce qui se passe lorsque des modules avec des globaux et des variables statiques sont liés dynamiquement à une application. Par modules, j'entends chaque projet dans une solution (je travaille beaucoup avec Visual Studio!). Ces modules sont soit intégrés dans * .lib ou * .dll ou le * .exe lui-même.

Je comprends que le binaire d'une application contient des données globales et statiques de toutes les unités de traduction individuelles (fichiers objet) dans le segment de données (et lecture seule du segment de données si const).

  • Que se passe-t-il lorsque cette application utilise un module A avec une liaison dynamique au moment du chargement? Je suppose que la DLL a une section pour ses globaux et statiques. Le système d'exploitation les charge-t-il? Si oui, où sont-ils chargés?

  • Et que se passe-t-il lorsque l'application utilise un module B avec une liaison dynamique à l'exécution?

  • Si j'ai deux modules dans mon application qui utilisent à la fois A et B, des copies des globaux A et B sont-elles créées comme mentionné ci-dessous (s'il s'agit de processus différents)?

  • Les DLL A et B ont-elles accès aux applications globales?

(Veuillez également indiquer vos raisons)

Citant de MSDN :

Les variables déclarées comme globales dans un fichier de code source DLL sont traitées comme des variables globales par le compilateur et l'éditeur de liens, mais chaque processus qui charge une DLL donnée obtient sa propre copie des variables globales de cette DLL. La portée des variables statiques est limitée au bloc dans lequel les variables statiques sont déclarées. Par conséquent, chaque processus a sa propre instance des variables globales et statiques DLL par défaut.

et d' ici :

Lors de la liaison dynamique de modules, il peut être difficile de savoir si différentes bibliothèques ont leurs propres instances de globals ou si les globals sont partagés.

Merci.

Raja
la source
3
Par modules, vous entendez probablement libs . Il y a une proposition d'ajouter des modules au standard C ++ avec une définition plus précise de ce que serait un module et une sémantique différente de celle des bibliothèques classiques à partir de maintenant.
David Rodríguez - dribeas
Ah, aurait dû clarifier cela. Je considère différents projets dans une solution (je travaille beaucoup avec Visual Studio) comme des modules. Ces modules sont intégrés dans * .lib ou * .dll.
Raja
3
@ DavidRodríguez-dribeas Le terme «module» est le terme technique correct pour les fichiers exécutables autonomes (entièrement liés), y compris: les programmes exécutables, les bibliothèques de liens dynamiques (.dll) ou les objets partagés (.so). C'est parfaitement approprié ici, et le sens est correct et bien compris. Jusqu'à ce qu'il y ait une fonctionnalité standard nommée "modules", sa définition reste la traditionnelle, comme je l'ai expliqué.
Mikael Persson

Réponses:

176

C'est une différence assez célèbre entre les systèmes Windows et Unix.

Peu importe ce que:

  • Chaque processus a son propre espace d'adressage, ce qui signifie qu'il n'y a jamais de mémoire partagée entre les processus (sauf si vous utilisez une bibliothèque ou des extensions de communication inter-processus).
  • La règle de définition unique (ODR) s'applique toujours, ce qui signifie que vous ne pouvez avoir qu'une seule définition de la variable globale visible au moment de la liaison (liaison statique ou dynamique).

Donc, le problème clé ici est vraiment la visibilité .

Dans tous les cas, staticles variables globales (ou fonctions) ne sont jamais visibles de l'extérieur d'un module (dll / so ou exécutable). Le standard C ++ exige qu'ils aient un lien interne, ce qui signifie qu'ils ne sont pas visibles à l'extérieur de l'unité de traduction (qui devient un fichier objet) dans lequel ils sont définis. Donc, cela règle ce problème.

Là où cela se complique, c'est lorsque vous avez externdes variables globales. Ici, les systèmes Windows et Unix sont complètement différents.

Dans le cas de Windows (.exe et .dll), les externvariables globales ne font pas partie des symboles exportés. En d'autres termes, différents modules ne sont en aucun cas conscients des variables globales définies dans d'autres modules. Cela signifie que vous obtiendrez des erreurs de l'éditeur de liens si vous essayez, par exemple, de créer un exécutable qui est censé utiliser une externvariable définie dans une DLL, car cela n'est pas autorisé. Vous devrez fournir un fichier objet (ou bibliothèque statique) avec une définition de cette variable extern et lier statiquement avec à la fois l'exécutable et la DLL, résultant en deux variables globales distinctes (une appartenance à l'exécutable et un appartenant à la DLL ).

Pour exporter réellement une variable globale sous Windows, vous devez utiliser une syntaxe similaire à la syntaxe d'export / import de la fonction, à savoir:

#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;

Lorsque vous faites cela, la variable globale est ajoutée à la liste des symboles exportés et peut être liée comme toutes les autres fonctions.

Dans le cas des environnements de type Unix (comme Linux), les bibliothèques dynamiques, appelées "objets partagés" avec extension, .soexportent toutes externles variables globales (ou fonctions). Dans ce cas, si vous effectuez une liaison au moment du chargement de n'importe où vers un fichier objet partagé, alors les variables globales sont partagées, c'est-à-dire liées ensemble comme une seule. Fondamentalement, les systèmes de type Unix sont conçus pour faire en sorte qu'il n'y ait pratiquement aucune différence entre la liaison avec une bibliothèque statique ou dynamique. Encore une fois, l'ODR s'applique à tous les niveaux: une externvariable globale sera partagée entre les modules, ce qui signifie qu'elle ne devrait avoir qu'une seule définition sur tous les modules chargés.

Enfin, dans les deux cas, pour les systèmes Windows ou de type Unix, vous pouvez effectuer une liaison d' exécution de la bibliothèque dynamique, c'est-à-dire en utilisant soit LoadLibrary()/ GetProcAddress()/ FreeLibrary()soit dlopen()/ dlsym()/ dlclose(). Dans ce cas, vous devez obtenir manuellement un pointeur vers chacun des symboles que vous souhaitez utiliser, et qui inclut les variables globales que vous souhaitez utiliser. Pour les variables globales, vous pouvez utiliser GetProcAddress()ou dlsym()tout simplement la même chose que pour les fonctions, à condition que les variables globales fassent partie de la liste de symboles exportée (selon les règles des paragraphes précédents).

Et bien sûr, comme une note finale nécessaire: les variables globales doivent être évitées . Et je crois que le texte que vous avez cité (à propos des choses "peu claires") fait référence exactement aux différences spécifiques à la plate-forme que je viens d'expliquer (les bibliothèques dynamiques ne sont pas vraiment définies par le standard C ++, c'est un territoire spécifique à la plate-forme, ce qui signifie qu'il est beaucoup moins fiable / portable).

Mikael Persson
la source
5
Excellente réponse, merci! J'ai un suivi: puisque la DLL est un morceau de code et de données autonome, a-t-il une section de segment de données similaire aux exécutables? J'essaie de comprendre où et comment ces données sont chargées (vers) lorsque la bibliothèque partagée est utilisée.
Raja
18
@Raja Oui, la DLL a un segment de données. En fait, en ce qui concerne les fichiers eux-mêmes, les exécutables et les DLL sont pratiquement identiques, la seule vraie différence est un drapeau qui est défini dans l'exécutable pour dire qu'il contient une fonction «principale». Lorsqu'un processus charge une DLL, son segment de données est copié quelque part dans l'espace d'adressage du processus et le code d'initialisation statique (qui initialiserait des variables globales non triviales) est également exécuté dans l'espace d'adressage du processus. Le chargement est le même que pour l'exécutable, sauf que l'espace d'adressage du processus est développé au lieu d'en créer un nouveau.
Mikael Persson
4
Qu'en est-il des variables statiques définies dans une fonction en ligne d'une classe? Par exemple, définissez "class A {void foo () {static int st_var = 0;}}" dans le fichier d'en-tête et incluez-le dans le module A et le module B, A / B partagera-t-il le même st_var ou chacun aura sa propre copie?
camino
2
@camino Si la classe est exportée (c'est-à-dire définie avec __attribute__((visibility("default")))), alors A / B partagera la même variable st_var. Mais si la classe est définie avec __attribute__((visibility("hidden"))), alors le module A et le module B auront sa propre copie, non partagée.
Wei Guo
1
@camino __declspec (dllexport)
ruipacheco