Condividi tramite


Procedure consigliate per la libreria a collegamento dinamico

**Aggiornato:**

  • 17 maggio 2006

API importanti

La creazione di DLL presenta una serie di sfide per gli sviluppatori. Le DLL non dispongono del controllo delle versioni applicate dal sistema. Quando in un sistema esistono più versioni di una DLL, la facilità di sovrascrittura associata alla mancanza di uno schema di controllo delle versioni crea conflitti tra dipendenze e API. La complessità nell'ambiente di sviluppo, l'implementazione del caricatore e le dipendenze dll hanno creato fragilità nell'ordine di carico e nel comportamento dell'applicazione. Infine, molte applicazioni si basano su DLL e hanno set complessi di dipendenze che devono essere rispettate affinché le applicazioni funzionino correttamente. Questo documento fornisce linee guida per gli sviluppatori di DLL che consentono di creare DLL più affidabili, portabili ed estendibili.

La sincronizzazione non corretta all'interno di DllMain può causare il deadlock di un'applicazione o l'accesso a dati o codice in una DLL non inizializzata. La chiamata di determinate funzioni dall'interno di DllMain causa problemi di questo tipo.

what happens when a library is loaded

Procedure consigliate generali

DllMain viene chiamato mentre viene mantenuto il blocco del caricatore. Di conseguenza, vengono imposte restrizioni significative sulle funzioni che possono essere chiamate all'interno di DllMain. Di conseguenza, DllMain è progettato per eseguire attività di inizializzazione minime, usando un piccolo subset dell'API Microsoft® Windows®. Non è possibile chiamare alcuna funzione in DllMain che tenta direttamente o indirettamente di acquisire il blocco del caricatore. In caso contrario, si presenterà la possibilità che l'applicazione si arresti in modo anomalo o si arresta in modo anomalo. Un errore in un'implementazione di DllMain può compromettere l'intero processo e tutti i relativi thread.

DllMain ideale sarebbe solo uno stub vuoto. Tuttavia, data la complessità di molte applicazioni, questo è in genere troppo restrittivo. Una buona regola generale per DllMain consiste nel posticipare il maggior numero possibile di inizializzazione. L'inizializzazione differita aumenta l'affidabilità dell'applicazione perché questa inizializzazione non viene eseguita mentre viene mantenuto il blocco del caricatore. Inoltre, l'inizializzazione differita consente di usare in modo sicuro molte più API di Windows.

Alcune attività di inizializzazione non possono essere posticipate. Ad esempio, una DLL che dipende da un file di configurazione non deve essere caricata se il file non è valido o contiene garbage. Per questo tipo di inizializzazione, la DLL deve tentare l'azione e non riuscire rapidamente anziché sprecare risorse completando altre operazioni.

Non eseguire mai le attività seguenti dall'interno di DllMain:

  • Chiamare LoadLibrary o LoadLibraryEx (direttamente o indirettamente). Ciò può causare un deadlock o un arresto anomalo.
  • Chiamare GetStringTypeA, GetStringTypeEx o GetStringTypeW (direttamente o indirettamente). Ciò può causare un deadlock o un arresto anomalo.
  • Eseguire la sincronizzazione con altri thread. Ciò può causare un deadlock.
  • Acquisire un oggetto di sincronizzazione di proprietà del codice in attesa di acquisire il blocco del caricatore. Ciò può causare un deadlock.
  • Inizializzare i thread COM usando CoInitializeEx. In determinate condizioni, questa funzione può chiamare LoadLibraryEx.
  • Chiamare le funzioni del Registro di sistema.
  • Chiamare CreateProcess. La creazione di un processo può caricare un'altra DLL.
  • Chiamare ExitThread. L'uscita da un thread durante la disconnessione della DLL può causare l'acquisizione del blocco del caricatore, causando un deadlock o un arresto anomalo.
  • Chiamare CreateThread. La creazione di un thread può funzionare se non si esegue la sincronizzazione con altri thread, ma è rischiosa.
  • Chiamare ShGetFolderPathW. La chiamata delle API della shell o delle cartelle note può comportare la sincronizzazione dei thread e può quindi causare deadlock.
  • Creare una named pipe o un altro oggetto denominato (solo Windows 2000). In Windows 2000, gli oggetti denominati vengono forniti dalla DLL di Servizi terminal. Se questa DLL non viene inizializzata, le chiamate alla DLL possono causare l'arresto anomalo del processo.
  • Usare la funzione di gestione della memoria dal runtime C dinamico (CRT). Se la DLL CRT non viene inizializzata, le chiamate a queste funzioni possono causare l'arresto anomalo del processo.
  • Chiamare le funzioni in User32.dll o Gdi32.dll. Alcune funzioni caricano un'altra DLL, che potrebbe non essere inizializzata.
  • Usare il codice gestito.

