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

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

Historial de revisiones (primera versión, publicada el 22 Aug 2026)
Primera publicación
Citar este artículo(DOI (archivo registrado): 10.5281/zenodo.22176699)

Los DOI siguientes remiten a versiones ya archivadas y pueden diferir del texto actual. Para citar el texto actual, utilice la URL de esta página.

Go Komura (2026). DllMain y el bloqueo del cargador — La razón real de que le digan «no haga nada en la inicialización de la DLL». KomuraSoft LLC. https://comcomponent.com/es/blog/dllmain-loader-lock/

DOI (archivo registrado)
10.5281/zenodo.22176699
DOI (última versión registrada)
10.5281/zenodo.22176700

«Cuando cargamos nuestra propia DLL, LoadLibrary no regresa nunca.» «Solo se cuelga en ciertos PC, o solo al arrancar el servicio.» Ante fallos como estos, uno de los primeros sitios que conviene comprobar es el código de inicialización de la DLL: DllMain y todo lo que llama.

DllMain no es como una función de inicialización ordinaria de una aplicación. Es una función que el sistema operativo llama mientras sostiene el bloqueo del cargador, así que lo que puede hacer está muy restringido. La postura de Microsoft de que «el DllMain ideal es un stub vacío» viene de que esta restricción no afecta solo a su propia DLL, sino a las demás DLL e hilos del proceso.1

Este artículo se dirige a desarrolladores que escriben DLL, complementos y envoltorios C++/CLI en Windows. Recorre el tema en este orden: por qué se detiene, luego a dónde mover la inicialización, luego cómo finalizar, luego cómo investigar un cuelgue.

1. La conclusión primero — mantener DllMain pequeño y cambiar el momento de ejecución

En lugar de buscar una forma segura de escribir algo dentro de DllMain, el enfoque básico es reducir el trabajo que se ejecuta ahí. Decida qué se queda en el orden siguiente.1

Decisión Política básica Dónde leer más
Cuándo inicializar Hacer de forma estática lo que se pueda decidir en tiempo de compilación; diferir el resto al primer uso después de que termine la carga Capítulo 5
Qué llamar desde DllMain Evitar LoadLibrary / FreeLibrary, la sincronización con otros hilos y el uso de User, Shell, COM y similares. Comprobar también las llamadas indirectas Capítulo 3
Qué hacer al finalizar Separar el caso en que solo se descarga la DLL del caso en que sale el proceso entero Capítulo 6

Lo fácil de pasar por alto es que las mismas restricciones se aplican a los constructores y destructores de objetos globales y estáticos de C++. En C++/CLI, elimine también todo camino que ejecute MSIL bajo el bloqueo del cargador. Que el cuerpo de DllMain esté vacío no basta para juzgar.23

Si está investigando un cuelgue ahora mismo, empiece por el capítulo 7; si está revisando un diseño, lea los capítulos 2 a 6 en orden, y podrá emparejar cada prohibición con su remedio.

2. El mecanismo — DllMain se llama dentro del bloqueo del cargador

2.1 Se llama no solo en la carga, sino también al arrancar y al salir hilos

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.2

Notificación Momento
DLL_PROCESS_ATTACH Cuando la DLL se carga en el proceso
DLL_THREAD_ATTACH Cuando 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

Una notificación de arranque de hilo no va solo a la DLL que creó el hilo, sino a todas las DLL cargadas en el proceso. DllMain no es «código que se ejecuta una vez cuando se carga mi DLL». En un proceso que crea hilos con frecuencia, se ejecuta en cada notificación.2

Si no necesita las notificaciones, una opción es llamar a DisableThreadLibraryCalls dentro de DLL_PROCESS_ATTACH. No se puede usar, no obstante, en una DLL enlazada con el CRT estático, y el TLS estático impone una condición propia, así que la sección 5.3 trata esa decisión por separado.4

2.2 Que se llame con el bloqueo sostenido es el punto de partida de todas las restricciones

