Inizializzazione di assembly misti

Gli sviluppatori di Windows devono sempre essere diffidenti del blocco del caricatore durante l'esecuzione del codice durante DllMain. Esistono tuttavia alcuni problemi aggiuntivi da considerare quando si gestiscono assembly in modalità mista C++/CLI.

Il codice all'interno di DllMain non deve accedere a .NET Common Language Runtime (CLR). Ciò significa che DllMain non deve effettuare chiamate alle funzioni gestite, direttamente o indirettamente; non deve essere dichiarato o implementato codice gestito in DllMaine non deve essere eseguito alcun processo di Garbage Collection o caricamento automatico della libreria all'interno DllMaindi .

Cause del blocco del caricatore

Con l'introduzione della piattaforma .NET, esistono due meccanismi distinti per il caricamento di un modulo di esecuzione (EXE o DLL): uno per Windows, che viene usato per i moduli non gestiti e uno per CLR, che carica gli assembly .NET. Il problema del caricamento delle DLL miste verte intorno al caricatore del sistema operativo Microsoft Windows.

Quando un assembly contenente solo costrutti .NET viene caricato in un processo, il caricatore CLR può eseguire tutte le attività di caricamento e inizializzazione necessarie. Tuttavia, per caricare assembly misti che possono contenere codice nativo e dati, è necessario usare anche il caricatore di Windows.

Il caricatore di Windows garantisce che nessun codice possa accedere al codice o ai dati in tale DLL prima dell'inizializzazione. E garantisce che nessun codice possa caricare in modo ridondante la DLL mentre è parzialmente inizializzata. A tale scopo, il caricatore di Windows usa una sezione critica globale del processo (spesso denominata "blocco del caricatore") che impedisce l'accesso non sicuro durante l'inizializzazione del modulo. Ne deriva che il processo di caricamento è vulnerabile a molti classici scenari di deadlock. Per gli assembly misti, i due scenari seguenti aumentano il rischio di deadlock:

  • In primo luogo, se gli utenti tentano di eseguire funzioni compilate in Microsoft Intermediate Language (MSIL) quando il blocco del caricatore viene mantenuto (da DllMain o in inizializzatori statici, ad esempio), può causare deadlock. Si consideri il caso in cui la funzione MSIL fa riferimento a un tipo in un assembly non ancora caricato. CLR tenterà di caricare automaticamente l'assembly, il quale potrebbe richiedere al caricatore di Windows di attivare il blocco sul blocco del caricatore. Si verifica un deadlock, poiché il blocco del caricatore è già mantenuto dal codice precedente nella sequenza di chiamata. Tuttavia, l'esecuzione di MSIL nel blocco del caricatore non garantisce che si verifichi un deadlock. Questo è ciò che rende questo scenario difficile da diagnosticare e correggere. In alcuni casi, ad esempio quando la DLL del tipo a cui si fa riferimento non contiene costrutti nativi e tutte le relative dipendenze non contengono costrutti nativi, il caricatore di Windows non è necessario per caricare l'assembly .NET del tipo a cui si fa riferimento. Inoltre, l'assembly richiesto o le relative dipendenze .NET/native miste possono essere già state caricate da altro codice. Di conseguenza, il verificarsi di un deadlock può essere difficile da prevedere e può variare a seconda della configurazione della macchina di destinazione.

  • In secondo luogo, quando si caricano DLL nelle versioni 1.0 e 1.1 di .NET Framework, CLR presuppone che il blocco del caricatore non sia stato mantenuto e che siano state eseguite diverse azioni non valide nel blocco del caricatore. Supponendo che il blocco del caricatore non sia mantenuto, è un presupposto valido per le DLL puramente .NET. Tuttavia, poiché le DLL miste eseguono routine di inizializzazione native, richiedono il caricatore nativo di Windows e di conseguenza il blocco del caricatore. Pertanto, anche se lo sviluppatore non tentava di eseguire funzioni MSIL durante l'inizializzazione della DLL, c'era ancora una piccola possibilità di deadlock non deterministico nelle versioni di .NET Framework 1.0 e 1.1.

