Inicjalizacja zestawów mieszanych

Deweloperzy systemu Windows muszą zawsze uważać na blokadę modułu ładującego podczas uruchamiania kodu podczas DllMain. Istnieją jednak pewne dodatkowe problemy, które należy wziąć pod uwagę podczas pracy z zestawami języka C++/interfejsu wiersza polecenia w trybie mieszanym.

Kod w pliku DllMain nie może uzyskać dostępu do środowiska uruchomieniowego języka wspólnego platformy .NET (CLR). Oznacza to, że DllMain nie powinno wykonywać żadnych wywołań funkcji zarządzanych, bezpośrednio lub pośrednio; nie należy deklarować ani implementować kodu zarządzanego w DllMainprogramie ; a w programie DllMainnie powinno odbywać się żadne automatyczne ładowanie pamięci ani automatyczne ładowanie biblioteki.

Przyczyny blokady modułu ładującego

Wraz z wprowadzeniem platformy .NET istnieją dwa odrębne mechanizmy ładowania modułu wykonawczego (EXE lub DLL): jeden dla systemu Windows, który jest używany dla modułów niezarządzanych i jeden dla środowiska CLR, który ładuje zestawy platformy .NET. Mieszany problem z ładowaniem bibliotek DLL koncentruje się wokół modułu ładującego systemu operacyjnego Microsoft Windows.

Gdy zestaw zawierający tylko konstrukcje platformy .NET jest ładowany do procesu, moduł ładujący CLR może wykonać wszystkie niezbędne zadania ładowania i inicjowania. Jednak aby załadować zestawy mieszane, które mogą zawierać kod natywny i dane, należy również użyć modułu ładującego systemu Windows.

Moduł ładujący systemu Windows gwarantuje, że żaden kod nie może uzyskać dostępu do kodu lub danych w tej dll przed jego zainicjowaniem. Gwarantuje to, że żaden kod nie może nadmiarowo załadować biblioteki DLL podczas jego częściowego inicjowania. W tym celu moduł ładujący systemu Windows używa sekcji krytycznej dla procesu (często nazywanej "blokadą modułu ładującego"), która uniemożliwia niebezpieczny dostęp podczas inicjowania modułu. W rezultacie proces ładowania jest podatny na wiele klasycznych scenariuszy zakleszczenia. W przypadku zestawów mieszanych następujące dwa scenariusze zwiększają ryzyko zakleszczenia:

  • Po pierwsze, jeśli użytkownicy próbują wykonać funkcje skompilowane w języku Microsoft Intermediate Language (MSIL), gdy blokada modułu ładującego jest przechowywana (na przykład z DllMain lub w statycznych inicjatorach), może to spowodować zakleszczenie. Rozważ przypadek, w którym funkcja MSIL odwołuje się do typu w zestawie, który nie został jeszcze załadowany. ClR podejmie próbę automatycznego załadowania tego zestawu, co może wymagać od modułu ładującego systemu Windows zablokowania blokady modułu ładującego. Występuje zakleszczenie, ponieważ blokada modułu ładującego jest już przechowywana przez kod wcześniej w sekwencji wywołań. Jednak wykonanie MSIL pod blokadą modułu ładującego nie gwarantuje, że wystąpi impas. To sprawia, że ten scenariusz jest trudny do diagnozowania i naprawiania. W niektórych okolicznościach, takich jak gdy biblioteka DLL typu, do których odwołuje się odwołanie, nie zawiera konstrukcji natywnych, a wszystkie jego zależności nie zawierają konstrukcji natywnych, moduł ładujący systemu Windows nie jest wymagany do załadowania zestawu platformy .NET typu, do których odwołuje się odwołanie. Ponadto wymagany zestaw lub jego mieszane zależności natywne/.NET mogły już zostać załadowane przez inny kod. W związku z tym impas może być trudny do przewidzenia i może się różnić w zależności od konfiguracji maszyny docelowej.

  • Po drugie podczas ładowania bibliotek DLL w wersjach 1.0 i 1.1 programu .NET Framework clR zakłada, że blokada modułu ładującego nie została zatrzymana i podjęła kilka akcji, które są nieprawidłowe w ramach blokady modułu ładującego. Zakładając, że blokada modułu ładującego nie jest przechowywana, jest prawidłowym założeniem wyłącznie dla bibliotek DLL platformy .NET. Jednak ze względu na to, że mieszane biblioteki DLL wykonują natywne procedury inicjowania, wymagają natywnego modułu ładującego systemu Windows i w związku z tym blokady modułu ładującego. Tak więc, nawet jeśli deweloper nie próbował wykonać żadnych funkcji MSIL podczas inicjowania biblioteki DLL, nadal istniała niewielka możliwość nieokreślonego zakleszczenia w programie .NET Framework w wersjach 1.0 i 1.1.

