Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


1.4.4b2  Librerías dinámicas

§1  Presentación

Antes de conocer los pormenores de la construcción y uso de una librería dinámica (DLL) en un programa C++, es conveniente tener una perspectiva general del mecanismo que rige su funcionamiento.

Como se indicó en la introducción ( 1.4.4b), una librería es simplemente un trozo de código que contiene recursos preconstridos; recursos que pueden ser utilizados en un ejecutable.  Sin embargo, si nos fijamos un poco, vemos que esta definición es un tanto ambigua, y puede encajar en objetos que no son realmente librerías.  En realidad, la utilización de recursos preconstruidos por parte de un ejecutable puede realizarse de tres formas que podríamos resumir del siguiente modo:

  • Utilización de librerías estáticas.  Es el método tradicional. Como hemos señalado, son las clásicas colecciones de ficheros objeto .obj (compilados), que en el momento de la construcción de la aplicación, son incluidos por el "Linker" en el propio ejecutable ( 1.4.4b1).

  • Utilización de librerías dinámicas.  En esta modalidad, los recursos ocupan un fichero independiente del ejecutable, que puede ser utilizado por cualquier aplicación que lo necesite.  En algún momento, durante la carga del ejecutable, o posteriormente, en run-time, el ejecutable deberá integrar este bloque de código en su propio espacio, de forma que pueda acceder a los recursos contenidos en él.

  • Utilización de programas externos.  Es también un recurso utilizado desde siempre en informática.  Un ejecutable puede llamar a ejecución a otro mediante mecanismos de varios tipos.  El ejecutable llamado proporciona alguna funcionalidad antes de su terminación, y dispone de su propio espacio de ejecución independiente del programa que lo invocó.

Nota: respecto a la utilización de programas externos "versus" librerías dinámicas, hay que tener en cuenta que las plataformas Windows disponen de un espacio de memoria protegida para cada proceso o programa que es iniciado por el Sistema, lo que es oneroso en término de recursos, y origina una sobrecarga similar a la que suponen los procesos involucrados en la invocación de funciones ( 4.4.6b).  Por contra, las librerías dinámicas no necesitan su propio espacio, y corren en el espacio del proceso que las invoca, lo que es mucho más rápido y ligero en término de recursos.


Las DLLs son trozos de código capaz de realizar determinadas operaciones, cuya "funcionalidad" puede ser utilizada desde otros ejecutables, y como puede verse, ocupan una posición intermedia (diríamos que una solución de compromiso) entre las posiciones extremas antes citadas. Con las librerías estáticas comparten la característica de que es un trozo de código que acaba siendo incluido en el espacio del ejecutable que las utiliza [1].  A su vez, comparten con los programas externos la característica de que constituyen ficheros distintos y físicamente independientes del ejecutable que los utiliza.

Antes de seguir adelante, debemos puntualizar un extremo que es importante para comprender el funcionamiento de las DLLs. En realidad, la DLL no es cargada en el espacio de memoria del ejecutable, sino que tiene su propio espacio.  Lo que ocurre es que este espacio es accesible desde el ejecutable, y está "mapeado" en él.  Es decir, en el ejecutable existe un cierto "mapa" de cómo está distribuida esa zona de memoria; dónde están sus objetos utilizables desde el exterior (exportables 1.4.4b2a). Como veremos a continuación, existen dos formas de incluir esta información en el ejecutable.  Además, el hecho de que la DLL disponga de su propio espacio (code segment), tiene una importante ventaja adicional:  si dos o más procesos que se están ejecutando simultáneamente en el Sistema necesitan de la misma DLL, no necesita ser cargada dos veces en memoria. Basta que ambos tengan acceso a ella y cierto conocimiento de su estructura interna. Por esta razón, en los entornos Unix/Linux son conocidas como librerías compartidas [5].

Nota: esta utilización del mismo código por varias aplicaciones es posible porque, como se ha indicado [1], los objetos creados por la DLL no pertenecen a esta, sino al programa usuario.  En otras palabras: se comparte el código pero no los datos.