Para mantener coherentes las cargas, descargas y notificaciones de DLL, el cargador del sistema operativo las serializa con un solo bloqueo del cargador por proceso. El punto importante es que adquiere este bloqueo antes de llamar a DllMain y sigue sosteniéndolo mientras DllMain se ejecuta.1

Durante ese tiempo, cualquier otro hilo del mismo proceso que intente cargar una DLL o continuar una notificación de arranque o de salida de hilo espera a que se libere el bloqueo. No es un sitio en el que pueda insertar una espera larga solo porque convenga a su propia DLL.

Por qué el trabajo en DllMain afecta las notificaciones de DLL de todo el procesoEl cargador adquiere el bloqueo del cargador a escala de proceso antes de llamar a DllMain, y las cargas de DLL y las notificaciones de hilo en otros hilos esperan a que se libere, así que el trabajo en DllMain también afecta a otras DLL e hilosEl cargador adquiere el bloqueo compartidoDllMain se ejecutaDllMain regresaSe libera el bloqueo del cargadorCarga de DLL o notificación en otro hiloEspera a que se libere el mismo bloqueoEl trabajo en espera puede continuar

Figura 1: El bloqueo compartido se sostiene mientras se ejecuta DllMain, así que una espera ahí detiene el avance de otras DLL e hilos.

Las prohibiciones que siguen se entienden una vez que pregunta: «¿este trabajo necesita, de forma directa o indirecta, el bloqueo del cargador o la inicialización de otra DLL?»

3. Por qué se detiene — entender las prohibiciones a través de cuatro caminos

3.1 Llamar a un tratamiento que carga otra DLL

Evite llamar a LoadLibrary / FreeLibrary desde DllMain. LoadLibrary crea dependencias circulares en el orden de carga y lleva a usar una DLL antes de que se haya ejecutado su código de inicialización. En el lado de la finalización, hay asimismo el riesgo de usar una DLL que ya se ha procesado o liberado.2

No haber escrito LoadLibrary usted mismo no le hace estar a salvo. Algunas funciones de User, Shell y COM cargan internamente otros componentes del sistema, y pueden tocar un componente aún no inicializado o ya liberado y provocar una violación de acceso.2

La base de lo que se puede llamar con seguridad son las funciones de Kernel32.dll que no cargan otras DLL, porque Kernel32.dll está garantizado como cargado para cuando se ejecuta DllMain. Por ejemplo, puede crear objetos de sincronización como secciones críticas y mutex, y puede usar TLS. La documentación oficial afirma, no obstante, de forma explícita que no existe una lista exhaustiva de funciones seguras. No razone «está en Kernel32, así que vale todo» ni «puedo crear un objeto de sincronización, así que puedo esperar a otros hilos».2

3.2 Esperar dentro de DllMain a que otro hilo salga

El interbloqueo clásico ocurre al descargar la DLL: DllMain pide a un hilo trabajador que se detenga y luego espera a que salga.

Incluso después de que el trabajador termina su propio trabajo, tiene que pasar por la notificación DLL_THREAD_DETACH a la salida del hilo. Esa notificación necesita el bloqueo del cargador, que sostiene el DllMain que espera. El resultado es que DllMain espera al trabajador, y el trabajador espera a que DllMain regrese.5

Por qué esperar a un hilo en DllMain se interbloqueaDllMain, sosteniendo el bloqueo del cargador, espera a que un hilo trabajador salga, pero el hilo trabajador que sale espera a que se libere el bloqueo del cargador para su notificación DLL_THREAD_DETACH, así que ambos se esperan y se interbloqueanHilo trabajadorDllMainCargador (sostiene el bloqueo)Hilo trabajadorDllMainCargador (sostiene el bloqueo)La notificación de salida necesita el bloqueo del cargadorDllMain espera sosteniendo el bloqueo, W espera el bloqueoNotificar DLL_PROCESS_DETACHPedir la salida y esperar la finalizaciónTerminar el trabajo y dirigirse a la salida del hilo

Figura 2: Incluso después de que el trabajador termina su trabajo, no puede pasar la notificación de salida de hilo, así que la espera de salida dentro de DllMain no se resuelve.