Questo comportamento non deterministico è stato quasi totalmente rimosso dal processo di caricamento delle DLL miste. Questa operazione è stata eseguita con queste modifiche:

  • CLR non si basa più su falsi presupposti durante il caricamento di DLL miste.

  • L'inizializzazione non gestita e gestita viene eseguita in due fasi separate e distinte. L'inizializzazione non gestita avviene prima (tramite DllMain) e l'inizializzazione gestita avviene successivamente, tramite un oggetto . Costrutto supportato da .cctor NET. Quest'ultimo è completamente trasparente per l'utente, a meno che /Zl non venga usato o /NODEFAULTLIB . Per altre informazioni, vedere/NODEFAULTLIB (Ignora librerie) e /Zl (omettere il nome predefinito della libreria).

Il blocco del caricatore può comunque ancora verificarsi, ma avviene in modo riproducibile e pertanto è individuabile. Se DllMain contiene istruzioni MSIL, il compilatore genera un avviso del compilatore ( livello 1) C4747. Inoltre, CRT o CLR tenteranno di rilevare e segnalare gli eventuali tentativi di eseguire codice MSIL con il blocco del caricatore attivo. Il rilevamento di CRT genererà l'errore R6033 di runtime del linguaggio C.

Nella parte restante di questo articolo vengono descritti gli scenari rimanenti per i quali MSIL può essere eseguito con il blocco del caricatore. Illustra come risolvere il problema in ognuno di questi scenari e tecniche di debug.

Scenari e soluzioni alternative

Ci sono diverse situazioni in cui il codice utente può eseguire codice MSIL con il blocco del caricatore attivo. Lo sviluppatore deve assicurarsi che l'implementazione del codice utente non tenti di eseguire istruzioni MSIL in ognuna di queste circostanze. Nelle seguenti sottosezioni vengono descritte tutte le possibilità e viene indicato come risolvere i problemi nei casi più frequenti.

DllMain

La DllMain funzione è un punto di ingresso definito dall'utente per una DLL. Salvo diversamente specificato dall'utente, la funzione DllMain viene richiamata ogni volta che un processo o un thread si connette o si disconnette dalla DLL che lo contiene. Poiché questa chiamata può verificarsi mentre il blocco del caricatore è attivo, in MSIL non deve essere compilata nessuna funzione DllMain fornita dall'utente. Inoltre, nessuna funzione nella struttura ad albero delle chiamate che ha origine nella funzione DllMain può essere compilata in MSIL. Per risolvere i problemi, il blocco di codice che definisce DllMain deve essere modificato con #pragma unmanaged. Lo stesso vale per ciascuna funzione chiamata da DllMain .

Nei casi in cui queste funzioni devono chiamare una funzione che richiede un'implementazione MSIL per altri contesti chiamanti, è possibile usare una strategia di duplicazione in cui vengono creati sia .NET che una versione nativa della stessa funzione.

In alternativa, se DllMain non è necessario o se non è necessario eseguirlo con il blocco del caricatore, è possibile rimuovere l'implementazione fornita dall'utente DllMain , eliminando così il problema.

Se DllMain tenta di eseguire direttamente MSIL, verrà restituito l'avviso del compilatore (livello 1) C4747 . Tuttavia, il compilatore non riesce a rilevare i casi in cui DllMain chiama una funzione in un altro modulo che a sua volta tenta di eseguire MSIL.

Per altre informazioni su questo scenario, vedere Ostacoli alla diagnosi.

Inizializzazione di oggetti statici

L'inizializzazione di oggetti statici può determinare un deadlock se è richiesto un inizializzatore dinamico. I casi semplici, ad esempio quando si assegna un valore noto in fase di compilazione a una variabile statica, non richiedono l'inizializzazione dinamica, quindi non esiste alcun rischio di deadlock. Tuttavia, alcune variabili statiche vengono inizializzate da chiamate di funzione, chiamate al costruttore o espressioni che non possono essere valutate in fase di compilazione. Tutte queste variabili richiedono l'esecuzione del codice durante l'inizializzazione del modulo.

Nel codice riportato di seguito vengono illustrati esempi di inizializzatori statici che richiedono l'inizializzazione dinamica: una chiamata di funzione, una costruzione di oggetto e l'inizializzazione di un puntatore. Questi esempi non sono statici, ma si presuppone che abbiano definizioni nell'ambito globale, che ha lo stesso effetto.

