DllMain e il loader lock — Il vero motivo per cui vi dicono di «non fare niente nell'inizializzazione della DLL»
· Go Komura · Windows, DLL, Sviluppo Windows, C++, Risoluzione dei problemi, Multithreading, Win32 API
«L’app si blocca all’avvio, ma solo in un ambiente particolare.» «Quando carichiamo la nostra DLL, LoadLibrary a volte non ritorna mai.» «Va in deadlock solo al momento dell’avvio di un servizio.» — Seguite indagini come queste abbastanza lontano e, più spesso che no, arrivate allo stesso posto. Il codice di inizializzazione della DLL — cioè, DllMain.
La documentazione di Microsoft avverte su DllMain con un tono insolitamente forte. Non chiamate LoadLibrary. Non sincronizzatevi con altri thread. Non chiamate funzioni User, Shell o COM. La DllMain ideale è uno stub vuoto — perché il linguaggio è così forte? La ragione si concentra in un singolo meccanismo interno, il loader lock. Rivolto agli sviluppatori che scrivono DLL, plug-in e wrapper C++/CLI su Windows, questo articolo spiega, a partire dalle fonti primarie, come funziona il loader lock, la struttura che fa tenere un deadlock, e il progetto sicuro e la procedura di indagine.
1. Prima di tutto, la conclusione
DllMainviene chiamata tenendo il loader lock, un lock condiviso di cui ce n’è esattamente uno per processo. Quindi chiamare, daDllMain, un lavoro che tenta di prendere il loader lock (direttamente o indirettamente) crea la possibilità di un deadlock, o di un crash per aver toccato una DLL che non è ancora stata inizializzata.1- Chiamare
LoadLibrary/FreeLibraryè vietato. Crea una dipendenza circolare sull’ordine di caricamento e può far girare codice di inizializzazione contro una DLL la cui propria inizializzazione non è ancora girata.2 - Sincronizzarsi con altri thread è altrettanto vietato. Le notifiche delle DLL sono serializzate, quindi attendere dentro
DllMainche un thread parta o esca lascia quel thread stesso fermo in attesa del loader lock, e andate in deadlock.23 - Ciò che potete chiamare in sicurezza è, in pratica, solo un sottoinsieme di Kernel32.dll. E la documentazione ufficiale afferma chiaramente che «un elenco completo di funzioni sicure non esiste». Le funzioni User, Shell e COM caricano altri componenti e provocano access violation.2
- In una DLL collegata con la CRT, le stesse restrizioni si applicano a costruttori e distruttori dei globali. Girano come parte di fatto di
DllMain.2 - Il progetto corretto è «rinviare». Fate l’inizializzazione che potete a compile time (in modo statico); rinviate ciò che non potete fino al primo uso. È la best practice ufficiale.1
- Le DLL miste C++/CLI sono particolarmente pericolose. Per evitare di eseguire MSIL sotto il loader lock,
DllMaine il suo albero di chiamate devono essere compilati nativi.4
2. Quando e come viene chiamata DllMain
DllMain è il punto di ingresso che il loader del sistema operativo chiama quando una DLL entra o esce da un processo o da un thread. Ci sono quattro notifiche.
| Notifica | Momento |
|---|---|
| DLL_PROCESS_ATTACH | Quando la DLL viene caricata nel processo |
| DLL_THREAD_ATTACH | Quando un nuovo thread viene avviato nel processo |
| DLL_THREAD_DETACH | Quando un thread esce normalmente |
| DLL_PROCESS_DETACH | Quando la DLL viene scaricata, o quando il processo esce |
Due fatti sono facili da perdere. Primo, ogni volta che viene creato un singolo thread, la DllMain di ogni DLL già caricata viene chiamata con DLL_THREAD_ATTACH. In altre parole DllMain non è «qualcosa che gira una volta quando la mia DLL viene caricata»; è codice che continua a essere chiamato per l’attività di thread del processo. Se non vi serve, potete fermarlo chiamando DisableThreadLibraryCalls dentro DLL_PROCESS_ATTACH (non chiamatela da una DLL collegata con la CRT statica).5
Secondo, in una DLL collegata con la CRT (il runtime C/C++), i costruttori e i distruttori di oggetti C++ globali e statici girano, tramite il punto di ingresso della CRT, come parte di DllMain.2 Anche se pensate «la nostra DllMain è vuota, quindi siamo al sicuro», un oggetto globale con un’inizializzazione elaborata è lo stesso che far girare quel lavoro in DllMain.
flowchart TB
accTitle: I quattro momenti in cui DllMain viene chiamata
accDescr: DLL_PROCESS_ATTACH gira al caricamento della DLL; DLL_THREAD_ATTACH e DETACH girano su ogni DLL già caricata a ogni avvio e uscita di thread nel processo; DLL_PROCESS_DETACH gira allo scaricamento o all'uscita del processo; e i costruttori degli oggetti statici girano anche qui tramite la CRT
load["Caricamento DLL"] --> pa["DLL_PROCESS_ATTACH"]
pa --> ta["DLL_THREAD_ATTACH (a ogni avvio di thread)"]
ta --> td["DLL_THREAD_DETACH (a ogni uscita di thread)"]
td --> pd["DLL_PROCESS_DETACH (allo scaricamento o all'uscita)"]
pa -.-> crt["Anche la costruzione degli oggetti statici gira qui"]
Figura 1: DllMain viene chiamata non solo al caricamento ma a ogni avvio e uscita di thread, e anche l’inizializzazione degli oggetti statici gira come parte di quello.
3. Il loader lock — Un lock che serializza ogni notifica
Perché le restrizioni su DllMain da sola sono così severe? La risposta è nella struttura del loader.
Per tenere coerente una serie di operazioni — caricamento, scaricamento e le varie notifiche delle DLL — il loader del sistema operativo serializza il lavoro con un singolo loader lock per processo. E il punto importante è che DllMain viene chiamata mentre questo loader lock è tenuto.1 Per tutto il tempo in cui siete dentro DllMain, ogni altro caricamento di DLL in quel processo, e ogni notifica di avvio di thread, aspetta che questo lock venga rilasciato.
Da quella struttura, le ragioni dei divieti seguono una dopo l’altra.
- Non dovete chiamare
LoadLibraryperché crea reentrancy del loader lock, o una dipendenza circolare sull’ordine di caricamento. Può anche risultare nella chiamata di una funzione su una DLL la cui inizializzazione non è ancora finita.2 - Sincronizzarsi con altri thread è pericoloso perché il thread che state aspettando ha momenti in cui ha bisogno del loader lock (notifiche all’avvio e all’uscita, chiamate alle API della famiglia
GetModuleHandle, e così via). Voi tenete il loader lock e aspettate l’altra parte; l’altra parte aspetta il loader lock — la classica inversione dell’ordine dei lock.6 - Le funzioni User, Shell e COM sono pericolose perché caricano internamente altri componenti di sistema. Toccate un componente prima che sia inizializzato, o dopo che è stato smontato, e ottenete un access violation.2
sequenceDiagram
accTitle: Perché attendere un thread dentro DllMain va in deadlock
accDescr: DllMain, tenendo il loader lock, aspetta che un thread worker esca, ma il worker che sta tentando di uscire aspetta che il loader lock venga rilasciato per poter ricevere DLL_THREAD_DETACH, quindi si aspettano l'un l'altro e vanno in deadlock
participant L as Loader(tiene il lock)
participant D as DllMain
participant W as Worker
L->>D: DLL_PROCESS_DETACH
D->>W: Richiedi l'uscita e attendi
W->>W: Finisci il lavoro, poi esci
Note over W: La notifica di uscita ha bisogno del lock
Note over D,W: DllMain tiene il lock, W aspetta
Figura 2: «DllMain aspetta che un thread esca» è un deadlock strutturale, perché l’uscita del thread stessa ha bisogno del loader lock.
Il punto è che questa non è il tipo di cosa che «succede se siete sfortunati»; è strutturalmente garantito che tenga. La documentazione vi dice di trattare il loader lock come il vertice della gerarchia di lock che l’app definisce (quello preso per primo). Dentro DllMain tenete già quel lock di vertice, quindi qualsiasi atto di andare avanti da lì ad aspettare qualcos’altro è pericoloso — è un modo utile di ricordarlo.6
flowchart TB
accTitle: Inversione dell'ordine dei lock tra il loader lock e un lock privato
accDescr: DllMain, tenendo il loader lock, va a prendere un lock privato, mentre un worker, tenendo quel lock privato, va a prendere il loader lock per GetModuleHandle o simile, quindi l'ordine di acquisizione si inverte e vanno in deadlock
d["DllMain: tiene il loader lock"] --> dg["Va a prendere il lock privato G"]
w["Worker: tiene il lock privato G"] --> wl["Va a prendere il loader lock"]
dg -.-> dead["Deadlock da ordine di acquisizione invertito"]
wl -.-> dead
wl -.-> api["GetModuleHandle e simili lo richiedono internamente"]
Figura 3: Anche un’API innocua come GetModuleHandle richiede internamente il loader lock, quindi un’inversione di ordine con un lock privato può tenere.
Inoltre, chiamare CreateThread da dentro DllMain stessa non è raccomandato. Il thread creato ha bisogno del loader lock per elaborare la notifica DLL_THREAD_ATTACH, quindi non può iniziare a girare finché la DllMain che sta eseguendo attualmente non ritorna e rilascia il lock. Quindi attendere dentro DllMain che quel thread parta o finisca è un deadlock immediato. C’è anche un problema di lifetime — se, dopo che DllMain ritorna, la DLL viene scaricata mentre è ancora rimasto un thread che non ha ancora iniziato a girare, l’indirizzo di avvio del thread punta ancora a codice già liberato e andate in crash.3
4. Due mine che gli sviluppatori C++ calpestano facilmente
Mina 1: Inizializzazione dinamica degli oggetti globali. Come ha detto il capitolo 2, i costruttori degli oggetti statici girano sotto le restrizioni di DllMain. Leggere un file di configurazione, alzare un impianto di logging, inizializzare COM, avviare un thread — nel momento in cui mettete in una DLL un globale il cui costruttore fa quel tipo di lavoro, state eseguendo «cose che non dovete fare in DllMain». L’inizializzazione costante fissata a compile time (qualsiasi cosa possiate rendere constexpr) è sicura; l’inizializzazione che coinvolge una chiamata di funzione va rinviata.
flowchart TB
accTitle: Il percorso per cui inizializzare un oggetto globale diventa una mina
accDescr: Il loader lock viene preso al caricamento della DLL, e i costruttori degli oggetti globali girano tramite il punto di ingresso della CRT, quindi LoadLibrary, sincronizzazione di thread e inizializzazione COM dentro quei costruttori sono esecuzioni dei divieti di DllMain
load["Caricamento DLL (loader lock acquisito)"] --> crt["Punto di ingresso della CRT"]
crt --> ctor["Costruttore di un oggetto globale"]
ctor --> ng1["Lavoro equivalente a LoadLibrary"]
ctor --> ng2["Avviare un thread e aspettarne la fine"]
ctor --> ng3["Usare COM o User32"]
ng1 -.-> risk["Tutte queste cadono sotto i divieti di DllMain"]
ng2 -.-> risk
ng3 -.-> risk
Figura 4: Anche «DllMain è vuota, quindi siamo al sicuro» rianima lo stesso pericolo nel momento in cui avete un globale con un’inizializzazione elaborata.
Mina 2: C++/CLI (assembly misti). In una configurazione che wrappa una DLL nativa con C++/CLI (la forma trattata nell’articolo sul wrapper), c’è il pericolo di eseguire MSIL (codice managed) sotto il loader lock. Eseguire MSIL può innescare l’inizializzazione del CLR o il caricamento di un altro assembly. Il compilatore emette l’avviso C4747 sul codice in cui DllMain esegue MSIL direttamente, ma non può rilevare l’esecuzione indiretta attraverso una funzione in un altro modulo. Compilate DllMain e le funzioni chiamate da essa come native con #pragma unmanaged, o usate una configurazione che non ha affatto una DllMain.4
flowchart TB
accTitle: Se l'esecuzione di MSIL sotto il loader lock può essere rilevata
accDescr: Il codice in cui DllMain esegue MSIL direttamente può essere rilevato dal compilatore con l'avviso C4747, ma l'esecuzione indiretta attraverso una funzione in un altro modulo no, quindi dovete prevenirla rivedendo l'albero di chiamate e insistendo su una compilazione nativa
d2["Chiamate da DllMain"] --> dir["Esegui MSIL direttamente"]
d2 --> ind["Esegui tramite un altro modulo"]
dir --> c47["Rilevabile con l'avviso C4747"]
ind --> nc["Il compilatore non può rilevarlo"]
nc -.-> rv["Previeni con review e #pragma unmanaged"]
Figura 5: C4747 vi protegge solo contro l’esecuzione diretta. I percorsi indiretti si possono catturare solo con la review.
5. Il progetto corretto — Fate di «rinviare» la politica predefinita
La raccomandazione ufficiale di best practice è chiara.1
- Finite l’inizializzazione che potete a compile time (in modo statico). Prima chiedetevi se un’inizializzazione dinamica può essere sostituita con una statica.
- Rinviate il resto fino al primo uso. Finché il primo uso avviene da un’API ordinaria che viene chiamata dopo che la DLL ha finito di caricarsi, l’inizializzazione gira fuori dal loader lock e potete usare in sicurezza quasi tutta l’API Windows. Per l’esclusione al primo accesso potete usare
INIT_ONCE(inizializzazione una tantum) o i magic statics C++ (statici locali di funzione). Il rinvio non è una panacea, però — se quel primo accesso stesso è fatto daDllMaino da un inizializzatore statico, l’inizializzatore gira ancora sotto il loader lock e siete di nuovo sotto le stesse restrizioni. - Fate un’eccezione solo per i fallimenti che dovete rilevare presto. Potete avere un requisito per cui un file di configurazione rotto deve far fallire il caricamento stesso. Anche allora, tenetelo al minimo di «prova e fallisci immediatamente».
- Considerate
DisableThreadLibraryCallsin DLL_PROCESS_ATTACH. Se la DLL non usa le notifiche di thread, potete togliere il costo della notifica stesso (tranne quando usate la CRT statica o TLS statico).5 - Ispezionate con Application Verifier. Molte delle chiamate pericolose dentro
DllMainsono quelle che Application Verifier rileverà a runtime.1
flowchart TB
accTitle: Guida di progetto per l'inizializzazione di una DLL
accDescr: Prima considerate se l'inizializzazione può essere un'inizializzazione statica a compile time; se no, il default è rinviare al primo uso, e lasciare in DllMain solo il minimo che deve essere rilevato presto come fallimento di caricamento
q1{"Si può decidere a compile time?"} -->|"sì"| s["Falla diventare inizializzazione statica"]
q1 -->|"no"| q2{"Il fallimento deve essere rilevato al caricamento?"}
q2 -->|"no"| lazy["Rinvia al primo uso (il default)"]
q2 -->|"sì"| min["Fai solo il minimo in DllMain"]
lazy -.-> once["Escludi con INIT_ONCE o uno statico locale di funzione"]
Figura 6: L’ordine di decisione è «può essere statica → può essere rinviata», e ciò che lasciate in DllMain è solo il minimo che deve essere rilevato presto.
Se applicare DisableThreadLibraryCalls si può decidere in modo meccanico con il ramo seguente.
flowchart TB
accTitle: Se chiamare DisableThreadLibraryCalls
accDescr: Non chiamatela da una DLL collegata con la CRT statica; se è in effetto TLS statico la chiamata stessa fallisce quindi non la chiamate; se nessuno dei due si applica e la DLL non usa le notifiche di thread, chiamatela in DLL_PROCESS_ATTACH, controllando il valore di ritorno, per tagliare il costo della notifica
q1{"Collegata con la CRT statica?"} -->|"sì"| no2["Non va chiamata"]
q1 -->|"no"| q2{"Usate TLS statico?"}
q2 -->|"sì"| eff["La chiamata fallisce comunque (FALSE)"]
q2 -->|"no"| q3{"Servono le notifiche di thread?"}
q3 -->|"no"| yes["Chiamala in ATTACH (controlla il valore di ritorno)"]
q3 -->|"sì"| keep["Non chiamarla; gestisci le notifiche"]
Figura 7: Le tre condizioni di CRT statica, TLS statico, e se servono le notifiche decidono in modo univoco se dovete chiamarla.
Per fermare i thread allo scaricamento, la documentazione ufficiale dà un protocollo concreto. Piuttosto che «aspettare» che i thread worker escano in DLL_PROCESS_DETACH (su uno scaricamento via FreeLibrary), la forma è (1) segnalare l’uscita con un evento, (2) il lato thread piega il suo lavoro fino a uno stato coerente, segnala indietro, ed entra in un’attesa infinita, (3) il lato DllMain conferma lo stato coerente e poi piega il thread con TerminateThread.3 Sembra rude, ma è documentato come la risposta realistica dentro il vincolo «non dovete aspettare l’uscita naturale di un thread dentro DllMain».
sequenceDiagram
accTitle: Protocollo per fermare un thread allo scaricamento
accDescr: DllMain segnala al thread worker di uscire con un evento; il worker piega il suo lavoro fino a uno stato coerente, segnala indietro, ed entra in un'attesa infinita; DllMain conferma lo stato coerente e poi termina il thread
participant D as DllMain (gestione DETACH)
participant W as Thread worker
D->>W: Segnala l'uscita con un evento
W->>W: Piega il lavoro fino a uno stato coerente
W->>D: Segnala coerenza completa e aspetta per sempre
D->>W: Termina con TerminateThread
Note over D,W: Nessuna attesa dell'uscita naturale, quindi nessun deadlock
Figura 8: Invece di «aspettare un’uscita naturale», «aspettare un segnale di coerenza e poi tagliarlo» evita una collisione con il loader lock.
Come questione di primi principi, il progetto più sicuro è evitare di possedere thread in una DLL che può essere scaricata, e tenere la proprietà dei thread sul lato EXE.
DLL_PROCESS_DETACH all’uscita del processo è l’opposto: non fare niente e ritornare è l’ideale. A questo punto ogni altro thread è già stato terminato forzatamente, e non potete fare affidamento nemmeno sullo stato delle DLL dipendenti o del runtime. Un lavoro elaborato qui provoca solo deadlock e crash. I dati che devono essere persistiti vanno scritti nel percorso di spegnimento proprio dell’app; non dipendete da questa notifica.3
6. Come indagare quando ci finite dentro
Gli hang da loader lock hanno un’impronta riconoscibile.
Guardate gli stack in un dump di hang. Prendete un dump del momento congelato e ispezionate lo stack di ogni thread. Se trovate una coppia di un thread in attesa su un lock dentro le funzioni del loader di ntdll.dll (la famiglia i cui nomi iniziano con Ldr) e un thread in attesa di qualcos’altro dentro DllMain o un inizializzatore statico (dynamic initializer), siete quasi certi. Un thread fermo in mezzo a una chiamata LoadLibrary è un altro personaggio tipico.
flowchart TB
accTitle: L'impronta di un hang da loader lock
accDescr: In un dump di hang, se trovate sia un thread in attesa su un lock dentro le funzioni del loader di ntdll sia un thread in attesa di qualcos'altro dentro DllMain o un inizializzatore statico, potete trattarlo come un deadlock da loader lock con quasi certezza
dump["Dump di hang"] --> t1["Thread in attesa su un lock in funzioni della famiglia Ldr"]
dump --> t2["Thread in attesa dentro DllMain o un inizializzatore statico"]
t1 --> pair{"Entrambi presenti?"}
t2 --> pair
pair -->|"sì"| conf["Quasi certamente un deadlock da loader lock"]
pair -->|"no"| other["Indaga come un hang di un altro tipo"]
Figura 9: Gli hang da loader lock hanno l’impronta riconoscibile «in attesa in Ldr + in attesa dentro DllMain».
Sospettate il carattere «dipendente dal timing». Un deadlock da loader lock tiene solo nel momento in cui un caricamento di DLL coincide con un avvio o un’uscita di thread. Condizioni di riproduzione come «occasionalmente all’avvio», «solo su una macchina particolare» e «solo quando gira come servizio» sono segni di questo tipo di problema.
Eseguite un’ispezione preventiva. Abilitate Application Verifier ed eseguite i vostri test, e potete rilevare le chiamate pericolose dentro DllMain a runtime.1 Per C++/CLI, non ignorate l’avviso C4747; nelle review delle funzioni raggiungibili da DllMain, aggiungete l’angolo delle «funzioni che chiamano LoadLibrary indirettamente» (inizializzazione COM, alcune funzionalità della CRT, import delay-loaded, e così via) alla checklist di review, e catturerete gli incidenti prima di spedirli. La prima chiamata di un import delay-loaded che diventa internamente LoadLibrary è un punto facile da perdere.
7. Riepilogo
DllMainviene chiamata tenendo il loader lock (uno per processo, il lock che serializza ogni notifica delle DLL). Ogni restrizione segue da quello.- Il nucleo dei divieti è «non chiamate
LoadLibrary/FreeLibrary», «non sincronizzatevi con altri thread», e «non chiamate funzioni che dipendono da una DLL diversa da Kernel32». I costruttori e i distruttori di oggetti statici che girano tramite la CRT cadono sotto le stesse restrizioni. - La politica di progetto di base è il rinvio. Rendete statica l’inizializzazione che potete rendere statica; rinviate il resto al primo uso. Usate
DisableThreadLibraryCallse Application Verifier. - Fermare i thread allo scaricamento segue il protocollo ufficiale (segnala → conferma la coerenza → termina). DLL_PROCESS_DETACH all’uscita del processo è idealmente vuoto.
- In C++/CLI, eseguire MSIL sotto il loader lock è una mina a sé. Insistete su una compilazione nativa dell’albero di chiamate di
DllMain.
Le restrizioni di DllMain sembrano, all’inizio, un elenco irragionevole di divieti. Ma una volta che tenete il singolo punto che «viene chiamata tenendo il loader lock, il lock di vertice», ogni divieto è una riformulazione dello stesso principio. Ricordatelo come un principio e, quando incontrate un caso limite che non è nella documentazione, dovreste comunque essere in grado di fare la domanda giusta: «È un lavoro che mi è consentito fare tenendo il lock?»
Articoli correlati
- Come funziona la risoluzione dei nomi delle DLL Windows - Ordine di ricerca e SxS
- Chiamare DLL native da C#: wrapper C++/CLI vs P/Invoke
- Best practice di multithreading in pratica: edizione C++ — Eliminare gli incidenti per struttura con RAII e jthread
- Sveglie spurie — Perché le condition variable si svegliano «senza essere notificate» e come attendere correttamente su Windows
- Leggere un crash dump con WinDbg + SOS — guida pratica all’analisi dopo la raccolta
- Fondamenti STA/MTA di COM - Modelli di threading e come evitare blocchi
Aree di consulenza correlate
KomuraSoft LLC si occupa di indagine sulle cause di hang e deadlock all’avvio o al caricamento di una DLL (analisi di dump), di revisioni di progetto intorno a DllMain e all’inizializzazione statica, e di rimedio di wrapper C++/CLI e DLL plug-in verso un progetto di inizializzazione sicuro. Potete consultarci anche allo stadio difficile da riprodurre di «si blocca all’avvio solo in un ambiente particolare».
- Indagine sui difetti e analisi delle cause
- Consulenza tecnica e revisione della progettazione
- Sviluppo di app Windows
- Contattaci
Riferimenti
-
Microsoft Learn, Dynamic-Link Library Best Practices. Sul fatto che DllMain viene chiamata mentre il loader lock è tenuto, così che le funzioni che potete chiamare sono severamente ristrette; sul fatto che la DllMain ideale è uno stub vuoto e l’inizializzazione va rinviata il più possibile; sulla raccomandazione dell’inizializzazione statica a compile time; sul fatto di fare solo il minimo per i fallimenti che devono essere rilevati presto; e sul fatto di rilevare gli errori tipici di DllMain con Application Verifier. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, DllMain entry point. Sul fatto di eseguire solo inizializzazione e terminazione semplici nel punto di ingresso; sul perché non dovete chiamare LoadLibrary / FreeLibrary (ordine di caricamento circolare e uso di una DLL prima dell’inizializzazione o dopo la terminazione); sul fatto che Kernel32.dll è garantita già caricata, così da poterla chiamare nel raggio che non carica altre DLL; sul fatto che non esiste un elenco esaustivo di funzioni sicure; sul fatto che le funzioni User, Shell e COM provocano access violation; sul fatto che le notifiche delle DLL sono serializzate, così che la comunicazione con altri thread o processi provoca deadlock; e sul fatto che le stesse restrizioni si applicano a costruttori e distruttori di oggetti statici quando è collegata la CRT. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. Sulla struttura che va in deadlock se aspettate che un thread esca dentro DllMain (la notifica DLL_THREAD_DETACH dell’uscita del thread ha bisogno del loader lock); sul protocollo per fermare un thread allo scaricamento (segnala con un evento, conferma uno stato coerente, poi termina); sul fatto che DLL_PROCESS_DETACH all’uscita del processo ha gli altri thread già terminati forzatamente e nessuna garanzia di coerenza dello spazio degli indirizzi, così che il gestore ideale è vuoto; e sul fatto che creare un thread in DllMain lascia le notifiche in coda con l’inizializzazione incompleta e provoca problemi. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Initialization of Mixed Assemblies. Sul fatto di non eseguire MSIL sotto il loader lock; sul fatto di non compilare DllMain e il suo albero di chiamate in MSIL e di affrontarlo via #pragma unmanaged; sul fatto che l’avviso C4747 viene emesso quando DllMain tenta di eseguire MSIL direttamente, ma l’esecuzione indiretta attraverso un altro modulo non è rilevabile; e sul fatto che gli inizializzatori dinamici di oggetti statici possono provocare lo stesso problema. ↩ ↩2
-
Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h). Sul fatto di disabilitare le notifiche DLL_THREAD_ATTACH / DLL_THREAD_DETACH per ridurre l’overhead alla creazione e distruzione di thread; sul fatto di non chiamarla da una DLL collegata con la CRT statica; e sul fatto che l’ottimizzazione non viene eseguita quando è in effetto TLS statico (thread_local o __declspec(thread)). ↩ ↩2
-
Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. Sul fatto di definire una gerarchia di lock e acquisire sempre nello stesso ordine; sul fatto che il loader acquisisce il loader lock prima di chiamare DllMain, così che il loader lock dovrebbe stare al vertice della gerarchia di lock; sull’osservare l’ordine di acquisizione tra API che prendono il loader lock indirettamente, come GetModuleFileName, e lock privati; e su un esempio concreto di deadlock da inversione dell’ordine dei lock. ↩ ↩2
Articoli correlati
Articoli recenti con gli stessi tag per approfondire argomenti vicini.
L'API thread pool Win32 — Concorrenza senza creare thread, tramite CreateThreadpoolWork
State spargendo chiamate CreateThread per tutto il codice nativo? Questo articolo spiega l'API thread pool Win32 ridisegnata in Vista — i...
Che cos'è davvero «Non risponde» — Come Windows decide che un'app è bloccata, e come progettare app che non lo sono
Il «Non risponde» di Windows è un meccanismo in cui il sistema operativo giudica che una finestra non ha prelevato un messaggio per 5 sec...
Sveglie spurie — Perché le condition variable si svegliano «senza essere notificate» e come attendere correttamente su Windows
L'attesa di una condition variable può ritornare anche quando non è arrivata nessuna notifica (una sveglia spuria). L'articolo spiega, a ...
Named pipe in pratica — L'IPC standard di Windows, dalla progettazione alla sicurezza
Guida pratica alle named pipe, il meccanismo standard di comunicazione tra processi su Windows. L'articolo organizza, a partire dalle fon...
App che si rompono alla ripresa dalla sospensione — Come funzionano gli eventi di alimentazione di Windows e come costruire app aziendali che li sopravvivono
Aprite il portatile e le connessioni dell'app aziendale sono morte — la causa è un progetto che non ha mai tenuto conto della sospensione...
Argomenti correlati
Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.
Argomenti tecnici Windows
Portale su sviluppo Windows, analisi dei problemi e valorizzazione delle risorse esistenti.
Servizi collegati all’argomento
L’articolo è direttamente collegato ai servizi seguenti.
Sviluppo di applicazioni Windows
Applicazioni aziendali, integrazione di dispositivi e strumenti di comunicazione, dai requisiti allo sviluppo.
Domande frequenti
Domande che ricorrono nelle consulenze sull’argomento dell’articolo.
- Si può davvero non fare proprio niente in DllMain?
- «Non fare niente» non è un'iperbole; è la posizione ufficiale di progetto, e Microsoft stessa dice che la DllMain ideale è uno stub quasi vuoto. Ciò che è sicuro è un sottoinsieme delle funzioni di Kernel32.dll — Kernel32 è garantito già caricato al momento in cui DllMain gira — nel raggio che non carica altre DLL. Creare una critical section o un mutex, e usare TLS, sono esempi di ciò che potete fare. Viceversa, LoadLibrary/FreeLibrary, sincronizzarsi con altri thread, e chiamare funzioni di User32, Shell, COM e simili sono vietati perché provocano deadlock e access violation. L'inizializzazione di cui non siete sicuri non va fatta in DllMain; rinviatele fino alla prima volta in cui viene usata.
- Anche i costruttori dei globali C++ (oggetti statici) cadono sotto le restrizioni di DllMain?
- Sì. Quando la DLL è collegata con la CRT (il runtime C++), i costruttori e i distruttori di oggetti globali e statici girano, tramite il punto di ingresso che la CRT fornisce, come parte di fatto di DllMain. Questo significa che chiamare LoadLibrary da un costruttore, avviare un altro thread e aspettare che finisca, inizializzare COM, e così via, portano tutti lo stesso pericolo di farli in DllMain. Per un oggetto globale con inizializzazione non banale, tenete un puntatore e costruitelo al primo accesso, o usate uno statico locale di funzione, così che il lavoro giri fuori da DllMain.
- Devo chiamare DisableThreadLibraryCalls?
- Condizionalmente, sì. Se la DLL non ha bisogno delle notifiche DLL_THREAD_ATTACH/DETACH, chiamare DisableThreadLibraryCalls in DLL_PROCESS_ATTACH ferma le notifiche per ogni creazione e uscita di thread e riduce l'overhead in un processo che crea thread di frequente. Ci sono due eccezioni. Non chiamatela da una DLL collegata con la CRT statica (la CRT statica ha bisogno delle notifiche di thread). E se è in effetto TLS statico via thread_local o __declspec(thread), la chiamata stessa fallisce e restituisce FALSE, quindi fate l'abitudine a controllare il valore di ritorno. Usatela su una DLL tipica che usa la CRT collegata dinamicamente, dopo aver confermato che niente dipende dalle notifiche di thread.
- Perché una DLL C++/CLI (mista managed) si blocca all'avvio?
- La causa tipica è tentare di eseguire MSIL (codice managed) mentre il loader lock è tenuto. In un assembly misto C++/CLI, se DllMain, le funzioni chiamate da essa, o gli inizializzatori dinamici dei globali sono compilati in MSIL, l'inizializzazione del CLR o il caricamento di un altro assembly può essere richiesto sotto il loader lock, e questo può andare in deadlock. Il compilatore emette l'avviso C4747 quando DllMain stessa tenta di eseguire MSIL direttamente, ma non può rilevare l'esecuzione indiretta attraverso un altro modulo. La mitigazione è compilare DllMain e il suo albero di chiamate come nativo con #pragma unmanaged — o non avere affatto una DllMain.
- Posso ripulire le risorse in DLL_PROCESS_DETACH?
- La risposta cambia tra «uscita del processo» e «scaricamento via FreeLibrary». Su DLL_PROCESS_DETACH all'uscita del processo, gli altri thread sono già stati terminati, e non c'è garanzia che lo spazio degli indirizzi sia ancora coerente, quindi una pulizia come liberare memoria è in realtà pericolosa; la guida ufficiale è che «il gestore ideale è vuoto». Scrivete i dati che devono essere persistiti nel percorso di spegnimento proprio dell'app, e qui essenzialmente non fate niente e ritornate. Su uno scaricamento via FreeLibrary, il processo continua, quindi serve una pulizia completa — fermare i thread, chiudere gli handle, e così via. Attendere l'uscita di un thread dentro DllMain va in deadlock, però, quindi dovete seguire il protocollo ufficiale: segnalare, aspettare fino a uno stato coerente, e finire il lavoro fuori da DllMain.
Profilo dell’autore
Pagina di presentazione dell’autore dell’articolo.
Go Komura
Rappresentante di KomuraSoft LLC
Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.