Esto no es un caso de «se detiene si tiene mala suerte»; la espera mutua se sostiene de forma estructural. La limpieza necesaria al descargar se trata en el capítulo 6, por separado del trabajo a la salida del proceso.

3.3 El orden de adquisición del bloqueo propio y del bloqueo del cargador se invierte

No es solo el arranque y la salida de hilo; API como GetModuleHandle también necesitan internamente el bloqueo del cargador. Cuando se superponen los dos caminos siguientes, el orden de adquisición de bloqueos se invierte.6

  • En el lado de DllMain, código que ya sostiene el bloqueo del cargador intenta tomar el bloqueo privado G.
  • En el lado del trabajador, código que ya sostiene el bloqueo privado G llama a una API e intenta tomar el bloqueo del cargador.
Inversión de orden entre el bloqueo del cargador y un bloqueo privadoDllMain va a por un bloqueo privado sosteniendo el bloqueo del cargador, y un hilo trabajador va a por el bloqueo del cargador, para GetModuleHandle y similares, sosteniendo el bloqueo privado, así que el orden de adquisición se invierte y se interbloqueanDllMain: sostiene el bloqueo del cargadorVa a por el bloqueo privado GTrabajador: sostiene el bloqueo privado GVa a por el bloqueo del cargadorInterbloqueo por orden de adquisición invertidoExigido internamente por GetModuleHandle y otros

Figura 3: Cuando un lado toma el bloqueo del cargador y luego G, y el otro toma G y luego el bloqueo del cargador, cada uno espera a que el otro libere.

La guía oficial pide tratar el bloqueo del cargador como la cima de la jerarquía de bloqueos de la aplicación, es decir, el bloqueo que se adquiere primero. Para cuando está dentro de DllMain, ese bloqueo ya se sostiene. Compruebe no solo el nombre de la función que llama, sino también qué bloqueos sostiene cuando la llama.6

3.4 Incluso solo crear un hilo deja problemas de espera de arranque y de vida útil

CreateThread dentro de DllMain tampoco se recomienda. El hilo nuevo no puede empezar a ejecutar su función de hilo hasta que se haya procesado la notificación DLL_THREAD_ATTACH. Como el DllMain actual sostiene el bloqueo del cargador, esperar dentro de ese DllMain a que el hilo arranque o termine se interbloquea.5

No esperar tampoco lo resuelve todo. Si la DLL se descarga después de que DllMain regresa pero antes de que el hilo que creó haya empezado a ejecutarse, la dirección de arranque del hilo apunta a código ya liberado, y sigue siendo posible un fallo.5

Dos problemas que quedan al crear un hilo en DllMainUn hilo creado en DllMain espera el bloqueo del cargador para su notificación de arranque, así que si DllMain espera a que arranque o termine se interbloquean, y aunque DllMain regrese sin esperar, la vida útil del código termina y falla si se descarga la DLL antes de que el hilo empiece a ejecutarseSíNoHilo creado dentro de DllMainEl hilo nuevo espera la notificación de arranque¿Esperar el arranque o la finalización dentro de DllMain?Espera mutua sosteniendo el bloqueoRegreso desde DllMainDLL descargada antes de que el hilo empiece a ejecutarseEl código de la dirección de arranque ya no está

Figura 4: No esperar dentro de DllMain y proteger la vida útil de la DLL que usa el hilo creado son dos requisitos distintos.

4. Dos lugares peligrosos incluso cuando DllMain está vacío

4.1 Inicialización dinámica de objetos globales y estáticos

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. De hecho forman parte de DllMain y están sujetos a las mismas restricciones.2

Poner la carga de un archivo de configuración, el arranque de un registro, la inicialización COM o el arranque de un hilo en un constructor solo oculta el lugar de la llamada; el momento en que se ejecuta sigue estando dentro del bloqueo del cargador. Si ese trabajo complejo incluye cargar otra DLL o sincronizar hilos, es exactamente tan peligroso como escribirlo en el cuerpo de DllMain.

