¿Qué sucede con las variables globales y estáticas en una biblioteca compartida cuando está vinculada dinámicamente?

127

Estoy tratando de entender qué sucede cuando los módulos con variables globales y estáticas se vinculan dinámicamente a una aplicación. Por módulos, me refiero a cada proyecto en una solución (¡trabajo mucho con Visual Studio!). Estos módulos están integrados en * .lib o * .dll o en el * .exe mismo.

Entiendo que el binario de una aplicación contiene datos globales y estáticos de todas las unidades de traducción individuales (archivos de objetos) en el segmento de datos (y solo segmento de datos de lectura si es constante).

  • ¿Qué sucede cuando esta aplicación usa un módulo A con enlace dinámico de tiempo de carga? Supongo que la DLL tiene una sección para sus globales y estática. ¿El sistema operativo los carga? Si es así, ¿dónde se cargan?

  • ¿Y qué sucede cuando la aplicación usa un módulo B con enlace dinámico en tiempo de ejecución?

  • Si tengo dos módulos en mi aplicación que usan A y B, ¿se crean copias de los globales de A y B como se menciona a continuación (si son procesos diferentes)?

  • ¿Las DLL A y B tienen acceso a las aplicaciones globales?

(Indique sus razones también)

Citando de MSDN :

Las variables que se declaran como globales en un archivo de código fuente DLL son tratadas como variables globales por el compilador y el vinculador, pero cada proceso que carga una DLL determinada obtiene su propia copia de las variables globales de esa DLL. El alcance de las variables estáticas se limita al bloque en el que se declaran las variables estáticas. Como resultado, cada proceso tiene su propia instancia de las variables globales y estáticas DLL por defecto.

y desde aquí :

Al vincular dinámicamente módulos, no puede quedar claro si las diferentes bibliotecas tienen sus propias instancias de globales o si las globales son compartidas.

Gracias.

Raja
fuente
3
Por módulos probablemente te refieres a libs . Existe una propuesta para agregar módulos al estándar C ++ con una definición más precisa de lo que sería un módulo y una semántica diferente a las bibliotecas regulares a partir de ahora.
David Rodríguez - dribeas
Ah, debería haber aclarado eso. Considero diferentes proyectos en una solución (trabajo mucho con Visual Studio) como módulos. Estos módulos están integrados en * .lib o * .dll 's.
Raja
3
@ DavidRodríguez-dribeas El término "módulo" es el término técnico correcto para los archivos ejecutables independientes (totalmente vinculados), que incluyen: programas ejecutables, bibliotecas de enlace dinámico (.dll) u objetos compartidos (.so). Aquí es perfectamente apropiado, y el significado es correcto y bien entendido. Hasta que haya una característica estándar llamada "módulos", la definición de la misma sigue siendo la tradicional, como expliqué.
Mikael Persson

Respuestas:

176

Esta es una diferencia bastante famosa entre Windows y sistemas similares a Unix.

No importa qué:

  • Cada proceso tiene su propio espacio de direcciones, lo que significa que nunca hay memoria compartida entre los procesos (a menos que use alguna biblioteca o extensiones de comunicación entre procesos).
  • La regla de una definición (ODR) todavía se aplica, lo que significa que solo puede tener una definición de la variable global visible en el tiempo de enlace (enlace estático o dinámico).

Entonces, la cuestión clave aquí es realmente visibilidad .

En todos los casos, staticlas variables globales (o funciones) nunca son visibles desde fuera de un módulo (dll / so o ejecutable). El estándar C ++ requiere que estos tengan un enlace interno, lo que significa que no son visibles fuera de la unidad de traducción (que se convierte en un archivo objeto) en el que están definidos. Entonces, eso resuelve ese problema.

Donde se complica es cuando tienes externvariables globales. Aquí, los sistemas similares a Windows y Unix son completamente diferentes.

