Procedimientos recomendados de biblioteca de vínculos dinámicos
**Actualizado: **
- 17 de mayo de 2006
API importantes
La creación de bibliotecas de vínculos dinámicos presenta una serie de desafíos para los desarrolladores. Las bibliotecas de vínculos dinámicos no tienen control de versiones aplicados por el sistema. Cuando existen varias versiones de una biblioteca de vínculos dinámicos en un sistema, la facilidad de sobrescribirse junto con la falta de un esquema de control de versiones crea conflictos de dependencias y API. La complejidad en el entorno de desarrollo, la implementación del cargador y las dependencias de biblioteca de vínculos dinámicos han creado fragilidad en el orden de carga y el comportamiento de la aplicación. Por último, muchas aplicaciones se basan en bibliotecas de vínculos dinámicos y tienen conjuntos complejos de dependencias que se deben respetar para que las aplicaciones funcionen correctamente. En este documento se proporcionan instrucciones para que los desarrolladores de bibliotecas de vínculos dinámicos ayuden a crear bibliotecas de vínculos dinámicos más sólidas, portátiles y extensibles.
La sincronización incorrecta en DllMain puede hacer que una aplicación interbloquee, o bien acceda a datos o al código en una biblioteca de vínculos dinámicos sin inicializar. Llamar a determinadas funciones desde DllMain provoca estos problemas.
Procedimientos recomendados generales
Se llama a DllMain mientras se mantiene el bloqueo del cargador. Por lo tanto, se imponen restricciones significativas en las funciones a las que se puede llamar en DllMain. Por lo tanto, DllMain está diseñado para realizar tareas de inicialización mínimas mediante un pequeño subconjunto de la API de Microsoft® Windows®. No se puede llamar a ninguna función de DllMain que intente adquirir el bloqueo del cargador directa o indirectamente. De lo contrario, presentará la posibilidad de que la aplicación interbloquee o se bloquee. Un error en una implementación de DllMain puede poner en peligro todo el proceso y sus subprocesos.
El DllMain ideal sería simplemente un código auxiliar vacío. Pero dada la complejidad de muchas aplicaciones, esto suele ser demasiado restrictivo. Una buena regla general para DllMain es posponer la mayor cantidad de inicialización posible. La inicialización diferida aumenta la solidez de la aplicación porque esta inicialización no se realiza mientras se mantiene el bloqueo del cargador. Además, la inicialización diferida permite usar de forma segura muchos más elementos de la API de Windows.
Algunas tareas de inicialización no se pueden posponer. Por ejemplo, una biblioteca de vínculos dinámicos que depende de un archivo de configuración no se puede cargar si el archivo tiene un formato incorrecto o contiene elementos no utilizados. Para este tipo de inicialización, la biblioteca de vínculos dinámicos debe intentar la acción y producir un error rápidamente en lugar de desperdiciar recursos completando otro trabajo.
Nunca debe realizar las siguientes tareas desde DllMain:
- Llame a LoadLibrary o a LoadLibraryEx (directa o indirectamente). Esto puede provocar un interbloqueo o un bloqueo.
- Llame a GetStringTypeA, GetStringTypeEx o a GetStringTypeW (directa o indirectamente). Esto puede provocar un interbloqueo o un bloqueo.
- Sincronice con otros subprocesos. Esto puede provocar un interbloqueo.
- Adquiera un objeto de sincronización que sea propiedad del código que está esperando adquirir el bloqueo del cargador. Esto puede provocar un interbloqueo.
- Inicialice subprocesos COM mediante CoInitializeEx. En determinadas condiciones, esta función puede llamar a LoadLibraryEx.
- Llame a las funciones del registro.
- Llame a CreateProcess. La creación de un proceso puede cargar otra biblioteca de vínculos dinámicos.
- Llame a ExitThread. Salir de un subproceso durante la desasociación de la biblioteca de vínculos dinámicos puede hacer que el bloqueo del cargador se vuelva a adquirir, lo que provoca un interbloqueo o un bloqueo.
- Llame a CreateThread. La creación de un subproceso puede funcionar si no se sincroniza con otros subprocesos, pero es arriesgado.
- Llame a ShGetFolderPathW. Llamar a las API de carpeta conocidas o de shell puede dar lugar a la sincronización de subprocesos y, por tanto, puede provocar interbloqueos.
- Cree una canalización con nombre u otro objeto con nombre (solo Windows 2000). En Windows 2000, la biblioteca de vínculos dinámicos de Terminal Services proporciona objetos con nombre. Si no se inicializa esta biblioteca de vínculos dinámicos, las llamadas a la biblioteca de vínculos dinámicos pueden provocar que el proceso se bloquee.
- Use la función de administración de memoria desde el tiempo de ejecución dinámico de C (CRT). Si la biblioteca de vínculos dinámicos de CRT no se inicializa, las llamadas a estas funciones pueden provocar que el proceso se bloquee.
- Llame a funciones en User32.dll o Gdi32.dll. Algunas funciones cargan otra biblioteca de vínculos dinámicos, que es posible que no se inicialice.
- Use código administrado.
Las siguientes tareas se pueden realizar de forma segura en DllMain:
- Inicializar estructuras de datos estáticas y miembros en tiempo de compilación.
- Crear e inicializar objetos de sincronización.
- Asignar memoria e inicializar estructuras de datos dinámicas (evitando las funciones enumeradas anteriormente).
- Configurar el almacenamiento local para el subproceso (TLS).
- Abrir archivos, leer desde estos y escribir en ellos.
- Llamar a funciones en Kernel32.dll (excepto las funciones enumeradas anteriormente).
- Establecer punteros globales en NULL y desactivar la inicialización de miembros dinámicos. En Microsoft Windows Vista™, puede usar las funciones de inicialización única para asegurarse de que un bloque de código se ejecuta solo una vez en un entorno multiproceso.
Interbloqueos causados por la inversión de orden de bloqueo
Al implementar código que usa varios objetos de sincronización como, por ejemplo, bloqueos, es fundamental respetar el orden de bloqueo. Cuando sea necesario adquirir más de un bloqueo a la vez, debe definir una precedencia explícita denominada jerarquía de bloqueo u orden de bloqueo. Por ejemplo, si el bloqueo A se adquiere antes del bloqueo B en algún lugar del código y el bloqueo B se adquiere antes del bloqueo C en otro lugar del código, el orden de bloqueo es A, B, C y este orden debe seguirse en todo el código. La inversión del orden de bloqueo se produce cuando no se sigue el orden de bloqueo; por ejemplo, si se adquiere el bloqueo B antes del bloqueo A. La inversión del orden de bloqueo puede provocar interbloqueos difíciles de depurar. Para evitar estos problemas, todos los subprocesos deben adquirir los bloqueos en el mismo orden.
Es importante tener en cuenta que el cargador llama a DllMain con el bloqueo del cargador ya adquirido, por lo que el bloqueo del cargador debe tener la prioridad más alta en la jerarquía de bloqueo. Tenga en cuenta también que el código solo tiene que adquirir los bloqueos que requiere para la sincronización adecuada; no tiene que adquirir todos los bloqueos definidos en la jerarquía. Por ejemplo, si una sección de código solo requiere bloqueos A y C para la sincronización adecuada, el código debe adquirir el bloqueo A antes de adquirir el bloqueo C; no es necesario que el código también adquiera el bloqueo B. Además, el código de biblioteca de vínculos dinámicos no puede adquirir explícitamente el bloqueo del cargador. Si el código debe llamar a una API como GetModuleFileName que pueda adquirir indirectamente el bloqueo del cargador y el código también debe adquirir un bloqueo privado, el código debe llamar a GetModuleFileName antes de adquirir el bloqueo P, lo que garantiza que se respeta el orden de carga.
La figura 2 es un ejemplo que muestra la inversión del orden de bloqueo. Considere una biblioteca de vínculos dinámicos cuyo subproceso principal contenga DllMain. El cargador de biblioteca adquiere el bloqueo del cargador L y, después, llama a DllMain. El subproceso principal crea objetos de sincronización A, B y G para serializar el acceso a sus estructuras de datos e intenta adquirir el bloqueo G. Un subproceso de trabajo que ya ha adquirido correctamente el bloqueo G llama a una función como GetModuleHandle, que intenta adquirir el bloqueo del cargador L. Por lo tanto, el subproceso de trabajo está bloqueado en L y el subproceso principal está bloqueado en G, lo que da lugar a un interbloqueo.
Para evitar los interbloqueos que causa la inversión del orden de bloqueo, todos los subprocesos deben intentar adquirir objetos de sincronización en el orden de carga definido en todo momento.
Procedimientos recomendados para la sincronización
Considere una biblioteca de vínculos dinámicos que crea subprocesos de trabajo como parte de su inicialización. Tras la limpieza de la biblioteca de vínculos dinámicos, es necesario sincronizar todos los subprocesos de trabajo para asegurarse de que las estructuras de datos están en un estado coherente y, después, finalizar los subprocesos de trabajo. En la actualidad, no hay ninguna manera sencilla de resolver completamente el problema de sincronizar y apagar las bibliotecas de vínculos dinámicos de forma limpia en un entorno multiproceso. En esta sección se describen los procedimientos recomendados actuales para la sincronización de subprocesos durante el apagado de la biblioteca de vínculos dinámicos.
Sincronización de subprocesos en DllMain durante la salida del proceso
- En el momento en que se llama a DllMain en la salida del proceso, todos los subprocesos del proceso se han limpiado forzosamente y existe la posibilidad de que el espacio de direcciones sea incoherente. En este caso, la sincronización no es necesaria. En otras palabras, el controlador de DLL_PROCESS_DETACH ideal está vacío.
- Windows Vista garantiza que las estructuras de datos principales (variables de entorno, directorio actual, montón de procesos, etc.) se encuentran en un estado coherente. Pero otras estructuras de datos se pueden dañar, por lo que la limpieza de la memoria no es segura.
- El estado persistente que debe guardarse debe vaciarse en el almacenamiento permanente.
Sincronización de subprocesos en DllMain para DLL_THREAD_DETACH durante la descarga de la biblioteca de vínculos dinámicos
- Cuando se descarga la biblioteca de vínculos dinámicos, el espacio de direcciones no se desecha. Por lo tanto, se espera que la biblioteca de vínculos dinámicos realice un apagado limpio. Esto incluye la sincronización de subprocesos, los controladores abiertos, el estado persistente y los recursos asignados.
- La sincronización de subprocesos es complicada porque esperar a que los subprocesos salgan en DllMain puede provocar un interbloqueo. Por ejemplo, la biblioteca de vínculos dinámicos A contiene el bloqueo del cargador. Indica que el subproceso T va a salir y espera a que el subproceso salga. El subproceso T sale y el cargador intenta adquirir el bloqueo del cargador para llamar a DllMain de la biblioteca de vínculos dinámicos A con DLL_THREAD_DETACH. Esto provoca un interbloqueo. Para minimizar el riesgo de un interbloqueo, haga lo siguiente:
- La biblioteca de vínculos dinámicos A obtiene un mensaje DLL_THREAD_DETACH en su DllMain y establece un evento para el subproceso T, que le indica que va a salir.
- El subproceso T finaliza su tarea actual, se pone en un estado coherente, señala la biblioteca de vínculos dinámicos A y espera infinitamente. Tenga en cuenta que las rutinas de comprobación de coherencia deben seguir las mismas restricciones que DllMain para evitar interbloqueos.
- La biblioteca de vínculos dinámicos A finaliza T, sabiendo que está en un estado coherente.
Si se descarga una biblioteca de vínculos dinámicos después de crear todos sus subprocesos, pero antes de empezar a ejecutarse, es posible que los subprocesos se bloqueen. Si la biblioteca de vínculos dinámicos creó subprocesos en su DllMain como parte de su inicialización, es posible que algunos subprocesos no hayan terminado de inicializarse y su mensaje DLL_THREAD_ATTACH siga esperando que se entregue a la biblioteca de vínculos dinámicos. En esta situación, si la biblioteca de vínculos dinámicos se descarga, comenzará a terminar los subprocesos. Pero algunos subprocesos pueden bloquearse detrás del bloqueo del cargador. Sus mensajes DLL_THREAD_ATTACH se procesan después de que la biblioteca de vínculos dinámicos se haya desasignado, lo que hace que el proceso se bloquee.
Recomendaciones
Se recomiendan las directrices siguientes:
- Use Application Verifier para detectar los errores más comunes en DllMain.
- Si usa un bloqueo privado dentro de DllMain, defina una jerarquía de bloqueo y úsela de forma coherente. El bloqueo del cargador debe estar en la parte inferior de esta jerarquía.
- Compruebe que ninguna llamada depende de otra biblioteca de vínculos dinámicos que aún no se haya cargado completamente.
- Realice inicializaciones sencillas de forma estática en tiempo de compilación, en lugar de en DllMain.
- Aplace las llamadas en DllMain que puedan hacerse más adelante.
- Aplace las tareas de inicialización que puedan hacerse más adelante. Se deben detectar ciertas condiciones de error al principio para que la aplicación pueda controlar los errores correctamente. Pero hay inconvenientes entre esta detección temprana y la pérdida de solidez que puede resultar de ella. Aplazar la inicialización suele ser la mejor opción.