Cały niedeterminizm został usunięty z mieszanego procesu ładowania bibliotek DLL. Udało się to wprowadzić następujące zmiany:

  • ClR nie wykonuje już fałszywych założeń podczas ładowania mieszanych bibliotek DLL.

  • Inicjowanie niezarządzane i zarządzane odbywa się w dwóch oddzielnych i odrębnych etapach. Inicjowanie niezarządzane odbywa się najpierw (za pośrednictwem DllMainmetody ), a następnie następuje zainicjowanie zarządzane za pośrednictwem elementu . Konstrukcja obsługiwana przez platformę .cctor NET. Ten ostatni jest całkowicie niewidoczny dla użytkownika, chyba że /Zl jest używany./NODEFAULTLIB Aby uzyskać więcej informacji, zobacz (Ignoruj biblioteki) i /Zl (Pomiń domyślną nazwę biblioteki)./NODEFAULTLIB

Blokada modułu ładującego nadal może wystąpić, ale teraz występuje odtwarzanie i jest wykrywana. Jeśli DllMain zawiera instrukcje MSIL, kompilator generuje ostrzeżenie Ostrzeżenie kompilatora (poziom 1) C4747. Ponadto CRT lub CLR spróbuje wykryć i zgłosić próby wykonania MSIL pod blokadą modułu ładującego. Wykrywanie CRT powoduje wystąpienie błędu czasu wykonywania języka C diagnostyki języka C R6033.

W pozostałej części tego artykułu opisano pozostałe scenariusze, dla których MSIL może być wykonywany w ramach blokady modułu ładującego. Pokazano w nim, jak rozwiązać problem w ramach każdego z tych scenariuszy i technik debugowania.

Scenariusze i obejścia

Istnieje kilka różnych sytuacji, w których kod użytkownika może wykonywać MSIL pod blokadą modułu ładującego. Deweloper musi upewnić się, że implementacja kodu użytkownika nie próbuje wykonać instrukcji MSIL w każdej z tych sytuacji. W poniższych podsekcjach opisano wszystkie możliwości z omówieniem sposobu rozwiązywania problemów w najczęstszych przypadkach.

Dllmain

Funkcja DllMain jest punktem wejścia zdefiniowanym przez użytkownika dla biblioteki DLL. Jeśli użytkownik nie określi inaczej, jest wywoływany za każdym razem, DllMain gdy proces lub wątek dołącza lub odłącza się od zawierającej biblioteki DLL. Ponieważ to wywołanie może wystąpić, gdy blokada modułu ładującego jest przechowywana, nie należy kompilować żadnej funkcji dostarczonej DllMain przez użytkownika do biblioteki MSIL. Ponadto nie można skompilować żadnej funkcji w drzewie wywołań rooted w DllMain pliku MSIL. Aby rozwiązać problemy, blok kodu, który definiuje DllMain , powinien zostać zmodyfikowany za pomocą polecenia #pragma unmanaged. Należy to zrobić dla każdej funkcji, która DllMain wywołuje.

W przypadkach, gdy te funkcje muszą wywołać funkcję, która wymaga implementacji MSIL dla innych kontekstów wywołujących, można użyć strategii duplikowania, w której są tworzone zarówno platformy .NET, jak i natywna wersja tej samej funkcji.

Alternatywnie, jeśli DllMain nie jest to wymagane lub jeśli nie musi być wykonywane w ramach blokady modułu ładującego, możesz usunąć implementację udostępnioną przez DllMain użytkownika, co eliminuje problem.

Jeśli DllMain próby wykonania MSIL bezpośrednio, zostanie wyświetlone ostrzeżenie kompilatora (poziom 1) C4747 . Jednak kompilator nie może wykrywać przypadków, w których DllMain wywołuje funkcję w innym module, który z kolei próbuje wykonać MSIL.

Aby uzyskać więcej informacji na temat tego scenariusza, zobacz Przeszkody w diagnozowaniu.

Inicjowanie obiektów statycznych