Le attività seguenti sono sicure da eseguire all'interno di DllMain:

  • Inizializzare strutture di dati statici e membri in fase di compilazione.
  • Creare e inizializzare oggetti di sincronizzazione.
  • Allocare memoria e inizializzare strutture di dati dinamiche (evitando le funzioni elencate in precedenza).
  • Configurare l'archiviazione locale del thread (TLS).
  • Aprire, leggere e scrivere nei file.
  • Chiamare le funzioni in Kernel32.dll (ad eccezione delle funzioni elencate in precedenza).
  • Impostare i puntatori globali su NULL, disattivando l'inizializzazione dei membri dinamici. In Microsoft Windows Vista™ è possibile usare le funzioni di inizializzazione monouso per assicurarsi che un blocco di codice venga eseguito una sola volta in un ambiente multithreading.

Deadlock causati dall'inversione dell'ordine di blocco

Quando si implementa codice che usa più oggetti di sincronizzazione, ad esempio i blocchi, è fondamentale rispettare l'ordine di blocco. Quando è necessario acquisire più blocchi alla volta, è necessario definire una precedenza esplicita denominata gerarchia di blocchi o ordine di blocco. Ad esempio, se il blocco A viene acquisito prima del blocco B nel codice e il blocco B viene acquisito prima di bloccare C altrove nel codice, l'ordine di blocco è A, B, C e questo ordine deve essere seguito in tutto il codice. L'inversione dell'ordine di blocco si verifica quando l'ordine di blocco non viene seguito, ad esempio se il blocco B viene acquisito prima del blocco A. L'inversione dell'ordine di blocco può causare deadlock difficili da eseguire nel debug. Per evitare tali problemi, tutti i thread devono acquisire blocchi nello stesso ordine.

È importante notare che il caricatore chiama DllMain con il blocco del caricatore già acquisito, quindi il blocco del caricatore deve avere la precedenza più alta nella gerarchia di blocco. Si noti anche che il codice deve acquisire solo i blocchi necessari per una corretta sincronizzazione; non deve acquisire ogni singolo blocco definito nella gerarchia. Ad esempio, se una sezione di codice richiede solo blocchi A e C per una corretta sincronizzazione, il codice deve acquisire il blocco A prima di acquisire il blocco C; non è necessario che il codice acquisisca anche il blocco B. Inoltre, il codice DLL non può acquisire in modo esplicito il blocco del caricatore. Se il codice deve chiamare un'API come GetModuleFileName che può acquisire indirettamente il blocco del caricatore e il codice deve anche acquisire un blocco privato, il codice deve chiamare GetModuleFileName prima di acquisire il blocco P, assicurando così che l'ordine di carico venga rispettato.

La figura 2 è un esempio che illustra l'inversione dell'ordine di blocco. Si consideri una DLL il cui thread principale contiene DllMain. Il caricatore di libreria acquisisce il blocco del caricatore L e quindi chiama in DllMain. Il thread principale crea oggetti di sincronizzazione A, B e G per serializzare l'accesso alle relative strutture di dati e quindi tenta di acquisire il blocco G. Un thread di lavoro che ha già acquisito correttamente il blocco G chiama quindi una funzione come GetModuleHandle che tenta di acquisire il blocco del caricatore L. Di conseguenza, il thread di lavoro viene bloccato su L e il thread principale è bloccato su G, causando un deadlock.

deadlock caused by lock order inversion

Per evitare deadlock causati dall'inversione dell'ordine di blocco, tutti i thread devono tentare di acquisire oggetti di sincronizzazione nell'ordine di caricamento definito in qualsiasi momento.

Procedure consigliate per la sincronizzazione