// dynamic initializer function generated
int a = init();
CObject o(arg1, arg2);
CObject* op = new CObject(arg1, arg2);

Questo rischio di deadlock dipende dal fatto che il modulo contenitore sia compilato con /clr e se verrà eseguito MSIL. In particolare, se la variabile statica viene compilata senza /clr (o è in un #pragma unmanaged blocco) e l'inizializzatore dinamico necessario per inizializzarlo comporta l'esecuzione di istruzioni MSIL, potrebbe verificarsi un deadlock. Perché, per i moduli compilati senza /clr, l'inizializzazione delle variabili statiche viene eseguita da DllMain. Al contrario, le variabili statiche compilate con /clr vengono inizializzate da .cctor, dopo il completamento della fase di inizializzazione non gestita e il blocco del caricatore è stato rilasciato.

Esistono diverse soluzioni per il deadlock causato dall'inizializzazione dinamica delle variabili statiche. Sono disposti qui approssimativamente in ordine di tempo necessario per risolvere il problema:

  • Il file di origine contenente la variabile statica può essere compilato con /clr.

  • Tutte le funzioni chiamate dalla variabile statica possono essere compilate in codice nativo usando la #pragma unmanaged direttiva .

  • È possibile clonare manualmente il codice da cui dipende la variabile statica, fornendo sia una versione .NET sia una versione nativa con nomi diversi. Gli sviluppatori possono quindi chiamare la versione nativa dagli inizializzatori statici nativi e la versione .NET da altri punti.

Funzioni fornite dall'utente che influiscono sull'avvio

Esistono numerose funzioni fornite dall'utente da cui dipendono le librerie per l'inizializzazione durante l'avvio. Ad esempio, quando si esegue l'overload globale degli operatori in C++ come gli new operatori e delete , le versioni fornite dall'utente vengono usate ovunque, inclusa l'inizializzazione e la distruzione della libreria standard C++. Di conseguenza, la libreria standard C++ e gli inizializzatori statici forniti dall'utente richiamano tutte le versioni fornite dall'utente di questi operatori.

Se le versioni fornite dall'utente sono compilate in MSIL, questi inizializzatori tenteranno di eseguire le istruzioni MSIL mentre il blocco del caricatore è attivo. Un utente fornito malloc ha le stesse conseguenze. Per risolvere questo problema, è necessario implementare uno qualsiasi di questi overload o definizioni fornite dall'utente come codice nativo usando la #pragma unmanaged direttiva .

Per altre informazioni su questo scenario, vedere Ostacoli alla diagnosi.

Impostazioni locali personalizzate

Se l'utente fornisce impostazioni locali globali personalizzate, queste impostazioni locali vengono usate per inizializzare tutti i flussi di I/O futuri, inclusi i flussi inizializzati in modo statico. Se l'oggetto impostazioni locali globali viene compilato in codice MSIL, è possibile che le funzioni membro dell'oggetto impostazioni locali compilate in MSIL siano richiamate mentre il blocco del caricatore è attivo.

Esistono tre soluzioni a questo problema:

I file di origine contenenti tutte le definizioni di flusso di I/O globali possono essere compilati usando l'opzione /clr . Impedisce l'esecuzione dei relativi inizializzatori statici in caso di blocco del caricatore.

Le definizioni delle funzioni delle impostazioni locali personalizzate possono essere compilate in codice nativo usando la #pragma unmanaged direttiva .

Si consiglia di non impostare le impostazioni locali personalizzate come impostazioni locali globali fintato che il blocco del caricatore non viene rilasciato. Configurare quindi in modo esplicito i flussi I/O creati durante l'inizializzazione con le impostazioni locali personalizzate.

Limiti alla diagnosi

In alcuni casi, è difficile rilevare l'origine dei deadlock. Nelle sottosezioni riportate di seguito vengono descritti questi scenari e i modi per ovviare ai problemi.

Implementazione nei file di intestazione

In certi casi, le implementazioni delle funzioni all'interno dei file di intestazione possono complicare la diagnostica. Le funzioni inline e il codice di modello richiedono entrambi la specifica di funzioni in un file di intestazione. Il linguaggio C++ specifica la regola di definizione unica, in base alla quale tutte le implementazioni di funzioni con lo stesso nome si equivalgono a livello semantico. Di conseguenza, per il linker di C++ non sono necessarie considerazioni speciali quando si esegue l'unione di file oggetto per i quali sono disponibili implementazioni duplicate di una data funzione.

Nelle versioni di Visual Studio precedenti a Visual Studio 2005, il linker sceglie semplicemente il più grande di queste definizioni semanticamente equivalenti. Questa operazione viene eseguita per supportare le dichiarazioni forward e gli scenari in cui vengono usate diverse opzioni di ottimizzazione per file di origine diversi. Crea un problema per le DLL native e .NET miste.

Poiché la stessa intestazione può essere inclusa sia dai file C++ con /clr abilitato che disabilitato oppure un #include può essere sottoposto a wrapping all'interno di un #pragma unmanaged blocco, è possibile avere sia MSIL che versioni native delle funzioni che forniscono implementazioni nelle intestazioni. Le implementazioni MSIL e native hanno una semantica diversa per l'inizializzazione nel blocco del caricatore, che viola effettivamente la regola di definizione. Di conseguenza, quando il linker sceglie l'implementazione più grande, può scegliere la versione MSIL di una funzione, anche se è stata compilata in modo esplicito nel codice nativo altrove usando la #pragma unmanaged direttiva . Per garantire che una versione MSIL di un modello o di una funzione inline non venga mai chiamata nel blocco del caricatore, ogni definizione di ogni funzione chiamata nel blocco del caricatore deve essere modificata con la #pragma unmanaged direttiva . Se il file di intestazione proviene da terze parti, il modo più semplice per apportare questa modifica consiste nel eseguire il push e visualizzare la direttiva intorno alla #pragma unmanaged direttiva #include per il file di intestazione che causa l'errore. Per un esempio, vedere gestito, non gestito . Tuttavia, questa strategia non funziona per le intestazioni che contengono altro codice che deve chiamare direttamente le API .NET.

Per facilitare la gestione del blocco del caricatore da parte degli utenti, il linker sceglierà l'implementazione nativa invece dell'implementazione gestita, se presenti entrambe. Questa impostazione predefinita evita i problemi precedenti. Tuttavia, esistono due eccezioni a questa regola in questa versione a causa di due problemi non risolti con il compilatore:

  • La chiamata a una funzione inline avviee tramite un puntatore a funzione statico globale. Questo scenario è tabella perché le funzioni virtuali vengono chiamate tramite puntatori a funzione globali. ad esempio:
#include "definesmyObject.h"
#include "definesclassC.h"

typedef void (*function_pointer_t)();

function_pointer_t myObject_p = &myObject;

#pragma unmanaged
void DuringLoaderlock(C & c)
{
    // Either of these calls could resolve to a managed implementation,
    // at link-time, even if a native implementation also exists.
    c.VirtualMember();
    myObject_p();
}

Diagnosi in modalità di debug

Tutte le diagnosi di problemi di blocco del caricatore devono essere eseguite con build di debug. Le build di versione potrebbero non produrre diagnostica. Inoltre, le ottimizzazioni effettuate in modalità rilascio potrebbero mascherare alcuni degli scenari di blocco del caricatore in caso di blocco del caricatore.

Come eseguire il debug dei problemi di blocco del caricatore

La diagnostica generata da CLR quando viene richiamata una funzione MSIL causa la sospensione dell'esecuzione di CLR. Ciò a sua volta determina la sospensione del debugger in modalità mista di Visual C++ anche quando si esegue il debug in-process. Tuttavia, quando si collega al processo, non è possibile ottenere uno stack di chiamate gestito per l'oggetto debug usando il debugger misto.

Per identificare la funzione MSIL specifica chiamata con il blocco del caricatore attivato, gli sviluppatori devono effettuare quanto riportato di seguito:

  1. Assicurarsi che siano disponibili simboli per mscoree.dll e mscorwks.dll.

    È possibile rendere i simboli disponibili in due modi. È innanzitutto possibile aggiungere i PDB per mscoree.dll e mscorwks.dll al percorso di ricerca dei simboli. Per aggiungerli, aprire la finestra di dialogo delle opzioni del percorso di ricerca dei simboli. (Da Scegliere Opzioni dal menu Strumenti. Nel riquadro sinistro della finestra di dialogo Opzioni aprire il nodo Debug e scegliere Simboli. Aggiungere il percorso ai file mscoree.dll e mscorwks.dll PDB all'elenco di ricerca. Questi file PDB sono installati in %VSINSTALLDIR%\SDK\v2.0\symbols. Scegliere OK.

    È anche possibile scaricare i file PDB per mscoree.dll e mscorwks.dll da Microsoft Symbol Server. Per configurare Microsoft Symbol Server, aprire la finestra di dialogo contenente le opzioni del percorso di ricerca dei simboli. (Da Scegliere Opzioni dal menu Strumenti. Nel riquadro sinistro della finestra di dialogo Opzioni aprire il nodo Debug e scegliere Simboli. Aggiungere questo percorso di ricerca all'elenco di ricerca: https://msdl.microsoft.com/download/symbols. Aggiungere una directory cache per i simboli nella casella di testo relativa alla cache del server di simboli. Scegliere OK.

  2. Impostare la modalità del debugger sulla modalità solo nativa.

    Aprire la griglia Proprietà per il progetto di avvio nella soluzione. Selezionare Debug delle proprietà>di configurazione. Impostare la proprietà Debugger Type su Solo nativo.

  3. Avviare il debugger (F5).

  4. Quando viene generata la /clr diagnostica, scegliere Riprova e quindi scegliere Interrompi.

  5. Aprire la finestra dello stack di chiamate (Sulla barra dei menu scegliere Eseguire il debug>dello stack di chiamate di Windows.> L'inizializzatore statico o offensivo DllMain viene identificato con una freccia verde. Se la funzione che causa l'errore non viene identificata, è necessario eseguire i passaggi seguenti per trovarla.

  6. Aprire la finestra Immediata (nella barra dei menu scegliere Debug>immediato di Windows).>

  7. Immettere .load sos.dll nella finestra Immediata per caricare il servizio di debug SOS.

  8. Immettere !dumpstack nella finestra Immediata per ottenere un elenco completo dello stack interno /clr .

  9. Cercare la prima istanza (più vicina alla fine dello stack) di _CorDllMain (se DllMain causa il problema) o _VTableBootstrapThunkInitHelperStub o GetTargetForVTableEntry (se un inizializzatore statico causa il problema). La voce riportata sotto questa chiamata corrisponde alla chiamata della funzione implementata MSIL che ha tentato l'esecuzione con il blocco del caricatore attivo.

  10. Passare al file di origine e al numero di riga identificato nel passaggio precedente e correggere il problema usando gli scenari e le soluzioni descritti nella sezione Scenari.

Esempio

Descrizione

Nell'esempio seguente viene illustrato come evitare il blocco del caricatore spostando il codice dal DllMain costruttore di un oggetto globale.

In questo esempio è presente un oggetto gestito globale il cui costruttore contiene l'oggetto gestito originariamente in DllMain. La seconda parte di questo esempio fa riferimento all'assembly, creando un'istanza dell'oggetto gestito per richiamare il costruttore del modulo che esegue l'inizializzazione.

Codice

// initializing_mixed_assemblies.cpp
// compile with: /clr /LD
#pragma once
#include <stdio.h>
#include <windows.h>
struct __declspec(dllexport) A {
   A() {
      System::Console::WriteLine("Module ctor initializing based on global instance of class.\n");
   }

   void Test() {
      printf_s("Test called so linker doesn't throw away unused object.\n");
   }
};

#pragma unmanaged
// Global instance of object
A obj;

extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) {
   // Remove all managed code from here and put it in constructor of A.
   return true;
}

Questo esempio illustra i problemi nell'inizializzazione di assembly misti:

// initializing_mixed_assemblies_2.cpp
// compile with: /clr initializing_mixed_assemblies.lib
#include <windows.h>
using namespace System;
#include <stdio.h>
#using "initializing_mixed_assemblies.dll"
struct __declspec(dllimport) A {
   void Test();
};

int main() {
   A obj;
   obj.Test();
}

Questo codice genera l'output seguente:

Module ctor initializing based on global instance of class.

Test called so linker doesn't throw away unused object.

Vedi anche

Assembly misti (nativi e gestiti)