DllMain y el bloqueo del cargador — La razón real de que le digan «no haga nada en la inicialización de la DLL»
· Go Komura · 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
DllMainse llama sosteniendo el bloqueo del cargador, un bloqueo compartido del que hay exactamente uno por proceso. Así que llamar, desdeDllMain, 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/FreeLibraryestá 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
DllMaina 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,
DllMainy 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.
flowchart TB
accTitle: Los cuatro momentos en que se llama a DllMain
accDescr: DLL_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 CRT
load["Carga de la DLL"] --> pa["DLL_PROCESS_ATTACH"]
pa --> ta["DLL_THREAD_ATTACH (en cada arranque de hilo)"]
ta --> td["DLL_THREAD_DETACH (en cada salida de hilo)"]
td --> pd["DLL_PROCESS_DETACH (al descargar o al salir)"]
pa -.-> crt["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
LoadLibraryporque 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
sequenceDiagram
accTitle: Por qué esperar a un hilo dentro de DllMain se interbloquea
accDescr: DllMain, 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 interbloquean
participant L as Cargador(tiene el bloqueo)
participant D as DllMain
participant W as Trabajador
L->>D: DLL_PROCESS_DETACH
D->>W: Pedir salida y esperar
W->>W: Terminar el trabajo y luego salir
Note over W: La notificación de salida necesita el bloqueo
Note over D,W: DllMain tiene el bloqueo, W espera
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
flowchart TB
accTitle: Inversión del orden de bloqueos entre el bloqueo del cargador y un bloqueo privado
accDescr: DllMain, 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 interbloquean
d["DllMain: sosteniendo el bloqueo del cargador"] --> dg["Va a tomar el bloqueo privado G"]
w["Trabajador: sosteniendo el bloqueo privado G"] --> wl["Va a tomar el bloqueo del cargador"]
dg -.-> dead["Interbloqueo por orden de adquisición invertido"]
wl -.-> dead
wl -.-> api["GetModuleHandle 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.
flowchart TB
accTitle: El camino por el que inicializar un objeto global se convierte en una mina
accDescr: El 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 DllMain
load["Carga de la DLL (bloqueo del cargador adquirido)"] --> crt["Punto de entrada del CRT"]
crt --> ctor["Constructor de un objeto global"]
ctor --> ng1["Trabajo equivalente a LoadLibrary"]
ctor --> ng2["Arrancar un hilo y esperar a que termine"]
ctor --> ng3["Usar COM o User32"]
ng1 -.-> risk["Todas estas caen bajo las prohibiciones de DllMain"]
ng2 -.-> risk
ng3 -.-> risk
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
flowchart TB
accTitle: Si se puede detectar la ejecución de MSIL bajo el bloqueo del cargador
accDescr: El 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 nativa
d2["Llamadas desde DllMain"] --> dir["Ejecutar MSIL de forma directa"]
d2 --> ind["Ejecutar a través de otro módulo"]
dir --> c47["Detectable con el aviso C4747"]
ind --> nc["El compilador no puede detectarlo"]
nc -.-> rv["Impedirlo 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
- 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.
- 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 desdeDllMaino un inicializador estático, el inicializador sigue ejecutándose bajo el bloqueo del cargador y vuelve a las mismas restricciones. - 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».
- Considere
DisableThreadLibraryCallsen 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 - Inspeccione con Application Verifier. Muchas de las llamadas peligrosas dentro de
DllMainson las que Application Verifier detectará en tiempo de ejecución.1
flowchart TB
accTitle: Guía de diseño para la inicialización de DLL
accDescr: Primero 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 carga
q1{"¿Se puede decidir en tiempo de compilación?"} -->|"sí"| s["Hacerla inicialización estática"]
q1 -->|"no"| q2{"¿Debe detectarse el fallo en el momento de la carga?"}
q2 -->|"no"| lazy["Diferir al primer uso (el valor predeterminado)"]
q2 -->|"sí"| min["Hacer solo el mínimo en DllMain"]
lazy -.-> once["Excluir 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.
flowchart TB
accTitle: Si llamar a DisableThreadLibraryCalls
accDescr: No 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ón
q1{"¿Enlazada con el CRT estático?"} -->|"sí"| no2["No debe llamarla"]
q1 -->|"no"| q2{"¿Usa TLS estático?"}
q2 -->|"sí"| eff["La llamada falla de todos modos (FALSE)"]
q2 -->|"no"| q3{"¿Se necesitan las notificaciones de hilo?"}
q3 -->|"no"| yes["Llamarla en ATTACH (comprobar el valor de retorno)"]
q3 -->|"sí"| keep["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».
sequenceDiagram
accTitle: Protocolo para detener un hilo al descargar
accDescr: DllMain 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 hilo
participant D as DllMain (manejo de DETACH)
participant W as Hilo trabajador
D->>W: Señalar la salida con un evento
W->>W: Plegar el trabajo hasta un estado coherente
W->>D: Señalar coherencia completa y esperar para siempre
D->>W: Terminar con TerminateThread
Note over D,W: No hay espera de salida natural, así que no hay interbloqueo
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.
flowchart TB
accTitle: La huella de un cuelgue del bloqueo del cargador
accDescr: En 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 certeza
dump["Volcado del cuelgue"] --> t1["Hilo esperando un bloqueo en funciones de la familia Ldr"]
dump --> t2["Hilo esperando dentro de DllMain o un inicializador estático"]
t1 --> pair{"¿Están ambos presentes?"}
t2 --> pair
pair -->|"sí"| conf["Casi con certeza un interbloqueo del bloqueo del cargador"]
pair -->|"no"| other["Investigar 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
DllMainse 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
DisableThreadLibraryCallsy 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
- Cómo funciona la resolución de nombres de DLL en Windows - Orden de búsqueda y SxS
- Llamar DLL nativas desde C#: envoltorio C++/CLI frente a P/Invoke
- Buenas prácticas de multithreading en la práctica — Edición C++: eliminando los accidentes desde la estructura con RAII y jthread
- Despertares espurios — Por qué las variables de condición despiertan «sin haber sido notificadas» y cómo esperar correctamente en Windows
- WinDbg + SOS para leer volcados de memoria — introducción práctica al análisis tras la recolección
- Conocimientos básicos de STA/MTA en COM - El modelo de subprocesos y cómo evitar los bloqueos
Á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».
- Investigación de fallos y análisis de causas
- Consultoría técnica y revisión de diseño
- Desarrollo de aplicaciones Windows
- Contacto
Referencias
-
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
-
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
-
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
-
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
-
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
-
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 relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
La API del grupo de hilos de Win32 — Concurrencia sin crear hilos, con CreateThreadpoolWork
¿Dispersa llamadas a CreateThread por todo el código nativo? Este artículo explica la API del grupo de hilos de Win32 rediseñada en Vista...
Qué es realmente «No responde» — Cómo decide Windows que una aplicación se ha colgado, y cómo diseñar aplicaciones que no lo hagan
«No responde» de Windows es un mecanismo en el que el sistema operativo juzga que una ventana no ha recuperado un mensaje durante 5 segun...
Despertares espurios — Por qué las variables de condición despiertan «sin haber sido notificadas» y cómo esperar correctamente en Windows
La espera de una variable de condición puede regresar incluso cuando no ha llegado ninguna notificación (un despertar espurio). Este artí...
Canalizaciones con nombre en la práctica — El IPC estándar de Windows, del diseño a la seguridad
Guía práctica de las canalizaciones con nombre, el IPC estándar de Windows. Organiza, a partir de fuentes primarias, la elección entre mo...
Aplicaciones que se rompen al reanudar de la suspensión — Cómo funcionan los eventos de energía de Windows y cómo construir aplicaciones empresariales que los sobrevivan
Abrió el portátil y las conexiones de la aplicación empresarial estaban muertas: la causa es un diseño que nunca contempló la suspensión....
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Investigación de fallos y problemas prolongados
Fallos intermitentes, diagnóstico de comunicaciones, bloqueos prolongados y pruebas de rutas de error.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
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.