Cómo la inicialización de objetos globales se convierte en una minaCargar la DLL adquiere el bloqueo del cargador y los constructores de objetos globales se ejecutan a través del CRT, así que un LoadLibrary, una sincronización de hilos o una inicialización COM dentro de ellos es una ejecución de lo que DllMain prohíbeCarga de la DLL (bloqueo del cargador adquirido)Punto de entrada del CRTConstructor del objeto globalTrabajo equivalente a LoadLibraryArrancar un hilo y esperar a que termineUso de COM o User32Todo esto son prohibiciones de DllMain

Figura 5: Revise no solo el cuerpo de DllMain, sino también la inicialización y la terminación de objetos estáticos llamados desde el CRT.

Trate la inicialización constante en tiempo de compilación, por ejemplo todo lo que pueda ser constexpr, por separado de una inicialización compleja en tiempo de ejecución. Difiere la inicialización dinámica que implica llamadas a funciones, y haga que el primer acceso ocurra también fuera de DllMain.

4.2 MSIL ejecutado en un destino de llamada C++/CLI

Envolver una DLL nativa en C++/CLI se trata en Llamar DLL nativas desde C#: envoltorio C++/CLI frente a P/Invoke. En esta configuración, vigile los caminos que ejecutan MSIL (código administrado) bajo el bloqueo del cargador. Si ejecutar el MSIL exige la inicialización del CLR o la carga de otro ensamblado, es posible un interbloqueo.3

El compilador emite el aviso C4747 para código en el que DllMain intenta de forma directa ejecutar MSIL. Pero no puede detectar la ejecución indirecta a través de una función de otro módulo. La ausencia del aviso por sí sola no es base para llamar seguro al código. Los inicializadores dinámicos de objetos estáticos también entran en el ámbito.3

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 prevenirla revisando el árbol de llamadas y compilando nativo de punta a puntaLlamada desde DllMainEjecuta MSIL de forma directaEjecuta a través de otro móduloDetectado por el aviso C4747El compilador no puede detectarloPrevenir con revisión y #pragma unmanaged

Figura 6: Además del camino directo que detecta C4747, revise también las llamadas que pasan por otro módulo.

El remedio es compilar DllMain y toda función alcanzable desde él como nativo con #pragma unmanaged, o no tener DllMain en absoluto. Incluso con lo segundo, no pase por alto caminos indirectos como los inicializadores estáticos.3

5. Diseñar la inicialización — hacerla estática, diferirla, dejar solo el mínimo

5.1 Antes de dejar algo en DllMain, plantee si se puede cambiar el momento de ejecución

La línea de base oficial es completar en tiempo de compilación la inicialización que pueda y diferir el resto tanto como sea posible. Solo el trabajo que debe detectarse pronto como un fallo de carga se queda, como excepción y al mínimo.1

Por ejemplo, puede haber el requisito de hacer fallar la propia carga de la DLL porque un archivo de configuración del que depende está corrupto. Incluso entonces, redúzcalo a «intentar el trabajo requerido y fallar de inmediato» en lugar de ejecutar primero otra inicialización y fallar después. Esto no es una excepción que permita una inicialización compleja en el momento de la carga.1

Directrices de diseño para la inicialización de una DLLPrimero considerar si la inicialización puede ser estática en tiempo de compilación; si no, diferir por defecto al primer uso, y dejar en DllMain solo el mínimo que debe detectarse pronto como un fallo de cargaSíNoNoSí¿Decisible en tiempo de compilación?Hacerla inicialización estática¿Debe detectarse el fallo en la carga?Diferir al primer uso (el valor por defecto)Hacer solo el mínimo en DllMainProteger con INIT_ONCE o un estático local de función

Figura 7: Considere primero la inicialización estática y la diferida, y deje en DllMain solo el mínimo que necesita detección temprana.

5.2 La inicialización diferida hay que diseñarla hasta «quién la llama primero»

Para la exclusión mutua en el primer uso, puede usar una inicialización de una sola vez con INIT_ONCE o un estático local de función de C++ (un static mágico). La idea es mover los objetos globales complejos a un puntero creado en el primer acceso o a un estático local de función.