Inicjowanie obiektów statycznych może spowodować zakleszczenie, jeśli wymagany jest dynamiczny inicjator. Proste przypadki (takie jak przypisywanie wartości znanej w czasie kompilacji do zmiennej statycznej) nie wymagają inicjowania dynamicznego, więc nie ma ryzyka zakleszczenia. Jednak niektóre zmienne statyczne są inicjowane przez wywołania funkcji, wywołania konstruktora lub wyrażenia, których nie można ocenić w czasie kompilacji. Wszystkie te zmienne wymagają wykonania kodu podczas inicjowania modułu.

Poniższy kod przedstawia przykłady statycznych inicjatorów wymagających inicjowania dynamicznego: wywołania funkcji, konstrukcji obiektu i inicjowania wskaźnika. (Te przykłady nie są statyczne, ale zakłada się, że mają definicje w zakresie globalnym, co ma taki sam efekt).

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

To ryzyko zakleszczenia zależy od tego, czy moduł zawierający jest kompilowany i /clr czy język MSIL zostanie wykonany. W szczególności, jeśli zmienna statyczna jest kompilowana bez /clr (lub znajduje się w #pragma unmanaged bloku), a dynamiczny inicjator wymagany do zainicjowania powoduje wykonanie instrukcji MSIL, może wystąpić zakleszczenie. Jest to spowodowane tym, że w przypadku modułów skompilowanych bez /clrmetody inicjowanie zmiennych statycznych jest wykonywane przez bibliotekę DllMain. Z kolei zmienne statyczne skompilowane /clr za pomocą programu są inicjowane przez .cctorelement , po zakończeniu etapu inicjowania niezarządzanych i zwolnieniu blokady modułu ładującego.

Istnieje wiele rozwiązań zakleszczenia spowodowanych przez dynamiczne inicjowanie zmiennych statycznych. Są one uporządkowane w przybliżeniu w kolejności czasu wymaganego do rozwiązania problemu:

  • Plik źródłowy zawierający zmienną statyczną można skompilować za pomocą /clrpolecenia .

  • Wszystkie funkcje wywoływane przez zmienną statyczną można skompilować do kodu natywnego przy użyciu #pragma unmanaged dyrektywy .

  • Ręcznie sklonuj kod, od którego zależy zmienna statyczna, podając zarówno platformę .NET, jak i wersję natywną o różnych nazwach. Deweloperzy mogą następnie wywoływać wersję natywną z natywnych statycznych inicjatorów i wywoływać wersję platformy .NET w innym miejscu.

Funkcje dostarczone przez użytkownika wpływające na uruchamianie

Istnieje kilka funkcji dostarczanych przez użytkownika, od których biblioteki zależą od inicjowania podczas uruchamiania. Na przykład w przypadku globalnego przeciążenia operatorów w języku C++, takich jak new operatory i delete , wersje dostarczane przez użytkownika są używane wszędzie, w tym w inicjowaniu i niszczeniu biblioteki standardowej języka C++. W związku z tym biblioteka Standardowa języka C++ i statyczne inicjatory udostępniane przez użytkownika będą wywoływać wszystkie wersje tych operatorów dostarczone przez użytkownika.

Jeśli wersje dostarczone przez użytkownika są kompilowane do MSIL, te inicjatory będą podejmować próby wykonania instrukcji MSIL podczas przechowywania blokady modułu ładującego. Użytkownik dostarczony malloc ma te same konsekwencje. Aby rozwiązać ten problem, należy zaimplementować dowolne z tych przeciążeń lub definicji dostarczonych przez użytkownika jako kod natywny przy użyciu #pragma unmanaged dyrektywy .

Aby uzyskać więcej informacji na temat tego scenariusza, zobacz Przeszkody w diagnozowaniu.

Niestandardowe ustawienia regionalne

Jeśli użytkownik udostępnia niestandardowe globalne ustawienia regionalne, to ustawienia regionalne są używane do inicjowania wszystkich przyszłych strumieni we/wy, w tym strumieni, które są statycznie inicjowane. Jeśli ten globalny obiekt ustawień regionalnych jest kompilowany do MSIL, funkcje składowe obiektu ustawień regionalnych skompilowane do MSIL mogą być wywoływane podczas blokowania modułu ładującego.

Istnieją trzy opcje rozwiązywania tego problemu:

Pliki źródłowe zawierające wszystkie globalne definicje strumieni we/wy można skompilować przy użyciu /clr opcji . Zapobiega to wykonywaniu ich statycznych inicjatorów w ramach blokady modułu ładującego.

Niestandardowe definicje funkcji ustawień regionalnych można skompilować do kodu natywnego przy użyciu #pragma unmanaged dyrektywy .

Powstrzymaj się od ustawiania niestandardowych ustawień regionalnych jako globalnych ustawień regionalnych do momentu zwolnienia blokady modułu ładującego. Następnie jawnie skonfiguruj strumienie we/wy utworzone podczas inicjowania przy użyciu niestandardowych ustawień regionalnych.

Przeszkody w diagnozowaniu

W niektórych przypadkach trudno jest wykryć źródło zakleszczeń. W poniższych podsekcjach omówiono te scenariusze i sposoby obejścia tych problemów.

Implementacja w nagłówkach

W wybranych przypadkach implementacje funkcji wewnątrz plików nagłówków mogą komplikować diagnostykę. Funkcje wbudowane i kod szablonu wymagają określenia funkcji w pliku nagłówkowym. Język C++ określa regułę jednej definicji, która wymusza wszystkie implementacje funkcji o tej samej nazwie, aby być semantycznie równoważne. W związku z tym konsolidator języka C++ nie musi uwzględniać żadnych specjalnych zagadnień podczas scalania plików obiektów, które mają zduplikowane implementacje danej funkcji.

W wersjach programu Visual Studio przed programem Visual Studio 2005 konsolidator po prostu wybiera największe z tych semantycznie równoważnych definicji. Należy to zrobić, aby uwzględnić deklaracje przekazywania i scenariusze, gdy różne opcje optymalizacji są używane dla różnych plików źródłowych. Powoduje to problem z mieszanymi bibliotekami DLL natywnymi i .NET.

Ponieważ ten sam nagłówek może być uwzględniony zarówno przez pliki języka C++ z /clr włączonymi, jak i wyłączonymi, lub #include można opakować wewnątrz #pragma unmanaged bloku, możliwe jest posiadanie zarówno biblioteki MSIL, jak i natywnych wersji funkcji, które zapewniają implementacje w nagłówkach. Implementacje MSIL i natywne mają różne semantyki inicjowania w ramach blokady modułu ładującego, co skutecznie narusza jedną regułę definicji. W związku z tym, gdy konsolidator wybiera największą implementację, może wybrać wersję MSIL funkcji, nawet jeśli został jawnie skompilowany do kodu natywnego w innym miejscu przy użyciu #pragma unmanaged dyrektywy . Aby zapewnić, że wersja MSIL szablonu lub funkcji wbudowanej nigdy nie jest wywoływana w ramach blokady modułu ładującego, każda definicja każdej takiej funkcji wywoływanej w ramach blokady modułu ładującego musi zostać zmodyfikowana za #pragma unmanaged pomocą dyrektywy . Jeśli plik nagłówka pochodzi od innej firmy, najprostszym sposobem wprowadzenia tej zmiany jest wypchnięcie i wyskakowanie #pragma unmanaged dyrektywy wokół dyrektywy #include dla pliku nagłówka o przestępstwach. (Zobacz zarządzane, niezarządzane na przykład). Jednak ta strategia nie działa w przypadku nagłówków zawierających inny kod, który musi bezpośrednio wywoływać interfejsy API platformy .NET.

Jako wygoda dla użytkowników zajmujących się blokadą modułu ładującego konsolidator wybierze natywną implementację nad zarządzanym po wyświetleniu obu tych elementów. To ustawienie domyślne pozwala uniknąć powyższych problemów. Istnieją jednak dwa wyjątki od tej reguły w tej wersji z powodu dwóch nierozwiązanych problemów z kompilatorem:

  • Wywołanie funkcji wbudowanej odbywa się za pośrednictwem globalnego wskaźnika funkcji statycznej. Ten scenariusz jest tabelą, ponieważ funkcje wirtualne są wywoływane za pomocą globalnych wskaźników funkcji. Przykład:
#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();
}

