DllMain y el bloqueo del cargador — La razón real de que le digan «no haga nada en la inicialización de la DLL»

· · Windows, DLL, Desarrollo en Windows, C++, Investigación de fallos, Multithreading, Win32 API

«La aplicación se cuelga al arrancar, pero solo en un entorno concreto.» «Cuando cargamos nuestra propia DLL, LoadLibrary a veces no regresa nunca.» «Se interbloquea solo en el momento de arrancar un servicio.» — Siga investigaciones como estas lo bastante lejos y, más a menudo que no, llega al mismo sitio. El código de inicialización de la DLL — es decir, DllMain.

La documentación de Microsoft advierte sobre DllMain con un tono inusualmente fuerte. No llame a LoadLibrary. No sincronice con otros hilos. No llame a funciones de User, Shell o COM. El DllMain ideal es un stub vacío — ¿por qué el lenguaje es tan fuerte? La razón se concentra en un solo mecanismo interno, el bloqueo del cargador (loader lock). Dirigido a desarrolladores que escriben DLL, complementos y envoltorios C++/CLI en Windows, este artículo explica, a partir de fuentes primarias, cómo funciona el bloqueo del cargador, la estructura que hace que un interbloqueo se sostenga, y el diseño seguro y el procedimiento de investigación.

1. Conclusión principal

  • DllMain se llama sosteniendo el bloqueo del cargador, un bloqueo compartido del que hay exactamente uno por proceso. Así que llamar, desde DllMain, a un trabajo que intenta tomar el bloqueo del cargador (de forma directa o indirecta) crea la posibilidad de un interbloqueo, o de un fallo por tocar una DLL que aún no se ha inicializado.1
  • Llamar a LoadLibrary / FreeLibrary está prohibido. Crea una dependencia circular de orden de carga y puede hacer que se ejecute código de inicialización contra una DLL cuya propia inicialización aún no se ha ejecutado.2
  • Sincronizar con otros hilos también está prohibido. Las notificaciones de DLL se serializan, así que esperar dentro de DllMain a que un hilo arranque o salga deja a ese hilo mismo detenido esperando el bloqueo del cargador, y se interbloquea.23
  • Lo que puede llamar con seguridad es, en la práctica, solo un subconjunto de Kernel32.dll. Y la documentación oficial afirma con claridad que «no existe una lista completa de funciones seguras». Las funciones de User, Shell y COM cargan otros componentes y provocan violaciones de acceso.2
  • En una DLL enlazada con el CRT, las mismas restricciones se aplican a los constructores y destructores de globales. Se ejecutan como una parte de facto de DllMain.2
  • El diseño correcto es «diferir». Haga la inicialización que pueda en tiempo de compilación (de forma estática); difiera lo que no pueda hasta el primer uso. Esa es la mejor práctica oficial.1
  • Las DLL mixtas C++/CLI son especialmente peligrosas. Para evitar ejecutar MSIL bajo el bloqueo del cargador, DllMain y su árbol de llamadas deben compilarse nativos.4

2. Cuándo y cómo se llama a DllMain

DllMain es el punto de entrada que el cargador del sistema operativo llama cuando una DLL entra o sale de un proceso o de un hilo. Hay cuatro notificaciones.

Notificación Momento
DLL_PROCESS_ATTACH Cuando la DLL se carga en el proceso
DLL_THREAD_ATTACH Cuando se arranca un hilo nuevo en el proceso
DLL_THREAD_DETACH Cuando un hilo sale de forma normal
DLL_PROCESS_DETACH Cuando se descarga la DLL, o cuando el proceso sale

Hay dos hechos fáciles de pasar por alto. Primero, cada vez que se crea un solo hilo, se llama al DllMain de todas las DLL ya cargadas con DLL_THREAD_ATTACH. En otras palabras, DllMain no es «algo que se ejecuta una vez cuando se carga mi DLL»; es código que sigue siendo llamado por la actividad de hilos del proceso. Si no lo necesita, puede detenerlo llamando a DisableThreadLibraryCalls dentro de DLL_PROCESS_ATTACH (no la llame desde una DLL enlazada con el CRT estático).5