Diferir solo, no obstante, no le saca del bloqueo del cargador. La restricción se levanta solo cuando el primer acceso viene de «una API ordinaria llamada después de que ha terminado la carga de la DLL». En ese caso, se puede diseñar como una inicialización ordinaria que puede usar casi toda la API de Windows.1

A la inversa, si ese primer acceso ocurre desde DllMain o un inicializador estático, la inicialización acaba ejecutándose de todos modos bajo el bloqueo del cargador. Más allá de extraer una función de inicialización, confirme quién la llama primero, y cuándo.

Cuándo la inicialización diferida sale del bloqueo del cargadorSi el primer acceso de la inicialización diferida viene de una API ordinaria después de que termina la carga, se puede inicializar fuera del bloqueo del cargador, pero si viene de DllMain o de un inicializador estático, se ejecuta bajo las mismas restriccionesAPI ordinaria después de que termina la cargaDllMain o un inicializador estático¿De dónde viene el primer acceso?Inicializar fuera del bloqueo del cargadorInicializar bajo las mismas restriccionesProteger con INIT_ONCE o un estático local de funciónMover también el momento del primer acceso

Figura 8: INIT_ONCE y los estáticos locales de función se ocupan de la exclusión mutua de la inicialización; no garantizan que se llame fuera del bloqueo del cargador.

5.3 Decidir DisableThreadLibraryCalls con tres condiciones

En una DLL que no depende de las notificaciones de hilo, llamar a DisableThreadLibraryCalls en DLL_PROCESS_ATTACH detiene las notificaciones DLL_THREAD_ATTACH / DLL_THREAD_DETACH. En un proceso que crea hilos con frecuencia, esto reduce la sobrecarga de notificaciones.4

Antes de aplicarla, compruebe el CRT estático, el TLS estático y si algo usa las notificaciones. Una DLL enlazada con el CRT estático no debe llamarla, porque el propio CRT necesita las notificaciones de hilo. Cuando hay TLS estático mediante thread_local o __declspec(thread), la propia llamada falla y devuelve FALSE.4

Decidir si llamar a DisableThreadLibraryCallsUna DLL enlazada con el CRT estático no debe llamarla, y con TLS estático en vigor la propia llamada falla así que no se llama, pero una DLL que no es ninguna de las dos y no usa notificaciones de hilo puede llamarla en DLL_PROCESS_ATTACH, comprobando el valor de retorno, para recortar el coste de notificacionesSíNoSíNoNoSí¿Enlazada con el CRT estático?No debe llamarla¿Usa TLS estático?La llamada falla de todos modos (FALSE)¿Necesita notificaciones de hilo?Llamarla en ATTACH (comprobar el valor de retorno)No llamarla; tratar las notificaciones

Figura 9: Distinga el CRT estático, donde no debe usarse, del TLS estático, donde falla, y compruebe el valor de retorno incluso cuando las notificaciones no hacen falta.

Es una optimización a considerar para una DLL típica que usa el CRT enlazado de forma dinámica y cumple estas condiciones. DisableThreadLibraryCalls existe para reducir notificaciones; no es un medio de hacer una inicialización compleja en DllMain.

6. Finalización — separar la descarga de la DLL de la salida del proceso

6.1 El mismo DLL_PROCESS_DETACH, pero lo que queda después es distinto

DLL_PROCESS_DETACH se entrega tanto cuando solo se descarga la DLL como cuando sale el proceso entero. La notificación tiene el mismo nombre, pero las premisas de la limpieza difieren.5

En una descarga mediante FreeLibrary, el proceso sigue ejecutándose después. Así que hay que detener hilos, y los identificadores abiertos, los recursos asignados, el estado que debe persistirse, etc., hay que limpiarlos bien. Como mostró la sección 3.2, esperar dentro de DllMain la salida natural de un hilo con ese fin se interbloquea.5