En realidad lo que caracteriza a una DLL es la forma en que es traída a ejecución; no directamente desde el shell del Sistema como un ejecutable .exe normal, sino desde otro ejecutable (que puede ser incluso otra DLL), de forma parecida a como se invoca una función (una especie de función externa al programa).  Por esta razón no disponen de una función main ( 4.4.4) o de un módulo de inicio en el sentido clásico (ver ejemplo 1.4.4b2a).

Al llegar aquí, es pertinente una observación para los que han programado para entornos como MS-DOS y se adentran en el territorio de la programación C++ con librerías dinámicas. Por ejemplo aplicaciones para MS Windows.

Algunos enlazadores para DOS [3] permiten que determinadas porciones del ejecutable se sitúen en ficheros independientes (generalmente con terminación .OVL), los denominados "overlays".  Estos overlays son traídos a memoria (cargados) automáticamente según convenga, de forma que salvo contadísimas excepciones, su funcionamiento es totalmente transparente para el programador.  Por su parte, las librerías dinámicas permiten también que partes del ejecutable se alojen en ficheros independientes.  Sin embargo, su comportamiento es mucho (muchísimo) menos flexible que su contrapartida DOS.  La razón es que durante la fase de enlazado de la librería dinámica, la tabla de símbolos del que será su ejecutable anfitrión no es accesible (ni siquiera conocido), de forma que la única información que puede ser intercambiada con el exterior por el código de la librería, es la que se recibe a través de los parámetros de sus funciones y los valores devueltos por estas [7].  El resultado es que, a todos los efectos, su funcionamiento se parece más a la invocación de un programa externo, que una vez ejecutado, devuelve el control al programa inicial.

El resultado es que si desde una DLL necesitamos utilizar una funcionalidad existente en el cuerpo del programa (una función o clase), no podemos accederla a menos que dicha función sea también incluida en una DLL independiente.  Naturalmente esto exige que la separación de partes del programa en DLLs se realice después de un estudio minucioso de las funcionalidades que serán utilizadas desde cada módulo.

Como nota complementaria, debemos añadir que en este, como en muchos otros aspectos, los entornos Unix son mucho más flexibles, ya que permiten la existencia de librerías compartidas de enlazado dinámico bidireccional automático. 

§2  Utilización

De lo dicho anteriormente se desprende que la utilización de los recursos contenidos en una DLL requiere dos condiciones:

a.-  Cargar en memoria el contenido de la DLL utilizando un espacio accesible desde el ejecutable que la utiliza. Esto puede efectuarse de dos formas:

a1.-  En el mismo momento de la carga inicial del programa.

a2.-  En el momento en que se necesite alguno de sus recursos (en runtime).

b.-  Conocer la topografía interna de ese trozo de código para poder acceder a sus objetos.


§2a1  En el primer caso (a1), las DLLs requeridas por el ejecutable .EXE son cargadas, y sus objetos inicializados por el módulo de inicio como cualquier otro módulo del programa. Es decir, que serán inicializadas antes que comience la ejecución de main.  Cuando la aplicación es cargada por el SO, este mira en el fichero .EXE para ver que DLLs se necesitan, y se encarga de cargarlas también.

Veremos que este tipo de utilización, denominada enlazado estático o implícito (librería dinámica enlazada estáticamente), es con diferencia la más usual  ( 1.4.4b2b).


§2a2  En el caso a2, la librería es cargada en runtime cuando la aplicación lo necesita. Esta forma de uso se denomina enlazado dinámico o explícito (librería dinámica enlazada dinámicamente). Para realizar la carga, el programador dispone de algunas funciones de la API de Windows que se encargan de realizar la tarea cuando él lo decide (de ahí que se denomine enlazado explícito).


