Udostępnij przez


Najlepsze rozwiązania dotyczące biblioteki Dynamic-Link

**Aktualizowano:**

  • 17 maja 2006 r.

ważne interfejsy API

Tworzenie bibliotek DLL stanowi szereg wyzwań dla deweloperów. Biblioteki DLL nie mają wymuszanej przez system kontroli wersji. Gdy w systemie istnieje wiele wersji biblioteki DLL, łatwość zastępowania w połączeniu z brakiem schematu przechowywania wersji powoduje konflikty zależności i interfejsu API. Złożoność środowiska programistycznego, implementacja modułu ładującego i zależności bibliotek DLL spowodowały kruchość w kolejności ładowania i zachowaniu aplikacji. Na koniec wiele aplikacji korzysta z bibliotek DLL i ma złożone zestawy zależności, które muszą być honorowane, aby aplikacje działały prawidłowo. Ten dokument zawiera wskazówki dla deweloperów bibliotek DLL, które ułatwiają tworzenie bardziej niezawodnych, przenośnych i rozszerzalnych bibliotek DLL.

Niewłaściwa synchronizacja w DllMain może spowodować zakleszczenie aplikacji lub uzyskanie dostępu do danych lub kodu w niezainicjowanej biblioteki DLL. Wywoływanie niektórych funkcji z poziomu DllMain powoduje takie problemy.

, co się stanie po załadowaniu biblioteki

Ogólne najlepsze rozwiązania

DllMain jest wywoływana podczas blokowania modułu ładującego. W związku z tym na funkcje, które mogą być wywoływane w DllMain, nakłada się znaczne ograniczenia. W związku z tym DllMain jest przeznaczona do wykonywania minimalnych zadań inicjowania przy użyciu małego podzestawu interfejsu API systemu Microsoft® Windows®. Nie można wywołać żadnej funkcji w DllMain, która bezpośrednio lub pośrednio próbuje uzyskać blokadę modułu ładującego. W przeciwnym razie wprowadzisz możliwość zakleszczenia lub awarii aplikacji. Błąd w implementacji DllMain może zagrozić całemu procesowi i wszystkim jego wątkom.

Idealny DllMain byłby tylko pustym szkieletem. Jednak ze względu na złożoność wielu aplikacji jest to ogólnie zbyt restrykcyjne. Praktyczna zasada dla DllMain to przesunąć jak najwięcej inicjalizacji. Leniwe inicjowanie zwiększa niezawodność aplikacji, ponieważ ta inicjalizacja nie jest wykonywana podczas posiadania blokady ładowarki. Ponadto leniwe inicjowanie umożliwia bezpieczne korzystanie ze znacznie większej części Windows API.

Niektórych zadań inicjowania nie można odłożyć. Na przykład biblioteka DLL, która zależy od pliku konfiguracji, nie może załadować, jeśli plik jest źle sformułowany lub zawiera śmieci. W przypadku tego typu inicjowania biblioteka DLL powinna podjąć próbę wykonania akcji i szybko zakończyć działanie, a nie marnować zasobów, wykonując inne prace.

Nigdy nie należy wykonywać następujących zadań z DllMain:

  • Wywołaj LoadLibrary lub LoadLibraryEx (bezpośrednio lub pośrednio). Może to spowodować zakleszczenie lub awarię.
  • Wywołaj GetStringTypeA, GetStringTypeExlub GetStringTypeW (bezpośrednio lub pośrednio). Może to spowodować zakleszczenie lub awarię.
  • Synchronizuj z innymi wątkami. Może to spowodować zakleszczenie.
  • Uzyskaj obiekt synchronizacji należący do kodu, który oczekuje na uzyskanie blokady modułu ładującego. Może to spowodować zakleszczenie.
  • Zainicjuj wątki COM przy użyciu CoInitializeEx. W pewnych warunkach ta funkcja może wywołać LoadLibraryEx.
  • Wywołaj funkcje rejestru.
  • Wywołaj CreateProcess. Tworzenie procesu może załadować inną bibliotekę DLL.
  • Wywołaj ExitThread. Zamknięcie wątku podczas odłączania bibliotek DLL może spowodować ponowne uzyskanie blokady modułu ładującego, powodując zakleszczenie lub awarię.
  • Wywołaj CreateThread. Tworzenie wątku może działać, jeśli nie synchronizujesz się z innymi wątkami, ale jest ryzykowne.
  • Wywołaj ShGetFolderPathW. Wywoływanie API powłok/znanych folderów może prowadzić do synchronizacji wątków, co w konsekwencji może powodować zakleszczenia.
  • Utwórz nazwany potok lub inny nazwany obiekt (tylko system Windows 2000). W systemie Windows 2000 nazwane obiekty są udostępniane przez bibliotekę DLL Terminal Services. Jeśli ta biblioteka DLL nie została zainicjowana, wywołania biblioteki DLL mogą spowodować awarię procesu.
  • Użyj funkcji zarządzania pamięcią z dynamicznej biblioteki C Run-Time (CRT). Jeśli biblioteka DLL CRT nie jest zainicjowana, wywołania tych funkcji mogą spowodować awarię procesu.
  • Wywołaj funkcje w User32.dll lub Gdi32.dll. Niektóre funkcje ładują inną bibliotekę DLL, która może nie zostać zainicjowana.
  • Użyj kodu zarządzanego.