Mejor aún, evite un diseño en el que una DLL que puede descargarse posea hilos; mover la propiedad de los hilos al lado del EXE es la opción más segura. Para un diseño existente en el que la DLL posee hilos, considere el siguiente protocolo de detención junto con sus restricciones, nunca uno sin el otro.

6.2 El protocolo de detención documentado oficialmente cuando la DLL posee un trabajador

Las mejores prácticas de Microsoft documentan un procedimiento de detención al descargar que espera no «la salida natural del hilo», sino «una señal de que ha alcanzado un estado coherente».5

  1. El lado de DllMain señala al trabajador que se detenga, mediante un evento.
  2. El trabajador recorta su trabajo actual hasta un estado coherente, señala la finalización y entra en una espera infinita.
  3. El lado de DllMain confirma el estado coherente y termina el hilo con TerminateThread.

Parece brusco, pero se documentó bajo la restricción de que esperar una salida natural hace chocar la notificación de salida con el bloqueo del cargador. La premisa es que el trabajo de alcanzar el estado coherente también obedece las mismas restricciones que DllMain. Si ese trabajo entra en la carga de otra DLL o en una espera del bloqueo del cargador, un interbloqueo con el lado que espera la señal es inevitable.5

Al descargar, esperar la finalización de coherencia en lugar de la salida naturalDllMain señala al trabajador que se detenga, el trabajador alcanza un estado coherente obedeciendo las mismas restricciones que DllMain, señala de vuelta y entra en una espera infinita, y DllMain confirma el estado coherente antes de terminar el hiloHilo trabajadorDllMain (al descargar)Hilo trabajadorDllMain (al descargar)Lo que se espera es la señal de coherencia, no la salida naturalSeñalar la detención con un eventoAlcanzar un estado coherente bajo las mismas restriccionesSeñalar que se alcanzó la coherenciaEntrar en una espera infinitaConfirmar la coherencia, luego TerminateThread

Figura 10: Incluso con este protocolo, el trabajo que establece el estado coherente no debe esperar el bloqueo del cargador.

Esto no significa que se pueda cortar de golpe cualquier hilo en ejecución. Primero considere si el hilo puede poseerse fuera de la DLL.

6.3 A la salida del proceso, lo ideal es volver sin hacer nada

Para cuando se entrega DLL_PROCESS_DETACH a la salida del proceso, los demás hilos han salido o han sido terminados de forma forzosa, y no se puede confiar en la coherencia del espacio de direcciones. El estado de las DLL y runtimes dependientes tampoco es de fiar, así que una limpieza como liberar memoria se vuelve peligrosa en lugar de útil. La guía oficial también dice que el manejador ideal en este caso está vacío.5

Escriba los datos que deban persistirse en el propio código de apagado de la aplicación. No use DLL_PROCESS_DETACH como el último sitio donde aún se puede recoger todo.

Las premisas de la limpieza difieren entre descarga de DLL y salida del procesoCuando solo se descarga la DLL, el proceso sigue ejecutándose así que hay que limpiar recursos, pero evite esperar la salida natural dentro de DllMain; a la salida del proceso, no confíe en la coherencia de otros hilos y recursos, esencialmente vuelva sin hacer nada, y escriba de antemano el estado a guardar en el código de apagado de la aplicaciónDescarga de la DLL solamenteSalida del proceso¿Razón de DLL_PROCESS_DETACH?El proceso sigue ejecutándose despuésLimpiar bien los recursos restantesNo esperar la salida natural dentro de DllMainEl estado de otros hilos y recursos es inciertoEsencialmente volver sin hacer nadaGuardar de antemano en el código de apagado de la aplicación

Figura 11: Decida no solo «¿hace falta limpieza?», sino si el proceso sigue ejecutándose y si se puede confiar en los recursos usados para la limpieza.

7. Investigar un cuelgue — encontrar el lado que espera el bloqueo del cargador y el lado que lo sostiene

7.1 Confirmar en un volcado el par de hilos que se esperan mutuamente

Capture un volcado en el momento de la congelación y examine la pila de cada hilo. La huella típica es un par: un hilo que espera un bloqueo dentro de una función del cargador en ntdll.dll cuyo nombre empieza por Ldr, y un hilo que espera otra cosa dentro de DllMain o de un inicializador estático (dynamic initializer).