Cualquiera que sea la forma de carga elegida, implícita o explícita, el orden de búsqueda seguido por el Sistema para localizar el fichero (.dll) a cargar, es siempre el mismo:

  • En el directorio que contiene el ejecutable (fichero .EXE)

    Fig. 1

    Fig. 2

  • El directorio actual de la aplicación [4].
  • El directorio de sistema de Windows
  • El directorio de Windows
  • Los directorios incluidos en la variable de entorno PATH del Sistema.

Si en una carga implícita el Sistema no encuentra el fichero .DLL en ninguno de los sitios anteriores, se muestra un mensaje de error y la aplicación no puede ejecutarse (fig. 1).

En caso de que fracase la carga del fichero durante la carga explícita, si el Sistema devuelve un error, es potestad del programador decidir que hacer.  La figura 2 es un ejemplo tomado de una aplicación real cuando no aparece la .DLL requerida [6].

 

El orden de carga mencionado es el que podríamos llamar "clásico"; de las versiones de Windows anteriores a XP SP1. A partir de esta, el orden se modificó, de forma que la búsqueda no comienza en el directorio que contiene el ejecutable, sino en los directorios de Windows.  La razón está relacionada con la seguridad    http://msdn.microsoft.com/. Incluimos el párrafo más significativo:

"DLL Search Order Has Changed

No longer is the current directory searched first when loading DLLs! This change was also made in Windows XP SP1. The default behavior now is to look in all the system locations first, then the current directory, and finally any user-defined paths. This will have an impact on your code if you install a DLL in the application's directory because Windows Server 2003 no longer loads the 'local' DLL if a DLL of the same name is in the system directory. A common example is if an application won't run with a specific version of a DLL, an older version is installed that does work in the application directory. This scenario will fail in Windows Server 2003.

The reason this change was made was to mitigate some kinds of trojaning attacks. An attacker may be able to sneak a bad DLL into your application directory or a directory that has files associated with your application. The DLL search order change removes this attack vector."


§2b 
Es evidente, que una vez cargado el código de la DLL, el programa anfitrión necesita conocer las direcciones de los recursos contenidos en esa zona de memoria para poder acceder a ellos.  El procedimiento es distinto según el método de carga utilizado:

En el caso de librería dinámica enlazada estáticamente, se construye una librería tradicional (.LIB) de un tipo especial denominado librería de importación, que es enlazada estáticamente con el ejecutable (formando parte de él).  La librería de importación no contiene código, en realidad es un índice o tabla de dos columnas.  En la primera están los nombres de las funciones exportables de las DLLs; la segunda está vacía, pero cuando las librerías son cargadas en memoria durante el proceso de carga del ejecutable, el programa cargador ya puede conocer las direcciones de estos recursos, y completa la segunda columna de la tabla con las direcciones adecuadas.  De esta forma, el ejecutable puede acceder a los recursos de la DLL con solo conocer los nombres adecuados.

En el caso de librería dinámica enlazada dinámicamente, una vez realizada la carga mediante las funciones correspondientes, la API del Sistema dispone de una función específica GetProcAddress(), que permite obtener punteros a las funciones de la DLL que deban utilizarse.

Es necesario mencionar que para obtener las direcciones de los recursos dentro del bloque de código de la DLL, tanto el programa cargador como la función GetProcAddress de la API, utilizan a su vez una tabla que acompaña a cualquier librería dinámica, la tabla de entrada ("entry table" 1.4.4b2a).

§3  Tabla de inicio

Cuando se compila cualquier módulo, en el objeto resultante existe un segmento denominado _INIT_ que contiene una referencia ("Init entry") al constructor de cada objeto global que deba ser inicializado, así como un orden de prioridad.  Más tarde, cuando el enlazador construye un ejecutable, estas entradas se agrupan en una tabla, la tabla de inicio ("Init table") que contiene ordenadamente todas las entradas de los constructores de los objetos que existen en los módulos que componen el programa.  Finalmente, esta tabla es utilizada por el módulo de inicio cuando el programa es iniciado.