En el caso de Windows (.exe y .dll), las externvariables globales no son parte de los símbolos exportados. En otras palabras, los diferentes módulos no son conscientes de las variables globales definidas en otros módulos. Esto significa que obtendrá errores de enlazador si intenta, por ejemplo, crear un ejecutable que se supone que usa una externvariable definida en una DLL, porque esto no está permitido. Debería proporcionar un archivo de objeto (o biblioteca estática) con una definición de esa variable externa y vincularlo estáticamente con ambos el ejecutable y el archivo DLL, lo que resulta en dos distintas variables globales (uno perteneciente al ejecutable y uno que pertenecen a la DLL )

Para exportar realmente una variable global en Windows, debe usar una sintaxis similar a la sintaxis de exportación / importación de funciones, es decir:

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

Cuando hace eso, la variable global se agrega a la lista de símbolos exportados y se puede vincular como todas las demás funciones.

En el caso de entornos similares a Unix (como Linux), las bibliotecas dinámicas, llamadas "objetos compartidos" con extensión, .soexportan todas externlas variables (o funciones) globales. En este caso, si realiza un enlace de tiempo de carga desde cualquier lugar a un archivo de objeto compartido, las variables globales se comparten, es decir, se unen como una sola. Básicamente, los sistemas tipo Unix están diseñados para que no haya prácticamente ninguna diferencia entre vincular con una biblioteca estática o dinámica. Una vez más, la ODR se aplica en todos los ámbitos: una externvariable global se compartirá entre los módulos, lo que significa que solo debe tener una definición en todos los módulos cargados.

Finalmente, en ambos casos, para sistemas similares a Windows o Unix, puede hacer un enlace en tiempo de ejecución de la biblioteca dinámica, es decir, usando LoadLibrary()/ GetProcAddress()/ FreeLibrary()o dlopen()/ dlsym()/ dlclose(). En ese caso, debe obtener manualmente un puntero a cada uno de los símbolos que desea usar, y eso incluye las variables globales que desea usar. Para las variables globales, puede usar GetProcAddress()o dlsym()lo mismo que para las funciones, siempre que las variables globales sean parte de la lista de símbolos exportados (según las reglas de los párrafos anteriores).

Y, por supuesto, como una nota final necesaria: se deben evitar las variables globales . Y creo que el texto que citó (acerca de que las cosas están "poco claras") se refiere exactamente a las diferencias específicas de la plataforma que acabo de explicar (las bibliotecas dinámicas no están realmente definidas por el estándar C ++, este es un territorio específico de la plataforma, lo que significa que es mucho menos confiable / portátil).

Mikael Persson
fuente
55
Gran respuesta, gracias! Tengo un seguimiento: dado que la DLL es una pieza de código y datos autocontenidos, ¿tiene una sección de segmento de datos similar a los ejecutables? Estoy tratando de entender dónde y cómo se cargan (a) estos datos cuando se usa la biblioteca compartida.
Raja
18
@Raja Sí, la DLL tiene un segmento de datos. De hecho, en términos de los archivos en sí, los archivos ejecutables y los archivos DLL son prácticamente idénticos, la única diferencia real es un indicador que se establece en el archivo ejecutable para decir que contiene una función "principal". Cuando un proceso carga una DLL, su segmento de datos se copia en algún lugar del espacio de direcciones del proceso, y el código de inicialización estático (que inicializaría variables globales no triviales) también se ejecuta dentro del espacio de direcciones del proceso. La carga es la misma que para el ejecutable, excepto que el espacio de direcciones del proceso se expande en lugar de crearse uno nuevo.
Mikael Persson
44
¿Qué hay de las variables estáticas definidas dentro de una función en línea de una clase? por ejemplo, defina "clase A {void foo () {static int st_var = 0;}}" en el archivo de encabezado e inclúyalo en el módulo A y el módulo B, ¿compartirá A / B el mismo st_var o cada uno tendrá su propia copia?
camino
2
@camino Si la clase se exporta (es decir, se define con __attribute__((visibility("default")))), A / B compartirá el mismo st_var. Pero si la clase se define con __attribute__((visibility("hidden"))), entonces el módulo A y el módulo B tendrán su propia copia, no compartida.
Wei Guo
1
@camino __declspec (dllexport)
ruipacheco