Un hilo detenido en medio de una llamada a LoadLibrary es otro participante típico. Una vez que se conecta la relación entre el lado que espera el bloqueo y el lado que lo sostiene mientras espera otra cosa, puede concluir casi con certeza que es un interbloqueo del bloqueo del cargador.

Confirmar una espera mutua del bloqueo del cargador a partir de un volcado de cuelgueEn la pila de cada hilo busque una espera de bloqueo en funciones Ldr y una espera dentro de DllMain o de un inicializador estático, confirme que ambos se esperan, y si el par típico no aparece, investigue también otros tipos de cuelgueSíNo visibleCapturar un volcado durante el cuelgueExaminar la pila de cada hiloEspera de bloqueo en funciones LdrEspera dentro de DllMain o de un inicializador estático¿Se conecta la relación de espera mutua?Interbloqueo del bloqueo del cargadorInvestigar también otros tipos de cuelgue

Figura 12: No decida a partir de una sola pila; busque el par del lado que espera el bloqueo del cargador y del lado que lo sostiene mientras espera.

Condiciones como «de vez en cuando al arrancar», «solo en una máquina concreta» o «solo cuando se ejecuta como servicio» también son pistas. La espera mutua se forma cuando una carga de DLL coincide con un arranque o una salida de hilo, así que las diferencias de entorno y el momento de ejecución cambian cómo se manifiesta.

7.2 Prevenirlo con Application Verifier y la revisión de los destinos de llamada

Ejecutar pruebas con Application Verifier habilitado detecta en tiempo de ejecución los errores típicos de DllMain.1 En C++/CLI, no ignore el aviso C4747, y compruebe los caminos indirectos que el aviso no puede detectar desde la perspectiva de la sección 4.2.

En la revisión, busque funciones que llaman a LoadLibrary de forma indirecta entre el trabajo alcanzable desde DllMain. La inicialización COM, algunas funciones del CRT y las importaciones de carga diferida (delay-load) son puntos a comprobar. En particular, es fácil pasar por alto que la primera llamada a una función de importación de carga diferida se convierte internamente en LoadLibrary. Aunque el nombre de la función viva fuera de DllMain, mientras el llamador sea DllMain, la restricción permanece.

8. Resumen — no mirar el nombre de la función, sino «cuándo y bajo qué bloqueo se ejecuta»

Las restricciones de DllMain se pueden entender todas a partir de un solo punto: se llama mientras se sostiene el bloqueo del cargador a escala de proceso. No solo su propio DllMain, sino también la inicialización y la terminación estáticas del CRT y las llamadas indirectas de C++/CLI caen en el mismo ámbito.

Empiece una revisión preguntando si la inicialización se puede hacer estática y si el resto se puede diferir hasta después de que termine la carga. Compruebe hasta el primer acceso del trabajo diferido y, si las notificaciones no hacen falta, considere DisableThreadLibraryCalls después de comprobar las condiciones de CRT estático y TLS estático.

En el lado de la finalización, separe una descarga solo de la DLL de una salida de todo el proceso. Si la DLL posee un trabajador, siga el protocolo de señal, comprobación de coherencia y terminación, y mueva si es posible la propiedad de los hilos al lado del EXE. Acerque DllMain a la salida del proceso al vacío, y ponga el guardado de datos en el propio código de apagado de la aplicación.

El orden para revisar DllMain y lo que llamaExaminar el ámbito incluyendo no solo DllMain sino la inicialización estática y las llamadas indirectas, mover la inicialización a estática o después de la carga, diseñar la detención y la limpieza según el motivo de finalización, luego confirmar con Application Verifier y volcadosDllMain, inicialización estática y destinos de llamadaMover la inicialización a estática o después de la cargaComprobar también el primer acceso del trabajo diferidoDiseñar la limpieza según el motivo de finalizaciónConfirmar con Verifier, avisos y volcados