Segundo, en una DLL enlazada con el CRT (el runtime de C/C++), los constructores y destructores de objetos globales y estáticos de C++ se ejecutan, a través del punto de entrada del CRT, como una parte de DllMain.2 Aunque piense «nuestro DllMain está vacío, así que estamos a salvo», un objeto global con una inicialización elaborada es lo mismo que ejecutar ese trabajo en DllMain.

Los cuatro momentos en que se llama a DllMainDLL_PROCESS_ATTACH se ejecuta al cargar la DLL; DLL_THREAD_ATTACH y DETACH se ejecutan en cada DLL ya cargada en cada arranque y salida de hilo del proceso; DLL_PROCESS_DETACH se ejecuta al descargar o al salir del proceso; y los constructores de objetos estáticos también se ejecutan dentro de esto a través del CRTCarga de la DLLDLL_PROCESS_ATTACHDLL_THREAD_ATTACH (en cada arranque de hilo)DLL_THREAD_DETACH (en cada salida de hilo)DLL_PROCESS_DETACH (al descargar o al salir)La construcción de objetos estáticos también se ejecuta aquí

Figura 1: DllMain se llama no solo en el momento de la carga sino en cada arranque y salida de hilo, y la inicialización de objetos estáticos también se ejecuta como parte de eso.

3. El bloqueo del cargador — Un bloqueo que serializa todas las notificaciones

¿Por qué las restricciones sobre DllMain son tan severas ellas solas? La respuesta está en la estructura del cargador.

Para mantener coherente una serie de operaciones — carga de DLL, descarga y las distintas notificaciones —, el cargador del sistema operativo serializa el trabajo con un único bloqueo del cargador por proceso. Y el punto importante es que DllMain se llama mientras se sostiene este bloqueo del cargador.1 Mientras esté dentro de DllMain, cualquier otra carga de DLL en ese proceso, y cualquier notificación de arranque de hilo, espera a que se libere este bloqueo.

A partir de esa estructura, las razones de las prohibiciones se siguen una tras otra.

  • No debe llamar a LoadLibrary porque crea reentrada del bloqueo del cargador, o una dependencia circular de orden de carga. También puede resultar en llamar a una función de una DLL cuya inicialización aún no ha terminado.2
  • Sincronizar con otros hilos es peligroso porque el hilo que espera tiene momentos en que necesita el bloqueo del cargador (notificaciones al arrancar y al salir, llamadas a APIs de la familia GetModuleHandle, etc.). Usted sostiene el bloqueo del cargador y espera al otro lado; el otro lado espera el bloqueo del cargador — la inversión clásica del orden de bloqueos.6
  • Las funciones de User, Shell y COM son peligrosas porque cargan internamente otros componentes del sistema. Toca un componente antes de que se inicialice, o después de que se haya desmontado, y obtiene una violación de acceso.2
Por qué esperar a un hilo dentro de DllMain se interbloqueaDllMain, sosteniendo el bloqueo del cargador, espera a que un hilo trabajador salga, pero el trabajador que intenta salir espera a que se libere el bloqueo del cargador para poder recibir DLL_THREAD_DETACH, así que se esperan mutuamente y se interbloqueanTrabajadorDllMainCargador(tiene el bloqueo)TrabajadorDllMainCargador(tiene el bloqueo)La notificación de salida necesita el bloqueoDllMain tiene el bloqueo, W esperaDLL_PROCESS_DETACHPedir salida y esperarTerminar el trabajo y luego salir

Figura 2: «DllMain espera a que un hilo salga» es un interbloqueo estructural, porque la propia salida del hilo necesita el bloqueo del cargador.

El punto es que esto no es de lo que «ocurre si tiene mala suerte»; está estructuralmente garantizado que se sostenga. La documentación le dice que trate el bloqueo del cargador como la cima de la jerarquía de bloqueos que define la aplicación (el que se toma primero). Dentro de DllMain ya sostiene ese bloqueo de nivel superior, así que cualquier acto de seguir desde ahí a esperar otra cosa es peligroso — esa es una forma útil de recordarlo.6