Si consideri una DLL che crea thread di lavoro come parte dell'inizializzazione. Dopo la pulizia della DLL, è necessario eseguire la sincronizzazione con tutti i thread di lavoro per assicurarsi che le strutture di dati siano in uno stato coerente e quindi terminare i thread di lavoro. Attualmente, non esiste un modo semplice per risolvere completamente il problema della sincronizzazione e dell'arresto delle DLL in un ambiente multithreading. Questa sezione descrive le procedure consigliate correnti per la sincronizzazione dei thread durante l'arresto della DLL.

Sincronizzazione dei thread in DllMain durante l'uscita del processo

  • Al momento in cui DllMain viene chiamato all'uscita del processo, tutti i thread del processo sono stati puliti forzatamente e c'è la possibilità che lo spazio indirizzi sia incoerente. La sincronizzazione non è necessaria in questo caso. In altre parole, il gestore DLL_PROCESS_DETACH ideale è vuoto.
  • Windows Vista garantisce che le strutture di dati di base (variabili di ambiente, directory corrente, heap di processo e così via) siano in uno stato coerente. Tuttavia, altre strutture di dati possono essere danneggiate, quindi la pulizia della memoria non è sicura.
  • Lo stato permanente che deve essere salvato deve essere scaricato nell'archiviazione permanente.

Sincronizzazione dei thread in DllMain per DLL_THREAD_DETACH durante il caricamento della DLL

  • Quando la DLL viene scaricata, lo spazio indirizzi non viene eliminato. Pertanto, è previsto che la DLL esegua un arresto pulito. Sono inclusi la sincronizzazione dei thread, gli handle aperti, lo stato permanente e le risorse allocate.
  • La sincronizzazione dei thread è complessa perché l'attesa dell'uscita dai thread in DllMain può causare un deadlock. Ad esempio, la DLL A contiene il blocco del caricatore. Segnala al thread T di uscire e attende l'uscita del thread. Il thread T viene chiuso e il caricatore tenta di acquisire il blocco del caricatore per chiamare nella DLL A con DLL_THREAD_DETACH. In questo modo si verifica un deadlock. Per ridurre al minimo il rischio di un deadlock:
    • LA DLL A ottiene un messaggio DLL_THREAD_DETACH nella dllMain e imposta un evento per il thread T, segnalandolo per uscire.
    • Il thread T completa l'attività corrente, porta se stesso a uno stato coerente, segnala la DLL A e attende infinitamente. Si noti che le routine di verifica della coerenza devono seguire le stesse restrizioni di DllMain per evitare il deadlocking.
    • DLL A termina T, sapendo che si trova in uno stato coerente.

Se una DLL viene scaricata dopo la creazione di tutti i thread, ma prima di iniziare l'esecuzione, i thread potrebbero arrestarsi in modo anomalo. Se la DLL ha creato thread nella dllMain come parte dell'inizializzazione, alcuni thread potrebbero non aver completato l'inizializzazione e il relativo messaggio DLL_THREAD_ATTACH è ancora in attesa di essere recapitato alla DLL. In questo caso, se la DLL viene scaricata, inizierà a terminare i thread. Tuttavia, alcuni thread potrebbero essere bloccati dietro il blocco del caricatore. I messaggi DLL_THREAD_ATTACH vengono elaborati dopo l'annullamento del mapping della DLL, causando l'arresto anomalo del processo.

Consigli

Di seguito sono riportate le linee guida consigliate:

  • Usare Application Verifier per rilevare gli errori più comuni in DllMain.
  • Se si usa un blocco privato all'interno di DllMain, definire una gerarchia di blocco e usarla in modo coerente. Il blocco del caricatore deve trovarsi nella parte inferiore di questa gerarchia.
  • Verificare che nessuna chiamata dipende da un'altra DLL che potrebbe non essere stata ancora caricata completamente.
  • Eseguire semplici inizializzazioni in modo statico in fase di compilazione, anziché in DllMain.
  • Rinviare tutte le chiamate in DllMain che possono attendere fino a un secondo momento.
  • Rinviare le attività di inizializzazione che possono attendere fino a un secondo momento. Alcune condizioni di errore devono essere rilevate in anticipo in modo che l'applicazione possa gestire correttamente gli errori. Tuttavia, ci sono compromessi tra questo rilevamento precoce e la perdita di robustezza che può risultare da esso. Rinviare l'inizializzazione è spesso preferibile.