En el caso de que este ejecutable sea una librería de enlazado dinámico, el orden de estas entradas en la tabla depende de dos factores: su prioridad, y el orden en que se encuentren los módulos-objeto en la orden de enlazado.  Por ejemplo, si la orden de enlazado incluye tres objetos A.oB.o y C.o, cada uno de los cuales tiene tres objetos globales cuyos constructores tienen entradas en el segmento _INIT_ de su módulo con la misma prioridad (supongamos que 0x20),  al construir la librería, puesto que sus prioridades son idénticas, el orden en que serán inicializados dichos objetos dependerá del orden de los módulos A, B y C en la orden de enlazado.  Cambiando este orden puede alterarse el orden en que se invocan los constructores de los objetos correspondientes.

Las librerías dinámicas enlazadas estáticamente a un ejecutable, son inicializadas junto con el resto de módulos del programa, por el módulo de inicio ( 1.5) del ejecutable antes que comience la ejecución de main (o WinMain).  Esto supone que cualquier directiva #pragma startup ( 4.9.10i) existente en los módulos .OBJ que componen la librería será ejecutada, y que todas sus variables estáticas, serán inicializadas según el orden precedente.

En el caso de que la DLL tenga enlazado dinámico, la inicialización solo se realiza cuando es cargado el módulo (.DLL).


§4  Recordar que para usar las funciones contenidas en una librería (estática o dinámica) se necesitan tres condiciones:

  • Un prototipo que permita conocer el nombre del fichero que compone la librería, su localización, parámetros y tipo de retorno de la función de librería que queramos utilizar (esto es lo normal para utilizar cualquier función 4.4.1).  En el caso de librerías que contienen clases predefinidas, esta condición se sustituye por el conocimiento de la interfaz de la clase.

  • Disponer de los tipos de datos que pasarán como argumentos (también normal para cualquier función).

  • En el caso de funciones, poder utilizar la convención de llamada que corresponda a la librería en cuestión.  Es decir, que el enlazador C++ utilizado permita usar la convención de llamada adecuada, de forma que estos módulos externos puedan ser llamados a ejecución desde nuestro programa [2].

  Inicio.


[1]  Esta característica es importante y debe ser tenida en cuenta.  Una DLL es un trozo de código inerte que no tiene vida por sí mismo hasta que es incluido en el espacio de la aplicación que la utiliza.  No tiene un proceso propio, y cualquier objeto creado por su código pertenece a la aplicación anfitriona.  Si los programas tienen vida, seguramente un biólogo compararía a las DLLs con los virus. Materia inerte que solo puede vivir en el interior de una célula anfitriona.

[2]  Recuerde que los compiladores más usados; Borland C++; MS Visual C++; GNU g++, etc., permiten especificar las convenciones de llamada más usuales en este tipo de librerías ( 4.4.6a).

[3]  Por ejemplo el magnífico enlazador Blinker.    http://blinkinc.com.

[4]  Generalmente una aplicación "reside" en el directorio donde se encuentra el ejecutable, pero puede cambiar su directorio activo durante la ejecución.

[5]  Un perfecto ejemplo es el caso de las DLLs que contienen la API de Windows, que son cargadas una sola vez en memoria y utilizadas por casi cualquier aplicación que se corra en el Sistema.

[6]  Mi recomendación es no utilizar un mensaje tan críptico como el de la figua-2, que sirve de escasa ayuda al usuario.  Es preferible mostrar un mensaje como el de la figura-1. De esta forma el usuario tiene una mejor información sobre el problema, incluso la posibilidad de solventarlo (quizás la DLL ha sido borrada accidentalmente; no ha sido instalada; se encuentra en una localización distinta, etc.)

[7]  Si se trata de aplicaciones "modernas", por ejemplo aplicaciones Windows, entre estos argumentos recibidos y valores devueltos por la librería, se encuentran los mensajes que sus "callbacks" (funciones con el especificador __stdcall 4.4.6a) pueden intercambiar con el sistema.