Następujące zadania można bezpiecznie wykonać w DllMain:

  • Inicjowanie statycznych struktur danych i elementów członkowskich w czasie kompilacji.
  • Tworzenie i inicjowanie obiektów synchronizacji.
  • Przydziel pamięć i zainicjuj dynamiczne struktury danych (unikając funkcji wymienionych powyżej).
  • Skonfiguruj magazyn lokalny wątku (TLS).
  • Otwieranie, odczytywanie i zapisywanie w plikach.
  • Wywoływanie funkcji w Kernel32.dll (z wyjątkiem funkcji wymienionych powyżej).
  • Ustaw globalne wskaźniki na wartość NULL, opóźniając inicjowanie dynamicznych elementów. W systemie Microsoft Windows Vista™ można użyć funkcji inicjowania jednorazowego, aby upewnić się, że blok kodu jest wykonywany tylko raz w środowisku wielowątkowym.

Zakleszczenia występujące w wyniku inwersji kolejności zamków

Podczas implementowania kodu korzystającego z wielu obiektów synchronizacji, takich jak blokady, ważne jest przestrzeganie kolejności blokady. Jeśli konieczne jest uzyskanie więcej niż jednej blokady jednocześnie, należy zdefiniować jawną precedencję zwaną hierarchią blokady lub kolejnością blokad. Jeśli na przykład blokada A zostanie uzyskana przed blokadą B w kodzie, a blokada B zostanie uzyskana przed blokadą C gdzie indziej w kodzie, kolejność blokad to A, B, C i tę kolejność należy zachować w całym kodzie. Inwersja kolejności blokady występuje, gdy kolejność blokowania nie jest przestrzegana — na przykład jeśli blokada B zostanie nabyta przed zablokowaniem A. Odwrócenie kolejności blokady może spowodować zakleszczenia, które są trudne do debugowania. Aby uniknąć takich problemów, wszystkie wątki muszą uzyskać blokady w tej samej kolejności.

Należy pamiętać, że moduł ładujący wywołuje DllMain z już pozyskaną blokadą modułu ładującego, więc blokada modułu ładującego powinna mieć najwyższy priorytet w hierarchii blokowania. Należy również pamiętać, że kod musi uzyskać tylko blokady wymagane do właściwej synchronizacji; nie musi uzyskiwać każdej pojedynczej blokady zdefiniowanej w hierarchii. Jeśli na przykład sekcja kodu wymaga tylko blokadY A i C do właściwej synchronizacji, kod powinien uzyskać blokadę A przed uzyskaniem blokady C; nie jest konieczne, aby kod uzyskał również blokadę B. Ponadto kod biblioteki DLL nie może jawnie uzyskać blokady modułu ładującego. Jeśli kod musi wywołać interfejs API, taki jak GetModuleFileName, który może pośrednio uzyskać blokadę modułu ładującego, a kod musi również uzyskać blokadę prywatną, kod powinien wywołać GetModuleFileName przed uzyskaniem blokady P, zapewniając, że kolejność ładowania jest przestrzegana.

Rysunek 2 to przykład ilustrujący inwersję kolejności blokowania. Rozważ bibliotekę DLL, której główny wątek zawiera DllMain. Ładowarka biblioteki uzyskuje blokadę ładowarki L, a następnie wywołuje funkcję DllMain. Główny wątek tworzy obiekty synchronizacji A, B i G w celu serializacji dostępu do jego struktur danych, a następnie próbuje uzyskać blokadę G. Wątek procesu roboczego, który już pomyślnie nabył blokadę G, wywołuje funkcję, taką jak GetModuleHandle, która próbuje uzyskać blokadę modułu ładującego L. W związku z tym wątek procesu roboczego jest zablokowany na L, a główny wątek jest zablokowany w G, co powoduje zakleszczenie.

zakleszczenie spowodowane inwersją kolejności blokad

Aby zapobiec zakleszczeniom spowodowanym przez inwersję kolejności blokad, wszystkie wątki powinny zawsze próbować uzyskiwać obiekty synchronizacji w zdefiniowanej kolejności ładowania.

Najlepsze rozwiązania dotyczące synchronizacji