Inversión del orden de bloqueos entre el bloqueo del cargador y un bloqueo privadoDllMain, sosteniendo el bloqueo del cargador, va a tomar un bloqueo privado, mientras un trabajador, sosteniendo ese bloqueo privado, va a tomar el bloqueo del cargador para GetModuleHandle o similar, así que el orden de adquisición se invierte y se interbloqueanDllMain: sosteniendo el bloqueo del cargadorVa a tomar el bloqueo privado GTrabajador: sosteniendo el bloqueo privado GVa a tomar el bloqueo del cargadorInterbloqueo por orden de adquisición invertidoGetModuleHandle y similares lo exigen internamente

Figura 3: Incluso una API inocua como GetModuleHandle exige el bloqueo del cargador internamente, así que una inversión de orden con un bloqueo privado puede sostenerse.

Además, llamar a CreateThread desde dentro del propio DllMain no se recomienda. El hilo creado necesita el bloqueo del cargador para procesar la notificación DLL_THREAD_ATTACH, así que no puede empezar a ejecutarse hasta que el DllMain que se está ejecutando en ese momento regrese y libere el bloqueo. Por tanto, esperar dentro de DllMain a que ese hilo arranque o termine es un interbloqueo inmediato. También hay un problema de vida útil — si, después de que DllMain regrese, se descarga la DLL mientras aún queda atrás un hilo que aún no ha empezado a ejecutarse, la dirección de arranque del hilo sigue apuntando a código ya liberado y falla.3

4. Dos minas que los desarrolladores de C++ pisan con facilidad

Mina 1: Inicialización dinámica de objetos globales. Como decía el capítulo 2, los constructores de objetos estáticos se ejecutan bajo las restricciones de DllMain. Leer un archivo de configuración, levantar una facilidad de registro, inicializar COM, arrancar un hilo — el momento en que pone en una DLL un global cuyo constructor hace ese tipo de trabajo, está ejecutando «cosas que no debe hacer en DllMain». La inicialización constante que se fija en tiempo de compilación (cualquier cosa que pueda hacer constexpr) es segura; la inicialización que involucra una llamada a función debe diferirse.

El camino por el que inicializar un objeto global se convierte en una minaEl bloqueo del cargador se toma al cargar la DLL, y los constructores de objetos globales se ejecutan a través del punto de entrada del CRT, así que LoadLibrary, la sincronización de hilos y la inicialización de COM dentro de esos constructores son ejecuciones de las prohibiciones de DllMainCarga de la DLL (bloqueo del cargador adquirido)Punto de entrada del CRTConstructor de un objeto globalTrabajo equivalente a LoadLibraryArrancar un hilo y esperar a que termineUsar COM o User32Todas estas caen bajo las prohibiciones de DllMain

Figura 4: Incluso «DllMain está vacío, así que estamos a salvo» revive el mismo peligro en el momento en que tiene un global con una inicialización elaborada.

Mina 2: C++/CLI (ensamblados mixtos). En una configuración que envuelve una DLL nativa con C++/CLI (la forma cubierta en el artículo del envoltorio), hay un peligro de ejecutar MSIL (código administrado) bajo el bloqueo del cargador. Ejecutar MSIL puede disparar la inicialización del CLR o la carga de otro ensamblado. El compilador emite el aviso C4747 en el código donde DllMain ejecuta MSIL de forma directa, pero no puede detectar la ejecución indirecta a través de una función de otro módulo. Compile DllMain y las funciones llamadas desde él como nativos con #pragma unmanaged, o use una configuración que no tenga un DllMain en absoluto.4

Si se puede detectar la ejecución de MSIL bajo el bloqueo del cargadorEl código en el que DllMain ejecuta MSIL de forma directa lo puede detectar el compilador con el aviso C4747, pero la ejecución indirecta a través de una función de otro módulo no, así que hay que impedirlo revisando el árbol de llamadas e insistiendo en una compilación nativaLlamadas desde DllMainEjecutar MSIL de forma directaEjecutar a través de otro móduloDetectable con el aviso C4747El compilador no puede detectarloImpedirlo con revisión y #pragma unmanaged

Figura 5: C4747 solo le protege contra la ejecución directa. Los caminos indirectos solo se pueden cazar con revisión.

5. El diseño correcto — Hacer de «diferir» la política predeterminada