Figura 13: Seguir no solo lo que hace la inicialización, sino cuándo se ejecuta y su vida útil al finalizar, permite tratar las prohibiciones como una sola política de diseño.

La pregunta que hay que hacerse en un caso límite es: «¿es este un trabajo aceptable de ejecutar mientras se sostiene el bloqueo del cargador?» No meter en DllMain una inicialización dudosa, y cambiar en su lugar el momento en que se ejecuta, es el punto de partida de un diseño de DLL seguro.

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC se encarga 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 ↩7

  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 se puede llamar 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, 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, mientras que la ejecución indirecta a través de otro módulo no se puede detectar; y que los inicializadores dinámicos de objetos estáticos pueden provocar el mismo problema. ↩ ↩2 ↩3 ↩4

  4. 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 ↩3

  5. Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. Sobre la estructura que se interbloquea cuando DllMain espera a que un hilo salga (la notificación DLL_THREAD_DETACH de la salida del hilo necesita el bloqueo del cargador); el protocolo para detener hilos al descargar (señalar con un evento, confirmar un estado coherente y luego terminar); DLL_PROCESS_DETACH a la salida del proceso, donde los demás hilos ya han sido terminados de forma forzosa, no hay garantía de coherencia del espacio de direcciones y 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 ↩5 ↩6 ↩7 ↩8

  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 una hipérbole; es la política oficial de diseño, y el propio Microsoft dice que el DllMain ideal es un stub casi vacío. Lo que es seguro se limita a las funciones de Kernel32.dll, que está garantizado como cargado para cuando se ejecuta DllMain, que no cargan otras DLL. Por ejemplo, puede crear secciones críticas y mutex y usar TLS. A la inversa, LoadLibrary/FreeLibrary, la sincronización con otros hilos y las llamadas a User32, Shell, COM y similares están prohibidos porque provocan interbloqueos y violaciones de acceso. Para la inicialización de la que no esté seguro, la respuesta correcta no es hacerla en DllMain, sino diferirla 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, de hecho como parte de DllMain. Eso significa que llamar a LoadLibrary en un constructor, arrancar otro hilo y esperar a que termine, inicializar COM, etc., conllevan el mismo peligro que hacerlos en DllMain. Para objetos globales con inicialización compleja, desplace el momento de ejecución fuera de DllMain: conserve un puntero y cree el objeto en el primer acceso, o use un estático local de función.
¿Debo llamar a DisableThreadLibraryCalls?
Es útil bajo condiciones. Si la DLL no necesita las notificaciones DLL_THREAD_ATTACH/DETACH, llamar a DisableThreadLibraryCalls en DLL_PROCESS_ATTACH detiene la notificación en cada creación y destrucción de hilo y reduce la sobrecarga en un proceso que crea hilos con frecuencia. Hay, no obstante, dos excepciones. No la llame en una DLL enlazada con el CRT estático (el CRT estático necesita las notificaciones de hilo). Y cuando 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 con 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 que llama o los inicializadores dinámicos de globales se compilan a MSIL, la inicialización del CLR o la carga de otro ensamblado se vuelve necesaria bajo el bloqueo del cargador, y eso puede interbloquearse. El compilador emite el aviso C4747 cuando DllMain intenta ejecutar MSIL de forma directa, pero no puede detectar la ejecución indirecta a través de otro módulo. El remedio 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 difiere 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 de forma forzosa y no hay garantía de que el espacio de direcciones sea coherente, así que una limpieza como liberar memoria es de hecho peligrosa, y la guía oficial es que «el manejador ideal está vacío». Escriba los datos que deban persistirse en el propio código de apagado de la aplicación, y en el manejador esencialmente no haga nada y regrese. En una descarga mediante FreeLibrary, por el contrario, el proceso sigue ejecutándose, así que se requiere una limpieza completa, como detener hilos y cerrar identificadores. Esperar a que un hilo salga dentro de DllMain se interbloquea, no obstante, así que debe seguir el protocolo que describe la documentación oficial: señalar, esperar hasta un estado coherente y hacer el trabajo final 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