Rozważ bibliotekę DLL, która tworzy wątki robocze jako część procesu inicjalizacji. Po oczyszczeniu biblioteki DLL należy zsynchronizować się ze wszystkimi wątkami roboczymi, aby upewnić się, że struktury danych są w stanie spójnym, a następnie zakończyć wątki robocze. Obecnie nie ma prostego sposobu całkowitego rozwiązania problemu czystego synchronizowania i zamykania bibliotek DLL w środowisku wielowątkowym. W tej sekcji opisano bieżące najlepsze rozwiązania dotyczące synchronizowania wątków podczas zamykania biblioteki DLL.

Synchronizacja wątków w DllMain podczas zamykania procesu

  • W momencie, gdy DllMain jest wywoływana przy zakończeniu procesu, wszystkie wątki procesu zostały przymusowo wyczyszczone i istnieje możliwość niespójności przestrzeni adresowej. Synchronizacja nie jest wymagana w tym przypadku. Idealnie, procedura obsługi DLL_PROCESS_DETACH powinna być pusta.
  • System Windows Vista zapewnia, że podstawowe struktury danych (zmienne środowiskowe, bieżący katalog, sterta procesów itd.) są w stanie spójnym. Jednak inne struktury danych mogą być uszkodzone, więc czyszczenie pamięci nie jest bezpieczne.
  • Stan trwały, który należy zapisać, musi zostać przeniesiony do pamięci trwałej.

Synchronizacja wątków w DllMain dla DLL_THREAD_DETACH podczas zwalniania biblioteki DLL

  • Gdy biblioteka DLL zostanie zwolniona, przestrzeń adresowa nie zostanie wyrzucona. W związku z tym oczekuje się, że biblioteka DLL powinna wykonać czyste zamknięcie. Obejmuje to synchronizację wątków, otwarte dojścia, stan trwały i przydzielone zasoby.
  • Synchronizacja wątków jest trudna, ponieważ oczekiwanie na zakończenie wątków w DllMain może spowodować zakleszczenie. Na przykład biblioteka DLL A przechowuje blokadę modułu ładującego. Sygnalizuje wątkowi T, aby zakończył działanie, i czeka, aż wątek zakończy działanie. Wątek T kończy działanie, a moduł ładujący próbuje uzyskać blokadę modułu ładującego w celu wywołania dllmain biblioteki DLL z DLL_THREAD_DETACH. Powoduje to zakleszczenie. Aby zminimalizować ryzyko zakleszczenia:
    • BIBLIOTEKA DLL A otrzymuje komunikat DLL_THREAD_DETACH w DllMain i ustawia zdarzenie dla wątku T, sygnalizując mu zakończenie.
    • Wątek T kończy bieżące zadanie, przenosi się do spójnego stanu, sygnalizuje bibliotekę DLL A i czeka nieskończonie. Należy pamiętać, że procedury sprawdzania spójności powinny przestrzegać tych samych ograniczeń, co DllMain, aby uniknąć zakleszczenia.
    • DLL A kończy proces/stos, wiedząc, że jest w stanie spójnym.

Jeśli biblioteka DLL zostanie zwolniona po utworzeniu wszystkich jej wątków, ale przed rozpoczęciem ich wykonywania, wątki mogą ulec awarii. Jeśli biblioteka DLL utworzyła wątki w DllMain w ramach inicjowania, niektóre wątki mogą nie zakończyć inicjowania, a komunikat DLL_THREAD_ATTACH nadal czeka na dostarczenie do biblioteki DLL. W takiej sytuacji, jeśli biblioteka DLL zostanie zwolniona, rozpocznie kończenie wątków. Jednak niektóre wątki mogą być zablokowane za blokadą modułu ładującego. Ich komunikaty DLL_THREAD_ATTACH są przetwarzane po rozpakowanym pliku DLL, co powoduje awarię procesu.

Zalecenia

Zalecane są następujące wskazówki:

  • Użyj weryfikatora aplikacji, aby przechwycić najczęstsze błędy w DllMain.
  • Jeśli używasz prywatnej blokady wewnątrz DllMain, zdefiniuj hierarchię blokowania i użyj jej spójnie. Blokada modułu ładującego musi znajdować się w dolnej części tej hierarchii.
  • Sprawdź, czy żadne wywołania nie zależą od innej biblioteki DLL, która mogła nie zostać jeszcze w pełni załadowana.
  • Wykonaj proste inicjacje statycznie w czasie kompilacji, a nie w DllMain.
  • Odrocz wszelkie wywołania w DllMain, które mogą poczekać później.
  • Odrocz inicjowanie zadań, które mogą czekać na później. Niektóre warunki błędów muszą być wykrywane wcześnie, aby aplikacja mogła bezpiecznie obsługiwać błędy. Istnieją jednak kompromisy między tym wczesnym wykrywaniem a utratą niezawodności, która może wynikać z niego. Opóźnienie inicjalizacji często jest najlepszym rozwiązaniem.