La recomendación oficial de mejores prácticas es clara.1

  1. Termine la inicialización que pueda en tiempo de compilación (de forma estática). Primero pregúntese si una inicialización dinámica puede sustituirse por una estática.
  2. Difiera el resto hasta el primer uso. Mientras el primer uso ocurra desde una API ordinaria que se llama después de que la DLL haya terminado de cargarse, la inicialización se ejecuta fuera del bloqueo del cargador y puede usar con seguridad casi toda la API de Windows. Para la exclusión en el primer acceso puede usar INIT_ONCE (inicialización de una sola vez) o los magic statics de C++ (estáticos locales de función). El aplazamiento no es una panacea, no obstante — si ese primer acceso se hace desde DllMain o un inicializador estático, el inicializador sigue ejecutándose bajo el bloqueo del cargador y vuelve a las mismas restricciones.
  3. Haga una excepción solo para los fallos que debe detectar pronto. Puede tener el requisito de que un archivo de configuración roto haga fallar la propia carga. Incluso entonces, limítelo al mínimo de «intentar y fallar de inmediato».
  4. Considere DisableThreadLibraryCalls en DLL_PROCESS_ATTACH. Si la DLL no usa las notificaciones de hilo, puede eliminar el propio coste de la notificación (excepto al usar el CRT estático o TLS estático).5
  5. Inspeccione con Application Verifier. Muchas de las llamadas peligrosas dentro de DllMain son las que Application Verifier detectará en tiempo de ejecución.1
Guía de diseño para la inicialización de DLLPrimero considere si la inicialización puede ser inicialización estática en tiempo de compilación; si no, el valor predeterminado es diferir al primer uso, y dejar en DllMain solo el mínimo que debe detectarse pronto como un fallo de carganono¿Se puede decidir en tiempo de compilación?Hacerla inicialización estática¿Debe detectarse el fallo en el momento de la carga?Diferir al primer uso (el valor predeterminado)Hacer solo el mínimo en DllMainExcluir con INIT_ONCE o un estático local de función

Figura 6: El orden de decisión es «¿puede ser estática → puede diferirse?», y lo que deja en DllMain es solo el mínimo que debe detectarse pronto.

Si aplicar DisableThreadLibraryCalls se puede decidir de forma mecánica con la siguiente ramificación.

Si llamar a DisableThreadLibraryCallsNo la llame desde una DLL enlazada con el CRT estático; si hay TLS estático la propia llamada falla, así que no la llama; si no se aplica ninguna de las dos y la DLL no usa notificaciones de hilo, llámela en DLL_PROCESS_ATTACH, comprobando el valor de retorno, para recortar el coste de la notificaciónnonono¿Enlazada con el CRT estático?No debe llamarla¿Usa TLS estático?La llamada falla de todos modos (FALSE)¿Se necesitan las notificaciones de hilo?Llamarla en ATTACH (comprobar el valor de retorno)No llamarla; manejar las notificaciones

Figura 7: Las tres condiciones de CRT estático, TLS estático y si se necesitan las notificaciones deciden de forma única si debe llamarla.

Para detener hilos al descargar, la documentación oficial da un protocolo concreto. En lugar de «esperar» a que los hilos trabajadores salgan en DLL_PROCESS_DETACH (en una descarga mediante FreeLibrary), la forma es (1) señalar la salida con un evento, (2) el lado del hilo pliega su trabajo hasta un estado coherente, señala de vuelta y entra en una espera infinita, (3) el lado de DllMain confirma el estado coherente y luego pliega el hilo con TerminateThread.3 Parece tosco, pero está documentado como la respuesta realista dentro de la restricción «no debe esperar la salida natural de un hilo dentro de DllMain».

Protocolo para detener un hilo al descargarDllMain señala al hilo trabajador que salga con un evento; el trabajador pliega su trabajo hasta un estado coherente, señala de vuelta y entra en una espera infinita; DllMain confirma el estado coherente y luego termina el hiloHilo trabajadorDllMain (manejo de DETACH)Hilo trabajadorDllMain (manejo de DETACH)No hay espera de salida natural, así que no hay interbloqueoSeñalar la salida con un eventoPlegar el trabajo hasta un estado coherenteSeñalar coherencia completa y esperar para siempreTerminar con TerminateThread