Diagnozowanie w trybie debugowania

Wszystkie diagnozy problemów z blokadą modułu ładującego powinny być wykonywane przy użyciu kompilacji debugowania. Kompilacje wydania mogą nie generować diagnostyki. Optymalizacje wykonane w trybie wydania mogą maskuje niektóre scenariusze blokady modułu ładującego MSIL.

Jak debugować problemy z blokowaniem modułu ładującego

Diagnostyka, która jest generowana przez CLR, gdy wywoływana jest funkcja MSIL, powoduje wstrzymanie wykonywania przez clR. To z kolei powoduje wstrzymanie debugera w trybie mieszanym visual C++, a także podczas uruchamiania debuggee w procesie. Jednak podczas dołączania do procesu nie można uzyskać zarządzanego stosu wywołań dla debuggee przy użyciu mieszanego debugera.

Aby zidentyfikować określoną funkcję MSIL, która została wywołana w ramach blokady modułu ładującego, deweloperzy powinni wykonać następujące czynności:

  1. Upewnij się, że dostępne są symbole mscoree.dll i mscorwks.dll.

    Symbole można udostępnić na dwa sposoby. Po pierwsze pliki PDB dla bibliotek mscoree.dll i mscorwks.dll można dodać do ścieżki wyszukiwania symboli. Aby je dodać, otwórz okno dialogowe opcji ścieżki wyszukiwania symboli. (Z Menu Narzędzia wybierz pozycję Opcje. W okienku po lewej stronie okna dialogowego Opcje otwórz węzeł Debugowanie i wybierz pozycję Symbole. Dodaj ścieżkę do plików mscoree.dll i mscorwks.dll PDB na liście wyszukiwania. Te pliki PDB są instalowane w folderze %VSINSTALLDIR%\SDK\v2.0\symbole. Wybierz pozycję OK.

    Po drugie pliki PDB dla bibliotek mscoree.dll i mscorwks.dll można pobrać z serwera symboli firmy Microsoft. Aby skonfigurować serwer symboli, otwórz okno dialogowe opcje ścieżki wyszukiwania symboli. (Z Menu Narzędzia wybierz pozycję Opcje. W okienku po lewej stronie okna dialogowego Opcje otwórz węzeł Debugowanie i wybierz pozycję Symbole. Dodaj tę ścieżkę wyszukiwania do listy wyszukiwania: https://msdl.microsoft.com/download/symbols. Dodaj katalog pamięci podręcznej symboli do pola tekstowego pamięci podręcznej serwera symboli. Wybierz pozycję OK.

  2. Ustaw tryb debugera na tryb natywny.

    Otwórz siatkę Właściwości dla projektu startowego w rozwiązaniu. Wybierz pozycję Debugowanie właściwości>konfiguracji. Ustaw właściwość Typ debugera na wartość Tylko natywna.

  3. Uruchom debuger (F5).

  4. Po wygenerowaniu diagnostyki /clr wybierz pozycję Ponów próbę , a następnie wybierz pozycję Przerwij.

  5. Otwórz okno stosu wywołań. (Na pasku menu wybierz pozycję Debugowanie>stosu wywołań systemu Windows).> Inicjator obraźliwy DllMain lub statyczny jest identyfikowany z zieloną strzałką. Jeśli funkcja obrażająca nie zostanie zidentyfikowana, należy wykonać następujące kroki, aby ją znaleźć.

  6. Otwórz okno Natychmiastowe (na pasku menu wybierz pozycję Debuguj>system Windows>Natychmiast).

  7. Wprowadź .load sos.dll polecenie w oknie Natychmiastowy, aby załadować usługę debugowania SOS.

  8. Wprowadź !dumpstack w oknie Natychmiastowe, aby uzyskać pełną listę stosu wewnętrznego /clr.

  9. Wyszukaj pierwsze wystąpienie (znajdujące się najbliżej dołu stosu) _CorDllMain (jeśli DllMain powoduje problem) lub _VTableBootstrapThunkInitHelperStub lub GetTargetForVTableEntry (jeśli statyczny inicjator powoduje problem). Wpis stosu tuż poniżej tego wywołania jest wywołaniem zaimplementowanej funkcji MSIL, która próbowała wykonać w ramach blokady modułu ładującego.

  10. Przejdź do pliku źródłowego i numeru wiersza zidentyfikowanych w poprzednim kroku i napraw problem przy użyciu scenariuszy i rozwiązań opisanych w sekcji Scenariusze.

Przykład

opis

Poniższy przykład pokazuje, jak uniknąć blokady modułu ładującego przez przeniesienie kodu z DllMain do konstruktora obiektu globalnego.

W tym przykładzie istnieje globalny obiekt zarządzany, którego konstruktor zawiera obiekt zarządzany, który pierwotnie znajdował się w DllMainobiekcie . Druga część tego przykładu odwołuje się do zestawu, tworząc wystąpienie obiektu zarządzanego w celu wywołania konstruktora modułu, który wykonuje inicjowanie.

Kod

// 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;
}

W tym przykładzie pokazano problemy z inicjowaniem zestawów mieszanych:

// 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();
}

Ten kod generuje następujące dane wyjściowe:

Module ctor initializing based on global instance of class.

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

Zobacz też

Zestawy mieszane (natywne i zarządzane)