Figura 8: En lugar de «esperar una salida natural», «esperar una señal de coherencia y luego cortarlo» evita una colisión con el bloqueo del cargador.

Como cuestión de primeros principios, el diseño más seguro es evitar poseer hilos en una DLL que se pueda descargar, y mantener la posesión de los hilos del lado del EXE.

DLL_PROCESS_DETACH a la salida del proceso es lo contrario: no hacer nada y regresar es lo ideal. En este punto todos los demás hilos ya han sido terminados de forma forzosa, y tampoco puede fiarse del estado de las DLL dependientes ni del runtime. Un trabajo elaborado aquí solo provoca interbloqueos y fallos. Los datos que deban persistirse deben escribirse en la propia ruta de apagado de la aplicación; no dependa de esta notificación.3

6. Cómo investigar cuando se topa con ello

Los cuelgues del bloqueo del cargador tienen una huella reconocible.

Mire las pilas en un volcado del cuelgue. Tome un volcado del momento congelado e inspeccione la pila de cada hilo. Si encuentra un par de un hilo esperando un bloqueo dentro de funciones del cargador de ntdll.dll (la familia cuyos nombres empiezan por Ldr) y un hilo esperando otra cosa dentro de DllMain o un inicializador estático (dynamic initializer), está casi seguro. Un hilo detenido en medio de una llamada a LoadLibrary es otro personaje típico.

La huella de un cuelgue del bloqueo del cargadorEn un volcado del cuelgue, si encuentra tanto un hilo esperando un bloqueo dentro de funciones del cargador de ntdll como un hilo esperando otra cosa dentro de DllMain o un inicializador estático, puede tratarlo como un interbloqueo del bloqueo del cargador con casi certezanoVolcado del cuelgueHilo esperando un bloqueo en funciones de la familia LdrHilo esperando dentro de DllMain o un inicializador estático¿Están ambos presentes?Casi con certeza un interbloqueo del bloqueo del cargadorInvestigar como un cuelgue de otro tipo

Figura 9: Los cuelgues del bloqueo del cargador tienen la huella reconocible «esperando en Ldr + esperando dentro de DllMain».

Sospeche el carácter «dependiente del momento». Un interbloqueo del bloqueo del cargador se sostiene solo en el momento en que una carga de DLL coincide con un arranque o una salida de hilo. Condiciones de reproducción como «de vez en cuando al arrancar», «solo en una máquina concreta» y «solo cuando se ejecuta como servicio» son señales de este tipo de problema.

Ejecute una inspección preventiva. Active Application Verifier y ejecute sus pruebas, y puede detectar llamadas peligrosas dentro de DllMain en tiempo de ejecución.1 Para C++/CLI, no ignore el aviso C4747; en las revisiones de funciones alcanzables desde DllMain, añada el ángulo de «funciones que llaman a LoadLibrary de forma indirecta» (inicialización de COM, algunas características del CRT, importaciones de carga diferida, etc.) a la lista de comprobación de la revisión, y cazará accidentes antes de enviarlos. La primera llamada de una importación de carga diferida convirtiéndose internamente en LoadLibrary es un punto fácil de pasar por alto.

7. Resumen

  • DllMain se llama sosteniendo el bloqueo del cargador (uno por proceso, el bloqueo que serializa todas las notificaciones de DLL). Todas las restricciones se siguen de eso.
  • El núcleo de las prohibiciones es «no llame a LoadLibrary / FreeLibrary», «no sincronice con otros hilos» y «no llame a funciones que dependen de una DLL distinta de Kernel32». Los constructores y destructores de objetos estáticos que se ejecutan a través del CRT caen bajo las mismas restricciones.
  • La política básica de diseño es el aplazamiento. Haga estática la inicialización que pueda hacer estática; difiera el resto al primer uso. Use DisableThreadLibraryCalls y Application Verifier.
  • Detener hilos al descargar sigue el protocolo oficial (señalar → confirmar coherencia → terminar). DLL_PROCESS_DETACH a la salida del proceso está idealmente vacío.
  • En C++/CLI, ejecutar MSIL bajo el bloqueo del cargador es una mina por sí sola. Insista en una compilación nativa del árbol de llamadas de DllMain.

Las restricciones de DllMain parecen, al principio, una lista irrazonable de prohibiciones. Pero una vez que sostiene el único punto de que «se llama sosteniendo el bloqueo del cargador, el bloqueo de nivel superior», cada prohibición es una reformulación del mismo principio. Recuérdelo como un principio y, cuando se encuentre un caso límite que no está en la documentación, debería poder seguir haciendo la pregunta correcta: «¿Es este un trabajo que me está permitido hacer mientras sostengo el bloqueo?».

Artículos relacionados

Áreas de consultoría relacionadas

En KomuraSoft LLC nos encargamos de la investigación de causa raíz de cuelgues e interbloqueos en el arranque o en el momento de cargar una DLL (análisis de volcados), de revisiones de diseño en torno a DllMain y la inicialización estática, y de remediar envoltorios C++/CLI y DLL de complemento hacia un diseño de inicialización seguro. Puede consultarnos incluso en la etapa difícil de reproducir de «solo se cuelga al arrancar en un entorno concreto».

Referencias

  1. Microsoft Learn, Dynamic-Link Library Best Practices. Sobre que DllMain se llama mientras se sostiene el bloqueo del cargador, de modo que las funciones que puede llamar están severamente restringidas; que el DllMain ideal es un stub vacío y la inicialización se difiere tanto como sea posible; la recomendación de inicialización estática en tiempo de compilación; hacer solo el mínimo para los fallos que deben detectarse pronto; y detectar errores típicos de DllMain con Application Verifier.  2 3 4 5 6

  2. Microsoft Learn, DllMain entry point. Sobre realizar solo inicialización y terminación simples en el punto de entrada; por qué no debe llamar a LoadLibrary / FreeLibrary (orden de carga circular y uso de una DLL antes de la inicialización o después de la terminación); que Kernel32.dll está garantizado como ya cargado, de modo que puede llamarlo en el rango que no carga otras DLL; que no hay una lista exhaustiva de funciones seguras; que las funciones de User, Shell y COM provocan violaciones de acceso; que las notificaciones de DLL se serializan, de modo que la comunicación con otros hilos o procesos provoca interbloqueos; y que las mismas restricciones se aplican a los constructores y destructores de objetos estáticos cuando se enlaza el CRT.  2 3 4 5 6 7

  3. Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. Sobre la estructura que se interbloquea si espera a que un hilo salga dentro de DllMain (la notificación DLL_THREAD_DETACH de la salida del hilo necesita el bloqueo del cargador); el protocolo para detener un hilo al descargar (señalar con un evento, confirmar un estado coherente y luego terminar); que DLL_PROCESS_DETACH a la salida del proceso tiene los demás hilos ya terminados de forma forzosa y no hay garantía de coherencia del espacio de direcciones, de modo que el manejador ideal está vacío; y que crear un hilo en DllMain deja notificaciones en cola con la inicialización incompleta y provoca problemas.  2 3 4

  4. Microsoft Learn, Initialization of Mixed Assemblies. Sobre no ejecutar MSIL bajo el bloqueo del cargador; no compilar DllMain y su árbol de llamadas a MSIL y tratar eso mediante #pragma unmanaged; que se emite el aviso C4747 cuando DllMain intenta ejecutar MSIL de forma directa, pero la ejecución indirecta a través de otro módulo no es detectable; y que los inicializadores dinámicos de objetos estáticos pueden provocar el mismo problema.  2

  5. Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h). Sobre deshabilitar las notificaciones DLL_THREAD_ATTACH / DLL_THREAD_DETACH para reducir la sobrecarga al crear y destruir hilos; no llamarla desde una DLL enlazada con el CRT estático; y que la optimización no se realiza cuando hay TLS estático (thread_local o __declspec(thread)).  2

  6. Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. Sobre definir una jerarquía de bloqueos y adquirir siempre en el mismo orden; que el cargador adquiere el bloqueo del cargador antes de llamar a DllMain, de modo que el bloqueo del cargador debería sentarse en la cima de la jerarquía de bloqueos; observar el orden de adquisición entre APIs que toman el bloqueo del cargador de forma indirecta, como GetModuleFileName, y bloqueos privados; y un ejemplo concreto de un interbloqueo por inversión del orden de bloqueos.  2

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿De verdad no se puede hacer nada en absoluto en DllMain?
«No haga nada» no es hipérbole; es la postura oficial de diseño, y el propio Microsoft dice que el DllMain ideal es un stub casi vacío. Lo que es seguro es un subconjunto de funciones de Kernel32.dll — Kernel32 está garantizado como cargado para cuando se ejecuta DllMain — en el rango que no carga otras DLL. Crear una sección crítica o un mutex, y usar TLS, son ejemplos de lo que puede hacer. A la inversa, LoadLibrary/FreeLibrary, sincronizar con otros hilos y llamar a funciones de User32, Shell, COM y similares están prohibidos porque provocan interbloqueos y violaciones de acceso. La inicialización de la que no esté seguro no debe hacerse en DllMain; difiérala hasta la primera vez que se use.
¿Los constructores de globales de C++ (objetos estáticos) también caen bajo las restricciones de DllMain?
Sí. Cuando la DLL se enlaza con el CRT (el runtime de C++), los constructores y destructores de objetos globales y estáticos se ejecutan, a través del punto de entrada que proporciona el CRT, como una parte de facto de DllMain. Eso significa que llamar a LoadLibrary desde un constructor, arrancar otro hilo y esperar a que termine, inicializar COM, etc., conllevan el mismo peligro que hacer esas cosas en DllMain. Para un objeto global con inicialización no trivial, conserve un puntero y constrúyalo en el primer acceso, o use un estático local de función, de modo que el trabajo se ejecute fuera de DllMain.
¿Debo llamar a DisableThreadLibraryCalls?
De forma condicional, sí. Si la DLL no necesita las notificaciones DLL_THREAD_ATTACH/DETACH, llamar a DisableThreadLibraryCalls en DLL_PROCESS_ATTACH detiene las notificaciones por creación y por salida de hilo y reduce la sobrecarga en un proceso que crea hilos con frecuencia. Hay dos excepciones. No la llame desde una DLL enlazada con el CRT estático (el CRT estático necesita las notificaciones de hilo). Y si hay TLS estático mediante thread_local o __declspec(thread), la propia llamada falla y devuelve FALSE, así que adquiera el hábito de comprobar el valor de retorno. Úsela en una DLL típica que usa el CRT enlazado de forma dinámica, después de haber confirmado que nada depende de las notificaciones de hilo.
¿Por qué se cuelga al arrancar una DLL C++/CLI (mixta administrada)?
La causa típica es intentar ejecutar MSIL (código administrado) mientras se sostiene el bloqueo del cargador. En un ensamblado mixto C++/CLI, si DllMain, las funciones llamadas desde él o los inicializadores dinámicos de globales se compilan a MSIL, puede exigirse la inicialización del CLR o la carga de otro ensamblado bajo el bloqueo del cargador, y eso puede interbloquearse. El compilador emite el aviso C4747 cuando el propio DllMain intenta ejecutar MSIL de forma directa, pero no puede detectar la ejecución indirecta a través de otro módulo. La mitigación es compilar DllMain y su árbol de llamadas como nativo con #pragma unmanaged — o no tener un DllMain en absoluto.
¿Puedo limpiar recursos en DLL_PROCESS_DETACH?
La respuesta cambia entre «salida del proceso» y «descarga mediante FreeLibrary». En DLL_PROCESS_DETACH a la salida del proceso, los demás hilos ya han sido terminados y no hay garantía de que el espacio de direcciones siga siendo coherente, así que una limpieza como liberar memoria es de hecho peligrosa; la guía oficial es que «el manejador ideal está vacío». Escriba los datos que deban persistirse en la propia ruta de apagado de la aplicación, y aquí esencialmente no haga nada y regrese. En una descarga mediante FreeLibrary, el proceso continúa, así que sí necesita una limpieza completa — detener hilos, cerrar identificadores, etc. Esperar a que un hilo salga dentro de DllMain se interbloquea, no obstante, así que debe seguir el protocolo oficial: señalar, esperar hasta un estado coherente y terminar el trabajo fuera de DllMain.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog